diff --git a/app/Http/Controllers/MicropubController.php b/app/Http/Controllers/MicropubController.php index b9471497..c9572f14 100644 --- a/app/Http/Controllers/MicropubController.php +++ b/app/Http/Controllers/MicropubController.php @@ -2,41 +2,35 @@ namespace App\Http\Controllers; -use Storage; use Monolog\Logger; use Ramsey\Uuid\Uuid; use App\Jobs\ProcessMedia; +use App\Services\TokenService; use Illuminate\Http\UploadedFile; use Monolog\Handler\StreamHandler; use App\{Like, Media, Note, Place}; use Intervention\Image\ImageManager; +use Illuminate\Support\Facades\Storage; use Illuminate\Http\{Request, Response}; use App\Exceptions\InvalidTokenException; use Phaza\LaravelPostgis\Geometries\Point; use Intervention\Image\Exception\NotReadableException; -use App\Services\{NoteService, PlaceService, TokenService}; use App\Services\Micropub\{HCardService, HEntryService, UpdateService}; class MicropubController extends Controller { protected $tokenService; - protected $noteService; - protected $placeService; protected $hentryService; protected $hcardService; protected $updateService; public function __construct( TokenService $tokenService, - NoteService $noteService, - PlaceService $placeService, HEntryService $hentryService, HCardService $hcardService, UpdateService $updateService ) { $this->tokenService = $tokenService; - $this->noteService = $noteService; - $this->placeService = $placeService; $this->hentryService = $hentryService; $this->hcardService = $hcardService; $this->updateService = $updateService; @@ -46,13 +40,12 @@ class MicropubController extends Controller * This function receives an API request, verifies the authenticity * then passes over the info to the relavent Service class. * - * @param \Illuminate\Http\Request request * @return \Illuminate\Http\Response */ - public function post(Request $request) + public function post() { try { - $tokenData = $this->tokenService->validateToken($request->bearerToken()); + $tokenData = $this->tokenService->validateToken(request()->bearerToken()); } catch (InvalidTokenException $e) { return $this->invalidTokenResponse(); } @@ -61,13 +54,13 @@ class MicropubController extends Controller return $this->tokenHasNoScopeResponse(); } - $this->logMicropubRequest($request); + $this->logMicropubRequest(request()->all()); - if (($request->input('h') == 'entry') || ($request->input('type.0') == 'h-entry')) { + if ((request()->input('h') == 'entry') || (request()->input('type.0') == 'h-entry')) { if (stristr($tokenData->getClaim('scope'), 'create') === false) { return $this->insufficientScopeResponse(); } - $location = $this->hentryService->process($request); + $location = $this->hentryService->process(request()->all(), $this->getCLientId()); return response()->json([ 'response' => 'created', @@ -75,11 +68,11 @@ class MicropubController extends Controller ], 201)->header('Location', $location); } - if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') { + if (request()->input('h') == 'card' || request()->input('type')[0] == 'h-card') { if (stristr($tokenData->getClaim('scope'), 'create') === false) { return $this->insufficientScopeResponse(); } - $location = $this->hcardService->process($request); + $location = $this->hcardService->process(request()->all()); return response()->json([ 'response' => 'created', @@ -87,13 +80,18 @@ class MicropubController extends Controller ], 201)->header('Location', $location); } - if ($request->input('action') == 'update') { + if (request()->input('action') == 'update') { if (stristr($tokenData->getClaim('scope'), 'update') === false) { - return $this->returnInsufficientScopeResponse(); + return $this->insufficientScopeResponse(); } - return $this->updateService->process($request); + return $this->updateService->process(request()->all()); } + + return response()->json([ + 'response' => 'error', + 'error_description' => 'unsupported_request_type', + ], 500); } /** @@ -102,34 +100,33 @@ class MicropubController extends Controller * appropriately. Further if the request has the query parameter * synidicate-to we respond with the known syndication endpoints. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ - public function get(Request $request) + public function get() { try { - $tokenData = $this->tokenService->validateToken($request->bearerToken()); + $tokenData = $this->tokenService->validateToken(request()->bearerToken()); } catch (InvalidTokenException $e) { return $this->invalidTokenResponse(); } - if ($request->input('q') === 'syndicate-to') { + if (request()->input('q') === 'syndicate-to') { return response()->json([ 'syndicate-to' => config('syndication.targets'), ]); } - if ($request->input('q') == 'config') { + if (request()->input('q') == 'config') { return response()->json([ 'syndicate-to' => config('syndication.targets'), 'media-endpoint' => route('media-endpoint'), ]); } - if (substr($request->input('q'), 0, 4) === 'geo:') { + if (substr(request()->input('q'), 0, 4) === 'geo:') { preg_match_all( '/([0-9\.\-]+)/', - $request->input('q'), + request()->input('q'), $matches ); $distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000; @@ -155,13 +152,12 @@ class MicropubController extends Controller /** * Process a media item posted to the media endpoint. * - * @param Illuminate\Http\Request $request * @return Illuminate\Http\Response */ - public function media(Request $request) + public function media() { try { - $tokenData = $this->tokenService->validateToken($request->bearerToken()); + $tokenData = $this->tokenService->validateToken(request()->bearerToken()); } catch (InvalidTokenException $e) { return $this->invalidTokenResponse(); } @@ -174,7 +170,7 @@ class MicropubController extends Controller return $this->insufficientScopeResponse(); } - if (($request->hasFile('file') && $request->file('file')->isValid()) === false) { + if ((request()->hasFile('file') && request()->file('file')->isValid()) === false) { return response()->json([ 'response' => 'error', 'error' => 'invalid_request', @@ -182,13 +178,13 @@ class MicropubController extends Controller ], 400); } - $this->logMicropubRequest($request); + $this->logMicropubRequest(request()->all()); - $filename = $this->saveFile($request->file('file')); + $filename = $this->saveFile(request()->file('file')); $manager = resolve(ImageManager::class); try { - $image = $manager->make($request->file('file')); + $image = $manager->make(request()->file('file')); $width = $image->width(); } catch (NotReadableException $exception) { // not an image @@ -196,9 +192,9 @@ class MicropubController extends Controller } $media = Media::create([ - 'token' => $request->bearerToken(), + 'token' => request()->bearerToken(), 'path' => 'media/' . $filename, - 'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()), + 'type' => $this->getFileTypeFromMimeType(request()->file('file')->getMimeType()), 'image_widths' => $width, ]); @@ -234,6 +230,7 @@ class MicropubController extends Controller $videoMimeTypes = [ 'video/mp4', 'video/mpeg', + 'video/ogg', 'video/quicktime', 'video/webm', ]; @@ -254,18 +251,24 @@ class MicropubController extends Controller return 'download'; } - private function logMicropubRequest(Request $request) + private function getClientId(): string + { + return resolve(TokenService::class) + ->validateToken(request()->bearerToken()) + ->getClaim('client_id'); + } + + private function logMicropubRequest(array $request) { $logger = new Logger('micropub'); $logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG); - $logger->debug('MicropubLog', $request->all()); + $logger->debug('MicropubLog', $request); } private function saveFile(UploadedFile $file) { $filename = Uuid::uuid4() . '.' . $file->extension(); - $size = $file->getClientSize(); - Storage::disk('local')->put($filename, $file->openFile()->fread($size)); + Storage::disk('local')->put($filename, $file); return $filename; } diff --git a/app/Place.php b/app/Place.php index 74588148..7937443b 100644 --- a/app/Place.php +++ b/app/Place.php @@ -136,6 +136,9 @@ class Place extends Model public function setExternalUrlsAttribute($url) { + if ($url === null) { + return; + } $type = $this->getType($url); $already = []; if (array_key_exists('external_urls', $this->attributes)) { diff --git a/app/Services/BookmarkService.php b/app/Services/BookmarkService.php index 0966a996..3265191e 100644 --- a/app/Services/BookmarkService.php +++ b/app/Services/BookmarkService.php @@ -21,25 +21,27 @@ class BookmarkService /** * Create a new Bookmark. * - * @param Request $request + * @param array $request + * @return Bookmark $bookmark */ - public function createBookmark(Request $request): Bookmark + public function createBookmark(array $request): Bookmark { - if ($request->header('Content-Type') == 'application/json') { + if (array_get($request, 'properties.bookmark-of.0')) { //micropub request - $url = normalize_url($request->input('properties.bookmark-of.0')); - $name = $request->input('properties.name.0'); - $content = $request->input('properties.content.0'); - $categories = $request->input('properties.category'); + $url = normalize_url(array_get($request, 'properties.bookmark-of.0')); + $name = array_get($request, 'properties.name.0'); + $content = array_get($request, 'properties.content.0'); + $categories = array_get($request, 'properties.category'); } - if (($request->header('Content-Type') == 'application/x-www-form-urlencoded') - || - (str_contains($request->header('Content-Type'), 'multipart/form-data')) - ) { - $url = normalize_url($request->input('bookmark-of')); - $name = $request->input('name'); - $content = $request->input('content'); - $categories = $request->input('category'); + if (array_get($request, 'bookmark-of')) { + $url = normalize_url(array_get($request, 'bookmark-of')); + $name = array_get($request, 'name'); + $content = array_get($request, 'content'); + $categories = array_get($request, 'category'); + } + + if (! isset($url)) { + throw new \Exception; } $bookmark = Bookmark::create([ @@ -55,11 +57,11 @@ class BookmarkService $targets = array_pluck(config('syndication.targets'), 'uid', 'service.name'); $mpSyndicateTo = null; - if ($request->has('mp-syndicate-to')) { - $mpSyndicateTo = $request->input('mp-syndicate-to'); + if (array_get($request, 'mp-syndicate-to')) { + $mpSyndicateTo = array_get($request, 'mp-syndicate-to'); } - if ($request->has('properties.mp-syndicate-to')) { - $mpSyndicateTo = $request->input('properties.mp-syndicate-to'); + if (array_get($request, 'properties.mp-syndicate-to')) { + $mpSyndicateTo = array_get($request, 'properties.mp-syndicate-to'); } if (is_string($mpSyndicateTo)) { $service = array_search($mpSyndicateTo, $targets); diff --git a/app/Services/LikeService.php b/app/Services/LikeService.php index 855640da..91d45a53 100644 --- a/app/Services/LikeService.php +++ b/app/Services/LikeService.php @@ -6,26 +6,27 @@ namespace App\Services; use App\Like; use App\Jobs\ProcessLike; -use Illuminate\Http\Request; class LikeService { /** * Create a new Like. * - * @param Request $request + * @param array $request + * @return Like $like */ - public function createLike(Request $request): Like + public function createLike(array $request): Like { - if ($request->header('Content-Type') == 'application/json') { + if (array_get($request, 'properties.like-of.0')) { //micropub request - $url = normalize_url($request->input('properties.like-of.0')); + $url = normalize_url(array_get($request, 'properties.like-of.0')); } - if (($request->header('Content-Type') == 'application/x-www-form-urlencoded') - || - ($request->header('Content-Type') == 'multipart/form-data') - ) { - $url = normalize_url($request->input('like-of')); + if (array_get($request, 'like-of')) { + $url = normalize_url(array_get($request, 'like-of')); + } + + if (! isset($url)) { + throw new \Exception(); } $like = Like::create(['url' => $url]); diff --git a/app/Services/Micropub/HCardService.php b/app/Services/Micropub/HCardService.php index 4488aaad..62907379 100644 --- a/app/Services/Micropub/HCardService.php +++ b/app/Services/Micropub/HCardService.php @@ -2,30 +2,23 @@ namespace App\Services\Micropub; -use Illuminate\Http\Request; use App\Services\PlaceService; class HCardService { - public function process(Request $request) + public function process(array $request) { $data = []; - if ($request->header('Content-Type') == 'application/json') { - $data['name'] = $request->input('properties.name'); - $data['description'] = $request->input('properties.description') ?? null; - if ($request->has('properties.geo')) { - $data['geo'] = $request->input('properties.geo'); - } + if (array_get($request, 'properties.name')) { + $data['name'] = array_get($request, 'properties.name'); + $data['description'] = array_get($request, 'properties.description'); + $data['geo'] = array_get($request, 'properties.geo'); } else { - $data['name'] = $request->input('name'); - $data['description'] = $request->input('description'); - if ($request->has('geo')) { - $data['geo'] = $request->input('geo'); - } - if ($request->has('latitude')) { - $data['latitude'] = $request->input('latitude'); - $data['longitude'] = $request->input('longitude'); - } + $data['name'] = array_get($request, 'name'); + $data['description'] = array_get($request, 'description'); + $data['geo'] = array_get($request, 'geo'); + $data['latitude'] = array_get($request, 'latitude'); + $data['longitude'] = array_get($request, 'longitude'); } $place = resolve(PlaceService::class)->createPlace($data); diff --git a/app/Services/Micropub/HEntryService.php b/app/Services/Micropub/HEntryService.php index acff6e2a..fe44c04a 100644 --- a/app/Services/Micropub/HEntryService.php +++ b/app/Services/Micropub/HEntryService.php @@ -2,26 +2,25 @@ namespace App\Services\Micropub; -use Illuminate\Http\Request; use App\Services\{BookmarkService, LikeService, NoteService}; class HEntryService { - public function process(Request $request) + public function process(array $request, string $client = null) { - if ($request->has('properties.like-of') || $request->has('like-of')) { + if (array_get($request, 'properties.like-of') || array_get($request, 'like-of')) { $like = resolve(LikeService::class)->createLike($request); return $like->longurl; } - if ($request->has('properties.bookmark-of') || $request->has('bookmark-of')) { + if (array_get($request, 'properties.bookmark-of') || array_get($request, 'bookmark-of')) { $bookmark = resolve(BookmarkService::class)->createBookmark($request); return $bookmark->longurl; } - $note = resolve(NoteService::class)->createNote($request); + $note = resolve(NoteService::class)->createNote($request, $client); return $note->longurl; } diff --git a/app/Services/Micropub/UpdateService.php b/app/Services/Micropub/UpdateService.php index d0138360..77adbef2 100644 --- a/app/Services/Micropub/UpdateService.php +++ b/app/Services/Micropub/UpdateService.php @@ -4,14 +4,13 @@ namespace App\Services\Micropub; use App\Note; use App\Media; -use Illuminate\Http\Request; use Illuminate\Database\Eloquent\ModelNotFoundException; class UpdateService { - public function process(Request $request) + public function process(array $request) { - $urlPath = parse_url($request->input('url'), PHP_URL_PATH); + $urlPath = parse_url(array_get($request, 'url'), PHP_URL_PATH); //is it a note we are updating? if (mb_substr($urlPath, 1, 5) !== 'notes') { @@ -31,8 +30,8 @@ class UpdateService } //got the note, are we dealing with a “replace” request? - if ($request->has('replace')) { - foreach ($request->input('replace') as $property => $value) { + if (array_get($request, 'replace')) { + foreach (array_get($request, 'replace') as $property => $value) { if ($property == 'content') { $note->note = $value[0]; } @@ -58,8 +57,8 @@ class UpdateService } //how about “add” - if ($request->has('add')) { - foreach ($request->input('add') as $property => $value) { + if (array_get($request, 'add')) { + foreach (array_get($request, 'add') as $property => $value) { if ($property == 'syndication') { foreach ($value as $syndicationURL) { if (starts_with($syndicationURL, 'https://www.facebook.com')) { @@ -75,7 +74,7 @@ class UpdateService } if ($property == 'photo') { foreach ($value as $photoURL) { - if (start_with($photo, 'https://')) { + if (starts_with($photoURL, 'https://')) { $media = new Media(); $media->path = $photoURL; $media->type = 'image'; @@ -91,5 +90,10 @@ class UpdateService 'response' => 'updated', ]); } + + return response()->json([ + 'response' => 'error', + 'error_description' => 'unsupported request', + ], 500); } } diff --git a/app/Services/NoteService.php b/app/Services/NoteService.php index dea0d6f3..6e529245 100644 --- a/app/Services/NoteService.php +++ b/app/Services/NoteService.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Services; -use Illuminate\Http\Request; use App\{Media, Note, Place}; use App\Jobs\{SendWebMentions, SyndicateNoteToFacebook, SyndicateNoteToTwitter}; @@ -13,190 +12,38 @@ class NoteService /** * Create a new note. * - * @param \Illuminate\Http\Request $request + * @param array $request + * @param string $client * @return \App\Note $note */ - public function createNote(Request $request): Note + public function createNote(array $request, string $client = null): Note { - //move the request to data code here before refactor - $data = []; - $data['client-id'] = resolve(TokenService::class) - ->validateToken($request->bearerToken()) - ->getClaim('client_id'); - if ($request->header('Content-Type') == 'application/json') { - if (is_string($request->input('properties.content.0'))) { - $data['content'] = $request->input('properties.content.0'); //plaintext content - } - if (is_array($request->input('properties.content.0')) - && array_key_exists('html', $request->input('properties.content.0')) - ) { - $data['content'] = $request->input('properties.content.0.html'); - } - $data['in-reply-to'] = $request->input('properties.in-reply-to.0'); - // check location is geo: string - if (is_string($request->input('properties.location.0'))) { - $data['location'] = $request->input('properties.location.0'); - } - // check location is h-card - if (is_array($request->input('properties.location.0'))) { - if ($request->input('properties.location.0.type.0' === 'h-card')) { - try { - $place = resolve(PlaceService::class)->createPlaceFromCheckin( - $request->input('properties.location.0') - ); - $data['checkin'] = $place->longurl; - } catch (\Exception $e) { - // - } - } - } - $data['published'] = $request->input('properties.published.0'); - //create checkin place - if (array_key_exists('checkin', $request->input('properties'))) { - $data['swarm-url'] = $request->input('properties.syndication.0'); - try { - $place = resolve(PlaceService::class)->createPlaceFromCheckin( - $request->input('properties.checkin.0') - ); - $data['checkin'] = $place->longurl; - } catch (\Exception $e) { - $data['checkin'] = null; - $data['swarm-url'] = null; - } - } - } else { - $data['content'] = $request->input('content'); - $data['in-reply-to'] = $request->input('in-reply-to'); - $data['location'] = $request->input('location'); - $data['published'] = $request->input('published'); - } - $data['syndicate'] = []; - $targets = array_pluck(config('syndication.targets'), 'uid', 'service.name'); - $mpSyndicateTo = null; - if ($request->has('mp-syndicate-to')) { - $mpSyndicateTo = $request->input('mp-syndicate-to'); - } - if ($request->has('properties.mp-syndicate-to')) { - $mpSyndicateTo = $request->input('properties.mp-syndicate-to'); - } - if (is_string($mpSyndicateTo)) { - $service = array_search($mpSyndicateTo, $targets); - if ($service == 'Twitter') { - $data['syndicate'][] = 'twitter'; - } - if ($service == 'Facebook') { - $data['syndicate'][] = 'facebook'; - } - } - if (is_array($mpSyndicateTo)) { - foreach ($mpSyndicateTo as $uid) { - $service = array_search($uid, $targets); - if ($service == 'Twitter') { - $data['syndicate'][] = 'twitter'; - } - if ($service == 'Facebook') { - $data['syndicate'][] = 'facebook'; - } - } - } - $data['photo'] = []; - $photos = null; - if ($request->has('photo')) { - $photos = $request->input('photo'); - } - if ($request->has('properties.photo')) { - $photos = $request->input('properties.photo'); - } - if ($photos !== null) { - foreach ($photos as $photo) { - if (is_string($photo)) { - //only supporting media URLs for now - $data['photo'][] = $photo; - } - } - if (starts_with($request->input('properties.syndication.0'), 'https://www.instagram.com')) { - $data['instagram-url'] = $request->input('properties.syndication.0'); - } - } - //check the input - if (array_key_exists('content', $data) === false) { - $data['content'] = null; - } - if (array_key_exists('in-reply-to', $data) === false) { - $data['in-reply-to'] = null; - } - if (array_key_exists('client-id', $data) === false) { - $data['client-id'] = null; - } $note = Note::create( [ - 'note' => $data['content'], - 'in_reply_to' => $data['in-reply-to'], - 'client_id' => $data['client-id'], + 'note' => $this->getContent($request), + 'in_reply_to' => $this->getInReplyTo($request), + 'client_id' => $client, ] ); - if (array_key_exists('published', $data) && empty($data['published']) === false) { - $note->created_at = $note->updated_at = carbon($data['published']) - ->toDateTimeString(); + if ($this->getPublished($request)) { + $note->created_at = $note->updated_at = $this->getPublished($request); } - if (array_key_exists('location', $data) && $data['location'] !== null && $data['location'] !== 'no-location') { - if (starts_with($data['location'], config('app.url'))) { - //uri of form http://host/places/slug, we want slug - //get the URL path, then take last part, we can hack with basename - //as path looks like file path. - $place = Place::where('slug', basename(parse_url($data['location'], PHP_URL_PATH)))->first(); - $note->place()->associate($place); - } - if (substr($data['location'], 0, 4) == 'geo:') { - preg_match_all( - '/([0-9\.\-]+)/', - $data['location'], - $matches - ); - $note->location = $matches[0][0] . ', ' . $matches[0][1]; + $note->location = $this->getLocation($request); + + if ($this->getCheckin($request)) { + $note->place()->associate($this->getCheckin($request)); + $note->swarm_url = $this->getSwarmUrl($request); + if ($note->note === null || $note->note == '') { + $note->note = 'I’ve just checked in with Swarm'; } } - if (array_key_exists('checkin', $data) && $data['checkin'] !== null) { - $place = Place::where('slug', basename(parse_url($data['checkin'], PHP_URL_PATH)))->first(); - if ($place !== null) { - $note->place()->associate($place); - $note->swarm_url = $data['swarm-url']; - if ($note->note === null || $note->note == '') { - $note->note = 'I’ve just checked in with Swarm'; - } - } - } + $note->instagram_url = $this->getInstagramUrl($request); - /* drop image support for now - //add images to media library - if ($request->hasFile('photo')) { - $files = $request->file('photo'); - foreach ($files as $file) { - $note->addMedia($file)->toCollectionOnDisk('images', 's3'); - } - } - */ - //add support for media uploaded as URLs - if (array_key_exists('photo', $data)) { - foreach ((array) $data['photo'] as $photo) { - // check the media was uploaded to my endpoint, and use path - if (starts_with($photo, config('filesystems.disks.s3.url'))) { - $path = substr($photo, strlen(config('filesystems.disks.s3.url'))); - $media = Media::where('path', ltrim($path, '/'))->firstOrFail(); - } else { - $media = Media::firstOrNew(['path' => $photo]); - // currently assuming this is a photo from Swarm or OwnYourGram - $media->type = 'image'; - $media->save(); - } - $note->media()->save($media); - } - if (array_key_exists('instagram-url', $data)) { - $note->instagram_url = $data['instagram-url']; - } + foreach ($this->getMedia($request) as $media) { + $note->media()->save($media); } $note->save(); @@ -204,15 +51,175 @@ class NoteService dispatch(new SendWebMentions($note)); //syndication targets - if (array_key_exists('syndicate', $data)) { - if (in_array('twitter', $data['syndicate'])) { + if (count($this->getSyndicationTargets($request)) > 0) { + if (in_array('twitter', $this->getSyndicationTargets($request))) { dispatch(new SyndicateNoteToTwitter($note)); } - if (in_array('facebook', $data['syndicate'])) { + if (in_array('facebook', $this->getSyndicationTargets($request))) { dispatch(new SyndicateNoteToFacebook($note)); } } return $note; } + + private function getContent(array $request): ?string + { + if (array_get($request, 'properties.content.0.html')) { + return array_get($request, 'properties.content.0.html'); + } + if (is_string(array_get($request, 'properties.content.0'))) { + return array_get($request, 'properties.content.0'); + } + + return array_get($request, 'content'); + } + + private function getInReplyTo(array $request): ?string + { + if (array_get($request, 'properties.in-reply-to.0')) { + return array_get($request, 'properties.in-reply-to.0'); + } + + return array_get($request, 'in-reply-to'); + } + + private function getPublished(array $request): ?string + { + if (array_get($request, 'properties.published.0')) { + return carbon(array_get($request, 'properties.published.0')) + ->toDateTimeString(); + } + if (array_get($request, 'published')) { + return carbon(array_get($request, 'published'))->toDateTimeString(); + } + + return null; + } + + private function getLocation(array $request): ?string + { + $location = array_get($request, 'properties.location.0') ?? array_get($request, 'location'); + if (is_string($location) && substr($location, 0, 4) == 'geo:') { + preg_match_all( + '/([0-9\.\-]+)/', + $location, + $matches + ); + + return $matches[0][0] . ', ' . $matches[0][1]; + } + + return null; + } + + private function getCheckin(array $request): ?Place + { + if (array_get($request, 'properties.location.0.type.0') === 'h-card') { + try { + $place = resolve(PlaceService::class)->createPlaceFromCheckin( + array_get($request, 'properties.location.0') + ); + } catch (\InvalidArgumentException $e) { + return null; + } + + return $place; + } + if (starts_with(array_get($request, 'properties.location.0'), config('app.url'))) { + return Place::where( + 'slug', + basename( + parse_url( + array_get($request, 'properties.location.0'), + PHP_URL_PATH + ) + ) + )->first(); + } + if (array_get($request, 'properties.checkin')) { + try { + $place = resolve(PlaceService::class)->createPlaceFromCheckin( + array_get($request, 'properties.checkin.0') + ); + } catch (\InvalidArgumentException $e) { + return null; + } + + return $place; + } + + return null; + } + + private function getSwarmUrl(array $request): ?string + { + if (stristr(array_get($request, 'properties.syndication.0', ''), 'swarmapp')) { + return array_get($request, 'properties.syndication.0'); + } + + return null; + } + + private function getSyndicationTargets(array $request): array + { + $syndication = []; + $targets = array_pluck(config('syndication.targets'), 'uid', 'service.name'); + $mpSyndicateTo = array_get($request, 'mp-syndicate-to') ?? array_get($request, 'properties.mp-syndicate-to'); + if (is_string($mpSyndicateTo)) { + $service = array_search($mpSyndicateTo, $targets); + if ($service == 'Twitter') { + $syndication[] = 'twitter'; + } + if ($service == 'Facebook') { + $syndication[] = 'facebook'; + } + } + if (is_array($mpSyndicateTo)) { + foreach ($mpSyndicateTo as $uid) { + $service = array_search($uid, $targets); + if ($service == 'Twitter') { + $syndication[] = 'twitter'; + } + if ($service == 'Facebook') { + $syndication[] = 'facebook'; + } + } + } + + return $syndication; + } + + private function getMedia(array $request): array + { + $media = []; + $photos = array_get($request, 'photo') ?? array_get($request, 'properties.photo'); + + if (isset($photos)) { + foreach ((array) $photos as $photo) { + // check the media was uploaded to my endpoint, and use path + if (starts_with($photo, config('filesystems.disks.s3.url'))) { + $path = substr($photo, strlen(config('filesystems.disks.s3.url'))); + $media[] = Media::where('path', ltrim($path, '/'))->firstOrFail(); + } else { + $newMedia = Media::firstOrNew(['path' => $photo]); + // currently assuming this is a photo from Swarm or OwnYourGram + $newMedia->type = 'image'; + $newMedia->save(); + $media[] = $newMedia; + } + } + } + + return $media; + } + + private function getInstagramUrl(array $request): ?string + { + if (starts_with(array_get($request, 'properties.syndication.0'), 'https://www.instagram.com')) { + return array_get($request, 'properties.syndication.0'); + } + + return null; + } } diff --git a/app/Services/PlaceService.php b/app/Services/PlaceService.php index 7d397a5e..ddce5cd6 100644 --- a/app/Services/PlaceService.php +++ b/app/Services/PlaceService.php @@ -19,7 +19,7 @@ class PlaceService { //obviously a place needs a lat/lng, but this could be sent in a geo-url //if no geo array key, we assume the array already has lat/lng values - if (array_key_exists('geo', $data)) { + if (array_key_exists('geo', $data) && $data['geo'] !== null) { preg_match_all( '/([0-9\.\-]+)/', $data['geo'], @@ -47,7 +47,7 @@ class PlaceService { //check if the place exists if from swarm if (array_has($checkin, 'properties.url')) { - $place = Place::whereExternalURL($checkin['properties']['url'][0])->get(); + $place = Place::whereExternalURL(array_get($checkin, 'properties.url.0'))->get(); if (count($place) === 1) { return $place->first(); } @@ -59,11 +59,11 @@ class PlaceService throw new \InvalidArgumentException('Missing required longitude/latitude'); } $place = new Place(); - $place->name = $checkin['properties']['name'][0]; - $place->external_urls = $checkin['properties']['url'][0]; + $place->name = array_get($checkin, 'properties.name.0'); + $place->external_urls = array_get($checkin, 'properties.url.0'); $place->location = new Point( - (float) $checkin['properties']['latitude'][0], - (float) $checkin['properties']['longitude'][0] + (float) array_get($checkin, 'properties.latitude.0'), + (float) array_get($checkin, 'properties.longitude.0') ); $place->save(); diff --git a/tests/Feature/MicropubControllerTest.php b/tests/Feature/MicropubControllerTest.php index 1fd006b2..d4e5f978 100644 --- a/tests/Feature/MicropubControllerTest.php +++ b/tests/Feature/MicropubControllerTest.php @@ -2,14 +2,21 @@ namespace Tests\Feature; +use App\Media; +use App\Place; +use Carbon\Carbon; use Tests\TestCase; use Tests\TestToken; use Lcobucci\JWT\Builder; use App\Jobs\ProcessMedia; +use App\Jobs\SendWebMentions; use Illuminate\Http\UploadedFile; +use App\Jobs\SyndicateNoteToTwitter; use Lcobucci\JWT\Signer\Hmac\Sha256; +use App\Jobs\SyndicateNoteToFacebook; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Storage; +use Phaza\LaravelPostgis\Geometries\Point; use Illuminate\Foundation\Testing\DatabaseTransactions; class MicropubControllerTest extends TestCase @@ -124,7 +131,9 @@ class MicropubControllerTest extends TestCase '/api/post', [ 'h' => 'entry', - 'content' => $note + 'content' => $note, + 'published' => Carbon::now()->toW3CString(), + 'location' => 'geo:1.23,4.56', ], [], [], @@ -134,6 +143,60 @@ class MicropubControllerTest extends TestCase $this->assertDatabaseHas('notes', ['note' => $note]); } + /** + * Test a valid micropub requests creates a new note and syndicates to Twitter. + * + * @return void + */ + public function test_micropub_post_request_creates_new_note_sends_to_twitter() + { + Queue::fake(); + $faker = \Faker\Factory::create(); + $note = $faker->text; + $response = $this->call( + 'POST', + '/api/post', + [ + 'h' => 'entry', + 'content' => $note, + 'mp-syndicate-to' => 'https://twitter.com/jonnybarnes' + ], + [], + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response->assertJson(['response' => 'created']); + $this->assertDatabaseHas('notes', ['note' => $note]); + Queue::assertPushed(SyndicateNoteToTwitter::class); + } + + /** + * Test a valid micropub requests creates a new note and syndicates to Facebook. + * + * @return void + */ + public function test_micropub_post_request_creates_new_note_sends_to_facebook() + { + Queue::fake(); + $faker = \Faker\Factory::create(); + $note = $faker->text; + $response = $this->call( + 'POST', + '/api/post', + [ + 'h' => 'entry', + 'content' => $note, + 'mp-syndicate-to' => 'https://facebook.com/jonnybarnes' + ], + [], + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response->assertJson(['response' => 'created']); + $this->assertDatabaseHas('notes', ['note' => $note]); + Queue::assertPushed(SyndicateNoteToFacebook::class); + } + /** * Test a valid micropub requests creates a new place. * @@ -182,12 +245,135 @@ class MicropubControllerTest extends TestCase $this->assertDatabaseHas('places', ['slug' => 'the-barton-arms']); } + public function test_micropub_post_request_with_invalid_token_returns_expected_error_response() + { + $response = $this->call( + 'POST', + '/api/post', + [ + 'h' => 'entry', + 'content' => 'A random note', + ], + [], + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] + ); + $response->assertStatus(400); + $response->assertJson(['error' => 'invalid_token']); + } + + public function test_micropub_post_request_with_scopeless_token_returns_expected_error_response() + { + $response = $this->call( + 'POST', + '/api/post', + [ + 'h' => 'entry', + 'content' => 'A random note', + ], + [], + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithNoScope()] + ); + $response->assertStatus(400); + $response->assertJson(['error_description' => 'The provided token has no scopes']); + } + + public function test_micropub_post_request_for_place_without_create_scope_errors() + { + $response = $this->call( + 'POST', + '/api/post', + [ + 'h' => 'card', + 'name' => 'The Barton Arms', + 'geo' => 'geo:53.4974,-2.3768' + ], + [], + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] + ); + $response->assertStatus(401); + $response->assertJson(['error' => 'insufficient_scope']); + } + /** * Test a valid micropub requests using JSON syntax creates a new note. * * @return void */ public function test_micropub_post_request_with_json_syntax_creates_new_note() + { + Queue::fake(); + Media::create([ + 'path' => 'test-photo.jpg', + 'type' => 'image', + ]); + $faker = \Faker\Factory::create(); + $note = $faker->text; + $response = $this->json( + 'POST', + '/api/post', + [ + 'type' => ['h-entry'], + 'properties' => [ + 'content' => [$note], + 'in-reply-to' => ['https://aaronpk.localhost'], + 'mp-syndicate-to' => [ + 'https://twitter.com/jonnybarnes', + 'https://facebook.com/jonnybarnes', + ], + 'photo' => [config('filesystems.disks.s3.url') . '/test-photo.jpg'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertStatus(201) + ->assertJson(['response' => 'created']); + Queue::assertPushed(SendWebMentions::class); + Queue::assertPushed(SyndicateNoteToTwitter::class); + Queue::assertPushed(SyndicateNoteToFacebook::class); + } + + /** + * Test a valid micropub requests using JSON syntax creates a new note with + * existing self-created place. + * + * @return void + */ + public function test_micropub_post_request_with_json_syntax_creates_new_note_with_existing_place_in_location() + { + $place = new Place(); + $place->name = 'Test Place'; + $place->location = new Point((float) 1.23, (float) 4.56); + $place->save(); + $faker = \Faker\Factory::create(); + $note = $faker->text; + $response = $this->json( + 'POST', + '/api/post', + [ + 'type' => ['h-entry'], + 'properties' => [ + 'content' => [$note], + 'location' => [$place->longurl], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertStatus(201) + ->assertJson(['response' => 'created']); + } + + /** + * Test a valid micropub requests using JSON syntax creates a new note with + * a new place defined in the location block. + * + * @return void + */ + public function test_micropub_post_request_with_json_syntax_creates_new_note_with_new_place_in_location() { $faker = \Faker\Factory::create(); $note = $faker->text; @@ -198,6 +384,14 @@ class MicropubControllerTest extends TestCase 'type' => ['h-entry'], 'properties' => [ 'content' => [$note], + 'location' => [[ + 'type' => ['h-card'], + 'properties' => [ + 'name' => ['Awesome Venue'], + 'latitude' => ['1.23'], + 'longitude' => ['4.56'], + ], + ]], ], ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] @@ -205,6 +399,44 @@ class MicropubControllerTest extends TestCase $response ->assertStatus(201) ->assertJson(['response' => 'created']); + $this->assertDatabaseHas('places', [ + 'name' => 'Awesome Venue', + ]); + } + + /** + * Test a valid micropub requests using JSON syntax creates a new note without + * a new place defined in the location block if there is missing data. + * + * @return void + */ + public function test_micropub_post_request_with_json_syntax_creates_new_note_without_new_place_in_location() + { + $faker = \Faker\Factory::create(); + $note = $faker->text; + $response = $this->json( + 'POST', + '/api/post', + [ + 'type' => ['h-entry'], + 'properties' => [ + 'content' => [$note], + 'location' => [[ + 'type' => ['h-card'], + 'properties' => [ + 'name' => ['Awesome Venue'], + ], + ]], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertStatus(201) + ->assertJson(['response' => 'created']); + $this->assertDatabaseMissing('places', [ + 'name' => 'Awesome Venue', + ]); } /** @@ -236,12 +468,12 @@ class MicropubControllerTest extends TestCase } /** - * Test a micropub requests using JSON syntax without a valis token returns + * Test a micropub requests using JSON syntax without a valid token returns * an error. Also check the message. * * @return void */ - public function test_micropub_post_request_with_json_syntax_with_invalid_token_returns_error() + public function test_micropub_post_request_with_json_syntax_with_insufficient_token_returns_error() { $faker = \Faker\Factory::create(); $note = $faker->text; @@ -254,7 +486,7 @@ class MicropubControllerTest extends TestCase 'content' => [$note], ], ], - ['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] ); $response ->assertJson([ @@ -264,6 +496,27 @@ class MicropubControllerTest extends TestCase ->assertStatus(401); } + public function test_micropub_post_request_with_json_syntax_for_unsupported_type_returns_error() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'type' => ['h-unsopported'], // a request type I don’t support + 'properties' => [ + 'content' => ['Some content'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertJson([ + 'response' => 'error', + 'error_description' => 'unsupported_request_type' + ]) + ->assertStatus(500); + } + public function test_micropub_post_request_with_json_syntax_creates_new_place() { $faker = \Faker\Factory::create(); @@ -332,7 +585,10 @@ class MicropubControllerTest extends TestCase 'action' => 'update', 'url' => config('app.url') . '/notes/A', 'add' => [ - 'syndication' => ['https://www.swarmapp.com/checkin/123'], + 'syndication' => [ + 'https://www.swarmapp.com/checkin/123', + 'https://www.facebook.com/checkin/123', + ], ], ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] @@ -341,7 +597,132 @@ class MicropubControllerTest extends TestCase ->assertJson(['response' => 'updated']) ->assertStatus(200); $this->assertDatabaseHas('notes', [ - 'swarm_url' => 'https://www.swarmapp.com/checkin/123' + 'swarm_url' => 'https://www.swarmapp.com/checkin/123', + 'facebook_url' => 'https://www.facebook.com/checkin/123', + ]); + } + + public function test_micropub_post_request_with_json_syntax_update_add_image_to_post() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'action' => 'update', + 'url' => config('app.url') . '/notes/A', + 'add' => [ + 'photo' => ['https://example.org/photo.jpg'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertJson(['response' => 'updated']) + ->assertStatus(200); + $this->assertDatabaseHas('media_endpoint', [ + 'path' => 'https://example.org/photo.jpg', + ]); + } + + public function test_micropub_post_request_with_json_syntax_update_add_post_errors_for_non_note() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'action' => 'update', + 'url' => config('app.url') . '/blog/A', + 'add' => [ + 'syndication' => ['https://www.swarmapp.com/checkin/123'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertJson(['error' => 'invalid']) + ->assertStatus(500); + } + + public function test_micropub_post_request_with_json_syntax_update_add_post_errors_for_note_not_found() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'action' => 'update', + 'url' => config('app.url') . '/notes/ZZZZ', + 'add' => [ + 'syndication' => ['https://www.swarmapp.com/checkin/123'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertJson(['error' => 'invalid_request']) + ->assertStatus(404); + } + + public function test_micropub_post_request_with_json_syntax_update_add_post_errors_for_unsupported_request() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'action' => 'update', + 'url' => config('app.url') . '/notes/A', + 'morph' => [ // or any other unsupported update type + 'syndication' => ['https://www.swarmapp.com/checkin/123'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertJson(['response' => 'error']) + ->assertStatus(500); + } + + public function test_micropub_post_request_with_json_syntax_update_errors_for_insufficient_scope() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'action' => 'update', + 'url' => config('app.url') . '/notes/B', + 'add' => [ + 'syndication' => ['https://www.swarmapp.com/checkin/123'], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] + ); + $response + ->assertStatus(401) + ->assertJson(['error' => 'insufficient_scope']); + } + + public function test_micropub_post_request_with_json_syntax_update_replace_post_syndication() + { + $response = $this->json( + 'POST', + '/api/post', + [ + 'action' => 'update', + 'url' => config('app.url') . '/notes/L', + 'replace' => [ + 'syndication' => [ + 'https://www.swarmapp.com/checkin/the-id', + 'https://www.facebook.com/post/the-id', + ], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertJson(['response' => 'updated']) + ->assertStatus(200); + $this->assertDatabaseHas('notes', [ + 'swarm_url' => 'https://www.swarmapp.com/checkin/the-id', + 'facebook_url' => 'https://www.facebook.com/post/the-id', ]); } @@ -359,6 +740,20 @@ class MicropubControllerTest extends TestCase $response->assertJsonFragment(['error_description' => 'The provided token did not pass validation']); } + public function test_media_endpoint_request_with_token_with_no_scope_returns_400_response() + { + $response = $this->call( + 'POST', + '/api/media', + [], + [], + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithNoScope()] + ); + $response->assertStatus(400); + $response->assertJsonFragment(['error_description' => 'The provided token has no scopes']); + } + public function test_media_endpoint_request_with_insufficient_token_scopes_returns_401_response() { $response = $this->call( @@ -367,7 +762,7 @@ class MicropubControllerTest extends TestCase [], [], [], - ['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] ); $response->assertStatus(401); $response->assertJsonFragment(['error_description' => 'The token’s scope does not have the necessary requirements.']); @@ -394,4 +789,91 @@ class MicropubControllerTest extends TestCase Queue::assertPushed(ProcessMedia::class); Storage::disk('local')->assertExists($filename); } + + public function test_media_endpoint_upload_an_audio_file() + { + Queue::fake(); + Storage::fake('local'); + $file = __DIR__ . '/../audio.mp3'; + + $response = $this->call( + 'POST', + '/api/media', + [], + [], + [ + 'file' => new UploadedFile($file, 'audio.mp3', 'audio/mpeg', filesize($file), null, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + } + + public function test_media_endpoint_upload_a_video_file() + { + Queue::fake(); + Storage::fake('local'); + $file = __DIR__ . '/../video.ogv'; + + $response = $this->call( + 'POST', + '/api/media', + [], + [], + [ + 'file' => new UploadedFile($file, 'video.ogv', 'video/ogg', filesize($file), null, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + } + + public function test_media_endpoint_upload_a_document_file() + { + Queue::fake(); + Storage::fake('local'); + + $response = $this->call( + 'POST', + '/api/media', + [], + [], + [ + 'file' => UploadedFile::fake()->create('document.pdf', 100), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + } + + public function test_media_endpoint_upload_an_invalid_file_return_error() + { + Queue::fake(); + Storage::fake('local'); + + $response = $this->call( + 'POST', + '/api/media', + [], + [], + [ + 'file' => new UploadedFile(__DIR__ . '/../aaron.png', 'aaron.png', 'image/png', UPLOAD_ERR_INI_SIZE, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response->assertStatus(400); + $response->assertJson(['error_description' => 'The uploaded file failed validation']); + } } diff --git a/tests/Feature/SwarmTest.php b/tests/Feature/SwarmTest.php index 1c0b0730..3b929602 100644 --- a/tests/Feature/SwarmTest.php +++ b/tests/Feature/SwarmTest.php @@ -153,4 +153,37 @@ class SwarmTest extends TestCase 'swarm_url' => 'https://www.swarmapp.com/checkin/def' ]); } + + public function test_faked_ownyourswarm_request_saves_just_post_when_error_in_checkin_data() + { + $response = $this->json( + 'POST', + 'api/post', + [ + 'type' => ['h-entry'], + 'properties' => [ + 'published' => [\Carbon\Carbon::now()->toDateTimeString()], + 'syndication' => ['https://www.swarmapp.com/checkin/abc'], + 'content' => [[ + 'value' => 'My first #checkin using Example Product', + 'html' => 'My first #checkin using Example Product', + ]], + 'checkin' => [[ + 'type' => ['h-card'], + 'properties' => [ + 'name' => ['Awesome Venue'], + 'url' => ['https://foursquare.com/v/123456'], + ], + ]], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response + ->assertStatus(201) + ->assertJson(['response' => 'created']); + $this->assertDatabaseMissing('places', [ + 'name' => 'Awesome Venue', + ]); + } } diff --git a/tests/Feature/TokenEndpointTest.php b/tests/Feature/TokenEndpointTest.php index 93208248..67739729 100644 --- a/tests/Feature/TokenEndpointTest.php +++ b/tests/Feature/TokenEndpointTest.php @@ -32,4 +32,46 @@ class TokenEndpointTest extends TestCase $this->assertEquals(config('app.url'), $output['me']); $this->assertTrue(array_key_exists('access_token', $output)); } + + public function test_token_endpoint_returns_error_when_auth_endpoint_lacks_me_data() + { + $mockClient = Mockery::mock(Client::class); + $mockClient->shouldReceive('discoverAuthorizationEndpoint') + ->with(normalize_url(config('app.url'))) + ->once() + ->andReturn('https://indieauth.com/auth'); + $mockClient->shouldReceive('verifyIndieAuthCode') + ->andReturn([ + 'error' => 'error_message', + ]); + $this->app->instance(Client::class, $mockClient); + $response = $this->post('/api/token', [ + 'me' => config('app.url'), + 'code' => 'abc123', + 'redirect_uri' => config('app.url') . '/indieauth-callback', + 'client_id' => config('app.url') . '/micropub-client', + 'state' => mt_rand(1000, 10000), + ]); + $response->assertStatus(400); + $response->assertSeeText('There was an error verifying the authorisation code.'); + } + + public function test_token_endpoint_returns_error_when_no_auth_endpoint_found() + { + $mockClient = Mockery::mock(Client::class); + $mockClient->shouldReceive('discoverAuthorizationEndpoint') + ->with(normalize_url(config('app.url'))) + ->once() + ->andReturn(null); + $this->app->instance(Client::class, $mockClient); + $response = $this->post('/api/token', [ + 'me' => config('app.url'), + 'code' => 'abc123', + 'redirect_uri' => config('app.url') . '/indieauth-callback', + 'client_id' => config('app.url') . '/micropub-client', + 'state' => mt_rand(1000, 10000), + ]); + $response->assertStatus(400); + $response->assertSeeText('Can’t determine the authorisation endpoint.'); + } } diff --git a/tests/TestToken.php b/tests/TestToken.php index 4cce0892..b34db254 100644 --- a/tests/TestToken.php +++ b/tests/TestToken.php @@ -21,7 +21,7 @@ trait TestToken return $token; } - public function getInvalidToken() + public function getTokenWithIncorrectScope() { $signer = new Sha256(); $token = (new Builder()) @@ -34,4 +34,24 @@ trait TestToken return $token; } + + public function getTokenWithNoScope() + { + $signer = new Sha256(); + $token = (new Builder()) + ->set('client_id', 'https://quill.p3k.io') + ->set('me', 'https://jonnybarnes.localhost') + ->set('issued_at', time()) + ->sign($signer, env('APP_KEY')) + ->getToken(); + + return $token; + } + + public function getInvalidToken() + { + $token = $this->getToken(); + + return substr($token, 0, -5); + } } diff --git a/tests/audio.mp3 b/tests/audio.mp3 new file mode 100644 index 00000000..5753d47d Binary files /dev/null and b/tests/audio.mp3 differ diff --git a/tests/video.ogv b/tests/video.ogv new file mode 100644 index 00000000..6777b3ba Binary files /dev/null and b/tests/video.ogv differ