lots of progress

This commit is contained in:
Doar Mihai
2026-01-17 13:46:20 +02:00
parent ab5d3cae98
commit 030a105992
77 changed files with 8507 additions and 20 deletions
+156
View File
@@ -0,0 +1,156 @@
<?php
namespace App\Jobs;
use App\Models\BrokenLink;
use App\Models\Site;
use App\Notifications\BrokenLinkAlert;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Crawler\Crawler;
use Spatie\Crawler\CrawlObservers\CrawlObserver;
class CheckBrokenLinks implements ShouldQueue
{
use Queueable, Batchable;
public $timeout = 300; // 5 minutes timeout for crawling
/**
* Create a new job instance.
*/
public function __construct(
public Site $site
) {
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
Log::info("Starting broken link check for site {$this->site->id} ({$this->site->url})");
// Parse the base URL to get the domain
$parsedUrl = parse_url($this->site->url);
if (!isset($parsedUrl['host'])) {
Log::warning("Invalid URL for broken link check on site {$this->site->id}: {$this->site->url}");
return;
}
$baseDomain = $parsedUrl['host'];
// Create a crawler observer to handle broken links
$observer = new class($this->site, $baseDomain) extends CrawlObserver {
private Site $site;
private string $baseDomain;
public function __construct(Site $site, string $baseDomain)
{
$this->site = $site;
$this->baseDomain = $baseDomain;
}
public function crawled(
UriInterface $url,
ResponseInterface $response,
?UriInterface $foundOnUrl = null,
?string $linkText = null
): void {
$statusCode = $response->getStatusCode();
// Check if the link is broken (4xx or 5xx)
if ($statusCode >= 400) {
// Only log internal links (same domain)
$linkHost = $url->getHost();
if ($linkHost === $this->baseDomain) {
BrokenLink::updateOrCreate(
[
'site_id' => $this->site->id,
'url' => (string) $url,
],
[
'status_code' => $statusCode,
'last_checked_at' => now(),
]
);
Log::info("Broken link detected on site {$this->site->id}: {$url} (HTTP {$statusCode})");
}
}
}
public function crawlFailed(
UriInterface $url,
RequestException $requestException,
?UriInterface $foundOnUrl = null,
?string $linkText = null
): void {
// Only log internal links (same domain)
$linkHost = $url->getHost();
if ($linkHost === $this->baseDomain) {
$statusCode = $requestException->getResponse()
? $requestException->getResponse()->getStatusCode()
: null;
BrokenLink::updateOrCreate(
[
'site_id' => $this->site->id,
'url' => (string) $url,
],
[
'status_code' => $statusCode,
'last_checked_at' => now(),
]
);
Log::info("Crawl failed for site {$this->site->id}: {$url}");
}
}
};
// Configure and start the crawler
Crawler::create()
->setConcurrency(3)
->setMaximumDepth(3)
->setMaximumCrawlCount(50)
->ignoreRobots()
->acceptNofollowLinks()
->setCrawlObserver($observer)
->startCrawling($this->site->url);
Log::info("Completed broken link check for site {$this->site->id}");
// Get all broken links for this site from current scan
$brokenLinks = BrokenLink::where('site_id', $this->site->id)
->where('last_checked_at', '>=', now()->subMinutes(10))
->get()
->map(fn($link) => [
'url' => $link->url,
'status_code' => $link->status_code,
])
->toArray();
// Send notification if broken links were found
if (!empty($brokenLinks)) {
$this->site->user->notify(new BrokenLinkAlert(
$this->site,
$brokenLinks
));
Log::info("Broken link alert sent for site {$this->site->id}: " . count($brokenLinks) . " broken links found");
}
} catch (\Exception $e) {
Log::error("Broken link check failed for site {$this->site->id}: {$e->getMessage()}");
}
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Jobs;
use App\Models\Notification;
use App\Models\Site;
use App\Models\SslCertificate;
use App\Notifications\SslExpiryAlert;
use Carbon\Carbon;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class CheckSslCertificate implements ShouldQueue
{
use Queueable, Batchable;
/**
* Create a new job instance.
*/
public function __construct(
public Site $site
) {
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
// Parse URL to get host
$parsedUrl = parse_url($this->site->url);
if (!isset($parsedUrl['host'])) {
Log::warning("Invalid URL for SSL check on site {$this->site->id}: {$this->site->url}");
return;
}
$host = $parsedUrl['host'];
$port = $parsedUrl['scheme'] === 'https' ? 443 : 443;
// Create SSL context
$context = stream_context_create([
'ssl' => [
'capture_peer_cert' => true,
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
// Connect to the server and get SSL certificate
$client = @stream_socket_client(
"ssl://{$host}:{$port}",
$errno,
$errstr,
10,
STREAM_CLIENT_CONNECT,
$context
);
if ($client === false) {
Log::warning("Failed to connect to {$host}:{$port} for SSL check: {$errstr}");
return;
}
$params = stream_context_get_params($client);
fclose($client);
if (!isset($params['options']['ssl']['peer_certificate'])) {
Log::warning("No SSL certificate found for site {$this->site->id}");
return;
}
$cert = openssl_x509_parse($params['options']['ssl']['peer_certificate']);
if ($cert === false) {
Log::warning("Failed to parse SSL certificate for site {$this->site->id}");
return;
}
// Extract certificate information
$issuer = $cert['issuer']['O'] ?? $cert['issuer']['CN'] ?? 'Unknown';
$expiresAt = Carbon::createFromTimestamp($cert['validTo_time_t']);
$checkedAt = now();
// Store the SSL certificate check result
SslCertificate::create([
'site_id' => $this->site->id,
'issuer' => $issuer,
'expires_at' => $expiresAt,
'checked_at' => $checkedAt,
]);
// Check if certificate expires within 30 days
$daysUntilExpiry = $checkedAt->diffInDays($expiresAt, false);
if ($daysUntilExpiry < 30 && $daysUntilExpiry >= 0) {
// Send notification to user
$this->site->user->notify(new SslExpiryAlert(
$this->site,
$issuer,
$expiresAt,
(int) $daysUntilExpiry,
false
));
Log::warning("SSL certificate expiring soon for site {$this->site->id}: {$daysUntilExpiry} days remaining");
} elseif ($daysUntilExpiry < 0) {
// Certificate already expired
$this->site->user->notify(new SslExpiryAlert(
$this->site,
$issuer,
$expiresAt,
(int) $daysUntilExpiry,
true
));
Log::error("SSL certificate expired for site {$this->site->id}");
}
Log::info("SSL check completed for site {$this->site->id} ({$this->site->url}): Issuer={$issuer}, Expires={$expiresAt->format('Y-m-d')}");
} catch (\Exception $e) {
Log::error("SSL check failed for site {$this->site->id}: {$e->getMessage()}");
}
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Jobs;
use App\Models\Site;
use App\Models\UptimeCheck;
use App\Notifications\UptimeAlert;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class CheckUptime implements ShouldQueue
{
use Queueable, Batchable;
/**
* Create a new job instance.
*/
public function __construct(
public Site $site
) {
}
/**
* Execute the job.
*/
public function handle(): void
{
$client = new Client([
'timeout' => 5,
'connect_timeout' => 5,
'allow_redirects' => [
'max' => 5,
'track_redirects' => true,
],
'http_errors' => false, // Don't throw exceptions on 4xx/5xx responses
]);
$statusCode = null;
$responseTime = null;
$startTime = microtime(true);
try {
// Validate URL format
if (!filter_var($this->site->url, FILTER_VALIDATE_URL)) {
Log::warning("Invalid URL format for site {$this->site->id}: {$this->site->url}");
$statusCode = null;
$responseTime = null;
} else {
// Send HEAD request for faster checks
$response = $client->head($this->site->url);
$endTime = microtime(true);
$responseTime = (int) round(($endTime - $startTime) * 1000); // Convert to milliseconds
$statusCode = $response->getStatusCode();
}
} catch (ConnectException $e) {
// Connection timeout or DNS failure
Log::info("Connection failed for site {$this->site->id}: {$e->getMessage()}");
$statusCode = null;
$responseTime = null;
} catch (RequestException $e) {
// HTTP error occurred
$endTime = microtime(true);
$responseTime = (int) round(($endTime - $startTime) * 1000);
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
Log::info("Request failed for site {$this->site->id}: HTTP {$statusCode}");
} catch (\Exception $e) {
// Any other exception
Log::error("Unexpected error checking site {$this->site->id}: {$e->getMessage()}");
$statusCode = null;
$responseTime = null;
}
// Store the uptime check result
UptimeCheck::create([
'site_id' => $this->site->id,
'status_code' => $statusCode,
'response_time_ms' => $responseTime,
'checked_at' => now(),
]);
// Send notification if site is down (status >= 400 or unreachable)
if ($statusCode === null || $statusCode >= 400) {
$this->site->user->notify(new UptimeAlert(
$this->site,
$statusCode,
$responseTime
));
Log::warning("Downtime alert sent for site {$this->site->id}: HTTP {$statusCode}");
}
Log::info("Uptime check completed for site {$this->site->id} ({$this->site->url}): HTTP {$statusCode}, {$responseTime}ms");
}
}