settings = $settings; $this->snapshots = $snapshots; $this->diff = $diff; } /** * Registers hooks and keeps the schedule in line with the setting. */ public function register() { add_action( self::HOOK, array( $this, 'scheduled' ) ); add_action( 'orbitstem_seo_snapshot_complete', array( $this, 'on_complete' ) ); add_action( 'update_option_' . Settings::OPTION, array( $this, 'reschedule' ) ); $this->reschedule(); } /** * Matches the cron event to the frequency setting. */ public function reschedule() { $frequency = (string) $this->settings->get( 'monitor_frequency' ); $next = wp_next_scheduled( self::HOOK ); $current = $next ? wp_get_schedule( self::HOOK ) : false; if ( 'off' === $frequency ) { if ( $next ) { wp_clear_scheduled_hook( self::HOOK ); } return; } if ( $current !== $frequency ) { wp_clear_scheduled_hook( self::HOOK ); wp_schedule_event( time() + HOUR_IN_SECONDS, $frequency, self::HOOK ); } } /** * Cron handler. */ public function scheduled() { $this->snapshots->start( 'scheduled' ); } /** * Compares a finished snapshot with the previous one and updates the alert. * * @param int $snapshot_id Snapshot. */ public function on_complete( $snapshot_id ) { $previous = $this->snapshots->previous( $snapshot_id ); if ( ! $previous ) { $this->snapshots->set_summary( $snapshot_id, array( 'first' => true ) ); return; } $result = $this->diff->compare( $previous, $snapshot_id ); $summary = array( 'compared_with' => $previous ) + $result['counts']; $this->snapshots->set_summary( $snapshot_id, $summary ); $serious = $result['counts']['unexplained']['error'] + $result['counts']['unexplained']['warning']; if ( $serious ) { update_option( self::ALERT_OPTION, array( 'snapshot' => (int) $snapshot_id, 'previous' => (int) $previous, 'counts' => $result['counts'], 'time' => time(), ), false ); if ( $this->settings->get( 'monitor_email' ) ) { $this->email( $result, $snapshot_id, $previous ); } } else { delete_option( self::ALERT_OPTION ); } delete_transient( Issues::SUMMARY_TRANSIENT ); } /** * The current alert, or null. * * @return array|null */ public function alert() { $alert = get_option( self::ALERT_OPTION, null ); return is_array( $alert ) ? $alert : null; } /** * Issue for SEO Issues: unexplained errors and warnings from the latest comparison. * * @return array */ public function issues() { $alert = $this->alert(); if ( ! $alert ) { return array(); } $result = $this->diff->compare( $alert['previous'], $alert['snapshot'] ); $rows = array(); foreach ( $result['changes'] as $c ) { if ( ! $c['explained'] && in_array( $c['severity'], array( 'error', 'warning' ), true ) ) { $rows[] = array( 'post_id' => $c['object_id'], 'url' => $c['url'], 'message' => Diff::label( $c['type'] ) . ( '' !== $c['after'] ? ': ' . $c['before'] . ( '' !== $c['before'] ? ' -> ' : '' ) . $c['after'] : '' ), ); } } if ( ! $rows ) { return array(); } return array( array( 'id' => 'regressions', 'severity' => $result['counts']['unexplained']['error'] ? 'error' : 'warning', 'title' => __( 'Unexplained SEO changes since the previous snapshot', 'orbitstem-seo' ), 'explanation' => __( 'The Regression Monitor found changes that no edit or Orbitstem change explains, such as pages that became noindex or stopped answering. These often come from theme, plugin or server updates.', 'orbitstem-seo' ), 'fix' => __( 'Open Orbitstem SEO > Monitor to see each change with before and after values, then check what was updated around that time.', 'orbitstem-seo' ), 'count' => count( $rows ), 'columns' => array( 'post_id' => __( 'Item', 'orbitstem-seo' ), 'url' => __( 'Address', 'orbitstem-seo' ), 'message' => __( 'Change', 'orbitstem-seo' ), ), 'rows' => array_slice( $rows, 0, Issues::ROW_LIMIT ), ), ); } /** * Sends the alert email to the site admin address. * * @param array $result Comparison. * @param int $snapshot_id Newer snapshot. * @param int $previous Older snapshot. * @return bool */ private function email( array $result, $snapshot_id, $previous ) { $lines = array(); foreach ( $result['changes'] as $c ) { if ( ! $c['explained'] && in_array( $c['severity'], array( 'error', 'warning' ), true ) ) { $lines[] = '- ' . Diff::label( $c['type'] ) . ( '' !== $c['url'] ? ': ' . $c['url'] : '' ) . ( '' !== $c['after'] ? ' (' . $c['after'] . ')' : '' ); } } $body = sprintf( /* translators: 1: site name, 2: list of changes, 3: admin URL */ __( "Orbitstem SEO compared the latest snapshot of %1\$s with the previous one and found changes nothing here explains:\n\n%2\$s\n\nDetails: %3\$s", 'orbitstem-seo' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), implode( "\n", array_slice( $lines, 0, 30 ) ), admin_url( 'admin.php?page=orbitstem-seo-monitor&a=' . (int) $previous . '&b=' . (int) $snapshot_id ) ); /* translators: %s: site name */ return wp_mail( get_option( 'admin_email' ), sprintf( __( '[%s] Unexplained SEO changes detected', 'orbitstem-seo' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ), $body ); } }