[MTM] Upgrade to Laravel 13 #91
117 changed files with 1878 additions and 2215 deletions
|
|
@ -70,11 +70,6 @@ ADMIN_USER=admin# pick something better, this is used for `/admin`
|
||||||
ADMIN_PASS=password
|
ADMIN_PASS=password
|
||||||
DISPLAY_NAME='Joe Bloggs'# This is used for example in the header and titles
|
DISPLAY_NAME='Joe Bloggs'# This is used for example in the header and titles
|
||||||
|
|
||||||
TWITTER_CONSUMER_KEY=
|
|
||||||
TWITTER_CONSUMER_SECRET=
|
|
||||||
TWITTER_ACCESS_TOKEN=
|
|
||||||
TWITTER_ACCESS_TOKEN_SECRET=
|
|
||||||
|
|
||||||
SCOUT_DRIVER=database
|
SCOUT_DRIVER=database
|
||||||
SCOUT_QUEUE=false
|
SCOUT_QUEUE=false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ namespace App\Http\Controllers\Admin;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\BadResponseException;
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
|
@ -103,7 +104,7 @@ class ContactsController extends Controller
|
||||||
* This method attempts to find the microformat marked-up profile image
|
* This method attempts to find the microformat marked-up profile image
|
||||||
* from a given homepage and save it accordingly
|
* from a given homepage and save it accordingly
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
* @return RedirectResponse|View
|
||||||
*/
|
*/
|
||||||
public function getAvatar(int $contactId)
|
public function getAvatar(int $contactId)
|
||||||
{
|
{
|
||||||
|
|
@ -115,7 +116,7 @@ class ContactsController extends Controller
|
||||||
$client = resolve(Client::class);
|
$client = resolve(Client::class);
|
||||||
try {
|
try {
|
||||||
$response = $client->get($contact->homepage);
|
$response = $client->get($contact->homepage);
|
||||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
} catch (BadResponseException $e) {
|
||||||
return redirect('/admin/contacts/'.$contactId.'/edit')
|
return redirect('/admin/contacts/'.$contactId.'/edit')
|
||||||
->with('error', 'Bad resposne from contact’s homepage');
|
->with('error', 'Bad resposne from contact’s homepage');
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +130,7 @@ class ContactsController extends Controller
|
||||||
if ($avatarURL !== null) {
|
if ($avatarURL !== null) {
|
||||||
try {
|
try {
|
||||||
$avatar = $client->get($avatarURL);
|
$avatar = $client->get($avatarURL);
|
||||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
} catch (BadResponseException $e) {
|
||||||
return redirect('/admin/contacts/'.$contactId.'/edit')
|
return redirect('/admin/contacts/'.$contactId.'/edit')
|
||||||
->with('error', 'Unable to download avatar');
|
->with('error', 'Unable to download avatar');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ class PasskeysController extends Controller
|
||||||
// Unset session data to mitigate replay attacks
|
// Unset session data to mitigate replay attacks
|
||||||
$request->session()->forget('create_options');
|
$request->session()->forget('create_options');
|
||||||
if (empty($publicKeyCredentialCreationOptionsData)) {
|
if (empty($publicKeyCredentialCreationOptionsData)) {
|
||||||
throw new WebAuthnException('No public key credential request options found');
|
throw new WebauthnException('No public key credential request options found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||||
|
|
@ -145,7 +145,7 @@ class PasskeysController extends Controller
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
||||||
throw new WebAuthnException('Invalid response type');
|
throw new WebauthnException('Invalid response type');
|
||||||
}
|
}
|
||||||
|
|
||||||
$algorithmManager = new Manager;
|
$algorithmManager = new Manager;
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ class PlacesController extends Controller
|
||||||
foreach ($place1->notes as $note) {
|
foreach ($place1->notes as $note) {
|
||||||
$note->place()->dissociate();
|
$note->place()->dissociate();
|
||||||
$note->place()->associate($place2->id);
|
$note->place()->associate($place2->id);
|
||||||
|
$note->save();
|
||||||
}
|
}
|
||||||
$place1->delete();
|
$place1->delete();
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +128,7 @@ class PlacesController extends Controller
|
||||||
foreach ($place2->notes as $note) {
|
foreach ($place2->notes as $note) {
|
||||||
$note->place()->dissociate();
|
$note->place()->dissociate();
|
||||||
$note->place()->associate($place1->id);
|
$note->place()->associate($place1->id);
|
||||||
|
$note->save();
|
||||||
}
|
}
|
||||||
$place2->delete();
|
$place2->delete();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class RedirectIfAuthenticated
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
* @param Closure(Request): (Response) $next
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ declare(strict_types=1);
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use Codebird\Codebird;
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use GuzzleHttp\Exception\RequestException;
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
|
@ -39,35 +37,6 @@ class ProcessLike implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function handle(Client $client, Authorship $authorship): int
|
public function handle(Client $client, Authorship $authorship): int
|
||||||
{
|
{
|
||||||
if ($this->isTweet($this->like->url)) {
|
|
||||||
$codebird = resolve(Codebird::class);
|
|
||||||
|
|
||||||
$tweet = $codebird->statuses_oembed(['url' => $this->like->url]);
|
|
||||||
|
|
||||||
$this->like->author_name = $tweet->author_name;
|
|
||||||
$this->like->author_url = $tweet->author_url;
|
|
||||||
$this->like->content = $tweet->html;
|
|
||||||
$this->like->save();
|
|
||||||
|
|
||||||
// POSSE like
|
|
||||||
try {
|
|
||||||
$client->request(
|
|
||||||
'POST',
|
|
||||||
'https://brid.gy/publish/webmention',
|
|
||||||
[
|
|
||||||
'form_params' => [
|
|
||||||
'source' => $this->like->url,
|
|
||||||
'target' => 'https://brid.gy/publish/twitter',
|
|
||||||
],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (RequestException) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $client->request('GET', $this->like->url);
|
$response = $client->request('GET', $this->like->url);
|
||||||
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
||||||
if (Arr::has($mf2, 'items.0.properties.content')) {
|
if (Arr::has($mf2, 'items.0.properties.content')) {
|
||||||
|
|
@ -91,15 +60,4 @@ class ProcessLike implements ShouldQueue
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a given URL is that of a Tweet.
|
|
||||||
*/
|
|
||||||
private function isTweet(string $url): bool
|
|
||||||
{
|
|
||||||
$host = parse_url($url, PHP_URL_HOST);
|
|
||||||
$parts = array_reverse(explode('.', $host));
|
|
||||||
|
|
||||||
return $parts[0] === 'com' && $parts[1] === 'twitter';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Mf2\Parser;
|
||||||
|
|
||||||
class SendWebMentions implements ShouldQueue
|
class SendWebMentions implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -83,7 +84,11 @@ class SendWebMentions implements ShouldQueue
|
||||||
// failed to find a header so parse HTML
|
// failed to find a header so parse HTML
|
||||||
$html = (string) $response->getBody();
|
$html = (string) $response->getBody();
|
||||||
|
|
||||||
$mf2 = new \Mf2\Parser($html, $url);
|
if ($html === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mf2 = new Parser($html, $url);
|
||||||
$rels = $mf2->parseRelsAndAlternates();
|
$rels = $mf2->parseRelsAndAlternates();
|
||||||
if (array_key_exists('webmention', $rels[0])) {
|
if (array_key_exists('webmention', $rels[0])) {
|
||||||
$endpoint = $rels[0]['webmention'][0];
|
$endpoint = $rels[0]['webmention'][0];
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Cviebrock\EloquentSluggable\Sluggable;
|
use Cviebrock\EloquentSluggable\Sluggable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -18,23 +20,14 @@ use League\CommonMark\MarkdownConverter;
|
||||||
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
||||||
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
||||||
|
|
||||||
|
#[Table('articles')]
|
||||||
|
#[Fillable(['url', 'title', 'main', 'published'])]
|
||||||
class Article extends Model
|
class Article extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Sluggable;
|
use Sluggable;
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $table = 'articles';
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = [
|
|
||||||
'url',
|
|
||||||
'title',
|
|
||||||
'main',
|
|
||||||
'published',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'created_at' => 'datetime',
|
'created_at' => 'datetime',
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,17 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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;
|
||||||
|
|
||||||
|
#[Fillable(['url', 'name', 'content'])]
|
||||||
class Bookmark extends Model
|
class Bookmark extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = ['url', 'name', 'content'];
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'syndicates' => 'array',
|
'syndicates' => 'array',
|
||||||
|
|
@ -26,7 +25,7 @@ class Bookmark extends Model
|
||||||
return $this->belongsToMany('App\Models\Tag');
|
return $this->belongsToMany('App\Models\Tag');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function local_uri(): Attribute
|
protected function localUri(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
get: fn () => config('app.url').'/bookmarks/'.$this->id,
|
get: fn () => config('app.url').'/bookmarks/'.$this->id,
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,18 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
#[Table('contacts')]
|
||||||
|
#[Fillable(['nick', 'name', 'homepage', 'twitter', 'facebook'])]
|
||||||
class Contact extends Model
|
class Contact extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $table = 'contacts';
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = ['nick', 'name', 'homepage', 'twitter', 'facebook'];
|
|
||||||
|
|
||||||
protected function photo(): Attribute
|
protected function photo(): Attribute
|
||||||
{
|
{
|
||||||
$photo = '/assets/profile-images/default-image';
|
$photo = '/assets/profile-images/default-image';
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,19 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\FilterHtml;
|
use App\Traits\FilterHtml;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Mf2;
|
use Mf2;
|
||||||
|
|
||||||
|
#[Fillable(['url'])]
|
||||||
class Like extends Model
|
class Like extends Model
|
||||||
{
|
{
|
||||||
use FilterHtml;
|
use FilterHtml;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = ['url'];
|
|
||||||
|
|
||||||
protected function url(): Attribute
|
protected function url(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::set(
|
return Attribute::set(
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
#[Table('media_endpoint')]
|
||||||
|
#[Fillable(['token', 'path', 'type', 'image_widths'])]
|
||||||
class Media extends Model
|
class Media extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $table = 'media_endpoint';
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = ['token', 'path', 'type', 'image_widths'];
|
|
||||||
|
|
||||||
public function notes(): BelongsToMany
|
public function notes(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Note::class)
|
return $this->belongsToMany(Note::class)
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,18 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
#[Table('clients')]
|
||||||
|
#[Fillable(['client_url', 'client_name'])]
|
||||||
class MicropubClient extends Model
|
class MicropubClient extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $table = 'clients';
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = ['client_url', 'client_name'];
|
|
||||||
|
|
||||||
public function notes(): HasMany
|
public function notes(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany('App\Models\Note', 'client_id', 'client_url');
|
return $this->hasMany('App\Models\Note', 'client_id', 'client_url');
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,12 @@ namespace App\Models;
|
||||||
|
|
||||||
use App\CommonMark\Generators\MentionGenerator;
|
use App\CommonMark\Generators\MentionGenerator;
|
||||||
use App\CommonMark\Renderers\MentionRenderer;
|
use App\CommonMark\Renderers\MentionRenderer;
|
||||||
use Codebird\Codebird;
|
use App\Observers\NoteObserver;
|
||||||
use Exception;
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -31,6 +34,10 @@ use Normalizer;
|
||||||
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
||||||
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
||||||
|
|
||||||
|
#[Table('notes')]
|
||||||
|
#[Fillable(['note', 'in_reply_to', 'client_id'])]
|
||||||
|
#[Hidden(['searchable'])]
|
||||||
|
#[ObservedBy(NoteObserver::class)]
|
||||||
class Note extends Model
|
class Note extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
@ -61,16 +68,6 @@ class Note extends Model
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $table = 'notes';
|
protected $table = 'notes';
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = [
|
|
||||||
'note',
|
|
||||||
'in_reply_to',
|
|
||||||
'client_id',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $hidden = ['searchable'];
|
|
||||||
|
|
||||||
public function tags(): BelongsToMany
|
public function tags(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Tag::class);
|
return $this->belongsToMany(Tag::class);
|
||||||
|
|
@ -241,43 +238,6 @@ class Note extends Model
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the OEmbed html for a tweet the note is a reply to.
|
|
||||||
*/
|
|
||||||
public function getTwitterAttribute(): ?object
|
|
||||||
{
|
|
||||||
if (
|
|
||||||
$this->in_reply_to === null ||
|
|
||||||
! $this->isTwitterLink($this->in_reply_to)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tweetId = basename($this->in_reply_to);
|
|
||||||
if (Cache::has($tweetId)) {
|
|
||||||
return Cache::get($tweetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$codebird = resolve(Codebird::class);
|
|
||||||
$oEmbed = $codebird->statuses_oembed([
|
|
||||||
'url' => $this->in_reply_to,
|
|
||||||
'dnt' => true,
|
|
||||||
'align' => 'center',
|
|
||||||
'maxwidth' => 512,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($oEmbed->httpstatus >= 400) {
|
|
||||||
throw new Exception;
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Cache::put($tweetId, $oEmbed, ($oEmbed->cache_age));
|
|
||||||
|
|
||||||
return $oEmbed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope a query to select a note via a NewBase60 id.
|
* Scope a query to select a note via a NewBase60 id.
|
||||||
*/
|
*/
|
||||||
|
|
@ -459,9 +419,4 @@ class Note extends Model
|
||||||
return $address;
|
return $address;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isTwitterLink(string $inReplyTo): bool
|
|
||||||
{
|
|
||||||
return str_starts_with($inReplyTo, 'https://twitter.com/');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,16 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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;
|
||||||
|
|
||||||
|
#[Fillable(['passkey_id', 'passkey'])]
|
||||||
class Passkey extends Model
|
class Passkey extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @inerhitDoc */
|
|
||||||
protected $fillable = [
|
|
||||||
'passkey_id',
|
|
||||||
'passkey',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Cviebrock\EloquentSluggable\Sluggable;
|
use Cviebrock\EloquentSluggable\Sluggable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,6 +13,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'slug'])]
|
||||||
class Place extends Model
|
class Place extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
@ -22,9 +24,6 @@ class Place extends Model
|
||||||
return 'slug';
|
return 'slug';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = ['name', 'slug'];
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'latitude' => 'float',
|
'latitude' => 'float',
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Visible;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
#[Fillable(['uid', 'name', 'service_name', 'service_url', 'service_photo', 'user_name', 'user_url', 'user_photo'])]
|
||||||
|
#[Visible(['uid', 'name', 'service', 'user'])]
|
||||||
|
#[Appends(['service', 'user'])]
|
||||||
class SyndicationTarget extends Model
|
class SyndicationTarget extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $fillable = [
|
|
||||||
'uid',
|
|
||||||
'name',
|
|
||||||
'service_name',
|
|
||||||
'service_url',
|
|
||||||
'service_photo',
|
|
||||||
'user_name',
|
|
||||||
'user_url',
|
|
||||||
'user_photo',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $visible = [
|
|
||||||
'uid',
|
|
||||||
'name',
|
|
||||||
'service',
|
|
||||||
'user',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $appends = [
|
|
||||||
'service',
|
|
||||||
'user',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function service(): Attribute
|
protected function service(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,18 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
#[Guarded(['id'])]
|
||||||
class Tag extends Model
|
class Tag extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $guarded = ['id'];
|
|
||||||
|
|
||||||
public function notes(): BelongsToMany
|
public function notes(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Note::class);
|
return $this->belongsToMany(Note::class);
|
||||||
|
|
|
||||||
|
|
@ -4,27 +4,34 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'password'])]
|
||||||
|
#[Hidden(['password', 'remember_token'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
use HasFactory;
|
/** @use HasFactory<UserFactory> */
|
||||||
use Notifiable;
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
/** @var array<int, string> */
|
/**
|
||||||
protected $fillable = [
|
* Get the attributes that should be cast.
|
||||||
'name', 'password',
|
*
|
||||||
];
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
/** @var array<int, string> */
|
protected function casts(): array
|
||||||
protected $hidden = [
|
{
|
||||||
'current_password',
|
return [
|
||||||
'password',
|
'email_verified_at' => 'datetime',
|
||||||
'remember_token',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function passkey(): HasMany
|
public function passkey(): HasMany
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,23 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\FilterHtml;
|
use App\Traits\FilterHtml;
|
||||||
use Codebird\Codebird;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||||
|
|
||||||
|
#[Table('webmentions')]
|
||||||
|
#[Guarded(['id'])]
|
||||||
class WebMention extends Model
|
class WebMention extends Model
|
||||||
{
|
{
|
||||||
use FilterHtml;
|
use FilterHtml;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $table = 'webmentions';
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
|
||||||
protected $guarded = ['id'];
|
|
||||||
|
|
||||||
public function commentable(): MorphTo
|
public function commentable(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
|
|
@ -127,19 +123,6 @@ class WebMention extends Model
|
||||||
return str_replace('http://', 'https://', $url);
|
return str_replace('http://', 'https://', $url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($host === 'twitter.com') {
|
|
||||||
if (Cache::has($url)) {
|
|
||||||
return Cache::get($url);
|
|
||||||
}
|
|
||||||
$username = ltrim(parse_url($url, PHP_URL_PATH), '/');
|
|
||||||
$codebird = resolve(Codebird::class);
|
|
||||||
$info = $codebird->users_show(['screen_name' => $username]);
|
|
||||||
$profile_image = $info->profile_image_url_https;
|
|
||||||
Cache::put($url, $profile_image, 10080); // 1 week
|
|
||||||
|
|
||||||
return $profile_image;
|
|
||||||
}
|
|
||||||
|
|
||||||
$filesystem = new Filesystem;
|
$filesystem = new Filesystem;
|
||||||
if ($filesystem->exists(public_path().'/assets/profile-images/'.$host.'/image')) {
|
if ($filesystem->exists(public_path().'/assets/profile-images/'.$host.'/image')) {
|
||||||
return '/assets/profile-images/'.$host.'/image';
|
return '/assets/profile-images/'.$host.'/image';
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Models\Note;
|
|
||||||
use App\Observers\NoteObserver;
|
|
||||||
use Codebird\Codebird;
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\ConnectException;
|
||||||
|
use GuzzleHttp\HandlerStack;
|
||||||
use GuzzleHttp\Middleware;
|
use GuzzleHttp\Middleware;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Intervention\Image\ImageManager;
|
||||||
use Lcobucci\JWT\Configuration;
|
use Lcobucci\JWT\Configuration;
|
||||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
|
@ -33,33 +33,11 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Note::observe(NoteObserver::class);
|
|
||||||
|
|
||||||
// configure Intervention/Image
|
// configure Intervention/Image
|
||||||
$this->app->bind('Intervention\Image\ImageManager', function () {
|
$this->app->bind('Intervention\Image\ImageManager', function () {
|
||||||
return \Intervention\Image\ImageManager::withDriver(config('image.driver'));
|
return ImageManager::withDriver(config('image.driver'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind the Codebird client
|
|
||||||
// Codebird gets mocked in tests
|
|
||||||
// @codeCoverageIgnoreStart
|
|
||||||
$this->app->bind('Codebird\Codebird', function () {
|
|
||||||
Codebird::setConsumerKey(
|
|
||||||
env('TWITTER_CONSUMER_KEY'),
|
|
||||||
env('TWITTER_CONSUMER_SECRET')
|
|
||||||
);
|
|
||||||
|
|
||||||
$cb = Codebird::getInstance();
|
|
||||||
|
|
||||||
$cb->setToken(
|
|
||||||
env('TWITTER_ACCESS_TOKEN'),
|
|
||||||
env('TWITTER_ACCESS_TOKEN_SECRET')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $cb;
|
|
||||||
});
|
|
||||||
// @codeCoverageIgnoreEnd
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paginate a standard Laravel Collection.
|
* Paginate a standard Laravel Collection.
|
||||||
*
|
*
|
||||||
|
|
@ -106,7 +84,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
|
|
||||||
// Configure Guzzle
|
// Configure Guzzle
|
||||||
$this->app->bind('RetryGuzzle', function () {
|
$this->app->bind('RetryGuzzle', function () {
|
||||||
$handlerStack = \GuzzleHttp\HandlerStack::create();
|
$handlerStack = HandlerStack::create();
|
||||||
$handlerStack->push(Middleware::retry(
|
$handlerStack->push(Middleware::retry(
|
||||||
function ($retries, $request, $response, $exception) {
|
function ($retries, $request, $response, $exception) {
|
||||||
// Limit the number of retries to 5
|
// Limit the number of retries to 5
|
||||||
|
|
@ -115,7 +93,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection exceptions
|
// Retry connection exceptions
|
||||||
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
if ($exception instanceof ConnectException) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,17 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
$middleware
|
$middleware
|
||||||
->append(LinkHeadersMiddleware::class)
|
->append(LinkHeadersMiddleware::class)
|
||||||
->validateCsrfTokens(except: [
|
->preventRequestForgery(
|
||||||
|
except: [
|
||||||
'auth', // This is the IndieAuth auth endpoint
|
'auth', // This is the IndieAuth auth endpoint
|
||||||
'token', // This is the IndieAuth token endpoint
|
'token', // This is the IndieAuth token endpoint
|
||||||
'api/post',
|
'api/post',
|
||||||
'api/media',
|
'api/media',
|
||||||
'micropub/places',
|
'micropub/places',
|
||||||
'webmention',
|
'webmention',
|
||||||
]);
|
],
|
||||||
|
originOnly: true
|
||||||
|
);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
Flare::handles($exceptions);
|
Flare::handles($exceptions);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use App\Providers\HorizonServiceProvider;
|
||||||
|
use App\Providers\MicropubServiceProvider;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
App\Providers\HorizonServiceProvider::class,
|
HorizonServiceProvider::class,
|
||||||
App\Providers\MicropubServiceProvider::class,
|
MicropubServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,34 +6,30 @@
|
||||||
"keywords": ["laravel", "framework", "indieweb"],
|
"keywords": ["laravel", "framework", "indieweb"],
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.3",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pgsql": "*",
|
"ext-pgsql": "*",
|
||||||
|
"ext-pdo": "*",
|
||||||
"ext-sodium": "*",
|
"ext-sodium": "*",
|
||||||
"cviebrock/eloquent-sluggable": "^12.0",
|
"cviebrock/eloquent-sluggable": "^13.0",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"indieauth/client": "^1.1",
|
"indieauth/client": "^1.1",
|
||||||
"intervention/image": "^3",
|
"intervention/image": "^3",
|
||||||
"jonnybarnes/indieweb": "~0.2",
|
"jonnybarnes/indieweb": "~0.2",
|
||||||
"jonnybarnes/webmentions-parser": "~0.5",
|
"jonnybarnes/webmentions-parser": "~0.5",
|
||||||
"jublonet/codebird-php": "4.0.0-beta.1",
|
"laravel/framework": "^13.0",
|
||||||
"laravel/framework": "^12.0",
|
|
||||||
"laravel/horizon": "^5.0",
|
"laravel/horizon": "^5.0",
|
||||||
"laravel/sanctum": "^4.0",
|
|
||||||
"laravel/scout": "^10.1",
|
"laravel/scout": "^10.1",
|
||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^3.0",
|
||||||
"lcobucci/jwt": "^5.0",
|
"lcobucci/jwt": "^5.0",
|
||||||
"league/commonmark": "^2.0",
|
"league/commonmark": "^2.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"mf2/mf2": "~0.3",
|
"mf2/mf2": "~0.3",
|
||||||
"phpdocumentor/reflection-docblock": "^5.3",
|
|
||||||
"spatie/commonmark-highlighter": "^3.0",
|
"spatie/commonmark-highlighter": "^3.0",
|
||||||
"spatie/laravel-flare": "^2.2",
|
"spatie/laravel-flare": "^2.2",
|
||||||
"symfony/html-sanitizer": "^7.0",
|
"symfony/html-sanitizer": "^8.0",
|
||||||
"symfony/property-access": "^7.0",
|
|
||||||
"symfony/serializer": "^7.0",
|
|
||||||
"web-auth/webauthn-lib": "^5.0"
|
"web-auth/webauthn-lib": "^5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
@ -41,13 +37,13 @@
|
||||||
"barryvdh/laravel-ide-helper": "^3.0",
|
"barryvdh/laravel-ide-helper": "^3.0",
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/dusk": "^8.0",
|
"laravel/dusk": "^8.0",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.5",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.27",
|
||||||
"laravel/sail": "^1.41",
|
"laravel/sail": "^1.41",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"phpunit/php-code-coverage": "^11.0",
|
"phpunit/php-code-coverage": "^12.0",
|
||||||
"phpunit/phpunit": "^11.5.50",
|
"phpunit/phpunit": "^12.5.12",
|
||||||
"spatie/laravel-ray": "^1.12",
|
"spatie/laravel-ray": "^1.12",
|
||||||
"spatie/x-ray": "^1.2"
|
"spatie/x-ray": "^1.2"
|
||||||
},
|
},
|
||||||
|
|
@ -72,7 +68,7 @@
|
||||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
"@php artisan key:generate",
|
"@php artisan key:generate",
|
||||||
"@php artisan migrate --force",
|
"@php artisan migrate --force",
|
||||||
"npm install",
|
"npm install --ignore-scripts",
|
||||||
"npm run build"
|
"npm run build"
|
||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
|
|
|
||||||
2245
composer.lock
generated
2245
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -62,7 +64,7 @@ return [
|
||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
'model' => env('AUTH_MODEL', User::class),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,10 @@ return [
|
||||||
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
|
||||||
'connection' => env('DB_CACHE_CONNECTION'),
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'file' => [
|
'file' => [
|
||||||
|
|
@ -111,6 +112,19 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'_cache_'),
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serializable Classes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the classes that can be unserialized from cache
|
||||||
|
| storage. By default, no PHP classes will be unserialized from your
|
||||||
|
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serializable_classes' => false,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
|
@ -59,7 +60,7 @@ return [
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -79,7 +80,7 @@ return [
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -95,7 +96,7 @@ return [
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => 'prefer',
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'sqlsrv' => [
|
'sqlsrv' => [
|
||||||
|
|
@ -148,7 +149,7 @@ return [
|
||||||
|
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'_database_'),
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
'persistent' => env('REDIS_PERSISTENT', false),
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ return [
|
||||||
| may even configure multiple disks for the same driver. Examples for
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
| most supported storage drivers are configured here for reference.
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
|
||||||
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ return [
|
||||||
'public' => [
|
'public' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/public'),
|
'root' => storage_path('app/public'),
|
||||||
'url' => rtrim(env('APP_URL'), '/').'/storage',
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
'visibility' => 'public',
|
'visibility' => 'public',
|
||||||
'throw' => false,
|
'throw' => false,
|
||||||
'report' => false,
|
'report' => false,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Spatie\FlareClient\Api;
|
use Spatie\FlareClient\Api;
|
||||||
|
use Spatie\FlareClient\Sampling\RateSampler;
|
||||||
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
|
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
|
||||||
use Spatie\LaravelFlare\FlareConfig;
|
use Spatie\LaravelFlare\FlareConfig;
|
||||||
|
use Spatie\LaravelFlare\Senders\LaravelHttpSender;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/*
|
/*
|
||||||
|
|
@ -152,7 +154,7 @@ return [
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'sender' => [
|
'sender' => [
|
||||||
'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class,
|
'class' => LaravelHttpSender::class,
|
||||||
'config' => [
|
'config' => [
|
||||||
'timeout' => 10,
|
'timeout' => 10,
|
||||||
],
|
],
|
||||||
|
|
@ -182,7 +184,7 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
'sampler' => [
|
'sampler' => [
|
||||||
'class' => \Spatie\FlareClient\Sampling\RateSampler::class,
|
'class' => RateSampler::class,
|
||||||
'config' => [
|
'config' => [
|
||||||
'rate' => 0.1,
|
'rate' => 0.1,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Monolog\Handler\NullHandler;
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
use Monolog\Handler\SyslogUdpHandler;
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
use Monolog\Processor\PsrLogMessageProcessor;
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ return [
|
||||||
| utilizes the Monolog PHP logging library, which includes a variety
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
| of powerful log handlers and formatters that you're free to use.
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
|
||||||
| Available Drivers: "single", "daily", "slack", "syslog",
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
| "errorlog", "monolog", "custom", "stack"
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
@ -75,7 +76,7 @@ return [
|
||||||
'slack' => [
|
'slack' => [
|
||||||
'driver' => 'slack',
|
'driver' => 'slack',
|
||||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
'level' => env('LOG_LEVEL', 'critical'),
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
|
|
@ -96,6 +97,7 @@ return [
|
||||||
'stderr' => [
|
'stderr' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
'handler_with' => [
|
'handler_with' => [
|
||||||
'stream' => 'php://stderr',
|
'stream' => 'php://stderr',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ return [
|
||||||
|
|
||||||
'from' => [
|
'from' => [
|
||||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Laravel\Sanctum\Sanctum;
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Stateful Domains
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Requests from the following domains / hosts will receive stateful API
|
|
||||||
| authentication cookies. Typically, these should include your local
|
|
||||||
| and production domains which access your API via a frontend SPA.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
|
||||||
'%s%s',
|
|
||||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
|
||||||
Sanctum::currentApplicationUrlWithPort()
|
|
||||||
))),
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Sanctum Guards
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This array contains the authentication guards that will be checked when
|
|
||||||
| Sanctum is trying to authenticate a request. If none of these guards
|
|
||||||
| are able to authenticate the request, Sanctum will use the bearer
|
|
||||||
| token that's present on an incoming request for authentication.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'guard' => ['web'],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Expiration Minutes
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value controls the number of minutes until an issued token will be
|
|
||||||
| considered expired. If this value is null, personal access tokens do
|
|
||||||
| not expire. This won't tweak the lifetime of first-party sessions.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'expiration' => null,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Sanctum Middleware
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When authenticating your first-party SPA with Sanctum you may need to
|
|
||||||
| customize some of the middleware Sanctum uses while processing the
|
|
||||||
| request. You may change the middleware listed below as required.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'middleware' => [
|
|
||||||
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
|
|
||||||
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
@ -214,4 +214,20 @@ return [
|
||||||
|
|
||||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Serialization
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the serialization strategy for session data, which
|
||||||
|
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||||
|
| objects in the session but can make an application vulnerable to
|
||||||
|
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
| Supported: "json", "php"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serialization' => 'json',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
// You can find the keys here : https://dev.twitter.com/
|
|
||||||
|
|
||||||
return [
|
|
||||||
'API_URL' => 'api.twitter.com',
|
|
||||||
'API_VERSION' => '1.1',
|
|
||||||
'AUTHENTICATE_URL' => 'https://api.twitter.com/oauth/authenticate',
|
|
||||||
'AUTHORIZE_URL' => 'https://api.twitter.com/oauth/authorize',
|
|
||||||
'ACCESS_TOKEN_URL' => 'oauth/access_token',
|
|
||||||
'REQUEST_TOKEN_URL' => 'oauth/request_token',
|
|
||||||
'USE_SSL' => true,
|
|
||||||
|
|
||||||
'CONSUMER_KEY' => env('TWITTER_CONSUMER_KEY'),
|
|
||||||
'CONSUMER_SECRET' => env('TWITTER_CONSUMER_SECRET'),
|
|
||||||
'ACCESS_TOKEN' => env('TWITTER_ACCESS_TOKEN'),
|
|
||||||
'ACCESS_TOKEN_SECRET' => env('TWITTER_ACCESS_TOKEN_SECRET'),
|
|
||||||
];
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
|
* @extends Factory<Article>
|
||||||
*/
|
*/
|
||||||
class ArticleFactory extends Factory
|
class ArticleFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Bio;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bio>
|
* @extends Factory<Bio>
|
||||||
*/
|
*/
|
||||||
class BioFactory extends Factory
|
class BioFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bookmark>
|
* @extends Factory<Bookmark>
|
||||||
*/
|
*/
|
||||||
class BookmarkFactory extends Factory
|
class BookmarkFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Contact>
|
* @extends Factory<Contact>
|
||||||
*/
|
*/
|
||||||
class ContactFactory extends Factory
|
class ContactFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Like>
|
* @extends Factory<Like>
|
||||||
*/
|
*/
|
||||||
class LikeFactory extends Factory
|
class LikeFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Media>
|
* @extends Factory<Media>
|
||||||
*/
|
*/
|
||||||
class MediaFactory extends Factory
|
class MediaFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\MicropubClient;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MicropubClient>
|
* @extends Factory<MicropubClient>
|
||||||
*/
|
*/
|
||||||
class MicropubClientFactory extends Factory
|
class MicropubClientFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Note>
|
* @extends Factory<Note>
|
||||||
*/
|
*/
|
||||||
class NoteFactory extends Factory
|
class NoteFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\Passkey;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Passkey>
|
* @extends Factory<Passkey>
|
||||||
*/
|
*/
|
||||||
class PasskeyFactory extends Factory
|
class PasskeyFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\Place;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Place>
|
* @extends Factory<Place>
|
||||||
*/
|
*/
|
||||||
class PlaceFactory extends Factory
|
class PlaceFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\SyndicationTarget;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SyndicationTarget>
|
* @extends Factory<SyndicationTarget>
|
||||||
*/
|
*/
|
||||||
class SyndicationTargetFactory extends Factory
|
class SyndicationTargetFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\Tag;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Tag>
|
* @extends Factory<Tag>
|
||||||
*/
|
*/
|
||||||
class TagFactory extends Factory
|
class TagFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
* @extends Factory<User>
|
||||||
*/
|
*/
|
||||||
class UserFactory extends Factory
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\WebMention;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebMention>
|
* @extends Factory<WebMention>
|
||||||
*/
|
*/
|
||||||
class WebMentionFactory extends Factory
|
class WebMentionFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use Faker\Generator;
|
use Faker\Generator;
|
||||||
|
use Faker\Provider\en_US\Person;
|
||||||
|
use Faker\Provider\Internet;
|
||||||
|
use Faker\Provider\Lorem;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
@ -19,9 +22,9 @@ class LikesTableSeeder extends Seeder
|
||||||
|
|
||||||
$now = Carbon::now()->subDays(rand(3, 6));
|
$now = Carbon::now()->subDays(rand(3, 6));
|
||||||
$faker = new Generator;
|
$faker = new Generator;
|
||||||
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
|
$faker->addProvider(new Person($faker));
|
||||||
$faker->addProvider(new \Faker\Provider\Lorem($faker));
|
$faker->addProvider(new Lorem($faker));
|
||||||
$faker->addProvider(new \Faker\Provider\Internet($faker));
|
$faker->addProvider(new Internet($faker));
|
||||||
$likeFromAuthor = Like::create([
|
$likeFromAuthor = Like::create([
|
||||||
'url' => $faker->url,
|
'url' => $faker->url,
|
||||||
'author_url' => $faker->url,
|
'author_url' => $faker->url,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
{
|
{
|
||||||
"preset": "laravel",
|
"preset": "laravel"
|
||||||
"rules": {
|
|
||||||
"concat_space": false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
@section('title')Merge Places « Admin CP « @stop
|
@section('title')Merge Places « Admin CP « @stop
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<p>We shall be merging {{ $first->name }}. It’s location is <code>Point({{ $first->location }})</code>.</p>
|
<p>We shall be merging {{ $first->name }}. It’s location is <code>{{ $first->latitude }}, {{ $first->longitude }}</code>.</p>
|
||||||
<ul>
|
<ul>
|
||||||
@foreach($places as $place)
|
@foreach($places as $place)
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@
|
||||||
@foreach($bookmarks as $bookmark)
|
@foreach($bookmarks as $bookmark)
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<div class="bookmark-link">
|
<div class="bookmark-link">
|
||||||
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->uri }}">
|
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->url }}">
|
||||||
@isset($bookmark->name)
|
@isset($bookmark->name)
|
||||||
{{ $bookmark->name }}
|
{{ $bookmark->name }}
|
||||||
@endisset
|
@endisset
|
||||||
|
|
||||||
@empty($bookmark->name)
|
@empty($bookmark->name)
|
||||||
{{ $bookmark->uri }}
|
{{ $bookmark->url }}
|
||||||
@endempty
|
@endempty
|
||||||
</a> <a href="{{ $bookmark->uri }}">🔗</a>
|
</a> <a href="{{ $bookmark->url }}">🔗</a>
|
||||||
</div>
|
</div>
|
||||||
@isset($bookmark->content)
|
@isset($bookmark->content)
|
||||||
<p>{{ $bookmark->content }}</p>
|
<p>{{ $bookmark->content }}</p>
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@
|
||||||
@foreach($bookmarks as $bookmark)
|
@foreach($bookmarks as $bookmark)
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<div class="bookmark-link">
|
<div class="bookmark-link">
|
||||||
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->uri }}">
|
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->url }}">
|
||||||
@isset($bookmark->name)
|
@isset($bookmark->name)
|
||||||
{{ $bookmark->name }}
|
{{ $bookmark->name }}
|
||||||
@endisset
|
@endisset
|
||||||
|
|
||||||
@empty($bookmark->name)
|
@empty($bookmark->name)
|
||||||
{{ $bookmark->uri }}
|
{{ $bookmark->url }}
|
||||||
@endempty
|
@endempty
|
||||||
</a> <a href="{{ $bookmark->uri }}">🔗</a>
|
</a> <a href="{{ $bookmark->url }}">🔗</a>
|
||||||
</div>
|
</div>
|
||||||
@isset($bookmark->content)
|
@isset($bookmark->content)
|
||||||
<p>{{ $bookmark->content }}</p>
|
<p>{{ $bookmark->content }}</p>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
@if ($note->twitter)
|
@if ($note->in_reply_to)
|
||||||
{!! $note->twitter->html !!}
|
|
||||||
@elseif ($note->in_reply_to)
|
|
||||||
<div class="u-in-reply-to h-cite reply-to">
|
<div class="u-in-reply-to h-cite reply-to">
|
||||||
In reply to <a href="{{ $note->in_reply_to }}" class="u-url">{{ $note->in_reply_to }}</a>
|
In reply to <a href="{{ $note->in_reply_to }}" class="u-url">{{ $note->in_reply_to }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||||
/**
|
/**
|
||||||
* Create the RemoteWebDriver instance.
|
* Create the RemoteWebDriver instance.
|
||||||
*
|
*
|
||||||
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
|
* @return RemoteWebDriver
|
||||||
*/
|
*/
|
||||||
protected function driver()
|
protected function driver()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests\Feature\Admin;
|
namespace Tests\Feature\Admin;
|
||||||
|
|
||||||
|
use App\Models\Note;
|
||||||
use App\Models\Place;
|
use App\Models\Place;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
@ -78,4 +79,62 @@ class PlacesTest extends TestCase
|
||||||
'name' => 'The Bridgewater',
|
'name' => 'The Bridgewater',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function merge_index_page_loads(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
// Use specific coordinates to avoid haversine acos overflow with extreme faker values
|
||||||
|
$place = Place::factory()->create(['latitude' => 53.48, 'longitude' => -2.24]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get('/admin/places/'.$place->id.'/merge');
|
||||||
|
$response->assertViewIs('admin.places.merge.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function merge_edit_page_loads(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$place1 = Place::factory()->create();
|
||||||
|
$place2 = Place::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get('/admin/places/'.$place1->id.'/merge/'.$place2->id);
|
||||||
|
$response->assertViewIs('admin.places.merge.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function merge_store_with_delete_one_moves_notes_and_deletes_place1(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$place1 = Place::factory()->create();
|
||||||
|
$place2 = Place::factory()->create();
|
||||||
|
$note = Note::factory()->create(['place_id' => $place1->id]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->post('/admin/places/merge', [
|
||||||
|
'place1' => $place1->id,
|
||||||
|
'place2' => $place2->id,
|
||||||
|
'delete' => '1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('places', ['id' => $place1->id]);
|
||||||
|
$this->assertDatabaseHas('notes', ['id' => $note->id, 'place_id' => $place2->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function merge_store_with_delete_two_moves_notes_and_deletes_place2(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$place1 = Place::factory()->create();
|
||||||
|
$place2 = Place::factory()->create();
|
||||||
|
$note = Note::factory()->create(['place_id' => $place2->id]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->post('/admin/places/merge', [
|
||||||
|
'place1' => $place1->id,
|
||||||
|
'place2' => $place2->id,
|
||||||
|
'delete' => '2',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('places', ['id' => $place2->id]);
|
||||||
|
$this->assertDatabaseHas('notes', ['id' => $note->id, 'place_id' => $place1->id]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
tests/Feature/Admin/SyndicationTargetsTest.php
Normal file
136
tests/Feature/Admin/SyndicationTargetsTest.php
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\Admin;
|
||||||
|
|
||||||
|
use App\Models\SyndicationTarget;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SyndicationTargetsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function index_requires_authentication(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/admin/syndication');
|
||||||
|
$response->assertRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function index_lists_syndication_targets(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$target = SyndicationTarget::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get('/admin/syndication');
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSeeText($target->uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function create_page_loads(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get('/admin/syndication/create');
|
||||||
|
$response->assertOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function store_creates_a_new_syndication_target(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
|
||||||
|
$this->actingAs($user)->post('/admin/syndication', [
|
||||||
|
'uid' => 'https://mastodon.social/users/me',
|
||||||
|
'name' => 'Mastodon',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('syndication_targets', [
|
||||||
|
'uid' => 'https://mastodon.social/users/me',
|
||||||
|
'name' => 'Mastodon',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function store_redirects_to_index_after_creation(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->post('/admin/syndication', [
|
||||||
|
'uid' => 'https://mastodon.social/users/me',
|
||||||
|
'name' => 'Mastodon',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect('/admin/syndication');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function edit_page_loads_with_target_data(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$target = SyndicationTarget::factory()->create(['name' => 'My Mastodon']);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get("/admin/syndication/{$target->id}/edit");
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('value="My Mastodon"', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_modifies_syndication_target(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$target = SyndicationTarget::factory()->create(['name' => 'Old Name']);
|
||||||
|
|
||||||
|
$this->actingAs($user)->put("/admin/syndication/{$target->id}", [
|
||||||
|
'uid' => $target->uid,
|
||||||
|
'name' => 'New Name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('syndication_targets', [
|
||||||
|
'id' => $target->id,
|
||||||
|
'name' => 'New Name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_redirects_to_index(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$target = SyndicationTarget::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->put("/admin/syndication/{$target->id}", [
|
||||||
|
'uid' => $target->uid,
|
||||||
|
'name' => 'Updated Name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect('/admin/syndication');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function destroy_deletes_syndication_target(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$target = SyndicationTarget::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user)->delete("/admin/syndication/{$target->id}");
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('syndication_targets', ['id' => $target->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function destroy_redirects_to_index(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
$target = SyndicationTarget::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->delete("/admin/syndication/{$target->id}");
|
||||||
|
|
||||||
|
$response->assertRedirect('/admin/syndication');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Jobs\ProcessBookmark;
|
use App\Jobs\ProcessBookmark;
|
||||||
use App\Models\Bookmark;
|
use App\Models\Bookmark;
|
||||||
|
use App\Models\Tag;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
@ -69,6 +70,27 @@ class BookmarksTest extends TestCase
|
||||||
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
|
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function tagged_bookmarks_page_only_shows_bookmarks_with_that_tag(): void
|
||||||
|
{
|
||||||
|
$tag = Tag::factory()->create(['tag' => 'php']);
|
||||||
|
$tagged = Bookmark::factory()->create();
|
||||||
|
$tagged->tags()->attach($tag);
|
||||||
|
$untagged = Bookmark::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/bookmarks/tagged/php');
|
||||||
|
$response->assertViewIs('bookmarks.tagged');
|
||||||
|
$response->assertSee($tagged->url);
|
||||||
|
$response->assertDontSee($untagged->url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function bookmark_local_uri_attribute_returns_correct_url(): void
|
||||||
|
{
|
||||||
|
$bookmark = Bookmark::factory()->create();
|
||||||
|
$this->assertEquals(config('app.url').'/bookmarks/'.$bookmark->id, $bookmark->local_uri);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function when_the_bookmark_is_created_check_necessary_tags_are_also_created(): void
|
public function when_the_bookmark_is_created_check_necessary_tags_are_also_created(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Jobs\ProcessLike;
|
use App\Jobs\ProcessLike;
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use Codebird\Codebird;
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Handler\MockHandler;
|
use GuzzleHttp\Handler\MockHandler;
|
||||||
use GuzzleHttp\HandlerStack;
|
use GuzzleHttp\HandlerStack;
|
||||||
|
|
@ -194,82 +193,6 @@ class LikesTest extends TestCase
|
||||||
$this->assertNull(Like::find($id)->author_name);
|
$this->assertNull(Like::find($id)->author_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function like_that_is_a_tweet(): void
|
|
||||||
{
|
|
||||||
$like = new Like;
|
|
||||||
$like->url = 'https://twitter.com/jonnybarnes/status/1050823255123251200';
|
|
||||||
$like->save();
|
|
||||||
$id = $like->id;
|
|
||||||
|
|
||||||
$job = new ProcessLike($like);
|
|
||||||
|
|
||||||
$mock = new MockHandler([
|
|
||||||
new Response(201, [], json_encode([
|
|
||||||
'url' => 'https://twitter.com/likes/id',
|
|
||||||
])),
|
|
||||||
]);
|
|
||||||
$handler = HandlerStack::create($mock);
|
|
||||||
$client = new Client(['handler' => $handler]);
|
|
||||||
$this->app->bind(Client::class, function () use ($client) {
|
|
||||||
return $client;
|
|
||||||
});
|
|
||||||
|
|
||||||
$info = (object) [
|
|
||||||
'author_name' => 'Jonny Barnes',
|
|
||||||
'author_url' => 'https://twitter.com/jonnybarnes',
|
|
||||||
'html' => '<div>HTML of the tweet embed</div>',
|
|
||||||
];
|
|
||||||
$codebirdMock = $this->createPartialMock(Codebird::class, ['__call']);
|
|
||||||
$codebirdMock->method('__call')
|
|
||||||
->with('statuses_oembed', $this->anything())
|
|
||||||
->willReturn($info);
|
|
||||||
$this->app->instance(Codebird::class, $codebirdMock);
|
|
||||||
|
|
||||||
$authorship = new Authorship;
|
|
||||||
|
|
||||||
$job->handle($client, $authorship);
|
|
||||||
|
|
||||||
$this->assertEquals('Jonny Barnes', Like::find($id)->author_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function no_error_for_failure_to_posse_with_bridgy(): void
|
|
||||||
{
|
|
||||||
$like = new Like;
|
|
||||||
$like->url = 'https://twitter.com/jonnybarnes/status/1050823255123251200';
|
|
||||||
$like->save();
|
|
||||||
$id = $like->id;
|
|
||||||
|
|
||||||
$job = new ProcessLike($like);
|
|
||||||
|
|
||||||
$mock = new MockHandler([
|
|
||||||
new Response(404, [], 'Not found'),
|
|
||||||
]);
|
|
||||||
$handler = HandlerStack::create($mock);
|
|
||||||
$client = new Client(['handler' => $handler]);
|
|
||||||
$this->app->bind(Client::class, function () use ($client) {
|
|
||||||
return $client;
|
|
||||||
});
|
|
||||||
|
|
||||||
$info = (object) [
|
|
||||||
'author_name' => 'Jonny Barnes',
|
|
||||||
'author_url' => 'https://twitter.com/jonnybarnes',
|
|
||||||
'html' => '<div>HTML of the tweet embed</div>',
|
|
||||||
];
|
|
||||||
$codebirdMock = $this->createPartialMock(Codebird::class, ['__call']);
|
|
||||||
$codebirdMock->method('__call')
|
|
||||||
->with('statuses_oembed', $this->anything())
|
|
||||||
->willReturn($info);
|
|
||||||
$this->app->instance(Codebird::class, $codebirdMock);
|
|
||||||
|
|
||||||
$authorship = new Authorship;
|
|
||||||
|
|
||||||
$job->handle($client, $authorship);
|
|
||||||
|
|
||||||
$this->assertEquals('Jonny Barnes', Like::find($id)->author_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function unknown_like_gives_not_found_response(): void
|
public function unknown_like_gives_not_found_response(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -820,7 +820,7 @@ class MicropubControllerTest extends TestCase
|
||||||
public function micropub_client_api_request_can_delete_photo(): void
|
public function micropub_client_api_request_can_delete_photo(): void
|
||||||
{
|
{
|
||||||
$note = Note::factory()->create();
|
$note = Note::factory()->create();
|
||||||
$media = new \App\Models\Media;
|
$media = new Media;
|
||||||
$media->path = 'https://example.org/photo.jpg';
|
$media->path = 'https://example.org/photo.jpg';
|
||||||
$media->type = 'image';
|
$media->type = 'image';
|
||||||
$media->save();
|
$media->save();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ namespace Tests\Unit\Jobs;
|
||||||
use App\Jobs\SaveScreenshot;
|
use App\Jobs\SaveScreenshot;
|
||||||
use App\Models\Bookmark;
|
use App\Models\Bookmark;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\ConnectException;
|
||||||
use GuzzleHttp\Handler\MockHandler;
|
use GuzzleHttp\Handler\MockHandler;
|
||||||
use GuzzleHttp\HandlerStack;
|
use GuzzleHttp\HandlerStack;
|
||||||
use GuzzleHttp\Middleware;
|
use GuzzleHttp\Middleware;
|
||||||
|
|
@ -45,7 +46,7 @@ class SaveScreenshotJobTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection exceptions
|
// Retry connection exceptions
|
||||||
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
if ($exception instanceof ConnectException) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +116,7 @@ class SaveScreenshotJobTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection exceptions
|
// Retry connection exceptions
|
||||||
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
if ($exception instanceof ConnectException) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ namespace Tests\Unit;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -38,4 +39,68 @@ class MediaTest extends TestCase
|
||||||
|
|
||||||
$this->assertEquals($absoluteUrl, $media->url);
|
$this->assertEquals($absoluteUrl, $media->url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function local_paths_get_storage_url_prepended(): void
|
||||||
|
{
|
||||||
|
$media = new Media;
|
||||||
|
$media->path = 'photo.jpg';
|
||||||
|
|
||||||
|
$this->assertEquals(config('app.url').'/storage/photo.jpg', $media->url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function medium_url_returns_resized_path(): void
|
||||||
|
{
|
||||||
|
$media = new Media;
|
||||||
|
$media->path = 'photo.jpg';
|
||||||
|
|
||||||
|
$this->assertEquals(config('app.url').'/storage/photo-medium.jpg', $media->mediumurl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function small_url_returns_resized_path(): void
|
||||||
|
{
|
||||||
|
$media = new Media;
|
||||||
|
$media->path = 'photo.jpg';
|
||||||
|
|
||||||
|
$this->assertEquals(config('app.url').'/storage/photo-small.jpg', $media->smallurl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function size_url_handles_dotted_basename(): void
|
||||||
|
{
|
||||||
|
$media = new Media;
|
||||||
|
$media->path = 'file.name.png';
|
||||||
|
|
||||||
|
$this->assertEquals(config('app.url').'/storage/file.name-medium.png', $media->mediumurl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider mimeTypeProvider
|
||||||
|
*/
|
||||||
|
#[DataProvider('mimeTypeProvider')]
|
||||||
|
public function mimetype_returns_correct_mime_for_extension(string $path, string $expected): void
|
||||||
|
{
|
||||||
|
$media = new Media;
|
||||||
|
$media->path = $path;
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $media->mimetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function mimeTypeProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['photo.gif', 'image/gif'],
|
||||||
|
['photo.jpeg', 'image/jpeg'],
|
||||||
|
['photo.jpg', 'image/jpeg'],
|
||||||
|
['photo.png', 'image/png'],
|
||||||
|
['photo.svg', 'image/svg+xml'],
|
||||||
|
['photo.tiff', 'image/tiff'],
|
||||||
|
['photo.webp', 'image/webp'],
|
||||||
|
['video.mp4', 'video/mp4'],
|
||||||
|
['video.mkv', 'video/mkv'],
|
||||||
|
['file.bin', 'application/octet-stream'],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
110
tests/Unit/MicropubUpdateDataTest.php
Normal file
110
tests/Unit/MicropubUpdateDataTest.php
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Services\Micropub\Data\UpdateData;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use JsonException;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class MicropubUpdateDataTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function from_request_parses_json_body(): void
|
||||||
|
{
|
||||||
|
$request = Request::create('/micropub', 'POST', [], [], [], [
|
||||||
|
'CONTENT_TYPE' => 'application/json',
|
||||||
|
], json_encode([
|
||||||
|
'token_data' => ['me' => 'https://example.com'],
|
||||||
|
'url' => 'https://example.com/notes/1',
|
||||||
|
'replace' => ['content' => ['new content']],
|
||||||
|
'add' => ['syndication' => ['https://twitter.com']],
|
||||||
|
'delete' => ['category' => ['old-tag']],
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
$dto = UpdateData::fromRequest($request);
|
||||||
|
|
||||||
|
$this->assertEquals(['me' => 'https://example.com'], $dto->tokenData);
|
||||||
|
$this->assertEquals('https://example.com/notes/1', $dto->updateUrl);
|
||||||
|
$this->assertEquals(['content' => ['new content']], $dto->updateReplace);
|
||||||
|
$this->assertEquals(['syndication' => ['https://twitter.com']], $dto->updateAdd);
|
||||||
|
$this->assertEquals(['category' => ['old-tag']], $dto->updateDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function from_request_parses_form_encoded_body(): void
|
||||||
|
{
|
||||||
|
$request = Request::create('/micropub', 'POST', [
|
||||||
|
'token_data' => ['me' => 'https://example.com'],
|
||||||
|
'url' => 'https://example.com/notes/1',
|
||||||
|
'replace' => ['content' => ['new content']],
|
||||||
|
'add' => ['syndication' => ['https://twitter.com']],
|
||||||
|
'delete' => ['category' => ['old-tag']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dto = UpdateData::fromRequest($request);
|
||||||
|
|
||||||
|
$this->assertEquals(['me' => 'https://example.com'], $dto->tokenData);
|
||||||
|
$this->assertEquals('https://example.com/notes/1', $dto->updateUrl);
|
||||||
|
$this->assertEquals(['content' => ['new content']], $dto->updateReplace);
|
||||||
|
$this->assertEquals(['syndication' => ['https://twitter.com']], $dto->updateAdd);
|
||||||
|
$this->assertEquals(['category' => ['old-tag']], $dto->updateDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function from_request_nullable_fields_default_to_null(): void
|
||||||
|
{
|
||||||
|
$request = Request::create('/micropub', 'POST', [], [], [], [
|
||||||
|
'CONTENT_TYPE' => 'application/json',
|
||||||
|
], json_encode([
|
||||||
|
'token_data' => ['me' => 'https://example.com'],
|
||||||
|
'url' => 'https://example.com/notes/1',
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
$dto = UpdateData::fromRequest($request);
|
||||||
|
|
||||||
|
$this->assertNull($dto->updateReplace);
|
||||||
|
$this->assertNull($dto->updateAdd);
|
||||||
|
$this->assertNull($dto->updateDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function from_array_maps_all_fields(): void
|
||||||
|
{
|
||||||
|
$dto = UpdateData::fromArray([
|
||||||
|
'token_data' => ['me' => 'https://example.com'],
|
||||||
|
'update_url' => 'https://example.com/notes/1',
|
||||||
|
'update_replace' => ['content' => ['updated']],
|
||||||
|
'update_add' => ['category' => ['new-tag']],
|
||||||
|
'update_delete' => ['syndication' => ['https://old.example.com']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(['me' => 'https://example.com'], $dto->tokenData);
|
||||||
|
$this->assertEquals('https://example.com/notes/1', $dto->updateUrl);
|
||||||
|
$this->assertEquals(['content' => ['updated']], $dto->updateReplace);
|
||||||
|
$this->assertEquals(['category' => ['new-tag']], $dto->updateAdd);
|
||||||
|
$this->assertEquals(['syndication' => ['https://old.example.com']], $dto->updateDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function from_array_nullable_fields_default_to_null(): void
|
||||||
|
{
|
||||||
|
$dto = UpdateData::fromArray([
|
||||||
|
'token_data' => ['me' => 'https://example.com'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertNull($dto->updateUrl);
|
||||||
|
$this->assertNull($dto->updateReplace);
|
||||||
|
$this->assertNull($dto->updateAdd);
|
||||||
|
$this->assertNull($dto->updateDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -383,24 +383,6 @@ class NotesTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For now, just reply on a cached object instead of actually querying Twitter.
|
|
||||||
*/
|
|
||||||
#[Test]
|
|
||||||
public function check_in_reply_to_is_twitter_link(): void
|
|
||||||
{
|
|
||||||
$tempContent = (object) [
|
|
||||||
'html' => 'something random',
|
|
||||||
];
|
|
||||||
Cache::put('933662564587855877', $tempContent);
|
|
||||||
|
|
||||||
$note = Note::factory()->create([
|
|
||||||
'in_reply_to' => 'https://twitter.com/someRando/status/933662564587855877',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertEquals($tempContent, $note->twitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function latitude_and_longitude_can_be_parsed_from_plain_location(): void
|
public function latitude_and_longitude_can_be_parsed_from_plain_location(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ namespace Tests\Unit;
|
||||||
|
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use App\Models\WebMention;
|
use App\Models\WebMention;
|
||||||
use Codebird\Codebird;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -82,41 +80,6 @@ class WebMentionTest extends TestCase
|
||||||
$this->assertEquals($expected, $webmention->createPhotoLink($twitterProfileImage));
|
$this->assertEquals($expected, $webmention->createPhotoLink($twitterProfileImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function create_photo_link_returns_cached_twitter_photo_links(): void
|
|
||||||
{
|
|
||||||
$webmention = new WebMention;
|
|
||||||
$twitterURL = 'https://twitter.com/example';
|
|
||||||
$expected = 'https://pbs.twimg.com/static_profile_link.jpg';
|
|
||||||
Cache::put($twitterURL, $expected, 1);
|
|
||||||
$this->assertEquals($expected, $webmention->createPhotoLink($twitterURL));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function create_photo_link_resolves_twitter_photo_links(): void
|
|
||||||
{
|
|
||||||
$info = (object) [
|
|
||||||
'profile_image_url_https' => 'https://pbs.twimg.com/static_profile_link.jpg',
|
|
||||||
];
|
|
||||||
$codebirdMock = $this->createPartialMock(Codebird::class, ['__call']);
|
|
||||||
$codebirdMock->method('__call')
|
|
||||||
->with('users_show', $this->anything())
|
|
||||||
->willReturn($info);
|
|
||||||
$this->app->instance(Codebird::class, $codebirdMock);
|
|
||||||
|
|
||||||
Cache::shouldReceive('has')
|
|
||||||
->once()
|
|
||||||
->andReturn(false);
|
|
||||||
Cache::shouldReceive('put')
|
|
||||||
->once()
|
|
||||||
->andReturn(true);
|
|
||||||
|
|
||||||
$webmention = new WebMention;
|
|
||||||
$twitterURL = 'https://twitter.com/example';
|
|
||||||
$expected = 'https://pbs.twimg.com/static_profile_link.jpg';
|
|
||||||
$this->assertEquals($expected, $webmention->createPhotoLink($twitterURL));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function get_reply_attribute_defaults_to_null(): void
|
public function get_reply_attribute_defaults_to_null(): void
|
||||||
{
|
{
|
||||||
|
|
@ -131,4 +94,37 @@ class WebMentionTest extends TestCase
|
||||||
$webmention->mf2 = json_encode(['no_html' => 'found_here']);
|
$webmention->mf2 = json_encode(['no_html' => 'found_here']);
|
||||||
$this->assertNull($webmention->reply);
|
$this->assertNull($webmention->reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function author_attribute_returns_null_when_mf2_is_null(): void
|
||||||
|
{
|
||||||
|
$webmention = new WebMention;
|
||||||
|
$webmention->mf2 = null;
|
||||||
|
$this->assertNull($webmention->author);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function author_attribute_returns_hcard_without_photo_key_when_no_photo_present(): void
|
||||||
|
{
|
||||||
|
$webmention = new WebMention;
|
||||||
|
$webmention->mf2 = json_encode([
|
||||||
|
'items' => [[
|
||||||
|
'type' => ['h-entry'],
|
||||||
|
'properties' => [
|
||||||
|
'author' => [[
|
||||||
|
'type' => ['h-card'],
|
||||||
|
'properties' => [
|
||||||
|
'name' => ['Test Person'],
|
||||||
|
'url' => ['https://example.com'],
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
'content' => ['hello'],
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$hCard = $webmention->author;
|
||||||
|
$this->assertNotNull($hCard);
|
||||||
|
$this->assertArrayNotHasKey('photo', $hCard['properties'] ?? []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue