Compare commits
No commits in common. "0ef822de0c63947a47196d16b9c01c0e13e9f31e" and "07db68882b0f6b8dffd42843a936cbcb3e06b008" have entirely different histories.
0ef822de0c
...
07db68882b
208 changed files with 3263 additions and 3140 deletions
14
.env.dusk.testing
Normal file
14
.env.dusk.testing
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
|
@ -4,6 +4,8 @@ APP_KEY=
|
|||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=https://example.com
|
||||
APP_LONGURL=example.com
|
||||
APP_SHORTURL=examp.le
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
|
70
.env.github
Normal file
70
.env.github
Normal file
|
@ -0,0 +1,70 @@
|
|||
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=
|
4
.gitattributes
vendored
4
.gitattributes
vendored
|
@ -5,3 +5,7 @@
|
|||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
|
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
144
.github/workflows/deploy.yml
vendored
Normal file
144
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,144 @@
|
|||
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 {}
|
65
.github/workflows/phpunit.yml
vendored
Normal file
65
.github/workflows/phpunit.yml
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
name: PHP Unit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
phpunit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: PHPUnit test suite
|
||||
|
||||
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: '8.3'
|
||||
extensions: mbstring, intl, phpredis, imagick
|
||||
coverage: xdebug
|
||||
tools: phpunit
|
||||
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@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-php-8.3-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-php-8.3-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
Normal file
38
.github/workflows/pint.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
name: Laravel Pint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
pint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Laravel Pint
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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@v4
|
||||
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
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
/public/coverage
|
||||
/public/hot
|
||||
/public/files
|
||||
/public/fonts
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
|
|
5
.phpactor.json
Normal file
5
.phpactor.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"$schema": "/Users/jonny/git/phpactor/phpactor.schema.json",
|
||||
"language_server_phpstan.enabled": false,
|
||||
"language_server_psalm.enabled": true
|
||||
}
|
9
.styleci.yml
Normal file
9
.styleci.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
php:
|
||||
preset: laravel
|
||||
disabled:
|
||||
- no_unused_imports
|
||||
finder:
|
||||
not-name:
|
||||
- index.php
|
||||
js: true
|
||||
css: true
|
|
@ -8,6 +8,8 @@ use Illuminate\Support\Facades\DB;
|
|||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class MigratePlaceDataFromPostgis extends Command
|
||||
{
|
||||
|
|
|
@ -9,6 +9,9 @@ use Illuminate\Console\Command;
|
|||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\FileSystem\FileSystem;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ParseCachedWebMentions extends Command
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,9 @@ use App\Jobs\DownloadWebMention;
|
|||
use App\Models\WebMention;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ReDownloadWebMentions extends Command
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class InvalidTokenScopeException extends \Exception {}
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class MicropubHandlerException extends \Exception {}
|
|
@ -9,6 +9,9 @@ use App\Models\Article;
|
|||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ArticlesController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
|
|
|
@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class BioController extends Controller
|
||||
{
|
||||
public function show(): View
|
||||
|
|
|
@ -9,6 +9,9 @@ use App\Models\MicropubClient;
|
|||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ClientsController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,9 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\Support\Arr;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ContactsController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Http\Controllers\Admin;
|
|||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,9 @@ use App\Models\Like;
|
|||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class LikesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,9 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class NotesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,6 @@ use Illuminate\Support\Facades\App;
|
|||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Random\RandomException;
|
||||
use Throwable;
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
|
||||
|
@ -39,6 +38,9 @@ use Webauthn\PublicKeyCredentialRpEntity;
|
|||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class PasskeysController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
|
@ -50,26 +52,22 @@ class PasskeysController extends Controller
|
|||
return view('admin.passkeys.index', compact('passkeys'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function getCreateOptions(Request $request): JsonResponse
|
||||
public function getCreateOptions(): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
// RP Entity i.e. the application
|
||||
$rpEntity = PublicKeyCredentialRpEntity::create(
|
||||
name: config('app.name'),
|
||||
id: config('app.url'),
|
||||
config('app.name'),
|
||||
config('url.longurl'),
|
||||
);
|
||||
|
||||
// User Entity
|
||||
$userEntity = PublicKeyCredentialUserEntity::create(
|
||||
name: $user->name,
|
||||
id: (string) $user->id,
|
||||
displayName: $user->name,
|
||||
$user->name,
|
||||
(string) $user->id,
|
||||
$user->name,
|
||||
);
|
||||
|
||||
// Challenge
|
||||
|
@ -87,38 +85,25 @@ class PasskeysController extends Controller
|
|||
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create(
|
||||
userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
|
||||
requireResidentKey: true,
|
||||
);
|
||||
|
||||
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::create(
|
||||
rp: $rpEntity,
|
||||
user: $userEntity,
|
||||
challenge: $challenge,
|
||||
pubKeyCredParams: $pubKeyCredParams,
|
||||
$options = PublicKeyCredentialCreationOptions::create(
|
||||
$rpEntity,
|
||||
$userEntity,
|
||||
$challenge,
|
||||
$pubKeyCredParams,
|
||||
authenticatorSelection: $authenticatorSelectionCriteria,
|
||||
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
|
||||
);
|
||||
|
||||
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||
$webauthnSerializerFactory = new WebauthnSerializerFactory(
|
||||
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||
);
|
||||
$webauthnSerializer = $webauthnSerializerFactory->create();
|
||||
$publicKeyCredentialCreationOptions = $webauthnSerializer->serialize(
|
||||
data: $publicKeyCredentialCreationOptions,
|
||||
format: 'json'
|
||||
);
|
||||
$options = json_encode($options, JSON_THROW_ON_ERROR);
|
||||
|
||||
$request->session()->put('create_options', $publicKeyCredentialCreationOptions);
|
||||
session(['create_options' => $options]);
|
||||
|
||||
return JsonResponse::fromJsonString($publicKeyCredentialCreationOptions);
|
||||
return JsonResponse::fromJsonString($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @throws WebauthnException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
|
@ -126,17 +111,17 @@ class PasskeysController extends Controller
|
|||
|
||||
$publicKeyCredentialCreationOptionsData = session('create_options');
|
||||
// Unset session data to mitigate replay attacks
|
||||
$request->session()->forget('create_options');
|
||||
session()->forget('create_options');
|
||||
if (empty($publicKeyCredentialCreationOptionsData)) {
|
||||
throw new WebAuthnException('No public key credential request options found');
|
||||
}
|
||||
|
||||
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||
$webauthnSerializerFactory = new WebauthnSerializerFactory(
|
||||
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||
);
|
||||
$webauthnSerializer = $webauthnSerializerFactory->create();
|
||||
|
||||
$webauthnSerializer = (new WebauthnSerializerFactory(
|
||||
$attestationStatementSupportManager
|
||||
))->create();
|
||||
|
||||
$publicKeyCredential = $webauthnSerializer->deserialize(
|
||||
json_encode($request->all(), JSON_THROW_ON_ERROR),
|
||||
|
@ -161,11 +146,11 @@ class PasskeysController extends Controller
|
|||
$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler(
|
||||
ExtensionOutputCheckerHandler::create()
|
||||
);
|
||||
$allowedOrigins = [];
|
||||
$securedRelyingPartyId = [];
|
||||
if (App::environment('local', 'development')) {
|
||||
$allowedOrigins = [config('app.url')];
|
||||
$securedRelyingPartyId = [config('url.longurl')];
|
||||
}
|
||||
$ceremonyStepManagerFactory->setAllowedOrigins($allowedOrigins);
|
||||
$ceremonyStepManagerFactory->setSecuredRelyingPartyId($securedRelyingPartyId);
|
||||
|
||||
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
|
||||
ceremonyStepManager: $ceremonyStepManagerFactory->creationCeremony()
|
||||
|
@ -180,7 +165,8 @@ class PasskeysController extends Controller
|
|||
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
|
||||
authenticatorAttestationResponse: $publicKeyCredential->response,
|
||||
publicKeyCredentialCreationOptions: $publicKeyCredentialCreationOptions,
|
||||
host: config('app.url')
|
||||
request: config('url.longurl'),
|
||||
securedRelyingPartyId: $securedRelyingPartyId,
|
||||
);
|
||||
|
||||
$user->passkey()->create([
|
||||
|
@ -194,37 +180,24 @@ class PasskeysController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function getRequestOptions(Request $request): JsonResponse
|
||||
public function getRequestOptions(): JsonResponse
|
||||
{
|
||||
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::create(
|
||||
challenge: random_bytes(16),
|
||||
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED
|
||||
);
|
||||
|
||||
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
|
||||
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
|
||||
$factory = new WebauthnSerializerFactory(
|
||||
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||
);
|
||||
$serializer = $factory->create();
|
||||
$publicKeyCredentialRequestOptions = $serializer->serialize(data: $publicKeyCredentialRequestOptions, format: 'json');
|
||||
$publicKeyCredentialRequestOptions = json_encode($publicKeyCredentialRequestOptions, JSON_THROW_ON_ERROR);
|
||||
|
||||
$request->session()->put('request_options', $publicKeyCredentialRequestOptions);
|
||||
session(['request_options' => $publicKeyCredentialRequestOptions]);
|
||||
|
||||
return JsonResponse::fromJsonString($publicKeyCredentialRequestOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$requestOptions = session('request_options');
|
||||
$request->session()->forget('request_options');
|
||||
session()->forget('request_options');
|
||||
|
||||
if (empty($requestOptions)) {
|
||||
return response()->json([
|
||||
|
@ -236,10 +209,9 @@ class PasskeysController extends Controller
|
|||
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport);
|
||||
|
||||
$webauthnSerializerFactory = new WebauthnSerializerFactory(
|
||||
attestationStatementSupportManager: $attestationStatementSupportManager
|
||||
);
|
||||
$webauthnSerializer = $webauthnSerializerFactory->create();
|
||||
$webauthnSerializer = (new WebauthnSerializerFactory(
|
||||
$attestationStatementSupportManager
|
||||
))->create();
|
||||
|
||||
$publicKeyCredential = $webauthnSerializer->deserialize(
|
||||
json_encode($request->all(), JSON_THROW_ON_ERROR),
|
||||
|
@ -284,11 +256,11 @@ class PasskeysController extends Controller
|
|||
$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler(
|
||||
ExtensionOutputCheckerHandler::create()
|
||||
);
|
||||
$allowedOrigins = [];
|
||||
$securedRelyingPartyId = [];
|
||||
if (App::environment('local', 'development')) {
|
||||
$allowedOrigins = [config('app.url')];
|
||||
$securedRelyingPartyId = [config('url.longurl')];
|
||||
}
|
||||
$ceremonyStepManagerFactory->setAllowedOrigins($allowedOrigins);
|
||||
$ceremonyStepManagerFactory->setSecuredRelyingPartyId($securedRelyingPartyId);
|
||||
|
||||
$authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create(
|
||||
ceremonyStepManager: $ceremonyStepManagerFactory->requestCeremony()
|
||||
|
@ -302,11 +274,12 @@ class PasskeysController extends Controller
|
|||
|
||||
try {
|
||||
$authenticatorAssertionResponseValidator->check(
|
||||
publicKeyCredentialSource: $publicKeyCredentialSource,
|
||||
credentialId: $publicKeyCredentialSource,
|
||||
authenticatorAssertionResponse: $publicKeyCredential->response,
|
||||
publicKeyCredentialRequestOptions: $publicKeyCredentialRequestOptions,
|
||||
host: config('app.url'),
|
||||
request: config('url.longurl'),
|
||||
userHandle: null,
|
||||
securedRelyingPartyId: $securedRelyingPartyId,
|
||||
);
|
||||
} catch (Throwable) {
|
||||
return response()->json([
|
||||
|
|
|
@ -10,6 +10,9 @@ use App\Services\PlaceService;
|
|||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class PlacesController extends Controller
|
||||
{
|
||||
protected PlaceService $placeService;
|
||||
|
|
|
@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class SyndicationTargetsController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\View\View;
|
||||
use Jonnybarnes\IndieWeb\Numbers;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ArticlesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,9 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Http\Controllers;
|
|||
use App\Models\Bookmark;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class BookmarksController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,9 @@ use App\Models\Contact;
|
|||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ContactsController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,9 @@ use App\Models\Note;
|
|||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class FeedsController extends Controller
|
||||
{
|
||||
/**
|
||||
|
@ -119,8 +122,8 @@ class FeedsController extends Controller
|
|||
|
||||
foreach ($notes as $key => $note) {
|
||||
$data['items'][$key] = [
|
||||
'id' => $note->uri,
|
||||
'url' => $note->uri,
|
||||
'id' => $note->longurl,
|
||||
'url' => $note->longurl,
|
||||
'content_text' => $note->content,
|
||||
'date_published' => $note->created_at->tz('UTC')->toRfc3339String(),
|
||||
'date_modified' => $note->updated_at->tz('UTC')->toRfc3339String(),
|
||||
|
@ -161,7 +164,7 @@ class FeedsController extends Controller
|
|||
'author' => [
|
||||
'type' => 'card',
|
||||
'name' => config('user.display_name'),
|
||||
'url' => config('app.url'),
|
||||
'url' => config('url.longurl'),
|
||||
],
|
||||
'children' => $items,
|
||||
], 200, [
|
||||
|
@ -180,8 +183,8 @@ class FeedsController extends Controller
|
|||
$items[] = [
|
||||
'type' => 'entry',
|
||||
'published' => $note->created_at,
|
||||
'uid' => $note->uri,
|
||||
'url' => $note->uri,
|
||||
'uid' => $note->longurl,
|
||||
'url' => $note->longurl,
|
||||
'content' => [
|
||||
'text' => $note->getRawOriginal('note'),
|
||||
'html' => $note->note,
|
||||
|
@ -197,7 +200,7 @@ class FeedsController extends Controller
|
|||
'author' => [
|
||||
'type' => 'card',
|
||||
'name' => config('user.display_name'),
|
||||
'url' => config('app.url'),
|
||||
'url' => config('url.longurl'),
|
||||
],
|
||||
'children' => $items,
|
||||
], 200, [
|
||||
|
|
|
@ -10,6 +10,9 @@ use App\Models\Note;
|
|||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class FrontPageController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Http\Controllers;
|
|||
use App\Models\Like;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class LikesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -4,73 +4,123 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Exceptions\MicropubHandlerException;
|
||||
use App\Http\Requests\MicropubRequest;
|
||||
use App\Http\Responses\MicropubResponses;
|
||||
use App\Models\Place;
|
||||
use App\Models\SyndicationTarget;
|
||||
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||
use App\Services\Micropub\HCardService;
|
||||
use App\Services\Micropub\HEntryService;
|
||||
use App\Services\Micropub\UpdateService;
|
||||
use App\Services\TokenService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\Encoding\CannotDecodeContent;
|
||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class MicropubController extends Controller
|
||||
{
|
||||
protected MicropubHandlerRegistry $handlerRegistry;
|
||||
protected TokenService $tokenService;
|
||||
|
||||
public function __construct(MicropubHandlerRegistry $handlerRegistry)
|
||||
{
|
||||
$this->handlerRegistry = $handlerRegistry;
|
||||
protected HEntryService $hentryService;
|
||||
|
||||
protected HCardService $hcardService;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a POST request to the micropub endpoint.
|
||||
*
|
||||
* 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.
|
||||
* This function receives an API request, verifies the authenticity
|
||||
* then passes over the info to the relevant Service class.
|
||||
*/
|
||||
public function post(MicropubRequest $request): JsonResponse
|
||||
public function post(Request $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 {
|
||||
$handler = $this->handlerRegistry->getHandler($type);
|
||||
$result = $handler->handle($request->getMicropubData());
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
// Return appropriate response based on the handler result
|
||||
return response()->json([
|
||||
'response' => $result['response'],
|
||||
'location' => $result['url'] ?? null,
|
||||
], 201)->header('Location', $result['url']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => $e->getMessage(),
|
||||
], 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);
|
||||
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')) {
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
||||
if (! in_array('create', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
}
|
||||
$location = $this->hentryService->process($request->all(), $this->getCLientId());
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $location,
|
||||
], 201)->header('Location', $location);
|
||||
}
|
||||
|
||||
if ($request->input('h') === 'card' || $request->input('type.0') === 'h-card') {
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
if (! in_array('create', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
}
|
||||
$location = $this->hcardService->process($request->all());
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $location,
|
||||
], 201)->header('Location', $location);
|
||||
}
|
||||
|
||||
if ($request->input('action') === 'update') {
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
if (! in_array('update', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
}
|
||||
|
||||
return $this->updateService->process($request->all());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error_description' => 'unsupported_request_type',
|
||||
], 500);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,6 +133,12 @@ class MicropubController extends Controller
|
|||
*/
|
||||
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') {
|
||||
return response()->json([
|
||||
'syndicate-to' => SyndicationTarget::all(),
|
||||
|
@ -114,17 +170,36 @@ class MicropubController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
// the default response is just to return the token data
|
||||
/** @var Token $tokenData */
|
||||
$tokenData = $request->input('token_data');
|
||||
|
||||
// default response is just to return the token data
|
||||
return response()->json([
|
||||
'response' => 'token',
|
||||
'token' => [
|
||||
'me' => $tokenData['me'],
|
||||
'scope' => $tokenData['scope'],
|
||||
'client_id' => $tokenData['client_id'],
|
||||
'me' => $tokenData->claims()->get('me'),
|
||||
'scope' => $tokenData->claims()->get('scope'),
|
||||
'client_id' => $tokenData->claims()->get('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,8 +7,10 @@ namespace App\Http\Controllers;
|
|||
use App\Http\Responses\MicropubResponses;
|
||||
use App\Jobs\ProcessMedia;
|
||||
use App\Models\Media;
|
||||
use App\Services\TokenService;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
@ -16,20 +18,46 @@ use Illuminate\Http\UploadedFile;
|
|||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class MicropubMediaController extends Controller
|
||||
{
|
||||
protected TokenService $tokenService;
|
||||
|
||||
public function __construct(TokenService $tokenService)
|
||||
{
|
||||
$this->tokenService = $tokenService;
|
||||
}
|
||||
|
||||
public function getHandler(Request $request): JsonResponse
|
||||
{
|
||||
$tokenData = $request->input('token_data');
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
$scopes = $tokenData['scope'];
|
||||
return $micropubResponses->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($tokenData->claims()->has('scope') === false) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->tokenHasNoScopeResponse();
|
||||
}
|
||||
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
if (! in_array('create', $scopes, true)) {
|
||||
return (new MicropubResponses)->insufficientScopeResponse();
|
||||
if (! in_array('create', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
}
|
||||
|
||||
if ($request->input('q') === 'last') {
|
||||
|
@ -80,14 +108,28 @@ class MicropubMediaController extends Controller
|
|||
*/
|
||||
public function media(Request $request): JsonResponse
|
||||
{
|
||||
$tokenData = $request->input('token_data');
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
$scopes = $tokenData['scope'];
|
||||
return $micropubResponses->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($tokenData->claims()->has('scope') === false) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->tokenHasNoScopeResponse();
|
||||
}
|
||||
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
if (! in_array('create', $scopes, true)) {
|
||||
return (new MicropubResponses)->insufficientScopeResponse();
|
||||
if (! in_array('create', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
}
|
||||
|
||||
if ($request->hasFile('file') === false) {
|
||||
|
@ -122,7 +164,7 @@ class MicropubMediaController extends Controller
|
|||
}
|
||||
|
||||
$media = Media::create([
|
||||
'token' => $request->input('access_token'),
|
||||
'token' => $request->bearerToken(),
|
||||
'path' => $filename,
|
||||
'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()),
|
||||
'image_widths' => $width,
|
||||
|
|
|
@ -14,6 +14,8 @@ use Jonnybarnes\IndieWeb\Numbers;
|
|||
|
||||
/**
|
||||
* @todo Need to sort out Twitter and webmentions!
|
||||
*
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class NotesController extends Controller
|
||||
{
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace App\Http\Controllers;
|
|||
use App\Models\Place;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class PlacesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,9 @@ use App\Models\Note;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function search(Request $request): View
|
||||
|
|
55
app/Http/Controllers/ShortURLsController.php
Normal file
55
app/Http/Controllers/ShortURLsController.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,9 @@ use Illuminate\Http\Response;
|
|||
use Illuminate\View\View;
|
||||
use Jonnybarnes\IndieWeb\Numbers;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class WebMentionsController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,8 @@ class CorsHeaders
|
|||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
|
|
|
@ -10,6 +10,8 @@ class LinkHeadersMiddleware
|
|||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
|
|
|
@ -14,6 +14,8 @@ class LocalhostSessionMiddleware
|
|||
* Whilst we are developing locally, automatically log in as
|
||||
* `['me' => config('app.url')]` as I can’t manually log in as
|
||||
* a .localhost domain.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<?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,6 +13,8 @@ class MyAuthMiddleware
|
|||
{
|
||||
/**
|
||||
* Check the user is logged in.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
|
|
|
@ -10,6 +10,8 @@ class ValidateSignature extends Middleware
|
|||
* The names of the query string parameters that should be ignored.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedProperty
|
||||
*/
|
||||
protected $except = [
|
||||
// 'fbclid',
|
||||
|
|
|
@ -4,14 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Responses\MicropubResponses;
|
||||
use Closure;
|
||||
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;
|
||||
|
||||
class VerifyMicropubToken
|
||||
|
@ -19,63 +13,24 @@ class VerifyMicropubToken
|
|||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$rawToken = null;
|
||||
|
||||
if ($request->input('access_token')) {
|
||||
$rawToken = $request->input('access_token');
|
||||
} elseif ($request->bearerToken()) {
|
||||
$rawToken = $request->bearerToken();
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (! $rawToken) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'unauthorized',
|
||||
'error_description' => 'No access token was provided in the request',
|
||||
], 401);
|
||||
if ($request->bearerToken()) {
|
||||
return $next($request->merge([
|
||||
'access_token' => $request->bearerToken(),
|
||||
]));
|
||||
}
|
||||
|
||||
try {
|
||||
$tokenData = $this->validateToken($rawToken);
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
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;
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'unauthorized',
|
||||
'error_description' => 'No access token was provided in the request',
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,7 +53,7 @@ class ProcessWebMention implements ShouldQueue
|
|||
// check webmention still references target
|
||||
// we try each type of mention (reply/like/repost)
|
||||
if ($webmention->type === 'in-reply-to') {
|
||||
if ($parser->checkInReplyTo($microformats, $this->note->uri) === false) {
|
||||
if ($parser->checkInReplyTo($microformats, $this->note->longurl) === false) {
|
||||
// it doesn’t so delete
|
||||
$webmention->delete();
|
||||
|
||||
|
@ -67,7 +67,7 @@ class ProcessWebMention implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
if ($webmention->type === 'like-of') {
|
||||
if ($parser->checkLikeOf($microformats, $this->note->uri) === false) {
|
||||
if ($parser->checkLikeOf($microformats, $this->note->longurl) === false) {
|
||||
// it doesn’t so delete
|
||||
$webmention->delete();
|
||||
|
||||
|
@ -75,7 +75,7 @@ class ProcessWebMention implements ShouldQueue
|
|||
} // note we don’t need to do anything if it still is a like
|
||||
}
|
||||
if ($webmention->type === 'repost-of') {
|
||||
if ($parser->checkRepostOf($microformats, $this->note->uri) === false) {
|
||||
if ($parser->checkRepostOf($microformats, $this->note->longurl) === false) {
|
||||
// it doesn’t so delete
|
||||
$webmention->delete();
|
||||
|
||||
|
@ -89,7 +89,7 @@ class ProcessWebMention implements ShouldQueue
|
|||
$type = $parser->getMentionType($microformats); // throw error here?
|
||||
dispatch(new SaveProfileImage($microformats));
|
||||
$webmention->source = $this->source;
|
||||
$webmention->target = $this->note->uri;
|
||||
$webmention->target = $this->note->longurl;
|
||||
$webmention->commentable_id = $this->note->id;
|
||||
$webmention->commentable_type = Note::class;
|
||||
$webmention->type = $type;
|
||||
|
|
|
@ -45,7 +45,7 @@ class SendWebMentions implements ShouldQueue
|
|||
$guzzle = resolve(Client::class);
|
||||
$guzzle->post($endpoint, [
|
||||
'form_params' => [
|
||||
'source' => $this->note->uri,
|
||||
'source' => $this->note->longurl,
|
||||
'target' => $url,
|
||||
],
|
||||
]);
|
||||
|
@ -61,7 +61,7 @@ class SendWebMentions implements ShouldQueue
|
|||
public function discoverWebmentionEndpoint(string $url): ?string
|
||||
{
|
||||
// let’s not send webmentions to myself
|
||||
if (parse_url($url, PHP_URL_HOST) === parse_url(config('app.url'), PHP_URL_HOST)) {
|
||||
if (parse_url($url, PHP_URL_HOST) === config('url.longurl')) {
|
||||
return null;
|
||||
}
|
||||
if (Str::startsWith($url, '/notes/tagged/')) {
|
||||
|
|
|
@ -26,7 +26,7 @@ class Bookmark extends Model
|
|||
return $this->belongsToMany('App\Models\Tag');
|
||||
}
|
||||
|
||||
protected function local_uri(): Attribute
|
||||
protected function longurl(): Attribute
|
||||
{
|
||||
return Attribute::get(
|
||||
get: fn () => config('app.url') . '/bookmarks/' . $this->id,
|
||||
|
|
|
@ -124,7 +124,7 @@ class Note extends Model
|
|||
public function getNoteAttribute(?string $value): ?string
|
||||
{
|
||||
if ($value === null && $this->place !== null) {
|
||||
$value = '📍: <a href="' . $this->place->uri . '">' . $this->place->name . '</a>';
|
||||
$value = '📍: <a href="' . $this->place->longurl . '">' . $this->place->name . '</a>';
|
||||
}
|
||||
|
||||
// if $value is still null, just return null
|
||||
|
@ -172,11 +172,16 @@ class Note extends Model
|
|||
return (string) resolve(Numbers::class)->numto60($this->id);
|
||||
}
|
||||
|
||||
public function getUriAttribute(): string
|
||||
public function getLongurlAttribute(): string
|
||||
{
|
||||
return config('app.url') . '/notes/' . $this->nb60id;
|
||||
}
|
||||
|
||||
public function getShorturlAttribute(): string
|
||||
{
|
||||
return config('url.shorturl') . '/notes/' . $this->nb60id;
|
||||
}
|
||||
|
||||
public function getIso8601Attribute(): string
|
||||
{
|
||||
return $this->updated_at->toISO8601String();
|
||||
|
|
|
@ -74,10 +74,24 @@ 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
|
||||
{
|
||||
return Attribute::get(
|
||||
get: static fn ($value, $attributes) => config('app.url') . '/places/' . $attributes['slug'],
|
||||
get: fn () => $this->longurl,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,15 @@ use App\Models\Tag;
|
|||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @todo Do we need psalm-suppress for these observer methods?
|
||||
*/
|
||||
class NoteObserver
|
||||
{
|
||||
/**
|
||||
* Listen to the Note created event.=
|
||||
* Listen to the Note created event.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function created(Note $note): void
|
||||
{
|
||||
|
@ -34,7 +39,9 @@ class NoteObserver
|
|||
}
|
||||
|
||||
/**
|
||||
* Listen to the Note updated event.=
|
||||
* Listen to the Note updated event.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function updated(Note $note): void
|
||||
{
|
||||
|
@ -58,7 +65,9 @@ class NoteObserver
|
|||
}
|
||||
|
||||
/**
|
||||
* Listen to the Note deleting event.=
|
||||
* Listen to the Note deleting event.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function deleting(Note $note): void
|
||||
{
|
||||
|
|
|
@ -5,6 +5,9 @@ namespace App\Providers;
|
|||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<?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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,13 +6,13 @@ namespace App\Services;
|
|||
|
||||
use App\Models\Article;
|
||||
|
||||
class ArticleService
|
||||
class ArticleService extends Service
|
||||
{
|
||||
public function create(array $data): Article
|
||||
public function create(array $request, ?string $client = null): Article
|
||||
{
|
||||
return Article::create([
|
||||
'title' => $data['name'],
|
||||
'main' => $data['content'],
|
||||
'title' => $this->getDataByKey($request, 'name'),
|
||||
'main' => $this->getDataByKey($request, 'content'),
|
||||
'published' => true,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -10,29 +10,28 @@ use App\Models\Bookmark;
|
|||
use App\Models\Tag;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BookmarkService
|
||||
class BookmarkService extends Service
|
||||
{
|
||||
/**
|
||||
* Create a new Bookmark.
|
||||
*/
|
||||
public function create(array $data): Bookmark
|
||||
public function create(array $request, ?string $client = null): Bookmark
|
||||
{
|
||||
if (Arr::get($data, 'properties.bookmark-of.0')) {
|
||||
if (Arr::get($request, 'properties.bookmark-of.0')) {
|
||||
// micropub request
|
||||
$url = normalize_url(Arr::get($data, 'properties.bookmark-of.0'));
|
||||
$name = Arr::get($data, 'properties.name.0');
|
||||
$content = Arr::get($data, 'properties.content.0');
|
||||
$categories = Arr::get($data, 'properties.category');
|
||||
$url = normalize_url(Arr::get($request, 'properties.bookmark-of.0'));
|
||||
$name = Arr::get($request, 'properties.name.0');
|
||||
$content = Arr::get($request, 'properties.content.0');
|
||||
$categories = Arr::get($request, 'properties.category');
|
||||
}
|
||||
if (Arr::get($data, 'bookmark-of')) {
|
||||
$url = normalize_url(Arr::get($data, 'bookmark-of'));
|
||||
$name = Arr::get($data, 'name');
|
||||
$content = Arr::get($data, 'content');
|
||||
$categories = Arr::get($data, 'category');
|
||||
if (Arr::get($request, 'bookmark-of')) {
|
||||
$url = normalize_url(Arr::get($request, 'bookmark-of'));
|
||||
$name = Arr::get($request, 'name');
|
||||
$content = Arr::get($request, 'content');
|
||||
$categories = Arr::get($request, 'category');
|
||||
}
|
||||
|
||||
$bookmark = Bookmark::create([
|
||||
|
@ -55,7 +54,6 @@ class BookmarkService
|
|||
* Given a URL, attempt to save it to the Internet Archive.
|
||||
*
|
||||
* @throws InternetArchiveException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getArchiveLink(string $url): string
|
||||
{
|
||||
|
|
|
@ -8,19 +8,19 @@ use App\Jobs\ProcessLike;
|
|||
use App\Models\Like;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class LikeService
|
||||
class LikeService extends Service
|
||||
{
|
||||
/**
|
||||
* Create a new Like.
|
||||
*/
|
||||
public function create(array $data): Like
|
||||
public function create(array $request, ?string $client = null): Like
|
||||
{
|
||||
if (Arr::get($data, 'properties.like-of.0')) {
|
||||
if (Arr::get($request, 'properties.like-of.0')) {
|
||||
// micropub request
|
||||
$url = normalize_url(Arr::get($data, 'properties.like-of.0'));
|
||||
$url = normalize_url(Arr::get($request, 'properties.like-of.0'));
|
||||
}
|
||||
if (Arr::get($data, 'like-of')) {
|
||||
$url = normalize_url(Arr::get($data, 'like-of'));
|
||||
if (Arr::get($request, 'like-of')) {
|
||||
$url = normalize_url(Arr::get($request, 'like-of'));
|
||||
}
|
||||
|
||||
$like = Like::create(['url' => $url]);
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
32
app/Services/Micropub/HCardService.php
Normal file
32
app/Services/Micropub/HCardService.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
34
app/Services/Micropub/HEntryService.php
Normal file
34
app/Services/Micropub/HEntryService.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub;
|
||||
|
||||
interface MicropubHandlerInterface
|
||||
{
|
||||
public function handle(array $data);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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,33 +4,21 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services\Micropub;
|
||||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Models\Media;
|
||||
use App\Models\Note;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/*
|
||||
* @todo Implement this properly
|
||||
*/
|
||||
class UpdateHandler implements MicropubHandlerInterface
|
||||
class UpdateService
|
||||
{
|
||||
/**
|
||||
* @throws InvalidTokenScopeException
|
||||
* Process a micropub request to update an entry.
|
||||
*/
|
||||
public function handle(array $data)
|
||||
public function process(array $request): JsonResponse
|
||||
{
|
||||
$scopes = $data['token_data']['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
||||
if (! in_array('update', $scopes, true)) {
|
||||
throw new InvalidTokenScopeException;
|
||||
}
|
||||
|
||||
$urlPath = parse_url(Arr::get($data, 'url'), PHP_URL_PATH);
|
||||
$urlPath = parse_url(Arr::get($request, 'url'), PHP_URL_PATH);
|
||||
|
||||
// is it a note we are updating?
|
||||
if (mb_substr($urlPath, 1, 5) !== 'notes') {
|
||||
|
@ -42,7 +30,7 @@ class UpdateHandler implements MicropubHandlerInterface
|
|||
|
||||
try {
|
||||
$note = Note::nb60(basename($urlPath))->firstOrFail();
|
||||
} catch (ModelNotFoundException) {
|
||||
} catch (ModelNotFoundException $exception) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'No known note with given ID',
|
||||
|
@ -50,8 +38,8 @@ class UpdateHandler implements MicropubHandlerInterface
|
|||
}
|
||||
|
||||
// got the note, are we dealing with a “replace” request?
|
||||
if (Arr::get($data, 'replace')) {
|
||||
foreach (Arr::get($data, 'replace') as $property => $value) {
|
||||
if (Arr::get($request, 'replace')) {
|
||||
foreach (Arr::get($request, 'replace') as $property => $value) {
|
||||
if ($property === 'content') {
|
||||
$note->note = $value[0];
|
||||
}
|
||||
|
@ -71,14 +59,14 @@ class UpdateHandler implements MicropubHandlerInterface
|
|||
}
|
||||
$note->save();
|
||||
|
||||
return [
|
||||
return response()->json([
|
||||
'response' => 'updated',
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
// how about “add”
|
||||
if (Arr::get($data, 'add')) {
|
||||
foreach (Arr::get($data, 'add') as $property => $value) {
|
||||
if (Arr::get($request, 'add')) {
|
||||
foreach (Arr::get($request, 'add') as $property => $value) {
|
||||
if ($property === 'syndication') {
|
||||
foreach ($value as $syndicationURL) {
|
||||
if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) {
|
|
@ -14,52 +14,49 @@ use App\Models\SyndicationTarget;
|
|||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class NoteService
|
||||
class NoteService extends Service
|
||||
{
|
||||
/**
|
||||
* Create a new note.
|
||||
*/
|
||||
public function create(array $data): Note
|
||||
public function create(array $request, ?string $client = null): 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' => $content,
|
||||
'in_reply_to' => $data['in-reply-to'],
|
||||
'client_id' => $data['token_data']['client_id'],
|
||||
'note' => $this->getDataByKey($request, 'content'),
|
||||
'in_reply_to' => $this->getDataByKey($request, 'in-reply-to'),
|
||||
'client_id' => $client,
|
||||
]
|
||||
);
|
||||
|
||||
if ($published = $this->getPublished($data)) {
|
||||
$note->created_at = $note->updated_at = $published;
|
||||
if ($this->getPublished($request)) {
|
||||
$note->created_at = $note->updated_at = $this->getPublished($request);
|
||||
}
|
||||
|
||||
$note->location = $this->getLocation($data);
|
||||
$note->location = $this->getLocation($request);
|
||||
|
||||
if ($this->getCheckin($data)) {
|
||||
$note->place()->associate($this->getCheckin($data));
|
||||
$note->swarm_url = $this->getSwarmUrl($data);
|
||||
if ($this->getCheckin($request)) {
|
||||
$note->place()->associate($this->getCheckin($request));
|
||||
$note->swarm_url = $this->getSwarmUrl($request);
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
dispatch(new SendWebMentions($note));
|
||||
|
||||
$this->dispatchSyndicationJobs($note, $data);
|
||||
if (in_array('mastodon', $this->getSyndicationTargets($request), true)) {
|
||||
dispatch(new SyndicateNoteToMastodon($note));
|
||||
}
|
||||
|
||||
if (in_array('bluesky', $this->getSyndicationTargets($request), true)) {
|
||||
dispatch(new SyndicateNoteToBluesky($note));
|
||||
}
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
@ -67,10 +64,14 @@ class NoteService
|
|||
/**
|
||||
* Get the published time from the request to create a new note.
|
||||
*/
|
||||
private function getPublished(array $data): ?string
|
||||
private function getPublished(array $request): ?string
|
||||
{
|
||||
if ($data['published']) {
|
||||
return carbon($data['published'])->toDateTimeString();
|
||||
if (Arr::get($request, 'properties.published.0')) {
|
||||
return carbon(Arr::get($request, 'properties.published.0'))
|
||||
->toDateTimeString();
|
||||
}
|
||||
if (Arr::get($request, 'published')) {
|
||||
return carbon(Arr::get($request, 'published'))->toDateTimeString();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -79,13 +80,12 @@ class NoteService
|
|||
/**
|
||||
* Get the location data from the request to create a new note.
|
||||
*/
|
||||
private function getLocation(array $data): ?string
|
||||
private function getLocation(array $request): ?string
|
||||
{
|
||||
$location = Arr::get($data, 'location');
|
||||
|
||||
$location = Arr::get($request, 'properties.location.0') ?? Arr::get($request, 'location');
|
||||
if (is_string($location) && str_starts_with($location, 'geo:')) {
|
||||
preg_match_all(
|
||||
'/([0-9.\-]+)/',
|
||||
'/([0-9\.\-]+)/',
|
||||
$location,
|
||||
$matches
|
||||
);
|
||||
|
@ -99,9 +99,9 @@ class NoteService
|
|||
/**
|
||||
* Get the checkin data from the request to create a new note. This will be a Place.
|
||||
*/
|
||||
private function getCheckin(array $data): ?Place
|
||||
private function getCheckin(array $request): ?Place
|
||||
{
|
||||
$location = Arr::get($data, 'location');
|
||||
$location = Arr::get($request, 'location');
|
||||
if (is_string($location) && Str::startsWith($location, config('app.url'))) {
|
||||
return Place::where(
|
||||
'slug',
|
||||
|
@ -113,12 +113,12 @@ class NoteService
|
|||
)
|
||||
)->first();
|
||||
}
|
||||
if (Arr::get($data, 'checkin')) {
|
||||
if (Arr::get($request, 'checkin')) {
|
||||
try {
|
||||
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
|
||||
Arr::get($data, 'checkin')
|
||||
Arr::get($request, 'checkin')
|
||||
);
|
||||
} catch (\InvalidArgumentException) {
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -142,47 +142,34 @@ class NoteService
|
|||
/**
|
||||
* Get the Swarm URL from the syndication data in the request to create a new note.
|
||||
*/
|
||||
private function getSwarmUrl(array $data): ?string
|
||||
private function getSwarmUrl(array $request): ?string
|
||||
{
|
||||
$syndication = Arr::get($data, 'syndication');
|
||||
if ($syndication === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_contains($syndication, 'swarmapp')) {
|
||||
return $syndication;
|
||||
if (str_contains(Arr::get($request, 'properties.syndication.0', ''), 'swarmapp')) {
|
||||
return Arr::get($request, 'properties.syndication.0');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch syndication jobs based on the request data.
|
||||
* Get the syndication targets from the request to create a new note.
|
||||
*/
|
||||
private function dispatchSyndicationJobs(Note $note, array $request): void
|
||||
private function getSyndicationTargets(array $request): array
|
||||
{
|
||||
// If no syndication targets are specified, return early
|
||||
if (empty($request['mp-syndicate-to'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
$syndication = [];
|
||||
$mpSyndicateTo = Arr::get($request, 'mp-syndicate-to') ?? Arr::get($request, 'properties.mp-syndicate-to');
|
||||
$mpSyndicateTo = Arr::wrap($mpSyndicateTo);
|
||||
foreach ($mpSyndicateTo as $uid) {
|
||||
$target = SyndicationTarget::where('uid', $uid)->first();
|
||||
if ($target && $target->service_name === 'Mastodon') {
|
||||
$syndication[] = 'mastodon';
|
||||
}
|
||||
if ($target && $target->service_name === 'Bluesky') {
|
||||
$syndication[] = 'bluesky';
|
||||
}
|
||||
}
|
||||
|
||||
return $syndication;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
30
app/Services/Service.php
Normal file
30
app/Services/Service.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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,6 +7,7 @@ namespace App\Services;
|
|||
use App\Jobs\AddClientToDatabase;
|
||||
use DateTimeImmutable;
|
||||
use Lcobucci\JWT\Configuration;
|
||||
use Lcobucci\JWT\Token;
|
||||
|
||||
class TokenService
|
||||
{
|
||||
|
@ -29,4 +30,20 @@ class TokenService
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,4 @@
|
|||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\MicropubServiceProvider::class,
|
||||
];
|
||||
|
|
|
@ -49,8 +49,7 @@
|
|||
"openai-php/client": "^0.10.1",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"spatie/laravel-ray": "^1.12",
|
||||
"spatie/x-ray": "^1.2"
|
||||
"spatie/laravel-ray": "^1.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
1266
composer.lock
generated
1266
composer.lock
generated
File diff suppressed because it is too large
Load diff
32
config/url.php
Normal file
32
config/url.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Here we set the long and short URLs our app shall use
|
||||
* You can override these settings in the .env file
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Long URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The long URL for the application
|
||||
|
|
||||
*/
|
||||
|
||||
'longurl' => env('APP_LONGURL', 'longurl.local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Short URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The short URL for the application
|
||||
|
|
||||
*/
|
||||
|
||||
'shorturl' => env('APP_SHORTURL', 'shorturl.local'),
|
||||
|
||||
];
|
|
@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
|
||||
*/
|
||||
class ArticleFactory extends Factory
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Database\Factories;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bio>
|
||||
*/
|
||||
class BioFactory extends Factory
|
||||
|
|
|
@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bookmark>
|
||||
*/
|
||||
class BookmarkFactory extends Factory
|
||||
|
|
|
@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Contact>
|
||||
*/
|
||||
class ContactFactory extends Factory
|
||||
|
|
|
@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Like>
|
||||
*/
|
||||
class LikeFactory extends Factory
|
||||
|
|
|
@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Media>
|
||||
*/
|
||||
class MediaFactory extends Factory
|
||||
|
|
|
@ -6,6 +6,8 @@ use App\Models\MicropubClient;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MicropubClient>
|
||||
*/
|
||||
class MicropubClientFactory extends Factory
|
||||
|
|
|
@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Note>
|
||||
*/
|
||||
class NoteFactory extends Factory
|
||||
|
|
|
@ -6,6 +6,8 @@ use App\Models\Place;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Place>
|
||||
*/
|
||||
class PlaceFactory extends Factory
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Database\Factories;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SyndicationTarget>
|
||||
*/
|
||||
class SyndicationTargetFactory extends Factory
|
||||
|
|
|
@ -6,6 +6,8 @@ use App\Models\Tag;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Tag>
|
||||
*/
|
||||
class TagFactory extends Factory
|
||||
|
|
|
@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
|
|
|
@ -6,6 +6,8 @@ use App\Models\WebMention;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebMention>
|
||||
*/
|
||||
class WebMentionFactory extends Factory
|
||||
|
|
|
@ -11,6 +11,8 @@ class ArticlesTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the articles table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -5,6 +5,9 @@ namespace Database\Seeders;
|
|||
use App\Models\Bio;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class BioSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,8 @@ class BookmarksTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the bookmarks table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -11,6 +11,8 @@ class ClientsTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the clients table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -10,6 +10,8 @@ class ContactsTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the contacts table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -4,6 +4,9 @@ namespace Database\Seeders;
|
|||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,8 @@ class LikesTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the likes table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -14,6 +14,8 @@ class NotesTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the notes table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -9,6 +9,8 @@ class PlacesTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the places table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -9,6 +9,8 @@ class UsersTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the users table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -9,6 +9,8 @@ class WebMentionsTableSeeder extends Seeder
|
|||
{
|
||||
/**
|
||||
* Seed the webmentions table.
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
services:
|
||||
laravel.test:
|
||||
build:
|
||||
context: ./vendor/laravel/sail/runtimes/8.4
|
||||
context: ./vendor/laravel/sail/runtimes/8.3
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
|
@ -26,7 +26,7 @@ services:
|
|||
- pgsql
|
||||
- redis
|
||||
pgsql:
|
||||
image: 'postgres:17'
|
||||
image: 'postgres:16'
|
||||
ports:
|
||||
- '${FORWARD_DB_PORT:-5432}:5432'
|
||||
environment:
|
||||
|
|
1533
package-lock.json
generated
1533
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
@ -7,23 +7,21 @@
|
|||
"license": "CC0-1.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@stylistic/eslint-plugin": "^5.1.0",
|
||||
"esbuild": "^0.25.2",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"eslint": "^9.7.0",
|
||||
"globals": "^16.0.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
"lightningcss-cli": "^1.29.3",
|
||||
"globals": "^15.8.0",
|
||||
"stylelint": "^16.7.0",
|
||||
"stylelint-config-standard": "^38.0.0"
|
||||
"stylelint-config-standard": "^37.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"eslint": "eslint public/assets/js/*.js",
|
||||
"stylelint": "stylelint resources/css/*.css",
|
||||
"stylelint": "stylelint public/assets/css/*.css",
|
||||
"lint": "npm run eslint && npm run stylelint",
|
||||
"lightningcss": "lightningcss --output-dir public/assets/css --sourcemap --bundle --minify resources/css/app.css",
|
||||
"fix-sourcemap": "./scripts/fix-sourcemap.sh",
|
||||
"build-css": "npm run lightningcss && npm run fix-sourcemap",
|
||||
"compress": "./scripts/compress.sh",
|
||||
"build": "npm run lint && npm run compress"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/is-land": "^4.0.0",
|
||||
"@zachleat/snow-fall": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
|
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