'array', 'sanitize_callback' => array( Settings::class, 'sanitize' ), ) ); add_filter( 'option_page_capability_maintained_log', static function ( $cap ) { return Plugin::user_can_manage() ? $cap : 'do_not_allow'; } ); } /** * Enqueues admin CSS on plugin screens. * * @param string $hook Current screen hook. */ public static function assets( $hook ) { if ( false === strpos( $hook, self::SLUG ) ) { return; } wp_enqueue_style( 'maintained-log-admin', MAINTAINED_LOG_URL . 'admin/admin.css', array(), MAINTAINED_LOG_VERSION ); wp_enqueue_script( 'maintained-log-admin', MAINTAINED_LOG_URL . 'admin/admin.js', array(), MAINTAINED_LOG_VERSION, true ); if ( false !== strpos( $hook, self::SETTINGS_SLUG ) ) { wp_enqueue_script( 'maintained-log-fetch', MAINTAINED_LOG_URL . 'admin/fetch-now.js', array(), MAINTAINED_LOG_VERSION, true ); wp_localize_script( 'maintained-log-fetch', 'maintainedLogFetch', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'maintained_log_fetch' ), 'working' => __( 'Working… keep this page open.', 'maintained-log' ), 'failed' => __( 'The request failed. Check your connection and try again.', 'maintained-log' ), ) ); } } /** * Settings link on the plugins screen. * * @param array $links Links. * @return array */ public static function action_links( $links ) { if ( ! Plugin::user_can_manage() ) { return $links; } array_unshift( $links, '' . esc_html__( 'Settings', 'maintained-log' ) . '' ); return $links; } /** * Warns on plugin screens when client emails are blocked. */ public static function environment_notice() { $screen = get_current_screen(); if ( ! $screen || false === strpos( $screen->id, self::SLUG ) || ! Plugin::user_can_manage() ) { return; } self::flash_notice(); $reason = Environment::blocking_reason(); if ( '' === $reason ) { return; } ?>

array_key_exists( $type, Log::types() ) ? $type : '', 'per_page' => $per_page, 'page' => $paged, ); $entries = Log::query( $args ); $total = Log::count( $args ); $pending = Alerts::pending_updates(); $original = $amends ? Log::get( $amends ) : null; $types = Log::types(); $cats = Log::categories(); $base_url = admin_url( 'admin.php?page=' . self::SLUG ); ?>

Maintained.log


$month_start, 'per_page' => 0, ); $month_tasks = Log::query( array_merge( $month_args, array( 'type' => Log::TYPE_MANUAL ) ) ); $minutes = array_sum( wp_list_pluck( $month_tasks, 'minutes' ) ); ?>

created_at, get_option( 'date_format' ) ) ) ); ?>
Backups::KIND_CHECKED ) ); self::action_button( 'backup_confirm', __( 'Restore test done', 'maintained-log' ), 'button button-small', array( 'kind' => Backups::KIND_RESTORE_TEST ) ); ?>
Log::TYPE_UPDATE ) ) ) ); ?>
  • |
  • $label ) : ?>
created_at, get_option( 'date_format' ) . ' H:i' ) ); ?> type ] ) ? $types[ $entry->type ] : $entry->type ); ?> title ); ?> type && isset( $cats[ $entry->category ] ) ) : ?> category ] ); ?> minutes ) : ?> minutes ) ); ?> amends_id ) : ?> amends_id ) ); ?> type ) : ?>
1 ) { echo '
'; echo wp_kses_post( paginate_links( array( 'base' => add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $paged, 'total' => $pages, ) ) ); echo '
'; } ?>

    $item ) : ?>

  • ·

id, $original->title ) ); ?>

setTime( 0, 0 ); } } if ( ! empty( $_GET['to'] ) ) { $to = \DateTimeImmutable::createFromFormat( 'Y-m-d', sanitize_text_field( wp_unslash( $_GET['to'] ) ), $tz ); if ( $to ) { $period['to'] = $to->setTime( 0, 0 )->modify( '+1 day' ); } } // phpcs:enable $report = Reporter::build( $period['from'], $period['to'] ); $html = Reporter::render( $report, 'preview' ); $recipients = Settings::get( 'report_recipients' ); $next = Reporter::due_period(); $last_day = $period['to']->format( 'H:i:s' ) === '00:00:00' ? $period['to']->modify( '-1 day' ) : $period['to']; ?>

' . esc_html__( 'Add client recipients in Settings.', 'maintained-log' ) . ''; } if ( $next ) { echo ' ' . esc_html__( 'A scheduled report is due and will be sent on the next cron run.', 'maintained-log' ); } echo ' ' . esc_html__( 'When is the automatic report sent?', 'maintained-log' ) . ''; ?>




Log::TYPE_MANUAL, 'category' => array_key_exists( $category, Log::categories() ) ? $category : 'other', 'status' => in_array( $status, array( 'success', 'warning', 'error' ), true ) ? $status : 'success', 'title' => $title, 'details' => array( 'notes' => isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '' ), 'minutes' => isset( $_POST['minutes'] ) ? absint( $_POST['minutes'] ) : 0, 'amends_id' => ( $amends && Log::get( $amends ) ) ? $amends : 0, 'source' => Log::SOURCE_MANUAL, ); // phpcs:enable $parsed = $date ? \DateTimeImmutable::createFromFormat( 'Y-m-d\TH:i', $date, wp_timezone() ) : false; if ( $parsed && $parsed->getTimestamp() <= time() + MINUTE_IN_SECONDS ) { $entry['created_at'] = gmdate( 'Y-m-d H:i:s', $parsed->getTimestamp() ); if ( $parsed->getTimestamp() < time() - HOUR_IN_SECONDS ) { // Back-dated entries are allowed (work is often logged later) but marked as such. $entry['details']['logged_at'] = current_time( 'mysql', true ); } } Log::add( $entry ) ? self::redirect( self::SLUG, 'success', __( 'Added to the log.', 'maintained-log' ) ) : self::redirect( self::SLUG, 'error', __( 'The entry could not be saved.', 'maintained-log' ) ); } /** * Runs site checks on demand. */ public static function handle_run_checks() { self::verify( 'maintained_log_run_checks' ); $run = Health_Checks::run( 'manual' ); $run['ok'] ? self::redirect( self::SLUG, 'success', __( 'All site checks passed.', 'maintained-log' ) ) : self::redirect( self::SLUG, 'error', __( 'Some site checks failed. See the log for details.', 'maintained-log' ) ); } /** * Records inventory changes on demand. */ public static function handle_snapshot() { self::verify( 'maintained_log_snapshot' ); $changes = Inventory::detect_changes( Log::SOURCE_EXTERNAL ); /* translators: %d: number of changes. */ self::redirect( self::SLUG, 'success', sprintf( _n( '%d change recorded.', '%d changes recorded.', count( $changes ), 'maintained-log' ), count( $changes ) ) ); } /** * Sends the report to the client or as a test. */ public static function handle_send_report() { self::verify( 'maintained_log_send_report' ); $tz = wp_timezone(); // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified above. $from = isset( $_POST['from'] ) ? \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', sanitize_text_field( wp_unslash( $_POST['from'] ) ), $tz ) : false; $to = isset( $_POST['to'] ) ? \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', sanitize_text_field( wp_unslash( $_POST['to'] ) ), $tz ) : false; $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : 'test'; // phpcs:enable if ( ! $from || ! $to ) { $period = Reporter::manual_period(); $from = $period['from']; $to = $period['to']; } $report = Reporter::build( $from, $to ); $result = 'client' === $mode ? Reporter::send( $report ) : Reporter::send( $report, wp_get_current_user()->user_email ); if ( is_wp_error( $result ) ) { self::redirect( self::REPORT_SLUG, 'error', $result->get_error_message() ); } if ( 'client' === $mode ) { $stored = get_option( Reporter::LAST_PERIOD_OPTION ); if ( ! $stored || $to->format( 'Y-m-d H:i:s' ) > $stored ) { update_option( Reporter::LAST_PERIOD_OPTION, $to->format( 'Y-m-d H:i:s' ), false ); } } self::redirect( self::REPORT_SLUG, 'success', 'client' === $mode ? __( 'Report sent to the client and recorded in the log.', 'maintained-log' ) : __( 'Test report sent to your email.', 'maintained-log' ) ); } /** * Streams the log as CSV. */ public static function handle_export() { if ( ! Plugin::user_can_manage() || ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), 'maintained_log_export' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'maintained-log' ), 403 ); } $type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : ''; $entries = Log::query( array( 'type' => $type, 'per_page' => 0, 'order' => 'ASC', ) ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=maintained-log-' . gmdate( 'Y-m-d' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- UTF-8 BOM for Excel. fputcsv( $out, array( 'id', 'date_utc', 'type', 'category', 'status', 'title', 'source', 'user', 'minutes', 'amends_id', 'details', 'hash' ) ); foreach ( $entries as $entry ) { fputcsv( $out, array_map( array( __CLASS__, 'csv_safe' ), array( $entry->id, $entry->created_at, $entry->type, $entry->category, $entry->status, $entry->title, $entry->source, self::author( $entry ), $entry->minutes, $entry->amends_id, wp_json_encode( $entry->details ), $entry->hash ) ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } /** * Verifies the hash chain. */ public static function handle_verify() { self::verify( 'maintained_log_verify' ); $result = Log::verify_chain(); if ( $result['ok'] ) { /* translators: %d: number of entries. */ self::redirect( self::SLUG, 'success', sprintf( __( 'Integrity verified: %d entries, none altered or deleted.', 'maintained-log' ), $result['checked'] ) ); } /* translators: %d: entry ID. */ self::redirect( self::SLUG, 'error', sprintf( __( 'The log was altered outside Maintained.log at or before entry #%d.', 'maintained-log' ), $result['broken_at'] ) ); } /** * Confirms that the current URL is the live site. */ public static function handle_confirm_production() { self::verify( 'maintained_log_confirm_production' ); Environment::confirm_production(); Log::add( array( 'type' => Log::TYPE_SYSTEM, /* translators: %s: site URL. */ 'title' => sprintf( __( 'Live site URL confirmed: %s', 'maintained-log' ), home_url() ), 'source' => Log::SOURCE_MANUAL, ) ); self::redirect( self::SLUG, 'success', __( 'Client emails are enabled for this URL.', 'maintained-log' ) ); } /** * Sends the pending-updates reminder now. */ public static function handle_pending_alert() { self::verify( 'maintained_log_pending_alert' ); Alerts::maybe_send_pending_updates_alert( true ) ? self::redirect( self::SETTINGS_SLUG, 'success', __( 'Reminder sent.', 'maintained-log' ) ) : self::redirect( self::SETTINGS_SLUG, 'info', __( 'No reminder sent: there are no pending updates or no administrator email is set.', 'maintained-log' ) ); } /** * "Get the data now" box: runs the vulnerability sync or a speed round * right away, driven step by step from the browser. */ private static function render_fetch_now() { $vuln_state = Vulnerabilities::sync_state(); $perf_last = Performance::measurements( null, null, 1 ); $boxes = array( array( 'what' => 'vuln', 'title' => __( 'Vulnerabilities (Wordfence)', 'maintained-log' ), 'text' => __( 'Downloads the vulnerability database now and checks this site\'s updates and installed software against it. Usually takes less than a minute.', 'maintained-log' ), 'button' => __( 'Get vulnerabilities now', 'maintained-log' ), 'enabled' => Vulnerabilities::is_enabled(), /* translators: %s: time since. */ 'last' => $vuln_state['last_success'] ? sprintf( __( 'Last update: %s ago.', 'maintained-log' ), human_time_diff( $vuln_state['last_success'] ) ) : __( 'Never downloaded.', 'maintained-log' ), ), array( 'what' => 'perf', 'title' => __( 'Speed (Google PageSpeed Insights)', 'maintained-log' ), 'text' => __( 'Measures every configured page and device now. Each measurement takes about 30 seconds.', 'maintained-log' ), 'button' => __( 'Measure speed now', 'maintained-log' ), 'enabled' => Performance::is_enabled(), /* translators: %s: time since. */ 'last' => $perf_last ? sprintf( __( 'Last measurement: %s ago.', 'maintained-log' ), human_time_diff( strtotime( $perf_last[0]->created_at . ' UTC' ) ) ) : __( 'Never measured.', 'maintained-log' ), ), ); ?>

__( 'Needs attention', 'maintained-log' ), 'inactive' => __( 'Deactivated', 'maintained-log' ), 'uncertain' => __( 'Possible match with another edition', 'maintained-log' ), 'reviewed' => __( 'Reviewed', 'maintained-log' ), ); ?>

display_name : '—', self::local_date( $exposure['review']['at'], get_option( 'date_format' ) ) ) ); ?>

$exposure['review_key'] ) ); ?>

$item ) : ?>

↗

$text ) { $key = sanitize_key( $key ); if ( isset( $items[ $key ] ) && Tech_Status::save_note( $key, (string) $text ) ) { ++$changed; } } self::redirect( self::SETTINGS_SLUG, 'success', $changed ? __( 'Notes saved. They will appear in the next client report.', 'maintained-log' ) : __( 'No changes.', 'maintained-log' ) ); } /** * One step of a "get the data now" run. */ public static function ajax_fetch() { check_ajax_referer( 'maintained_log_fetch' ); if ( ! Plugin::user_can_manage() ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'maintained-log' ) ), 403 ); } $what = isset( $_POST['what'] ) ? sanitize_key( wp_unslash( $_POST['what'] ) ) : ''; $op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : 'step'; if ( 'vuln' === $what ) { self::ajax_vuln_step( $op ); } if ( 'perf' === $what ) { self::ajax_perf_step( $op ); } wp_send_json_error( array( 'message' => __( 'Invalid request.', 'maintained-log' ) ), 400 ); } /** * Vulnerability sync, one step per request. * * @param string $op start or step. */ private static function ajax_vuln_step( $op ) { if ( 'start' === $op ) { $started = Vulnerabilities::start(); // A sync already running (e.g. started by cron) is simply continued from here. if ( is_wp_error( $started ) && 'running' !== $started->get_error_code() ) { wp_send_json_error( array( 'message' => $started->get_error_message() ) ); } wp_clear_scheduled_hook( Vulnerabilities::TICK_HOOK ); wp_send_json_success( array( 'done' => false, 'progress' => 2, 'message' => __( 'Downloading the vulnerability database…', 'maintained-log' ), ) ); } Vulnerabilities::tick(); wp_clear_scheduled_hook( Vulnerabilities::TICK_HOOK ); $state = Vulnerabilities::sync_state(); if ( 'idle' !== $state['state'] ) { wp_send_json_success( array( 'done' => false, 'progress' => Vulnerabilities::progress(), 'message' => 'parse' === $state['state'] ? __( 'Checking the vulnerabilities against this site…', 'maintained-log' ) : __( 'Downloading the vulnerability database…', 'maintained-log' ), ) ); } if ( $state['last_error'] ) { wp_send_json_error( array( 'message' => $state['last_error'] ) ); } $all = Vulnerabilities::exposures(); $exposures = Vulnerabilities::active_exposures(); $uncertain = count( array_filter( $all, static function ( $exposure ) { return 'uncertain' === $exposure['state']; } ) ); // Counted once per vulnerability, as in the report. $fixed_ids = array(); foreach ( Log::query( array( 'type' => Log::TYPE_UPDATE, 'from' => gmdate( 'Y-m-d H:i:s', time() - 31 * DAY_IN_SECONDS ), 'per_page' => 0, ) ) as $entry ) { foreach ( Vulnerabilities::fixed_by_entry( $entry ) as $vuln ) { $fixed_ids[ $vuln['id'] ] = true; } } $fixed = count( $fixed_ids ); $lines = array( /* translators: 1: vulnerabilities checked, 2: relevant to this site. */ sprintf( __( '%1$s vulnerabilities checked; %2$s concern software on this site.', 'maintained-log' ), number_format_i18n( isset( $state['total'] ) ? $state['total'] : 0 ), number_format_i18n( isset( $state['indexed'] ) ? $state['indexed'] : 0 ) ), /* translators: %d: number of vulnerabilities. */ sprintf( _n( 'Updates of the last month fixed %d known vulnerability.', 'Updates of the last month fixed %d known vulnerabilities.', $fixed, 'maintained-log' ), $fixed ), $exposures /* translators: %d: number of components. */ ? sprintf( _n( 'Warning: %d installed component has known vulnerabilities. See the log.', 'Warning: %d installed components have known vulnerabilities. See the log.', count( $exposures ), 'maintained-log' ), count( $exposures ) ) : __( 'No installed software has known vulnerabilities.', 'maintained-log' ), ); $inactive = count( wp_list_filter( $all, array( 'state' => 'inactive' ) ) ); if ( $inactive ) { /* translators: %d: number of components. */ $lines[] = sprintf( _n( 'Also %d deactivated plugin or theme has known vulnerabilities: delete it if it is not used.', 'Also %d deactivated plugins or themes have known vulnerabilities: delete them if they are not used.', $inactive, 'maintained-log' ), $inactive ); } if ( $uncertain ) { /* translators: %d: number of components. */ $lines[] = sprintf( _n( '%d possible match with another edition (e.g. Premium) to check.', '%d possible matches with another edition (e.g. Premium) to check.', $uncertain, 'maintained-log' ), $uncertain ); } wp_send_json_success( array( 'done' => true, 'progress' => 100, 'warning' => (bool) $exposures, 'lines' => $lines, /* translators: %s: time since. */ 'last' => sprintf( __( 'Last update: %s ago.', 'maintained-log' ), human_time_diff( time() - 1 ) ), ) ); } /** * Speed round, one measurement per request. * * @param string $op start or step. */ private static function ajax_perf_step( $op ) { if ( 'start' === $op ) { if ( ! Performance::queue_round() ) { wp_send_json_error( array( 'message' => __( 'Enable the speed module, add a Google API key and choose at least one device in the settings.', 'maintained-log' ) ) ); } wp_clear_scheduled_hook( Performance::TICK_HOOK ); $total = count( (array) get_option( Performance::QUEUE_OPTION, array() ) ); set_transient( 'maintained_log_perf_run_' . get_current_user_id(), array( 'total' => $total, 'lines' => array(), ), HOUR_IN_SECONDS ); wp_send_json_success( array( 'done' => false, 'progress' => 2, /* translators: %d: number of measurements. */ 'message' => sprintf( _n( '%d measurement to do…', '%d measurements to do…', $total, 'maintained-log' ), $total ), ) ); } $run = get_transient( 'maintained_log_perf_run_' . get_current_user_id() ); $run = is_array( $run ) ? $run : array( 'total' => 1, 'lines' => array(), ); $item = Performance::tick( false ); if ( $item ) { $label = Performance::url_label( $item['url'] ) . ' · ' . Performance::strategy_label( $item['strategy'] ); if ( is_wp_error( $item['result'] ) ) { $run['lines'][] = $label . ': ' . $item['result']->get_error_message(); } else { $rating = Performance::rating( $item['result']['score'] ); $run['lines'][] = sprintf( '%s: %s (%d/100)', $label, $rating['label'], $item['result']['score'] ); } set_transient( 'maintained_log_perf_run_' . get_current_user_id(), $run, HOUR_IN_SECONDS ); } $remaining = $item ? $item['remaining'] : 0; if ( $remaining > 0 ) { wp_send_json_success( array( 'done' => false, 'progress' => (int) floor( 100 * ( $run['total'] - $remaining ) / max( 1, $run['total'] ) ), /* translators: %d: measurements left. */ 'message' => sprintf( _n( 'Measuring… %d left.', 'Measuring… %d left.', $remaining, 'maintained-log' ), $remaining ), 'lines' => $run['lines'], ) ); } delete_transient( 'maintained_log_perf_run_' . get_current_user_id() ); wp_send_json_success( array( 'done' => true, 'progress' => 100, 'lines' => $run['lines'], /* translators: %s: time since. */ 'last' => sprintf( __( 'Last measurement: %s ago.', 'maintained-log' ), human_time_diff( time() - 1 ) ), ) ); } /** * Sync status line for the settings screen. */ private static function render_vuln_status() { $state = Vulnerabilities::sync_state(); if ( 'idle' !== $state['state'] ) { echo '

' . esc_html__( 'Updating vulnerability data…', 'maintained-log' ) . '

'; } elseif ( $state['last_success'] ) { echo '

'; /* translators: 1: time since, 2: records read, 3: records relevant to this site. */ echo esc_html( sprintf( __( 'Last update %1$s ago: %2$s vulnerabilities checked, %3$s concern software on this site (installed now or in the past).', 'maintained-log' ), human_time_diff( $state['last_success'] ), number_format_i18n( isset( $state['total'] ) ? $state['total'] : 0 ), number_format_i18n( isset( $state['indexed'] ) ? $state['indexed'] : 0 ) ) ); echo '

'; } else { echo '

' . esc_html__( 'Not downloaded yet.', 'maintained-log' ) . '

'; } if ( $state['last_error'] ) { /* translators: %s: error message. */ echo '

' . esc_html( sprintf( __( 'Last error: %s', 'maintained-log' ), $state['last_error'] ) ) . '

'; } } /** * Security badge and explanation on update entries that fixed vulnerabilities. * * @param object $entry Entry. */ private static function render_security_fixes( $entry ) { if ( Log::TYPE_UPDATE !== $entry->type || ! Settings::get( 'vuln_enabled' ) ) { return; } $fixed = Vulnerabilities::fixed_by_entry( $entry ); if ( ! $fixed ) { return; } $severity = Vulnerabilities::severity( $fixed[0] ); echo '
'; printf( '%2$s ', esc_attr( $severity['color'] ), /* translators: %d: number of vulnerabilities. */ esc_html( sprintf( _n( 'Fixed %d vulnerability', 'Fixed %d vulnerabilities', count( $fixed ), 'maintained-log' ), count( $fixed ) ) . ' · ' . $severity['label'] ) ); echo '
'; } /** * Replaces the public report token. */ public static function handle_regenerate_token() { self::verify( 'maintained_log_regenerate_token' ); Public_Report::regenerate(); Log::add( array( 'type' => Log::TYPE_SYSTEM, 'title' => __( 'Public report link regenerated: previous links no longer work', 'maintained-log' ), 'source' => Log::SOURCE_MANUAL, ) ); self::redirect( self::SETTINGS_SLUG, 'success', __( 'New link created. Previous links no longer work.', 'maintained-log' ) ); } /** * Recomputes the technical status. */ public static function handle_tech_refresh() { self::verify( 'maintained_log_tech_refresh' ); Tech_Status::refresh(); self::redirect( self::SLUG, 'success', __( 'Technical status updated.', 'maintained-log' ) ); } /** * Logs a backup check or restore test. */ public static function handle_backup_confirm() { self::verify( 'maintained_log_backup_confirm' ); $kind = isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( $_POST['kind'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above. Backups::confirm( $kind ) ? self::redirect( self::SLUG, 'success', __( 'Recorded in the log. It will appear in the next client report.', 'maintained-log' ) ) : self::redirect( self::SLUG, 'error', __( 'The confirmation could not be saved.', 'maintained-log' ) ); } /** * Reinstalls a previous version of a plugin or theme. */ public static function handle_rollback() { self::verify( 'maintained_log_rollback' ); if ( ! current_user_can( 'update_plugins' ) || ! current_user_can( 'update_themes' ) ) { self::redirect( self::SLUG, 'error', __( 'You are not allowed to change plugins or themes.', 'maintained-log' ) ); } // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified above. $component = isset( $_POST['component'] ) ? sanitize_key( wp_unslash( $_POST['component'] ) ) : ''; $key = isset( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; $version = isset( $_POST['version'] ) ? sanitize_text_field( wp_unslash( $_POST['version'] ) ) : ''; // phpcs:enable if ( ! in_array( $component, array( 'plugin', 'theme' ), true ) || validate_file( $key ) ) { self::redirect( self::SLUG, 'error', __( 'Invalid request.', 'maintained-log' ) ); } $result = Rollback::restore( $component, $key, $version ); if ( is_wp_error( $result ) ) { self::redirect( self::SLUG, 'error', $result->get_error_message() ); } // The version change itself is logged by the inventory diff; verify the site right away. if ( Settings::get( 'checks_after_update' ) ) { wp_schedule_single_event( time(), Plugin::POST_UPDATE_HOOK ); } /* translators: %s: version. */ self::redirect( self::SLUG, 'success', sprintf( __( 'Version %s restored. Site checks will run in a moment.', 'maintained-log' ), $version ) ); } /** * Restore button for update entries whose previous version is archived. * * @param object $entry Entry. */ private static function render_rollback_action( $entry ) { $d = $entry->details; if ( Log::TYPE_UPDATE !== $entry->type || empty( $d['kind'] ) || ! in_array( $d['kind'], array( 'updated', 'downgraded' ), true ) ) { return; } if ( empty( $d['component'] ) || ! in_array( $d['component'], array( 'plugin', 'theme' ), true ) || ! Rollback::has( $d['component'], $d['key'], $d['from'] ) ) { return; } if ( ! current_user_can( 'update_plugins' ) || ! current_user_can( 'update_themes' ) ) { return; } echo '
'; self::action_button( 'rollback', /* translators: %s: version. */ sprintf( __( 'Restore version %s', 'maintained-log' ), $d['from'] ), 'button button-small', array( 'component' => $d['component'], 'key' => $d['key'], 'version' => $d['from'], ), /* translators: 1: component name, 2: version. */ sprintf( __( 'Reinstall %1$s %2$s? The current version will be kept so you can switch back.', 'maintained-log' ), $d['name'], $d['from'] ) ); echo '
'; } /** * Renders structured details of an entry. * * @param object $entry Entry. */ private static function render_details( $entry ) { $d = $entry->details; if ( Log::TYPE_CHECK === $entry->type && ! empty( $d['items'] ) ) { echo ''; return; } if ( Log::TYPE_CHECK === $entry->type && ! empty( $d['results'] ) ) { echo ''; return; } if ( Log::TYPE_PERFORMANCE === $entry->type && isset( $d['score'] ) ) { $parts = array(); if ( isset( $d['metrics']['lcp'] ) ) { $parts[] = 'LCP ' . number_format_i18n( $d['metrics']['lcp'] / 1000, 1 ) . ' s'; } if ( isset( $d['metrics']['tbt'] ) ) { $parts[] = 'TBT ' . number_format_i18n( $d['metrics']['tbt'] ) . ' ms'; } if ( isset( $d['metrics']['cls'] ) ) { $parts[] = 'CLS ' . number_format_i18n( $d['metrics']['cls'], 2 ); } if ( ! empty( $d['field']['category'] ) ) { /* translators: %s: FAST, AVERAGE or SLOW. */ $parts[] = sprintf( __( 'real visitors: %s', 'maintained-log' ), $d['field']['category'] ); } echo '
' . esc_html( implode( ' · ', $parts ) ) . '
'; $texts = Performance::theme_texts(); if ( ! empty( $d['themes'] ) ) { echo ''; } return; } if ( Log::TYPE_ALERT === $entry->type && ! empty( $d['vulnerability'] ) ) { echo ''; if ( ! empty( $d['advice'] ) ) { echo '
' . esc_html( $d['advice'] ) . '
'; } return; } if ( Log::TYPE_ALERT === $entry->type && ! empty( $d['pending'] ) ) { echo ''; return; } if ( ! empty( $d['notes'] ) ) { echo '
' . wp_kses_post( nl2br( esc_html( $d['notes'] ) ) ) . '
'; } if ( ! empty( $d['logged_at'] ) ) { /* translators: %s: date. */ echo '
' . esc_html( sprintf( __( 'Logged on %s', 'maintained-log' ), self::local_date( $d['logged_at'], get_option( 'date_format' ) . ' H:i' ) ) ) . '
'; } } /** * Who made an entry. * * @param object $entry Entry. * @return string */ private static function author( $entry ) { $sources = array( Log::SOURCE_AUTO => __( 'Auto-update', 'maintained-log' ), Log::SOURCE_CLI => 'WP-CLI', Log::SOURCE_EXTERNAL => __( 'Detected', 'maintained-log' ), Log::SOURCE_SYSTEM => 'Maintained.log', ); $user = $entry->user_id ? get_userdata( $entry->user_id ) : false; if ( $user ) { $name = $user->display_name; return isset( $sources[ $entry->source ] ) ? $name . ' (' . $sources[ $entry->source ] . ')' : $name; } return isset( $sources[ $entry->source ] ) ? $sources[ $entry->source ] : '—'; } /** * Prevents CSV formula injection when opened in a spreadsheet. * * @param mixed $value Cell. * @return string */ private static function csv_safe( $value ) { $value = (string) $value; return ( '' !== $value && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) ? "'" . $value : $value; } /** * A one-button form posting to admin-post.php. * * @param string $action Action suffix. * @param string $label Button label. * @param string $class Extra class. * @param array $fields Extra hidden fields. * @param string $confirm Confirmation question, if any. */ private static function action_button( $action, $label, $class = 'button', array $fields = array(), $confirm = '' ) { ?>
> $value ) : ?>

label. * @param array $s Settings. */ private static function field_select( $key, $label, array $choices, array $s ) { $id = 'ml-' . $key; ?> getTimestamp() <= time() ? __( 'now (it is due)', 'maintained-log' ) : wp_date( get_option( 'date_format' ), $next->getTimestamp() ) ) : '', ); } $rows[] = '' !== $s['report_recipients'] ? array( 'ok', __( 'There is a client email', 'maintained-log' ), $s['report_recipients'] ) : array( 'error', __( 'There is no client email', 'maintained-log' ), __( 'Nothing is sent: the period is closed without sending and starts again with the next one. Add the email below.', 'maintained-log' ) ); $reason = Environment::blocking_reason(); $rows[] = '' === $reason ? array( 'ok', __( 'This is the live site', 'maintained-log' ), __( 'On a staging copy, or if the site address changes, client emails are blocked and recorded in the log, so a copy never writes to your client.', 'maintained-log' ) ) : array( 'error', __( 'Client emails are blocked here', 'maintained-log' ), $reason ); $rows[] = $s['report_send_if_empty'] ? array( 'ok', __( 'Sent even in a quiet period', 'maintained-log' ), __( 'If nothing was done in the period, the report still goes out to show that the site was watched.', 'maintained-log' ) ) : array( 'info', __( 'Quiet periods are skipped', 'maintained-log' ), __( 'If nothing was done in the period, no report is sent. Turn on "Send even if nothing changed" below to send it anyway.', 'maintained-log' ) ); if ( $held ) { $rows[] = array( 'error', __( 'The report is on hold now', 'maintained-log' ), /* translators: %s: time since. */ sprintf( __( 'A site check failed before sending, %s ago. It goes out when the checks pass or when the maximum days below are reached.', 'maintained-log' ), human_time_diff( (int) $held ) ), ); } elseif ( $s['report_hold'] ) { $rows[] = array( 'ok', __( 'Checked right before sending', 'maintained-log' ), /* translators: %d: number of days. */ sprintf( _n( 'If a page fails, the report waits and you get an email. It goes out as soon as the checks pass, or after %d day showing those pages as "under review".', 'If a page fails, the report waits and you get an email. It goes out as soon as the checks pass, or after %d days showing those pages as "under review".', (int) $s['report_hold_days'], 'maintained-log' ), (int) $s['report_hold_days'] ), ); } else { $rows[] = array( 'info', __( 'Checked right before sending, without waiting', 'maintained-log' ), __( 'If a page fails, the report goes out on time showing it as "under review". Turn on "Hold the scheduled report" below to wait until it is fixed.', 'maintained-log' ) ); } $cron_rows = array( Cron_Status::REAL => array( 'ok', __( 'A real cron job runs the tasks', 'maintained-log' ), __( 'Reports and alerts go out on time, whether or not the site has visits.', 'maintained-log' ) ), Cron_Status::VISITS => array( 'warning', __( 'Tasks depend on visits', 'maintained-log' ), __( 'WordPress only runs scheduled tasks when someone visits the site. With few visits, the report can arrive hours late. Set up a real cron job (see below).', 'maintained-log' ) ), Cron_Status::STOPPED => array( 'error', __( 'Scheduled tasks are not running', 'maintained-log' ), __( 'WP-Cron is disabled in wp-config.php and no cron job has called it recently: no report or alert will be sent until you set one up (see below).', 'maintained-log' ) ), Cron_Status::UNKNOWN => array( 'info', __( 'Waiting for the first run of the scheduled tasks', 'maintained-log' ), __( 'Come back later to see whether they run on their own. Setting up a real cron job (see below) makes sending reliable.', 'maintained-log' ) ), ); $rows[] = $cron_rows[ $cron['mode'] ]; $rows[] = array( 'info', __( 'If the email fails, it is retried', 'maintained-log' ), __( 'If the server cannot send it, it is tried again on the next run and the period is not closed. An SMTP plugin avoids most failures and reports ending up in spam.', 'maintained-log' ) ); $rows[] = array( 'info', __( 'Nothing is repeated', 'maintained-log' ), __( 'If you send a report by hand in the middle of a period, the next automatic one starts where it ended. The first automatic report never covers time before the plugin was installed.', 'maintained-log' ) ); $icons = array( 'ok' => 'dashicons-yes-alt', 'info' => 'dashicons-info-outline', 'warning' => 'dashicons-warning', 'error' => 'dashicons-dismiss', ); ?>

array( 'ok', __( 'Working: a real cron job is calling WordPress.', 'maintained-log' ) ), Cron_Status::VISITS => array( 'warning', __( 'Not set up: scheduled tasks run only when someone visits the site.', 'maintained-log' ) ), Cron_Status::STOPPED => array( 'error', __( 'Stopped: WP-Cron is disabled in wp-config.php and nothing is calling it.', 'maintained-log' ) ), Cron_Status::UNKNOWN => array( 'info', __( 'No run recorded yet.', 'maintained-log' ) ), ); $status = $texts[ $state['mode'] ]; ?>

$type, 'message' => $message, ), MINUTE_IN_SECONDS ); wp_safe_redirect( admin_url( 'admin.php?page=' . $page ) ); exit; } /** * Prints and clears the one-time notice. */ private static function flash_notice() { $key = 'maintained_log_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! $notice ) { return; } delete_transient( $key ); printf( '

%2$s

', esc_attr( $notice['type'] ), esc_html( $notice['message'] ) ); } }