diff --git a/.env.github b/.env.github index 6ebe09fb..dbc69b67 100644 --- a/.env.github +++ b/.env.github @@ -7,7 +7,7 @@ APP_LOG_LEVEL=warning DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 -DB_DATABASE=jbuktest +DB_DATABASE=jbukdev_testing DB_USERNAME=postgres DB_PASSWORD=postgres diff --git a/.env.travis b/.env.travis deleted file mode 100644 index 3b70d5d2..00000000 --- a/.env.travis +++ /dev/null @@ -1,17 +0,0 @@ -APP_ENV=testing -APP_DEBUG=true -APP_KEY=base64:6DJhvZLVjE6dD4Cqrteh+6Z5vZlG+v/soCKcDHLOAH0= -APP_URL=http://jonnybarnes.localhost -APP_LONGURL=jonnybarnes.localhost -APP_SHORTURL=jmb.localhost - -DB_CONNECTION=travis - -CACHE_DRIVER=array -SESSION_DRIVER=array -QUEUE_DRIVER=sync - -SCOUT_DRIVER=pgsql - -DISPLAY_NAME='Travis Test' -USER_NAME=travis diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index fa56088f..f424f904 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,11 +11,11 @@ jobs: services: postgres: - image: postgres:13.1 + image: postgres:13.4 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: jbuktest + POSTGRES_DB: jbukdev_testing ports: - 5432:5432 @@ -36,15 +36,13 @@ jobs: - name: Copy .env run: php -r "file_exists('.env') || copy('.env.github', '.env');" - name: Install dependencies - run: composer install -q --no-ansi --no-interaction --no-progress + run: composer install --quiet --no-ansi --no-interaction --no-progress - name: Generate key run: php artisan key:generate - name: Setup directory permissions run: chmod -R 777 storage bootstrap/cache - name: Setup test database - run: | - php artisan migrate - php artisan db:seed + run: php artisan migrate - name: Execute tests (Unit and Feature tests) via PHPUnit run: vendor/bin/phpunit - name: Run phpcs diff --git a/app/Console/Commands/ParseCachedWebMentions.php b/app/Console/Commands/ParseCachedWebMentions.php index 2183cd4a..6bdd1862 100644 --- a/app/Console/Commands/ParseCachedWebMentions.php +++ b/app/Console/Commands/ParseCachedWebMentions.php @@ -24,16 +24,6 @@ class ParseCachedWebMentions extends Command */ protected $description = 'Re-parse the webmention’s cached HTML'; - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - /** * Execute the console command. * @@ -41,15 +31,15 @@ class ParseCachedWebMentions extends Command */ public function handle(FileSystem $filesystem) { - $HTMLfiles = $filesystem->allFiles(storage_path() . '/HTML'); - foreach ($HTMLfiles as $file) { - if ($file->getExtension() != 'backup') { //we don’t want to parse.backup files + $htmlFiles = $filesystem->allFiles(storage_path() . '/HTML'); + foreach ($htmlFiles as $file) { + if ($file->getExtension() !== 'backup') { //we don’t want to parse `.backup` files $filepath = $file->getPathname(); $this->info('Loading HTML from: ' . $filepath); $html = $filesystem->get($filepath); - $url = $this->URLFromFilename($filepath); - $microformats = \Mf2\parse($html, $url); + $url = $this->urlFromFilename($filepath); $webmention = WebMention::where('source', $url)->firstOrFail(); + $microformats = \Mf2\parse($html, $url); $webmention->mf2 = json_encode($microformats); $webmention->save(); $this->info('Saved the microformats to the database.'); @@ -63,12 +53,12 @@ class ParseCachedWebMentions extends Command * @param string * @return string */ - private function URLFromFilename(string $filepath): string + private function urlFromFilename(string $filepath): string { $dir = mb_substr($filepath, mb_strlen(storage_path() . '/HTML/')); $url = str_replace(['http/', 'https/'], ['http://', 'https://'], $dir); - if (mb_substr($url, -10) == 'index.html') { - $url = mb_substr($url, 0, mb_strlen($url) - 10); + if (mb_substr($url, -10) === 'index.html') { + $url = mb_substr($url, 0, -10); } return $url; diff --git a/app/Http/Controllers/Admin/ArticlesController.php b/app/Http/Controllers/Admin/ArticlesController.php index 30e5e275..3f390304 100644 --- a/app/Http/Controllers/Admin/ArticlesController.php +++ b/app/Http/Controllers/Admin/ArticlesController.php @@ -7,16 +7,10 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Article; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Illuminate\View\View; class ArticlesController extends Controller { - /** - * List the articles that can be edited. - * - * @return \Illuminate\View\View - */ public function index(): View { $posts = Article::select('id', 'title', 'published')->orderBy('id', 'desc')->get(); @@ -24,11 +18,6 @@ class ArticlesController extends Controller return view('admin.articles.index', ['posts' => $posts]); } - /** - * Show the new article form. - * - * @return \Illuminate\View\View - */ public function create(): View { $message = session('message'); @@ -36,11 +25,6 @@ class ArticlesController extends Controller return view('admin.articles.create', ['message' => $message]); } - /** - * Process an incoming request for a new article and save it. - * - * @return \Illuminate\Http\RedirectResponse - */ public function store(): RedirectResponse { //if a `.md` is attached use that for the main content. @@ -49,42 +33,21 @@ class ArticlesController extends Controller $content = $file->fread($file->getSize()); } $main = $content ?? request()->input('main'); - $article = Article::create( - [ - 'url' => request()->input('url'), - 'title' => request()->input('title'), - 'main' => $main, - 'published' => request()->input('published') ?? 0, - ] - ); + Article::create([ + 'url' => request()->input('url'), + 'title' => request()->input('title'), + 'main' => $main, + 'published' => request()->input('published') ?? 0, + ]); return redirect('/admin/blog'); } - /** - * Show the edit form for an existing article. - * - * @param int $articleId - * @return \Illuminate\View\View - */ - public function edit(int $articleId): View + public function edit(Article $article): View { - $post = Article::select( - 'title', - 'main', - 'url', - 'published' - )->where('id', $articleId)->get(); - - return view('admin.articles.edit', ['id' => $articleId, 'post' => $post]); + return view('admin.articles.edit', ['article' => $article]); } - /** - * Process an incoming request to edit an article. - * - * @param int $articleId - * @return \Illuminate\Http\RedirectResponse - */ public function update(int $articleId): RedirectResponse { $article = Article::find($articleId); @@ -97,12 +60,6 @@ class ArticlesController extends Controller return redirect('/admin/blog'); } - /** - * Process a request to delete an aricle. - * - * @param int $articleId - * @return \Illuminate\Http\RedirectResponse - */ public function destroy(int $articleId): RedirectResponse { Article::where('id', $articleId)->delete(); diff --git a/app/Http/Controllers/ArticlesController.php b/app/Http/Controllers/ArticlesController.php index 6d16589a..d70209d7 100644 --- a/app/Http/Controllers/ArticlesController.php +++ b/app/Http/Controllers/ArticlesController.php @@ -59,18 +59,14 @@ class ArticlesController extends Controller * We only have the ID, work out post title, year and month * and redirect to it. * - * @param int $idFromUrl + * @param string $idFromUrl * @return RedirectResponse */ - public function onlyIdInUrl(int $idFromUrl): RedirectResponse + public function onlyIdInUrl(string $idFromUrl): RedirectResponse { - $realId = resolve(Numbers::class)->b60tonum((string) $idFromUrl); + $realId = resolve(Numbers::class)->b60tonum($idFromUrl); - try { - $article = Article::findOrFail($realId); - } catch (ModelNotFoundException $exception) { - abort(404); - } + $article = Article::findOrFail($realId); return redirect($article->link); } diff --git a/app/Http/Middleware/CSPHeader.php b/app/Http/Middleware/CSPHeader.php index 649f3a03..3e3f7f32 100644 --- a/app/Http/Middleware/CSPHeader.php +++ b/app/Http/Middleware/CSPHeader.php @@ -18,19 +18,15 @@ class CSPHeader { // headers have to be single-line strings, // so we concat multiple lines - // phpcs:disable + // phpcs:disable Generic.Files.LineLength.TooLong return $next($request) ->header( 'Content-Security-Policy', "default-src 'self'; " . - "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://api.mapbox.com https://api.tiles.mapbox.com blob:; " . - "style-src 'self' 'unsafe-inline' https://api.mapbox.com https://api.tiles.mapbox.com cloud.typography.com jonnybarnes.uk; " . - "img-src 'self' data: blob: https://pbs.twimg.com https://api.mapbox.com https://*.tiles.mapbox.com https://jbuk-media.s3-eu-west-1.amazonaws.com https://jbuk-media-dev.s3-eu-west-1.amazonaws.com https://secure.gravatar.com https://graph.facebook.com *.fbcdn.net https://*.cdninstagram.com https://*.4sqi.net https://upload.wikimedia.org; " . + "style-src 'self' cloud.typography.com jonnybarnes.uk; " . + "img-src 'self' data: blob: https://pbs.twimg.com https://jbuk-media.s3-eu-west-1.amazonaws.com https://jbuk-media-dev.s3-eu-west-1.amazonaws.com https://secure.gravatar.com https://graph.facebook.com *.fbcdn.net https://*.cdninstagram.com https://*.4sqi.net https://upload.wikimedia.org; " . "font-src 'self' data:; " . - "connect-src 'self' https://api.mapbox.com https://*.tiles.mapbox.com https://events.mapbox.com data: blob:; " . - "worker-src 'self' blob:; " . "frame-src 'self' https://www.youtube.com blob:; " . - 'child-src blob:; ' . 'upgrade-insecure-requests; ' . 'block-all-mixed-content; ' . 'report-to csp-endpoint; ' . @@ -43,6 +39,6 @@ class CSPHeader "'max-age': 10886400" . '}' ); - // phpcs:enable + // phpcs:enable Generic.Files.LineLength.TooLong } } diff --git a/app/Jobs/SaveProfileImage.php b/app/Jobs/SaveProfileImage.php index cf2197e3..ebdf0ca9 100644 --- a/app/Jobs/SaveProfileImage.php +++ b/app/Jobs/SaveProfileImage.php @@ -41,11 +41,13 @@ class SaveProfileImage implements ShouldQueue { try { $author = $authorship->findAuthor($this->microformats); - } catch (AuthorshipParserException $e) { - return; + } catch (AuthorshipParserException) { + return null; } + $photo = Arr::get($author, 'properties.photo.0'); $home = Arr::get($author, 'properties.url.0'); + //dont save pbs.twimg.com links if ( $photo @@ -53,16 +55,18 @@ class SaveProfileImage implements ShouldQueue && parse_url($photo, PHP_URL_HOST) != 'twitter.com' ) { $client = resolve(Client::class); + try { $response = $client->get($photo); $image = $response->getBody(); - } catch (RequestException $e) { + } catch (RequestException) { // we are opening and reading the default image so that $default = public_path() . '/assets/profile-images/default-image'; $handle = fopen($default, 'rb'); $image = fread($handle, filesize($default)); fclose($handle); } + $path = public_path() . '/assets/profile-images/' . parse_url($home, PHP_URL_HOST) . '/image'; $parts = explode('/', $path); $name = array_pop($parts); diff --git a/app/Jobs/SendWebMentions.php b/app/Jobs/SendWebMentions.php index 2c566718..73aff831 100644 --- a/app/Jobs/SendWebMentions.php +++ b/app/Jobs/SendWebMentions.php @@ -6,7 +6,10 @@ namespace App\Jobs; use App\Models\Note; use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Header; use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Psr7\UriResolver; +use GuzzleHttp\Psr7\Utils; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; @@ -65,14 +68,14 @@ class SendWebMentions implements ShouldQueue * @param string $url * @return string|null */ - public function discoverWebmentionEndpoint(string $url) + public function discoverWebmentionEndpoint(string $url): ?string { //let’s not send webmentions to myself - if (parse_url($url, PHP_URL_HOST) == config('app.longurl')) { - return; + if (parse_url($url, PHP_URL_HOST) === config('app.longurl')) { + return null; } if (Str::startsWith($url, '/notes/tagged/')) { - return; + return null; } $endpoint = null; @@ -80,7 +83,7 @@ class SendWebMentions implements ShouldQueue $guzzle = resolve(Client::class); $response = $guzzle->get($url); //check HTTP Headers for webmention endpoint - $links = \GuzzleHttp\Psr7\parse_header($response->getHeader('Link')); + $links = Header::parse($response->getHeader('Link')); foreach ($links as $link) { if (mb_stristr($link['rel'], 'webmention')) { return $this->resolveUri(trim($link[0], '<>'), $url); @@ -110,7 +113,7 @@ class SendWebMentions implements ShouldQueue */ public function getLinks(?string $html): array { - if ($html == '' || is_null($html)) { + if ($html === '' || is_null($html)) { return []; } @@ -136,13 +139,13 @@ class SendWebMentions implements ShouldQueue */ public function resolveUri(string $url, string $base): string { - $endpoint = \GuzzleHttp\Psr7\uri_for($url); - if ($endpoint->getScheme() != '') { + $endpoint = Utils::uriFor($url); + if ($endpoint->getScheme() !== '') { return (string) $endpoint; } - return (string) Uri::resolve( - \GuzzleHttp\Psr7\uri_for($base), + return (string) UriResolver::resolve( + Utils::uriFor($base), $endpoint ); } diff --git a/app/Models/Article.php b/app/Models/Article.php index a48c1ed9..5c9516c8 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -7,6 +7,7 @@ namespace App\Models; use Cviebrock\EloquentSluggable\Sluggable; use Eloquent; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -58,6 +59,7 @@ use Spatie\CommonMarkHighlighter\IndentedCodeRenderer; */ class Article extends Model { + use HasFactory; use Sluggable; use SoftDeletes; diff --git a/app/Models/Contact.php b/app/Models/Contact.php index a15a4fc2..30187264 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -6,6 +6,7 @@ namespace App\Models; use Eloquent; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; @@ -35,6 +36,8 @@ use Illuminate\Support\Carbon; */ class Contact extends Model { + use HasFactory; + /** * The database table used by the model. * diff --git a/app/Models/Media.php b/app/Models/Media.php index df8a4666..e7b0cd88 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -6,6 +6,7 @@ namespace App\Models; use Eloquent; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Carbon; @@ -41,6 +42,8 @@ use Illuminate\Support\Str; */ class Media extends Model { + use HasFactory; + /** * The table associated with the model. * @@ -62,7 +65,7 @@ class Media extends Model */ public function note(): BelongsTo { - return $this->belongsTo('App\Models\Note'); + return $this->belongsTo(Note::class); } /** @@ -118,7 +121,7 @@ class Media extends Model $filenameParts = explode('.', $path); array_pop($filenameParts); - return ltrim(array_reduce($filenameParts, function ($carry, $item) { + return ltrim(array_reduce($filenameParts, static function ($carry, $item) { return $carry . '.' . $item; }, ''), '.'); } diff --git a/app/Models/Note.php b/app/Models/Note.php index d86b8eac..6e4c2f8e 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -5,14 +5,15 @@ declare(strict_types=1); namespace App\Models; use App\Exceptions\TwitterContentException; +use Barryvdh\LaravelIdeHelper\Eloquent; use Codebird\Codebird; -use Eloquent; use Exception; use GuzzleHttp\Client; use Illuminate\Database\Eloquent\Relations\{BelongsTo, BelongsToMany, HasMany, MorphMany}; use Illuminate\Database\Eloquent\{Builder, Collection, Factories\HasFactory, Model, SoftDeletes}; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; +use JetBrains\PhpStorm\ArrayShape; use Jonnybarnes\IndieWeb\Numbers; use Laravel\Scout\Searchable; use League\CommonMark\Block\Element\{FencedCode, IndentedCode}; @@ -20,6 +21,11 @@ use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\{CommonMarkConverter, Environment}; use Normalizer; use Spatie\CommonMarkHighlighter\{FencedCodeRenderer, IndentedCodeRenderer}; +use App\Models\Tag; +use App\Models\MicropubClient; +use App\Models\WebMention; +use App\Models\Place; +use App\Models\Media; /** * App\Models\Note. @@ -102,12 +108,12 @@ class Note extends Model /** * This variable is used to keep track of contacts in a note. */ - protected $contacts; + protected ?array $contacts; /** * Set our contacts variable to null. * - * @param array $attributes + * @param array $attributes */ public function __construct(array $attributes = []) { @@ -145,9 +151,9 @@ class Note extends Model * * @return BelongsToMany */ - public function tags() + public function tags(): BelongsToMany { - return $this->belongsToMany('App\Models\Tag'); + return $this->belongsToMany(Tag::class); } /** @@ -155,9 +161,9 @@ class Note extends Model * * @return BelongsTo */ - public function client() + public function client(): BelongsTo { - return $this->belongsTo('App\Models\MicropubClient', 'client_id', 'client_url'); + return $this->belongsTo(MicropubClient::class, 'client_id', 'client_url'); } /** @@ -165,9 +171,9 @@ class Note extends Model * * @return MorphMany */ - public function webmentions() + public function webmentions(): MorphMany { - return $this->morphMany('App\Models\WebMention', 'commentable'); + return $this->morphMany(WebMention::class, 'commentable'); } /** @@ -175,9 +181,9 @@ class Note extends Model * * @return BelongsTo */ - public function place() + public function place(): BelongsTo { - return $this->belongsTo('App\Models\Place'); + return $this->belongsTo(Place::class); } /** @@ -185,9 +191,9 @@ class Note extends Model * * @return HasMany */ - public function media() + public function media(): HasMany { - return $this->hasMany('App\Models\Media'); + return $this->hasMany(Media::class); } /** @@ -195,6 +201,7 @@ class Note extends Model * * @return array */ + #[ArrayShape(['note' => "null|string"])] public function toSearchableArray(): array { return [ @@ -207,7 +214,7 @@ class Note extends Model * * @param string|null $value */ - public function setNoteAttribute(?string $value) + public function setNoteAttribute(?string $value): void { if ($value !== null) { $normalized = normalizer_normalize($value, Normalizer::FORM_C); @@ -253,13 +260,13 @@ class Note extends Model $note = $this->note; foreach ($this->media as $media) { - if ($media->type == 'image') { + if ($media->type === 'image') { $note .= ''; } - if ($media->type == 'audio') { + if ($media->type === 'audio') { $note .= '