name ) : ''; case 'primary_category': $term = self::primary_category( $post_id ); return $term ? self::plain( $term->name ) : ''; case 'author': $author = get_userdata( (int) $post->post_author ); return $author ? self::plain( $author->display_name ) : ''; case 'date': return self::plain( get_the_date( '', $post ) ); case 'modified': return self::plain( get_the_modified_date( '', $post ) ); case 'excerpt': if ( post_password_required( $post ) ) { return ''; // Never leak protected content into metadata. } if ( '' !== trim( (string) $post->post_excerpt, " \t\n\r\0\x0B" ) ) { return SeoData::clean( $post->post_excerpt ); } return SeoData::auto_description( $post ); case 'id': return (string) $post_id; case 'permalink': $meta = new MetaGenerator(); return self::url( $meta->canonical_for_post( $post_id ) ); } return ''; } /** * Sanitize template text that an editor or administrator typed, WITHOUT damaging %%variables%%. * * sanitize_text_field() removes anything that looks like a percent-encoded octet. "%%date%%" contains "%da" * and "%%category%%" contains "%ca", so both were silently corrupted ("%te%%", "%tegory%%"). * Well-formed variable tokens are therefore shielded while the rest of the string is sanitized. * * @param mixed $value Raw value (already unslashed). * @param int $length Maximum length in characters; 0 for no limit. * @return string */ public static function sanitize_template( $value, $length = 0 ) { $value = is_scalar( $value ) ? (string) $value : ''; $map = array(); $index = 0; $value = preg_replace_callback( self::TOKEN_PATTERN, static function ( $match ) use ( &$map, &$index ) { $key = '{{prtok' . $index . '}}'; $map[ $key ] = $match[0]; ++$index; return $key; }, $value ); $value = sanitize_text_field( is_string( $value ) ? $value : '' ); if ( $map ) { $value = strtr( $value, $map ); } return $length > 0 ? self::cut( $value, $length ) : $value; } /** Configured SEO title separator (never a hard-coded symbol). */ public static function separator() { return self::plain( get_option( 'publishrank_seo_title_separator', '|' ) ); } /** First assigned category (WordPress order). */ private static function first_category( $post_id ) { $terms = get_the_category( $post_id ); if ( is_array( $terms ) && ! empty( $terms ) && $terms[0] instanceof \WP_Term ) { return $terms[0]; } return null; } /** Editor-selected primary category when valid for the post, else the first assigned category. */ private static function primary_category( $post_id ) { $selected = absint( PostMeta::get( $post_id, PostMeta::PRIMARY_CATEGORY, 0 ) ); if ( $selected && has_category( $selected, $post_id ) ) { $term = get_term( $selected, 'category' ); if ( $term instanceof \WP_Term ) { return $term; } } return self::first_category( $post_id ); } /** "Page 2" style suffix, empty on the first page. */ private static function page_suffix( $page ) { if ( $page < 2 ) { return ''; } /* translators: %d: page number. */ return sprintf( __( 'Page %d', 'publishrank' ), $page ); } /** Current pagination number of the main query (paged archives and paginated posts). */ public static function current_page() { return max( 1, (int) get_query_var( 'paged' ), (int) get_query_var( 'page' ) ); } /** Normalise to plain text: no tags, decoded entities, collapsed whitespace. */ private static function plain( $value ) { $value = is_scalar( $value ) ? wp_check_invalid_utf8( (string) $value ) : ''; $value = wp_strip_all_tags( $value ); $value = html_entity_decode( $value, ENT_QUOTES, 'UTF-8' ); $value = preg_replace( '/\s+/u', ' ', $value ); return is_string( $value ) ? trim( $value, " \t\n\r\0\x0B" ) : ''; } /** Validate a URL; invalid values become an empty string. */ private static function url( $value ) { return esc_url_raw( is_scalar( $value ) ? (string) $value : '' ); } /** * Collapse whitespace, remove dangling or doubled separators left by empty variables * (for example "Title | | Site" or "Site |"), and strip any markup. */ private static function tidy( $text, $sep, $is_title = true ) { $text = wp_strip_all_tags( $text ); $text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' ); $text = preg_replace( '/\s+/u', ' ', $text ); $text = is_string( $text ) ? trim( $text, " \t\n\r\0\x0B" ) : ''; if ( '' === $text || ! $is_title ) { return $text; } // Only clean around separators that are punctuation/symbols; a word-like separator must be left alone. if ( '' !== $sep && ! preg_match( '/[\p{L}\p{N}]/u', $sep ) ) { $quoted = preg_quote( $sep, '/' ); $text = preg_replace( '/(?:\s*' . $quoted . '\s*){2,}/u', ' ' . $sep . ' ', $text ); $text = preg_replace( '/^(?:\s*' . $quoted . ')+\s*|\s*(?:' . $quoted . '\s*)+$/u', '', (string) $text ); } // Legacy behaviour: a title never starts or ends with a bare dash or pipe. $text = trim( (string) $text, " \t\n\r\0\x0B|–—-" ); return preg_replace( '/\s+/u', ' ', $text ); } /** Multibyte-safe substring. */ private static function cut( $text, $length ) { return function_exists( 'mb_substr' ) ? mb_substr( $text, 0, $length ) : substr( $text, 0, $length ); } }