__('Open Reading Settings', 'crownify'), 'url' => admin_url('options-reading.php')]); self::mark('noindex'); } /** * A sitemap that stopped serving is invisible until someone checks. * * The request is a loopback to this same site, which fails for reasons that * have nothing to do with Crownify on plenty of hosts. A failure is therefore * treated as "could not tell" and says nothing rather than crying wolf: a * false alarm about a working sitemap costs more trust than a missed one. */ protected static function checkSitemap() { if ('on' !== Utils::getSetting('xmlSitemap')) { self::clear('sitemap'); return; } $response = wp_remote_get(home_url('/sitemap.xml'), [ 'timeout' => 10, 'redirection' => 2, 'sslverify' => false, 'headers' => ['Accept' => 'application/xml'], ]); if (is_wp_error($response)) { return; } $code = (int) wp_remote_retrieve_response_code($response); if ($code >= 200 && $code < 300) { self::clear('sitemap'); return; } if (!self::due('sitemap')) { return; } /* translators: %s: site name. */ $subject = __('[%s] Your sitemap has stopped responding', 'crownify'); /* translators: %d: HTTP status code. */ $status = __('HTTP %d', 'crownify'); Mailer::send(sprintf($subject, get_bloginfo('name')), __('The XML sitemap is not being served', 'crownify'), __('Crownify is set to publish a sitemap, but requesting it returned an error. Search engines that already know the address are getting the same response. Re-saving permalinks rebuilds the rewrite rules and fixes this most of the time.', 'crownify'), [['label' => __('Address', 'crownify'), 'value' => home_url('/sitemap.xml')], ['label' => __('Response', 'crownify'), 'value' => sprintf($status, $code), 'tone' => 'bad']], ['label' => __('Open Permalink Settings', 'crownify'), 'url' => admin_url('options-permalink.php')]); self::mark('sitemap'); } /** * A URL that keeps 404ing is a link someone else is still sending traffic to. * * Alerts on the count rather than the event, and remembers the count it * alerted at — so a URL that crossed the line yesterday only comes back when * it has doubled, not on every day it stays broken. */ protected static function check404s() { global $wpdb; if (!class_exists('\DaftPlug\Rankify\Features\Redirects\Database')) { return; } $threshold = max(2, (int) Utils::getSetting('notify404Threshold')); $table = \DaftPlug\Rankify\Features\Redirects\Database::logTable(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom 404 table. The alert has to see the current counts. $rows = $wpdb->get_results($wpdb->prepare('SELECT url, url_hash, hits FROM %i WHERE hits >= %d ORDER BY hits DESC LIMIT 20', $table, $threshold), ARRAY_A); if (empty($rows)) { return; } $state = self::state(); $seen = isset($state['404s']) && is_array($state['404s']) ? $state['404s'] : []; $fresh = []; foreach ($rows as $row) { $hash = $row['url_hash']; $hits = (int) $row['hits']; $alertedAt = isset($seen[$hash]) ? (int) $seen[$hash] : 0; // Never alerted for, or it has doubled since the last time it was. if (0 === $alertedAt || $hits >= $alertedAt * 2) { $fresh[] = ['url' => $row['url'], 'hits' => $hits]; $seen[$hash] = $hits; } } if (empty($fresh)) { return; } // Anything no longer over the threshold has been redirected or cleared, and // its entry would otherwise sit in the option forever. $current = wp_list_pluck($rows, 'url_hash'); foreach (array_keys($seen) as $hash) { if (!in_array($hash, $current, true)) { unset($seen[$hash]); } } $lines = []; foreach (array_slice($fresh, 0, 10) as $item) { $lines[] = [ 'label' => $item['url'], /* translators: %s: number of times the URL was requested. */ 'value' => sprintf(_n('%s hit', '%s hits', $item['hits'], 'crownify'), number_format_i18n($item['hits'])), 'tone' => 'warn', ]; } /* translators: 1: site name, 2: number of URLs. */ $subject = _n('[%1$s] %2$s URL is 404ing repeatedly', '[%1$s] %2$s URLs are 404ing repeatedly', count($fresh), 'crownify'); /* translators: %s: the configured hit threshold. */ $intro = __('These addresses have been requested at least %s times and returned nothing. Each one is a link somewhere that still points at this site. A redirect turns that traffic back into visits.', 'crownify'); Mailer::send(sprintf($subject, get_bloginfo('name'), number_format_i18n(count($fresh))), __('Broken URLs are still getting traffic', 'crownify'), sprintf($intro, number_format_i18n($threshold)), $lines, ['label' => __('Open the 404 log', 'crownify'), 'url' => admin_url('admin.php?page=rankify-redirects')]); $state['404s'] = $seen; self::saveState($state); } public static function state() { $state = get_option(self::STATE_OPTION, []); return is_array($state) ? $state : []; } public static function saveState(array $state) { update_option(self::STATE_OPTION, $state, false); } public static function due($key, $cooldown = self::COOLDOWN) { $state = self::state(); $last = isset($state[$key]) ? (int) $state[$key] : 0; return time() - $last >= $cooldown; } public static function mark($key) { $state = self::state(); $state[$key] = time(); self::saveState($state); } /** * Forgetting a resolved condition means the next occurrence alerts at once * rather than waiting out a cooldown that belongs to a problem already fixed. */ public static function clear($key) { $state = self::state(); if (isset($state[$key])) { unset($state[$key]); self::saveState($state); } } }