Compare commits
431 commits
Author | SHA1 | Date | |
---|---|---|---|
83d10e1a70 | |||
23c275945a | |||
70f90dd456 | |||
cd5c97afd3 | |||
97f3848b66 | |||
540bd17792 | |||
1fe9a42d8d | |||
cf978cd749 | |||
126bb29ae2 | |||
7a58287b34 | |||
328c9badb4 | |||
1dfa17abca | |||
f2025b801b | |||
4e7b911fe9 | |||
cf6e65cc03 | |||
70e5b9bec7 | |||
84383ecd31 | |||
1d59d57c2e | |||
43447ac47b | |||
d7da42b626 | |||
d80e8164c8 | |||
37a8e4bba8 | |||
9abe4a6110 | |||
d77f2302ba | |||
dd04921e6e | |||
|
71393add2f | ||
85aae94496 | |||
|
4ca14542d7 | ||
65ca2d3d6c | |||
|
3a47f3b33c | ||
a93219f4bb | |||
|
f384e16ef1 | ||
520892f789 | |||
|
290445a4e1 | ||
e1f6c9b1e3 | |||
|
8a7f320b3a | ||
9ade631b82 | |||
|
43e9fa3530 | ||
370f33fcbf | |||
|
4250b946f4 | ||
e2b8b3ba1a | |||
|
ed8903050b | ||
6c9ee8bb7d | |||
|
207ff9fda3 | ||
c5e826a3e2 | |||
|
ea2fca89ed | ||
17a6f15175 | |||
|
3dd44d23cc | ||
bfaec2f3a5 | |||
|
2d04d68484 | ||
fe9ae25a60 | |||
|
bd09e5a65f | ||
9a883e05bb | |||
|
01713b7d79 | ||
d6a0b1dcf6 | |||
bcf61bb6a0 | |||
baee7ade4f | |||
55afa8f01d | |||
8e077045d2 | |||
1475ecdde5 | |||
a2c0fb2298 | |||
7b93692ba5 | |||
|
51c3ba8ecf | ||
19f2803590 | |||
|
be95bd6468 | ||
96f340ddec | |||
|
8792bee249 | ||
d628edf213 | |||
|
05769c410c | ||
024cc73c9d | |||
|
c25c1672f1 | ||
47b8208bea | |||
|
ed8d232ce4 | ||
1fa4d71714 | |||
|
8baac038de | ||
49078156f1 | |||
|
929f26fe76 | ||
e0f82643e0 | |||
|
f8063d1bca | ||
afee38c04b | |||
3cf11b0d72 | |||
06c5d811be | |||
|
411dc19c5a | ||
179938d97c | |||
|
0e68fffc16 | ||
6a575f9058 | |||
|
0eea92930b | ||
e8a9edc2bb | |||
|
74db2cf3e1 | ||
0fee4eedfa | |||
|
5481bab429 | ||
55a1444ba8 | |||
|
145c388b65 | ||
cabc9fef84 | |||
1521130f55 | |||
de3661ab77 | |||
5d8929ac29 | |||
1cd4e54b44 | |||
3e980b9a6a | |||
427a63af0f | |||
|
466378433b | ||
bf94de46ab | |||
|
92d66f733e | ||
cb32b30663 | |||
|
a49ee852ee | ||
84940844b0 | |||
|
0991c53dd6 | ||
f7c9c044e0 | |||
|
9348b7b0ce | ||
527bfb31d6 | |||
|
f7f47a22f0 | ||
0066311bde | |||
|
a015ba2775 | ||
f456199c48 | |||
|
78712aef2f | ||
bb980eec47 | |||
|
4749b166bf | ||
f51e2610f4 | |||
|
ae0a8290f7 | ||
6afa01b58d | |||
|
0ae30c18f0 | ||
0b864a3286 | |||
|
5a6db7dee4 | ||
0e76d42743 | |||
|
39770f6f7c | ||
df0a388da6 | |||
|
6943197ec5 | ||
9855c5ce04 | |||
8432934643 | |||
5a52fda97d | |||
|
ba13e3e4ca | ||
ca6eefd0a9 | |||
|
9496648221 | ||
c40bbf3a53 | |||
|
548f156ad0 | ||
42f69bd168 | |||
|
333412c810 | ||
699096c008 | |||
|
6de8f25a31 | ||
255ff5227f | |||
|
e84d1018cc | ||
72b0a4f133 | |||
|
4a8e2dd0fc | ||
a440533a76 | |||
d98a66f42b | |||
58b31bb4c1 | |||
7f70f75d05 | |||
5b2bfd5270 | |||
7ad5d56f1b | |||
|
7f95842308 | ||
25fceca2ff | |||
|
dd862be3a9 | ||
1b08703cd9 | |||
|
1475e8f859 | ||
e72d9ca231 | |||
|
1db7cde639 | ||
76061e10e3 | |||
|
936acc6196 | ||
13e2f8ade1 | |||
|
7d47ba6edf | ||
71f35e6c2e | |||
|
8d053195d1 | ||
5c420584c0 | |||
|
4840120971 | ||
c26e528d9f | |||
|
9a2364563a | ||
5749bf2e69 | |||
|
32ad6d7d88 | ||
a200c9d43e | |||
1f9a8fee99 | |||
cbbe87e23c | |||
5d6d611707 | |||
|
16ce12b5df | ||
d172a1ace8 | |||
|
b0ce4efbd8 | ||
8143f7a1a6 | |||
|
db670de2f0 | ||
1429563b74 | |||
|
38480ddaad | ||
3bb33cdec7 | |||
7f8553127d | |||
8726fc329c | |||
ed2b3c99da | |||
db8f885092 | |||
174cd4db33 | |||
e95186e1fd | |||
03b8c1677c | |||
cf62d756af | |||
dfe74e7538 | |||
47b39edaeb | |||
21e2b5217e | |||
d877061077 | |||
1cb1eac8fe | |||
12d3da5fdb | |||
8b276938e9 | |||
|
5cd3b63b73 | ||
a910a52f83 | |||
|
fdcd159bec | ||
b7b4082ca3 | |||
|
3d48d54834 | ||
d61dc7d784 | |||
|
2d0c01d399 | ||
f6d87fe745 | |||
be06fd66f8 | |||
7660b1c731 | |||
c78aea2581 | |||
|
d6e35856d1 | ||
1e7a588157 | |||
|
9666daf068 | ||
5c7557b2d3 | |||
|
af7f2fb800 | ||
965c69748f | |||
|
508f92dd17 | ||
80184002b7 | |||
|
ec04fefbb0 | ||
a3f1016709 | |||
|
6ce729633c | ||
908cc66c99 | |||
|
d040160681 | ||
b16232b258 | |||
|
e4eb18a5fc | ||
be34e5fb57 | |||
|
a677d870c4 | ||
196b3eb426 | |||
|
997ad535b0 | ||
b5b900090a | |||
|
88ab482e53 | ||
f15f4c0a00 | |||
5dd3f70519 | |||
|
4312ef0e2d | ||
4b8adf3e03 | |||
16fe8c5366 | |||
|
f7b53250d5 | ||
|
679d9fcac7 | ||
3aaf48acf4 | |||
|
59274bab3e | ||
33e5855140 | |||
659bc57e24 | |||
acbb58c294 | |||
|
f629929162 | ||
68a06e0ec0 | |||
|
7d2eed3ef7 | ||
9a469c27eb | |||
|
744c983c86 | ||
01943ce478 | |||
|
68b6cb5c0e | ||
2e26924808 | |||
|
2e9ad7d2e5 | ||
88e986c883 | |||
|
1b036b1a73 | ||
0bf81d307a | |||
41ea09cf5d | |||
396b0dccf1 | |||
f770235e86 | |||
7efb474fb8 | |||
811f968fb9 | |||
7e6a047bc8 | |||
8875662434 | |||
|
19172a8ab3 | ||
22d8e6407e | |||
|
78db64f1dc | ||
433ed909c1 | |||
|
4ef0a4f6f1 | ||
0b02f156ea | |||
|
253561701d | ||
66de08f0bc | |||
|
816e9a984b | ||
978fa84acf | |||
|
8b9566e540 | ||
c5b7dbf829 | |||
4f5f4132d6 | |||
3d74174bab | |||
1874bc6f94 | |||
5fa788b362 | |||
35c54d05c1 | |||
|
caaaae0265 | ||
615c88c7cb | |||
|
be28421eb2 | ||
a8f9324414 | |||
|
e1b8d96636 | ||
07cd2a9eee | |||
|
6e48893e5b | ||
45846bec5d | |||
|
564f477352 | ||
2a4f50ae7d | |||
|
0c13bf8b91 | ||
b78f0343d4 | |||
|
0c92c1ff8e | ||
71990a0a6e | |||
|
eb89271001 | ||
eff5ad6cbb | |||
|
46c5702e5e | ||
100885cea4 | |||
|
32b2c84106 | ||
00a7d23925 | |||
|
fa2d75f53e | ||
ae2c960e05 | |||
|
9940c62a07 | ||
e5184a4695 | |||
|
c673cd6399 | ||
146ec9f50e | |||
|
d70df0a435 | ||
05ddf27b8c | |||
|
6944f7f330 | ||
6e3b702ed5 | |||
|
40745c7713 | ||
ff3ed3102d | |||
|
f1758a4d00 | ||
b305062d46 | |||
|
a548a91291 | ||
a3c8257f60 | |||
|
78128af30e | ||
9be807d02a | |||
|
a54dd4ac06 | ||
d7ed220ee7 | |||
|
7eb2687d6d | ||
d4f06349ef | |||
|
37bdd29a89 | ||
83f122171a | |||
|
116a336cb1 | ||
5c0300a5c9 | |||
|
68cbc72671 | ||
b7f8a71da5 | |||
|
34f4dba696 | ||
95d727f14f | |||
|
34bd2bbf97 | ||
b46fc7b65d | |||
|
5c21934c39 | ||
78b3913249 | |||
|
e9dbf346e4 | ||
a8ec0df6fb | |||
|
ad4d1d7c4d | ||
76ebc1b4f7 | |||
|
409c1ae983 | ||
5ae455487a | |||
|
7b8625f1f9 | ||
5751c54809 | |||
|
ea35e57d22 | ||
5852c52c7a | |||
|
df5d10b42a | ||
cfe442983a | |||
|
c23c05a0fc | ||
6a50d4e719 | |||
|
a2f1fd5d71 | ||
777d70499a | |||
|
b9416fcde8 | ||
0367603d42 | |||
|
9984f38474 | ||
e6687e9263 | |||
|
6ba0849efb | ||
1dc64ca8f2 | |||
|
4861e6b162 | ||
9c99a05577 | |||
|
81b31cf619 | ||
ca0287ad8b | |||
|
0a760f4500 | ||
73fb3a2189 | |||
|
fa75a462ff | ||
ed529fdf8a | |||
|
cf90ae6f77 | ||
2428bf217a | |||
|
fea5319e8e | ||
1b0f323f1e | |||
|
823f539a9d | ||
d3c2a05a33 | |||
|
1590314afc | ||
64a8d0a53a | |||
|
962eae31f0 | ||
a0645144f5 | |||
|
13176ec88d | ||
055a715181 | |||
|
132c48a9dc | ||
1b56b0fb31 | |||
|
78dd40297e | ||
8c3d6d0053 | |||
|
bb01fac0f4 | ||
dd0079edb4 | |||
|
8486725212 | ||
424300ec12 | |||
|
f95738b7d1 | ||
f160538c13 | |||
|
74c175e80d | ||
bc854d5522 | |||
|
2481c05e44 | ||
e42e432bd5 | |||
|
c78d563b63 | ||
2e74adcb1d | |||
|
fa65db433c | ||
7a8162821b | |||
|
660b436e00 | ||
c404c218c1 | |||
|
a46263285e | ||
8e811b331f | |||
|
4fadc3cc7f | ||
4ec28451aa | |||
|
82ec2f59fc | ||
f7e7b65173 | |||
|
dffd55ffbc | ||
51655e980b | |||
|
715c18107a | ||
08bea213b3 | |||
|
cc083bc09b | ||
7e6bdf1ac4 | |||
|
fd41486418 | ||
2c4dba225e | |||
|
6999978779 | ||
ca0ae1a387 | |||
|
8e29e3c498 | ||
7f168d16d2 | |||
|
e7c85e9456 | ||
a216c194d4 | |||
ec17f65107 | |||
1671323012 | |||
|
3d4585df5d | ||
306d19781e | |||
|
e4137d8718 | ||
f32b0068c6 | |||
f51c37103c | |||
2aa60db548 | |||
24ca9f86d2 | |||
ba9330cdf1 | |||
d264b8bdc3 | |||
|
49c407aade | ||
0cbc1d3b7b | |||
|
9bcec3df7c | ||
133abd2935 | |||
|
aaf5a2aed2 | ||
30d58e9e15 | |||
|
24f4571a3d | ||
e9f9f527a5 | |||
|
2faa92b642 |
266 changed files with 8866 additions and 19855 deletions
|
@ -1,14 +0,0 @@
|
||||||
APP_ENV=testing
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_KEY=base64:6DJhvZLVjE6dD4Cqrteh+6Z5vZlG+v/soCKcDHLOAH0=
|
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
APP_LONGURL=localhost
|
|
||||||
APP_SHORTURL=local
|
|
||||||
|
|
||||||
DB_CONNECTION=travis
|
|
||||||
|
|
||||||
CACHE_DRIVER=array
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
QUEUE_DRIVER=sync
|
|
||||||
|
|
||||||
SCOUT_DRIVER=pgsql
|
|
67
.env.example
67
.env.example
|
@ -2,11 +2,22 @@ APP_NAME=Laravel
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
APP_URL=https://example.com
|
APP_URL=https://example.com
|
||||||
APP_LONGURL=example.com
|
|
||||||
APP_SHORTURL=examp.le
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
@ -17,22 +28,30 @@ DB_DATABASE=laravel
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
SESSION_DRIVER=database
|
||||||
CACHE_DRIVER=file
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
MAIL_MAILER=log
|
||||||
MAIL_HOST=mailhog
|
MAIL_SCHEME=null
|
||||||
MAIL_PORT=1025
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
@ -45,19 +64,7 @@ AWS_DEFAULT_REGION=us-east-1
|
||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
PUSHER_HOST=
|
|
||||||
PUSHER_PORT=443
|
|
||||||
PUSHER_SCHEME=https
|
|
||||||
PUSHER_APP_CLUSTER=mt1
|
|
||||||
|
|
||||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
|
||||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
|
||||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
|
||||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
|
||||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
|
||||||
|
|
||||||
ADMIN_USER=admin# pick something better, this is used for `/admin`
|
ADMIN_USER=admin# pick something better, this is used for `/admin`
|
||||||
ADMIN_PASS=password
|
ADMIN_PASS=password
|
||||||
|
@ -71,21 +78,13 @@ TWITTER_ACCESS_TOKEN_SECRET=
|
||||||
SCOUT_DRIVER=database
|
SCOUT_DRIVER=database
|
||||||
SCOUT_QUEUE=false
|
SCOUT_QUEUE=false
|
||||||
|
|
||||||
PIWIK=false
|
|
||||||
PIWIK_ID=1
|
|
||||||
PIWIK_URL=https://analytics.jmb.lv/piwik.php
|
|
||||||
|
|
||||||
FATHOM_ID=
|
|
||||||
|
|
||||||
APP_TIMEZONE=UTC
|
|
||||||
APP_LANG=en
|
|
||||||
APP_LOG=daily
|
|
||||||
SESSION_SECURE_COOKIE=true
|
SESSION_SECURE_COOKIE=true
|
||||||
|
SESSION_SAME_SITE=strict
|
||||||
|
|
||||||
LOG_SLACK_WEBHOOK_URL=
|
LOG_SLACK_WEBHOOK_URL=
|
||||||
|
|
||||||
FLARE_KEY=
|
FLARE_KEY=
|
||||||
|
|
||||||
IGNITION_OPEN_AI_KEY=
|
IGNITION_OPEN_AI_KEY=
|
||||||
|
|
||||||
FONT_LINK=
|
|
||||||
|
|
||||||
BRIDGY_MASTODON_TOKEN=
|
BRIDGY_MASTODON_TOKEN=
|
||||||
|
|
70
.env.github
70
.env.github
|
@ -1,70 +0,0 @@
|
||||||
APP_NAME=Laravel
|
|
||||||
APP_ENV=testing
|
|
||||||
APP_KEY=SomeRandomString # Leave this
|
|
||||||
APP_DEBUG=false
|
|
||||||
APP_LOG_LEVEL=warning
|
|
||||||
|
|
||||||
DB_CONNECTION=pgsql
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_DATABASE=jbukdev_testing
|
|
||||||
DB_USERNAME=postgres
|
|
||||||
DB_PASSWORD=postgres
|
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
|
||||||
CACHE_DRIVER=file
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
QUEUE_DRIVER=sync
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
MAIL_DRIVER=smtp
|
|
||||||
MAIL_HOST=smtp.mailtrap.io
|
|
||||||
MAIL_PORT=2525
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_ENCRYPTION=null
|
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
|
|
||||||
AWS_S3_KEY=your-key
|
|
||||||
AWS_S3_SECRET=your-secret
|
|
||||||
AWS_S3_REGION=region
|
|
||||||
AWS_S3_BUCKET=your-bucket
|
|
||||||
AWS_S3_URL=https://xxxxxxx.s3-region.amazonaws.com
|
|
||||||
|
|
||||||
APP_URL=https://example.com # This one is necessary
|
|
||||||
APP_LONGURL=example.com
|
|
||||||
APP_SHORTURL=examp.le
|
|
||||||
|
|
||||||
ADMIN_USER=admin # pick something better, this is used for `/admin`
|
|
||||||
ADMIN_PASS=password
|
|
||||||
DISPLAY_NAME="Joe Bloggs" # This is used for example in the header and titles
|
|
||||||
|
|
||||||
TWITTER_CONSUMER_KEY=
|
|
||||||
TWITTER_CONSUMER_SECRET=
|
|
||||||
TWITTER_ACCESS_TOKEN=
|
|
||||||
TWITTER_ACCESS_TOKEN_SECRET=
|
|
||||||
|
|
||||||
SCOUT_DRIVER=database
|
|
||||||
SCOUT_QUEUE=false
|
|
||||||
|
|
||||||
PIWIK=false
|
|
||||||
|
|
||||||
FATHOM_ID=
|
|
||||||
|
|
||||||
APP_TIMEZONE=UTC
|
|
||||||
APP_LANG=en
|
|
||||||
APP_LOG=daily
|
|
||||||
SECURE_SESSION_COOKIE=true
|
|
||||||
|
|
||||||
LOG_SLACK_WEBHOOK_URL=
|
|
||||||
FLARE_KEY=
|
|
||||||
|
|
||||||
FONT_LINK=
|
|
||||||
|
|
||||||
BRIDGY_MASTODON_TOKEN=
|
|
|
@ -1,38 +0,0 @@
|
||||||
parserOptions:
|
|
||||||
sourceType: 'module'
|
|
||||||
ecmaVersion: 'latest'
|
|
||||||
extends: 'eslint:recommended'
|
|
||||||
env:
|
|
||||||
browser: true
|
|
||||||
es6: true
|
|
||||||
ignorePatterns:
|
|
||||||
- webpack.config.js
|
|
||||||
rules:
|
|
||||||
indent:
|
|
||||||
- error
|
|
||||||
- 2
|
|
||||||
linebreak-style:
|
|
||||||
- error
|
|
||||||
- unix
|
|
||||||
quotes:
|
|
||||||
- error
|
|
||||||
- single
|
|
||||||
semi:
|
|
||||||
- error
|
|
||||||
- always
|
|
||||||
no-console:
|
|
||||||
- error
|
|
||||||
- allow:
|
|
||||||
- warn
|
|
||||||
- error
|
|
||||||
no-await-in-loop:
|
|
||||||
- error
|
|
||||||
no-promise-executor-return:
|
|
||||||
- error
|
|
||||||
require-atomic-updates:
|
|
||||||
- error
|
|
||||||
max-nested-callbacks:
|
|
||||||
- error
|
|
||||||
- 3
|
|
||||||
prefer-promise-reject-errors:
|
|
||||||
- error
|
|
4
.gitattributes
vendored
4
.gitattributes
vendored
|
@ -5,7 +5,3 @@
|
||||||
*.html diff=html
|
*.html diff=html
|
||||||
*.md diff=markdown
|
*.md diff=markdown
|
||||||
*.php diff=php
|
*.php diff=php
|
||||||
|
|
||||||
/.github export-ignore
|
|
||||||
CHANGELOG.md export-ignore
|
|
||||||
.styleci.yml export-ignore
|
|
||||||
|
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
|
@ -1,12 +0,0 @@
|
||||||
version: 2
|
|
||||||
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "composer"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
144
.github/workflows/deploy.yml
vendored
144
.github/workflows/deploy.yml
vendored
|
@ -1,144 +0,0 @@
|
||||||
name: Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: Deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: Hetzner
|
|
||||||
env:
|
|
||||||
repository: 'jonnybarnes/jonnybarnes.uk'
|
|
||||||
newReleaseName: '${{ github.run_id }}'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 🌍 Set Environment Variables
|
|
||||||
run: |
|
|
||||||
echo "releasesDir=${{ secrets.DEPLOYMENT_BASE_DIR }}/releases" >> $GITHUB_ENV
|
|
||||||
echo "persistentDir=${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent" >> $GITHUB_ENV
|
|
||||||
echo "currentDir=${{ secrets.DEPLOYMENT_BASE_DIR }}/current" >> $GITHUB_ENV
|
|
||||||
- name: 🌎 Set Environment Variables Part 2
|
|
||||||
run: |
|
|
||||||
echo "newReleaseDir=${{ env.releasesDir }}/${{ env.newReleaseName }}" >> $GITHUB_ENV
|
|
||||||
- name: 🔄 Clone Repository
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
[ -d ${{ env.releasesDir }} ] || mkdir ${{ env.releasesDir }}
|
|
||||||
[ -d ${{ env.persistentDir }} ] || mkdir ${{ env.persistentDir }}
|
|
||||||
[ -d ${{ env.persistentDir }}/storage ] || mkdir ${{ env.persistentDir }}/storage
|
|
||||||
|
|
||||||
cd ${{ env.releasesDir }}
|
|
||||||
|
|
||||||
# Create new release directory
|
|
||||||
mkdir ${{ env.newReleaseDir }}
|
|
||||||
|
|
||||||
# Clone app
|
|
||||||
git clone --depth 1 --branch ${{ github.ref_name }} https://github.com/${{ env.repository }} ${{ env.newReleaseName }}
|
|
||||||
|
|
||||||
# Mark release
|
|
||||||
cd ${{ env.newReleaseDir }}
|
|
||||||
echo "${{ env.newReleaseName }}" > public/release-name.txt
|
|
||||||
|
|
||||||
# Fix cache directory permissions
|
|
||||||
sudo chown -R ${{ secrets.HTTP_USER }}:${{ secrets.HTTP_USER }} bootstrap/cache
|
|
||||||
|
|
||||||
- name: 🎵 Run Composer
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
cd ${{ env.newReleaseDir }}
|
|
||||||
composer install --prefer-dist --no-scripts --no-dev --no-progress --optimize-autoloader --quiet --no-interaction
|
|
||||||
|
|
||||||
- name: 🔗 Update Symlinks
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
# Import the environment config
|
|
||||||
cd ${{ env.newReleaseDir }};
|
|
||||||
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/.env .env;
|
|
||||||
|
|
||||||
# Remove the storage directory and replace with persistent data
|
|
||||||
rm -rf ${{ env.newReleaseDir }}/storage;
|
|
||||||
cd ${{ env.newReleaseDir }};
|
|
||||||
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/storage storage;
|
|
||||||
|
|
||||||
# Remove the public/profile-images directory and replace with persistent data
|
|
||||||
rm -rf ${{ env.newReleaseDir }}/public/assets/profile-images;
|
|
||||||
cd ${{ env.newReleaseDir }};
|
|
||||||
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/profile-images public/assets/profile-images;
|
|
||||||
|
|
||||||
# Add the persistent files data
|
|
||||||
cd ${{ env.newReleaseDir }};
|
|
||||||
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/files public/files;
|
|
||||||
|
|
||||||
# Add the persistent fonts data
|
|
||||||
cd ${{ env.newReleaseDir }};
|
|
||||||
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/fonts public/fonts;
|
|
||||||
|
|
||||||
- name: ✨ Optimize Installation
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
cd ${{ env.newReleaseDir }};
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan clear-compiled;
|
|
||||||
|
|
||||||
- name: 🙈 Migrate database
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
cd ${{ env.newReleaseDir }}
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan migrate --force
|
|
||||||
|
|
||||||
- name: 🙏 Bless release
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
ln -nfs ${{ env.newReleaseDir }} ${{ env.currentDir }};
|
|
||||||
cd ${{ env.newReleaseDir }}
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan horizon:terminate
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan config:cache
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan event:cache
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan route:cache
|
|
||||||
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan view:cache
|
|
||||||
|
|
||||||
sudo systemctl restart php-fpm.service
|
|
||||||
sudo systemctl restart jbuk-horizon.service
|
|
||||||
|
|
||||||
- name: 🚾 Clean up old releases
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOYMENT_HOST }}
|
|
||||||
port: ${{ secrets.DEPLOYMENT_PORT }}
|
|
||||||
username: ${{ secrets.DEPLOYMENT_USER }}
|
|
||||||
key: ${{ secrets.DEPLOYMENT_KEY }}
|
|
||||||
script: |
|
|
||||||
fd '.+' ${{ env.releasesDir }} -d 1 | head -n -3 | xargs -d "\n" -I'{}' sudo chown -R ${{ secrets.DEPLOYMENT_USER }}:${{ secrets.DEPLOYMENT_USER }} {}
|
|
||||||
fd '.+' ${{ env.releasesDir }} -d 1 | head -n -3 | xargs -d "\n" -I'{}' rm -rf {}
|
|
71
.github/workflows/phpunit.yml
vendored
71
.github/workflows/phpunit.yml
vendored
|
@ -1,71 +0,0 @@
|
||||||
name: PHP Unit
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
phpunit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
name: PHPUnit test suite
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
operating-system: ['ubuntu-latest']
|
|
||||||
php-versions: ['8.2', '8.3']
|
|
||||||
phpunit-versions: ['latest']
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:latest
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: jbukdev_testing
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php-versions }}
|
|
||||||
extensions: mbstring, intl, phpredis, imagick
|
|
||||||
coverage: xdebug
|
|
||||||
tools: phpunit:${{ matrix.phpunit-versions }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Copy .env
|
|
||||||
run: php -r "file_exists('.env') || copy('.env.github', '.env');"
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
|
||||||
id: composer-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
|
||||||
|
|
||||||
- name: Cache composer dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-php-${{ matrix.php-version }}-composer-
|
|
||||||
|
|
||||||
- name: Install Composer Dependencies
|
|
||||||
run: composer install --quiet --no-ansi --no-interaction --no-progress
|
|
||||||
|
|
||||||
- name: Generate Key
|
|
||||||
run: php artisan key:generate
|
|
||||||
|
|
||||||
- name: Setup Directory Permissions
|
|
||||||
run: chmod -R 777 storage bootstrap/cache
|
|
||||||
|
|
||||||
- name: Setup Database
|
|
||||||
run: php artisan migrate
|
|
||||||
|
|
||||||
- name: Execute PHPUnit Tests
|
|
||||||
run: vendor/bin/phpunit
|
|
38
.github/workflows/pint.yml
vendored
38
.github/workflows/pint.yml
vendored
|
@ -1,38 +0,0 @@
|
||||||
name: Laravel Pint
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
name: Laravel Pint
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup PHP with pecl extensions
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.2'
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
|
||||||
id: composer-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
|
||||||
|
|
||||||
- name: Cache composer dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-composer-
|
|
||||||
|
|
||||||
- name: Install Composer Dependencies
|
|
||||||
run: composer install --quiet --no-ansi --no-interaction --no-progress
|
|
||||||
|
|
||||||
- name: Check Files with Laravel Pint
|
|
||||||
run: vendor/bin/pint --test
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,7 +4,6 @@
|
||||||
/public/coverage
|
/public/coverage
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/files
|
/public/files
|
||||||
/public/fonts
|
|
||||||
/public/storage
|
/public/storage
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/vendor
|
/vendor
|
||||||
|
@ -21,3 +20,5 @@ yarn-error.log
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
ray.php
|
ray.php
|
||||||
|
/public/gpg.key
|
||||||
|
/public/assets/img/favicon.png
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
php:
|
|
||||||
preset: laravel
|
|
||||||
disabled:
|
|
||||||
- no_unused_imports
|
|
||||||
finder:
|
|
||||||
not-name:
|
|
||||||
- index.php
|
|
||||||
js: true
|
|
||||||
css: true
|
|
69
app/Console/Commands/CopyMediaToLocal.php
Normal file
69
app/Console/Commands/CopyMediaToLocal.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Media;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class CopyMediaToLocal extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:copy-media-to-local';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Copy any historic media saved to S3 to the local filesystem';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// Load all the Media records
|
||||||
|
$media = Media::all();
|
||||||
|
|
||||||
|
// Loop through each media record and copy the file from S3 to the local filesystem
|
||||||
|
foreach ($media as $mediaItem) {
|
||||||
|
$filename = $mediaItem->path;
|
||||||
|
|
||||||
|
$this->info('Processing: ' . $filename);
|
||||||
|
|
||||||
|
// If the file is already saved locally skip to next one
|
||||||
|
if (Storage::disk('local')->exists('public/' . $filename)) {
|
||||||
|
$this->info('File already exists locally, skipping');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file from S3 to the local filesystem
|
||||||
|
if (! Storage::disk('s3')->exists($filename)) {
|
||||||
|
$this->error('File does not exist on S3');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$contents = Storage::disk('s3')->get($filename);
|
||||||
|
Storage::disk('local')->put('public/' . $filename, $contents);
|
||||||
|
|
||||||
|
// Copy -medium and -small versions if they exist
|
||||||
|
$filenameParts = explode('.', $filename);
|
||||||
|
$extension = array_pop($filenameParts);
|
||||||
|
$basename = trim(implode('.', $filenameParts), '.');
|
||||||
|
$mediumFilename = $basename . '-medium.' . $extension;
|
||||||
|
$smallFilename = $basename . '-small.' . $extension;
|
||||||
|
if (Storage::disk('s3')->exists($mediumFilename)) {
|
||||||
|
Storage::disk('local')->put('public/' . $mediumFilename, Storage::disk('s3')->get($mediumFilename));
|
||||||
|
}
|
||||||
|
if (Storage::disk('s3')->exists($smallFilename)) {
|
||||||
|
Storage::disk('local')->put('public/' . $smallFilename, Storage::disk('s3')->get($smallFilename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,6 @@ use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @codeCoverageIgnore
|
* @codeCoverageIgnore
|
||||||
*
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
*/
|
||||||
class MigratePlaceDataFromPostgis extends Command
|
class MigratePlaceDataFromPostgis extends Command
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,9 +9,6 @@ use Illuminate\Console\Command;
|
||||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||||
use Illuminate\FileSystem\FileSystem;
|
use Illuminate\FileSystem\FileSystem;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ParseCachedWebMentions extends Command
|
class ParseCachedWebMentions extends Command
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -37,7 +34,7 @@ class ParseCachedWebMentions extends Command
|
||||||
{
|
{
|
||||||
$htmlFiles = $filesystem->allFiles(storage_path() . '/HTML');
|
$htmlFiles = $filesystem->allFiles(storage_path() . '/HTML');
|
||||||
foreach ($htmlFiles as $file) {
|
foreach ($htmlFiles as $file) {
|
||||||
if ($file->getExtension() !== 'backup') { //we don’t want to parse `.backup` files
|
if ($file->getExtension() !== 'backup') { // we don’t want to parse `.backup` files
|
||||||
$filepath = $file->getPathname();
|
$filepath = $file->getPathname();
|
||||||
$this->info('Loading HTML from: ' . $filepath);
|
$this->info('Loading HTML from: ' . $filepath);
|
||||||
$html = $filesystem->get($filepath);
|
$html = $filesystem->get($filepath);
|
||||||
|
|
|
@ -8,9 +8,6 @@ use App\Jobs\DownloadWebMention;
|
||||||
use App\Models\WebMention;
|
use App\Models\WebMention;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ReDownloadWebMentions extends Command
|
class ReDownloadWebMentions extends Command
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
36
app/Console/Commands/UpdateWebmentionsRelationship.php
Normal file
36
app/Console/Commands/UpdateWebmentionsRelationship.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Note;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class UpdateWebmentionsRelationship extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'webmentions:update-model-relationship';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Update webmentions to relate to the correct note model class';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
DB::table('webmentions')
|
||||||
|
->where('commentable_type', '=', 'App\Model\Note')
|
||||||
|
->update(['commentable_type' => Note::class]);
|
||||||
|
|
||||||
|
$this->info('All webmentions updated to relate to the correct note model class');
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,4 @@
|
||||||
|
|
||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
class InternetArchiveException extends \Exception
|
class InternetArchiveException extends \Exception {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
7
app/Exceptions/InvalidTokenScopeException.php
Normal file
7
app/Exceptions/InvalidTokenScopeException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
class InvalidTokenScopeException extends \Exception {}
|
7
app/Exceptions/MicropubHandlerException.php
Normal file
7
app/Exceptions/MicropubHandlerException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
class MicropubHandlerException extends \Exception {}
|
|
@ -6,5 +6,5 @@ use Exception;
|
||||||
|
|
||||||
class RemoteContentNotFoundException extends Exception
|
class RemoteContentNotFoundException extends Exception
|
||||||
{
|
{
|
||||||
//used when guzzle can’t find the remote content
|
// used when guzzle can’t find the remote content
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,6 @@ use App\Models\Article;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ArticlesController extends Controller
|
class ArticlesController extends Controller
|
||||||
{
|
{
|
||||||
public function index(): View
|
public function index(): View
|
||||||
|
@ -30,7 +27,7 @@ class ArticlesController extends Controller
|
||||||
|
|
||||||
public function store(): RedirectResponse
|
public function store(): RedirectResponse
|
||||||
{
|
{
|
||||||
//if a `.md` is attached use that for the main content.
|
// if a `.md` is attached use that for the main content.
|
||||||
if (request()->hasFile('article')) {
|
if (request()->hasFile('article')) {
|
||||||
$file = request()->file('article')->openFile();
|
$file = request()->file('article')->openFile();
|
||||||
$content = $file->fread($file->getSize());
|
$content = $file->fread($file->getSize());
|
||||||
|
|
|
@ -10,9 +10,6 @@ use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class BioController extends Controller
|
class BioController extends Controller
|
||||||
{
|
{
|
||||||
public function show(): View
|
public function show(): View
|
||||||
|
|
|
@ -9,9 +9,6 @@ use App\Models\MicropubClient;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ClientsController extends Controller
|
class ClientsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,9 +12,6 @@ use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ContactsController extends Controller
|
class ContactsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -40,7 +37,7 @@ class ContactsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function store(): RedirectResponse
|
public function store(): RedirectResponse
|
||||||
{
|
{
|
||||||
$contact = new Contact();
|
$contact = new Contact;
|
||||||
$contact->name = request()->input('name');
|
$contact->name = request()->input('name');
|
||||||
$contact->nick = request()->input('nick');
|
$contact->nick = request()->input('nick');
|
||||||
$contact->homepage = request()->input('homepage');
|
$contact->homepage = request()->input('homepage');
|
||||||
|
@ -79,7 +76,7 @@ class ContactsController extends Controller
|
||||||
if (request()->hasFile('avatar') && (request()->input('homepage') != '')) {
|
if (request()->hasFile('avatar') && (request()->input('homepage') != '')) {
|
||||||
$dir = parse_url(request()->input('homepage'), PHP_URL_HOST);
|
$dir = parse_url(request()->input('homepage'), PHP_URL_HOST);
|
||||||
$destination = public_path() . '/assets/profile-images/' . $dir;
|
$destination = public_path() . '/assets/profile-images/' . $dir;
|
||||||
$filesystem = new Filesystem();
|
$filesystem = new Filesystem;
|
||||||
if ($filesystem->isDirectory($destination) === false) {
|
if ($filesystem->isDirectory($destination) === false) {
|
||||||
$filesystem->makeDirectory($destination);
|
$filesystem->makeDirectory($destination);
|
||||||
}
|
}
|
||||||
|
@ -139,7 +136,7 @@ class ContactsController extends Controller
|
||||||
}
|
}
|
||||||
if ($avatar !== null) {
|
if ($avatar !== null) {
|
||||||
$directory = public_path() . '/assets/profile-images/' . parse_url($contact->homepage, PHP_URL_HOST);
|
$directory = public_path() . '/assets/profile-images/' . parse_url($contact->homepage, PHP_URL_HOST);
|
||||||
$filesystem = new Filesystem();
|
$filesystem = new Filesystem;
|
||||||
if ($filesystem->isDirectory($directory) === false) {
|
if ($filesystem->isDirectory($directory) === false) {
|
||||||
$filesystem->makeDirectory($directory);
|
$filesystem->makeDirectory($directory);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,6 @@ namespace App\Http\Controllers\Admin;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,9 +10,6 @@ use App\Models\Like;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class LikesController extends Controller
|
class LikesController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,9 +11,6 @@ use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class NotesController extends Controller
|
class NotesController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -67,7 +64,7 @@ class NotesController extends Controller
|
||||||
*/
|
*/
|
||||||
public function update(int $noteId): RedirectResponse
|
public function update(int $noteId): RedirectResponse
|
||||||
{
|
{
|
||||||
//update note data
|
// update note data
|
||||||
$note = Note::findOrFail($noteId);
|
$note = Note::findOrFail($noteId);
|
||||||
$note->note = request()->input('content');
|
$note->note = request()->input('content');
|
||||||
$note->in_reply_to = request()->input('in-reply-to');
|
$note->in_reply_to = request()->input('in-reply-to');
|
||||||
|
|
|
@ -18,8 +18,8 @@ use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||||
|
use Random\RandomException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Webauthn\AttestationStatement\AttestationObjectLoader;
|
|
||||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||||
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
|
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
|
||||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||||
|
@ -28,18 +28,17 @@ use Webauthn\AuthenticatorAssertionResponseValidator;
|
||||||
use Webauthn\AuthenticatorAttestationResponse;
|
use Webauthn\AuthenticatorAttestationResponse;
|
||||||
use Webauthn\AuthenticatorAttestationResponseValidator;
|
use Webauthn\AuthenticatorAttestationResponseValidator;
|
||||||
use Webauthn\AuthenticatorSelectionCriteria;
|
use Webauthn\AuthenticatorSelectionCriteria;
|
||||||
|
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
|
||||||
|
use Webauthn\Denormalizer\WebauthnSerializerFactory;
|
||||||
use Webauthn\Exception\WebauthnException;
|
use Webauthn\Exception\WebauthnException;
|
||||||
|
use Webauthn\PublicKeyCredential;
|
||||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||||
use Webauthn\PublicKeyCredentialLoader;
|
|
||||||
use Webauthn\PublicKeyCredentialParameters;
|
use Webauthn\PublicKeyCredentialParameters;
|
||||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||||
use Webauthn\PublicKeyCredentialRpEntity;
|
use Webauthn\PublicKeyCredentialRpEntity;
|
||||||
use Webauthn\PublicKeyCredentialSource;
|
use Webauthn\PublicKeyCredentialSource;
|
||||||
use Webauthn\PublicKeyCredentialUserEntity;
|
use Webauthn\PublicKeyCredentialUserEntity;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class PasskeysController extends Controller
|
class PasskeysController extends Controller
|
||||||
{
|
{
|
||||||
public function index(): View
|
public function index(): View
|
||||||
|
@ -51,22 +50,26 @@ class PasskeysController extends Controller
|
||||||
return view('admin.passkeys.index', compact('passkeys'));
|
return view('admin.passkeys.index', compact('passkeys'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreateOptions(): JsonResponse
|
/**
|
||||||
|
* @throws RandomException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function getCreateOptions(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
// RP Entity i.e. the application
|
// RP Entity i.e. the application
|
||||||
$rpEntity = PublicKeyCredentialRpEntity::create(
|
$rpEntity = PublicKeyCredentialRpEntity::create(
|
||||||
config('app.name'),
|
name: config('app.name'),
|
||||||
config('url.longurl'),
|
id: config('app.url'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// User Entity
|
// User Entity
|
||||||
$userEntity = PublicKeyCredentialUserEntity::create(
|
$userEntity = PublicKeyCredentialUserEntity::create(
|
||||||
$user->name,
|
name: $user->name,
|
||||||
(string) $user->id,
|
id: (string) $user->id,
|
||||||
$user->name,
|
displayName: $user->name,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Challenge
|
// Challenge
|
||||||
|
@ -84,70 +87,100 @@ class PasskeysController extends Controller
|
||||||
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create(
|
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create(
|
||||||
userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||||
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
|
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
|
||||||
requireResidentKey: true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$options = PublicKeyCredentialCreationOptions::create(
|
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::create(
|
||||||
$rpEntity,
|
rp: $rpEntity,
|
||||||
$userEntity,
|
user: $userEntity,
|
||||||
$challenge,
|
challenge: $challenge,
|
||||||
$pubKeyCredParams,
|
pubKeyCredParams: $pubKeyCredParams,
|
||||||
authenticatorSelection: $authenticatorSelectionCriteria,
|
authenticatorSelection: $authenticatorSelectionCriteria,
|
||||||
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
|
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
|
||||||
);
|
);
|
||||||
|
|
||||||
$options = json_encode($options, JSON_THROW_ON_ERROR);
|
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||||
|
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||||
|
$webauthnSerializerFactory = new WebauthnSerializerFactory(
|
||||||
|
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||||
|
);
|
||||||
|
$webauthnSerializer = $webauthnSerializerFactory->create();
|
||||||
|
$publicKeyCredentialCreationOptions = $webauthnSerializer->serialize(
|
||||||
|
data: $publicKeyCredentialCreationOptions,
|
||||||
|
format: 'json'
|
||||||
|
);
|
||||||
|
|
||||||
session(['create_options' => $options]);
|
$request->session()->put('create_options', $publicKeyCredentialCreationOptions);
|
||||||
|
|
||||||
return JsonResponse::fromJsonString($options);
|
return JsonResponse::fromJsonString($publicKeyCredentialCreationOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Throwable
|
||||||
|
* @throws WebauthnException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
public function create(Request $request): JsonResponse
|
public function create(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
$publicKeyCredentialCreationOptionsData = session('create_options');
|
$publicKeyCredentialCreationOptionsData = session('create_options');
|
||||||
|
// Unset session data to mitigate replay attacks
|
||||||
|
$request->session()->forget('create_options');
|
||||||
if (empty($publicKeyCredentialCreationOptionsData)) {
|
if (empty($publicKeyCredentialCreationOptionsData)) {
|
||||||
throw new WebAuthnException('No public key credential request options found');
|
throw new WebAuthnException('No public key credential request options found');
|
||||||
}
|
}
|
||||||
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromString($publicKeyCredentialCreationOptionsData);
|
|
||||||
|
|
||||||
// Unset session data to mitigate replay attacks
|
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||||
session()->forget('create_options');
|
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||||
|
$webauthnSerializerFactory = new WebauthnSerializerFactory(
|
||||||
|
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||||
|
);
|
||||||
|
$webauthnSerializer = $webauthnSerializerFactory->create();
|
||||||
|
|
||||||
$attestationSupportManager = AttestationStatementSupportManager::create();
|
$publicKeyCredential = $webauthnSerializer->deserialize(
|
||||||
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
|
json_encode($request->all(), JSON_THROW_ON_ERROR),
|
||||||
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
|
PublicKeyCredential::class,
|
||||||
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
|
'json'
|
||||||
|
);
|
||||||
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
|
|
||||||
|
|
||||||
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
||||||
throw new WebAuthnException('Invalid response type');
|
throw new WebAuthnException('Invalid response type');
|
||||||
}
|
}
|
||||||
|
|
||||||
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
|
$algorithmManager = new Manager;
|
||||||
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
|
$algorithmManager->add(new Ed25519);
|
||||||
|
$algorithmManager->add(new ES256);
|
||||||
|
$algorithmManager->add(new RS256);
|
||||||
|
|
||||||
|
$ceremonyStepManagerFactory = new CeremonyStepManagerFactory;
|
||||||
|
$ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager);
|
||||||
|
$ceremonyStepManagerFactory->setAttestationStatementSupportManager(
|
||||||
|
$attestationStatementSupportManager
|
||||||
|
);
|
||||||
|
$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler(
|
||||||
|
ExtensionOutputCheckerHandler::create()
|
||||||
|
);
|
||||||
|
$allowedOrigins = [];
|
||||||
|
if (App::environment('local', 'development')) {
|
||||||
|
$allowedOrigins = [config('app.url')];
|
||||||
|
}
|
||||||
|
$ceremonyStepManagerFactory->setAllowedOrigins($allowedOrigins);
|
||||||
|
|
||||||
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
|
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
|
||||||
attestationStatementSupportManager: $attestationStatementSupportManager,
|
ceremonyStepManager: $ceremonyStepManagerFactory->creationCeremony()
|
||||||
publicKeyCredentialSourceRepository: null,
|
|
||||||
tokenBindingHandler: null,
|
|
||||||
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$securedRelyingPartyId = [];
|
$publicKeyCredentialCreationOptions = $webauthnSerializer->deserialize(
|
||||||
if (App::environment('local', 'development')) {
|
$publicKeyCredentialCreationOptionsData,
|
||||||
$securedRelyingPartyId = [config('url.longurl')];
|
PublicKeyCredentialCreationOptions::class,
|
||||||
}
|
'json'
|
||||||
|
);
|
||||||
|
|
||||||
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
|
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
|
||||||
authenticatorAttestationResponse: $publicKeyCredential->response,
|
authenticatorAttestationResponse: $publicKeyCredential->response,
|
||||||
publicKeyCredentialCreationOptions: $publicKeyCredentialCreationOptions,
|
publicKeyCredentialCreationOptions: $publicKeyCredentialCreationOptions,
|
||||||
request: config('url.longurl'),
|
host: config('app.url')
|
||||||
securedRelyingPartyId: $securedRelyingPartyId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$user->passkey()->create([
|
$user->passkey()->create([
|
||||||
|
@ -161,24 +194,37 @@ class PasskeysController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRequestOptions(): JsonResponse
|
/**
|
||||||
|
* @throws RandomException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function getRequestOptions(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::create(
|
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::create(
|
||||||
challenge: random_bytes(16),
|
challenge: random_bytes(16),
|
||||||
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED
|
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED
|
||||||
);
|
);
|
||||||
|
|
||||||
$publicKeyCredentialRequestOptions = json_encode($publicKeyCredentialRequestOptions, JSON_THROW_ON_ERROR);
|
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
|
||||||
|
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
|
||||||
|
$factory = new WebauthnSerializerFactory(
|
||||||
|
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||||
|
);
|
||||||
|
$serializer = $factory->create();
|
||||||
|
$publicKeyCredentialRequestOptions = $serializer->serialize(data: $publicKeyCredentialRequestOptions, format: 'json');
|
||||||
|
|
||||||
session(['request_options' => $publicKeyCredentialRequestOptions]);
|
$request->session()->put('request_options', $publicKeyCredentialRequestOptions);
|
||||||
|
|
||||||
return JsonResponse::fromJsonString($publicKeyCredentialRequestOptions);
|
return JsonResponse::fromJsonString($publicKeyCredentialRequestOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
public function login(Request $request): JsonResponse
|
public function login(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$requestOptions = session('request_options');
|
$requestOptions = session('request_options');
|
||||||
session()->forget('request_options');
|
$request->session()->forget('request_options');
|
||||||
|
|
||||||
if (empty($requestOptions)) {
|
if (empty($requestOptions)) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
@ -187,14 +233,19 @@ class PasskeysController extends Controller
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($requestOptions);
|
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||||
|
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||||
|
|
||||||
$attestationSupportManager = AttestationStatementSupportManager::create();
|
$webauthnSerializerFactory = new WebauthnSerializerFactory(
|
||||||
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
|
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||||
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
|
);
|
||||||
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
|
$webauthnSerializer = $webauthnSerializerFactory->create();
|
||||||
|
|
||||||
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
|
$publicKeyCredential = $webauthnSerializer->deserialize(
|
||||||
|
json_encode($request->all(), JSON_THROW_ON_ERROR),
|
||||||
|
PublicKeyCredential::class,
|
||||||
|
'json'
|
||||||
|
);
|
||||||
|
|
||||||
if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
|
if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
@ -211,33 +262,51 @@ class PasskeysController extends Controller
|
||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$credential = PublicKeyCredentialSource::createFromArray(json_decode($passkey->passkey, true, 512, JSON_THROW_ON_ERROR));
|
$publicKeyCredentialSource = $webauthnSerializer->deserialize(
|
||||||
|
$passkey->passkey,
|
||||||
$algorithmManager = Manager::create();
|
PublicKeyCredentialSource::class,
|
||||||
$algorithmManager->add(new Ed25519());
|
'json'
|
||||||
$algorithmManager->add(new ES256());
|
|
||||||
$algorithmManager->add(new RS256());
|
|
||||||
|
|
||||||
$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
|
|
||||||
publicKeyCredentialSourceRepository: null,
|
|
||||||
tokenBindingHandler: null,
|
|
||||||
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
|
|
||||||
algorithmManager: $algorithmManager,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$securedRelyingPartyId = [];
|
$algorithmManager = new Manager;
|
||||||
|
$algorithmManager->add(new Ed25519);
|
||||||
|
$algorithmManager->add(new ES256);
|
||||||
|
$algorithmManager->add(new RS256);
|
||||||
|
|
||||||
|
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||||
|
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||||
|
|
||||||
|
$ceremonyStepManagerFactory = new CeremonyStepManagerFactory;
|
||||||
|
$ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager);
|
||||||
|
$ceremonyStepManagerFactory->setAttestationStatementSupportManager(
|
||||||
|
$attestationStatementSupportManager
|
||||||
|
);
|
||||||
|
$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler(
|
||||||
|
ExtensionOutputCheckerHandler::create()
|
||||||
|
);
|
||||||
|
$allowedOrigins = [];
|
||||||
if (App::environment('local', 'development')) {
|
if (App::environment('local', 'development')) {
|
||||||
$securedRelyingPartyId = [config('url.longurl')];
|
$allowedOrigins = [config('app.url')];
|
||||||
}
|
}
|
||||||
|
$ceremonyStepManagerFactory->setAllowedOrigins($allowedOrigins);
|
||||||
|
|
||||||
|
$authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create(
|
||||||
|
ceremonyStepManager: $ceremonyStepManagerFactory->requestCeremony()
|
||||||
|
);
|
||||||
|
|
||||||
|
$publicKeyCredentialRequestOptions = $webauthnSerializer->deserialize(
|
||||||
|
$requestOptions,
|
||||||
|
PublicKeyCredentialRequestOptions::class,
|
||||||
|
'json'
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$authenticatorAssertionResponseValidator->check(
|
$authenticatorAssertionResponseValidator->check(
|
||||||
credentialId: $credential,
|
publicKeyCredentialSource: $publicKeyCredentialSource,
|
||||||
authenticatorAssertionResponse: $publicKeyCredential->response,
|
authenticatorAssertionResponse: $publicKeyCredential->response,
|
||||||
publicKeyCredentialRequestOptions: $publicKeyCredentialRequestOptions,
|
publicKeyCredentialRequestOptions: $publicKeyCredentialRequestOptions,
|
||||||
request: config('url.longurl'),
|
host: config('app.url'),
|
||||||
userHandle: null,
|
userHandle: null,
|
||||||
securedRelyingPartyId: $securedRelyingPartyId,
|
|
||||||
);
|
);
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
|
@ -10,9 +10,6 @@ use App\Services\PlaceService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class PlacesController extends Controller
|
class PlacesController extends Controller
|
||||||
{
|
{
|
||||||
protected PlaceService $placeService;
|
protected PlaceService $placeService;
|
||||||
|
|
|
@ -10,9 +10,6 @@ use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class SyndicationTargetsController extends Controller
|
class SyndicationTargetsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,9 +10,6 @@ use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Jonnybarnes\IndieWeb\Numbers;
|
use Jonnybarnes\IndieWeb\Numbers;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ArticlesController extends Controller
|
class ArticlesController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,9 +9,6 @@ use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class AuthController extends Controller
|
class AuthController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,9 +7,6 @@ namespace App\Http\Controllers;
|
||||||
use App\Models\Bookmark;
|
use App\Models\Bookmark;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class BookmarksController extends Controller
|
class BookmarksController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,9 +8,6 @@ use App\Models\Contact;
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ContactsController extends Controller
|
class ContactsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +15,7 @@ class ContactsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index(): View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$filesystem = new Filesystem();
|
$filesystem = new Filesystem;
|
||||||
$contacts = Contact::all();
|
$contacts = Contact::all();
|
||||||
foreach ($contacts as $contact) {
|
foreach ($contacts as $contact) {
|
||||||
$contact->homepageHost = parse_url($contact->homepage, PHP_URL_HOST);
|
$contact->homepageHost = parse_url($contact->homepage, PHP_URL_HOST);
|
||||||
|
@ -40,7 +37,7 @@ class ContactsController extends Controller
|
||||||
$contact->homepageHost = parse_url($contact->homepage, PHP_URL_HOST);
|
$contact->homepageHost = parse_url($contact->homepage, PHP_URL_HOST);
|
||||||
$file = public_path() . '/assets/profile-images/' . $contact->homepageHost . '/image';
|
$file = public_path() . '/assets/profile-images/' . $contact->homepageHost . '/image';
|
||||||
|
|
||||||
$filesystem = new Filesystem();
|
$filesystem = new Filesystem;
|
||||||
$image = ($filesystem->exists($file)) ?
|
$image = ($filesystem->exists($file)) ?
|
||||||
'/assets/profile-images/' . $contact->homepageHost . '/image'
|
'/assets/profile-images/' . $contact->homepageHost . '/image'
|
||||||
:
|
:
|
||||||
|
|
|
@ -2,11 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
abstract class Controller
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
|
||||||
|
|
||||||
class Controller extends BaseController
|
|
||||||
{
|
{
|
||||||
use AuthorizesRequests, ValidatesRequests;
|
//
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,6 @@ use App\Models\Note;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class FeedsController extends Controller
|
class FeedsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -73,10 +70,16 @@ class FeedsController extends Controller
|
||||||
{
|
{
|
||||||
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
|
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
|
||||||
$data = [
|
$data = [
|
||||||
'version' => 'https://jsonfeed.org/version/1',
|
'version' => 'https://jsonfeed.org/version/1.1',
|
||||||
'title' => 'The JSON Feed for ' . config('user.display_name') . '’s blog',
|
'title' => 'The JSON Feed for ' . config('user.display_name') . '’s blog',
|
||||||
'home_page_url' => config('app.url') . '/blog',
|
'home_page_url' => config('app.url') . '/blog',
|
||||||
'feed_url' => config('app.url') . '/blog/feed.json',
|
'feed_url' => config('app.url') . '/blog/feed.json',
|
||||||
|
'authors' => [
|
||||||
|
[
|
||||||
|
'name' => config('user.display_name'),
|
||||||
|
'url' => config('app.url'),
|
||||||
|
],
|
||||||
|
],
|
||||||
'items' => [],
|
'items' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -88,9 +91,6 @@ class FeedsController extends Controller
|
||||||
'content_html' => $article->main,
|
'content_html' => $article->main,
|
||||||
'date_published' => $article->created_at->tz('UTC')->toRfc3339String(),
|
'date_published' => $article->created_at->tz('UTC')->toRfc3339String(),
|
||||||
'date_modified' => $article->updated_at->tz('UTC')->toRfc3339String(),
|
'date_modified' => $article->updated_at->tz('UTC')->toRfc3339String(),
|
||||||
'author' => [
|
|
||||||
'name' => config('user.display_name'),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,26 +102,32 @@ class FeedsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function notesJson(): array
|
public function notesJson(): array
|
||||||
{
|
{
|
||||||
$notes = Note::latest()->with('media')->take(20)->get();
|
$notes = Note::latest()->with('media', 'place', 'tags')->take(20)->get();
|
||||||
$data = [
|
$data = [
|
||||||
'version' => 'https://jsonfeed.org/version/1',
|
'version' => 'https://jsonfeed.org/version/1.1',
|
||||||
'title' => 'The JSON Feed for ' . config('user.display_name') . '’s notes',
|
'title' => 'The JSON Feed for ' . config('user.display_name') . '’s notes',
|
||||||
'home_page_url' => config('app.url') . '/notes',
|
'home_page_url' => config('app.url') . '/notes',
|
||||||
'feed_url' => config('app.url') . '/notes/feed.json',
|
'feed_url' => config('app.url') . '/notes/feed.json',
|
||||||
|
'authors' => [
|
||||||
|
[
|
||||||
|
'name' => config('user.display_name'),
|
||||||
|
'url' => config('app.url'),
|
||||||
|
],
|
||||||
|
],
|
||||||
'items' => [],
|
'items' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($notes as $key => $note) {
|
foreach ($notes as $key => $note) {
|
||||||
$data['items'][$key] = [
|
$data['items'][$key] = [
|
||||||
'id' => $note->longurl,
|
'id' => $note->uri,
|
||||||
'url' => $note->longurl,
|
'url' => $note->uri,
|
||||||
'content_html' => $note->content,
|
'content_text' => $note->content,
|
||||||
'date_published' => $note->created_at->tz('UTC')->toRfc3339String(),
|
'date_published' => $note->created_at->tz('UTC')->toRfc3339String(),
|
||||||
'date_modified' => $note->updated_at->tz('UTC')->toRfc3339String(),
|
'date_modified' => $note->updated_at->tz('UTC')->toRfc3339String(),
|
||||||
'author' => [
|
|
||||||
'name' => config('user.display_name'),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
if ($note->tags->count() > 0) {
|
||||||
|
$data['items'][$key]['tags'] = implode(',', $note->tags->pluck('tag')->toArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
|
@ -155,7 +161,7 @@ class FeedsController extends Controller
|
||||||
'author' => [
|
'author' => [
|
||||||
'type' => 'card',
|
'type' => 'card',
|
||||||
'name' => config('user.display_name'),
|
'name' => config('user.display_name'),
|
||||||
'url' => config('url.longurl'),
|
'url' => config('app.url'),
|
||||||
],
|
],
|
||||||
'children' => $items,
|
'children' => $items,
|
||||||
], 200, [
|
], 200, [
|
||||||
|
@ -174,8 +180,8 @@ class FeedsController extends Controller
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'type' => 'entry',
|
'type' => 'entry',
|
||||||
'published' => $note->created_at,
|
'published' => $note->created_at,
|
||||||
'uid' => $note->longurl,
|
'uid' => $note->uri,
|
||||||
'url' => $note->longurl,
|
'url' => $note->uri,
|
||||||
'content' => [
|
'content' => [
|
||||||
'text' => $note->getRawOriginal('note'),
|
'text' => $note->getRawOriginal('note'),
|
||||||
'html' => $note->note,
|
'html' => $note->note,
|
||||||
|
@ -191,7 +197,7 @@ class FeedsController extends Controller
|
||||||
'author' => [
|
'author' => [
|
||||||
'type' => 'card',
|
'type' => 'card',
|
||||||
'name' => config('user.display_name'),
|
'name' => config('user.display_name'),
|
||||||
'url' => config('url.longurl'),
|
'url' => config('app.url'),
|
||||||
],
|
],
|
||||||
'children' => $items,
|
'children' => $items,
|
||||||
], 200, [
|
], 200, [
|
||||||
|
|
|
@ -10,9 +10,6 @@ use App\Models\Note;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class FrontPageController extends Controller
|
class FrontPageController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
327
app/Http/Controllers/IndieAuthController.php
Normal file
327
app/Http/Controllers/IndieAuthController.php
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\TokenService;
|
||||||
|
use Exception;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Psr7\Uri;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Random\RandomException;
|
||||||
|
use SodiumException;
|
||||||
|
|
||||||
|
class IndieAuthController extends Controller
|
||||||
|
{
|
||||||
|
public function indieAuthMetadataEndpoint(): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'issuer' => config('app.url'),
|
||||||
|
'authorization_endpoint' => route('indieauth.start'),
|
||||||
|
'token_endpoint' => route('indieauth.token'),
|
||||||
|
'code_challenge_methods_supported' => ['S256'],
|
||||||
|
// 'introspection_endpoint' => route('indieauth.introspection'),
|
||||||
|
// 'introspection_endpoint_auth_methods_supported' => ['none'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a GET request to the IndieAuth endpoint.
|
||||||
|
*
|
||||||
|
* This is the first step in the IndieAuth flow, where the client app sends the user to the IndieAuth endpoint.
|
||||||
|
*/
|
||||||
|
public function start(Request $request): View
|
||||||
|
{
|
||||||
|
// First check all required params are present
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'response_type' => 'required:string',
|
||||||
|
'client_id' => 'required',
|
||||||
|
'redirect_uri' => 'required',
|
||||||
|
'state' => 'required',
|
||||||
|
'code_challenge' => 'required:string',
|
||||||
|
'code_challenge_method' => 'required:string',
|
||||||
|
], [
|
||||||
|
'response_type' => 'response_type is required',
|
||||||
|
'client_id.required' => 'client_id is required to display which app is asking for authentication',
|
||||||
|
'redirect_uri.required' => 'redirect_uri is required so we can progress successful requests',
|
||||||
|
'state.required' => 'state is required',
|
||||||
|
'code_challenge.required' => 'code_challenge is required',
|
||||||
|
'code_challenge_method.required' => 'code_challenge_method is required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return view('indieauth.error')->withErrors($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->get('response_type') !== 'code') {
|
||||||
|
return view('indieauth.error')->withErrors(['response_type' => 'only a response_type of "code" is supported']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strtoupper($request->get('code_challenge_method')) !== 'S256') {
|
||||||
|
return view('indieauth.error')->withErrors(['code_challenge_method' => 'only a code_challenge_method of "S256" is supported']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->isValidRedirectUri($request->get('client_id'), $request->get('redirect_uri'))) {
|
||||||
|
return view('indieauth.error')->withErrors(['redirect_uri' => 'redirect_uri is not valid for this client_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopes = $request->get('scope', '');
|
||||||
|
$scopes = explode(' ', $scopes);
|
||||||
|
|
||||||
|
return view('indieauth.start', [
|
||||||
|
'me' => $request->get('me'),
|
||||||
|
'client_id' => $request->get('client_id'),
|
||||||
|
'redirect_uri' => $request->get('redirect_uri'),
|
||||||
|
'state' => $request->get('state'),
|
||||||
|
'scopes' => $scopes,
|
||||||
|
'code_challenge' => $request->get('code_challenge'),
|
||||||
|
'code_challenge_method' => $request->get('code_challenge_method'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm an IndieAuth approval request.
|
||||||
|
*
|
||||||
|
* Generates an auth code and redirects the user back to the client app.
|
||||||
|
*
|
||||||
|
* @throws RandomException
|
||||||
|
*/
|
||||||
|
public function confirm(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$authCode = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
|
$cacheKey = hash('xxh3', $request->get('client_id'));
|
||||||
|
|
||||||
|
$indieAuthRequestData = [
|
||||||
|
'code_challenge' => $request->get('code_challenge'),
|
||||||
|
'code_challenge_method' => $request->get('code_challenge_method'),
|
||||||
|
'client_id' => $request->get('client_id'),
|
||||||
|
'redirect_uri' => $request->get('redirect_uri'),
|
||||||
|
'auth_code' => $authCode,
|
||||||
|
'scope' => implode(' ', $request->get('scope', '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
Cache::put($cacheKey, $indieAuthRequestData, now()->addMinutes(10));
|
||||||
|
|
||||||
|
$redirectUri = new Uri($request->get('redirect_uri'));
|
||||||
|
$redirectUri = Uri::withQueryValues($redirectUri, [
|
||||||
|
'code' => $authCode,
|
||||||
|
'state' => $request->get('state'),
|
||||||
|
'iss' => config('app.url'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->away($redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a POST request to the IndieAuth auth endpoint.
|
||||||
|
*
|
||||||
|
* This is one possible second step in the IndieAuth flow, where the client app sends the auth code to the IndieAuth
|
||||||
|
* endpoint. As it is to the auth endpoint we return profile information. A similar request can be made to the token
|
||||||
|
* endpoint to get an access token.
|
||||||
|
*/
|
||||||
|
public function processCodeExchange(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$invalidCodeResponse = $this->validateAuthorizationCode($request);
|
||||||
|
|
||||||
|
if ($invalidCodeResponse instanceof JsonResponse) {
|
||||||
|
return $invalidCodeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'me' => config('app.url'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a POST request to the IndieAuth token endpoint.
|
||||||
|
*
|
||||||
|
* This is another possible second step in the IndieAuth flow, where the client app sends the auth code to the
|
||||||
|
* IndieAuth token endpoint. As it is to the token endpoint we return an access token.
|
||||||
|
*
|
||||||
|
* @throws SodiumException
|
||||||
|
*/
|
||||||
|
public function processTokenRequest(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$indieAuthData = $this->validateAuthorizationCode($request);
|
||||||
|
|
||||||
|
if ($indieAuthData instanceof JsonResponse) {
|
||||||
|
return $indieAuthData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($indieAuthData['scope'] === '') {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'scope' => [
|
||||||
|
'The scope property must be non-empty for an access token to be issued.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokenData = [
|
||||||
|
'me' => config('app.url'),
|
||||||
|
'client_id' => $request->get('client_id'),
|
||||||
|
'scope' => $indieAuthData['scope'],
|
||||||
|
];
|
||||||
|
$tokenService = resolve(TokenService::class);
|
||||||
|
$token = $tokenService->getNewToken($tokenData);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'access_token' => $token,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'scope' => $indieAuthData['scope'],
|
||||||
|
'me' => config('app.url'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isValidRedirectUri(string $clientId, string $redirectUri): bool
|
||||||
|
{
|
||||||
|
// If client_id is not a valid URL, then it's not valid
|
||||||
|
$clientIdParsed = \Mf2\parseUriToComponents($clientId);
|
||||||
|
if (! isset($clientIdParsed['authority'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If redirect_uri is not a valid URL, then it's not valid
|
||||||
|
$redirectUriParsed = \Mf2\parseUriToComponents($redirectUri);
|
||||||
|
if (! isset($redirectUriParsed['authority'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If client_id and redirect_uri are the same host, then it's valid
|
||||||
|
if ($clientIdParsed['authority'] === $redirectUriParsed['authority']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we need to check the redirect_uri is in the client_id's redirect_uris
|
||||||
|
$guzzle = resolve(Client::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$clientInfo = $guzzle->get($clientId);
|
||||||
|
} catch (Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientInfoParsed = \Mf2\parse($clientInfo->getBody()->getContents(), $clientId);
|
||||||
|
|
||||||
|
$redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? [];
|
||||||
|
|
||||||
|
return in_array($redirectUri, $redirectUris, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SodiumException
|
||||||
|
*/
|
||||||
|
protected function validateAuthorizationCode(Request $request): JsonResponse|array
|
||||||
|
{
|
||||||
|
// First check all the data is present
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'grant_type' => 'required:string',
|
||||||
|
'code' => 'required:string',
|
||||||
|
'client_id' => 'required',
|
||||||
|
'redirect_uri' => 'required',
|
||||||
|
'code_verifier' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['errors' => $validator->errors()], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->get('grant_type') !== 'authorization_code') {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'grant_type' => [
|
||||||
|
'Only a grant type of "authorization_code" is supported.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache for auth code
|
||||||
|
$cacheKey = hash('xxh3', $request->get('client_id'));
|
||||||
|
$indieAuthRequestData = Cache::pull($cacheKey);
|
||||||
|
|
||||||
|
if ($indieAuthRequestData === null) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'code' => [
|
||||||
|
'The code is invalid.',
|
||||||
|
],
|
||||||
|
]], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the IndieAuth code
|
||||||
|
if (! array_key_exists('auth_code', $indieAuthRequestData)) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'code' => [
|
||||||
|
'The code is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
if ($indieAuthRequestData['auth_code'] !== $request->get('code')) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'code' => [
|
||||||
|
'The code is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check code verifier
|
||||||
|
if (! array_key_exists('code_challenge', $indieAuthRequestData)) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'code_verifier' => [
|
||||||
|
'The code verifier is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
if (! hash_equals(
|
||||||
|
$indieAuthRequestData['code_challenge'],
|
||||||
|
sodium_bin2base64(
|
||||||
|
hash('sha256', $request->get('code_verifier'), true),
|
||||||
|
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'code_verifier' => [
|
||||||
|
'The code verifier is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check redirect_uri
|
||||||
|
if (! array_key_exists('redirect_uri', $indieAuthRequestData)) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'redirect_uri' => [
|
||||||
|
'The redirect uri is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
if ($indieAuthRequestData['redirect_uri'] !== $request->get('redirect_uri')) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'redirect_uri' => [
|
||||||
|
'The redirect uri is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check client_id
|
||||||
|
if (! array_key_exists('client_id', $indieAuthRequestData)) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'client_id' => [
|
||||||
|
'The client id is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
if ($indieAuthRequestData['client_id'] !== $request->get('client_id')) {
|
||||||
|
return response()->json(['errors' => [
|
||||||
|
'client_id' => [
|
||||||
|
'The client id is invalid.',
|
||||||
|
],
|
||||||
|
]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $indieAuthRequestData;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,6 @@ namespace App\Http\Controllers;
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class LikesController extends Controller
|
class LikesController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,110 +4,73 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Responses\MicropubResponses;
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
|
use App\Exceptions\MicropubHandlerException;
|
||||||
|
use App\Http\Requests\MicropubRequest;
|
||||||
use App\Models\Place;
|
use App\Models\Place;
|
||||||
use App\Models\SyndicationTarget;
|
use App\Models\SyndicationTarget;
|
||||||
use App\Services\Micropub\HCardService;
|
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||||
use App\Services\Micropub\HEntryService;
|
|
||||||
use App\Services\Micropub\UpdateService;
|
|
||||||
use App\Services\TokenService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Lcobucci\JWT\Encoding\CannotDecodeContent;
|
use Lcobucci\JWT\Token;
|
||||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
|
||||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
|
||||||
use Monolog\Handler\StreamHandler;
|
|
||||||
use Monolog\Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class MicropubController extends Controller
|
class MicropubController extends Controller
|
||||||
{
|
{
|
||||||
protected TokenService $tokenService;
|
protected MicropubHandlerRegistry $handlerRegistry;
|
||||||
|
|
||||||
protected HEntryService $hentryService;
|
public function __construct(MicropubHandlerRegistry $handlerRegistry)
|
||||||
|
{
|
||||||
protected HCardService $hcardService;
|
$this->handlerRegistry = $handlerRegistry;
|
||||||
|
|
||||||
protected UpdateService $updateService;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
TokenService $tokenService,
|
|
||||||
HEntryService $hentryService,
|
|
||||||
HCardService $hcardService,
|
|
||||||
UpdateService $updateService
|
|
||||||
) {
|
|
||||||
$this->tokenService = $tokenService;
|
|
||||||
$this->hentryService = $hentryService;
|
|
||||||
$this->hcardService = $hcardService;
|
|
||||||
$this->updateService = $updateService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function receives an API request, verifies the authenticity
|
* Respond to a POST request to the micropub endpoint.
|
||||||
* then passes over the info to the relevant Service class.
|
*
|
||||||
|
* The request is initially processed by the MicropubRequest form request
|
||||||
|
* class. The normalizes the data, so we can pass it into the handlers for
|
||||||
|
* the different micropub requests, h-entry or h-card, for example.
|
||||||
*/
|
*/
|
||||||
public function post(Request $request): JsonResponse
|
public function post(MicropubRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$type = $request->getType();
|
||||||
|
|
||||||
|
if (! $type) {
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'invalid_request',
|
||||||
|
'error_description' => 'Microformat object type is missing, for example: h-entry or h-card',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
$handler = $this->handlerRegistry->getHandler($type);
|
||||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
$result = $handler->handle($request->getMicropubData());
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->invalidTokenResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($tokenData->claims()->has('scope') === false) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->tokenHasNoScopeResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logMicropubRequest($request->all());
|
|
||||||
|
|
||||||
if (($request->input('h') === 'entry') || ($request->input('type.0') === 'h-entry')) {
|
|
||||||
if (stripos($tokenData->claims()->get('scope'), 'create') === false) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->insufficientScopeResponse();
|
|
||||||
}
|
|
||||||
$location = $this->hentryService->process($request->all(), $this->getCLientId());
|
|
||||||
|
|
||||||
|
// Return appropriate response based on the handler result
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'response' => 'created',
|
'response' => $result['response'],
|
||||||
'location' => $location,
|
'location' => $result['url'] ?? null,
|
||||||
], 201)->header('Location', $location);
|
], 201)->header('Location', $result['url']);
|
||||||
}
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
|
||||||
if ($request->input('h') === 'card' || $request->input('type.0') === 'h-card') {
|
|
||||||
if (stripos($tokenData->claims()->get('scope'), 'create') === false) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->insufficientScopeResponse();
|
|
||||||
}
|
|
||||||
$location = $this->hcardService->process($request->all());
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'response' => 'created',
|
'error' => 'invalid_request',
|
||||||
'location' => $location,
|
'error_description' => $e->getMessage(),
|
||||||
], 201)->header('Location', $location);
|
], 400);
|
||||||
|
} catch (MicropubHandlerException) {
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'Unknown Micropub type',
|
||||||
|
'error_description' => 'The request could not be processed by this server',
|
||||||
|
], 500);
|
||||||
|
} catch (InvalidTokenScopeException) {
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'invalid_scope',
|
||||||
|
'error_description' => 'The token does not have the required scope for this request',
|
||||||
|
], 403);
|
||||||
|
} catch (\Exception) {
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'server_error',
|
||||||
|
'error_description' => 'An error occurred processing the request',
|
||||||
|
], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->input('action') === 'update') {
|
|
||||||
if (stripos($tokenData->claims()->get('scope'), 'update') === false) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->insufficientScopeResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->updateService->process($request->all());
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'response' => 'error',
|
|
||||||
'error_description' => 'unsupported_request_type',
|
|
||||||
], 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,12 +83,6 @@ class MicropubController extends Controller
|
||||||
*/
|
*/
|
||||||
public function get(Request $request): JsonResponse
|
public function get(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
|
||||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
|
||||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
|
||||||
return (new MicropubResponses())->invalidTokenResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->input('q') === 'syndicate-to') {
|
if ($request->input('q') === 'syndicate-to') {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'syndicate-to' => SyndicationTarget::all(),
|
'syndicate-to' => SyndicationTarget::all(),
|
||||||
|
@ -157,36 +114,17 @@ class MicropubController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// default response is just to return the token data
|
// the default response is just to return the token data
|
||||||
|
/** @var Token $tokenData */
|
||||||
|
$tokenData = $request->input('token_data');
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'response' => 'token',
|
'response' => 'token',
|
||||||
'token' => [
|
'token' => [
|
||||||
'me' => $tokenData->claims()->get('me'),
|
'me' => $tokenData['me'],
|
||||||
'scope' => $tokenData->claims()->get('scope'),
|
'scope' => $tokenData['scope'],
|
||||||
'client_id' => $tokenData->claims()->get('client_id'),
|
'client_id' => $tokenData['client_id'],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the client id from the access token sent with the request.
|
|
||||||
*
|
|
||||||
* @throws RequiredConstraintsViolated
|
|
||||||
*/
|
|
||||||
private function getClientId(): string
|
|
||||||
{
|
|
||||||
return resolve(TokenService::class)
|
|
||||||
->validateToken(app('request')->input('access_token'))
|
|
||||||
->claims()->get('client_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the details of the micropub request to a log file.
|
|
||||||
*/
|
|
||||||
private function logMicropubRequest(array $request): void
|
|
||||||
{
|
|
||||||
$logger = new Logger('micropub');
|
|
||||||
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')));
|
|
||||||
$logger->debug('MicropubLog', $request);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,55 +7,29 @@ namespace App\Http\Controllers;
|
||||||
use App\Http\Responses\MicropubResponses;
|
use App\Http\Responses\MicropubResponses;
|
||||||
use App\Jobs\ProcessMedia;
|
use App\Jobs\ProcessMedia;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Services\TokenService;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
use Illuminate\Http\File;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Intervention\Image\Exceptions\DecoderException;
|
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
|
||||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class MicropubMediaController extends Controller
|
class MicropubMediaController extends Controller
|
||||||
{
|
{
|
||||||
protected TokenService $tokenService;
|
|
||||||
|
|
||||||
public function __construct(TokenService $tokenService)
|
|
||||||
{
|
|
||||||
$this->tokenService = $tokenService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getHandler(Request $request): JsonResponse
|
public function getHandler(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
$tokenData = $request->input('token_data');
|
||||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
|
||||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->invalidTokenResponse();
|
$scopes = $tokenData['scope'];
|
||||||
|
if (is_string($scopes)) {
|
||||||
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
}
|
||||||
|
if (! in_array('create', $scopes, true)) {
|
||||||
if ($tokenData->claims()->has('scope') === false) {
|
return (new MicropubResponses)->insufficientScopeResponse();
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->tokenHasNoScopeResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Str::contains($tokenData->claims()->get('scope'), 'create') === false) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->insufficientScopeResponse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->input('q') === 'last') {
|
if ($request->input('q') === 'last') {
|
||||||
|
@ -106,24 +80,14 @@ class MicropubMediaController extends Controller
|
||||||
*/
|
*/
|
||||||
public function media(Request $request): JsonResponse
|
public function media(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
$tokenData = $request->input('token_data');
|
||||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
|
||||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->invalidTokenResponse();
|
$scopes = $tokenData['scope'];
|
||||||
|
if (is_string($scopes)) {
|
||||||
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
}
|
||||||
|
if (! in_array('create', $scopes, true)) {
|
||||||
if ($tokenData->claims()->has('scope') === false) {
|
return (new MicropubResponses)->insufficientScopeResponse();
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->tokenHasNoScopeResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Str::contains($tokenData->claims()->get('scope'), 'create') === false) {
|
|
||||||
$micropubResponses = new MicropubResponses();
|
|
||||||
|
|
||||||
return $micropubResponses->insufficientScopeResponse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->hasFile('file') === false) {
|
if ($request->hasFile('file') === false) {
|
||||||
|
@ -134,7 +98,10 @@ class MicropubMediaController extends Controller
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->file('file')->isValid() === false) {
|
/** @var UploadedFile $file */
|
||||||
|
$file = $request->file('file');
|
||||||
|
|
||||||
|
if ($file->isValid() === false) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'response' => 'error',
|
'response' => 'error',
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
|
@ -142,32 +109,25 @@ class MicropubMediaController extends Controller
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = $this->saveFile($request->file('file'));
|
$filename = Storage::disk('local')->putFile('media', $file);
|
||||||
|
|
||||||
/** @var ImageManager $manager */
|
/** @var ImageManager $manager */
|
||||||
$manager = resolve(ImageManager::class);
|
$manager = resolve(ImageManager::class);
|
||||||
try {
|
try {
|
||||||
$image = $manager->read($request->file('file'));
|
$image = $manager->read($request->file('file'));
|
||||||
$width = $image->width();
|
$width = $image->width();
|
||||||
} catch (DecoderException) {
|
} catch (Exception) {
|
||||||
// not an image
|
// not an image
|
||||||
$width = null;
|
$width = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$media = Media::create([
|
$media = Media::create([
|
||||||
'token' => $request->bearerToken(),
|
'token' => $request->input('access_token'),
|
||||||
'path' => 'media/' . $filename,
|
'path' => $filename,
|
||||||
'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()),
|
'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()),
|
||||||
'image_widths' => $width,
|
'image_widths' => $width,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// put the file on S3 initially, the ProcessMedia job may edit this
|
|
||||||
Storage::disk('s3')->putFileAs(
|
|
||||||
'media',
|
|
||||||
new File(storage_path('app') . '/' . $filename),
|
|
||||||
$filename
|
|
||||||
);
|
|
||||||
|
|
||||||
ProcessMedia::dispatch($filename);
|
ProcessMedia::dispatch($filename);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
@ -189,7 +149,7 @@ class MicropubMediaController extends Controller
|
||||||
*/
|
*/
|
||||||
private function getFileTypeFromMimeType(string $mimeType): string
|
private function getFileTypeFromMimeType(string $mimeType): string
|
||||||
{
|
{
|
||||||
//try known images
|
// try known images
|
||||||
$imageMimeTypes = [
|
$imageMimeTypes = [
|
||||||
'image/gif',
|
'image/gif',
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
|
@ -201,7 +161,7 @@ class MicropubMediaController extends Controller
|
||||||
if (in_array($mimeType, $imageMimeTypes)) {
|
if (in_array($mimeType, $imageMimeTypes)) {
|
||||||
return 'image';
|
return 'image';
|
||||||
}
|
}
|
||||||
//try known video
|
// try known video
|
||||||
$videoMimeTypes = [
|
$videoMimeTypes = [
|
||||||
'video/mp4',
|
'video/mp4',
|
||||||
'video/mpeg',
|
'video/mpeg',
|
||||||
|
@ -212,7 +172,7 @@ class MicropubMediaController extends Controller
|
||||||
if (in_array($mimeType, $videoMimeTypes)) {
|
if (in_array($mimeType, $videoMimeTypes)) {
|
||||||
return 'video';
|
return 'video';
|
||||||
}
|
}
|
||||||
//try known audio types
|
// try known audio types
|
||||||
$audioMimeTypes = [
|
$audioMimeTypes = [
|
||||||
'audio/midi',
|
'audio/midi',
|
||||||
'audio/mpeg',
|
'audio/mpeg',
|
||||||
|
@ -231,7 +191,7 @@ class MicropubMediaController extends Controller
|
||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private function saveFile(UploadedFile $file): string
|
private function saveFileToLocal(UploadedFile $file): string
|
||||||
{
|
{
|
||||||
$filename = Uuid::uuid4()->toString() . '.' . $file->extension();
|
$filename = Uuid::uuid4()->toString() . '.' . $file->extension();
|
||||||
Storage::disk('local')->putFileAs('', $file, $filename);
|
Storage::disk('local')->putFileAs('', $file, $filename);
|
||||||
|
|
|
@ -14,8 +14,6 @@ use Jonnybarnes\IndieWeb\Numbers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo Need to sort out Twitter and webmentions!
|
* @todo Need to sort out Twitter and webmentions!
|
||||||
*
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
*/
|
||||||
class NotesController extends Controller
|
class NotesController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -67,7 +65,7 @@ class NotesController extends Controller
|
||||||
*/
|
*/
|
||||||
public function redirect(int $decId): RedirectResponse
|
public function redirect(int $decId): RedirectResponse
|
||||||
{
|
{
|
||||||
return redirect(config('app.url') . '/notes/' . (new Numbers())->numto60($decId));
|
return redirect(config('app.url') . '/notes/' . (new Numbers)->numto60($decId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,9 +7,6 @@ namespace App\Http\Controllers;
|
||||||
use App\Models\Place;
|
use App\Models\Place;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class PlacesController extends Controller
|
class PlacesController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,9 +6,6 @@ use App\Models\Note;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
{
|
{
|
||||||
public function search(Request $request): View
|
public function search(Request $request): View
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class ShortURLsController extends Controller
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Short URL Controller
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This redirects the short urls to long ones
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect from '/' to the long url.
|
|
||||||
*/
|
|
||||||
public function baseURL(): RedirectResponse
|
|
||||||
{
|
|
||||||
return redirect(config('app.url'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect from '/@' to a twitter profile.
|
|
||||||
*/
|
|
||||||
public function twitter(): RedirectResponse
|
|
||||||
{
|
|
||||||
return redirect('https://twitter.com/jonnybarnes');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect a short url of this site out to a long one based on post type.
|
|
||||||
*
|
|
||||||
* Further redirects may happen.
|
|
||||||
*/
|
|
||||||
public function expandType(string $type, string $postId): RedirectResponse
|
|
||||||
{
|
|
||||||
if ($type === 't') {
|
|
||||||
$type = 'notes';
|
|
||||||
}
|
|
||||||
if ($type === 'b') {
|
|
||||||
$type = 'blog/s';
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(config('app.url') . '/' . $type . '/' . $postId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Services\TokenService;
|
|
||||||
use GuzzleHttp\Client as GuzzleClient;
|
|
||||||
use GuzzleHttp\Exception\BadResponseException;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use IndieAuth\Client;
|
|
||||||
use JsonException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class TokenEndpointController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var Client The IndieAuth Client.
|
|
||||||
*/
|
|
||||||
protected Client $client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var GuzzleClient The GuzzleHttp client.
|
|
||||||
*/
|
|
||||||
protected GuzzleClient $guzzle;
|
|
||||||
|
|
||||||
protected TokenService $tokenService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject the dependencies.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
Client $client,
|
|
||||||
GuzzleClient $guzzle,
|
|
||||||
TokenService $tokenService
|
|
||||||
) {
|
|
||||||
$this->client = $client;
|
|
||||||
$this->guzzle = $guzzle;
|
|
||||||
$this->tokenService = $tokenService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the user has auth’d via the IndieAuth protocol, issue a valid token.
|
|
||||||
*/
|
|
||||||
public function create(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$auth = $this->verifyIndieAuthCode(
|
|
||||||
config('url.authorization_endpoint'),
|
|
||||||
$request->input('code'),
|
|
||||||
$request->input('redirect_uri'),
|
|
||||||
$request->input('client_id'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($auth === null || ! array_key_exists('me', $auth)) {
|
|
||||||
return response()->json([
|
|
||||||
'error' => 'There was an error verifying the IndieAuth code',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope = $auth['scope'] ?? '';
|
|
||||||
$tokenData = [
|
|
||||||
'me' => config('app.url'),
|
|
||||||
'client_id' => $request->input('client_id'),
|
|
||||||
'scope' => $scope,
|
|
||||||
];
|
|
||||||
$token = $this->tokenService->getNewToken($tokenData);
|
|
||||||
$content = [
|
|
||||||
'me' => config('app.url'),
|
|
||||||
'scope' => $scope,
|
|
||||||
'access_token' => $token,
|
|
||||||
];
|
|
||||||
|
|
||||||
return response()->json($content);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function verifyIndieAuthCode(
|
|
||||||
string $authorizationEndpoint,
|
|
||||||
string $code,
|
|
||||||
string $redirectUri,
|
|
||||||
string $clientId
|
|
||||||
): ?array {
|
|
||||||
try {
|
|
||||||
$response = $this->guzzle->request('POST', $authorizationEndpoint, [
|
|
||||||
'headers' => [
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
],
|
|
||||||
'form_params' => [
|
|
||||||
'code' => $code,
|
|
||||||
'me' => config('app.url'),
|
|
||||||
'redirect_uri' => $redirectUri,
|
|
||||||
'client_id' => $clientId,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
} catch (BadResponseException) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$authData = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
} catch (JsonException) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $authData;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,9 +12,6 @@ use Illuminate\Http\Response;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Jonnybarnes\IndieWeb\Numbers;
|
use Jonnybarnes\IndieWeb\Numbers;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class WebMentionsController extends Controller
|
class WebMentionsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +30,7 @@ class WebMentionsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function receive(Request $request): Response
|
public function receive(Request $request): Response
|
||||||
{
|
{
|
||||||
//first we trivially reject requests that lack all required inputs
|
// first we trivially reject requests that lack all required inputs
|
||||||
if (($request->has('target') !== true) || ($request->has('source') !== true)) {
|
if (($request->has('target') !== true) || ($request->has('source') !== true)) {
|
||||||
return response(
|
return response(
|
||||||
'You need both the target and source parameters',
|
'You need both the target and source parameters',
|
||||||
|
@ -41,12 +38,12 @@ class WebMentionsController extends Controller
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//next check the $target is valid
|
// next check the $target is valid
|
||||||
$path = parse_url($request->input('target'), PHP_URL_PATH);
|
$path = parse_url($request->input('target'), PHP_URL_PATH);
|
||||||
$pathParts = explode('/', $path);
|
$pathParts = explode('/', $path);
|
||||||
|
|
||||||
if ($pathParts[1] === 'notes') {
|
if ($pathParts[1] === 'notes') {
|
||||||
//we have a note
|
// we have a note
|
||||||
$noteId = $pathParts[2];
|
$noteId = $pathParts[2];
|
||||||
try {
|
try {
|
||||||
$note = Note::findOrFail(resolve(Numbers::class)->b60tonum($noteId));
|
$note = Note::findOrFail(resolve(Numbers::class)->b60tonum($noteId));
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The application's global HTTP middleware stack.
|
|
||||||
*
|
|
||||||
* These middleware are run during every request to your application.
|
|
||||||
*
|
|
||||||
* @var array<int, class-string|string>
|
|
||||||
*/
|
|
||||||
protected $middleware = [
|
|
||||||
// \App\Http\Middleware\TrustHosts::class,
|
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
|
||||||
\Illuminate\Http\Middleware\HandleCors::class,
|
|
||||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's route middleware groups.
|
|
||||||
*
|
|
||||||
* @var array<string, array<int, class-string|string>>
|
|
||||||
*/
|
|
||||||
protected $middlewareGroups = [
|
|
||||||
'web' => [
|
|
||||||
\App\Http\Middleware\EncryptCookies::class,
|
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
\App\Http\Middleware\LinkHeadersMiddleware::class,
|
|
||||||
\App\Http\Middleware\LocalhostSessionMiddleware::class,
|
|
||||||
\App\Http\Middleware\CSPHeader::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
'api' => [
|
|
||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
|
||||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's middleware aliases.
|
|
||||||
*
|
|
||||||
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
|
|
||||||
*
|
|
||||||
* @var array<string, class-string|string>
|
|
||||||
*/
|
|
||||||
protected $middlewareAliases = [
|
|
||||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
|
||||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
|
||||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
|
||||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
|
||||||
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
|
|
||||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
|
||||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
|
||||||
'micropub.token' => \App\Http\Middleware\VerifyMicropubToken::class,
|
|
||||||
'myauth' => \App\Http\Middleware\MyAuthMiddleware::class,
|
|
||||||
'cors' => \App\Http\Middleware\CorsHeaders::class,
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\App;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class CSPHeader
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next): Response
|
|
||||||
{
|
|
||||||
if (App::environment('local', 'development')) {
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// headers have to be single-line strings,
|
|
||||||
// so we concat multiple lines
|
|
||||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
return $next($request)
|
|
||||||
->header(
|
|
||||||
'Content-Security-Policy',
|
|
||||||
"default-src 'self'; " .
|
|
||||||
"style-src 'self' 'unsafe-inline' cloud.typography.com jonnybarnes.uk; " .
|
|
||||||
"img-src 'self' data: blob: https://pbs.twimg.com https://jbuk-media.s3-eu-west-1.amazonaws.com https://jbuk-media-dev.s3-eu-west-1.amazonaws.com https://secure.gravatar.com https://graph.facebook.com *.fbcdn.net https://*.cdninstagram.com https://*.4sqi.net https://upload.wikimedia.org; " .
|
|
||||||
"font-src 'self' data:; " .
|
|
||||||
"frame-src 'self' https://www.youtube.com blob:; " .
|
|
||||||
'upgrade-insecure-requests; ' .
|
|
||||||
'block-all-mixed-content; ' .
|
|
||||||
'report-to csp-endpoint; ' .
|
|
||||||
'report-uri https://jonnybarnes.report-uri.io/r/default/csp/enforce;'
|
|
||||||
)->header(
|
|
||||||
'Report-To',
|
|
||||||
'{' .
|
|
||||||
"'url': 'https://jonnybarnes.report-uri.io/r/default/csp/enforce', " .
|
|
||||||
"'group': 'csp-endpoint', " .
|
|
||||||
"'max-age': 10886400" .
|
|
||||||
'}'
|
|
||||||
);
|
|
||||||
// phpcs:enable Generic.Files.LineLength.TooLong
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,8 +10,6 @@ class CorsHeaders
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,16 +10,15 @@ class LinkHeadersMiddleware
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
$response = $next($request);
|
$response = $next($request);
|
||||||
$response->header('Link', '<https://indieauth.com/auth>; rel="authorization_endpoint"', false);
|
$response->header('Link', '<' . route('indieauth.metadata') . '>; rel="indieauth-metadata"', false);
|
||||||
$response->header('Link', '<' . config('app.url') . '/api/token>; rel="token_endpoint"', false);
|
$response->header('Link', '<' . route('indieauth.start') . '>; rel="authorization_endpoint"', false);
|
||||||
$response->header('Link', '<' . config('app.url') . '/api/post>; rel="micropub"', false);
|
$response->header('Link', '<' . route('indieauth.token') . '>; rel="token_endpoint"', false);
|
||||||
$response->header('Link', '<' . config('app.url') . '/webmention>; rel="webmention"', false);
|
$response->header('Link', '<' . route('micropub-endpoint') . '>; rel="micropub"', false);
|
||||||
|
$response->header('Link', '<' . route('webmention-endpoint') . '>; rel="webmention"', false);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,6 @@ class LocalhostSessionMiddleware
|
||||||
* Whilst we are developing locally, automatically log in as
|
* Whilst we are developing locally, automatically log in as
|
||||||
* `['me' => config('app.url')]` as I can’t manually log in as
|
* `['me' => config('app.url')]` as I can’t manually log in as
|
||||||
* a .localhost domain.
|
* a .localhost domain.
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
|
|
24
app/Http/Middleware/LogMicropubRequest.php
Normal file
24
app/Http/Middleware/LogMicropubRequest.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Logger;
|
||||||
|
|
||||||
|
class LogMicropubRequest
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response|JsonResponse
|
||||||
|
{
|
||||||
|
$logger = new Logger('micropub');
|
||||||
|
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')));
|
||||||
|
$logger->debug('MicropubLog', $request->all());
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,13 +13,13 @@ class MyAuthMiddleware
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Check the user is logged in.
|
* Check the user is logged in.
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if (Auth::check() === false) {
|
if (Auth::check() === false) {
|
||||||
// they’re not logged in, so send them to login form
|
// they’re not logged in, so send them to login form
|
||||||
|
redirect()->setIntendedUrl($request->fullUrl());
|
||||||
|
|
||||||
return redirect()->route('login');
|
return redirect()->route('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@ class ValidateSignature extends Middleware
|
||||||
* The names of the query string parameters that should be ignored.
|
* The names of the query string parameters that should be ignored.
|
||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var array<int, string>
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedProperty
|
|
||||||
*/
|
*/
|
||||||
protected $except = [
|
protected $except = [
|
||||||
// 'fbclid',
|
// 'fbclid',
|
||||||
|
|
|
@ -4,8 +4,14 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Http\Responses\MicropubResponses;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Lcobucci\JWT\Configuration;
|
||||||
|
use Lcobucci\JWT\Encoding\CannotDecodeContent;
|
||||||
|
use Lcobucci\JWT\Token;
|
||||||
|
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||||
|
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class VerifyMicropubToken
|
class VerifyMicropubToken
|
||||||
|
@ -13,24 +19,63 @@ class VerifyMicropubToken
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
* @param Closure(Request): (Response) $next
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
|
$rawToken = null;
|
||||||
|
|
||||||
if ($request->input('access_token')) {
|
if ($request->input('access_token')) {
|
||||||
return $next($request);
|
$rawToken = $request->input('access_token');
|
||||||
|
} elseif ($request->bearerToken()) {
|
||||||
|
$rawToken = $request->bearerToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->bearerToken()) {
|
if (! $rawToken) {
|
||||||
return $next($request->merge([
|
return response()->json([
|
||||||
'access_token' => $request->bearerToken(),
|
'response' => 'error',
|
||||||
]));
|
'error' => 'unauthorized',
|
||||||
|
'error_description' => 'No access token was provided in the request',
|
||||||
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
try {
|
||||||
'response' => 'error',
|
$tokenData = $this->validateToken($rawToken);
|
||||||
'error' => 'unauthorized',
|
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
||||||
'error_description' => 'No access token was provided in the request',
|
$micropubResponses = new MicropubResponses;
|
||||||
], 401);
|
|
||||||
|
return $micropubResponses->invalidTokenResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tokenData->claims()->has('scope') === false) {
|
||||||
|
$micropubResponses = new MicropubResponses;
|
||||||
|
|
||||||
|
return $micropubResponses->tokenHasNoScopeResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request->merge([
|
||||||
|
'access_token' => $rawToken,
|
||||||
|
'token_data' => [
|
||||||
|
'me' => $tokenData->claims()->get('me'),
|
||||||
|
'scope' => $tokenData->claims()->get('scope'),
|
||||||
|
'client_id' => $tokenData->claims()->get('client_id'),
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the token signature is valid.
|
||||||
|
*/
|
||||||
|
private function validateToken(string $bearerToken): Token
|
||||||
|
{
|
||||||
|
$config = resolve(Configuration::class);
|
||||||
|
|
||||||
|
$token = $config->parser()->parse($bearerToken);
|
||||||
|
|
||||||
|
$constraints = $config->validationConstraints();
|
||||||
|
|
||||||
|
$config->validator()->assert($token, ...$constraints);
|
||||||
|
|
||||||
|
return $token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
106
app/Http/Requests/MicropubRequest.php
Normal file
106
app/Http/Requests/MicropubRequest.php
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class MicropubRequest extends FormRequest
|
||||||
|
{
|
||||||
|
protected array $micropubData = [];
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Validation rules
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMicropubData(): array
|
||||||
|
{
|
||||||
|
return $this->micropubData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
// Return consistent type regardless of input format
|
||||||
|
return $this->micropubData['type'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
// Normalize the request data based on content type
|
||||||
|
if ($this->isJson()) {
|
||||||
|
$this->normalizeMicropubJson();
|
||||||
|
} else {
|
||||||
|
$this->normalizeMicropubForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMicropubJson(): void
|
||||||
|
{
|
||||||
|
$json = $this->json();
|
||||||
|
if ($json === null) {
|
||||||
|
throw new \InvalidArgumentException('`isJson()` passed but there is no json data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $json->all();
|
||||||
|
|
||||||
|
// Convert JSON type (h-entry) to simple type (entry)
|
||||||
|
if (isset($data['type']) && is_array($data['type'])) {
|
||||||
|
$type = current($data['type']);
|
||||||
|
if (strpos($type, 'h-') === 0) {
|
||||||
|
$this->micropubData['type'] = substr($type, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Or set the type to update
|
||||||
|
elseif (isset($data['action']) && $data['action'] === 'update') {
|
||||||
|
$this->micropubData['type'] = 'update';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in the token data
|
||||||
|
$this->micropubData['token_data'] = $data['token_data'];
|
||||||
|
|
||||||
|
// Add h-entry values
|
||||||
|
$this->micropubData['content'] = Arr::get($data, 'properties.content.0');
|
||||||
|
$this->micropubData['in-reply-to'] = Arr::get($data, 'properties.in-reply-to.0');
|
||||||
|
$this->micropubData['published'] = Arr::get($data, 'properties.published.0');
|
||||||
|
$this->micropubData['location'] = Arr::get($data, 'location');
|
||||||
|
$this->micropubData['bookmark-of'] = Arr::get($data, 'properties.bookmark-of.0');
|
||||||
|
$this->micropubData['like-of'] = Arr::get($data, 'properties.like-of.0');
|
||||||
|
$this->micropubData['mp-syndicate-to'] = Arr::get($data, 'properties.mp-syndicate-to');
|
||||||
|
|
||||||
|
// Add h-card values
|
||||||
|
$this->micropubData['name'] = Arr::get($data, 'properties.name.0');
|
||||||
|
$this->micropubData['description'] = Arr::get($data, 'properties.description.0');
|
||||||
|
$this->micropubData['geo'] = Arr::get($data, 'properties.geo.0');
|
||||||
|
|
||||||
|
// Add checkin value
|
||||||
|
$this->micropubData['checkin'] = Arr::get($data, 'checkin');
|
||||||
|
$this->micropubData['syndication'] = Arr::get($data, 'properties.syndication.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMicropubForm(): void
|
||||||
|
{
|
||||||
|
// Convert form h=entry to type=entry
|
||||||
|
if ($h = $this->input('h')) {
|
||||||
|
$this->micropubData['type'] = $h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some fields to the micropub data with default null values
|
||||||
|
$this->micropubData['in-reply-to'] = null;
|
||||||
|
$this->micropubData['published'] = null;
|
||||||
|
$this->micropubData['location'] = null;
|
||||||
|
$this->micropubData['description'] = null;
|
||||||
|
$this->micropubData['geo'] = null;
|
||||||
|
$this->micropubData['latitude'] = null;
|
||||||
|
$this->micropubData['longitude'] = null;
|
||||||
|
|
||||||
|
// Map form fields to micropub data
|
||||||
|
foreach ($this->except(['h', 'access_token']) as $key => $value) {
|
||||||
|
$this->micropubData[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,8 +24,7 @@ class DownloadWebMention implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $source
|
protected string $source
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
@ -36,30 +35,30 @@ class DownloadWebMention implements ShouldQueue
|
||||||
public function handle(Client $guzzle): void
|
public function handle(Client $guzzle): void
|
||||||
{
|
{
|
||||||
$response = $guzzle->request('GET', $this->source);
|
$response = $guzzle->request('GET', $this->source);
|
||||||
//4XX and 5XX responses should get Guzzle to throw an exception,
|
// 4XX and 5XX responses should get Guzzle to throw an exception,
|
||||||
//Laravel should catch and retry these automatically.
|
// Laravel should catch and retry these automatically.
|
||||||
if ($response->getStatusCode() === 200) {
|
if ($response->getStatusCode() === 200) {
|
||||||
$filesystem = new FileSystem();
|
$filesystem = new FileSystem;
|
||||||
$filename = storage_path('HTML') . '/' . $this->createFilenameFromURL($this->source);
|
$filename = storage_path('HTML') . '/' . $this->createFilenameFromURL($this->source);
|
||||||
//backup file first
|
// backup file first
|
||||||
$filenameBackup = $filename . '.' . date('Y-m-d') . '.backup';
|
$filenameBackup = $filename . '.' . date('Y-m-d') . '.backup';
|
||||||
if ($filesystem->exists($filename)) {
|
if ($filesystem->exists($filename)) {
|
||||||
$filesystem->copy($filename, $filenameBackup);
|
$filesystem->copy($filename, $filenameBackup);
|
||||||
}
|
}
|
||||||
//check if base directory exists
|
// check if base directory exists
|
||||||
if (! $filesystem->exists($filesystem->dirname($filename))) {
|
if (! $filesystem->exists($filesystem->dirname($filename))) {
|
||||||
$filesystem->makeDirectory(
|
$filesystem->makeDirectory(
|
||||||
$filesystem->dirname($filename),
|
$filesystem->dirname($filename),
|
||||||
0755, //mode
|
0755, // mode
|
||||||
true //recursive
|
true // recursive
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//save new HTML
|
// save new HTML
|
||||||
$filesystem->put(
|
$filesystem->put(
|
||||||
$filename,
|
$filename,
|
||||||
(string) $response->getBody()
|
(string) $response->getBody()
|
||||||
);
|
);
|
||||||
//remove backup if the same
|
// remove backup if the same
|
||||||
if ($filesystem->exists($filenameBackup)) {
|
if ($filesystem->exists($filenameBackup)) {
|
||||||
if ($filesystem->get($filename) === $filesystem->get($filenameBackup)) {
|
if ($filesystem->get($filename) === $filesystem->get($filenameBackup)) {
|
||||||
$filesystem->delete($filenameBackup);
|
$filesystem->delete($filenameBackup);
|
||||||
|
|
|
@ -25,8 +25,7 @@ class ProcessBookmark implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Bookmark $bookmark
|
protected Bookmark $bookmark
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
|
|
@ -30,8 +30,7 @@ class ProcessLike implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Like $like
|
protected Like $like
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
@ -50,7 +49,7 @@ class ProcessLike implements ShouldQueue
|
||||||
$this->like->content = $tweet->html;
|
$this->like->content = $tweet->html;
|
||||||
$this->like->save();
|
$this->like->save();
|
||||||
|
|
||||||
//POSSE like
|
// POSSE like
|
||||||
try {
|
try {
|
||||||
$client->request(
|
$client->request(
|
||||||
'POST',
|
'POST',
|
||||||
|
|
|
@ -25,43 +25,45 @@ class ProcessMedia implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $filename
|
protected string $filename
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(ImageManager $manager): void
|
public function handle(ImageManager $manager): void
|
||||||
{
|
{
|
||||||
//open file
|
// Load file
|
||||||
|
$file = Storage::disk('local')->get('media/' . $this->filename);
|
||||||
|
|
||||||
|
// Open file
|
||||||
try {
|
try {
|
||||||
$image = $manager->read(storage_path('app') . '/' . $this->filename);
|
$image = $manager->read($file);
|
||||||
} catch (DecoderException) {
|
} catch (DecoderException) {
|
||||||
// not an image; delete file and end job
|
// not an image; delete file and end job
|
||||||
unlink(storage_path('app') . '/' . $this->filename);
|
Storage::disk('local')->delete('media/' . $this->filename);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//create smaller versions if necessary
|
|
||||||
|
// Save the file publicly
|
||||||
|
Storage::disk('public')->put('media/' . $this->filename, $file);
|
||||||
|
|
||||||
|
// Create smaller versions if necessary
|
||||||
if ($image->width() > 1000) {
|
if ($image->width() > 1000) {
|
||||||
$filenameParts = explode('.', $this->filename);
|
$filenameParts = explode('.', $this->filename);
|
||||||
$extension = array_pop($filenameParts);
|
$extension = array_pop($filenameParts);
|
||||||
// the following achieves this data flow
|
// the following achieves this data flow
|
||||||
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
|
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
|
||||||
$basename = ltrim(array_reduce($filenameParts, function ($carry, $item) {
|
$basename = trim(implode('.', $filenameParts), '.');
|
||||||
return $carry . '.' . $item;
|
|
||||||
}, ''), '.');
|
$medium = $image->resize(width: 1000);
|
||||||
$medium = $image->resize(1000, null, function ($constraint) {
|
Storage::disk('public')->put('media/' . $basename . '-medium.' . $extension, (string) $medium->encode());
|
||||||
$constraint->aspectRatio();
|
|
||||||
});
|
$small = $image->resize(width: 500);
|
||||||
Storage::disk('s3')->put('media/' . $basename . '-medium.' . $extension, (string) $medium->encode());
|
Storage::disk('public')->put('media/' . $basename . '-small.' . $extension, (string) $small->encode());
|
||||||
$small = $image->resize(500, null, function ($constraint) {
|
|
||||||
$constraint->aspectRatio();
|
|
||||||
});
|
|
||||||
Storage::disk('s3')->put('media/' . $basename . '-small.' . $extension, (string) $small->encode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now we can delete the locally saved image
|
// Now we can delete the locally saved image
|
||||||
unlink(storage_path('app') . '/' . $this->filename);
|
Storage::disk('local')->delete('media/' . $this->filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,7 @@ class ProcessWebMention implements ShouldQueue
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Note $note,
|
protected Note $note,
|
||||||
protected string $source
|
protected string $source
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
@ -45,7 +44,7 @@ class ProcessWebMention implements ShouldQueue
|
||||||
try {
|
try {
|
||||||
$response = $guzzle->request('GET', $this->source);
|
$response = $guzzle->request('GET', $this->source);
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
throw new RemoteContentNotFoundException();
|
throw new RemoteContentNotFoundException;
|
||||||
}
|
}
|
||||||
$this->saveRemoteContent((string) $response->getBody(), $this->source);
|
$this->saveRemoteContent((string) $response->getBody(), $this->source);
|
||||||
$microformats = Mf2\parse((string) $response->getBody(), $this->source);
|
$microformats = Mf2\parse((string) $response->getBody(), $this->source);
|
||||||
|
@ -54,7 +53,7 @@ class ProcessWebMention implements ShouldQueue
|
||||||
// check webmention still references target
|
// check webmention still references target
|
||||||
// we try each type of mention (reply/like/repost)
|
// we try each type of mention (reply/like/repost)
|
||||||
if ($webmention->type === 'in-reply-to') {
|
if ($webmention->type === 'in-reply-to') {
|
||||||
if ($parser->checkInReplyTo($microformats, $this->note->longurl) === false) {
|
if ($parser->checkInReplyTo($microformats, $this->note->uri) === false) {
|
||||||
// it doesn’t so delete
|
// it doesn’t so delete
|
||||||
$webmention->delete();
|
$webmention->delete();
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ class ProcessWebMention implements ShouldQueue
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($webmention->type === 'like-of') {
|
if ($webmention->type === 'like-of') {
|
||||||
if ($parser->checkLikeOf($microformats, $this->note->longurl) === false) {
|
if ($parser->checkLikeOf($microformats, $this->note->uri) === false) {
|
||||||
// it doesn’t so delete
|
// it doesn’t so delete
|
||||||
$webmention->delete();
|
$webmention->delete();
|
||||||
|
|
||||||
|
@ -76,7 +75,7 @@ class ProcessWebMention implements ShouldQueue
|
||||||
} // note we don’t need to do anything if it still is a like
|
} // note we don’t need to do anything if it still is a like
|
||||||
}
|
}
|
||||||
if ($webmention->type === 'repost-of') {
|
if ($webmention->type === 'repost-of') {
|
||||||
if ($parser->checkRepostOf($microformats, $this->note->longurl) === false) {
|
if ($parser->checkRepostOf($microformats, $this->note->uri) === false) {
|
||||||
// it doesn’t so delete
|
// it doesn’t so delete
|
||||||
$webmention->delete();
|
$webmention->delete();
|
||||||
|
|
||||||
|
@ -86,13 +85,13 @@ class ProcessWebMention implements ShouldQueue
|
||||||
}// foreach
|
}// foreach
|
||||||
|
|
||||||
// no webmention in the db so create new one
|
// no webmention in the db so create new one
|
||||||
$webmention = new WebMention();
|
$webmention = new WebMention;
|
||||||
$type = $parser->getMentionType($microformats); // throw error here?
|
$type = $parser->getMentionType($microformats); // throw error here?
|
||||||
dispatch(new SaveProfileImage($microformats));
|
dispatch(new SaveProfileImage($microformats));
|
||||||
$webmention->source = $this->source;
|
$webmention->source = $this->source;
|
||||||
$webmention->target = $this->note->longurl;
|
$webmention->target = $this->note->uri;
|
||||||
$webmention->commentable_id = $this->note->id;
|
$webmention->commentable_id = $this->note->id;
|
||||||
$webmention->commentable_type = 'App\Model\Note';
|
$webmention->commentable_type = Note::class;
|
||||||
$webmention->type = $type;
|
$webmention->type = $type;
|
||||||
$webmention->mf2 = json_encode($microformats);
|
$webmention->mf2 = json_encode($microformats);
|
||||||
$webmention->save();
|
$webmention->save();
|
||||||
|
|
|
@ -25,8 +25,7 @@ class SaveProfileImage implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected array $microformats
|
protected array $microformats
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
@ -50,7 +49,7 @@ class SaveProfileImage implements ShouldQueue
|
||||||
$home = array_shift($home);
|
$home = array_shift($home);
|
||||||
}
|
}
|
||||||
|
|
||||||
//dont save pbs.twimg.com links
|
// dont save pbs.twimg.com links
|
||||||
if (
|
if (
|
||||||
$photo
|
$photo
|
||||||
&& parse_url($photo, PHP_URL_HOST) !== 'pbs.twimg.com'
|
&& parse_url($photo, PHP_URL_HOST) !== 'pbs.twimg.com'
|
||||||
|
|
|
@ -23,8 +23,7 @@ class SaveScreenshot implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Bookmark $bookmark
|
protected Bookmark $bookmark
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
|
|
@ -27,8 +27,7 @@ class SendWebMentions implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Note $note
|
protected Note $note
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
@ -46,7 +45,7 @@ class SendWebMentions implements ShouldQueue
|
||||||
$guzzle = resolve(Client::class);
|
$guzzle = resolve(Client::class);
|
||||||
$guzzle->post($endpoint, [
|
$guzzle->post($endpoint, [
|
||||||
'form_params' => [
|
'form_params' => [
|
||||||
'source' => $this->note->longurl,
|
'source' => $this->note->uri,
|
||||||
'target' => $url,
|
'target' => $url,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
@ -62,7 +61,7 @@ class SendWebMentions implements ShouldQueue
|
||||||
public function discoverWebmentionEndpoint(string $url): ?string
|
public function discoverWebmentionEndpoint(string $url): ?string
|
||||||
{
|
{
|
||||||
// let’s not send webmentions to myself
|
// let’s not send webmentions to myself
|
||||||
if (parse_url($url, PHP_URL_HOST) === config('url.longurl')) {
|
if (parse_url($url, PHP_URL_HOST) === parse_url(config('app.url'), PHP_URL_HOST)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (Str::startsWith($url, '/notes/tagged/')) {
|
if (Str::startsWith($url, '/notes/tagged/')) {
|
||||||
|
@ -73,15 +72,15 @@ class SendWebMentions implements ShouldQueue
|
||||||
|
|
||||||
$guzzle = resolve(Client::class);
|
$guzzle = resolve(Client::class);
|
||||||
$response = $guzzle->get($url);
|
$response = $guzzle->get($url);
|
||||||
//check HTTP Headers for webmention endpoint
|
// check HTTP Headers for webmention endpoint
|
||||||
$links = Header::parse($response->getHeader('Link'));
|
$links = Header::parse($response->getHeader('Link'));
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
if (mb_stristr($link['rel'], 'webmention')) {
|
if (array_key_exists('rel', $link) && mb_stristr($link['rel'], 'webmention')) {
|
||||||
return $this->resolveUri(trim($link[0], '<>'), $url);
|
return $this->resolveUri(trim($link[0], '<>'), $url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//failed to find a header so parse HTML
|
// failed to find a header so parse HTML
|
||||||
$html = (string) $response->getBody();
|
$html = (string) $response->getBody();
|
||||||
|
|
||||||
$mf2 = new \Mf2\Parser($html, $url);
|
$mf2 = new \Mf2\Parser($html, $url);
|
||||||
|
@ -109,7 +108,7 @@ class SendWebMentions implements ShouldQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
$urls = [];
|
$urls = [];
|
||||||
$dom = new \DOMDocument();
|
$dom = new \DOMDocument;
|
||||||
$dom->loadHTML($html);
|
$dom->loadHTML($html);
|
||||||
$anchors = $dom->getElementsByTagName('a');
|
$anchors = $dom->getElementsByTagName('a');
|
||||||
foreach ($anchors as $anchor) {
|
foreach ($anchors as $anchor) {
|
||||||
|
|
62
app/Jobs/SyndicateNoteToBluesky.php
Normal file
62
app/Jobs/SyndicateNoteToBluesky.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Note;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class SyndicateNoteToBluesky implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected Note $note
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function handle(Client $guzzle): void
|
||||||
|
{
|
||||||
|
// We can only make the request if we have an access token
|
||||||
|
if (config('bridgy.bluesky_token') === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make micropub request
|
||||||
|
$response = $guzzle->request(
|
||||||
|
'POST',
|
||||||
|
'https://brid.gy/micropub',
|
||||||
|
[
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . config('bridgy.bluesky_token'),
|
||||||
|
],
|
||||||
|
'json' => [
|
||||||
|
'type' => ['h-entry'],
|
||||||
|
'properties' => [
|
||||||
|
'content' => [$this->note->getRawOriginal('note')],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse for syndication URL
|
||||||
|
if ($response->getStatusCode() === 201) {
|
||||||
|
$this->note->bluesky_url = $response->getHeader('Location')[0];
|
||||||
|
$this->note->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,8 +22,7 @@ class SyndicateNoteToMastodon implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Note $note
|
protected Note $note
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
|
|
@ -58,10 +58,10 @@ class Article extends Model
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
get: function () {
|
get: function () {
|
||||||
$environment = new Environment();
|
$environment = new Environment;
|
||||||
$environment->addExtension(new CommonMarkCoreExtension());
|
$environment->addExtension(new CommonMarkCoreExtension);
|
||||||
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer());
|
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer);
|
||||||
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer());
|
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer);
|
||||||
$markdownConverter = new MarkdownConverter($environment);
|
$markdownConverter = new MarkdownConverter($environment);
|
||||||
|
|
||||||
return $markdownConverter->convert($this->main)->getContent();
|
return $markdownConverter->convert($this->main)->getContent();
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Bookmark extends Model
|
||||||
return $this->belongsToMany('App\Models\Tag');
|
return $this->belongsToMany('App\Models\Tag');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function longurl(): Attribute
|
protected function local_uri(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
get: fn () => config('app.url') . '/bookmarks/' . $this->id,
|
get: fn () => config('app.url') . '/bookmarks/' . $this->id,
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Media extends Model
|
||||||
return $attributes['path'];
|
return $attributes['path'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return config('filesystems.disks.s3.url') . '/' . $attributes['path'];
|
return config('app.url') . '/storage/' . $attributes['path'];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ class Media extends Model
|
||||||
$basename = $this->getBasename($path);
|
$basename = $this->getBasename($path);
|
||||||
$extension = $this->getExtension($path);
|
$extension = $this->getExtension($path);
|
||||||
|
|
||||||
return config('filesystems.disks.s3.url') . '/' . $basename . '-' . $size . '.' . $extension;
|
return config('app.url') . '/storage/' . $basename . '-' . $size . '.' . $extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getBasename(string $path): string
|
private function getBasename(string $path): string
|
||||||
|
|
|
@ -111,7 +111,7 @@ class Note extends Model
|
||||||
{
|
{
|
||||||
if ($value !== null) {
|
if ($value !== null) {
|
||||||
$normalized = normalizer_normalize($value, Normalizer::FORM_C);
|
$normalized = normalizer_normalize($value, Normalizer::FORM_C);
|
||||||
if ($normalized === '') { //we don’t want to save empty strings to the db
|
if ($normalized === '') { // we don’t want to save empty strings to the db
|
||||||
$normalized = null;
|
$normalized = null;
|
||||||
}
|
}
|
||||||
$this->attributes['note'] = $normalized;
|
$this->attributes['note'] = $normalized;
|
||||||
|
@ -124,7 +124,7 @@ class Note extends Model
|
||||||
public function getNoteAttribute(?string $value): ?string
|
public function getNoteAttribute(?string $value): ?string
|
||||||
{
|
{
|
||||||
if ($value === null && $this->place !== null) {
|
if ($value === null && $this->place !== null) {
|
||||||
$value = '📍: <a href="' . $this->place->longurl . '">' . $this->place->name . '</a>';
|
$value = '📍: <a href="' . $this->place->uri . '">' . $this->place->name . '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// if $value is still null, just return null
|
// if $value is still null, just return null
|
||||||
|
@ -144,17 +144,17 @@ class Note extends Model
|
||||||
*/
|
*/
|
||||||
public function getContentAttribute(): string
|
public function getContentAttribute(): string
|
||||||
{
|
{
|
||||||
$note = $this->note;
|
$note = $this->getRawOriginal('note');
|
||||||
|
|
||||||
foreach ($this->media as $media) {
|
foreach ($this->media as $media) {
|
||||||
if ($media->type === 'image') {
|
if ($media->type === 'image') {
|
||||||
$note .= '<img src="' . $media->url . '" alt="">';
|
$note .= PHP_EOL . '<img src="' . $media->url . '" alt="">';
|
||||||
}
|
}
|
||||||
if ($media->type === 'audio') {
|
if ($media->type === 'audio') {
|
||||||
$note .= '<audio src="' . $media->url . '">';
|
$note .= PHP_EOL . '<audio src="' . $media->url . '">';
|
||||||
}
|
}
|
||||||
if ($media->type === 'video') {
|
if ($media->type === 'video') {
|
||||||
$note .= '<video src="' . $media->url . '">';
|
$note .= PHP_EOL . '<video src="' . $media->url . '">';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,16 +172,11 @@ class Note extends Model
|
||||||
return (string) resolve(Numbers::class)->numto60($this->id);
|
return (string) resolve(Numbers::class)->numto60($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLongurlAttribute(): string
|
public function getUriAttribute(): string
|
||||||
{
|
{
|
||||||
return config('app.url') . '/notes/' . $this->nb60id;
|
return config('app.url') . '/notes/' . $this->nb60id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getShorturlAttribute(): string
|
|
||||||
{
|
|
||||||
return config('url.shorturl') . '/notes/' . $this->nb60id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getIso8601Attribute(): string
|
public function getIso8601Attribute(): string
|
||||||
{
|
{
|
||||||
return $this->updated_at->toISO8601String();
|
return $this->updated_at->toISO8601String();
|
||||||
|
@ -271,7 +266,7 @@ class Note extends Model
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($oEmbed->httpstatus >= 400) {
|
if ($oEmbed->httpstatus >= 400) {
|
||||||
throw new Exception();
|
throw new Exception;
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -388,18 +383,18 @@ class Note extends Model
|
||||||
'mentions_handle' => [
|
'mentions_handle' => [
|
||||||
'prefix' => '@',
|
'prefix' => '@',
|
||||||
'pattern' => '([\w@.])+(\b)',
|
'pattern' => '([\w@.])+(\b)',
|
||||||
'generator' => new MentionGenerator(),
|
'generator' => new MentionGenerator,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$environment = new Environment($config);
|
$environment = new Environment($config);
|
||||||
$environment->addExtension(new CommonMarkCoreExtension());
|
$environment->addExtension(new CommonMarkCoreExtension);
|
||||||
$environment->addExtension(new AutolinkExtension());
|
$environment->addExtension(new AutolinkExtension);
|
||||||
$environment->addExtension(new MentionExtension());
|
$environment->addExtension(new MentionExtension);
|
||||||
$environment->addRenderer(Mention::class, new MentionRenderer());
|
$environment->addRenderer(Mention::class, new MentionRenderer);
|
||||||
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer());
|
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer);
|
||||||
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer());
|
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer);
|
||||||
$markdownConverter = new MarkdownConverter($environment);
|
$markdownConverter = new MarkdownConverter($environment);
|
||||||
|
|
||||||
return $markdownConverter->convert($note)->getContent();
|
return $markdownConverter->convert($note)->getContent();
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Place extends Model
|
||||||
* sin(radians(places.latitude))))";
|
* sin(radians(places.latitude))))";
|
||||||
|
|
||||||
return $query
|
return $query
|
||||||
->select() //pick the columns you want here.
|
->select() // pick the columns you want here.
|
||||||
->selectRaw("{$haversine} AS distance")
|
->selectRaw("{$haversine} AS distance")
|
||||||
->whereRaw("{$haversine} < ?", [$distance]);
|
->whereRaw("{$haversine} < ?", [$distance]);
|
||||||
}
|
}
|
||||||
|
@ -74,24 +74,10 @@ class Place extends Model
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function longurl(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::get(
|
|
||||||
get: fn ($value, $attributes) => config('app.url') . '/places/' . $attributes['slug'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function shorturl(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::get(
|
|
||||||
get: fn ($value, $attributes) => config('url.shorturl') . '/places/' . $attributes['slug'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function uri(): Attribute
|
protected function uri(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
get: fn () => $this->longurl,
|
get: static fn ($value, $attributes) => config('app.url') . '/places/' . $attributes['slug'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class WebMention extends Model
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$authorship = new Authorship();
|
$authorship = new Authorship;
|
||||||
$hCard = $authorship->findAuthor(json_decode($attributes['mf2'], true));
|
$hCard = $authorship->findAuthor(json_decode($attributes['mf2'], true));
|
||||||
|
|
||||||
if ($hCard === false) {
|
if ($hCard === false) {
|
||||||
|
@ -109,13 +109,21 @@ class WebMention extends Model
|
||||||
/**
|
/**
|
||||||
* Create the photo link.
|
* Create the photo link.
|
||||||
*/
|
*/
|
||||||
public function createPhotoLink(string $url): string
|
public function createPhotoLink(string|array $url): string
|
||||||
{
|
{
|
||||||
|
if (is_array($url)) {
|
||||||
|
if (! array_key_exists('value', $url)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $url['value'];
|
||||||
|
}
|
||||||
|
|
||||||
$url = normalize_url($url);
|
$url = normalize_url($url);
|
||||||
$host = parse_url($url, PHP_URL_HOST);
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
|
||||||
if ($host === 'pbs.twimg.com') {
|
if ($host === 'pbs.twimg.com') {
|
||||||
//make sure we use HTTPS, we know twitter supports it
|
// make sure we use HTTPS, we know twitter supports it
|
||||||
return str_replace('http://', 'https://', $url);
|
return str_replace('http://', 'https://', $url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +135,12 @@ class WebMention extends Model
|
||||||
$codebird = resolve(Codebird::class);
|
$codebird = resolve(Codebird::class);
|
||||||
$info = $codebird->users_show(['screen_name' => $username]);
|
$info = $codebird->users_show(['screen_name' => $username]);
|
||||||
$profile_image = $info->profile_image_url_https;
|
$profile_image = $info->profile_image_url_https;
|
||||||
Cache::put($url, $profile_image, 10080); //1 week
|
Cache::put($url, $profile_image, 10080); // 1 week
|
||||||
|
|
||||||
return $profile_image;
|
return $profile_image;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filesystem = new Filesystem();
|
$filesystem = new Filesystem;
|
||||||
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
|
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
|
||||||
return '/assets/profile-images/' . $host . '/image';
|
return '/assets/profile-images/' . $host . '/image';
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,10 @@ use App\Models\Tag;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo Do we need psalm-suppress for these observer methods?
|
|
||||||
*/
|
|
||||||
class NoteObserver
|
class NoteObserver
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Listen to the Note created event.
|
* Listen to the Note created event.=
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function created(Note $note): void
|
public function created(Note $note): void
|
||||||
{
|
{
|
||||||
|
@ -39,9 +34,7 @@ class NoteObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen to the Note updated event.
|
* Listen to the Note updated event.=
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function updated(Note $note): void
|
public function updated(Note $note): void
|
||||||
{
|
{
|
||||||
|
@ -65,9 +58,7 @@ class NoteObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen to the Note deleting event.
|
* Listen to the Note deleting event.=
|
||||||
*
|
|
||||||
* @psalm-suppress PossiblyUnusedMethod
|
|
||||||
*/
|
*/
|
||||||
public function deleting(Note $note): void
|
public function deleting(Note $note): void
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Laravel\Dusk\DuskServiceProvider;
|
|
||||||
use Lcobucci\JWT\Configuration;
|
use Lcobucci\JWT\Configuration;
|
||||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
@ -21,6 +20,14 @@ use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap any application services.
|
* Bootstrap any application services.
|
||||||
*/
|
*/
|
||||||
|
@ -81,9 +88,9 @@ class AppServiceProvider extends ServiceProvider
|
||||||
$this->app->bind('Lcobucci\JWT\Configuration', function () {
|
$this->app->bind('Lcobucci\JWT\Configuration', function () {
|
||||||
$key = InMemory::plainText(config('app.key'));
|
$key = InMemory::plainText(config('app.key'));
|
||||||
|
|
||||||
$config = Configuration::forSymmetricSigner(new Sha256(), $key);
|
$config = Configuration::forSymmetricSigner(new Sha256, $key);
|
||||||
|
|
||||||
$config->setValidationConstraints(new SignedWith(new Sha256(), $key));
|
$config->setValidationConstraints(new SignedWith(new Sha256, $key));
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
});
|
});
|
||||||
|
@ -91,7 +98,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
// Configure HtmlSanitizer
|
// Configure HtmlSanitizer
|
||||||
$this->app->bind(HtmlSanitizer::class, function () {
|
$this->app->bind(HtmlSanitizer::class, function () {
|
||||||
return new HtmlSanitizer(
|
return new HtmlSanitizer(
|
||||||
(new HtmlSanitizerConfig())
|
(new HtmlSanitizerConfig)
|
||||||
->allowSafeElements()
|
->allowSafeElements()
|
||||||
->forceAttribute('a', 'rel', 'noopener nofollow')
|
->forceAttribute('a', 'rel', 'noopener nofollow')
|
||||||
);
|
);
|
||||||
|
@ -132,14 +139,4 @@ class AppServiceProvider extends ServiceProvider
|
||||||
// Turn on Eloquent strict mode when developing
|
// Turn on Eloquent strict mode when developing
|
||||||
Model::shouldBeStrict(! $this->app->isProduction());
|
Model::shouldBeStrict(! $this->app->isProduction());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any application services.
|
|
||||||
*/
|
|
||||||
public function register(): void
|
|
||||||
{
|
|
||||||
if ($this->app->environment('local', 'testing')) {
|
|
||||||
$this->app->register(DuskServiceProvider::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
// use Illuminate\Support\Facades\Gate;
|
|
||||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The model to policy mappings for the application.
|
|
||||||
*
|
|
||||||
* @var array<class-string, class-string>
|
|
||||||
*/
|
|
||||||
protected $policies = [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any authentication / authorization services.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @codeCoverageIgnore
|
|
||||||
*/
|
|
||||||
class BroadcastServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Bootstrap any application services.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
Broadcast::routes();
|
|
||||||
|
|
||||||
require base_path('routes/channels.php');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Auth\Events\Registered;
|
|
||||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The event to listener mappings for the application.
|
|
||||||
*
|
|
||||||
* @var array<class-string, array<int, class-string>>
|
|
||||||
*/
|
|
||||||
protected $listen = [
|
|
||||||
Registered::class => [
|
|
||||||
SendEmailVerificationNotification::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any events for your application.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,12 +3,8 @@
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Laravel\Horizon\Horizon;
|
|
||||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-suppress UnusedClass
|
|
||||||
*/
|
|
||||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -17,12 +13,6 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
|
||||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
|
||||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
|
||||||
|
|
||||||
Horizon::night();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
26
app/Providers/MicropubServiceProvider.php
Normal file
26
app/Providers/MicropubServiceProvider.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Services\Micropub\CardHandler;
|
||||||
|
use App\Services\Micropub\EntryHandler;
|
||||||
|
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class MicropubServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->app->singleton(MicropubHandlerRegistry::class, function () {
|
||||||
|
$registry = new MicropubHandlerRegistry;
|
||||||
|
|
||||||
|
// Register handlers
|
||||||
|
$registry->register('card', new CardHandler);
|
||||||
|
$registry->register('entry', new EntryHandler);
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
|
||||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
class RouteServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The path to your application's "home" route.
|
|
||||||
*
|
|
||||||
* Typically, users are redirected here after authentication.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public const HOME = '/admin';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define your route model bindings, pattern filters, and other route configuration.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
RateLimiter::for('api', function (Request $request) {
|
|
||||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->routes(function () {
|
|
||||||
Route::middleware('api')
|
|
||||||
->prefix('api')
|
|
||||||
->group(base_path('routes/api.php'));
|
|
||||||
|
|
||||||
Route::middleware('web')
|
|
||||||
->group(base_path('routes/web.php'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,13 +6,13 @@ namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
|
|
||||||
class ArticleService extends Service
|
class ArticleService
|
||||||
{
|
{
|
||||||
public function create(array $request, ?string $client = null): Article
|
public function create(array $data): Article
|
||||||
{
|
{
|
||||||
return Article::create([
|
return Article::create([
|
||||||
'title' => $this->getDataByKey($request, 'name'),
|
'title' => $data['name'],
|
||||||
'main' => $this->getDataByKey($request, 'content'),
|
'main' => $data['content'],
|
||||||
'published' => true,
|
'published' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,28 +10,29 @@ use App\Models\Bookmark;
|
||||||
use App\Models\Tag;
|
use App\Models\Tag;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\ClientException;
|
use GuzzleHttp\Exception\ClientException;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class BookmarkService extends Service
|
class BookmarkService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Create a new Bookmark.
|
* Create a new Bookmark.
|
||||||
*/
|
*/
|
||||||
public function create(array $request, ?string $client = null): Bookmark
|
public function create(array $data): Bookmark
|
||||||
{
|
{
|
||||||
if (Arr::get($request, 'properties.bookmark-of.0')) {
|
if (Arr::get($data, 'properties.bookmark-of.0')) {
|
||||||
//micropub request
|
// micropub request
|
||||||
$url = normalize_url(Arr::get($request, 'properties.bookmark-of.0'));
|
$url = normalize_url(Arr::get($data, 'properties.bookmark-of.0'));
|
||||||
$name = Arr::get($request, 'properties.name.0');
|
$name = Arr::get($data, 'properties.name.0');
|
||||||
$content = Arr::get($request, 'properties.content.0');
|
$content = Arr::get($data, 'properties.content.0');
|
||||||
$categories = Arr::get($request, 'properties.category');
|
$categories = Arr::get($data, 'properties.category');
|
||||||
}
|
}
|
||||||
if (Arr::get($request, 'bookmark-of')) {
|
if (Arr::get($data, 'bookmark-of')) {
|
||||||
$url = normalize_url(Arr::get($request, 'bookmark-of'));
|
$url = normalize_url(Arr::get($data, 'bookmark-of'));
|
||||||
$name = Arr::get($request, 'name');
|
$name = Arr::get($data, 'name');
|
||||||
$content = Arr::get($request, 'content');
|
$content = Arr::get($data, 'content');
|
||||||
$categories = Arr::get($request, 'category');
|
$categories = Arr::get($data, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookmark = Bookmark::create([
|
$bookmark = Bookmark::create([
|
||||||
|
@ -54,6 +55,7 @@ class BookmarkService extends Service
|
||||||
* Given a URL, attempt to save it to the Internet Archive.
|
* Given a URL, attempt to save it to the Internet Archive.
|
||||||
*
|
*
|
||||||
* @throws InternetArchiveException
|
* @throws InternetArchiveException
|
||||||
|
* @throws GuzzleException
|
||||||
*/
|
*/
|
||||||
public function getArchiveLink(string $url): string
|
public function getArchiveLink(string $url): string
|
||||||
{
|
{
|
||||||
|
@ -61,8 +63,8 @@ class BookmarkService extends Service
|
||||||
try {
|
try {
|
||||||
$response = $client->request('GET', 'https://web.archive.org/save/' . $url);
|
$response = $client->request('GET', 'https://web.archive.org/save/' . $url);
|
||||||
} catch (ClientException $e) {
|
} catch (ClientException $e) {
|
||||||
//throw an exception to be caught
|
// throw an exception to be caught
|
||||||
throw new InternetArchiveException();
|
throw new InternetArchiveException;
|
||||||
}
|
}
|
||||||
if ($response->hasHeader('Content-Location')) {
|
if ($response->hasHeader('Content-Location')) {
|
||||||
if (Str::startsWith(Arr::get($response->getHeader('Content-Location'), 0), '/web')) {
|
if (Str::startsWith(Arr::get($response->getHeader('Content-Location'), 0), '/web')) {
|
||||||
|
@ -70,7 +72,7 @@ class BookmarkService extends Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//throw an exception to be caught
|
// throw an exception to be caught
|
||||||
throw new InternetArchiveException();
|
throw new InternetArchiveException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,19 @@ use App\Jobs\ProcessLike;
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class LikeService extends Service
|
class LikeService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Create a new Like.
|
* Create a new Like.
|
||||||
*/
|
*/
|
||||||
public function create(array $request, ?string $client = null): Like
|
public function create(array $data): Like
|
||||||
{
|
{
|
||||||
if (Arr::get($request, 'properties.like-of.0')) {
|
if (Arr::get($data, 'properties.like-of.0')) {
|
||||||
//micropub request
|
// micropub request
|
||||||
$url = normalize_url(Arr::get($request, 'properties.like-of.0'));
|
$url = normalize_url(Arr::get($data, 'properties.like-of.0'));
|
||||||
}
|
}
|
||||||
if (Arr::get($request, 'like-of')) {
|
if (Arr::get($data, 'like-of')) {
|
||||||
$url = normalize_url(Arr::get($request, 'like-of'));
|
$url = normalize_url(Arr::get($data, 'like-of'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$like = Like::create(['url' => $url]);
|
$like = Like::create(['url' => $url]);
|
||||||
|
|
34
app/Services/Micropub/CardHandler.php
Normal file
34
app/Services/Micropub/CardHandler.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub;
|
||||||
|
|
||||||
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
|
use App\Services\PlaceService;
|
||||||
|
|
||||||
|
class CardHandler implements MicropubHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws InvalidTokenScopeException
|
||||||
|
*/
|
||||||
|
public function handle(array $data): array
|
||||||
|
{
|
||||||
|
// Handle h-card requests
|
||||||
|
$scopes = $data['token_data']['scope'];
|
||||||
|
if (is_string($scopes)) {
|
||||||
|
$scopes = explode(' ', $scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array('create', $scopes, true)) {
|
||||||
|
throw new InvalidTokenScopeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
$location = resolve(PlaceService::class)->createPlace($data)->uri;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'response' => 'created',
|
||||||
|
'url' => $location,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
41
app/Services/Micropub/EntryHandler.php
Normal file
41
app/Services/Micropub/EntryHandler.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub;
|
||||||
|
|
||||||
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
|
use App\Services\ArticleService;
|
||||||
|
use App\Services\BookmarkService;
|
||||||
|
use App\Services\LikeService;
|
||||||
|
use App\Services\NoteService;
|
||||||
|
|
||||||
|
class EntryHandler implements MicropubHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws InvalidTokenScopeException
|
||||||
|
*/
|
||||||
|
public function handle(array $data)
|
||||||
|
{
|
||||||
|
$scopes = $data['token_data']['scope'];
|
||||||
|
if (is_string($scopes)) {
|
||||||
|
$scopes = explode(' ', $scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array('create', $scopes, true)) {
|
||||||
|
throw new InvalidTokenScopeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
$location = match (true) {
|
||||||
|
isset($data['like-of']) => resolve(LikeService::class)->create($data)->url,
|
||||||
|
isset($data['bookmark-of']) => resolve(BookmarkService::class)->create($data)->uri,
|
||||||
|
isset($data['name']) => resolve(ArticleService::class)->create($data)->link,
|
||||||
|
default => resolve(NoteService::class)->create($data)->uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
'response' => 'created',
|
||||||
|
'url' => $location,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
|
||||||
|
|
||||||
use App\Services\PlaceService;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
|
|
||||||
class HCardService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create a Place from h-card data, return the URL.
|
|
||||||
*/
|
|
||||||
public function process(array $request): string
|
|
||||||
{
|
|
||||||
$data = [];
|
|
||||||
if (Arr::get($request, 'properties.name')) {
|
|
||||||
$data['name'] = Arr::get($request, 'properties.name');
|
|
||||||
$data['description'] = Arr::get($request, 'properties.description');
|
|
||||||
$data['geo'] = Arr::get($request, 'properties.geo');
|
|
||||||
} else {
|
|
||||||
$data['name'] = Arr::get($request, 'name');
|
|
||||||
$data['description'] = Arr::get($request, 'description');
|
|
||||||
$data['geo'] = Arr::get($request, 'geo');
|
|
||||||
$data['latitude'] = Arr::get($request, 'latitude');
|
|
||||||
$data['longitude'] = Arr::get($request, 'longitude');
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(PlaceService::class)->createPlace($data)->longurl;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
|
||||||
|
|
||||||
use App\Services\ArticleService;
|
|
||||||
use App\Services\BookmarkService;
|
|
||||||
use App\Services\LikeService;
|
|
||||||
use App\Services\NoteService;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
|
|
||||||
class HEntryService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create the relevant model from some h-entry data.
|
|
||||||
*/
|
|
||||||
public function process(array $request, ?string $client = null): ?string
|
|
||||||
{
|
|
||||||
if (Arr::get($request, 'properties.like-of') || Arr::get($request, 'like-of')) {
|
|
||||||
return resolve(LikeService::class)->create($request)->longurl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Arr::get($request, 'properties.bookmark-of') || Arr::get($request, 'bookmark-of')) {
|
|
||||||
return resolve(BookmarkService::class)->create($request)->longurl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Arr::get($request, 'properties.name') || Arr::get($request, 'name')) {
|
|
||||||
return resolve(ArticleService::class)->create($request)->longurl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(NoteService::class)->create($request, $client)->longurl;
|
|
||||||
}
|
|
||||||
}
|
|
10
app/Services/Micropub/MicropubHandlerInterface.php
Normal file
10
app/Services/Micropub/MicropubHandlerInterface.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub;
|
||||||
|
|
||||||
|
interface MicropubHandlerInterface
|
||||||
|
{
|
||||||
|
public function handle(array $data);
|
||||||
|
}
|
34
app/Services/Micropub/MicropubHandlerRegistry.php
Normal file
34
app/Services/Micropub/MicropubHandlerRegistry.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub;
|
||||||
|
|
||||||
|
use App\Exceptions\MicropubHandlerException;
|
||||||
|
|
||||||
|
class MicropubHandlerRegistry
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var MicropubHandlerInterface[]
|
||||||
|
*/
|
||||||
|
protected array $handlers = [];
|
||||||
|
|
||||||
|
public function register(string $type, MicropubHandlerInterface $handler): self
|
||||||
|
{
|
||||||
|
$this->handlers[$type] = $handler;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws MicropubHandlerException
|
||||||
|
*/
|
||||||
|
public function getHandler(string $type): MicropubHandlerInterface
|
||||||
|
{
|
||||||
|
if (! isset($this->handlers[$type])) {
|
||||||
|
throw new MicropubHandlerException("No handler registered for '{$type}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->handlers[$type];
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,23 +4,35 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
namespace App\Services\Micropub;
|
||||||
|
|
||||||
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class UpdateService
|
/*
|
||||||
|
* @todo Implement this properly
|
||||||
|
*/
|
||||||
|
class UpdateHandler implements MicropubHandlerInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Process a micropub request to update an entry.
|
* @throws InvalidTokenScopeException
|
||||||
*/
|
*/
|
||||||
public function process(array $request): JsonResponse
|
public function handle(array $data)
|
||||||
{
|
{
|
||||||
$urlPath = parse_url(Arr::get($request, 'url'), PHP_URL_PATH);
|
$scopes = $data['token_data']['scope'];
|
||||||
|
if (is_string($scopes)) {
|
||||||
|
$scopes = explode(' ', $scopes);
|
||||||
|
}
|
||||||
|
|
||||||
//is it a note we are updating?
|
if (! in_array('update', $scopes, true)) {
|
||||||
|
throw new InvalidTokenScopeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlPath = parse_url(Arr::get($data, 'url'), PHP_URL_PATH);
|
||||||
|
|
||||||
|
// is it a note we are updating?
|
||||||
if (mb_substr($urlPath, 1, 5) !== 'notes') {
|
if (mb_substr($urlPath, 1, 5) !== 'notes') {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'invalid',
|
'error' => 'invalid',
|
||||||
|
@ -30,16 +42,16 @@ class UpdateService
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$note = Note::nb60(basename($urlPath))->firstOrFail();
|
$note = Note::nb60(basename($urlPath))->firstOrFail();
|
||||||
} catch (ModelNotFoundException $exception) {
|
} catch (ModelNotFoundException) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
'error_description' => 'No known note with given ID',
|
'error_description' => 'No known note with given ID',
|
||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
//got the note, are we dealing with a “replace” request?
|
// got the note, are we dealing with a “replace” request?
|
||||||
if (Arr::get($request, 'replace')) {
|
if (Arr::get($data, 'replace')) {
|
||||||
foreach (Arr::get($request, 'replace') as $property => $value) {
|
foreach (Arr::get($data, 'replace') as $property => $value) {
|
||||||
if ($property === 'content') {
|
if ($property === 'content') {
|
||||||
$note->note = $value[0];
|
$note->note = $value[0];
|
||||||
}
|
}
|
||||||
|
@ -59,14 +71,14 @@ class UpdateService
|
||||||
}
|
}
|
||||||
$note->save();
|
$note->save();
|
||||||
|
|
||||||
return response()->json([
|
return [
|
||||||
'response' => 'updated',
|
'response' => 'updated',
|
||||||
]);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
//how about “add”
|
// how about “add”
|
||||||
if (Arr::get($request, 'add')) {
|
if (Arr::get($data, 'add')) {
|
||||||
foreach (Arr::get($request, 'add') as $property => $value) {
|
foreach (Arr::get($data, 'add') as $property => $value) {
|
||||||
if ($property === 'syndication') {
|
if ($property === 'syndication') {
|
||||||
foreach ($value as $syndicationURL) {
|
foreach ($value as $syndicationURL) {
|
||||||
if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) {
|
if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) {
|
||||||
|
@ -83,7 +95,7 @@ class UpdateService
|
||||||
if ($property === 'photo') {
|
if ($property === 'photo') {
|
||||||
foreach ($value as $photoURL) {
|
foreach ($value as $photoURL) {
|
||||||
if (Str::startsWith($photoURL, 'https://')) {
|
if (Str::startsWith($photoURL, 'https://')) {
|
||||||
$media = new Media();
|
$media = new Media;
|
||||||
$media->path = $photoURL;
|
$media->path = $photoURL;
|
||||||
$media->type = 'image';
|
$media->type = 'image';
|
||||||
$media->save();
|
$media->save();
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Jobs\SendWebMentions;
|
use App\Jobs\SendWebMentions;
|
||||||
|
use App\Jobs\SyndicateNoteToBluesky;
|
||||||
use App\Jobs\SyndicateNoteToMastodon;
|
use App\Jobs\SyndicateNoteToMastodon;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
|
@ -13,45 +14,52 @@ use App\Models\SyndicationTarget;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class NoteService extends Service
|
class NoteService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Create a new note.
|
* Create a new note.
|
||||||
*/
|
*/
|
||||||
public function create(array $request, ?string $client = null): Note
|
public function create(array $data): Note
|
||||||
{
|
{
|
||||||
|
// Get the content we want to save
|
||||||
|
if (is_string($data['content'])) {
|
||||||
|
$content = $data['content'];
|
||||||
|
} elseif (isset($data['content']['html'])) {
|
||||||
|
$content = $data['content']['html'];
|
||||||
|
} else {
|
||||||
|
$content = null;
|
||||||
|
}
|
||||||
|
|
||||||
$note = Note::create(
|
$note = Note::create(
|
||||||
[
|
[
|
||||||
'note' => $this->getDataByKey($request, 'content'),
|
'note' => $content,
|
||||||
'in_reply_to' => $this->getDataByKey($request, 'in-reply-to'),
|
'in_reply_to' => $data['in-reply-to'],
|
||||||
'client_id' => $client,
|
'client_id' => $data['token_data']['client_id'],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->getPublished($request)) {
|
if ($published = $this->getPublished($data)) {
|
||||||
$note->created_at = $note->updated_at = $this->getPublished($request);
|
$note->created_at = $note->updated_at = $published;
|
||||||
}
|
}
|
||||||
|
|
||||||
$note->location = $this->getLocation($request);
|
$note->location = $this->getLocation($data);
|
||||||
|
|
||||||
if ($this->getCheckin($request)) {
|
if ($this->getCheckin($data)) {
|
||||||
$note->place()->associate($this->getCheckin($request));
|
$note->place()->associate($this->getCheckin($data));
|
||||||
$note->swarm_url = $this->getSwarmUrl($request);
|
$note->swarm_url = $this->getSwarmUrl($data);
|
||||||
}
|
|
||||||
|
|
||||||
$note->instagram_url = $this->getInstagramUrl($request);
|
|
||||||
|
|
||||||
foreach ($this->getMedia($request) as $media) {
|
|
||||||
$note->media()->save($media);
|
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// $note->instagram_url = $this->getInstagramUrl($request);
|
||||||
|
//
|
||||||
|
// foreach ($this->getMedia($request) as $media) {
|
||||||
|
// $note->media()->save($media);
|
||||||
|
// }
|
||||||
|
|
||||||
$note->save();
|
$note->save();
|
||||||
|
|
||||||
dispatch(new SendWebMentions($note));
|
dispatch(new SendWebMentions($note));
|
||||||
|
|
||||||
if (in_array('mastodon', $this->getSyndicationTargets($request), true)) {
|
$this->dispatchSyndicationJobs($note, $data);
|
||||||
dispatch(new SyndicateNoteToMastodon($note));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $note;
|
return $note;
|
||||||
}
|
}
|
||||||
|
@ -59,14 +67,10 @@ class NoteService extends Service
|
||||||
/**
|
/**
|
||||||
* Get the published time from the request to create a new note.
|
* Get the published time from the request to create a new note.
|
||||||
*/
|
*/
|
||||||
private function getPublished(array $request): ?string
|
private function getPublished(array $data): ?string
|
||||||
{
|
{
|
||||||
if (Arr::get($request, 'properties.published.0')) {
|
if ($data['published']) {
|
||||||
return carbon(Arr::get($request, 'properties.published.0'))
|
return carbon($data['published'])->toDateTimeString();
|
||||||
->toDateTimeString();
|
|
||||||
}
|
|
||||||
if (Arr::get($request, 'published')) {
|
|
||||||
return carbon(Arr::get($request, 'published'))->toDateTimeString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -75,12 +79,13 @@ class NoteService extends Service
|
||||||
/**
|
/**
|
||||||
* Get the location data from the request to create a new note.
|
* Get the location data from the request to create a new note.
|
||||||
*/
|
*/
|
||||||
private function getLocation(array $request): ?string
|
private function getLocation(array $data): ?string
|
||||||
{
|
{
|
||||||
$location = Arr::get($request, 'properties.location.0') ?? Arr::get($request, 'location');
|
$location = Arr::get($data, 'location');
|
||||||
|
|
||||||
if (is_string($location) && str_starts_with($location, 'geo:')) {
|
if (is_string($location) && str_starts_with($location, 'geo:')) {
|
||||||
preg_match_all(
|
preg_match_all(
|
||||||
'/([0-9\.\-]+)/',
|
'/([0-9.\-]+)/',
|
||||||
$location,
|
$location,
|
||||||
$matches
|
$matches
|
||||||
);
|
);
|
||||||
|
@ -94,9 +99,9 @@ class NoteService extends Service
|
||||||
/**
|
/**
|
||||||
* Get the checkin data from the request to create a new note. This will be a Place.
|
* Get the checkin data from the request to create a new note. This will be a Place.
|
||||||
*/
|
*/
|
||||||
private function getCheckin(array $request): ?Place
|
private function getCheckin(array $data): ?Place
|
||||||
{
|
{
|
||||||
$location = Arr::get($request, 'location');
|
$location = Arr::get($data, 'location');
|
||||||
if (is_string($location) && Str::startsWith($location, config('app.url'))) {
|
if (is_string($location) && Str::startsWith($location, config('app.url'))) {
|
||||||
return Place::where(
|
return Place::where(
|
||||||
'slug',
|
'slug',
|
||||||
|
@ -108,12 +113,12 @@ class NoteService extends Service
|
||||||
)
|
)
|
||||||
)->first();
|
)->first();
|
||||||
}
|
}
|
||||||
if (Arr::get($request, 'checkin')) {
|
if (Arr::get($data, 'checkin')) {
|
||||||
try {
|
try {
|
||||||
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
|
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
|
||||||
Arr::get($request, 'checkin')
|
Arr::get($data, 'checkin')
|
||||||
);
|
);
|
||||||
} catch (\InvalidArgumentException $e) {
|
} catch (\InvalidArgumentException) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,34 +142,47 @@ class NoteService extends Service
|
||||||
/**
|
/**
|
||||||
* Get the Swarm URL from the syndication data in the request to create a new note.
|
* Get the Swarm URL from the syndication data in the request to create a new note.
|
||||||
*/
|
*/
|
||||||
private function getSwarmUrl(array $request): ?string
|
private function getSwarmUrl(array $data): ?string
|
||||||
{
|
{
|
||||||
if (str_contains(Arr::get($request, 'properties.syndication.0', ''), 'swarmapp')) {
|
$syndication = Arr::get($data, 'syndication');
|
||||||
return Arr::get($request, 'properties.syndication.0');
|
if ($syndication === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($syndication, 'swarmapp')) {
|
||||||
|
return $syndication;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the syndication targets from the request to create a new note.
|
* Dispatch syndication jobs based on the request data.
|
||||||
*/
|
*/
|
||||||
private function getSyndicationTargets(array $request): array
|
private function dispatchSyndicationJobs(Note $note, array $request): void
|
||||||
{
|
{
|
||||||
$syndication = [];
|
// If no syndication targets are specified, return early
|
||||||
$mpSyndicateTo = Arr::get($request, 'mp-syndicate-to') ?? Arr::get($request, 'properties.mp-syndicate-to');
|
if (empty($request['mp-syndicate-to'])) {
|
||||||
$mpSyndicateTo = Arr::wrap($mpSyndicateTo);
|
return;
|
||||||
foreach ($mpSyndicateTo as $uid) {
|
|
||||||
$target = SyndicationTarget::where('uid', $uid)->first();
|
|
||||||
if ($target && $target->service_name === 'Twitter') {
|
|
||||||
$syndication[] = 'twitter';
|
|
||||||
}
|
|
||||||
if ($target && $target->service_name === 'Mastodon') {
|
|
||||||
$syndication[] = 'mastodon';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $syndication;
|
// Get the configured syndication targets
|
||||||
|
$syndicationTargets = SyndicationTarget::all();
|
||||||
|
|
||||||
|
foreach ($syndicationTargets as $target) {
|
||||||
|
// Check if the target is in the request data
|
||||||
|
if (in_array($target->uid, $request['mp-syndicate-to'], true)) {
|
||||||
|
// Dispatch the appropriate job based on the target service name
|
||||||
|
switch ($target->service_name) {
|
||||||
|
case 'Mastodon':
|
||||||
|
dispatch(new SyndicateNoteToMastodon($note));
|
||||||
|
break;
|
||||||
|
case 'Bluesky':
|
||||||
|
dispatch(new SyndicateNoteToBluesky($note));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,8 +14,8 @@ class PlaceService
|
||||||
*/
|
*/
|
||||||
public function createPlace(array $data): Place
|
public function createPlace(array $data): Place
|
||||||
{
|
{
|
||||||
//obviously a place needs a lat/lng, but this could be sent in a geo-url
|
// obviously a place needs a lat/lng, but this could be sent in a geo-url
|
||||||
//if no geo array key, we assume the array already has lat/lng values
|
// if no geo array key, we assume the array already has lat/lng values
|
||||||
if (array_key_exists('geo', $data) && $data['geo'] !== null) {
|
if (array_key_exists('geo', $data) && $data['geo'] !== null) {
|
||||||
preg_match_all(
|
preg_match_all(
|
||||||
'/([0-9\.\-]+)/',
|
'/([0-9\.\-]+)/',
|
||||||
|
@ -25,7 +25,7 @@ class PlaceService
|
||||||
$data['latitude'] = $matches[0][0];
|
$data['latitude'] = $matches[0][0];
|
||||||
$data['longitude'] = $matches[0][1];
|
$data['longitude'] = $matches[0][1];
|
||||||
}
|
}
|
||||||
$place = new Place();
|
$place = new Place;
|
||||||
$place->name = $data['name'];
|
$place->name = $data['name'];
|
||||||
$place->description = $data['description'];
|
$place->description = $data['description'];
|
||||||
$place->latitude = $data['latitude'];
|
$place->latitude = $data['latitude'];
|
||||||
|
@ -40,7 +40,7 @@ class PlaceService
|
||||||
*/
|
*/
|
||||||
public function createPlaceFromCheckin(array $checkin): Place
|
public function createPlaceFromCheckin(array $checkin): Place
|
||||||
{
|
{
|
||||||
//check if the place exists if from swarm
|
// check if the place exists if from swarm
|
||||||
if (Arr::has($checkin, 'properties.url')) {
|
if (Arr::has($checkin, 'properties.url')) {
|
||||||
$place = Place::whereExternalURL(Arr::get($checkin, 'properties.url.0'))->get();
|
$place = Place::whereExternalURL(Arr::get($checkin, 'properties.url.0'))->get();
|
||||||
if (count($place) === 1) {
|
if (count($place) === 1) {
|
||||||
|
@ -53,7 +53,7 @@ class PlaceService
|
||||||
if (Arr::has($checkin, 'properties.latitude') === false) {
|
if (Arr::has($checkin, 'properties.latitude') === false) {
|
||||||
throw new \InvalidArgumentException('Missing required longitude/latitude');
|
throw new \InvalidArgumentException('Missing required longitude/latitude');
|
||||||
}
|
}
|
||||||
$place = new Place();
|
$place = new Place;
|
||||||
$place->name = Arr::get($checkin, 'properties.name.0');
|
$place->name = Arr::get($checkin, 'properties.name.0');
|
||||||
$place->external_urls = Arr::get($checkin, 'properties.url.0');
|
$place->external_urls = Arr::get($checkin, 'properties.url.0');
|
||||||
$place->latitude = Arr::get($checkin, 'properties.latitude.0');
|
$place->latitude = Arr::get($checkin, 'properties.latitude.0');
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
|
|
||||||
abstract class Service
|
|
||||||
{
|
|
||||||
abstract public function create(array $request, ?string $client = null): Model;
|
|
||||||
|
|
||||||
protected function getDataByKey(array $request, string $key): ?string
|
|
||||||
{
|
|
||||||
if (Arr::get($request, "properties.{$key}.0.html")) {
|
|
||||||
return Arr::get($request, "properties.{$key}.0.html");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string(Arr::get($request, "properties.{$key}.0"))) {
|
|
||||||
return Arr::get($request, "properties.{$key}.0");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string(Arr::get($request, "properties.{$key}"))) {
|
|
||||||
return Arr::get($request, "properties.{$key}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Arr::get($request, $key);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ namespace App\Services;
|
||||||
use App\Jobs\AddClientToDatabase;
|
use App\Jobs\AddClientToDatabase;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Lcobucci\JWT\Configuration;
|
use Lcobucci\JWT\Configuration;
|
||||||
use Lcobucci\JWT\Token;
|
|
||||||
|
|
||||||
class TokenService
|
class TokenService
|
||||||
{
|
{
|
||||||
|
@ -19,7 +18,7 @@ class TokenService
|
||||||
$config = resolve(Configuration::class);
|
$config = resolve(Configuration::class);
|
||||||
|
|
||||||
$token = $config->builder()
|
$token = $config->builder()
|
||||||
->issuedAt(new DateTimeImmutable())
|
->issuedAt(new DateTimeImmutable)
|
||||||
->withClaim('client_id', $data['client_id'])
|
->withClaim('client_id', $data['client_id'])
|
||||||
->withClaim('me', $data['me'])
|
->withClaim('me', $data['me'])
|
||||||
->withClaim('scope', $data['scope'])
|
->withClaim('scope', $data['scope'])
|
||||||
|
@ -30,20 +29,4 @@ class TokenService
|
||||||
|
|
||||||
return $token->toString();
|
return $token->toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the token signature is valid.
|
|
||||||
*/
|
|
||||||
public function validateToken(string $bearerToken): Token
|
|
||||||
{
|
|
||||||
$config = resolve('Lcobucci\JWT\Configuration');
|
|
||||||
|
|
||||||
$token = $config->parser()->parse($bearerToken);
|
|
||||||
|
|
||||||
$constraints = $config->validationConstraints();
|
|
||||||
|
|
||||||
$config->validator()->assert($token, ...$constraints);
|
|
||||||
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
51
artisan
51
artisan
|
@ -1,53 +1,16 @@
|
||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
define('LARAVEL_START', microtime(true));
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
/*
|
// Register the Composer autoloader...
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Register The Auto Loader
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Composer provides a convenient, automatically generated class loader
|
|
||||||
| for our application. We just need to utilize it! We'll require it
|
|
||||||
| into the script here so that we do not have to worry about the
|
|
||||||
| loading of any of our classes manually. It's great to relax.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
require __DIR__.'/vendor/autoload.php';
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
// Bootstrap Laravel and handle the command...
|
||||||
|
$status = (require_once __DIR__.'/bootstrap/app.php')
|
||||||
/*
|
->handleCommand(new ArgvInput);
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Run The Artisan Application
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When we run the console application, the current CLI command will be
|
|
||||||
| executed in this console and the response sent back to a terminal
|
|
||||||
| or another output device for the developers. Here goes nothing!
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
|
||||||
|
|
||||||
$status = $kernel->handle(
|
|
||||||
$input = new Symfony\Component\Console\Input\ArgvInput,
|
|
||||||
new Symfony\Component\Console\Output\ConsoleOutput
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Shutdown The Application
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Once Artisan has finished running, we will fire off the shutdown events
|
|
||||||
| so that any final work may be done by the application before we shut
|
|
||||||
| down the process. This is the last thing to happen to the request.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$kernel->terminate($input, $status);
|
|
||||||
|
|
||||||
exit($status);
|
exit($status);
|
||||||
|
|
||||||
|
|
|
@ -1,55 +1,28 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
use App\Http\Middleware\LinkHeadersMiddleware;
|
||||||
|--------------------------------------------------------------------------
|
use Illuminate\Foundation\Application;
|
||||||
| Create The Application
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|--------------------------------------------------------------------------
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
|
||||||
| The first thing we will do is create a new Laravel application instance
|
|
||||||
| which serves as the "glue" for all the components of Laravel, and is
|
|
||||||
| the IoC container for the system binding all of the various parts.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$app = new Illuminate\Foundation\Application(
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
->withRouting(
|
||||||
);
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
/*
|
health: '/up',
|
||||||
|--------------------------------------------------------------------------
|
)
|
||||||
| Bind Important Interfaces
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|--------------------------------------------------------------------------
|
$middleware
|
||||||
|
|
->append(LinkHeadersMiddleware::class)
|
||||||
| Next, we need to bind some important interfaces into the container so
|
->validateCsrfTokens(except: [
|
||||||
| we will be able to resolve them when needed. The kernels serve the
|
'auth', // This is the IndieAuth auth endpoint
|
||||||
| incoming requests to this application from both the web and CLI.
|
'token', // This is the IndieAuth token endpoint
|
||||||
|
|
'api/post',
|
||||||
*/
|
'api/media',
|
||||||
|
'micropub/places',
|
||||||
$app->singleton(
|
'webmention',
|
||||||
Illuminate\Contracts\Http\Kernel::class,
|
]);
|
||||||
App\Http\Kernel::class
|
})
|
||||||
);
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
//
|
||||||
$app->singleton(
|
})->create();
|
||||||
Illuminate\Contracts\Console\Kernel::class,
|
|
||||||
App\Console\Kernel::class
|
|
||||||
);
|
|
||||||
|
|
||||||
$app->singleton(
|
|
||||||
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
|
||||||
App\Exceptions\Handler::class
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Return The Application
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This script returns the application instance. The instance is given to
|
|
||||||
| the calling script so we can separate the building of the instances
|
|
||||||
| from the actual running of the application and sending responses.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
return $app;
|
|
||||||
|
|
7
bootstrap/providers.php
Normal file
7
bootstrap/providers.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\HorizonServiceProvider::class,
|
||||||
|
App\Providers\MicropubServiceProvider::class,
|
||||||
|
];
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
"name": "jonnybarnes/jonnybarnes.uk",
|
"name": "jonnybarnes/jonnybarnes.uk",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"description": "The code for jonnybarnes.uk, based on Laravel 10",
|
"description": "The code for jonnybarnes.uk, based on Laravel 11",
|
||||||
"keywords": ["laravel", "framework", "indieweb"],
|
"keywords": ["laravel", "framework", "indieweb"],
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -10,43 +11,46 @@
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pgsql": "*",
|
"ext-pgsql": "*",
|
||||||
"cviebrock/eloquent-sluggable": "^10.0",
|
"ext-sodium": "*",
|
||||||
|
"cviebrock/eloquent-sluggable": "^12.0",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"indieauth/client": "^1.1",
|
"indieauth/client": "^1.1",
|
||||||
"intervention/image": "^3",
|
"intervention/image": "^3",
|
||||||
"jonnybarnes/indieweb": "~0.2",
|
"jonnybarnes/indieweb": "~0.2",
|
||||||
"jonnybarnes/webmentions-parser": "~0.5",
|
"jonnybarnes/webmentions-parser": "~0.5",
|
||||||
"jublonet/codebird-php": "4.0.0-beta.1",
|
"jublonet/codebird-php": "4.0.0-beta.1",
|
||||||
"laravel/framework": "^10.10",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/horizon": "^5.0",
|
"laravel/horizon": "^5.0",
|
||||||
"laravel/sanctum": "^3.2",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/scout": "^10.1",
|
"laravel/scout": "^10.1",
|
||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^2.8",
|
||||||
"lcobucci/jwt": "^5.0",
|
"lcobucci/jwt": "^5.0",
|
||||||
"league/commonmark": "^2.0",
|
"league/commonmark": "^2.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"mf2/mf2": "~0.3",
|
"mf2/mf2": "~0.3",
|
||||||
|
"phpdocumentor/reflection-docblock": "^5.3",
|
||||||
"spatie/commonmark-highlighter": "^3.0",
|
"spatie/commonmark-highlighter": "^3.0",
|
||||||
"spatie/laravel-ignition": "^2.1",
|
"spatie/laravel-ignition": "^2.1",
|
||||||
"symfony/html-sanitizer": "^7.0",
|
"symfony/html-sanitizer": "^7.0",
|
||||||
"web-auth/webauthn-lib": "^4.7"
|
"symfony/property-access": "^7.0",
|
||||||
|
"symfony/serializer": "^7.0",
|
||||||
|
"web-auth/webauthn-lib": "^5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.0",
|
"barryvdh/laravel-debugbar": "^3.0",
|
||||||
"barryvdh/laravel-ide-helper": "^2.6",
|
"barryvdh/laravel-ide-helper": "^3.0",
|
||||||
"beyondcode/laravel-dump-server": "^1.0",
|
|
||||||
"fakerphp/faker": "^1.9.2",
|
"fakerphp/faker": "^1.9.2",
|
||||||
"laravel/dusk": "^7.0",
|
"laravel/dusk": "^8.0",
|
||||||
|
"laravel/pail": "^1.2",
|
||||||
"laravel/pint": "^1.0",
|
"laravel/pint": "^1.0",
|
||||||
"laravel/sail": "^1.18",
|
"laravel/sail": "^1.18",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.4.4",
|
||||||
"nunomaduro/collision": "^7.0",
|
"nunomaduro/collision": "^8.1",
|
||||||
"openai-php/client": "^0.8.0",
|
"openai-php/client": "^0.10.1",
|
||||||
"phpunit/php-code-coverage": "^10.0",
|
"phpunit/php-code-coverage": "^11.0",
|
||||||
"phpunit/phpunit": "^10.1",
|
"phpunit/phpunit": "^11.0",
|
||||||
"psalm/plugin-laravel": "^2.8",
|
|
||||||
"spatie/laravel-ray": "^1.12",
|
"spatie/laravel-ray": "^1.12",
|
||||||
"vimeo/psalm": "^5.0"
|
"spatie/x-ray": "^1.2"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -75,7 +79,13 @@
|
||||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
],
|
],
|
||||||
"post-create-project-cmd": [
|
"post-create-project-cmd": [
|
||||||
"@php artisan key:generate --ansi"
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
|
|
6720
composer.lock
generated
6720
composer.lock
generated
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue