resolver = $resolver; } public function register() { if ( '1' !== get_option( self::OPTION, '0' ) ) { return; } add_filter( 'the_title', array( $this, 'filter' ), 10, 2 ); add_filter( 'single_post_title', array( $this, 'filter' ), 10, 2 ); add_filter( 'nav_menu_item_title', array( $this, 'filter_menu_title' ), 10, 2 ); add_filter( 'render_block_core/navigation', array( $this, 'filter_navigation_block' ), 10, 2 ); add_filter( 'render_block_core/navigation-link', array( $this, 'filter_navigation_block' ), 10, 2 ); add_filter( 'render_block_core/navigation-submenu', array( $this, 'filter_navigation_block' ), 10, 2 ); add_filter( 'render_block_core/page-list', array( $this, 'filter_navigation_block' ), 10, 2 ); // Automatic slugs come from the resolved title, only while a post is saved or previewed. add_filter( 'wp_insert_post_data', array( $this, 'filter_post_slug' ), 10, 3 ); add_filter( 'get_sample_permalink', array( $this, 'filter_sample_permalink' ), 10, 5 ); } public function filter( $title, $post = 0 ) { if ( false === strpos( $title, '[glvr' ) || ! $this->should_resolve( $title, $post ) ) { return $title; } $pattern = get_shortcode_regex( array( 'glvr' ) ); return preg_replace_callback( '/' . $pattern . '/', function ( $matches ) use ( $post ) { if ( '[' === $matches[1] && ']' === $matches[6] ) { return substr( $matches[0], 1, -1 ); } return esc_html( $this->render_match( $matches, $post ) ); }, $title ); } /** * Build an automatic slug from the resolved title, so a published post * titled "Call [glvr key=…] now" gets "call-harbor-view-now". Runs only * while a post is saved, and only when WordPress made the slug from the * title (no slug was submitted). * * @param array $data Slashed post data about to be saved. * @param array $postarr Sanitized post data. * @param array $unsanitized_postarr Post data as submitted. * @return array */ public function filter_post_slug( $data, $postarr = array(), $unsanitized_postarr = array() ) { $submitted = is_array( $unsanitized_postarr ) ? (string) ( $unsanitized_postarr['post_name'] ?? '' ) : ''; $status = (string) ( $data['post_status'] ?? '' ); $title = wp_unslash( (string) ( $data['post_title'] ?? '' ) ); if ( '' !== $submitted || in_array( $status, array( 'draft', 'pending', 'auto-draft' ), true ) || false === strpos( $title, '[glvr' ) ) { return $data; } $slug = sanitize_title( $this->slug_source( $title ) ); if ( '' !== $slug ) { $post_id = absint( is_array( $postarr ) ? ( $postarr['ID'] ?? 0 ) : 0 ); $data['post_name'] = wp_unique_post_slug( $slug, $post_id, $status, (string) ( $data['post_type'] ?? 'post' ), absint( $data['post_parent'] ?? 0 ) ); } return $data; } /** * Show the same automatic slug in the editors' permalink preview (the * Classic Editor's permalink and the block editor's generated slug). * * @param array $permalink Permalink template and slug. * @param int $post_id Post ID. * @param string $title Title given to the preview, if any. * @param string $name Slug given to the preview, if any. * @param \WP_Post $post Post. * @return array */ public function filter_sample_permalink( $permalink, $post_id, $title = null, $name = null, $post = null ) { $post = get_post( $post ? $post : $post_id ); if ( ! $post || ! is_array( $permalink ) || '' !== (string) $name || '' !== (string) $post->post_name ) { return $permalink; } $title = null !== $name && null !== $title ? (string) $title : (string) $post->post_title; if ( false === strpos( $title, '[glvr' ) ) { return $permalink; } $slug = sanitize_title( $this->slug_source( $title ) ); if ( '' !== $slug ) { $permalink[1] = wp_unique_post_slug( $slug, $post->ID, 'publish', $post->post_type, $post->post_parent ); } return $permalink; } /** * A title with every Site Variable shortcode replaced by its plain value. * * @param string $title Unslashed title. * @return string */ private function slug_source( $title ) { $resolved = preg_replace_callback( '/' . get_shortcode_regex( array( 'glvr' ) ) . '/', function ( $matches ) { if ( '[' === $matches[1] && ']' === $matches[6] ) { return substr( $matches[0], 1, -1 ); } return $this->render_match( $matches, 0 ); }, $title ); return null === $resolved ? $title : $resolved; } public function filter_menu_title( $title, $item ) { $post_id = is_object( $item ) && 'post_type' === ( $item->type ?? '' ) ? absint( $item->object_id ?? 0 ) : 0; return $this->filter( $title, $post_id ); } public function filter_navigation_block( $content, $block ) { $post_id = isset( $block['attrs']['id'] ) ? absint( $block['attrs']['id'] ) : get_the_ID(); return $this->filter( $content, $post_id ); } private function should_resolve( $title, $post ) { $allowed = ! ( is_admin() && ! wp_doing_ajax() ); if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { $allowed = false; } // The menu editor fills new items from titles over AJAX. A resolved title // would be saved as a fixed label and never follow the variable again. $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing. if ( wp_doing_ajax() && in_array( $action, array( 'add-menu-item', 'menu-get-metabox', 'menu-quick-search', 'menu-locations-save' ), true ) ) { $allowed = false; } /** * Filters whether Site Variable shortcodes should resolve in a title. * * @param bool $allowed Whether title resolution is enabled in this request. * @param string $title Original title. * @param int|\WP_Post|mixed $post Post identifier or object supplied by WordPress. */ return (bool) apply_filters( 'glovario_should_resolve_title_shortcodes', $allowed, $title, $post ); } /** * Every Site Variable shortcode and token in a text replaced with its value as * plain text (tags removed, not HTML-escaped), for strings that their consumer * escapes itself, such as the titles and descriptions of SEO plugins. * * @param string $text Text. * @param int|\WP_Post|null $post Post giving the resolution context. * @return string */ public function plain( $text, $post = 0 ) { if ( ! is_string( $text ) || ( false === strpos( $text, '[glvr' ) && false === strpos( $text, '{{glvr:' ) ) ) { return $text; } if ( false !== strpos( $text, '[glvr' ) ) { $resolved = preg_replace_callback( '/' . get_shortcode_regex( array( 'glvr' ) ) . '/', function ( $matches ) use ( $post ) { if ( '[' === $matches[1] && ']' === $matches[6] ) { return substr( $matches[0], 1, -1 ); } return $this->render_match( $matches, $post ); }, $text ); $text = null === $resolved ? $text : $resolved; } return ( new TokenParser() )->replace( $text, function ( $key, $original, $date_format ) use ( $post ) { return $this->render_key( $key, $post, $date_format, $original ); } ); } private function render_match( $matches, $post ) { $attributes = shortcode_atts( array( 'key' => '', 'fallback' => '', 'format' => BuiltinVariables::DEFAULT_DATE_FORMAT, ), shortcode_parse_atts( $matches[3] ), 'glvr' ); return $this->render_key( sanitize_key( $attributes['key'] ), $post, $attributes['format'], $attributes['fallback'] ); } private function render_key( $key, $post, $format, $fallback ) { $post = get_post( $post ); $context = array( 'post_id' => $post ? $post->ID : 0, 'post_type' => $post ? $post->post_type : '', 'source' => 'title', ); if ( BuiltinVariables::CURRENT_DATE_KEY === $key ) { $context['date_format'] = BuiltinVariables::sanitize_date_format( $format ); } $resolved = $key ? $this->resolver->resolve( $key, $context ) : null; $value = $resolved ? $resolved['value'] : $fallback; $type = $resolved ? $resolved['variable']['type'] : 'text'; $value = $resolved ? Renderer::text( $value, $type ) : $value; $value = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $value ) ) ); /** * Filters the plain-text value inserted into a title, menu label, or navigation block. * * Titles, menus, and navigation blocks HTML-escape the result, so it is safe in * text and in attributes such as a navigation link's title. * * @param string $value Plain-text value. * @param string $key Variable key. * @param string $type Variable type. * @param array $context Resolution context. */ return (string) apply_filters( 'glovario_title_variable_value', $value, $key, $type, $context ); } }