Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cc2ff2b2a2 |
|||
|
73b9668088 |
|||
|
e8f2535cd1 |
|||
|
5481956d78 |
|||
|
7ce256baa5 |
|||
|
c3cf597950 |
|||
|
d2bc91794b |
|||
|
a31c218784 |
|||
|
eb35a0aa2d |
|||
|
6727138f43 |
|||
|
4aa93d63bb |
|||
|
77998a963e |
|||
|
568ae78864 |
|||
|
9e9fbdc127 |
|||
|
9532aae37e |
|||
|
e9d8f4c74b |
|||
|
be2bc267ad |
|||
|
a76c84b40f |
|||
|
99f306ede1 |
|||
|
714cd95d79 |
|||
|
6a0bb623fc |
|||
|
30ec2ec0e8 |
|||
|
4ed98c30b2 |
|||
|
faf8e5c1ec |
|||
|
24da24a677 |
|||
|
9d6cf6c815 |
|||
|
9c9a6392c8 |
|||
|
d5706b5f8f |
|||
|
f9f2744fad |
|||
|
31c49ac3fc |
|||
|
287520ad7b |
|||
|
2eab5749ea |
|||
|
e08763c526 |
123 changed files with 3627 additions and 2005 deletions
|
|
@ -78,6 +78,8 @@ SESSION_SAME_SITE=strict
|
||||||
|
|
||||||
LOG_SLACK_WEBHOOK_URL=
|
LOG_SLACK_WEBHOOK_URL=
|
||||||
|
|
||||||
|
BRRR_WEBHOOK_URL=
|
||||||
|
|
||||||
FLARE_KEY=
|
FLARE_KEY=
|
||||||
|
|
||||||
IGNITION_OPEN_AI_KEY=
|
IGNITION_OPEN_AI_KEY=
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Image\ImageException;
|
||||||
|
use Illuminate\Support\Facades\Image;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Intervention\Image\Exceptions\DecoderException;
|
|
||||||
use Intervention\Image\ImageManager;
|
|
||||||
|
|
||||||
class ReprocessMediaImages extends Command
|
class ReprocessMediaImages extends Command
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +16,7 @@ class ReprocessMediaImages extends Command
|
||||||
|
|
||||||
protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling';
|
protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling';
|
||||||
|
|
||||||
public function handle(ImageManager $manager): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$media = Media::where('type', 'image')
|
$media = Media::where('type', 'image')
|
||||||
->whereNotNull('image_widths')
|
->whereNotNull('image_widths')
|
||||||
|
|
@ -44,10 +44,10 @@ class ReprocessMediaImages extends Command
|
||||||
|
|
||||||
$this->info("Processing: {$path}");
|
$this->info("Processing: {$path}");
|
||||||
|
|
||||||
|
$image = Image::fromStorage($path, 'public');
|
||||||
try {
|
try {
|
||||||
$file = Storage::disk('public')->get($path);
|
$image->width();
|
||||||
$image = $manager->read($file);
|
} catch (ImageException) {
|
||||||
} catch (DecoderException) {
|
|
||||||
$this->warn(' Could not decode image, skipping.');
|
$this->warn(' Could not decode image, skipping.');
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -57,11 +57,8 @@ class ReprocessMediaImages extends Command
|
||||||
$extension = array_pop($filenameParts);
|
$extension = array_pop($filenameParts);
|
||||||
$basename = trim(implode('.', $filenameParts), '.');
|
$basename = trim(implode('.', $filenameParts), '.');
|
||||||
|
|
||||||
$medium = $image->scale(width: 1000);
|
Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
|
||||||
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode());
|
Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
|
||||||
|
|
||||||
$small = $image->scale(width: 500);
|
|
||||||
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
|
|
||||||
|
|
||||||
$this->info(' Done.');
|
$this->info(' Done.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
app/Http/Controllers/AboutPageController.php
Normal file
16
app/Http/Controllers/AboutPageController.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\About;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AboutPageController extends Controller
|
||||||
|
{
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
return view('about', [
|
||||||
|
'about' => About::first()?->content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Http/Controllers/Admin/AboutController.php
Normal file
32
app/Http/Controllers/Admin/AboutController.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\About;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AboutController extends Controller
|
||||||
|
{
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
$about = About::first();
|
||||||
|
|
||||||
|
return view('admin.about.show', [
|
||||||
|
'aboutEntry' => $about,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$about = About::firstOrNew();
|
||||||
|
$about->content = $request->input('content');
|
||||||
|
$about->save();
|
||||||
|
|
||||||
|
return redirect()->route('admin.about.show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,11 @@ namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\BadResponseException;
|
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ContactsController extends Controller
|
class ContactsController extends Controller
|
||||||
|
|
@ -113,14 +113,13 @@ class ContactsController extends Controller
|
||||||
$avatar = null;
|
$avatar = null;
|
||||||
$contact = Contact::findOrFail($contactId);
|
$contact = Contact::findOrFail($contactId);
|
||||||
if ($contact->homepage !== null && mb_strlen($contact->homepage) !== 0) {
|
if ($contact->homepage !== null && mb_strlen($contact->homepage) !== 0) {
|
||||||
$client = resolve(Client::class);
|
|
||||||
try {
|
try {
|
||||||
$response = $client->get($contact->homepage);
|
$response = Http::throw()->get($contact->homepage);
|
||||||
} catch (BadResponseException $e) {
|
} catch (RequestException $e) {
|
||||||
return redirect('/admin/contacts/'.$contactId.'/edit')
|
return redirect('/admin/contacts/'.$contactId.'/edit')
|
||||||
->with('error', 'Bad resposne from contact’s homepage');
|
->with('error', 'Bad resposne from contact’s homepage');
|
||||||
}
|
}
|
||||||
$mf2 = \Mf2\parse((string) $response->getBody(), $contact->homepage);
|
$mf2 = \Mf2\parse($response->body(), $contact->homepage);
|
||||||
foreach ($mf2['items'] as $microformat) {
|
foreach ($mf2['items'] as $microformat) {
|
||||||
if (Arr::get($microformat, 'type.0') === 'h-card') {
|
if (Arr::get($microformat, 'type.0') === 'h-card') {
|
||||||
$avatarURL = Arr::get($microformat, 'properties.photo.0.value');
|
$avatarURL = Arr::get($microformat, 'properties.photo.0.value');
|
||||||
|
|
@ -129,8 +128,8 @@ class ContactsController extends Controller
|
||||||
}
|
}
|
||||||
if ($avatarURL !== null) {
|
if ($avatarURL !== null) {
|
||||||
try {
|
try {
|
||||||
$avatar = $client->get($avatarURL);
|
$avatar = Http::throw()->get($avatarURL);
|
||||||
} catch (BadResponseException $e) {
|
} catch (RequestException $e) {
|
||||||
return redirect('/admin/contacts/'.$contactId.'/edit')
|
return redirect('/admin/contacts/'.$contactId.'/edit')
|
||||||
->with('error', 'Unable to download avatar');
|
->with('error', 'Unable to download avatar');
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +140,7 @@ class ContactsController extends Controller
|
||||||
if ($filesystem->isDirectory($directory) === false) {
|
if ($filesystem->isDirectory($directory) === false) {
|
||||||
$filesystem->makeDirectory($directory);
|
$filesystem->makeDirectory($directory);
|
||||||
}
|
}
|
||||||
$filesystem->put($directory.'/image', $avatar->getBody());
|
$filesystem->put($directory.'/image', $avatar->body());
|
||||||
|
|
||||||
return view('admin.contacts.getavatarsuccess', [
|
return view('admin.contacts.getavatarsuccess', [
|
||||||
'homepage' => parse_url($contact->homepage, PHP_URL_HOST),
|
'homepage' => parse_url($contact->homepage, PHP_URL_HOST),
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ class PasskeysController extends Controller
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$passkey = Passkey::firstWhere('passkey_id', $publicKeyCredential->id);
|
$passkey = Passkey::firstWhere('passkey_id', Base64UrlSafe::encodeUnpadded($publicKeyCredential->rawId));
|
||||||
if (! $passkey) {
|
if (! $passkey) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
|
|
|
||||||
32
app/Http/Controllers/Admin/SettingsController.php
Normal file
32
app/Http/Controllers/Admin/SettingsController.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class SettingsController extends Controller
|
||||||
|
{
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
$settings = Setting::first();
|
||||||
|
|
||||||
|
return view('admin.settings.show', [
|
||||||
|
'settings' => $settings,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$settings = Setting::firstOrNew();
|
||||||
|
$settings->winter_effect_enabled = $request->boolean('winter_effect_enabled');
|
||||||
|
$settings->save();
|
||||||
|
|
||||||
|
return redirect()->route('admin.settings.show');
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Http/Controllers/Admin/TokensController.php
Normal file
65
app/Http/Controllers/Admin/TokensController.php
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\MicropubToken;
|
||||||
|
use App\Services\TokenService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class TokensController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show a list of issued Micropub tokens.
|
||||||
|
*/
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$tokens = MicropubToken::latest()->get();
|
||||||
|
|
||||||
|
return view('admin.tokens.index', compact('tokens'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form to manually generate a new Micropub token.
|
||||||
|
*
|
||||||
|
* This is for clients (e.g. iA Writer) that don't support the IndieAuth
|
||||||
|
* PKCE flow and instead expect to be given a token directly.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('admin.tokens.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually generate a new Micropub token.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'client_id' => 'required|string',
|
||||||
|
'scope' => 'required|array|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = resolve(TokenService::class)->getNewToken([
|
||||||
|
'me' => config('app.url'),
|
||||||
|
'client_id' => $validated['client_id'],
|
||||||
|
'scope' => implode(' ', $validated['scope']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect('/admin/tokens')->with('new_token', $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a Micropub token.
|
||||||
|
*/
|
||||||
|
public function revoke(MicropubToken $token): RedirectResponse
|
||||||
|
{
|
||||||
|
$token->revoke();
|
||||||
|
|
||||||
|
return redirect('/admin/tokens');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ class ArticlesController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index(?int $year = null, ?int $month = null): View
|
public function index(?int $year = null, ?int $month = null): View
|
||||||
{
|
{
|
||||||
$articles = Article::where('published', '1')
|
$articles = Article::published()
|
||||||
->date($year, $month)
|
->date($year, $month)
|
||||||
->orderBy('updated_at', 'desc')
|
->orderBy('updated_at', 'desc')
|
||||||
->simplePaginate(5);
|
->simplePaginate(5);
|
||||||
|
|
|
||||||
|
|
@ -7,68 +7,15 @@ namespace App\Http\Controllers;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Response;
|
|
||||||
|
|
||||||
class FeedsController extends Controller
|
class FeedsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Returns the blog RSS feed.
|
|
||||||
*/
|
|
||||||
public function blogRss(): Response
|
|
||||||
{
|
|
||||||
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
|
|
||||||
$buildDate = $articles->first()->updated_at->toRssString();
|
|
||||||
|
|
||||||
return response()
|
|
||||||
->view('articles.rss', compact('articles', 'buildDate'))
|
|
||||||
->header('Content-Type', 'application/rss+xml; charset=utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the blog Atom feed.
|
|
||||||
*/
|
|
||||||
public function blogAtom(): Response
|
|
||||||
{
|
|
||||||
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
|
|
||||||
|
|
||||||
return response()
|
|
||||||
->view('articles.atom', compact('articles'))
|
|
||||||
->header('Content-Type', 'application/atom+xml; charset=utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the notes RSS feed.
|
|
||||||
*/
|
|
||||||
public function notesRss(): Response
|
|
||||||
{
|
|
||||||
$notes = Note::latest()->take(20)->get();
|
|
||||||
$buildDate = $notes->first()->updated_at->toRssString();
|
|
||||||
|
|
||||||
return response()
|
|
||||||
->view('notes.rss', compact('notes', 'buildDate'))
|
|
||||||
->header('Content-Type', 'application/rss+xml; charset=utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the notes Atom feed.
|
|
||||||
*/
|
|
||||||
public function notesAtom(): Response
|
|
||||||
{
|
|
||||||
$notes = Note::latest()->take(20)->get();
|
|
||||||
|
|
||||||
return response()
|
|
||||||
->view('notes.atom', compact('notes'))
|
|
||||||
->header('Content-Type', 'application/atom+xml; charset=utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @todo sort out return type for json responses */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the blog JSON feed.
|
* Returns the blog JSON feed.
|
||||||
*/
|
*/
|
||||||
public function blogJson(): array
|
public function blogJson(): JsonResponse
|
||||||
{
|
{
|
||||||
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
|
$articles = Article::published()->latest('updated_at')->take(20)->get();
|
||||||
$data = [
|
$data = [
|
||||||
'version' => 'https://jsonfeed.org/version/1.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',
|
||||||
|
|
@ -94,13 +41,15 @@ class FeedsController extends Controller
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return response()->json($data, 200, [
|
||||||
|
'Content-Type' => 'application/feed+json',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the notes JSON feed.
|
* Returns the notes JSON feed.
|
||||||
*/
|
*/
|
||||||
public function notesJson(): array
|
public function notesJson(): JsonResponse
|
||||||
{
|
{
|
||||||
$notes = Note::latest()->with('media', 'place', 'tags')->take(20)->get();
|
$notes = Note::latest()->with('media', 'place', 'tags')->take(20)->get();
|
||||||
$data = [
|
$data = [
|
||||||
|
|
@ -130,7 +79,9 @@ class FeedsController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return response()->json($data, 200, [
|
||||||
|
'Content-Type' => 'application/feed+json',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,7 +89,7 @@ class FeedsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function blogJf2(): JsonResponse
|
public function blogJf2(): JsonResponse
|
||||||
{
|
{
|
||||||
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
|
$articles = Article::published()->latest('updated_at')->take(20)->get();
|
||||||
$items = [];
|
$items = [];
|
||||||
foreach ($articles as $article) {
|
foreach ($articles as $article) {
|
||||||
$items[] = [
|
$items[] = [
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class FrontPageController extends Controller
|
||||||
->withCount(['webmentions AS reposts' => function ($query) {
|
->withCount(['webmentions AS reposts' => function ($query) {
|
||||||
$query->where('type', 'repost-of');
|
$query->where('type', 'repost-of');
|
||||||
}])->get();
|
}])->get();
|
||||||
$articles = Article::latest()->get();
|
$articles = Article::published()->latest()->get();
|
||||||
$bookmarks = Bookmark::latest()->with('tags')->get();
|
$bookmarks = Bookmark::latest()->with('tags')->get();
|
||||||
$likes = Like::latest()->get();
|
$likes = Like::latest()->get();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\MicropubToken;
|
||||||
use App\Services\TokenService;
|
use App\Services\TokenService;
|
||||||
use Exception;
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Psr7\Uri;
|
use GuzzleHttp\Psr7\Uri;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Random\RandomException;
|
use Random\RandomException;
|
||||||
|
|
@ -25,9 +25,10 @@ class IndieAuthController extends Controller
|
||||||
'issuer' => config('app.url'),
|
'issuer' => config('app.url'),
|
||||||
'authorization_endpoint' => route('indieauth.start'),
|
'authorization_endpoint' => route('indieauth.start'),
|
||||||
'token_endpoint' => route('indieauth.token'),
|
'token_endpoint' => route('indieauth.token'),
|
||||||
|
'revocation_endpoint' => route('indieauth.revocation'),
|
||||||
|
'introspection_endpoint' => route('indieauth.introspection'),
|
||||||
|
'introspection_endpoint_auth_methods_supported' => ['Bearer'],
|
||||||
'code_challenge_methods_supported' => ['S256'],
|
'code_challenge_methods_supported' => ['S256'],
|
||||||
// 'introspection_endpoint' => route('indieauth.introspection'),
|
|
||||||
// 'introspection_endpoint_auth_methods_supported' => ['none'],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,6 +180,50 @@ class IndieAuthController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a POST request to the IndieAuth revocation endpoint (RFC 7009).
|
||||||
|
*
|
||||||
|
* Per spec this always returns HTTP 200, whether the token was revoked,
|
||||||
|
* unknown, or already revoked, so callers can't probe token validity.
|
||||||
|
*/
|
||||||
|
public function processRevocationRequest(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
MicropubToken::findActive($request->get('token'))?->revoke();
|
||||||
|
|
||||||
|
return response()->json([], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a POST request to the IndieAuth token introspection endpoint
|
||||||
|
* (RFC 7662, extended by IndieAuth to require the `me` property).
|
||||||
|
*
|
||||||
|
* The caller must itself present a currently-active token as a Bearer
|
||||||
|
* credential to use this endpoint, per spec ("MUST also require some
|
||||||
|
* form of authorization"). Per spec, an inactive token being introspected
|
||||||
|
* still gets a 200 response containing only `active: false` - no other
|
||||||
|
* information about why it's inactive is given.
|
||||||
|
*/
|
||||||
|
public function processIntrospectionRequest(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
if (! MicropubToken::findActive($request->bearerToken())) {
|
||||||
|
return response()->json([], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = MicropubToken::findActive($request->get('token'));
|
||||||
|
|
||||||
|
if (! $token) {
|
||||||
|
return response()->json(['active' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'active' => true,
|
||||||
|
'me' => $token->me,
|
||||||
|
'client_id' => $token->client_id,
|
||||||
|
'scope' => $token->scope,
|
||||||
|
'iat' => $token->created_at->timestamp,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
protected function isValidRedirectUri(string $clientId, string $redirectUri): bool
|
protected function isValidRedirectUri(string $clientId, string $redirectUri): bool
|
||||||
{
|
{
|
||||||
// If client_id is not a valid URL, then it's not valid
|
// If client_id is not a valid URL, then it's not valid
|
||||||
|
|
@ -199,15 +244,13 @@ class IndieAuthController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we need to check the redirect_uri is in the client_id's redirect_uris
|
// Otherwise we need to check the redirect_uri is in the client_id's redirect_uris
|
||||||
$guzzle = resolve(Client::class);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$clientInfo = $guzzle->get($clientId);
|
$clientInfo = Http::throw()->get($clientId);
|
||||||
} catch (Exception) {
|
} catch (\Throwable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$clientInfoParsed = \Mf2\parse($clientInfo->getBody()->getContents(), $clientId);
|
$clientInfoParsed = \Mf2\parse($clientInfo->body(), $clientId);
|
||||||
|
|
||||||
$redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? [];
|
$redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use App\Services\Micropub\MicropubHandlerRegistry;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Lcobucci\JWT\Token;
|
|
||||||
|
|
||||||
class MicropubController extends Controller
|
class MicropubController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -71,7 +70,9 @@ class MicropubController extends Controller
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
'error_description' => 'No known note with given ID',
|
'error_description' => 'No known note with given ID',
|
||||||
], 404);
|
], 404);
|
||||||
} catch (MicropubUnsupportedModelException) {
|
} catch (MicropubUnsupportedModelException $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'invalid',
|
'error' => 'invalid',
|
||||||
'error_description' => 'This implementation currently only supports the updating of notes',
|
'error_description' => 'This implementation currently only supports the updating of notes',
|
||||||
|
|
@ -81,12 +82,16 @@ class MicropubController extends Controller
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
'error_description' => $e->getMessage(),
|
'error_description' => $e->getMessage(),
|
||||||
], 400);
|
], 400);
|
||||||
} catch (MicropubHandlerException) {
|
} catch (MicropubHandlerException $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'unsupported_operation',
|
'error' => 'unsupported_operation',
|
||||||
'error_description' => 'The request could not be processed by this server',
|
'error_description' => 'The request could not be processed by this server',
|
||||||
], 500);
|
], 500);
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'server_error',
|
'error' => 'server_error',
|
||||||
'error_description' => 'An error occurred processing the request',
|
'error_description' => 'An error occurred processing the request',
|
||||||
|
|
@ -135,7 +140,7 @@ class MicropubController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// the default response is just to return the token data
|
// the default response is just to return the token data
|
||||||
/** @var Token $tokenData */
|
/** @var array $tokenData */
|
||||||
$tokenData = $request->input('token_data');
|
$tokenData = $request->input('token_data');
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,10 @@ 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\Image\ImageException;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Image;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Intervention\Image\ImageManager;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
class MicropubMediaController extends Controller
|
class MicropubMediaController extends Controller
|
||||||
|
|
@ -25,9 +26,7 @@ class MicropubMediaController extends Controller
|
||||||
$tokenData = $request->input('token_data');
|
$tokenData = $request->input('token_data');
|
||||||
|
|
||||||
$scopes = $tokenData['scope'];
|
$scopes = $tokenData['scope'];
|
||||||
if (is_string($scopes)) {
|
|
||||||
$scopes = explode(' ', $scopes);
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
|
||||||
if (! in_array('create', $scopes, true)) {
|
if (! in_array('create', $scopes, true)) {
|
||||||
return (new MicropubResponses)->insufficientScopeResponse();
|
return (new MicropubResponses)->insufficientScopeResponse();
|
||||||
}
|
}
|
||||||
|
|
@ -83,9 +82,7 @@ class MicropubMediaController extends Controller
|
||||||
$tokenData = $request->input('token_data');
|
$tokenData = $request->input('token_data');
|
||||||
|
|
||||||
$scopes = $tokenData['scope'];
|
$scopes = $tokenData['scope'];
|
||||||
if (is_string($scopes)) {
|
|
||||||
$scopes = explode(' ', $scopes);
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
|
||||||
if (! in_array('create', $scopes, true)) {
|
if (! in_array('create', $scopes, true)) {
|
||||||
return (new MicropubResponses)->insufficientScopeResponse();
|
return (new MicropubResponses)->insufficientScopeResponse();
|
||||||
}
|
}
|
||||||
|
|
@ -111,12 +108,9 @@ class MicropubMediaController extends Controller
|
||||||
|
|
||||||
$filename = Storage::disk('local')->putFile('media', $file);
|
$filename = Storage::disk('local')->putFile('media', $file);
|
||||||
|
|
||||||
/** @var ImageManager $manager */
|
|
||||||
$manager = resolve(ImageManager::class);
|
|
||||||
try {
|
try {
|
||||||
$image = $manager->read($request->file('file'));
|
$width = Image::fromUpload($request->file('file'))->width();
|
||||||
$width = $image->width();
|
} catch (ImageException) {
|
||||||
} catch (Exception) {
|
|
||||||
// not an image
|
// not an image
|
||||||
$width = null;
|
$width = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ class LinkHeadersMiddleware
|
||||||
$response->header('Link', '<'.route('indieauth.metadata').'>; rel="indieauth-metadata"', false);
|
$response->header('Link', '<'.route('indieauth.metadata').'>; rel="indieauth-metadata"', false);
|
||||||
$response->header('Link', '<'.route('indieauth.start').'>; rel="authorization_endpoint"', false);
|
$response->header('Link', '<'.route('indieauth.start').'>; rel="authorization_endpoint"', false);
|
||||||
$response->header('Link', '<'.route('indieauth.token').'>; rel="token_endpoint"', false);
|
$response->header('Link', '<'.route('indieauth.token').'>; rel="token_endpoint"', false);
|
||||||
|
$response->header('Link', '<'.route('indieauth.revocation').'>; rel="revocation_endpoint"', false);
|
||||||
|
$response->header('Link', '<'.route('indieauth.introspection').'>; rel="introspection_endpoint"', false);
|
||||||
$response->header('Link', '<'.route('micropub-endpoint').'>; rel="micropub"', false);
|
$response->header('Link', '<'.route('micropub-endpoint').'>; rel="micropub"', false);
|
||||||
$response->header('Link', '<'.route('webmention-endpoint').'>; rel="webmention"', false);
|
$response->header('Link', '<'.route('webmention-endpoint').'>; rel="webmention"', false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,9 @@ declare(strict_types=1);
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Http\Responses\MicropubResponses;
|
use App\Http\Responses\MicropubResponses;
|
||||||
|
use App\Models\MicropubToken;
|
||||||
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
|
||||||
|
|
@ -39,15 +35,15 @@ class VerifyMicropubToken
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$token = MicropubToken::findActive($rawToken);
|
||||||
$tokenData = $this->validateToken($rawToken);
|
|
||||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
if (! $token) {
|
||||||
$micropubResponses = new MicropubResponses;
|
$micropubResponses = new MicropubResponses;
|
||||||
|
|
||||||
return $micropubResponses->invalidTokenResponse();
|
return $micropubResponses->invalidTokenResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($tokenData->claims()->has('scope') === false) {
|
if ($token->scope === '') {
|
||||||
$micropubResponses = new MicropubResponses;
|
$micropubResponses = new MicropubResponses;
|
||||||
|
|
||||||
return $micropubResponses->tokenHasNoScopeResponse();
|
return $micropubResponses->tokenHasNoScopeResponse();
|
||||||
|
|
@ -56,26 +52,10 @@ class VerifyMicropubToken
|
||||||
return $next($request->merge([
|
return $next($request->merge([
|
||||||
'access_token' => $rawToken,
|
'access_token' => $rawToken,
|
||||||
'token_data' => [
|
'token_data' => [
|
||||||
'me' => $tokenData->claims()->get('me'),
|
'me' => $token->me,
|
||||||
'scope' => $tokenData->claims()->get('scope'),
|
'scope' => $token->scope,
|
||||||
'client_id' => $tokenData->claims()->get('client_id'),
|
'client_id' => $token->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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\FileSystem\FileSystem;
|
use Illuminate\FileSystem\FileSystem;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class DownloadWebMention implements ShouldQueue
|
class DownloadWebMention implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -29,15 +29,15 @@ class DownloadWebMention implements ShouldQueue
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*
|
*
|
||||||
* @throws GuzzleException
|
* @throws RequestException
|
||||||
* @throws FileNotFoundException
|
* @throws FileNotFoundException
|
||||||
*/
|
*/
|
||||||
public function handle(Client $guzzle): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$response = $guzzle->request('GET', $this->source);
|
// 4XX and 5XX responses should throw so Laravel can catch and
|
||||||
// 4XX and 5XX responses should get Guzzle to throw an exception,
|
// retry these automatically.
|
||||||
// Laravel should catch and retry these automatically.
|
$response = Http::throw()->get($this->source);
|
||||||
if ($response->getStatusCode() === 200) {
|
if ($response->status() === 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
|
||||||
|
|
@ -56,7 +56,7 @@ class DownloadWebMention implements ShouldQueue
|
||||||
// save new HTML
|
// save new HTML
|
||||||
$filesystem->put(
|
$filesystem->put(
|
||||||
$filename,
|
$filename,
|
||||||
(string) $response->getBody()
|
$response->body()
|
||||||
);
|
);
|
||||||
// remove backup if the same
|
// remove backup if the same
|
||||||
if ($filesystem->exists($filenameBackup)) {
|
if ($filesystem->exists($filenameBackup)) {
|
||||||
|
|
|
||||||
56
app/Jobs/NotifyBrrrOfWebMention.php
Normal file
56
app/Jobs/NotifyBrrrOfWebMention.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\WebMention;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class NotifyBrrrOfWebMention implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected WebMention $webMention
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$webhookUrl = config('services.brrr.webhook_url');
|
||||||
|
|
||||||
|
if (blank($webhookUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Http::post($webhookUrl, [
|
||||||
|
'title' => $this->title(),
|
||||||
|
'message' => "From {$this->webMention->source}",
|
||||||
|
'open_url' => $this->webMention->target,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a notification title based on the webmention type.
|
||||||
|
*/
|
||||||
|
private function title(): string
|
||||||
|
{
|
||||||
|
return match ($this->webMention->type) {
|
||||||
|
'in-reply-to' => 'New reply',
|
||||||
|
'like-of' => 'New like',
|
||||||
|
'repost-of' => 'New repost',
|
||||||
|
default => 'New webmention',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,14 +5,13 @@ declare(strict_types=1);
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||||
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
|
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
|
||||||
|
|
||||||
|
|
@ -32,13 +31,11 @@ class ProcessLike implements ShouldQueue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*
|
|
||||||
* @throws GuzzleException
|
|
||||||
*/
|
*/
|
||||||
public function handle(Client $client, Authorship $authorship): int
|
public function handle(Authorship $authorship): int
|
||||||
{
|
{
|
||||||
$response = $client->request('GET', $this->like->url);
|
$response = Http::throw()->get($this->like->url);
|
||||||
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
$mf2 = \Mf2\parse($response->body(), $this->like->url);
|
||||||
if (Arr::has($mf2, 'items.0.properties.content')) {
|
if (Arr::has($mf2, 'items.0.properties.content')) {
|
||||||
$this->like->content = $mf2['items'][0]['properties']['content'][0]['html'];
|
$this->like->content = $mf2['items'][0]['properties']['content'][0]['html'];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ namespace App\Jobs;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Image\ImageException;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Image;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Intervention\Image\Exceptions\DecoderException;
|
|
||||||
use Intervention\Image\ImageManager;
|
|
||||||
|
|
||||||
class ProcessMedia implements ShouldQueue
|
class ProcessMedia implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -30,15 +30,16 @@ class ProcessMedia implements ShouldQueue
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(ImageManager $manager): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
// Load file
|
// Load file
|
||||||
$file = Storage::disk('local')->get($this->filename);
|
$file = Storage::disk('local')->get($this->filename);
|
||||||
|
|
||||||
// Open file
|
// Open file
|
||||||
|
$image = Image::fromStorage($this->filename, 'local');
|
||||||
try {
|
try {
|
||||||
$image = $manager->read($file);
|
$width = $image->width();
|
||||||
} catch (DecoderException) {
|
} catch (ImageException) {
|
||||||
// not an image; delete file and end job
|
// not an image; delete file and end job
|
||||||
Storage::disk('local')->delete($this->filename);
|
Storage::disk('local')->delete($this->filename);
|
||||||
|
|
||||||
|
|
@ -49,18 +50,15 @@ class ProcessMedia implements ShouldQueue
|
||||||
Storage::disk('public')->put($this->filename, $file);
|
Storage::disk('public')->put($this->filename, $file);
|
||||||
|
|
||||||
// Create smaller versions if necessary
|
// Create smaller versions if necessary
|
||||||
if ($image->width() > 1000) {
|
if ($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 = trim(implode('.', $filenameParts), '.');
|
$basename = trim(implode('.', $filenameParts), '.');
|
||||||
|
|
||||||
$medium = $image->scale(width: 1000);
|
Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
|
||||||
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode());
|
Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
|
||||||
|
|
||||||
$small = $image->scale(width: 500);
|
|
||||||
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we can delete the locally saved image
|
// Now we can delete the locally saved image
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue