|null */ protected static $cache = null; /** * Discards the cache as soon as the settings change. * * @since 0.1.2 * * @return void */ public static function flush_cache() { self::$cache = null; } /** * Registers the cache invalidation (once when the plugin starts). * * @since 0.1.2 * * @return void */ public static function register() { add_action( 'add_option_' . self::OPTION_NAME, array( __CLASS__, 'flush_cache' ) ); add_action( 'update_option_' . self::OPTION_NAME, array( __CLASS__, 'flush_cache' ) ); add_action( 'delete_option_' . self::OPTION_NAME, array( __CLASS__, 'flush_cache' ) ); add_action( 'switch_blog', array( __CLASS__, 'flush_cache' ) ); } /** * Returns all defaults. * * @since 0.1.0 * * @return array */ public static function defaults() { $site_name = wp_specialchars_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES ); $admin_mail = (string) get_option( 'admin_email' ); $defaults = array( // Agency. 'agency_name' => '', 'agency_url' => '', // Dashboard / client view. 'care_message' => sprintf( /* translators: %s: site name. */ __( 'Your website %s is technically maintained by us. You do not have to worry about anything – focus on your business.', 'oms-quietadmin' ), $site_name ), 'show_green_light' => true, 'show_care_message' => true, 'show_update_note' => true, 'show_quick_actions' => true, 'show_help_button' => true, 'show_report_widget' => true, 'remove_default_widgets' => true, 'hide_admin_bar' => false, 'hide_wp_logo' => true, 'quick_actions' => self::default_quick_action_keys(), // Menu allowlist. 'menu_whitelist' => array( 'index.php', 'edit.php', 'edit.php?post_type=page', 'upload.php', 'edit-comments.php' ), 'submenu_whitelist' => array( 'edit.php||post-new.php', 'edit.php||edit-tags.php?taxonomy=post_tag', 'edit.php||edit-tags.php?taxonomy=category', 'edit.php?post_type=page||post-new.php?post_type=page', 'upload.php||media-new.php', ), 'strict_submenus' => true, // Notifications. 'suppress_notices' => true, 'suppress_update_nags' => true, 'suppress_comment_bubble' => true, 'suppress_site_health' => true, 'login_protection_enabled' => true, // Support. 'support_email' => $admin_mail, 'support_url' => '', 'support_phone' => '', 'support_button_label' => __( 'Request or problem? Direct support', 'oms-quietadmin' ), 'support_email_subject' => sprintf( /* translators: %s: site name. */ __( '[%s] Support request from the dashboard', 'oms-quietadmin' ), $site_name ), 'support_email_body' => __( "Hello support team,\n\nI have a question about my website:\n\n{message}\n\nKind regards\n{name}\nWebsite: {site}\nUser: {user} ({email})", 'oms-quietadmin' ), // Care report. 'report_include_backup' => true, // Behavior. 'delete_on_uninstall' => false, ); /** * Filters the default settings. * * @since 0.1.0 * * @param array $defaults Defaults. */ return apply_filters( 'quietadmin_default_options', $defaults ); } /** * Keys of the bundled quick actions. * * @since 0.1.0 * * @return string[] */ public static function default_quick_action_keys() { return array( 'new_post', 'edit_pages', 'upload_media', 'contact_data', 'support' ); } /** * Reads the complete options array (merged with defaults). * * @since 0.1.0 * * @return array */ public static function all() { if ( null !== self::$cache ) { return self::$cache; } $stored = get_option( self::OPTION_NAME, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } self::$cache = wp_parse_args( $stored, self::defaults() ); return self::$cache; } /** * Reads a single value. * * @since 0.1.0 * * @param string $key Key. * @param mixed $default_value Fallback (default: value from defaults()). * @return mixed */ public static function get( $key, $default_value = null ) { $options = self::all(); if ( array_key_exists( $key, $options ) ) { return $options[ $key ]; } if ( null !== $default_value ) { return $default_value; } $defaults = self::defaults(); return array_key_exists( $key, $defaults ) ? $defaults[ $key ] : null; } /** * Writes a single value. * * @since 0.1.0 * * @param string $key Key. * @param mixed $value Value. * @return bool */ public static function set( $key, $value ) { $options = self::all(); $options[ $key ] = $value; $saved = update_option( self::OPTION_NAME, self::sanitize( $options ) ); self::flush_cache(); return $saved; } /** * Adds missing defaults without overwriting existing values. * * @since 0.1.0 * * @return void */ public static function maybe_migrate() { $stored = get_option( self::OPTION_NAME, false ); if ( false === $stored ) { add_option( self::OPTION_NAME, self::defaults(), '', false ); return; } if ( is_array( $stored ) ) { $merged = array_merge( self::defaults(), $stored ); if ( $merged !== $stored ) { update_option( self::OPTION_NAME, $merged ); } } } /** * Callback for register_setting(): sanitizes the entire form input. * * @since 0.1.0 * * @param mixed $input Raw data from the settings form. * @return array */ public static function sanitize( $input ) { if ( ! is_array( $input ) ) { return self::all(); } $current = self::all(); // Only overwrite fields of the tab being saved, keep the rest. $input = self::scope_input_to_tab( $input, $current ); $clean = array(); // Text / URL / email. $text_fields = array( 'agency_name' => 120, 'agency_url' => 'url', 'care_message' => 'textarea', 'support_email' => 'email', 'support_url' => 'url', 'support_phone' => 60, 'support_button_label' => 120, 'support_email_subject' => 200, 'support_email_body' => 'plaintext', ); foreach ( $text_fields as $field => $type ) { $raw = isset( $input[ $field ] ) ? $input[ $field ] : ''; switch ( $type ) { case 'url': $clean[ $field ] = esc_url_raw( trim( (string) $raw ) ); break; case 'email': $email = sanitize_email( (string) $raw ); $clean[ $field ] = is_email( $email ) ? $email : ''; break; case 'textarea': $clean[ $field ] = wp_kses_post( trim( (string) $raw ) ); break; case 'plaintext': $clean[ $field ] = sanitize_textarea_field( (string) $raw ); break; default: $clean[ $field ] = sanitize_text_field( substr( (string) $raw, 0, (int) $type ) ); break; } } // Checkboxes. $bool_fields = array( 'show_green_light', 'show_care_message', 'show_update_note', 'show_quick_actions', 'show_help_button', 'show_report_widget', 'remove_default_widgets', 'hide_admin_bar', 'hide_wp_logo', 'strict_submenus', 'suppress_notices', 'suppress_update_nags', 'suppress_comment_bubble', 'suppress_site_health', 'login_protection_enabled', 'report_include_backup', 'delete_on_uninstall', ); foreach ( $bool_fields as $field ) { $clean[ $field ] = ! empty( $input[ $field ] ); } // Quick actions (list of known keys plus custom actions). if ( isset( $input['quick_actions'] ) && is_array( $input['quick_actions'] ) ) { $clean['quick_actions'] = array_values( array_filter( array_map( 'sanitize_key', $input['quick_actions'] ) ) ); } else { $clean['quick_actions'] = $current['quick_actions']; } // Menu allowlist (tokens, validated against the actual menu tree at runtime). $clean['menu_whitelist'] = self::sanitize_slug_list( isset( $input['menu_whitelist'] ) ? $input['menu_whitelist'] : array() ); $clean['submenu_whitelist'] = self::sanitize_submenu_list( isset( $input['submenu_whitelist'] ) ? $input['submenu_whitelist'] : array() ); // Values this plugin does not know (e.g. from add-ons) are preserved. foreach ( $current as $key => $value ) { if ( ! array_key_exists( $key, $clean ) ) { $clean[ $key ] = $value; } } /** * Filters the sanitized settings. * * Add-ons sanitize their own keys here. * * @since 0.1.0 * * @param array $clean Sanitized values. * @param array $input Raw input (already merged with the existing values). */ return apply_filters( 'quietadmin_sanitize_options', $clean, $input ); } /** * Limits the form input to the submitted tab. * * Without this limit, saving one tab would flip all * checkboxes of the other tabs to false (a classic Settings API pitfall). * * @since 0.1.0 * * @param array $input Raw input. * @param array $current Existing values. * @return array */ protected static function scope_input_to_tab( $input, $current ) { $tab = isset( $input['qa_tab'] ) ? sanitize_key( $input['qa_tab'] ) : ''; $tabs = class_exists( '\\QuietAdmin\\Admin\\Settings_Page' ) ? \QuietAdmin\Admin\Settings_Page::tabs() : array(); $field_types = array(); if ( $tab && isset( $tabs[ $tab ] ) ) { foreach ( (array) $tabs[ $tab ]['fields'] as $field ) { if ( empty( $field['id'] ) ) { continue; } $field_types[ $field['id'] ] = isset( $field['type'] ) ? $field['type'] : 'text'; if ( ! empty( $field['url_id'] ) ) { $field_types[ $field['url_id'] ] = 'url'; } // Related option keys that are saved with this field. foreach ( (array) ( isset( $field['also_reset'] ) ? $field['also_reset'] : array() ) as $extra ) { $field_types[ $extra ] = isset( $field['type'] ) ? $field['type'] : 'text'; } } } // Set missing fields of the current tab to their "empty value". foreach ( $field_types as $field_id => $type ) { if ( array_key_exists( $field_id, $input ) ) { continue; } if ( 'checkbox' === $type ) { $input[ $field_id ] = false; } elseif ( in_array( $type, (array) apply_filters( 'quietadmin_array_field_types', array( 'actions', 'menu_tree' ) ), true ) ) { $input[ $field_id ] = array(); } } // Keep all options that were not submitted. $merged = $current; foreach ( $input as $key => $value ) { if ( 'qa_tab' === $key ) { continue; } if ( array_key_exists( $key, $current ) ) { $merged[ $key ] = $value; } } return $merged; } /** * Sanitizes a list of slugs. * * @since 0.1.0 * * @param mixed $items Raw list. * @return string[] */ private static function sanitize_slug_list( $items ) { if ( is_string( $items ) ) { $items = preg_split( '/[\s,]+/', $items ); } if ( ! is_array( $items ) ) { return array(); } $clean = array(); foreach ( $items as $slug ) { $slug = sanitize_text_field( (string) $slug ); if ( '' === $slug ) { continue; } $clean[] = $slug; } return array_values( array_unique( array_slice( $clean, 0, 100 ) ) ); } /** * Sanitizes submenu tokens in the format `parent||submenu`. * * @since 0.1.0 * * @param mixed $items Raw list. * @return string[] */ private static function sanitize_submenu_list( $items ) { if ( ! is_array( $items ) ) { return array(); } $clean = array(); foreach ( $items as $token ) { $token = sanitize_text_field( (string) $token ); if ( false === strpos( $token, '||' ) ) { continue; } list( $parent, $sub ) = explode( '||', $token, 2 ); if ( '' === $parent || '' === $sub ) { continue; } $clean[] = $parent . '||' . $sub; } return array_values( array_unique( array_slice( $clean, 0, 300 ) ) ); } /** * URL of the agency logo. * * The plugin itself has no logo - extensions provide it via the * `quietadmin_agency_logo_url` filter. * * @since 0.1.0 * * @param string $size Image size. * @return string */ public static function agency_logo_url( $size = 'medium' ) { /** * URL of the agency logo. * * @since 0.9.0 * * @param string $url Logo URL (default: empty). * @param string $size Requested image size. */ return (string) apply_filters( 'quietadmin_agency_logo_url', '', $size ); } }