-
-
Save naquad/d3f0e1765548b7c1beaebd02d3373320 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Console\Commands; | |
use App\Models\Link; | |
use App\Models\User; | |
use App\Notifications\LinkCheckNotification; | |
use Exception; | |
use Illuminate\Console\Command; | |
use Illuminate\Support\Facades\Cache; | |
use Illuminate\Support\Facades\Http; | |
use Illuminate\Support\Facades\Notification; | |
class CheckLinksCommand extends Command | |
{ | |
protected $signature = 'links:check {--limit=} {--noWait}'; | |
protected $description = 'This command checks the current status of a chunk of links. It is intended to be run on a schedule.'; | |
/** @var int $limit Check a maximum of 100 links at once */ | |
public $limit = 100; | |
/** @var int */ | |
protected $offset; | |
/** @var int */ | |
protected $total; | |
/** @var int */ | |
protected $checkedLinkCount; | |
/** @var string */ | |
protected $cacheKeyOffset = 'command_links:check_offset'; | |
/** @var string */ | |
protected $cacheKeySkipTimestamp = 'command_links:check_skip_timestamp'; | |
/** @var string */ | |
protected $cacheKeyCheckedCount = 'command_links:check_checked_count'; | |
/** @var array */ | |
protected $movedLinks = []; | |
/** @var array */ | |
protected $brokenLinks = []; | |
/** @var array */ | |
protected $validUrlSchemes = ['http', 'https']; | |
public function handle(): void | |
{ | |
// Check if the command should skip the execution | |
$skipTimestamp = Cache::get($this->cacheKeySkipTimestamp); | |
$this->offset = Cache::get($this->cacheKeyOffset, 0); | |
$this->checkedLinkCount = Cache::get($this->cacheKeyCheckedCount, 0); | |
if (now()->timestamp < $skipTimestamp) { | |
return; | |
} | |
if ($this->option('limit')) { | |
$this->limit = $this->option('limit'); | |
} | |
$links = $this->getLinks(); | |
// Cancel if there are no links to check | |
if ($links->isEmpty()) { | |
Cache::forget($this->cacheKeyOffset); | |
Cache::forget($this->cacheKeySkipTimestamp); | |
$this->comment('No links found, aborting...'); | |
return; | |
} | |
// Check all provided links | |
$this->comment('Checking ' . $links->count() . ' links now.'); | |
$links->each(function ($link) { | |
$this->checkLink($link); | |
// Prevent spam-ish behaviour by throttling outgoing HTTP requests | |
if ($this->option('noWait') === null) { | |
sleep(1); | |
} | |
}); | |
$this->sendNotification(); | |
$checkedCount = $this->checkedLinkCount + $links->count(); | |
Cache::forever($this->cacheKeyCheckedCount, $checkedCount); | |
if ($this->total > $checkedCount) { | |
// If yes, simply save the offset to the cache. | |
// The next link check will pick it up and continue the check. | |
$nextOffset = $this->offset + $this->limit; | |
Cache::forever($this->cacheKeyOffset, $nextOffset); | |
$this->comment('Saving offset for next link check.'); | |
} else { | |
// If not, all links have been successfully checked. | |
// Save a cache flag that prevents link checks for the next days. | |
$nextCheck = now()->addDays(20)->timestamp; | |
Cache::forever($this->cacheKeySkipTimestamp, $nextCheck); | |
$this->comment( | |
'All existing links checked, next link check scheduled for ' . now()->addDays(5)->toDateTimeString() | |
); | |
} | |
} | |
/** | |
* Get links but limit the results to a fixed number of links. | |
* If there is an offset saved, use this instead of beginning from the first entry. | |
* | |
* @return \LaravelIdea\Helper\App\Models\_IH_Link_C|Link[] | |
*/ | |
protected function getLinks() | |
{ | |
// Get the total amount of remaining links | |
$this->total = Link::count(); | |
// Get a portion of the remaining links based on the limit | |
return Link::where('check_disabled', false) | |
->orderBy('id', 'ASC') | |
->offset($this->offset) | |
->limit($this->limit) | |
->get(); | |
} | |
/** | |
* Check the URL of an link and set the status accordingly. | |
* | |
* @param Link $link | |
* @return void | |
*/ | |
protected function checkLink(Link $link): void | |
{ | |
$this->output->write('Checking link ' . $link->url . ' '); | |
$urlScheme = parse_url($link->url, PHP_URL_SCHEME); | |
if (in_array($urlScheme, $this->validUrlSchemes) === false) { | |
$this->warn('› Invalid scheme [' . $urlScheme . '], skipping Link.'); | |
return; | |
} | |
try { | |
$request = Http::timeout(20); | |
//$request->withOptions(['debug' => true]); | |
if (config('html-meta.user_agents', false)) { | |
$agents = config('html-meta.user_agents'); | |
$request->withHeaders(['User-Agent' => $agents[array_rand($agents)]]); | |
} | |
$request->withHeaders([ | |
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', | |
'Accept-Encoding' => 'gzip, deflate', | |
]); | |
$response = $request->get($link->url); | |
$statusCode = $response->status(); | |
//if ($statusCode >= 400) { | |
// var_dump($response); | |
// echo $response->body(); | |
//} | |
} catch (Exception $e) { | |
// Set status code to null so the link will be marked as broken | |
$statusCode = 0; | |
} | |
if ($statusCode >= 400) { | |
$this->processBrokenLink($link); | |
} elseif ($statusCode >= 300) { | |
$this->processMovedLink($link); | |
} else { | |
$this->processWorkingLink($link); | |
} | |
} | |
/** | |
* Set the Link status to either moved or broken depending on the given | |
* status code. | |
* | |
* @param Link $link | |
*/ | |
protected function processMovedLink(Link $link): void | |
{ | |
$link->status = Link::STATUS_MOVED; | |
$link->save(); | |
$this->warn('› Link moved to another URL!'); | |
$this->movedLinks[] = $link; | |
} | |
/** | |
* Set the Link status to either moved or broken depending on the given | |
* status code. | |
* | |
* @param Link $link | |
*/ | |
protected function processBrokenLink(Link $link): void | |
{ | |
$link->status = Link::STATUS_BROKEN; | |
$link->save(); | |
$this->error('› Link seems to be broken!'); | |
$this->brokenLinks[] = $link; | |
} | |
/** | |
* If the Link has not the "ok" status, set it to ok. | |
* | |
* @param Link $link | |
*/ | |
protected function processWorkingLink(Link $link): void | |
{ | |
if ($link->status !== Link::STATUS_OK) { | |
$link->status = Link::STATUS_OK; | |
$link->save(); | |
} | |
$this->info('› Link looks okay.'); | |
} | |
/** | |
* Send notification to the main user if not running from the console. | |
* | |
* @return void | |
*/ | |
protected function sendNotification(): void | |
{ | |
return; | |
if (empty($this->movedLinks) && empty($this->brokenLinks)) { | |
// Do not send a notification if there are no errors | |
return; | |
} | |
Notification::send( | |
User::find(1), | |
new LinkCheckNotification($this->movedLinks, $this->brokenLinks) | |
); | |
$this->info('› Notification sent to the user.'); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment