v1.0.0.1
StocketBase
- <?php
-
- declare(strict_types=1);
-
- final class IndexNow
- {
- private const ENDPOINT = 'https://api.indexnow.org/indexnow';
- private const MIN_INTERVAL_MINUTES = 10;
-
- public static function key(): string
- {
- try {
- $statement = Database::site()->prepare('SELECT setting_key, setting_value FROM site_settings WHERE setting_key IN (:enabled, :key)');
- $statement->execute(['enabled' => 'indexnow_enabled', 'key' => 'indexnow_key']);
- $values = $statement->fetchAll(PDO::FETCH_KEY_PAIR);
- } catch (PDOException $e) {
- return '';
- }
-
- if (($values['indexnow_enabled'] ?? '0') !== '1') {
- return '';
- }
-
- return (string) ($values['indexnow_key'] ?? '');
- }
-
- public static function keyLocation(string $key): string
- {
- $basePath = rtrim((string) Config::get('APP_BASE_PATH', ''), '/');
-
- return rtrim((string) Config::get('APP_URL', ''), '/') . $basePath . '/' . rawurlencode($key) . '.txt';
- }
-
- public static function notifyProduct(int $productId): void
- {
- if (self::key() === '' || $productId <= 0) {
- return;
- }
-
- $statement = Database::site()->prepare('SELECT slug, status FROM products WHERE id = :id LIMIT 1');
- $statement->execute(['id' => $productId]);
- $product = $statement->fetch();
-
- if (!$product || $product['status'] !== 'published' || (string) $product['slug'] === '') {
- return;
- }
-
- self::submit([self::absoluteProductUrl((string) $product['slug'])], $productId);
- }
-
- public static function notifyRemoved(string $slug, int $productId): void
- {
- if (self::key() === '' || $slug === '') {
- return;
- }
-
- self::submit([self::absoluteProductUrl($slug)], $productId);
- }
-
- private static function absoluteProductUrl(string $slug): string
- {
- return rtrim((string) Config::get('APP_URL', ''), '/') . SiteFront::productUrl($slug);
- }
-
- private static function submit(array $urls, int $productId): void
- {
- $key = self::key();
- $host = (string) parse_url((string) Config::get('APP_URL', ''), PHP_URL_HOST);
- if ($key === '' || $host === '') {
- return;
- }
-
- if (self::recentlySubmitted($productId)) {
- if (class_exists('ActivityLog')) {
- ActivityLog::record('indexnow.skipped', 'product', $productId, 'already submitted in the last ' . self::MIN_INTERVAL_MINUTES . ' minutes');
- }
-
- return;
- }
-
- $body = [
- 'host' => $host,
- 'key' => $key,
- 'urlList' => array_values($urls),
- ];
- if (rtrim((string) Config::get('APP_BASE_PATH', ''), '/') !== '') {
- $body['keyLocation'] = self::keyLocation($key);
- }
-
- $status = self::post((string) json_encode($body, JSON_UNESCAPED_SLASHES));
-
- if ($status === 200 || $status === 202) {
- self::markSubmitted($productId);
- }
-
- if (class_exists('ActivityLog')) {
- ActivityLog::record('indexnow.submit', 'product', $productId, $status > 0 ? 'HTTP ' . $status : 'request failed');
- }
- }
-
- private static function recentlySubmitted(int $productId): bool
- {
- try {
- $statement = Database::site()->prepare(
- 'SELECT COUNT(*) FROM indexnow_log WHERE product_id = :id AND submitted_at > DATE_SUB(NOW(), INTERVAL ' . self::MIN_INTERVAL_MINUTES . ' MINUTE)'
- );
- $statement->execute(['id' => $productId]);
-
- return (int) $statement->fetchColumn() > 0;
- } catch (PDOException $e) {
- return false;
- }
- }
-
- private static function markSubmitted(int $productId): void
- {
- try {
- Database::site()->prepare(
- 'INSERT INTO indexnow_log (product_id, submitted_at) VALUES (:id, NOW()) ON DUPLICATE KEY UPDATE submitted_at = NOW()'
- )->execute(['id' => $productId]);
- } catch (PDOException $e) {
- error_log('StocketBase: could not record IndexNow submission: ' . $e->getMessage());
- }
- }
-
- private static function post(string $payload): int
- {
- if (function_exists('curl_init')) {
- $handle = curl_init(self::ENDPOINT);
- curl_setopt_array($handle, [
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => $payload,
- CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_CONNECTTIMEOUT => 2,
- CURLOPT_TIMEOUT => 4,
- ]);
- curl_exec($handle);
- $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE);
-
- return $status;
- }
-
- $context = stream_context_create([
- 'http' => [
- 'method' => 'POST',
- 'header' => "Content-Type: application/json; charset=utf-8\r\n",
- 'content' => $payload,
- 'timeout' => 4,
- 'ignore_errors' => true,
- ],
- ]);
- $result = @file_get_contents(self::ENDPOINT, false, $context);
- if ($result === false || empty($http_response_header[0])) {
- return 0;
- }
-
- return preg_match('#\s(\d{3})\s#', $http_response_header[0], $matches) === 1 ? (int) $matches[1] : 0;
- }
- }
-