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; } public function toArray(): array { if ($this->bundle !== null) { $existingData = $this->bundle->metadata('links')->get($this->url, [], false); 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()), ]; } if (!$this->isAnchor()) { if ($this->bundle !== null) { $this->bundle->metadata('links')->set($this->url, $data, false); $this->bundle->metadata('links')->save(); } Cache::put($cacheKey, $data, now()->addWeek()); } return $data; } /** * Return the link as a HtmlElement */ public function toHtmlElement(): HtmlElement { $data = $this->toArray(); return new HtmlElement('a', [ 'href' => $data['finalUrl'], 'rel' => $data['rel'], 'title' => $data['title'], 'class' => $data['classes'], ], $this->innerHtml ?? $this->url); } /** * Tries HEAD and GET requests to find out if URL can be reached */ private function fetchIsDead() { 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); if ($result->ok()) { $isDead = false; break; } if ($result->status() === 403) { $isDead = false; break; } $reason = strval($result->status()); } catch (Exception $ex) { $reason = $ex->getMessage(); } } } $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() ); } else { $title .= sprintf( ' (vérifié le %s) ', $this->checked()->format('d/m/Y') ); } $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'); $title = $bundle->getArticleTitle(); $section = $bundle->getSection(); if (!empty($date)) { return sprintf( 'Lien interne : [%s] %s | Publié le %s', $section->getArticleTitle(), $title, Carbon::parse($date)->format('d/m/Y') ); } else { return sprintf( 'Lien interne : [%s] %s', $section->getArticleTitle(), $title, ); } } }