plugin_name = is_string( $plugin_name ) && '' !== $plugin_name ? $plugin_name : 'ajax-loading'; if ( ! is_string( $version ) || '' === $version ) { $version = defined( 'WPAJXL_PLUGIN_VERSION' ) ? WPAJXL_PLUGIN_VERSION : '2.0.0'; } $this->version = $version; } /** * Register all admin hooks. * * @return void */ public function register() { if ( ! class_exists( 'WPAJXL_Settings' ) ) { return; } add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_page_access_denied', array( $this, 'maybe_redirect_legacy_slug' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_post_' . self::ACTION, array( $this, 'handle_action' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); add_filter( 'plugin_action_links_' . $this->plugin_basename(), array( $this, 'action_links' ) ); add_filter( 'removable_query_args', array( $this, 'removable_query_args' ) ); } /* * --------------------------------------------------------------------- * Hooks * --------------------------------------------------------------------- */ /** * Add Settings → AJAX Loading. * * @return void */ public function add_menu() { $suffix = add_options_page( __( 'AJAX Loading', 'ajax-loading' ), __( 'AJAX Loading', 'ajax-loading' ), self::CAPABILITY, self::PAGE_SLUG, array( $this, 'render_page' ) ); $this->hook_suffix = is_string( $suffix ) ? $suffix : ''; } /** * Register the option with the Settings API (options.php handles saving). * * @return void */ public function register_settings() { register_setting( self::OPTION_GROUP, self::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( WPAJXL_Settings::instance(), 'sanitize_option_input' ), 'show_in_rest' => false, ) ); } /** * Redirect the 0.x page slug to the current settings screen. * * Runs on 'admin_page_access_denied', which WordPress fires for the * unregistered legacy slug right before it would show "not allowed". * * @return void */ public function maybe_redirect_legacy_slug() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( self::LEGACY_PAGE_SLUG !== $page || ! current_user_can( self::CAPABILITY ) ) { return; } wp_safe_redirect( $this->page_url() ); exit; } /** * Enqueue admin assets on the plugin screen only. * * @param string $hook_suffix Current admin page hook suffix. * @return void */ public function enqueue_assets( $hook_suffix ) { if ( '' === $this->hook_suffix || $hook_suffix !== $this->hook_suffix ) { return; } $tab = $this->current_tab(); wp_enqueue_style( 'wp-color-picker' ); if ( 'loading' === $tab ) { // Logo picker and live preview are only on the Loading Experience tab. wp_enqueue_media(); wp_enqueue_style( 'ajax-loading-preview', $this->asset_url( 'frontend', 'css' ), array(), $this->asset_version( 'frontend', 'css' ) ); } wp_enqueue_style( 'ajax-loading-admin', $this->asset_url( 'admin', 'css' ), array( 'wp-color-picker' ), $this->asset_version( 'admin', 'css' ) ); wp_enqueue_script( 'ajax-loading-admin', $this->asset_url( 'admin', 'js' ), array( 'jquery', 'wp-color-picker' ), $this->asset_version( 'admin', 'js' ), array( 'in_footer' => true ) ); $json = wp_json_encode( $this->script_data( $tab ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ); wp_add_inline_script( 'ajax-loading-admin', 'window.ajaxLoadingAdmin = ' . ( false === $json ? '{}' : $json ) . ';', 'before' ); } /** * Quick actions (admin-post.php?action=ajax_loading_action). * * @return void */ public function handle_action() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to manage AJAX Loading.', 'ajax-loading' ), esc_html__( 'Forbidden', 'ajax-loading' ), array( 'response' => 403, 'back_link' => true, ) ); } $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : ''; if ( 'POST' !== $method ) { wp_die( esc_html__( 'This action must be submitted from the AJAX Loading settings screen.', 'ajax-loading' ), esc_html__( 'Method not allowed', 'ajax-loading' ), array( 'response' => 405, 'back_link' => true, ) ); } check_admin_referer( self::NONCE_ACTION ); $do = ''; if ( isset( $_POST['do'] ) ) { $do = sanitize_key( wp_unslash( $_POST['do'] ) ); } elseif ( isset( $_GET['do'] ) ) { $do = sanitize_key( wp_unslash( $_GET['do'] ) ); } $tab = isset( $_POST['ajxl_tab'] ) ? sanitize_key( wp_unslash( $_POST['ajxl_tab'] ) ) : 'overview'; $return = ( isset( $_POST['ajxl_return'] ) && 'plugins' === sanitize_key( wp_unslash( $_POST['ajxl_return'] ) ) ) ? 'plugins' : 'page'; if ( 'export' === $do ) { $this->send_export( $tab ); } $notice = 'action_failed'; try { $notice = $this->run_action( $do ); } catch ( Throwable $e ) { self::debug_log( 'quick action "' . $do . '" failed: ' . $e->getMessage() ); $notice = 'action_failed'; } $this->redirect_after_action( $tab, $notice, $return ); } /** * Admin notices: action results and state warnings (own screen + Plugins screen). * * @return void */ public function admin_notices() { if ( ! current_user_can( self::CAPABILITY ) || ! function_exists( 'get_current_screen' ) ) { return; } $screen = get_current_screen(); if ( ! $screen ) { return; } $own = ( '' !== $this->hook_suffix && $screen->id === $this->hook_suffix ); $plugins = ( 'plugins' === $screen->id ); if ( ! $own && ! $plugins ) { return; } $code = $this->notice_code(); if ( '' !== $code ) { $this->print_result_notice( $code ); } elseif ( $own && $this->settings_were_saved() && class_exists( 'WPAJXL_Compat' ) && method_exists( 'WPAJXL_Compat', 'has_page_cache' ) && WPAJXL_Compat::has_page_cache() ) { printf( '

%s

', esc_html( self::page_cache_hint() ) ); } $this->print_state_notices( $own, $code ); } /** * Whether this request follows a successful save of the settings form (options.php redirect). * * @return bool */ protected function settings_were_saved() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. return isset( $_GET['settings-updated'] ) && 'true' === sanitize_key( wp_unslash( $_GET['settings-updated'] ) ); } /** * Advice shown after a change that only reaches page-cached pages once the cache is purged. * * @return string */ protected static function page_cache_hint() { return __( 'A page cache is active on this site: purge it, otherwise visitors keep getting cached pages built with the previous settings.', 'ajax-loading' ); } /** * Plugin list links: Settings first, then Say Thanks. * * @param array $links Existing links. * @return array */ public function action_links( $links ) { $links = is_array( $links ) ? $links : array(); $mine = array( 'ajxl-settings' => sprintf( '%2$s', esc_url( $this->page_url() ), esc_html__( 'Settings', 'ajax-loading' ) ), 'ajxl-thanks' => sprintf( '%2$s %3$s', esc_url( self::THANKS_URL ), esc_html__( 'Say Thanks', 'ajax-loading' ), esc_html__( '(opens in a new tab)', 'ajax-loading' ) ), ); return array_merge( $mine, $links ); } /** * Strip one-time notice codes from the address bar after display. * * @param array $args Removable query args. * @return array */ public function removable_query_args( $args ) { $args = is_array( $args ) ? $args : array(); $args[] = 'ajxl_notice'; return $args; } /* * --------------------------------------------------------------------- * Page rendering * --------------------------------------------------------------------- */ /** * Render the settings screen. * * @return void */ public function render_page() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to manage AJAX Loading.', 'ajax-loading' ), 403 ); } $tab = $this->current_tab(); $tabs = $this->tabs(); $status = $this->navigation_status(); ?>

version ); ?>


load_view( $tab ); ?>
render_action_forms( $tab ); ?>
tabs(); if ( ! isset( $tabs[ $view ] ) ) { return; } $file = __DIR__ . '/views/' . $view . '.php'; if ( ! is_readable( $file ) ) { return; } $admin = $this; $settings = $this->settings(); $values = $this->values(); $schema = $this->schema(); $tab = $view; include $file; } /** * Open the options.php settings form for a tab. * * @param string $tab Tab slug. * @return void */ public function open_settings_form( $tab ) { printf( '
', esc_url( admin_url( 'options.php' ) ) ); settings_fields( self::OPTION_GROUP ); // A clean return URL (the current request may carry a one-time ajxl_notice); the later field wins. printf( '', esc_attr( $this->relative_tab_url( $tab ) ) ); printf( '', esc_attr( WPAJXL_Admin_Fields::name( '_ajxl_tab' ) ), esc_attr( $tab ) ); } /** * Close the settings form with a sticky save bar. * * @return void */ public function close_settings_form() { echo '
'; } /** * Render every schema field of a tab (grouped in cards). * * @param string $tab Tab slug. * @return void */ public function render_fields( $tab ) { WPAJXL_Admin_Fields::render_tab( $tab, $this->schema(), $this->values() ); } /** * Echo a submit button bound to a hidden quick-action form. * * @param string $do Action. * @param string $label Button text. * @param array $args { class, name, value, confirm, icon, data (name => value, without the data- prefix) }. * @return void */ public function action_button( $do, $label, array $args = array() ) { $args = wp_parse_args( $args, array( 'class' => 'button', 'name' => '', 'value' => '', 'confirm' => '', 'icon' => '', 'data' => array(), ) ); $extra = ''; if ( '' !== $args['name'] ) { $extra .= sprintf( ' name="%s" value="%s"', esc_attr( $args['name'] ), esc_attr( $args['value'] ) ); } if ( '' !== $args['confirm'] ) { $extra .= sprintf( ' data-ajxl-confirm="%s"', esc_attr( $args['confirm'] ) ); } foreach ( (array) $args['data'] as $data_name => $data_value ) { $extra .= sprintf( ' data-%s="%s"', esc_attr( sanitize_key( $data_name ) ), esc_attr( (string) $data_value ) ); } $icon = '' !== $args['icon'] ? sprintf( ' ', esc_attr( $args['icon'] ) ) : ''; printf( '', esc_attr( 'ajxl-action-' . sanitize_key( $do ) ), esc_attr( $args['class'] ), $extra, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped when built. $icon, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped when built. esc_html( $label ) ); } /** * Echo the hidden fields of a quick-action form. * * @param string $do Action. * @param string $tab Tab to return to. * @param string $return 'page' or 'plugins'. * @return void */ public function action_fields( $do, $tab, $return = 'page' ) { printf( '', esc_attr( self::ACTION ) ); printf( '', esc_attr( $do ) ); printf( '', esc_attr( $tab ) ); printf( '', esc_attr( wp_create_nonce( self::NONCE_ACTION ) ) ); if ( 'plugins' === $return ) { echo ''; } } /** * Open a visible quick-action form (used for import/reset/export). * * @param string $do Action. * @param string $tab Tab to return to. * @param array $attrs Extra attributes: class, enctype, confirm. * @return void */ public function open_action_form( $do, $tab, array $attrs = array() ) { $extra = ''; if ( ! empty( $attrs['enctype'] ) ) { $extra .= sprintf( ' enctype="%s"', esc_attr( $attrs['enctype'] ) ); } if ( ! empty( $attrs['confirm'] ) ) { $extra .= sprintf( ' data-ajxl-confirm="%s"', esc_attr( $attrs['confirm'] ) ); } printf( '
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( trim( 'ajxl-action-form ' . ( isset( $attrs['class'] ) ? $attrs['class'] : '' ) ) ), $extra // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped when built. ); $this->action_fields( $do, $tab ); } /** * Hidden forms targeted by action_button() (forms cannot be nested in the settings form). * * @param string $tab Current tab. * @return void */ protected function render_action_forms( $tab ) { $actions = array( 'toggle_enable', 'toggle_safe_mode', 'toggle_visibility', 'pause', 'resume', 'clear_cache', 'clear_log' ); echo '
'; } /* * --------------------------------------------------------------------- * Data helpers used by views * --------------------------------------------------------------------- */ /** * Tabs: slug => [ label, icon, form ]. * * @return array */ public function tabs() { return array( 'overview' => array( 'label' => __( 'Overview', 'ajax-loading' ), 'icon' => 'dashicons-dashboard', ), 'general' => array( 'label' => __( 'General', 'ajax-loading' ), 'icon' => 'dashicons-admin-generic', ), 'loading' => array( 'label' => __( 'Loading Experience', 'ajax-loading' ), 'icon' => 'dashicons-update', ), 'performance' => array( 'label' => __( 'Performance', 'ajax-loading' ), 'icon' => 'dashicons-performance', ), 'seo' => array( 'label' => __( 'SEO & Integrations', 'ajax-loading' ), 'icon' => 'dashicons-chart-line', ), 'exclusions' => array( 'label' => __( 'Exclusions', 'ajax-loading' ), 'icon' => 'dashicons-shield', ), 'tools' => array( 'label' => __( 'Tools & Diagnostics', 'ajax-loading' ), 'icon' => 'dashicons-admin-tools', ), 'about' => array( 'label' => __( 'About', 'ajax-loading' ), 'icon' => 'dashicons-editor-help', ), 'thanks' => array( 'label' => __( 'Say Thanks', 'ajax-loading' ), 'icon' => 'dashicons-heart', ), ); } /** * Current tab from the query string (legacy &con=about|donate supported). * * @return string */ public function current_tab() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only view routing. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; if ( '' === $tab && isset( $_GET['con'] ) ) { $legacy = sanitize_key( wp_unslash( $_GET['con'] ) ); if ( 'about' === $legacy ) { $tab = 'about'; } elseif ( 'donate' === $legacy ) { $tab = 'thanks'; } } // phpcs:enable $tabs = $this->tabs(); return isset( $tabs[ $tab ] ) ? $tab : 'overview'; } /** * Settings page URL (no tab). * * @return string */ public function page_url() { return add_query_arg( 'page', self::PAGE_SLUG, admin_url( 'options-general.php' ) ); } /** * URL of a tab. * * @param string $tab Tab slug. * @param array $args Extra query args. * @param string $fragment Optional #fragment (without '#'). * @return string */ public function tab_url( $tab, array $args = array(), $fragment = '' ) { $url = add_query_arg( array_merge( array( 'page' => self::PAGE_SLUG, 'tab' => sanitize_key( $tab ), ), $args ), admin_url( 'options-general.php' ) ); return '' === $fragment ? $url : $url . '#' . rawurlencode( $fragment ); } /** * Root-relative URL of a tab (used as the options.php return address). * * @param string $tab Tab slug. * @return string */ protected function relative_tab_url( $tab ) { $path = wp_parse_url( admin_url( 'options-general.php' ), PHP_URL_PATH ); $path = is_string( $path ) && '' !== $path ? $path : '/wp-admin/options-general.php'; return $path . '?page=' . rawurlencode( self::PAGE_SLUG ) . '&tab=' . rawurlencode( sanitize_key( $tab ) ); } /** * Settings service. * * @return WPAJXL_Settings */ public function settings() { return WPAJXL_Settings::instance(); } /** * Stored settings merged over defaults. * * @return array */ public function values() { $values = $this->settings()->all(); return is_array( $values ) ? $values : array(); } /** * Settings schema. * * @return array */ public function schema() { $schema = $this->settings()->schema(); return is_array( $schema ) ? $schema : array(); } /** * One stored value with a fallback. * * @param string $key Setting key. * @param mixed $fallback Fallback. * @return mixed */ public function value( $key, $fallback = null ) { $values = $this->values(); return array_key_exists( $key, $values ) ? $values[ $key ] : $fallback; } /** * Boolean view of a stored value. * * @param string $key Setting key. * @return bool */ public function flag( $key ) { return WPAJXL_Admin_Fields::to_bool( $this->value( $key, false ) ); } /** * Human label of a setting's current (or given) choice. * * @param string $key Setting key. * @param string|null $value Value (null = current). * @return string */ public function choice_label( $key, $value = null ) { $schema = $this->schema(); $value = null === $value ? $this->value( $key, '' ) : $value; return WPAJXL_Admin_Fields::choice_label( $key, is_scalar( $value ) ? (string) $value : '', isset( $schema[ $key ] ) && is_array( $schema[ $key ] ) ? $schema[ $key ] : array() ); } /** * Navigation state for the header pill, overview card and notices. * * @return array { status, label, detail, flags[] } */ public function navigation_status() { $settings = $this->settings(); $flags = array(); if ( 'on' !== $this->value( 'enable_plugin', 'off' ) ) { return array( 'status' => 'error', 'label' => __( 'Off', 'ajax-loading' ), 'detail' => __( 'AJAX navigation is turned off. Every page loads normally.', 'ajax-loading' ), 'flags' => array(), ); } if ( $this->flag( 'safe_mode' ) ) { $flags[] = __( 'Safe mode', 'ajax-loading' ); } if ( $settings->is_paused() ) { return array( 'status' => 'warning', 'label' => __( 'Paused', 'ajax-loading' ), 'detail' => sprintf( /* translators: %s: date and time. */ __( 'Paused until %s. Pages load normally until then.', 'ajax-loading' ), $this->format_time( (int) $this->value( 'pause_until', 0 ) ) ), 'flags' => $flags, ); } if ( 'admins' === $this->value( 'visibility', 'everyone' ) ) { return array( 'status' => 'info', 'label' => __( 'Administrators only', 'ajax-loading' ), 'detail' => __( 'Test mode: only logged-in administrators get AJAX navigation. Visitors get normal page loads.', 'ajax-loading' ), 'flags' => $flags, ); } return array( 'status' => 'ok', 'label' => __( 'Active', 'ajax-loading' ), 'detail' => empty( $flags ) ? __( 'AJAX navigation is active for all visitors.', 'ajax-loading' ) : __( 'AJAX navigation is active for all visitors, with conservative safe-mode behaviour.', 'ajax-loading' ), 'flags' => $flags, ); } /** * Recent client errors (newest first). * * @param int $limit 0 = all. * @return array */ public function log_entries( $limit = 0 ) { $entries = self::invoke( 'WPAJXL_Logger', 'get', array(), array() ); if ( ! is_array( $entries ) ) { return array(); } $entries = array_values( array_filter( $entries, 'is_array' ) ); usort( $entries, static function ( $a, $b ) { $ta = isset( $a['time'] ) ? (int) $a['time'] : 0; $tb = isset( $b['time'] ) ? (int) $b['time'] : 0; return $tb - $ta; } ); return $limit > 0 ? array_slice( $entries, 0, (int) $limit ) : $entries; } /** * Environment report items, normalised. * * @return array List of [ id, status, label, message ]. */ public function environment_report() { $items = self::invoke( 'WPAJXL_Compat', 'environment_report', array(), array() ); $out = array(); if ( ! is_array( $items ) ) { return $out; } foreach ( $items as $index => $item ) { if ( ! is_array( $item ) ) { continue; } $status = isset( $item['status'] ) ? (string) $item['status'] : 'info'; $out[] = array( 'id' => isset( $item['id'] ) ? (string) $item['id'] : (string) $index, 'status' => in_array( $status, array( 'ok', 'info', 'warning', 'error' ), true ) ? $status : 'info', 'label' => isset( $item['label'] ) ? (string) $item['label'] : '', 'message' => isset( $item['message'] ) ? (string) $item['message'] : '', ); } return $out; } /** * Last client diagnostics summary stored by the REST API. * * @return array { time, ok, checks[], counts{ok,info,warning,error} } or empty array. */ public function last_diagnostics() { $stored = get_option( self::DIAGNOSTICS_OPTION, array() ); if ( ! is_array( $stored ) || empty( $stored['time'] ) ) { return array(); } $checks = isset( $stored['checks'] ) && is_array( $stored['checks'] ) ? array_values( array_filter( $stored['checks'], 'is_array' ) ) : array(); $counts = array( 'ok' => 0, 'info' => 0, 'warning' => 0, 'error' => 0, ); foreach ( $checks as $check ) { $status = isset( $check['status'] ) ? (string) $check['status'] : 'info'; if ( isset( $counts[ $status ] ) ) { ++$counts[ $status ]; } } return array( 'time' => (int) $stored['time'], 'ok' => ! empty( $stored['ok'] ), 'checks' => $checks, 'counts' => $counts, ); } /** * Built-in safety rules (read-only) as [ label, detail ] pairs. * * @return array */ public function safety_rules() { $rules = self::invoke( 'WPAJXL_Exclusions', 'readonly_safety_rules', array(), array() ); $out = array(); if ( ! is_array( $rules ) ) { return $out; } foreach ( $rules as $key => $rule ) { if ( is_string( $rule ) && '' !== $rule ) { $out[] = array( 'label' => $rule, 'detail' => '', ); } elseif ( is_array( $rule ) ) { $label = ''; $detail = ''; foreach ( array( 'label', 'title', 'name' ) as $field ) { if ( ! empty( $rule[ $field ] ) && is_string( $rule[ $field ] ) ) { $label = $rule[ $field ]; break; } } foreach ( array( 'description', 'detail', 'message' ) as $field ) { if ( ! empty( $rule[ $field ] ) && is_string( $rule[ $field ] ) ) { $detail = $rule[ $field ]; break; } } if ( ! empty( $rule['patterns'] ) && is_array( $rule['patterns'] ) ) { $detail = trim( $detail . ' ' . implode( ', ', array_filter( $rule['patterns'], 'is_string' ) ) ); } if ( '' === $label && is_string( $key ) ) { $label = $key; } if ( '' !== $label || '' !== $detail ) { $out[] = array( 'label' => '' !== $label ? $label : $detail, 'detail' => '' !== $label ? $detail : '', ); } } } return $out; } /** * Summary of exclusion rules for the Overview tab. * * @return array { urls, selectors, post_types, sets, total } */ public function exclusion_summary() { $urls = $this->count_lines( $this->value( 'exclude_urls', '' ) ); $selectors = $this->count_lines( $this->value( 'exclude_selectors', '' ) ); $post_types = $this->value( 'exclude_post_types', array() ); $post_types = is_array( $post_types ) ? count( array_filter( $post_types, 'strlen' ) ) : 0; $sets = 1; // WordPress system paths (admin, login, feeds, REST, files) are always excluded. if ( $this->flag( 'exclude_woocommerce' ) && class_exists( 'WooCommerce' ) ) { ++$sets; } if ( $this->flag( 'exclude_membership' ) ) { ++$sets; } return array( 'urls' => $urls, 'selectors' => $selectors, 'post_types' => $post_types, 'sets' => $sets, 'total' => $urls + $selectors + $post_types + $sets, ); } /** * Count non-empty lines of a "lines" value. * * @param mixed $value String or list. * @return int */ public function count_lines( $value ) { if ( is_array( $value ) ) { $lines = $value; } elseif ( is_string( $value ) ) { $lines = preg_split( '/\r\n|\r|\n/', $value ); } else { return 0; } $count = 0; foreach ( (array) $lines as $line ) { if ( is_scalar( $line ) && '' !== trim( (string) $line ) ) { ++$count; } } return $count; } /** * Sample front-end URLs for diagnostics (home, latest post, a page, a category, search). * * @return array List of [ id, label, url ]. */ public function sample_urls() { if ( null !== $this->sample_urls ) { return $this->sample_urls; } $urls = array(); $urls[] = array( 'id' => 'home', 'label' => __( 'Home page', 'ajax-loading' ), 'url' => home_url( '/' ), ); $posts = get_posts( array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => 1, 'has_password' => false, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( ! empty( $posts ) ) { $link = get_permalink( (int) $posts[0] ); if ( $link ) { $urls[] = array( 'id' => 'post', /* translators: %s: post title. */ 'label' => sprintf( __( 'Latest post: %s', 'ajax-loading' ), $this->plain_title( (int) $posts[0] ) ), 'url' => $link, ); } } $skip = array_filter( array( (int) get_option( 'page_on_front' ), (int) get_option( 'page_for_posts' ) ) ); $pages = get_posts( array( 'post_type' => 'page', 'post_status' => 'publish', 'posts_per_page' => 1, 'has_password' => false, 'post__not_in' => $skip, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- at most two ids, admin only. 'orderby' => array( 'menu_order' => 'ASC', 'date' => 'DESC', ), 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( ! empty( $pages ) ) { $link = get_permalink( (int) $pages[0] ); if ( $link ) { $urls[] = array( 'id' => 'page', /* translators: %s: page title. */ 'label' => sprintf( __( 'Page: %s', 'ajax-loading' ), $this->plain_title( (int) $pages[0] ) ), 'url' => $link, ); } } $terms = get_terms( array( 'taxonomy' => 'category', 'number' => 1, 'hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC', ) ); if ( ! is_wp_error( $terms ) && ! empty( $terms ) && $terms[0] instanceof WP_Term ) { $link = get_term_link( $terms[0] ); if ( ! is_wp_error( $link ) ) { $urls[] = array( 'id' => 'category', /* translators: %s: category name. */ 'label' => sprintf( __( 'Category: %s', 'ajax-loading' ), wp_strip_all_tags( $terms[0]->name ) ), 'url' => $link, ); } } $urls[] = array( 'id' => 'search', 'label' => __( 'Search results', 'ajax-loading' ), 'url' => add_query_arg( 's', 'a', home_url( '/' ) ), ); $this->sample_urls = $urls; return $urls; } /** * Plain-text title of a post. * * @param int $post_id Post id. * @return string */ protected function plain_title( $post_id ) { $title = trim( wp_strip_all_tags( html_entity_decode( get_the_title( $post_id ), ENT_QUOTES, 'UTF-8' ) ) ); /* translators: %d: post id. */ return '' !== $title ? $title : sprintf( __( '#%d (no title)', 'ajax-loading' ), $post_id ); } /** * Format a unix timestamp in the site's date/time format. * * @param int $timestamp Unix timestamp. * @return string */ public function format_time( $timestamp ) { $timestamp = (int) $timestamp; if ( $timestamp <= 0 ) { return ''; } $format = trim( get_option( 'date_format', 'Y-m-d' ) . ' ' . get_option( 'time_format', 'H:i' ) ); $output = wp_date( $format, $timestamp ); return is_string( $output ) ? $output : gmdate( 'Y-m-d H:i', $timestamp ); } /** * "5 minutes ago" style relative time. * * @param int $timestamp Unix timestamp. * @return string */ public function time_ago( $timestamp ) { $timestamp = (int) $timestamp; if ( $timestamp <= 0 ) { return ''; } /* translators: %s: human-readable time difference. */ return sprintf( __( '%s ago', 'ajax-loading' ), human_time_diff( $timestamp, time() ) ); } /** * Human labels of navigation error codes. * * @return array code => label */ public function error_code_labels() { return array( 'network' => __( 'Network error', 'ajax-loading' ), 'timeout' => __( 'Request timed out', 'ajax-loading' ), 'http_status' => __( 'Server returned an error status', 'ajax-loading' ), 'invalid_content_type' => __( 'Response was not an HTML page', 'ajax-loading' ), 'parse_error' => __( 'Page could not be parsed', 'ajax-loading' ), 'marker_missing' => __( 'Destination page is not served by AJAX Loading (cached copy or plugin inactive there)', 'ajax-loading' ), 'bypass' => __( 'Destination page is excluded', 'ajax-loading' ), 'container_mismatch' => __( 'Content area not found on both pages', 'ajax-loading' ), 'layout_mismatch' => __( 'Pages use different layouts', 'ajax-loading' ), 'cross_origin_redirect' => __( 'Redirected to another site', 'ajax-loading' ), 'excluded_redirect' => __( 'Redirected to an excluded URL', 'ajax-loading' ), 'integration_excluded' => __( 'Page uses an integration set to reload', 'ajax-loading' ), 'interactive_content' => __( 'Page contains interactive blocks', 'ajax-loading' ), 'exception' => __( 'Unexpected script error', 'ajax-loading' ), 'circuit_open' => __( 'Automatic fallback active for this visitor', 'ajax-loading' ), 'prevented' => __( 'Cancelled by a site script', 'ajax-loading' ), 'device_disabled' => __( 'Disabled for this device type', 'ajax-loading' ), 'language_changed' => __( 'Page is in another language', 'ajax-loading' ), 'asset_changed' => __( 'Tracked assets changed', 'ajax-loading' ), 'personalized' => __( 'Personalised page', 'ajax-loading' ), 'unsupported_charset' => __( 'Unsupported character set', 'ajax-loading' ), ); } /** * Human label of an error code. * * @param string $code Error code. * @return string */ public function error_code_label( $code ) { $labels = $this->error_code_labels(); return isset( $labels[ $code ] ) ? $labels[ $code ] : (string) $code; } /** * Status pill markup (escaped). * * @param string $status ok|info|warning|error|neutral. * @param string $label Text. * @return string */ public static function pill( $status, $label ) { $status = in_array( $status, array( 'ok', 'info', 'warning', 'error', 'neutral' ), true ) ? $status : 'neutral'; return sprintf( '%2$s', esc_attr( $status ), esc_html( $label ) ); } /** * Human label of a diagnostic status. * * @param string $status Status. * @return string */ public static function status_label( $status ) { $labels = array( 'ok' => __( 'OK', 'ajax-loading' ), 'info' => __( 'Info', 'ajax-loading' ), 'warning' => __( 'Warning', 'ajax-loading' ), 'error' => __( 'Problem', 'ajax-loading' ), ); return isset( $labels[ $status ] ) ? $labels[ $status ] : $labels['info']; } /** * Call a method of another plugin module whether it is static or an instance method. * * Keeps the dashboard working (with the fallback) if a module is missing or throws. * * @param string $class Class name. * @param string $method Method name. * @param array $args Arguments. * @param mixed $fallback Returned on any failure. * @return mixed */ public static function invoke( $class, $method, array $args = array(), $fallback = null ) { if ( ! class_exists( $class ) || ! method_exists( $class, $method ) ) { return $fallback; } try { $ref = new ReflectionMethod( $class, $method ); if ( ! $ref->isPublic() ) { return $fallback; } if ( $ref->isStatic() ) { return call_user_func_array( array( $class, $method ), $args ); } if ( method_exists( $class, 'instance' ) ) { $instance = call_user_func( array( $class, 'instance' ) ); } else { $class_ref = new ReflectionClass( $class ); if ( ! $class_ref->isInstantiable() ) { return $fallback; } $ctor = $class_ref->getConstructor(); $required = $ctor ? $ctor->getNumberOfRequiredParameters() : 0; if ( 0 === $required ) { $instance = new $class(); } elseif ( 1 === $required && class_exists( 'WPAJXL_Settings' ) ) { $instance = new $class( WPAJXL_Settings::instance() ); } else { return $fallback; } } return is_object( $instance ) ? call_user_func_array( array( $instance, $method ), $args ) : $fallback; } catch ( Throwable $e ) { self::debug_log( $class . '::' . $method . ' failed: ' . $e->getMessage() ); return $fallback; } } /** * Log a message when WP_DEBUG is on. * * @param string $message Message. * @return void */ protected static function debug_log( $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( '[AJAX Loading] ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug only. } } /* * --------------------------------------------------------------------- * Quick actions * --------------------------------------------------------------------- */ /** * Run a quick action and return the notice code. * * @param string $do Action. * @return string */ protected function run_action( $do ) { // Nonce verified in handle_action(). // phpcs:disable WordPress.Security.NonceVerification.Missing $settings = $this->settings(); switch ( $do ) { case 'toggle_enable': $enable = ( 'on' !== $this->value( 'enable_plugin', 'off' ) ); $settings->update( array( 'enable_plugin' => $enable ? 'on' : 'off' ) ); return $enable ? 'enabled' : 'disabled'; case 'toggle_safe_mode': $on = ! $this->flag( 'safe_mode' ); $settings->update( array( 'safe_mode' => $on ) ); return $on ? 'safe_mode_on' : 'safe_mode_off'; case 'toggle_visibility': $admins = ( 'admins' !== $this->value( 'visibility', 'everyone' ) ); $settings->update( array( 'visibility' => $admins ? 'admins' : 'everyone' ) ); return $admins ? 'visibility_admins' : 'visibility_everyone'; case 'pause': $duration = isset( $_POST['duration'] ) ? absint( wp_unslash( $_POST['duration'] ) ) : 3600; $duration = in_array( $duration, array( HOUR_IN_SECONDS, DAY_IN_SECONDS ), true ) ? $duration : HOUR_IN_SECONDS; $settings->pause( $duration ); return 'paused'; case 'resume': $settings->resume(); return 'resumed'; case 'clear_cache': $settings->bump_cache_version(); return 'cache_cleared'; case 'reset': if ( empty( $_POST['ajxl_confirm_reset'] ) ) { return 'reset_unconfirmed'; } $settings->reset(); return 'reset'; case 'import': return $this->handle_import(); case 'clear_log': self::invoke( 'WPAJXL_Logger', 'clear' ); return 'log_cleared'; } // phpcs:enable return 'action_failed'; } /** * Validate the uploaded settings file and import it. * * @return string Notice code. */ protected function handle_import() { // Nonce verified in handle_action(); every part of the upload is validated below. // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( empty( $_FILES['ajxl_import_file'] ) || ! is_array( $_FILES['ajxl_import_file'] ) ) { return 'import_no_file'; } $file = $_FILES['ajxl_import_file']; $error = isset( $file['error'] ) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE; // phpcs:enable if ( UPLOAD_ERR_NO_FILE === $error ) { return 'import_no_file'; } if ( UPLOAD_ERR_INI_SIZE === $error || UPLOAD_ERR_FORM_SIZE === $error ) { return 'import_too_large'; } if ( UPLOAD_ERR_OK !== $error || empty( $file['tmp_name'] ) || ! is_string( $file['tmp_name'] ) ) { return 'import_failed'; } $size = isset( $file['size'] ) ? (int) $file['size'] : 0; if ( $size > self::IMPORT_MAX_BYTES ) { return 'import_too_large'; } if ( $size <= 0 ) { return 'import_invalid'; } $name = isset( $file['name'] ) && is_string( $file['name'] ) ? sanitize_file_name( $file['name'] ) : ''; if ( 'json' !== strtolower( (string) pathinfo( $name, PATHINFO_EXTENSION ) ) ) { return 'import_wrong_type'; } if ( ! is_uploaded_file( $file['tmp_name'] ) ) { return 'import_failed'; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading PHP's own upload temp file. $raw = file_get_contents( $file['tmp_name'], false, null, 0, self::IMPORT_MAX_BYTES + 1 ); if ( false === $raw ) { return 'import_failed'; } if ( strlen( $raw ) > self::IMPORT_MAX_BYTES ) { return 'import_too_large'; } if ( 0 === strpos( $raw, "\xEF\xBB\xBF" ) ) { $raw = substr( $raw, 3 ); } $data = json_decode( $raw, true ); if ( ! is_array( $data ) ) { return 'import_invalid'; } $result = $this->settings()->import( $data ); if ( is_wp_error( $result ) ) { set_transient( $this->import_error_key(), $result->get_error_message(), 5 * MINUTE_IN_SECONDS ); return 'import_invalid'; } return $result ? 'imported' : 'import_invalid'; } /** * Stream the settings export as a JSON download and exit. * * @param string $tab Tab to return to on failure. * @return void */ protected function send_export( $tab ) { $data = $this->settings()->export(); $json = is_array( $data ) ? wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) : false; if ( false === $json ) { $this->redirect_after_action( $tab, 'action_failed', 'page' ); } $host = wp_parse_url( home_url(), PHP_URL_HOST ); $file = sanitize_file_name( 'ajax-loading-settings-' . ( is_string( $host ) ? $host . '-' : '' ) . gmdate( 'Y-m-d' ) . '.json' ); nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $file . '"' ); header( 'X-Content-Type-Options: nosniff' ); header( 'Content-Length: ' . strlen( $json ) ); echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON file download, not HTML. exit; } /** * Redirect back after a quick action. * * @param string $tab Tab slug. * @param string $notice Notice code. * @param string $return 'page' or 'plugins'. * @return void */ protected function redirect_after_action( $tab, $notice, $return ) { $tabs = $this->tabs(); if ( 'plugins' === $return ) { $url = add_query_arg( 'ajxl_notice', $notice, admin_url( 'plugins.php' ) ); } else { $url = $this->tab_url( isset( $tabs[ $tab ] ) ? $tab : 'overview', array( 'ajxl_notice' => $notice ) ); } wp_safe_redirect( $url ); exit; } /** * Transient key for the last import error of the current user. * * @return string */ protected function import_error_key() { return 'ajxl_import_error_' . get_current_user_id(); } /* * --------------------------------------------------------------------- * Notices * --------------------------------------------------------------------- */ /** * Whitelisted notice code from the query string. * * @return string */ protected function notice_code() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only, whitelisted. $code = isset( $_GET['ajxl_notice'] ) ? sanitize_key( wp_unslash( $_GET['ajxl_notice'] ) ) : ''; $messages = $this->notice_messages(); return isset( $messages[ $code ] ) ? $code : ''; } /** * Notice code => [ type, message ]. * * @return array */ protected function notice_messages() { return array( 'enabled' => array( 'success', __( 'AJAX navigation is now enabled.', 'ajax-loading' ) ), 'disabled' => array( 'warning', __( 'AJAX navigation is now disabled. Every page loads normally.', 'ajax-loading' ) ), 'safe_mode_on' => array( 'success', __( 'Safe mode is on.', 'ajax-loading' ) ), 'safe_mode_off' => array( 'success', __( 'Safe mode is off.', 'ajax-loading' ) ), 'visibility_admins' => array( 'success', __( 'Test mode is on: only administrators get AJAX navigation.', 'ajax-loading' ) ), 'visibility_everyone' => array( 'success', __( 'AJAX navigation is now active for all visitors.', 'ajax-loading' ) ), 'paused' => array( 'success', __( 'AJAX navigation is paused.', 'ajax-loading' ) ), 'resumed' => array( 'success', __( 'AJAX navigation has resumed.', 'ajax-loading' ) ), 'cache_cleared' => array( 'success', __( 'Navigation cache cleared. Visitors\' browsers discard their cached pages the next time they load a page from your server, or when the cache lifetime ends.', 'ajax-loading' ) ), 'reset' => array( 'success', __( 'All settings were restored to their defaults.', 'ajax-loading' ) ), 'reset_unconfirmed' => array( 'error', __( 'Please confirm that you want to restore the default settings.', 'ajax-loading' ) ), 'imported' => array( 'success', __( 'Settings imported.', 'ajax-loading' ) ), 'import_no_file' => array( 'error', __( 'Choose a settings file to import.', 'ajax-loading' ) ), 'import_too_large' => array( 'error', __( 'The settings file is larger than 100 KB.', 'ajax-loading' ) ), 'import_wrong_type' => array( 'error', __( 'Please choose a .json file exported by AJAX Loading.', 'ajax-loading' ) ), 'import_failed' => array( 'error', __( 'The file could not be uploaded. Please try again.', 'ajax-loading' ) ), 'import_invalid' => array( 'error', __( 'This file is not a valid AJAX Loading settings export. Nothing was changed.', 'ajax-loading' ) ), 'log_cleared' => array( 'success', __( 'Recent errors cleared.', 'ajax-loading' ) ), 'action_failed' => array( 'error', __( 'That action could not be completed.', 'ajax-loading' ) ), ); } /** * Print the result notice of a quick action. * * @param string $code Notice code. * @return void */ protected function print_result_notice( $code ) { $messages = $this->notice_messages(); list( $type, $message ) = $messages[ $code ]; if ( 'paused' === $code && $this->settings()->is_paused() ) { $message = sprintf( /* translators: %s: date and time. */ __( 'AJAX navigation is paused until %s. Pages load normally until then.', 'ajax-loading' ), $this->format_time( (int) $this->value( 'pause_until', 0 ) ) ); } if ( in_array( $code, self::PAGE_CACHE_NOTICE_CODES, true ) && class_exists( 'WPAJXL_Compat' ) && method_exists( 'WPAJXL_Compat', 'has_page_cache' ) && WPAJXL_Compat::has_page_cache() ) { $message .= ' ' . self::page_cache_hint(); } $detail = ''; if ( 'import_invalid' === $code ) { $stored = get_transient( $this->import_error_key() ); if ( is_string( $stored ) && '' !== $stored ) { $detail = $stored; delete_transient( $this->import_error_key() ); } } printf( '

%2$s%3$s

', esc_attr( $type ), esc_html( $message ), '' !== $detail ? ' ' . esc_html( $detail ) . '' : '' ); } /** * Persistent notices for paused / safe mode / administrators-only states. * * @param bool $own On the plugin's own screen. * @param string $code Result notice already shown (to avoid repeating it). * @return void */ protected function print_state_notices( $own, $code ) { if ( 'on' !== $this->value( 'enable_plugin', 'off' ) ) { return; } $return = $own ? 'page' : 'plugins'; $tab = $own ? $this->current_tab() : 'overview'; if ( $this->settings()->is_paused() && 'paused' !== $code ) { $this->print_state_notice( 'warning', sprintf( /* translators: %s: date and time. */ __( 'AJAX Loading is paused until %s. Visitors get normal page loads until then.', 'ajax-loading' ), $this->format_time( (int) $this->value( 'pause_until', 0 ) ) ), 'resume', __( 'Resume now', 'ajax-loading' ), $tab, $return ); } if ( $this->flag( 'safe_mode' ) && 'safe_mode_on' !== $code ) { $this->print_state_notice( 'info', __( 'AJAX Loading safe mode is on: prefetching, caching and page builder support are limited while you troubleshoot.', 'ajax-loading' ), 'toggle_safe_mode', __( 'Turn off safe mode', 'ajax-loading' ), $tab, $return ); } if ( 'admins' === $this->value( 'visibility', 'everyone' ) && 'visibility_admins' !== $code ) { $this->print_state_notice( 'info', __( 'AJAX Loading is in test mode: only administrators get AJAX navigation.', 'ajax-loading' ), 'toggle_visibility', __( 'Enable for everyone', 'ajax-loading' ), $tab, $return ); } } /** * One state notice with an inline action form. * * @param string $type notice type. * @param string $message Message. * @param string $do Quick action. * @param string $button Button label. * @param string $tab Tab to return to. * @param string $return 'page' or 'plugins'. * @return void */ protected function print_state_notice( $type, $message, $do, $button, $tab, $return ) { printf( '

%2$s

', esc_attr( $type ), esc_html( $message ) ); printf( '

', esc_url( admin_url( 'admin-post.php' ) ) ); $this->action_fields( $do, $tab, $return ); printf( '', esc_html( $button ) ); if ( 'plugins' === $return ) { printf( ' %2$s', esc_url( $this->page_url() ), esc_html__( 'Open AJAX Loading settings', 'ajax-loading' ) ); } echo '

'; } /* * --------------------------------------------------------------------- * Assets * --------------------------------------------------------------------- */ /** * Plugin basename (works when the folder is renamed). * * @return string */ protected function plugin_basename() { if ( defined( 'WPAJXL_PLUGIN_BASENAME' ) && WPAJXL_PLUGIN_BASENAME ) { return WPAJXL_PLUGIN_BASENAME; } return plugin_basename( dirname( __DIR__ ) . '/ajax-loading.php' ); } /** * Plugin root URL with trailing slash. * * @return string */ public function plugin_url() { if ( defined( 'WPAJXL_PLUGIN_URL' ) && WPAJXL_PLUGIN_URL ) { return trailingslashit( WPAJXL_PLUGIN_URL ); } return plugin_dir_url( __DIR__ ); } /** * Plugin root path with trailing slash. * * @return string */ protected function plugin_path() { return trailingslashit( dirname( __DIR__ ) ); } /** * Relative path of a built asset (.min unless SCRIPT_DEBUG; falls back to the unminified build). * * @param string $name Base name (admin|frontend). * @param string $ext js|css. * @return string */ protected function asset_path( $name, $ext ) { $plain = 'assets/dist/' . $name . '.' . $ext; $min = 'assets/dist/' . $name . '.min.' . $ext; if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { return file_exists( $this->plugin_path() . $plain ) ? $plain : $min; } return file_exists( $this->plugin_path() . $min ) ? $min : $plain; } /** * URL of a built asset. * * @param string $name Base name. * @param string $ext Extension. * @return string */ protected function asset_url( $name, $ext ) { return $this->plugin_url() . $this->asset_path( $name, $ext ); } /** * Asset version (plugin version; file time added while SCRIPT_DEBUG is on). * * @param string $name Base name. * @param string $ext Extension. * @return string */ protected function asset_version( $name, $ext ) { if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { $mtime = @filemtime( $this->plugin_path() . $this->asset_path( $name, $ext ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- missing build file is not an error. if ( $mtime ) { return $this->version . '.' . $mtime; } } return $this->version; } /** * Front-end config (§5) used by the preview and diagnostics. * * @return array */ protected function frontend_config() { $config = self::invoke( 'WPAJXL_Frontend', 'get_config', array(), array() ); return is_array( $config ) ? $config : array(); } /** * Data for window.ajaxLoadingAdmin. * * @param string $tab Current tab. * @return array */ protected function script_data( $tab ) { $schema = $this->schema(); return array( 'tab' => $tab, 'restUrl' => esc_url_raw( rest_url( 'ajax-loading/v1/' ) ), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'config' => $this->frontend_config(), 'types' => WPAJXL_Admin_Fields::types( $schema ), 'sampleUrls' => 'tools' === $tab ? $this->sample_urls() : array(), 'logging' => $this->flag( 'error_logging' ), 'visibility' => (string) $this->value( 'visibility', 'everyone' ), 'protect' => array( 'woocommerce' => $this->flag( 'exclude_woocommerce' ), 'membership' => $this->flag( 'exclude_membership' ), ), 'importMax' => self::IMPORT_MAX_BYTES, 'errorCodes' => $this->error_code_labels(), 'i18n' => $this->script_i18n(), ); } /** * Translated strings for the admin script. * * @return array */ protected function script_i18n() { return array( // Generic. 'statusOk' => self::status_label( 'ok' ), 'statusInfo' => self::status_label( 'info' ), 'statusWarning' => self::status_label( 'warning' ), 'statusError' => self::status_label( 'error' ), 'loading' => __( 'Loading…', 'ajax-loading' ), /* translators: %s: error message. */ 'requestFailed' => __( 'The request failed: %s', 'ajax-loading' ), 'unsavedLeave' => __( 'You have unsaved changes on this tab. Leave without saving?', 'ajax-loading' ), 'copied' => __( 'Copied', 'ajax-loading' ), 'copyFailed' => __( 'Press Ctrl+C (or Cmd+C) to copy.', 'ajax-loading' ), 'importTooLarge' => __( 'The settings file is larger than 100 KB.', 'ajax-loading' ), 'importWrongType' => __( 'Please choose a .json file exported by AJAX Loading.', 'ajax-loading' ), 'importNoFile' => __( 'Choose a settings file to import.', 'ajax-loading' ), // Media picker. 'mediaTitle' => __( 'Choose a logo for the loading indicator', 'ajax-loading' ), 'mediaButton' => __( 'Use this image', 'ajax-loading' ), 'mediaNone' => __( 'No image selected', 'ajax-loading' ), 'mediaSelected' => __( 'Logo image selected.', 'ajax-loading' ), // Preview. 'previewPlaying' => __( 'Playing preview…', 'ajax-loading' ), 'previewDone' => __( 'Preview finished.', 'ajax-loading' ), /* translators: %s: error message. */ 'previewFailed' => __( 'The preview could not run: %s', 'ajax-loading' ), 'reducedMotionOn' => __( 'Your device asks for reduced motion and "Respect reduced motion" is on, so animations are skipped here, exactly as for visitors with this preference.', 'ajax-loading' ), 'reducedMotionOff' => __( 'Your device asks for reduced motion, but "Respect reduced motion" is off: visitors with this preference will still see animations.', 'ajax-loading' ), // Diagnostics. 'running' => __( 'Running checks…', 'ajax-loading' ), /* translators: 1: start page path, 2: destination page path. */ 'testTitle' => __( 'Navigation test: %1$s → %2$s', 'ajax-loading' ), 'selectorsTitle' => __( 'Content selectors on sample pages', 'ajax-loading' ), 'invalidUrl' => __( 'Enter a full URL of a page on this site.', 'ajax-loading' ), /* translators: %s: site origin (scheme and host). */ 'notSameOrigin' => __( 'Only pages on this site (%s) can be tested.', 'ajax-loading' ), /* translators: 1: HTTP status code, 2: size in KB, 3: duration in milliseconds. */ 'fetchOk' => __( 'HTTP %1$s, %2$s KB in %3$s ms', 'ajax-loading' ), /* translators: %s: error message. */ 'fetchFailed' => __( 'Could not load the page: %s', 'ajax-loading' ), /* translators: %s: HTTP status code. */ 'fetchStatus' => __( 'The server answered with HTTP %s.', 'ajax-loading' ), /* translators: %s: content type. */ 'fetchType' => __( 'The response is not an HTML page (%s).', 'ajax-loading' ), /* translators: %s: URL path. */ 'fetchRedirect' => __( 'Redirected to %s.', 'ajax-loading' ), 'fetchTimeout' => __( 'The request timed out.', 'ajax-loading' ), /* translators: %s: URL path. */ 'labelFetch' => __( 'Load %s', 'ajax-loading' ), /* translators: %s: URL path. */ 'labelMarker' => __( 'Plugin marker on %s', 'ajax-loading' ), 'markerEnabled' => __( 'AJAX Loading is active on this page.', 'ajax-loading' ), 'markerBypass' => __( 'This page is excluded on the server (for example cart, checkout, login or an excluded post type). Links to it load normally.', 'ajax-loading' ), 'markerMissing' => __( 'The marker tag is missing: the plugin is off, paused, in test mode for other users, or a page cache serves a copy from before activation. Navigation to this page falls back to a normal load.', 'ajax-loading' ), /* translators: 1: plugin version found in the page, 2: installed plugin version. */ 'markerVersion' => __( 'The page was rendered by AJAX Loading %1$s but this site runs %2$s. Clear your page cache.', 'ajax-loading' ), 'labelLinkRule' => __( 'Link rules for the destination', 'ajax-loading' ), 'linkOk' => __( 'A link to this URL is handled with AJAX.', 'ajax-loading' ), /* translators: %s: technical reason code. */ 'linkSkipped' => __( 'A link to this URL loads normally (%s).', 'ajax-loading' ), 'labelPrefetch' => __( 'Prefetch for visitors', 'ajax-loading' ), 'prefetchOk' => __( 'Visitors may prefetch this URL.', 'ajax-loading' ), /* translators: %s: technical reason code. */ 'prefetchSkipped' => __( 'Visitors never prefetch this URL (%s).', 'ajax-loading' ), 'prefetchOff' => __( 'Prefetching is turned off.', 'ajax-loading' ), 'labelDetect' => __( 'Content to replace', 'ajax-loading' ), /* translators: 1: detection mode, 2: list of regions. */ 'detectOk' => __( 'Mode "%1$s": %2$s', 'ajax-loading' ), /* translators: 1: error code, 2: explanation. */ 'detectFailed' => __( 'No matching content area (%1$s): %2$s', 'ajax-loading' ), /* translators: %s: CSS selector. */ 'regionContent' => __( 'content %s', 'ajax-loading' ), /* translators: %s: CSS selector. */ 'regionHeader' => __( 'header %s', 'ajax-loading' ), /* translators: %s: CSS selector. */ 'regionFooter' => __( 'footer %s', 'ajax-loading' ), 'regionBody' => __( 'whole body', 'ajax-loading' ), 'labelHead' => __( 'Head tags that will be updated', 'ajax-loading' ), /* translators: 1: page title, 2: number of tags, 3: list of tag names. */ 'headSummary' => __( 'Title "%1$s"; %2$s tag(s) replaced: %3$s', 'ajax-loading' ), /* translators: %s: page title. */ 'headTitleOnly' => __( 'Title "%s"; head tag syncing is off or there is nothing to sync.', 'ajax-loading' ), 'labelScripts' => __( 'Scripts and styles', 'ajax-loading' ), /* translators: 1: number of scripts, 2: number of scripts, 3: number of stylesheets. */ 'scriptsSummary' => __( '%1$s script(s) inside the new content, %2$s new external script(s), %3$s new stylesheet(s).', 'ajax-loading' ), 'scriptsNoExec' => __( 'Scripts inside the new content are not executed ("Run scripts inside new content" is off).', 'ajax-loading' ), 'documentWrite' => __( 'An inline script uses document.write and will be skipped after AJAX navigation.', 'ajax-loading' ), 'labelLinks' => __( 'Links on the start page', 'ajax-loading' ), /* translators: 1: number of links, 2: total number of links, 3: list of reasons. */ 'linksSummary' => __( '%1$s of %2$s links load with AJAX. Others: %3$s', 'ajax-loading' ), 'linksNone' => __( 'No links found on the start page.', 'ajax-loading' ), 'labelWarnings' => __( 'Things that force a normal page load', 'ajax-loading' ), 'warnInteractive' => __( 'The destination content uses Interactivity API blocks, so it loads normally ("Interactive blocks" is set to reload).', 'ajax-loading' ), 'warnInteractiveSwap' => __( 'The destination content uses Interactivity API blocks; they are swapped with AJAX and may not work.', 'ajax-loading' ), 'warnRouter' => __( 'The start page loads the WordPress Interactivity router, so AJAX Loading stays inactive on it.', 'ajax-loading' ), /* translators: 1: language code of the start page, 2: language code of the destination. */ 'warnLanguage' => __( 'The pages are in different languages (%1$s → %2$s), so this navigation reloads the page.', 'ajax-loading' ), /* translators: %s: list of CSS class names. */ 'warnBodyClass' => __( 'The destination is a protected page (%s), so it loads normally.', 'ajax-loading' ), /* translators: %s: integration key. */ 'warnIntegrationExcluded' => __( 'The destination contains content of the "%s" integration, which is set to "Exclude (full page load)", so it loads normally.', 'ajax-loading' ), 'warnTracked' => __( 'Assets marked with data-ajax-loading-track="reload" differ between the pages, so this navigation reloads.', 'ajax-loading' ), 'warnPersonalized' => __( 'The destination is personalised for you (logged in, cart or comment cookies). It is never prefetched or cached.', 'ajax-loading' ), 'warnVisibility' => __( 'Test mode is on: visitors do not get AJAX navigation yet.', 'ajax-loading' ), 'noWarnings' => __( 'Nothing forces a normal page load.', 'ajax-loading' ), 'testPassed' => __( 'This navigation will use AJAX.', 'ajax-loading' ), 'testFailed' => __( 'This navigation will fall back to a normal page load.', 'ajax-loading' ), 'asVisitorNote' => __( 'Pages are fetched with your logged-in session; visitors may see slightly different markup.', 'ajax-loading' ), 'colPage' => __( 'Page', 'ajax-loading' ), 'colContent' => __( 'Content area', 'ajax-loading' ), 'colHeader' => __( 'Header', 'ajax-loading' ), 'colFooter' => __( 'Footer', 'ajax-loading' ), 'colMarker' => __( 'Marker', 'ajax-loading' ), /* translators: %s: page name. */ 'colFromFirst' => __( 'Navigation from %s', 'ajax-loading' ), 'notFound' => __( 'Not found', 'ajax-loading' ), 'markerShortEnabled' => __( 'Active', 'ajax-loading' ), 'markerShortBypass' => __( 'Excluded', 'ajax-loading' ), 'markerShortMissing' => __( 'Missing', 'ajax-loading' ), /* translators: 1: CSS selector, 2: number of matches. */ 'selectorsCustom' => __( 'Custom selector %1$s: %2$s', 'ajax-loading' ), /* translators: %s: number of matches. */ 'matchCount' => __( '%s match(es)', 'ajax-loading' ), 'sameAsFirst' => __( 'Start page', 'ajax-loading' ), 'noSamples' => __( 'No sample pages are available.', 'ajax-loading' ), 'saved' => __( 'Results saved. They appear on the Overview tab.', 'ajax-loading' ), /* translators: %s: error message. */ 'saveFailed' => __( 'The results could not be saved: %s', 'ajax-loading' ), // Environment and log. 'envEmpty' => __( 'No environment information is available.', 'ajax-loading' ), 'envRefreshed' => __( 'Environment report refreshed.', 'ajax-loading' ), /* translators: 1: date and time, 2: result summary. */ 'lastClientRun' => __( 'Last compatibility check: %1$s (%2$s)', 'ajax-loading' ), 'clientOk' => __( 'no problems found', 'ajax-loading' ), 'clientIssues' => __( 'issues found', 'ajax-loading' ), 'logEmpty' => __( 'No navigation errors recorded.', 'ajax-loading' ), 'logRefreshed' => __( 'Recent errors refreshed.', 'ajax-loading' ), 'logCleared' => __( 'Recent errors cleared.', 'ajax-loading' ), 'confirmClearLog' => __( 'Delete all recorded navigation errors?', 'ajax-loading' ), ); } } if ( ! class_exists( 'WP_ajax_loading_Admin', false ) ) { class_alias( 'WPAJXL_Admin', 'WP_ajax_loading_Admin' ); }