PingPush page. * * @package PingPush */ defined( 'ABSPATH' ) || exit; /** * Settings page. */ class PingPush_Settings { const PAGE = 'pingpush'; const NONCE = 'pingpush_admin'; /** * Cached settings. * * @var array|null */ private static $cache = null; /* ---------- Storage ---------- */ /** * Defaults. * * @return array */ public static function defaults() { return array( 'token' => '', 'user_key' => '', 'default_priority' => 0, 'include_site_name' => 1, 'log_enabled' => 1, 'log_size' => 100, 'events' => array(), 'custom_hooks' => array(), ); } /** * Current settings merged with defaults. * * @return array */ public static function get() { if ( null === self::$cache ) { $saved = get_option( PINGPUSH_OPTION, array() ); self::$cache = array_merge( self::defaults(), is_array( $saved ) ? $saved : array() ); add_action( 'update_option_' . PINGPUSH_OPTION, array( __CLASS__, 'flush' ) ); add_action( 'add_option_' . PINGPUSH_OPTION, array( __CLASS__, 'flush' ) ); } return self::$cache; } /** * Clear the cache. */ public static function flush() { self::$cache = null; } /* ---------- Admin ---------- */ /** * Constructor (admin only). */ public function __construct() { add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_init', array( $this, 'register' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_action( 'wp_ajax_pingpush_validate', array( $this, 'ajax_validate' ) ); add_action( 'wp_ajax_pingpush_test', array( $this, 'ajax_test' ) ); add_action( 'admin_post_pingpush_clear_log', array( $this, 'clear_log' ) ); add_filter( 'plugin_action_links_' . plugin_basename( PINGPUSH_FILE ), array( $this, 'action_links' ) ); } /** * Settings page URL. * * @param string $tab Tab. * @return string */ public static function url( $tab = '' ) { return admin_url( 'admin.php?page=' . ( 'log' === $tab ? self::PAGE . '-log' : self::PAGE ) ); } /** * Menu icon as an SVG data URI (WordPress recolours it to match the admin colour scheme). * * @return string */ public static function menu_icon() { $svg = ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Data URI for the admin menu icon, as WordPress expects. return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } /** * Menu. */ public function menu() { add_menu_page( __( 'PingPush Alerts for Pushover', 'pingpush' ), 'PingPush', 'manage_options', self::PAGE, array( $this, 'render_settings_page' ), self::menu_icon(), 81 // Just below Settings. ); add_submenu_page( self::PAGE, __( 'PingPush Alerts for Pushover', 'pingpush' ), __( 'Settings', 'pingpush' ), 'manage_options', self::PAGE, array( $this, 'render_settings_page' ) ); add_submenu_page( self::PAGE, __( 'PingPush Log', 'pingpush' ), __( 'Log', 'pingpush' ), 'manage_options', self::PAGE . '-log', array( $this, 'render_log_page' ) ); } /** * Plugins screen link. * * @param string[] $links Links. * @return string[] */ public function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'pingpush' ) . '' ); return $links; } /** * Register the option. */ public function register() { register_setting( 'pingpush', PINGPUSH_OPTION, array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize' ), 'default' => self::defaults(), ) ); } /** * Admin CSS/JS on our page only. * * @param string $hook Screen hook. */ public function assets( $hook ) { if ( ! preg_match( '/_page_' . self::PAGE . '(-log)?$/', (string) $hook ) ) { return; } wp_enqueue_style( 'pingpush-admin', PINGPUSH_URL . 'assets/admin.css', array(), PINGPUSH_VERSION ); wp_enqueue_script( 'pingpush-admin', PINGPUSH_URL . 'assets/admin.js', array(), PINGPUSH_VERSION, true ); wp_localize_script( 'pingpush-admin', 'PingPushAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE ), 'i18n' => array( 'working' => __( 'Working…', 'pingpush' ), 'failed' => __( 'Request failed. Please try again.', 'pingpush' ), 'show' => __( 'Show', 'pingpush' ), 'hide' => __( 'Hide', 'pingpush' ), 'confirmR' => __( 'Remove this hook?', 'pingpush' ), ), ) ); } /** * Resolve option choices (array or callable). * * @param array $opt Option definition. * @return array */ private static function choices( array $opt ) { $choices = $opt['choices'] ?? array(); if ( is_callable( $choices ) ) { $choices = call_user_func( $choices ); } return is_array( $choices ) ? $choices : array(); } /** * Add a settings error when the admin API is loaded (the option can also be updated * from WP-CLI or front-end code, where add_settings_error() does not exist). * * @param string $code Code. * @param string $message Message. */ private static function settings_error( $code, $message ) { if ( function_exists( 'add_settings_error' ) ) { add_settings_error( PINGPUSH_OPTION, $code, $message ); } } /** * Sanitize the whole option. * * @param mixed $input Raw input. * @return array */ public function sanitize( $input ) { $old = self::get(); if ( ! is_array( $input ) ) { return $old; } if ( ! current_user_can( 'manage_options' ) && ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { return $old; } $out = self::defaults(); foreach ( array( 'token', 'user_key' ) as $key ) { $value = isset( $input[ $key ] ) ? preg_replace( '/\s+/', '', sanitize_text_field( (string) $input[ $key ] ) ) : ''; if ( '' !== $value && ! PingPush_API::looks_like_key( $value ) ) { self::settings_error( 'pingpush_' . $key, 'token' === $key ? __( 'The API token must be exactly 30 letters and numbers, so it was not saved and the previous value was kept. If your browser or password manager filled in a password, paste the token from pushover.net instead.', 'pingpush' ) : __( 'The user key must be exactly 30 letters and numbers, so it was not saved and the previous value was kept. If your browser or password manager filled in a password, paste the key from pushover.net instead.', 'pingpush' ) ); $value = $old[ $key ]; } $out[ $key ] = $value; } $priority = isset( $input['default_priority'] ) ? (int) $input['default_priority'] : 0; $out['default_priority'] = PingPush_API::sanitize_priority( $priority ); $out['include_site_name'] = empty( $input['include_site_name'] ) ? 0 : 1; $out['log_enabled'] = empty( $input['log_enabled'] ) ? 0 : 1; $out['log_size'] = max( 10, min( 1000, (int) ( $input['log_size'] ?? 100 ) ) ); // Events. Keep saved settings for events whose plugin is currently inactive. $events = is_array( $old['events'] ) ? $old['events'] : array(); $raw_events = isset( $input['events'] ) && is_array( $input['events'] ) ? $input['events'] : array(); $sounds = PingPush_API::sounds(); foreach ( PingPush_Events::all() as $id => $def ) { if ( 'custom' === $def['source'] ) { continue; } $raw = isset( $raw_events[ $id ] ) && is_array( $raw_events[ $id ] ) ? $raw_events[ $id ] : array(); $cur = PingPush_Events::settings_for( $id ); $priority = $raw['priority'] ?? $cur['priority']; $priority = ( 'default' === $priority ) ? 'default' : PingPush_API::sanitize_priority( $priority ); $sound = (string) ( $raw['sound'] ?? $cur['sound'] ); $row = array( 'enabled' => empty( $raw['enabled'] ) ? 0 : 1, 'priority' => $priority, 'sound' => array_key_exists( $sound, $sounds ) ? $sound : '', 'title' => sanitize_text_field( (string) ( $raw['title'] ?? '' ) ), 'message' => sanitize_textarea_field( (string) ( $raw['message'] ?? '' ) ), ); // Store blank when equal to default so future default improvements apply. if ( $row['title'] === $def['title'] ) { $row['title'] = ''; } if ( $row['message'] === $def['message'] ) { $row['message'] = ''; } foreach ( $def['options'] as $key => $opt ) { $type = $opt['type'] ?? 'text'; $choices = self::choices( $opt ); if ( 'checkboxes' === $type ) { $picked = isset( $raw[ $key ] ) && is_array( $raw[ $key ] ) ? array_map( 'strval', $raw[ $key ] ) : array(); $row[ $key ] = array_values( array_intersect( $picked, array_map( 'strval', array_keys( $choices ) ) ) ); } elseif ( 'select' === $type ) { $value = (string) ( $raw[ $key ] ?? '' ); $row[ $key ] = array_key_exists( $value, $choices ) ? $value : (string) ( $opt['default'] ?? '' ); } else { $row[ $key ] = sanitize_text_field( (string) ( $raw[ $key ] ?? '' ) ); } } $events[ $id ] = $row; } $out['events'] = $events; // Custom hooks. $hooks = array(); $seen = array(); $raw = isset( $input['custom_hooks'] ) && is_array( $input['custom_hooks'] ) ? $input['custom_hooks'] : array(); foreach ( $raw as $row ) { if ( ! is_array( $row ) ) { continue; } $hook = trim( sanitize_text_field( (string) ( $row['hook'] ?? '' ) ) ); if ( '' === $hook ) { continue; } $valid = PingPush_Event_Custom_Hook::validate( $hook ); if ( true !== $valid ) { /* translators: 1: hook name, 2: reason. */ self::settings_error( 'pingpush_hook_' . md5( $hook ), sprintf( __( 'Custom hook "%1$s" was not saved: %2$s', 'pingpush' ), $hook, $valid ) ); continue; } if ( isset( $seen[ $hook ] ) || count( $hooks ) >= PingPush_Event_Custom_Hook::MAX_HOOKS ) { continue; } $seen[ $hook ] = true; $sound = (string) ( $row['sound'] ?? '' ); $hooks[] = array( 'hook' => $hook, 'enabled' => empty( $row['enabled'] ) ? 0 : 1, 'title' => sanitize_text_field( (string) ( $row['title'] ?? '' ) ), 'message' => sanitize_textarea_field( (string) ( $row['message'] ?? '' ) ), 'priority' => PingPush_API::sanitize_priority( $row['priority'] ?? 0 ), 'sound' => array_key_exists( $sound, $sounds ) ? $sound : '', ); } $out['custom_hooks'] = $hooks; return $out; } /* ---------- AJAX ---------- */ /** * Keys from the request (so they can be tested before saving), falling back to saved ones. * * @return array{0:string,1:string} */ private function request_keys() { check_ajax_referer( self::NONCE, 'nonce' ); $settings = self::get(); $token = isset( $_POST['token'] ) ? preg_replace( '/\s+/', '', sanitize_text_field( wp_unslash( $_POST['token'] ) ) ) : ''; $user = isset( $_POST['user_key'] ) ? preg_replace( '/\s+/', '', sanitize_text_field( wp_unslash( $_POST['user_key'] ) ) ) : ''; return array( '' !== $token ? $token : (string) $settings['token'], '' !== $user ? $user : (string) $settings['user_key'], ); } /** * Check keys format. * * @param string $token Token. * @param string $user User. */ private function require_keys( $token, $user ) { if ( ! PingPush_API::looks_like_key( $token ) ) { wp_send_json_error( array( 'message' => __( 'The Application API token must be exactly 30 letters and numbers. Paste it from your application page on pushover.net (if a password manager filled the field, clear it first).', 'pingpush' ) ) ); } if ( ! PingPush_API::looks_like_key( $user ) ) { wp_send_json_error( array( 'message' => __( 'The User or Group key must be exactly 30 letters and numbers. Paste it from your pushover.net dashboard (if a password manager filled the field, clear it first).', 'pingpush' ) ) ); } } /** * Validate keys. */ public function ajax_validate() { check_ajax_referer( self::NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'pingpush' ) ), 403 ); } list( $token, $user ) = $this->request_keys(); $this->require_keys( $token, $user ); $result = PingPush_API::validate( $token, $user ); if ( ! $result['ok'] ) { wp_send_json_error( array( 'message' => $result['error'] ) ); } $message = __( 'Keys are valid.', 'pingpush' ); if ( ! empty( $result['devices'] ) ) { /* translators: %s: device names. */ $message .= ' ' . sprintf( __( 'Active devices: %s.', 'pingpush' ), implode( ', ', $result['devices'] ) ); } wp_send_json_success( array( 'message' => $message ) ); } /** * Send a test notification (synchronously, so the result shows right away). */ public function ajax_test() { check_ajax_referer( self::NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'pingpush' ) ), 403 ); } list( $token, $user ) = $this->request_keys(); $this->require_keys( $token, $user ); $site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); $title = __( 'PingPush test', 'pingpush' ); $result = PingPush_API::send( $token, $user, array( 'title' => $title, /* translators: %s: site name. */ 'message' => sprintf( __( 'Notifications from %s are working.', 'pingpush' ), $site ), 'url' => self::url(), 'url_title' => __( 'PingPush settings', 'pingpush' ), 'priority' => 0, ) ); PingPush_Logger::add( 'test', $title, $result['ok'] ? 'sent' : 'failed', $result['code'], $result['error'] ); if ( ! $result['ok'] ) { wp_send_json_error( array( 'message' => $result['error'] ) ); } wp_send_json_success( array( 'message' => __( 'Test notification sent. Check your device.', 'pingpush' ) ) ); } /** * Clear the log. */ public function clear_log() { check_admin_referer( 'pingpush_clear_log' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'pingpush' ), 403 ); } PingPush_Logger::clear(); self::flash( __( 'Log cleared.', 'pingpush' ) ); wp_safe_redirect( self::url( 'log' ) ); exit; } /* ---------- Rendering ---------- */ /** * Show a one-time admin message to the current user on the next PingPush page load. * Used after redirects instead of a URL parameter. * * @param string $message Message. * @param string $type success|error|warning|info. */ public static function flash( $message, $type = 'success' ) { set_transient( 'pingpush_notice_' . get_current_user_id(), array( 'message' => (string) $message, 'type' => (string) $type, ), MINUTE_IN_SECONDS * 5 ); } /** * Print and clear the current user's one-time message, if any. */ private function render_flash() { $key = 'pingpush_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) || empty( $notice['message'] ) ) { return; } delete_transient( $key ); $type = in_array( $notice['type'] ?? '', array( 'success', 'error', 'warning', 'info' ), true ) ? $notice['type'] : 'success'; echo '

' . esc_html( $notice['message'] ) . '

'; } /** * Settings page. */ public function render_settings_page() { $this->render( 'settings' ); } /** * Log page. */ public function render_log_page() { $this->render( 'log' ); } /** * Page wrapper. * * @param string $tab settings|log. */ private function render( $tab ) { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>

render_flash(); ?> render_log(); } else { $this->render_settings(); } $this->render_footer(); ?>
>
  1. pushover.net' ); ?>
  2. pushover.net/apps/build' ); ?>

inactive_integrations(); $all = PingPush_Events::all(); foreach ( PingPush_Events::groups() as $group => $label ) { $events = array_filter( $all, static function ( $def ) use ( $group ) { return $def['group'] === $group && 'custom' !== $def['source']; } ); if ( empty( $events ) && ! in_array( $group, array( 'forms', 'ecommerce' ), true ) ) { continue; } echo '

' . esc_html( $label ) . '

'; if ( empty( $events ) ) { echo '

' . esc_html( 'forms' === $group ? __( 'No supported form plugin is active.', 'pingpush' ) : __( 'No supported ecommerce plugin is active.', 'pingpush' ) ) . '

'; } echo '
'; foreach ( $events as $id => $def ) { $this->render_event( $id, $def ); } echo '
'; if ( 'forms' === $group || 'ecommerce' === $group ) { $names = array_intersect( $inactive, 'forms' === $group ? array( 'Contact Form 7', 'WPForms', 'Gravity Forms', 'Fluent Forms', 'Elementor Forms', 'Ninja Forms', 'Forminator', 'Formidable Forms' ) : array( 'WooCommerce', 'Easy Digital Downloads' ) ); if ( $names ) { /* translators: %s: plugin names. */ echo '

' . esc_html( sprintf( __( 'Also supported, shown here when active: %s.', 'pingpush' ), implode( ', ', $names ) ) ) . '

'; } } } $this->render_custom_hooks( $s ); submit_button( __( 'Save settings', 'pingpush' ) ); ?>
priority_select( $base . '[priority]', (string) $cur['priority'], true, $dom . '-priority' ); ?>
sound_select( $base . '[sound]', (string) $cur['sound'], $dom . '-sound' ); ?>
$opt ) : ?> render_option( $base, $dom, $key, $opt, $cur[ $key ] ?? ( $opt['default'] ?? '' ) ); ?>
{' . esc_html( $ph ) . '} '; } ?>
' . esc_html( $opt['label'] ?? $key ) . '
'; foreach ( $choices as $ck => $cl ) { printf( ' ', esc_attr( $base ), esc_attr( $key ), esc_attr( (string) $ck ), checked( in_array( (string) $ck, array_map( 'strval', $value ), true ), true, false ), esc_html( $cl ) ); } echo '
'; return; } echo '
'; if ( 'select' === $type ) { echo ''; } else { echo ''; } echo '
'; } /** * Priority select. * * @param string $name Name. * @param string $value Value. * @param bool $with_default Include "Use default". * @param string $id DOM id. */ private function priority_select( $name, $value, $with_default, $id = '' ) { echo ''; } /** * Sound select. * * @param string $name Name. * @param string $value Value. * @param string $id DOM id. */ private function sound_select( $name, $value, $id = '' ) { echo ''; } /** * Custom hooks section. * * @param array $s Settings. */ private function render_custom_hooks( array $s ) { $rows = is_array( $s['custom_hooks'] ) ? $s['custom_hooks'] : array(); ?>

$row ) { $this->render_hook_row( (string) $i, $row ); } ?>

priority_select( $base . '[priority]', (string) $row['priority'], false ); ?> sound_select( $base . '[sound]', (string) $row['sound'] ); ?>
__( 'Sent', 'pingpush' ), 'failed' => __( 'Failed', 'pingpush' ), 'retrying' => __( 'Retrying', 'pingpush' ), ); if ( null !== $quota && false !== $quota ) { /* translators: %s: number of messages. */ echo '

' . esc_html( sprintf( __( 'Pushover messages left this month: %s', 'pingpush' ), number_format_i18n( (int) $quota ) ) ) . '

'; } if ( empty( self::get()['log_enabled'] ) ) { echo '

' . esc_html__( 'Logging is turned off in Settings.', 'pingpush' ) . '

'; } ?>
event ); $event = $def ? $def['label'] : ( 'test' === $row->event ? __( 'Test notification', 'pingpush' ) : $row->event ); $time = get_date_from_gmt( $row->created_at, get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ); ?>
title ); ?> status ] ?? $row->status ); ?> code ? 'HTTP ' . $row->code . ' ' : '' ) . (string) $row->detail ) ); ?>