*/ public static function defaults() { return array( 'output_title' => 1, 'output_description' => 1, 'output_canonical' => 1, 'output_robots' => 1, 'output_social' => 1, 'output_schema' => 1, 'default_social_image' => '', 'twitter_site' => '', 'sitemap_enabled' => 1, 'sitemap_post_types' => array( 'post', 'page' ), 'disable_core_sitemap' => 0, 'stats_enabled' => 1, 'stats_column' => 1, 'show_credit' => 0, 'schema_all_posts' => 1, 'home_title' => '', 'home_description' => '', 'google_verification' => '', 'bing_verification' => '', 'ga4_id' => '', 'ga4_skip_editors' => 1, ); } /** * Return every setting, with defaults filled in. * * @return array */ public static function all() { $stored = get_option( self::OPTION, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } // Sites set up before 1.0.3 keep per entry JSON-LD until the owner // ticks the every post setting. New installs get it on by default. if ( ! empty( $stored ) && ! array_key_exists( 'schema_all_posts', $stored ) ) { $stored['schema_all_posts'] = 0; } return wp_parse_args( $stored, self::defaults() ); } /** * Return a single setting. * * @param string $key Setting key. * @param mixed $fallback Value returned when the key is unknown. * @return mixed */ public static function get( $key, $fallback = null ) { $all = self::all(); return array_key_exists( $key, $all ) ? $all[ $key ] : $fallback; } /** * Create the option row with defaults when it does not exist yet. * * @return void */ public static function ensure_defaults() { if ( false === get_option( self::OPTION, false ) ) { add_option( self::OPTION, self::defaults() ); } } /** * Post types the user may include in the sitemap and in the stats column. * * @return array Post type name => label. */ public static function selectable_post_types() { $types = get_post_types( array( 'public' => true, ), 'objects' ); $result = array(); foreach ( $types as $type ) { if ( 'attachment' === $type->name ) { continue; } $result[ $type->name ] = $type->labels->singular_name; } return $result; } /** * Sanitize the whole settings array coming from the settings screen. * * Unknown keys are dropped and every value is forced into its expected type, * so a crafted request cannot store anything the plugin does not expect. * * @param mixed $input Raw value submitted by the settings API. * @return array */ public static function sanitize( $input ) { $defaults = self::defaults(); $clean = $defaults; if ( ! is_array( $input ) ) { return $clean; } $checkboxes = array( 'output_title', 'output_description', 'output_canonical', 'output_robots', 'output_social', 'output_schema', 'sitemap_enabled', 'disable_core_sitemap', 'stats_enabled', 'stats_column', 'show_credit', 'schema_all_posts', 'ga4_skip_editors', ); foreach ( $checkboxes as $key ) { $clean[ $key ] = ( isset( $input[ $key ] ) && '1' === (string) $input[ $key ] ) ? 1 : 0; } $clean['default_social_image'] = isset( $input['default_social_image'] ) ? esc_url_raw( trim( (string) $input['default_social_image'] ) ) : ''; $twitter = isset( $input['twitter_site'] ) ? (string) $input['twitter_site'] : ''; $twitter = sanitize_text_field( $twitter ); $twitter = ltrim( trim( $twitter ), '@' ); $twitter = preg_replace( '/[^A-Za-z0-9_]/', '', $twitter ); $clean['twitter_site'] = is_string( $twitter ) ? substr( $twitter, 0, 15 ) : ''; $clean['home_title'] = isset( $input['home_title'] ) ? sanitize_text_field( wp_unslash( (string) $input['home_title'] ) ) : ''; $clean['home_description'] = isset( $input['home_description'] ) ? sanitize_textarea_field( wp_unslash( (string) $input['home_description'] ) ) : ''; $clean['google_verification'] = isset( $input['google_verification'] ) ? self::sanitize_verification( (string) $input['google_verification'] ) : ''; $clean['bing_verification'] = isset( $input['bing_verification'] ) ? self::sanitize_verification( (string) $input['bing_verification'] ) : ''; $clean['ga4_id'] = isset( $input['ga4_id'] ) ? self::sanitize_ga4_id( (string) $input['ga4_id'] ) : ''; $allowed_types = array_keys( self::selectable_post_types() ); $selected = array(); if ( isset( $input['sitemap_post_types'] ) && is_array( $input['sitemap_post_types'] ) ) { foreach ( $input['sitemap_post_types'] as $type ) { $type = sanitize_key( (string) $type ); if ( in_array( $type, $allowed_types, true ) ) { $selected[] = $type; } } } $clean['sitemap_post_types'] = array_values( array_unique( $selected ) ); return $clean; } /** * Share title for the site front page. * * @return string */ public static function home_title() { $title = (string) self::get( 'home_title', '' ); return '' !== $title ? $title : get_bloginfo( 'name', 'display' ); } /** * Description for the site front page. * * @return string */ public static function home_description() { $description = (string) self::get( 'home_description', '' ); return '' !== $description ? $description : get_bloginfo( 'description', 'display' ); } /** * Keep only the token of a site verification code. * * Accepts either the bare token or the whole meta tag copied from the * search engine, and returns the value of its content attribute. * * @param string $value Raw value. * @return string */ public static function sanitize_verification( $value ) { $value = trim( wp_unslash( $value ) ); if ( preg_match( '/content\s*=\s*["\']([^"\']*)["\']/i', $value, $matches ) ) { $value = $matches[1]; } $value = preg_replace( '/[^A-Za-z0-9_\-]/', '', $value ); return is_string( $value ) ? substr( $value, 0, 100 ) : ''; } /** * Accept a Google Analytics 4 measurement ID such as G-ABC123XYZ. * * @param string $value Raw value. * @return string The upper cased ID, or an empty string when it does not match. */ public static function sanitize_ga4_id( $value ) { $value = strtoupper( trim( sanitize_text_field( wp_unslash( $value ) ) ) ); return preg_match( '/^G-[A-Z0-9]{4,20}$/', $value ) ? $value : ''; } }