Merge pull request #244 from jonnybarnes/develop

MTM: Various dependency updates and new deployment code
This commit is contained in:
Jonny Barnes 2022-01-08 19:58:35 +00:00 • committed by GitHub
commit 3f891b580c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 14734 additions and 10416 deletions

View file

@ -16,3 +16,6 @@ indent_size = 4
[Makefile] [Makefile]
indent_style = tab indent_style = tab
tab_width = 4 tab_width = 4
[yml]
indent_size = 2

View file

@ -7,7 +7,7 @@ APP_LOG_LEVEL=warning
DB_CONNECTION=pgsql DB_CONNECTION=pgsql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=5432 DB_PORT=5432
DB_DATABASE=jbuktest DB_DATABASE=jbukdev_testing
DB_USERNAME=postgres DB_USERNAME=postgres
DB_PASSWORD=postgres DB_PASSWORD=postgres

View file

@ -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

142
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,142 @@
name: Deploy
on:
workflow_dispatch:
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: Hetzner
env:
repository: 'jonnybarnes/jonnybarnes.uk'
newReleaseName: '${{ github.run_id }}'
steps:
- name: 🌍 Set Environment Variables
run: |
echo "releasesDir=${{ secrets.DEPLOYMENT_BASE_DIR }}/releases" >> $GITHUB_ENV
echo "persistentDir=${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent" >> $GITHUB_ENV
echo "currentDir=${{ secrets.DEPLOYMENT_BASE_DIR }}/current" >> $GITHUB_ENV
- name: 🌎 Set Environment Variables Part 2
run: |
echo "newReleaseDir=${{ env.releasesDir }}/${{ env.newReleaseName }}" >> $GITHUB_ENV
- name: 🔄 Clone Repository
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
[ -d ${{ env.releasesDir }} ] || mkdir ${{ env.releasesDir }}
[ -d ${{ env.persistentDir }} ] || mkdir ${{ env.persistentDir }}
[ -d ${{ env.persistentDir }}/storage ] || mkdir ${{ env.persistentDir }}/storage
cd ${{ env.releasesDir }}
# Create new release directory
mkdir ${{ env.newReleaseDir }}
# Clone app
git clone --depth 1 --branch main https://github.com/${{ env.repository }} ${{ env.newReleaseName }}
# Mark release
cd ${{ env.newReleaseDir }}
echo "${{ env.newReleaseName }}" > public/release-name.txt
# Fix cache directory permissions
sudo chown -R ${{ secrets.HTTP_USER }}:${{ secrets.HTTP_USER }} bootstrap/cache
- name: 🎵 Run Composer
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
cd ${{ env.newReleaseDir }}
composer install --prefer-dist --no-scripts --no-dev --no-progress --optimize-autoloader --quiet --no-interaction
- name: 🔗 Update Symlinks
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
# Import the environment config
cd ${{ env.newReleaseDir }};
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/.env .env;
# Remove the storage directory and replace with persistent data
rm -rf ${{ env.newReleaseDir }}/storage;
cd ${{ env.newReleaseDir }};
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/storage storage;
# Remove the public/profile-images directory and replace with persistent data
rm -rf ${{ env.newReleaseDir }}/public/assets/profile-images;
cd ${{ env.newReleaseDir }};
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/profile-images public/assets/profile-images;
# Add the persistent files data
cd ${{ env.newReleaseDir }};
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/files public/files;
# Add the persistent fonts data
cd ${{ env.newReleaseDir }};
ln -nfs ${{ secrets.DEPLOYMENT_BASE_DIR }}/persistent/fonts public/fonts;
- name: ✨ Optimize Installation
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
cd ${{ env.newReleaseDir }};
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan clear-compiled;
- name: 🙈 Migrate database
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
cd ${{ env.newReleaseDir }}
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan migrate --force
- name: 🙏 Bless release
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
ln -nfs ${{ env.newReleaseDir }} ${{ env.currentDir }};
cd ${{ env.newReleaseDir }}
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan horizon:terminate
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan config:cache
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan event:cache
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan route:cache
sudo runuser -u ${{ secrets.HTTP_USER }} -- php artisan view:cache
sudo systemctl restart php-fpm.service
sudo systemctl restart jbuk-horizon.service
- name: 🚾 Clean up old releases
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
port: ${{ secrets.DEPLOYMENT_PORT }}
username: ${{ secrets.DEPLOYMENT_USER }}
key: ${{ secrets.DEPLOYMENT_KEY }}
script: |
fd '.+' ${{ env.releasesDir }} -d 1 | head -n -3 | xargs -d "\n" -I'{}' sudo chown -R ${{ secrets.DEPLOYMENT_USER }}:${{ secrets.DEPLOYMENT_USER }} {}/bootstrap/cache;
fd '.+' ${{ env.releasesDir }} -d 1 | head -n -3 | xargs -d "\n" -I'{}' rm -rf {};

View file

@ -11,11 +11,11 @@ jobs:
services: services:
postgres: postgres:
image: postgres:12 image: postgres:13.4
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: jbuktest POSTGRES_DB: jbukdev_testing
ports: ports:
- 5432:5432 - 5432:5432
@ -36,18 +36,14 @@ jobs:
- name: Copy .env - name: Copy .env
run: php -r "file_exists('.env') || copy('.env.github', '.env');" run: php -r "file_exists('.env') || copy('.env.github', '.env');"
- name: Install dependencies - 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 - name: Generate key
run: php artisan key:generate run: php artisan key:generate
- name: Setup directory permissions - name: Setup directory permissions
run: chmod -R 777 storage bootstrap/cache run: chmod -R 777 storage bootstrap/cache
- name: Setup test database - name: Setup test database
run: | run: php artisan migrate
php artisan migrate
php artisan db:seed
- name: Execute tests (Unit and Feature tests) via PHPUnit - name: Execute tests (Unit and Feature tests) via PHPUnit
run: vendor/bin/phpunit run: vendor/bin/phpunit
- name: Run phpcs - name: Run phpcs
run: phpcs run: phpcs
- name: Check for security vulnerabilities
run: php vendor/bin/security-checker security:check

View file

@ -1,5 +1,5 @@
{ {
"extends": ["stylelint-config-standard", "stylelint-a11y/recommended"], "extends": ["stylelint-config-standard"],
"rules": { "rules": {
"indentation": 4 "indentation": 4
} }

View file

@ -24,16 +24,6 @@ class ParseCachedWebMentions extends Command
*/ */
protected $description = 'Re-parse the webmention’s cached HTML'; 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. * Execute the console command.
* *
@ -41,15 +31,15 @@ class ParseCachedWebMentions extends Command
*/ */
public function handle(FileSystem $filesystem) public function handle(FileSystem $filesystem)
{ {
$HTMLfiles = $filesystem->allFiles(storage_path() . '/HTML'); $htmlFiles = $filesystem->allFiles(storage_path() . '/HTML');
foreach ($HTMLfiles as $file) { foreach ($htmlFiles as $file) {
if ($file->getExtension() != 'backup') { //we don’t want to parse.backup files if ($file->getExtension() !== 'backup') { //we don’t want to parse `.backup` files
$filepath = $file->getPathname(); $filepath = $file->getPathname();
$this->info('Loading HTML from: ' . $filepath); $this->info('Loading HTML from: ' . $filepath);
$html = $filesystem->get($filepath); $html = $filesystem->get($filepath);
$url = $this->URLFromFilename($filepath); $url = $this->urlFromFilename($filepath);
$microformats = \Mf2\parse($html, $url);
$webmention = WebMention::where('source', $url)->firstOrFail(); $webmention = WebMention::where('source', $url)->firstOrFail();
$microformats = \Mf2\parse($html, $url);
$webmention->mf2 = json_encode($microformats); $webmention->mf2 = json_encode($microformats);
$webmention->save(); $webmention->save();
$this->info('Saved the microformats to the database.'); $this->info('Saved the microformats to the database.');
@ -63,12 +53,12 @@ class ParseCachedWebMentions extends Command
* @param string * @param string
* @return string * @return string
*/ */
private function URLFromFilename(string $filepath): string private function urlFromFilename(string $filepath): string
{ {
$dir = mb_substr($filepath, mb_strlen(storage_path() . '/HTML/')); $dir = mb_substr($filepath, mb_strlen(storage_path() . '/HTML/'));
$url = str_replace(['http/', 'https/'], ['http://', 'https://'], $dir); $url = str_replace(['http/', 'https/'], ['http://', 'https://'], $dir);
if (mb_substr($url, -10) == 'index.html') { if (mb_substr($url, -10) === 'index.html') {
$url = mb_substr($url, 0, mb_strlen($url) - 10); $url = mb_substr($url, 0, -10);
} }
return $url; return $url;

View file

@ -1,66 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use SensioLabs\Security\SecurityChecker;
/**
* @codeCoverageIgnore
*/
class SecurityCheck extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'security:check';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the SensioLab’s Security Check tool';
/**
* The Security Checker intergation service.
*
* @var SecurityChecker
*/
protected $securityChecker;
/**
* Create a new command instance.
*
* @param SecurityChecker $SecurityChecker
* @return void
*/
public function __construct(SecurityChecker $securityChecker)
{
parent::__construct();
$this->securityChecker = $securityChecker;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
{
$alerts = $this->securityChecker->check(base_path() . '/composer.lock');
if (count($alerts) === 0) {
$this->info('No security vulnerabilities found.');
return 0;
}
$this->error('vulnerabilities found');
return 1;
}
}

View file

@ -13,7 +13,6 @@ class Kernel extends ConsoleKernel
* @var array * @var array
*/ */
protected $commands = [ protected $commands = [
Commands\SecurityCheck::class,
Commands\ParseCachedWebMentions::class, Commands\ParseCachedWebMentions::class,
Commands\ReDownloadWebMentions::class, Commands\ReDownloadWebMentions::class,
]; ];

View file

@ -7,16 +7,10 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Article; use App\Models\Article;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
class ArticlesController extends Controller class ArticlesController extends Controller
{ {
/**
* List the articles that can be edited.
*
* @return \Illuminate\View\View
*/
public function index(): View public function index(): View
{ {
$posts = Article::select('id', 'title', 'published')->orderBy('id', 'desc')->get(); $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]); return view('admin.articles.index', ['posts' => $posts]);
} }
/**
* Show the new article form.
*
* @return \Illuminate\View\View
*/
public function create(): View public function create(): View
{ {
$message = session('message'); $message = session('message');
@ -36,11 +25,6 @@ class ArticlesController extends Controller
return view('admin.articles.create', ['message' => $message]); 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 public function store(): RedirectResponse
{ {
//if a `.md` is attached use that for the main content. //if a `.md` is attached use that for the main content.
@ -49,42 +33,21 @@ class ArticlesController extends Controller
$content = $file->fread($file->getSize()); $content = $file->fread($file->getSize());
} }
$main = $content ?? request()->input('main'); $main = $content ?? request()->input('main');
$article = Article::create( Article::create([
[
'url' => request()->input('url'), 'url' => request()->input('url'),
'title' => request()->input('title'), 'title' => request()->input('title'),
'main' => $main, 'main' => $main,
'published' => request()->input('published') ?? 0, 'published' => request()->input('published') ?? 0,
] ]);
);
return redirect('/admin/blog'); return redirect('/admin/blog');
} }
/** public function edit(Article $article): View
* Show the edit form for an existing article.
*
* @param int $articleId
* @return \Illuminate\View\View
*/
public function edit(int $articleId): View
{ {
$post = Article::select( return view('admin.articles.edit', ['article' => $article]);
'title',
'main',
'url',
'published'
)->where('id', $articleId)->get();
return view('admin.articles.edit', ['id' => $articleId, 'post' => $post]);
} }
/**
* Process an incoming request to edit an article.
*
* @param int $articleId
* @return \Illuminate\Http\RedirectResponse
*/
public function update(int $articleId): RedirectResponse public function update(int $articleId): RedirectResponse
{ {
$article = Article::find($articleId); $article = Article::find($articleId);
@ -97,12 +60,6 @@ class ArticlesController extends Controller
return redirect('/admin/blog'); return redirect('/admin/blog');
} }
/**
* Process a request to delete an aricle.
*
* @param int $articleId
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(int $articleId): RedirectResponse public function destroy(int $articleId): RedirectResponse
{ {
Article::where('id', $articleId)->delete(); Article::where('id', $articleId)->delete();

View file

@ -59,18 +59,14 @@ class ArticlesController extends Controller
* We only have the ID, work out post title, year and month * We only have the ID, work out post title, year and month
* and redirect to it. * and redirect to it.
* *
* @param int $idFromUrl * @param string $idFromUrl
* @return RedirectResponse * @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); $article = Article::findOrFail($realId);
} catch (ModelNotFoundException $exception) {
abort(404);
}
return redirect($article->link); return redirect($article->link);
} }

View file

@ -9,6 +9,7 @@ use App\Models\Place;
use App\Services\Micropub\{HCardService, HEntryService, UpdateService}; use App\Services\Micropub\{HCardService, HEntryService, UpdateService};
use App\Services\TokenService; use App\Services\TokenService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Lcobucci\JWT\Encoding\CannotDecodeContent;
use Lcobucci\JWT\Token\InvalidTokenStructure; use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
@ -43,7 +44,7 @@ class MicropubController extends Controller
{ {
try { try {
$tokenData = $this->tokenService->validateToken(request()->input('access_token')); $tokenData = $this->tokenService->validateToken(request()->input('access_token'));
} catch (RequiredConstraintsViolated | InvalidTokenStructure $exception) { } catch (RequiredConstraintsViolated | InvalidTokenStructure | CannotDecodeContent $exception) {
$micropubResponses = new MicropubResponses(); $micropubResponses = new MicropubResponses();
return $micropubResponses->invalidTokenResponse(); return $micropubResponses->invalidTokenResponse();

View file

@ -18,19 +18,15 @@ class CSPHeader
{ {
// headers have to be single-line strings, // headers have to be single-line strings,
// so we concat multiple lines // so we concat multiple lines
// phpcs:disable // phpcs:disable Generic.Files.LineLength.TooLong
return $next($request) return $next($request)
->header( ->header(
'Content-Security-Policy', 'Content-Security-Policy',
"default-src 'self'; " . "default-src 'self'; " .
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://api.mapbox.com https://api.tiles.mapbox.com blob:; " . "style-src 'self' cloud.typography.com jonnybarnes.uk; " .
"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://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; " .
"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; " .
"font-src 'self' data:; " . "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:; " . "frame-src 'self' https://www.youtube.com blob:; " .
'child-src blob:; ' .
'upgrade-insecure-requests; ' . 'upgrade-insecure-requests; ' .
'block-all-mixed-content; ' . 'block-all-mixed-content; ' .
'report-to csp-endpoint; ' . 'report-to csp-endpoint; ' .
@ -43,6 +39,6 @@ class CSPHeader
"'max-age': 10886400" . "'max-age': 10886400" .
'}' '}'
); );
// phpcs:enable // phpcs:enable Generic.Files.LineLength.TooLong
} }
} }

View file

@ -41,11 +41,13 @@ class SaveProfileImage implements ShouldQueue
{ {
try { try {
$author = $authorship->findAuthor($this->microformats); $author = $authorship->findAuthor($this->microformats);
} catch (AuthorshipParserException $e) { } catch (AuthorshipParserException) {
return; return null;
} }
$photo = Arr::get($author, 'properties.photo.0'); $photo = Arr::get($author, 'properties.photo.0');
$home = Arr::get($author, 'properties.url.0'); $home = Arr::get($author, 'properties.url.0');
//dont save pbs.twimg.com links //dont save pbs.twimg.com links
if ( if (
$photo $photo
@ -53,16 +55,18 @@ class SaveProfileImage implements ShouldQueue
&& parse_url($photo, PHP_URL_HOST) != 'twitter.com' && parse_url($photo, PHP_URL_HOST) != 'twitter.com'
) { ) {
$client = resolve(Client::class); $client = resolve(Client::class);
try { try {
$response = $client->get($photo); $response = $client->get($photo);
$image = $response->getBody(); $image = $response->getBody();
} catch (RequestException $e) { } catch (RequestException) {
// we are opening and reading the default image so that // we are opening and reading the default image so that
$default = public_path() . '/assets/profile-images/default-image'; $default = public_path() . '/assets/profile-images/default-image';
$handle = fopen($default, 'rb'); $handle = fopen($default, 'rb');
$image = fread($handle, filesize($default)); $image = fread($handle, filesize($default));
fclose($handle); fclose($handle);
} }
$path = public_path() . '/assets/profile-images/' . parse_url($home, PHP_URL_HOST) . '/image'; $path = public_path() . '/assets/profile-images/' . parse_url($home, PHP_URL_HOST) . '/image';
$parts = explode('/', $path); $parts = explode('/', $path);
$name = array_pop($parts); $name = array_pop($parts);

View file

@ -6,7 +6,10 @@ namespace App\Jobs;
use App\Models\Note; use App\Models\Note;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Header;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;
use GuzzleHttp\Psr7\Utils;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@ -65,14 +68,14 @@ class SendWebMentions implements ShouldQueue
* @param string $url * @param string $url
* @return string|null * @return string|null
*/ */
public function discoverWebmentionEndpoint(string $url) public function discoverWebmentionEndpoint(string $url): ?string
{ {
//let’s not send webmentions to myself //let’s not send webmentions to myself
if (parse_url($url, PHP_URL_HOST) == config('app.longurl')) { if (parse_url($url, PHP_URL_HOST) === config('app.longurl')) {
return; return null;
} }
if (Str::startsWith($url, '/notes/tagged/')) { if (Str::startsWith($url, '/notes/tagged/')) {
return; return null;
} }
$endpoint = null; $endpoint = null;
@ -80,7 +83,7 @@ class SendWebMentions implements ShouldQueue
$guzzle = resolve(Client::class); $guzzle = resolve(Client::class);
$response = $guzzle->get($url); $response = $guzzle->get($url);
//check HTTP Headers for webmention endpoint //check HTTP Headers for webmention endpoint
$links = \GuzzleHttp\Psr7\parse_header($response->getHeader('Link')); $links = Header::parse($response->getHeader('Link'));
foreach ($links as $link) { foreach ($links as $link) {
if (mb_stristr($link['rel'], 'webmention')) { if (mb_stristr($link['rel'], 'webmention')) {
return $this->resolveUri(trim($link[0], '<>'), $url); return $this->resolveUri(trim($link[0], '<>'), $url);
@ -110,7 +113,7 @@ class SendWebMentions implements ShouldQueue
*/ */
public function getLinks(?string $html): array public function getLinks(?string $html): array
{ {
if ($html == '' || is_null($html)) { if ($html === '' || is_null($html)) {
return []; return [];
} }
@ -136,13 +139,13 @@ class SendWebMentions implements ShouldQueue
*/ */
public function resolveUri(string $url, string $base): string public function resolveUri(string $url, string $base): string
{ {
$endpoint = \GuzzleHttp\Psr7\uri_for($url); $endpoint = Utils::uriFor($url);
if ($endpoint->getScheme() != '') { if ($endpoint->getScheme() !== '') {
return (string) $endpoint; return (string) $endpoint;
} }
return (string) Uri::resolve( return (string) UriResolver::resolve(
\GuzzleHttp\Psr7\uri_for($base), Utils::uriFor($base),
$endpoint $endpoint
); );
} }

View file

@ -7,6 +7,7 @@ namespace App\Models;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
use Eloquent; use Eloquent;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -58,6 +59,7 @@ use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
*/ */
class Article extends Model class Article extends Model
{ {
use HasFactory;
use Sluggable; use Sluggable;
use SoftDeletes; use SoftDeletes;

View file

@ -6,6 +6,7 @@ namespace App\Models;
use Eloquent; use Eloquent;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -35,6 +36,8 @@ use Illuminate\Support\Carbon;
*/ */
class Contact extends Model class Contact extends Model
{ {
use HasFactory;
/** /**
* The database table used by the model. * The database table used by the model.
* *

View file

@ -6,6 +6,7 @@ namespace App\Models;
use Eloquent; use Eloquent;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -41,6 +42,8 @@ use Illuminate\Support\Str;
*/ */
class Media extends Model class Media extends Model
{ {
use HasFactory;
/** /**
* The table associated with the model. * The table associated with the model.
* *
@ -62,7 +65,7 @@ class Media extends Model
*/ */
public function note(): BelongsTo 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); $filenameParts = explode('.', $path);
array_pop($filenameParts); array_pop($filenameParts);
return ltrim(array_reduce($filenameParts, function ($carry, $item) { return ltrim(array_reduce($filenameParts, static function ($carry, $item) {
return $carry . '.' . $item; return $carry . '.' . $item;
}, ''), '.'); }, ''), '.');
} }

View file

@ -5,14 +5,15 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Exceptions\TwitterContentException; use App\Exceptions\TwitterContentException;
use Barryvdh\LaravelIdeHelper\Eloquent;
use Codebird\Codebird; use Codebird\Codebird;
use Eloquent;
use Exception; use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Relations\{BelongsTo, BelongsToMany, HasMany, MorphMany}; use Illuminate\Database\Eloquent\Relations\{BelongsTo, BelongsToMany, HasMany, MorphMany};
use Illuminate\Database\Eloquent\{Builder, Collection, Factories\HasFactory, Model, SoftDeletes}; use Illuminate\Database\Eloquent\{Builder, Collection, Factories\HasFactory, Model, SoftDeletes};
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use JetBrains\PhpStorm\ArrayShape;
use Jonnybarnes\IndieWeb\Numbers; use Jonnybarnes\IndieWeb\Numbers;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
use League\CommonMark\Block\Element\{FencedCode, IndentedCode}; use League\CommonMark\Block\Element\{FencedCode, IndentedCode};
@ -20,6 +21,11 @@ use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\{CommonMarkConverter, Environment}; use League\CommonMark\{CommonMarkConverter, Environment};
use Normalizer; use Normalizer;
use Spatie\CommonMarkHighlighter\{FencedCodeRenderer, IndentedCodeRenderer}; 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. * App\Models\Note.
@ -102,7 +108,7 @@ class Note extends Model
/** /**
* This variable is used to keep track of contacts in a note. * This variable is used to keep track of contacts in a note.
*/ */
protected $contacts; protected ?array $contacts;
/** /**
* Set our contacts variable to null. * Set our contacts variable to null.
@ -145,9 +151,9 @@ class Note extends Model
* *
* @return BelongsToMany * @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 * @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 * @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 * @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 * @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 * @return array
*/ */
#[ArrayShape(['note' => "null|string"])]
public function toSearchableArray(): array public function toSearchableArray(): array
{ {
return [ return [
@ -207,7 +214,7 @@ class Note extends Model
* *
* @param string|null $value * @param string|null $value
*/ */
public function setNoteAttribute(?string $value) public function setNoteAttribute(?string $value): void
{ {
if ($value !== null) { if ($value !== null) {
$normalized = normalizer_normalize($value, Normalizer::FORM_C); $normalized = normalizer_normalize($value, Normalizer::FORM_C);
@ -253,13 +260,13 @@ class Note extends Model
$note = $this->note; $note = $this->note;
foreach ($this->media as $media) { foreach ($this->media as $media) {
if ($media->type == 'image') { if ($media->type === 'image') {
$note .= '<img src="' . $media->url . '" alt="">'; $note .= '<img src="' . $media->url . '" alt="">';
} }
if ($media->type == 'audio') { if ($media->type === 'audio') {
$note .= '<audio src="' . $media->url . '">'; $note .= '<audio src="' . $media->url . '">';
} }
if ($media->type == 'video') { if ($media->type === 'video') {
$note .= '<video src="' . $media->url . '">'; $note .= '<video src="' . $media->url . '">';
} }
} }
@ -398,7 +405,10 @@ class Note extends Model
*/ */
public function getTwitterAttribute(): ?object public function getTwitterAttribute(): ?object
{ {
if ($this->in_reply_to == null || mb_substr($this->in_reply_to, 0, 20, 'UTF-8') !== 'https://twitter.com/') { if (
$this->in_reply_to === null ||
!$this->isTwitterLink($this->in_reply_to)
) {
return null; return null;
} }
@ -569,7 +579,7 @@ class Note extends Model
public function autoLinkHashtag(string $note): string public function autoLinkHashtag(string $note): string
{ {
return preg_replace_callback( return preg_replace_callback(
'/#([^\s]*)\b/', '/#([^\s[:punct:]]+)/',
function ($matches) { function ($matches) {
return '<a rel="tag" class="p-category" href="/notes/tagged/' return '<a rel="tag" class="p-category" href="/notes/tagged/'
. Tag::normalize($matches[1]) . '">#' . Tag::normalize($matches[1]) . '">#'
@ -660,4 +670,9 @@ class Note extends Model
return $address; return $address;
}); });
} }
private function isTwitterLink(string $inReplyTo): bool
{
return str_starts_with($inReplyTo, 'https://twitter.com/');
}
} }

View file

@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
/** /**
* App\Models\Tag. * App\Models\Tag.
@ -50,7 +51,7 @@ class Tag extends Model
*/ */
public function notes() public function notes()
{ {
return $this->belongsToMany('App\Models\Note'); return $this->belongsToMany(Note::class);
} }
/** /**
@ -82,13 +83,6 @@ class Tag extends Model
*/ */
public static function normalize(string $tag): string public static function normalize(string $tag): string
{ {
return mb_strtolower( return Str::slug($tag);
preg_replace(
'/&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml|caron);/i',
'$1',
htmlentities($tag)
),
'UTF-8'
);
} }
} }

Some files were not shown because too many files have changed in this diff Show more