2024-04-20 23:27:47 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Classes;
|
|
|
|
|
|
|
|
use App\Exceptions\PartnerCannotBeFound;
|
|
|
|
use App\Services\Partners\Contracts\Partner;
|
|
|
|
use App\Services\Partners\Facades\Partner as PartnerFactory;
|
|
|
|
use Carbon\Carbon;
|
|
|
|
use Exception;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
use Illuminate\Support\Str;
|
|
|
|
use League\CommonMark\Util\HtmlElement;
|
|
|
|
|
|
|
|
class Link
|
|
|
|
{
|
|
|
|
private bool $isExternal;
|
|
|
|
|
|
|
|
private bool $isDead;
|
|
|
|
|
2024-04-24 11:39:23 +02:00
|
|
|
private ?string $reason;
|
2024-04-20 23:27:47 +02:00
|
|
|
|
|
|
|
private Carbon $checked;
|
|
|
|
|
|
|
|
private ?Partner $partner;
|
|
|
|
|
|
|
|
private string $finalUrl;
|
|
|
|
|
|
|
|
private string $title;
|
|
|
|
|
2024-04-24 11:39:23 +02:00
|
|
|
public function __construct(protected string $url, protected ?string $innerHtml, protected ?Bundle $bundle = null)
|
2024-04-20 23:27:47 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a boolean value indicating if specified url is an anchor
|
|
|
|
*/
|
|
|
|
public function isAnchor(): bool
|
|
|
|
{
|
|
|
|
return Str::startsWith($this->url, '#');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a boolean value indicating if the URL is either internal or
|
|
|
|
* external to current website
|
|
|
|
*/
|
|
|
|
public function isExternal(): bool
|
|
|
|
{
|
|
|
|
if (!isset($this->isExternal)) {
|
|
|
|
$this->isExternal = Str::startsWith($this->url, [
|
|
|
|
'http://',
|
|
|
|
'https://',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->isExternal;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a boolean value indicating if URL can be reached
|
|
|
|
*/
|
|
|
|
public function isDead(): bool
|
|
|
|
{
|
|
|
|
if (!isset($this->isDead)) {
|
|
|
|
$this->isDead = $this->fetchIsDead()['isDead'];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->isDead;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If URL cannot be reached, return the reason why
|
|
|
|
*/
|
|
|
|
public function reason(): ?string
|
|
|
|
{
|
|
|
|
if (!isset($this->reason)) {
|
|
|
|
$this->reason = $this->fetchIsDead()['reason'] ?? null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->reason;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the last date/time the URL was checked
|
|
|
|
*/
|
|
|
|
public function checked(): ?Carbon
|
|
|
|
{
|
|
|
|
if (!isset($this->checked)) {
|
|
|
|
$this->checked = $this->fetchIsDead()['checked'] ?? null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->checked;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a partner associated with URL, if any
|
|
|
|
*/
|
|
|
|
public function partner(): ?Partner
|
|
|
|
{
|
|
|
|
if (!isset($this->partner)) {
|
|
|
|
if (!$this->isExternal()) {
|
|
|
|
$this->partner = null;
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
$this->partner = PartnerFactory::getPartner($this->url);
|
|
|
|
} catch (PartnerCannotBeFound $ex) {
|
|
|
|
$this->partner = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->partner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the final URL of the link, after any kind of modifications we can
|
|
|
|
* make
|
|
|
|
*/
|
|
|
|
public function finalUrl(): string
|
|
|
|
{
|
|
|
|
if (!isset($this->finalUrl)) {
|
|
|
|
$this->finalUrl = $this->fetchFinalUrl();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->finalUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a suitable title for the link
|
|
|
|
*/
|
|
|
|
public function title(): string
|
|
|
|
{
|
|
|
|
if (!isset($this->title)) {
|
|
|
|
$this->title = $this->fetchTitle();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->title;
|
|
|
|
}
|
|
|
|
|
2024-04-24 11:39:23 +02:00
|
|
|
public function toArray(): array
|
|
|
|
{
|
|
|
|
if ($this->bundle !== null) {
|
|
|
|
$existingData = $this->bundle->metadata('links')->get($this->url, []);
|
|
|
|
|
|
|
|
if (!empty($existingData['checked']) && Carbon::parse($existingData['checked'])->gt(now()->subMonth())) {
|
|
|
|
return $existingData;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$cacheKey = sprintf('link_%s', Str::slug($this->url));
|
|
|
|
|
|
|
|
if (Cache::has($cacheKey)) {
|
|
|
|
$data = Cache::get($cacheKey);
|
|
|
|
} else {
|
|
|
|
$data = [
|
|
|
|
'isAnchor' => $this->isAnchor(),
|
|
|
|
'isExternal' => $this->isExternal(),
|
|
|
|
'isDead' => $this->isDead(),
|
|
|
|
'reason' => $this->reason(),
|
|
|
|
'checked' => $this->checked(),
|
|
|
|
'partner' => $this->partner() ? get_class($this->partner()) : null,
|
|
|
|
'finalUrl' => $this->finalUrl(),
|
|
|
|
'title' => $this->title(),
|
|
|
|
'rel' => implode(' ', $this->fetchRel()),
|
|
|
|
'classes' => implode(' ', $this->fetchCssClasses()),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2024-04-24 12:08:27 +02:00
|
|
|
if (!$this->isAnchor()) {
|
|
|
|
if ($this->bundle !== null) {
|
|
|
|
$this->bundle->metadata('links')->set($this->url, $data, false);
|
|
|
|
$this->bundle->metadata('links')->save();
|
|
|
|
}
|
2024-04-24 11:39:23 +02:00
|
|
|
|
2024-04-24 12:08:27 +02:00
|
|
|
Cache::put($cacheKey, $data, now()->addWeek());
|
|
|
|
}
|
2024-04-24 11:39:23 +02:00
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2024-04-20 23:27:47 +02:00
|
|
|
/**
|
|
|
|
* Return the link as a HtmlElement
|
|
|
|
*/
|
|
|
|
public function toHtmlElement(): HtmlElement
|
|
|
|
{
|
2024-04-24 11:39:23 +02:00
|
|
|
$data = $this->toArray();
|
2024-04-20 23:27:47 +02:00
|
|
|
|
|
|
|
return new HtmlElement('a', [
|
2024-04-24 11:39:23 +02:00
|
|
|
'href' => $data['finalUrl'],
|
|
|
|
'rel' => $data['rel'],
|
|
|
|
'title' => $data['title'],
|
|
|
|
'class' => $data['classes'],
|
2024-04-20 23:27:47 +02:00
|
|
|
], $this->innerHtml);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tries HEAD and GET requests to find out if URL can be reached
|
|
|
|
*/
|
|
|
|
private function fetchIsDead()
|
|
|
|
{
|
2024-04-24 11:39:23 +02:00
|
|
|
if ($this->isAnchor()) {
|
|
|
|
$isDead = false;
|
|
|
|
$reason = null;
|
|
|
|
$checked = now();
|
|
|
|
} elseif (!$this->isExternal()) {
|
|
|
|
$isDead = false;
|
|
|
|
$reason = null;
|
|
|
|
$checked = now();
|
|
|
|
|
|
|
|
$bundle = new Bundle($this->url, Storage::disk(env('CONTENT_DISK')));
|
|
|
|
|
|
|
|
$isDead = !$bundle->exists();
|
|
|
|
} else {
|
|
|
|
$isDead = true;
|
|
|
|
$reason = null;
|
|
|
|
$checked = now();
|
|
|
|
|
|
|
|
foreach (['head', 'get'] as $method) {
|
|
|
|
try {
|
|
|
|
$result = Http::timeout(10)->{$method}($this->url);
|
2024-04-20 23:27:47 +02:00
|
|
|
|
2024-04-24 11:39:23 +02:00
|
|
|
if ($result->ok()) {
|
|
|
|
$isDead = false;
|
|
|
|
break;
|
|
|
|
}
|
2024-04-23 23:55:48 +02:00
|
|
|
|
2024-04-24 11:39:23 +02:00
|
|
|
if ($result->status() === 403) {
|
|
|
|
$isDead = false;
|
|
|
|
break;
|
|
|
|
}
|
2024-04-23 23:55:48 +02:00
|
|
|
|
2024-04-24 11:39:23 +02:00
|
|
|
$reason = strval($result->status());
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
$reason = $ex->getMessage();
|
2024-04-20 23:27:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$result = compact('isDead', 'reason', 'checked');
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the final URL
|
|
|
|
*/
|
|
|
|
private function fetchFinalUrl(): string
|
|
|
|
{
|
|
|
|
if ($this->isAnchor()) {
|
|
|
|
return $this->url;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($this->partner())) {
|
|
|
|
return $this->partner()->getAffiliateLink();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->url;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an array of appropriate rel attributes
|
|
|
|
*/
|
|
|
|
private function fetchRel(): array
|
|
|
|
{
|
|
|
|
$rel = [];
|
|
|
|
|
|
|
|
if (!empty($this->partner())) {
|
|
|
|
$rel[] = 'nofollow';
|
|
|
|
} elseif ($this->isExternal()) {
|
|
|
|
$rel = [
|
|
|
|
'nofollow',
|
|
|
|
'noreferrer',
|
|
|
|
'noopener',
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $rel;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return applicable CSS classes
|
|
|
|
*/
|
|
|
|
private function fetchCssClasses(): array
|
|
|
|
{
|
|
|
|
$classes = [];
|
|
|
|
|
|
|
|
if ($this->isExternal()) {
|
|
|
|
$classes = [
|
|
|
|
config('markdown.external_link.html_class'),
|
|
|
|
];
|
|
|
|
|
|
|
|
if (!empty($this->partner())) {
|
|
|
|
$classes[] = 'affiliate';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->isDead()) {
|
|
|
|
$classes[] = 'dead';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $classes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a suitable title for this link
|
|
|
|
*/
|
|
|
|
private function fetchTitle(): string
|
|
|
|
{
|
|
|
|
if ($this->isAnchor()) {
|
|
|
|
return sprintf('Lien direct vers %s', $this->url);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->isExternal()) {
|
|
|
|
$title = 'Lien ';
|
|
|
|
|
|
|
|
if (!empty($this->partner())) {
|
|
|
|
$title .= 'affilié ';
|
|
|
|
} else {
|
|
|
|
$title .= 'externe ';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->isDead()) {
|
|
|
|
$title .= sprintf(
|
|
|
|
' (mort depuis le %s - %s) ',
|
|
|
|
$this->checked()->format('d/m/Y'),
|
|
|
|
$this->reason()
|
|
|
|
);
|
2024-04-23 23:55:48 +02:00
|
|
|
} else {
|
|
|
|
$title .= sprintf(
|
|
|
|
' (vérifié le %s) ',
|
|
|
|
$this->checked()->format('d/m/Y')
|
|
|
|
);
|
2024-04-20 23:27:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$title .= sprintf(' : %s', $this->url);
|
|
|
|
|
|
|
|
return $title;
|
|
|
|
} else {
|
|
|
|
return $this->fetchBundleTitle();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build link title from internal bundle
|
|
|
|
*/
|
|
|
|
private function fetchBundleTitle(): string
|
|
|
|
{
|
|
|
|
$bundle = new Bundle($this->url, Storage::disk(env('CONTENT_DISK')));
|
|
|
|
|
|
|
|
$bundle->load();
|
|
|
|
|
|
|
|
$date = $bundle->metadata()->get('date');
|
2024-04-21 16:27:34 +02:00
|
|
|
$title = $bundle->getArticleTitle();
|
2024-04-20 23:27:47 +02:00
|
|
|
$section = $bundle->getSection();
|
|
|
|
|
|
|
|
if (!empty($date)) {
|
|
|
|
return sprintf(
|
|
|
|
'Lien interne : [%s] %s | Publié le %s',
|
2024-04-21 16:46:27 +02:00
|
|
|
$section->getArticleTitle(),
|
2024-04-20 23:27:47 +02:00
|
|
|
$title,
|
|
|
|
Carbon::parse($date)->format('d/m/Y')
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return sprintf(
|
|
|
|
'Lien interne : [%s] %s',
|
2024-04-21 16:46:27 +02:00
|
|
|
$section->getArticleTitle(),
|
2024-04-20 23:27:47 +02:00
|
|
|
$title,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|