YoPingMe. * * The page has two faces. Until the plugin is set up it is a short wizard: * connect an account, then choose the badge and start watching this site. * After that it is an ordinary settings page. Which one you get is derived * from state on every render rather than from a stored step counter, so a * half-finished setup cannot strand anyone on a screen that no longer * matches reality. */ class Yopingme_Settings { const PAGE = 'yopingme'; /** * Hook everything up. */ public static function init() { add_action( 'admin_menu', array( __CLASS__, 'add_page' ) ); add_action( 'admin_init', array( __CLASS__, 'register' ) ); add_action( 'admin_post_yopingme_remove_key', array( __CLASS__, 'handle_remove_key' ) ); add_action( 'admin_post_yopingme_finish_setup', array( __CLASS__, 'handle_finish_setup' ) ); add_action( 'wp_ajax_yopingme_test', array( __CLASS__, 'handle_test' ) ); add_action( 'admin_post_yopingme_add_site', array( __CLASS__, 'handle_add_site' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_css' ) ); add_filter( 'plugin_action_links_' . plugin_basename( YOPINGME_PLUGIN_FILE ), array( __CLASS__, 'action_links' ) ); } /** * A few rules for the page, inline so the plugin ships no CSS file. * The test-connection button's JS is a real enqueued asset instead, * since only that needs a script tag. * * @param string $hook Current admin screen. */ public static function enqueue_css( $hook ) { if ( 'settings_page_' . self::PAGE !== $hook ) { return; } wp_add_inline_style( 'common', '.yopingme-wrap .card{max-width:640px;margin-top:16px} .yopingme-links{margin:4px 0 0} .yopingme-step{color:#646970;text-transform:uppercase;font-size:11px;letter-spacing:.06em;margin:0 0 4px} .yopingme-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px} .yopingme-wrap h2{margin-top:0} .yopingme-sub{margin-top:24px}' ); if ( '' === (string) get_option( 'yopingme_api_key', '' ) ) { return; } wp_enqueue_script( 'yopingme-settings-test', plugins_url( 'assets/js/settings-test.js', YOPINGME_PLUGIN_FILE ), array(), YOPINGME_VERSION, true ); wp_localize_script( 'yopingme-settings-test', 'yopingmeSettings', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'yopingme_test' ), 'testing' => __( 'Testing...', 'yopingme' ), 'failed' => __( 'Could not run the test.', 'yopingme' ), ) ); } /** * @param array $links Existing plugin row links. * @return array */ public static function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'yopingme' ) . '' ); return $links; } /** * @return string */ public static function url() { return admin_url( 'options-general.php?page=' . self::PAGE ); } /** * Add the options page. */ public static function add_page() { add_options_page( __( 'YoPingMe', 'yopingme' ), __( 'YoPingMe', 'yopingme' ), 'manage_options', self::PAGE, array( __CLASS__, 'render' ) ); } /** * Register settings, section and fields. */ public static function register() { register_setting( 'yopingme', 'yopingme_api_key', array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_key' ), 'default' => '', 'show_in_rest' => false, ) ); register_setting( 'yopingme', 'yopingme_show_badge', array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_badge' ), 'default' => '', 'show_in_rest' => false, ) ); register_setting( 'yopingme', 'yopingme_downtime_notice', array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_badge' ), 'default' => '1', 'show_in_rest' => false, ) ); register_setting( 'yopingme', 'yopingme_status_page', array( 'type' => 'array', 'sanitize_callback' => array( 'Yopingme_Settings_Status', 'sanitize' ), 'default' => array(), 'show_in_rest' => false, ) ); add_settings_section( 'yopingme_main', '', '__return_false', self::PAGE ); add_settings_field( 'yopingme_api_key', __( 'API key', 'yopingme' ), array( __CLASS__, 'field_api_key' ), self::PAGE, 'yopingme_main' ); add_settings_field( 'yopingme_downtime_notice', __( 'Downtime notice', 'yopingme' ), array( __CLASS__, 'field_downtime_notice' ), self::PAGE, 'yopingme_main' ); add_settings_field( 'yopingme_show_badge', __( 'Footer badge', 'yopingme' ), array( __CLASS__, 'field_badge' ), self::PAGE, 'yopingme_main' ); add_settings_field( 'yopingme_status_page', __( 'Status page', 'yopingme' ), array( __CLASS__, 'field_status_page' ), self::PAGE, 'yopingme_main' ); add_settings_field( 'yopingme_this_site', __( 'This site', 'yopingme' ), array( __CLASS__, 'field_this_site' ), self::PAGE, 'yopingme_main' ); } /** * This site's monitoring state: its status once watched, otherwise the * offer to start watching it. * * @param array|null $state Optional pre-fetched cache state. */ public static function field_this_site() { $url = home_url( '/' ); $state = self::state(); // Once the site is watched there is nothing to add, so the button gives // way to what the owner actually wants to see: how it is doing. $monitor = self::find_monitor( $url, $state ); if ( $monitor ) { self::render_site_status( $monitor, $state ); return; } if ( '' === (string) get_option( 'yopingme_api_key', '' ) ) { echo '
' . esc_html__( 'Save an API key first, then you can add this site to your YoPingMe account from here.', 'yopingme' ) . '
'; return; } if ( ! Yopingme_Client::is_public_url( $url ) ) { printf( '%s
', esc_html( sprintf( /* translators: %s: the site URL. */ __( 'YoPingMe checks from the public internet, so it cannot watch %s.', 'yopingme' ), $url ) ) ); return; } // Never offer a button the key cannot honour. A read-only key is not // offered create_monitor by the server at all, so this is the branch // it lands in — say so, because it is the one cause the owner can fix // themselves. Test connection re-asks the server, so a replacement key // takes effect without waiting out the cached tool list. if ( ! Yopingme_Client::supports_tool( 'create_monitor' ) ) { printf( '%s %s
', esc_html__( 'This API key cannot add monitors. Create a key with permission to add monitors in YoPingMe, then save it here.', 'yopingme' ), esc_url( yopingme_app_url() ), esc_html__( 'Or add this site in YoPingMe', 'yopingme' ) ); return; } ?>' . esc_html( $url ) . '' ); ?>
' . esc_html__( 'YoPingMe account page', 'yopingme' ) . ''; if ( '' !== $key ) { // Only meaningful once there is a key to keep. printf( /* translators: %s: link to the YoPingMe account page. */ esc_html__( 'Create a key with the MCP scope on your %s, then paste it here. Leave the field blank to keep the key you already saved.', 'yopingme' ), $account_link // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from esc_url/esc_html above. ); } else { printf( /* translators: %s: link to the YoPingMe account page. */ esc_html__( 'Create a key with the MCP scope on your %s, then paste it here.', 'yopingme' ), $account_link // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from esc_url/esc_html above. ); } ?>
(string) $page['slug'], 'url' => isset( $page['url'] ) ? (string) $page['url'] : '', 'title' => isset( $page['title'] ) ? (string) $page['title'] : '', 'enabled' => ! empty( $page['enabled'] ), ); } // An empty answer is cached briefly, a real one for the full hour. // Opening this screen before creating the first status page is the // normal order of events, and an hour of "no pages found" after the // customer just made one reads as the feature being broken. set_transient( 'yopingme_status_pages', $pages, $pages ? HOUR_IN_SECONDS : 5 * MINUTE_IN_SECONDS ); return $pages; } /** * Status page radios: none, one per page discovered via MCP, and a * manual slug/URL fallback for accounts (or servers) discovery cannot * see. The discovered options are rendered from the cache/API read * above, never from the stored choice, so a page renamed or disabled * upstream is reflected here even before the form is saved again. */ public static function field_status_page() { $stored = get_option( 'yopingme_status_page', array() ); $stored = is_array( $stored ) ? $stored : array(); $slug = isset( $stored['slug'] ) ? (string) $stored['slug'] : ''; $source = isset( $stored['source'] ) ? (string) $stored['source'] : ''; $pages = self::discovered_status_pages(); $pages = is_array( $pages ) ? $pages : array(); $checked = '' === $slug ? 'none' : ( 'manual' === $source ? 'manual' : $slug ); // The page itself is created in YoPingMe, not here, and there is // nothing on this screen that says so - an owner who picks "Manual" // before making one just gets a strip that never appears. Say it // first, and link straight to the screen that does it. printf( '
%s %s
', esc_html__( 'Status pages are created in your YoPingMe account. Make one there first, then choose it here.', 'yopingme' ), esc_url( yopingme_app_url( 'status-pages' ) ), esc_html__( 'Set up your status page', 'yopingme' ) ); ?> ', 'success' === $notice['type'] ? 'success' : 'error', esc_html( $notice['text'] ) ); } if ( self::setup_done() ) { self::render_settings( $state ); } elseif ( 3 === self::current_step( $state ) ) { self::render_wizard_finish( $state ); } else { self::render_wizard_connect( $state ); } ?>
__( 'You are not allowed to do that.', 'yopingme' ) ), 403 ); } // Testing the connection is the one moment the owner is explicitly // asking what this key can do, so re-ask the server rather than // answering from an hour-old list. delete_transient( 'yopingme_tools' ); // Always a real request: a cached answer would report a revoked key as // connected, which is the one lie this button must never tell. $result = Yopingme_Cache::get_monitors( true, true ); if ( empty( $result['error'] ) ) { $count = isset( $result['data']['monitors'] ) ? count( $result['data']['monitors'] ) : 0; wp_send_json_success( array( 'message' => sprintf( /* translators: %d: number of monitors. */ _n( 'Connected. %d monitor found.', 'Connected. %d monitors found.', $count, 'yopingme' ), $count ), ) ); } wp_send_json_success( array( 'message' => Yopingme_Widget::error_message( $result['error'] ) ) ); } /** * Add this site as a monitor. Always an explicit click - never on * activation, because creating something in someone's account is not a * side effect anyone should discover after the fact. */ public static function handle_add_site() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'yopingme' ) ); } check_admin_referer( 'yopingme_add_site' ); $url = home_url( '/' ); if ( ! Yopingme_Client::is_public_url( $url ) ) { set_transient( 'yopingme_add_site_notice', array( 'type' => 'error', 'text' => sprintf( /* translators: %s: the site URL. */ __( 'YoPingMe checks from the public internet, so it cannot watch %s. Add this site once it has a public address.', 'yopingme' ), $url ), ), 60 ); wp_safe_redirect( self::url() ); exit; } // Nothing stops the API creating a second monitor for the same URL, and // a double click would quietly spend one of the plan's slots. The // cached list is enough to catch the ordinary case. if ( null !== self::find_monitor( $url ) ) { set_transient( 'yopingme_add_site_notice', array( 'type' => 'success', 'text' => sprintf( /* translators: %s: the site URL. */ __( 'YoPingMe is already watching %s.', 'yopingme' ), $url ), ), 60 ); wp_safe_redirect( self::url() ); exit; } $result = Yopingme_Client::call_tool( 'create_monitor', array( 'url' => $url, 'name' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), ) ); if ( ! empty( $result['ok'] ) ) { // The new monitor will not be in the cached list yet. Yopingme_Cache::flush(); $notice = array( 'type' => 'success', 'text' => sprintf( /* translators: %s: the site URL. */ __( 'YoPingMe is now watching %s. It appears on your dashboard once the first check runs.', 'yopingme' ), $url ), ); } else { $notice = array( 'type' => 'error', 'text' => self::add_site_error( $result['error'] ), ); } set_transient( 'yopingme_add_site_notice', $notice, 60 ); wp_safe_redirect( self::url() ); exit; } /** * The monitor watching this URL, if there is one. * * Compares hosts rather than whole URLs: a site whose home_url gains or * loses a trailing slash, or moves between http and https, is still the * same site to watch. * * @param string $url Site URL. * @param array|null $state Pre-fetched cache state, so a page that already * has one does not ask for it twice. * @return array|null The monitor, or null when this site is not watched. */ private static function find_monitor( $url, $state = null ) { if ( ! is_array( $state ) ) { $state = Yopingme_Cache::get_monitors(); } return Yopingme_Cache::monitor_for( $state, $url ); } /** * How this site is doing, for the owner who has already added it. * * Reuses the widget's palette and formatting so one monitor never looks * like two different things in two places on the same screen. * * @param array $monitor The monitor watching this site. * @param array|null $state Cache state, for the "as of" stamp. */ private static function render_site_status( $monitor, $state = null ) { if ( ! is_array( $state ) ) { $state = Yopingme_Cache::get_monitors(); } $paused = ! empty( $monitor['paused'] ); $status = $paused ? 'paused' : ( isset( $monitor['current_status'] ) ? (string) $monitor['current_status'] : 'unknown' ); ?>
—