[short title used in the progress bar]. * * @return array */ public static function steps() { return array( 'welcome' => __( 'Welcome', 'maintained-log' ), 'brand' => __( 'Your agency', 'maintained-log' ), 'report' => __( 'Client report', 'maintained-log' ), 'alerts' => __( 'Your alerts', 'maintained-log' ), 'checks' => __( 'Site checks', 'maintained-log' ), 'backups' => __( 'Backups', 'maintained-log' ), 'visibility' => __( 'Visibility', 'maintained-log' ), 'modules' => __( 'Extras', 'maintained-log' ), 'done' => __( 'Ready', 'maintained-log' ), ); } /** * Saved progress. * * @return array{step:string,completed:string[],done:bool,dismissed:bool} */ public static function state() { $state = get_option( self::OPTION ); return array_merge( array( 'step' => 'welcome', 'completed' => array(), 'done' => false, 'dismissed' => false, ), is_array( $state ) ? $state : array() ); } /** * URL of a step. * * @param string $step Step key. * @return string */ public static function url( $step = '' ) { $args = array( 'page' => self::SLUG ); if ( $step ) { $args['step'] = $step; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * Adds the page: visible in the menu until the setup is finished, then * reachable only from the settings screen. */ public static function menu() { // Once finished, it hangs from a parent that is not in the menu: hidden, but still reachable // (remove_submenu_page() would make WordPress deny access to the page). $parent = self::state()['done'] ? 'options.php' : Admin::SLUG; add_submenu_page( $parent, __( 'Setup assistant', 'maintained-log' ), __( 'Setup assistant', 'maintained-log' ), Plugin::user_can_manage() ? Plugin::capability() : 'do_not_allow', self::SLUG, array( __CLASS__, 'render' ) ); } /** * Opens the assistant right after the plugin is activated by hand. */ public static function maybe_redirect() { if ( ! get_transient( self::REDIRECT ) ) { return; } delete_transient( self::REDIRECT ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only reads whether several plugins were activated at once. if ( wp_doing_ajax() || is_network_admin() || isset( $_GET['activate-multi'] ) || self::state()['done'] || ! Plugin::user_can_manage() ) { return; } wp_safe_redirect( self::url() ); exit; } /** * Reminds to finish the setup on the plugin screens. */ public static function resume_notice() { $screen = get_current_screen(); $state = self::state(); if ( ! $screen || $state['done'] || $state['dismissed'] || false === strpos( $screen->id, Admin::SLUG ) || false !== strpos( $screen->id, self::SLUG ) || ! Plugin::user_can_manage() ) { return; } $keys = array_keys( self::steps() ); $number = array_search( $state['step'], $keys, true ) + 1; ?>

__( 'Choose your logo', 'maintained-log' ), 'useLogo' => __( 'Use this logo', 'maintained-log' ), ) ); } /** * Signed link to leave the assistant. * * @param string $nav dismiss (configure it myself) or skip_all (use the defaults). * @return string */ private static function nav_url( $nav ) { $url = add_query_arg( array( 'action' => 'maintained_log_onboarding', 'nav' => $nav, ), admin_url( 'admin-post.php' ) ); return wp_nonce_url( $url, 'maintained_log_onboarding' ); } /** * Renders the current step. */ public static function render() { if ( ! Plugin::user_can_manage() ) { wp_die( esc_html__( 'You are not allowed to access this page.', 'maintained-log' ), 403 ); } $steps = self::steps(); $state = self::state(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only. $step = isset( $_GET['step'] ) ? sanitize_key( wp_unslash( $_GET['step'] ) ) : $state['step']; $step = isset( $steps[ $step ] ) ? $step : 'welcome'; $keys = array_keys( $steps ); $index = array_search( $step, $keys, true ); $s = Settings::all(); ?>

    $label ) : ?>
0 && 'done' !== $step ) : ?>

Log::TYPE_SYSTEM, 'title' => __( 'Setup assistant skipped: default settings in use', 'maintained-log' ), 'source' => Log::SOURCE_MANUAL, ) ); } wp_safe_redirect( admin_url( 'admin.php?page=' . Admin::SLUG ) ); exit; } if ( ! in_array( $step, $steps, true ) ) { wp_safe_redirect( self::url() ); exit; } if ( 'back' !== $nav ) { $message = self::save_step( $step, $input, $nav ); if ( 'test_checks' === $nav || 'test_report' === $nav ) { self::flash( $message['type'], $message['text'] ); wp_safe_redirect( self::url( $step ) ); exit; } if ( ! in_array( $step, $state['completed'], true ) ) { $state['completed'][] = $step; } } $index = array_search( $step, $steps, true ); $next = 'back' === $nav ? $steps[ max( 0, $index - 1 ) ] : $steps[ min( count( $steps ) - 1, $index + 1 ) ]; // The saved step is the furthest one reached, so going back does not lose progress. if ( array_search( $next, $steps, true ) > array_search( $state['step'], $steps, true ) ) { $state['step'] = $next; } update_option( self::OPTION, $state, false ); wp_safe_redirect( self::url( $next ) ); exit; } /** * Applies the fields of one step. * * @param string $step Step key. * @param array $input Submitted fields. * @param string $nav next, test_checks or test_report. * @return array{type:string,text:string} */ private static function save_step( $step, array $input, $nav ) { $text = static function ( $key ) use ( $input ) { return isset( $input[ $key ] ) ? (string) $input[ $key ] : ''; }; $flag = static function ( $key ) use ( $input ) { return empty( $input[ $key ] ) ? 0 : 1; }; switch ( $step ) { case 'brand': Settings::update( array( 'agency_name' => $text( 'agency_name' ), 'agency_url' => $text( 'agency_url' ), 'logo_url' => $text( 'logo_url' ), 'brand_color' => $text( 'brand_color' ), ) ); break; case 'report': Settings::update( array( 'client_name' => $text( 'client_name' ), 'report_recipients' => $text( 'report_recipients' ), 'report_frequency' => $text( 'report_frequency' ), ) ); break; case 'alerts': Settings::update( array( 'admin_recipients' => $text( 'admin_recipients' ), 'maintainers' => isset( $input['maintainers'] ) && is_array( $input['maintainers'] ) ? $input['maintainers'] : array(), 'pending_alert' => $text( 'pending_alert' ), 'alert_on_failure' => $flag( 'alert_on_failure' ), 'tech_alert' => $flag( 'tech_alert' ), ) ); break; case 'checks': $lines = array(); $pages = isset( $input['pages'] ) && is_array( $input['pages'] ) ? $input['pages'] : array(); foreach ( $pages as $page ) { if ( empty( $page['on'] ) || empty( $page['url'] ) ) { continue; } $expected = isset( $page['text'] ) ? trim( str_replace( '|', '', (string) $page['text'] ) ) : ''; $lines[] = trim( (string) $page['url'] ) . ( '' !== $expected ? ' | ' . $expected : '' ); } Settings::update( array( 'check_urls' => implode( "\n", $lines ), 'checks_after_update' => $flag( 'checks_after_update' ), ) ); if ( 'test_checks' === $nav ) { if ( ! Health_Checks::targets() ) { return array( 'type' => 'warning', 'text' => __( 'Choose at least one page to check.', 'maintained-log' ), ); } $run = Health_Checks::run( 'manual', false ); set_transient( 'maintained_log_setup_checks_' . get_current_user_id(), $run['results'], HOUR_IN_SECONDS ); return $run['ok'] ? array( 'type' => 'success', 'text' => __( 'All the pages respond correctly. This test is already in the log.', 'maintained-log' ), ) : array( 'type' => 'warning', 'text' => __( 'Some pages did not pass. Check the details below: maybe the expected text is not exactly as it appears on the page.', 'maintained-log' ), ); } break; case 'backups': $who = $text( 'backup_who' ); $provider = ''; if ( 'hosting' === $who ) { $provider = __( 'the hosting provider', 'maintained-log' ); } elseif ( 'other' === $who ) { $provider = $text( 'backup_other' ); } Settings::update( array( 'backup_provider' => $provider, 'backup_frequency' => $text( 'backup_frequency' ), 'backup_retention' => $text( 'backup_retention' ), 'backup_reminder' => $text( 'backup_reminder' ), ) ); break; case 'visibility': Settings::update( array( 'dashboard_widget' => $flag( 'dashboard_widget' ), 'public_report_enabled' => $flag( 'public_report_enabled' ), ) ); break; case 'modules': Settings::update( array( 'vuln_enabled' => $flag( 'vuln_enabled' ), 'vuln_api_key' => $text( 'vuln_api_key' ), 'perf_enabled' => $flag( 'perf_enabled' ), 'perf_api_key' => $text( 'perf_api_key' ), ) ); break; case 'done': if ( 'test_report' === $nav ) { $period = Reporter::manual_period(); $result = Reporter::send( Reporter::build( $period['from'], $period['to'] ), wp_get_current_user()->user_email ); return is_wp_error( $result ) ? array( 'type' => 'error', 'text' => $result->get_error_message(), ) : array( 'type' => 'success', /* translators: %s: email address. */ 'text' => sprintf( __( 'Test report sent to %s. Check your inbox (and the spam folder, just in case).', 'maintained-log' ), wp_get_current_user()->user_email ), ); } break; } return array( 'type' => 'success', 'text' => '', ); } /** * Step 1. */ private static function step_welcome() { ?>

array( __( 'Monthly', 'maintained-log' ), __( 'Recommended. On the 1st of each month, with everything from the previous month.', 'maintained-log' ) ), 'weekly' => array( __( 'Weekly', 'maintained-log' ), __( 'Every Monday. For clients who want closer follow-up.', 'maintained-log' ) ), 'bimonthly' => array( __( 'Every two months', 'maintained-log' ), __( 'In January, March, May, July, September and November, with the previous two months.', 'maintained-log' ) ), 'quarterly' => array( __( 'Quarterly', 'maintained-log' ), __( 'In January, April, July and October, with the previous three months. For sites with little activity.', 'maintained-log' ) ), 'semiannual' => array( __( 'Every six months', 'maintained-log' ), __( 'In January and July, with the previous six months.', 'maintained-log' ) ), 'never' => array( __( 'Only when I send it', 'maintained-log' ), __( 'Nothing is sent automatically; you send it from the "Client report" screen.', 'maintained-log' ) ), ) ); self::hint( __( 'Before each automatic report, the site is checked again. If something fails, the report waits until it is fixed and you get an email, so the client never receives bad news you have not seen first.', 'maintained-log' ) ); } /** * Step 4. * * @param array $s Settings. */ private static function step_alerts( array $s ) { self::heading( __( 'Your alerts', 'maintained-log' ), __( 'These emails are for you, not for the client. They warn you when there is something to do, so you do not have to remember to check.', 'maintained-log' ) ); self::text( 'admin_recipients', __( 'Where should we warn you', 'maintained-log' ), $s['admin_recipients'], __( 'Your email or your team\'s. Client replies to the report will also arrive here.', 'maintained-log' ) ); $maintainers = Settings::maintainers() ? Settings::maintainers() : array( get_current_user_id() ); ?>

array( __( 'Weekly', 'maintained-log' ), __( 'Recommended. One summary a week with what is waiting to be updated.', 'maintained-log' ) ), 'daily' => array( __( 'Daily', 'maintained-log' ), __( 'For sites where you want to update as soon as possible.', 'maintained-log' ) ), 'never' => array( __( 'Never', 'maintained-log' ), __( 'If you already track updates another way.', 'maintained-log' ) ), ) ); self::toggle( 'alert_on_failure', __( 'Warn me right away if something breaks', 'maintained-log' ), $s['alert_on_failure'], __( 'If a page stops working after an update, you get an email at once.', 'maintained-log' ) ); self::toggle( 'tech_alert', __( 'Warn me about technical problems', 'maintained-log' ), $s['tech_alert'], __( 'For example, when the security certificate is about to expire or the PHP version stops receiving security updates.', 'maintained-log' ) ); } /** * Step 5. * * @param array $s Settings. */ private static function step_checks( array $s ) { self::heading( __( 'Site checks', 'maintained-log' ), __( 'After each update, Maintained.log opens the pages you choose here, as a visitor would, and checks that they load. It is the proof that the site still works.', 'maintained-log' ) ); $current = array(); foreach ( Health_Checks::targets() as $target ) { $current[ untrailingslashit( $target['url'] ) ] = $target['expected']; } $candidates = array( home_url( '/' ) => __( 'Home page', 'maintained-log' ) ); $pages = get_pages( array( 'sort_column' => 'menu_order,post_title', 'number' => 25, ) ); foreach ( $pages as $page ) { $candidates[ get_permalink( $page ) ] = $page->post_title ? $page->post_title : __( '(no title)', 'maintained-log' ); } foreach ( $current as $url => $expected ) { $found = false; foreach ( array_keys( $candidates ) as $candidate ) { if ( untrailingslashit( $candidate ) === $url ) { $found = true; } } if ( ! $found ) { $candidates[ $url ] = $url; } } ?>

$title ) : $key = untrailingslashit( $url ); $checked = isset( $current[ $key ] ) || ( ! $current && 0 === $i ); ?>
">?
>

array( __( 'Monthly', 'maintained-log' ), __( 'Recommended. You get an email; checking takes one minute in the hosting panel and one click here.', 'maintained-log' ) ), '90' => array( __( 'Quarterly', 'maintained-log' ), '' ), '0' => array( __( 'Never', 'maintained-log' ), '' ), ) ); self::hint( __( 'WordPress cannot see the copies made by the hosting, so the plugin does not check them by itself. Confirming them now and then is what lets the report say "backups checked on…" with a real date.', 'maintained-log' ) ); } /** * Step 7. * * @param array $s Settings. */ private static function step_visibility( array $s ) { self::heading( __( 'Visibility for your client', 'maintained-log' ), __( 'Besides the email, your client can see the maintenance every day, without asking you.', 'maintained-log' ) ); self::toggle( 'dashboard_widget', __( 'A "Website maintenance" box on the WordPress dashboard', 'maintained-log' ), $s['dashboard_widget'], __( 'Recommended. When your client logs in to WordPress, they see at the top that the site is maintained by you, when it was last reviewed and whether everything works.', 'maintained-log' ) ); self::toggle( 'public_report_enabled', __( 'A private link to each report', 'maintained-log' ), $s['public_report_enabled'], __( 'The report can also be opened in a browser, saved as PDF and forwarded. The link is secret but anyone who has it can see the report (which lists the installed plugins), and you can cancel it at any time.', 'maintained-log' ) ); } /** * Step 8. * * @param array $s Settings. */ private static function step_modules( array $s ) { self::heading( __( 'Extras (optional)', 'maintained-log' ), __( 'Two features that add a lot to the report. Each one needs a free key from an external service, so you can leave them for later without any problem.', 'maintained-log' ) ); ?>

[title, explanation]. */ private static function choice( $key, $label, $value, array $options ) { ?>
$texts ) : ?>
' . esc_html( $text ) . '

'; } /** * Stores a one-time message for the next screen. * * @param string $type success|warning|error. * @param string $text Message. */ private static function flash( $type, $text ) { if ( '' !== $text ) { set_transient( 'maintained_log_setup_notice_' . get_current_user_id(), array( 'type' => $type, 'text' => $text, ), MINUTE_IN_SECONDS ); } } /** * Prints and clears the one-time message. */ private static function notice() { $key = 'maintained_log_setup_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( $notice ) { delete_transient( $key ); printf( '
%2$s
', esc_attr( $notice['type'] ), esc_html( $notice['text'] ) ); } } }