lots of progress
This commit is contained in:
@@ -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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user