Airworthy (single site) or Network Admin > Settings > Airworthy (multisite, where * the network admin has no Tools menu). * * The start card is rendered here (the target picker needs PHP's view of the server); the * progress and results views are built by assets/admin.js from the REST API * (/airworthy/v1). Everything the script shows from a scan is inserted as text, never HTML. */ final class Page { const SLUG = 'airworthy'; /** * Hook suffix returned when the page is registered; used to load assets only here. * * @var string */ private $hook = ''; /** * URL of the screen. * * @return string */ public static function url() { return is_multisite() ? network_admin_url( 'settings.php?page=' . self::SLUG ) : admin_url( 'tools.php?page=' . self::SLUG ); } /** * Registers menu and asset hooks. */ public function register() { add_action( is_multisite() ? 'network_admin_menu' : 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); } /** * Adds the menu item for users with the required capability. */ public function add_menu() { $title = __( 'Airworthy: PHP Upgrade Check', 'airworthy' ); $menu = __( 'Airworthy', 'airworthy' ); if ( is_multisite() ) { $this->hook = (string) add_submenu_page( 'settings.php', $title, $menu, Capabilities::required(), self::SLUG, array( $this, 'render' ) ); } else { $this->hook = (string) add_management_page( $title, $menu, Capabilities::required(), self::SLUG, array( $this, 'render' ) ); } } /** * Loads the screen's assets on this page only. * * @param string $hook_suffix Current admin page hook. */ public function enqueue( $hook_suffix ) { if ( '' === $this->hook || $hook_suffix !== $this->hook ) { return; } wp_enqueue_style( 'airworthy-admin', AIRWORTHY_URL . 'assets/admin.css', array(), self::asset_version( 'assets/admin.css' ) ); wp_enqueue_script( 'airworthy-admin', AIRWORTHY_URL . 'assets/admin.js', array( 'wp-api-fetch', 'wp-i18n' ), self::asset_version( 'assets/admin.js' ), true ); wp_set_script_translations( 'airworthy-admin', 'airworthy' ); wp_add_inline_script( 'airworthy-admin', 'window.airworthyConfig = ' . wp_json_encode( array( 'rest' => '/airworthy/v1', 'cronDisabled' => defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON, 'resultsActions' => self::results_actions(), 'review' => Review::config(), 'pluginUrl' => 'https://wordpress.org/plugins/airworthy/', // Airworthy Pro's full upgrade plan, when Pro adds it as a tab. 'planUrl' => isset( self::extra_tabs()['plan'] ) ? add_query_arg( 'tab', 'plan', self::url() ) : '', ) ) . ';', 'before' ); } /** * Extra buttons for the results header, from add-ons (Airworthy Pro's client report). * Each is array( 'label' => …, 'url' => … ) where "__SCAN__" in the URL becomes the scan ID. * * @return array */ private static function results_actions() { /** * Filters the extra buttons shown next to Export CSV on the results. * * @param array $actions Each array( 'label' => string, 'url' => string with "__SCAN__" ). */ $actions = (array) apply_filters( 'airworthy_results_actions', array() ); $out = array(); foreach ( $actions as $a ) { if ( is_array( $a ) && ! empty( $a['label'] ) && ! empty( $a['url'] ) ) { $out[] = array( 'label' => (string) $a['label'], 'url' => esc_url_raw( (string) $a['url'] ), ); } } return $out; } /** * One tab in the header's tab bar, with its icon. * * @param string $slug Tab slug. * @param string $label Tab label. * @param string $url Tab URL. * @param string $current The tab being shown. */ private static function tab_link( $slug, $label, $url, $current ) { /** * Filters the tab icons: slug => Dashicons class (e.g. "dashicons-chart-bar"). * * @param array $icons slug => class. */ $icons = (array) apply_filters( 'airworthy_admin_tab_icons', array( 'scan' => 'dashicons-search', 'settings' => 'dashicons-admin-generic', ) ); $icon = isset( $icons[ $slug ] ) ? sanitize_html_class( (string) $icons[ $slug ] ) : ''; $active = $slug === $current; printf( '%4$s%5$s', esc_url( $url ), $active ? ' is-active' : '', $active ? ' aria-current="page"' : '', '' !== $icon ? '' : '', esc_html( $label ) ); } /** * Extra tabs on the Airworthy screen, from add-ons: slug => label. Each is drawn by the * "airworthy_render_tab_{slug}" action. * * @return array */ public static function extra_tabs() { /** * Filters extra tabs on the Airworthy screen (after Scan and Settings). * * @param array $tabs slug => label. */ $tabs = (array) apply_filters( 'airworthy_admin_tabs', array() ); $out = array(); foreach ( $tabs as $slug => $label ) { $slug = sanitize_key( $slug ); if ( '' !== $slug && ! in_array( $slug, array( 'scan', 'settings' ), true ) ) { $out[ $slug ] = (string) $label; } } return $out; } /** * Asset version: the plugin version plus the file's modified time. A reinstall of the same * version still gets a new URL, so browsers and CDNs (which often keep CSS and JS for a year) * never serve a stale copy. * * @param string $relative Asset path relative to the plugin folder. * @return string */ private static function asset_version( $relative ) { return AIRWORTHY_VERSION . '.' . (int) @filemtime( AIRWORTHY_DIR . $relative ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Missing file: version 0. } /** * Picker label: version, security-support status, and "recommended" for the default. * * @param string $target Target version. * @param string $recommended Default (recommended) target. * @return string */ private static function target_label( $target, $recommended ) { $until = Targets::security_until( $target ); $date = $until ? date_i18n( get_option( 'date_format' ), strtotime( $until . ' 12:00:00 UTC' ) ) : ''; if ( ! Targets::is_released( $target ) ) { $label = sprintf( /* translators: 1: PHP version, 2: release date. */ __( 'PHP %1$s (release candidate, out on %2$s)', 'airworthy' ), $target, date_i18n( get_option( 'date_format' ), strtotime( Targets::RELEASED[ $target ] . ' 12:00:00 UTC' ) ) ); } elseif ( ! Targets::is_supported( $target ) ) { /* translators: 1: PHP version, 2: date security support ended. */ $label = sprintf( __( 'PHP %1$s (security fixes ended %2$s)', 'airworthy' ), $target, $date ); } elseif ( $until ) { /* translators: 1: PHP version, 2: date security support ends. */ $label = sprintf( __( 'PHP %1$s (security fixes until %2$s)', 'airworthy' ), $target, $date ); } else { /* translators: %s: PHP version. */ $label = sprintf( __( 'PHP %s', 'airworthy' ), $target ); } return $target === $recommended ? $label . ' ' . __( '– recommended', 'airworthy' ) : $label; } /** * One line explaining why the default target was chosen. * * @param string $current Server PHP version. * @param string $recommended Default (recommended) target. * @return string */ private static function default_explanation( $current, $recommended ) { $current_minor = implode( '.', array_slice( explode( '.', $current ), 0, 2 ) ); if ( version_compare( $recommended, $current_minor, '<=' ) ) { foreach ( Targets::ALL as $target ) { if ( version_compare( $target, $current_minor, '>' ) && ! Targets::is_released( $target ) ) { return sprintf( /* translators: 1: PHP version, 2: release date. */ __( 'Your server already runs the newest released PHP version. PHP %1$s comes out on %2$s: you can already check against its release candidate.', 'airworthy' ), $target, date_i18n( get_option( 'date_format' ), strtotime( Targets::RELEASED[ $target ] . ' 12:00:00 UTC' ) ) ); } } return __( 'Your server already runs the newest PHP version Airworthy can check.', 'airworthy' ); } $oldest = null; foreach ( Targets::ALL as $target ) { if ( Targets::is_supported( $target ) ) { $oldest = $target; break; } } if ( $recommended === $oldest ) { /* translators: %s: PHP version. */ return sprintf( __( 'PHP %s is recommended: it is the oldest version that still gets security fixes, so it is the smallest upgrade that keeps your site protected. Pick a newer version to plan further ahead.', 'airworthy' ), $recommended ); } /* translators: %s: PHP version. */ return sprintf( __( 'PHP %s is recommended: it is the next version up from your server\'s that still gets security fixes. Pick a newer version to plan further ahead.', 'airworthy' ), $recommended ); } /** * Renders the screen: the start card, plus containers the script fills with the progress * and results views. */ public function render() { if ( ! Capabilities::current_user_can_scan() ) { wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'airworthy' ), 403 ); } Notice::mark_seen(); // The results are on screen now. if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $extra_tabs = self::extra_tabs(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks which tab to show. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'scan'; $tab = 'settings' === $tab || isset( $extra_tabs[ $tab ] ) ? $tab : 'scan'; $components = \Airworthy\Scan\Components::discover(); // With the site's settings applied. $skipped = \Airworthy\Scan\Components::$last_skipped; $plugin_count = count( wp_list_filter( $components, array( 'type' => 'plugin' ) ) ); $theme = wp_get_theme(); $current = \Airworthy\Env::php_version(); $default = Targets::default_for( $current ); ?>

'; return; } if ( 'scan' !== $tab ) { /** * Draws an add-on's tab (see the airworthy_admin_tabs filter). */ do_action( 'airworthy_render_tab_' . $tab ); echo '
'; return; } ?>

array( __( 'Gentle', 'airworthy' ), __( 'Short batches with pauses in between. Takes longer; easiest on cheap shared hosting.', 'airworthy' ) ), 'normal' => array( __( 'Normal (recommended)', 'airworthy' ), __( 'Batches of up to 20 seconds, one after another.', 'airworthy' ) ), 'fast' => array( __( 'Fast', 'airworthy' ), __( 'Longer batches, for servers with room to spare. Still stops well before your server\'s time limit.', 'airworthy' ) ), ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only shows the saved message. $updated = isset( $_GET['updated'] ); ?>

$speed ) : ?>