variables = $variables; $this->rules = $rules; $this->tokens = $tokens ? $tokens : new TokenParser(); } public function replace( $variable, $matches, $strategy, $finalize = null ) { if ( ! in_array( $strategy, array( 'value', 'empty' ), true ) ) { return new \WP_Error( 'invalid_replacement_strategy', __( 'Select a valid reference replacement option.', 'glovario' ) ); } $replacement = 'value' === $strategy ? (string) $variable['value'] : ''; $locations = $this->unique_locations( $matches ); foreach ( $locations as $match ) { if ( 'content' === $match['kind'] && ! current_user_can( 'edit_post', $match['id'] ) ) { return new \WP_Error( 'replacement_forbidden', __( 'You do not have permission to update every detected content item, so no deletion was attempted.', 'glovario' ) ); } } return Transaction::run( function () use ( $variable, $replacement, $locations, $finalize ) { $result = $this->apply( $variable['variable_key'], $replacement, $locations, 'replace' ); return is_wp_error( $result ) || ! is_callable( $finalize ) ? $result : call_user_func( $finalize, $result ); } ); } public function migrate_key( $variable, $new_key, $matches, $finalize = null ) { $new_key = sanitize_key( $new_key ); if ( '' === $new_key || $new_key === $variable['variable_key'] ) { return new \WP_Error( 'invalid_migration_key', __( 'Enter a different valid key before migrating references.', 'glovario' ) ); } $locations = $this->unique_locations( $matches ); foreach ( $locations as $match ) { if ( $this->is_post_storage( $match['storage'] ?? $match['kind'] ) && ! current_user_can( 'edit_post', $match['id'] ) ) { return new \WP_Error( 'replacement_forbidden', __( 'You do not have permission to update every detected content item, so no migration was attempted.', 'glovario' ) ); } } return Transaction::run( function () use ( $variable, $new_key, $locations, $finalize ) { $result = $this->apply( $variable['variable_key'], $new_key, $locations, 'key' ); return is_wp_error( $result ) || ! is_callable( $finalize ) ? $result : call_user_func( $finalize, $result ); } ); } public function preview( $variable, $match, $strategy ) { $source = $this->source_content( $match ); if ( null === $source ) { return null; } $replacement = 'value' === $strategy ? (string) $variable['value'] : ''; if ( 'keep' === $strategy ) { $after = $source; } elseif ( 'post_content' === ( $match['storage'] ?? $match['kind'] ) || 'content' === ( $match['storage'] ?? $match['kind'] ) ) { $after = $this->replace_post_content( $source, $variable['variable_key'], $replacement ); } else { $after = $this->replace_references( $source, $variable['variable_key'], $replacement ); } $after_display = $after; if ( 'value' === $strategy ) { $marked_replacement = self::PREVIEW_START . $replacement . self::PREVIEW_END; if ( 'post_content' === ( $match['storage'] ?? $match['kind'] ) || 'content' === ( $match['storage'] ?? $match['kind'] ) ) { $after_display = $this->replace_post_content( $source, $variable['variable_key'], $marked_replacement ); } else { $after_display = $this->replace_references( $source, $variable['variable_key'], $marked_replacement ); } } return array( 'location' => $match['title'], 'before' => $this->excerpt( $source, $variable['variable_key'] ), 'after' => $this->excerpt( $after, $variable['variable_key'] ), 'after_display' => $this->excerpt( $after_display, $variable['variable_key'] ), ); } private function unique_locations( $matches ) { $locations = array(); foreach ( (array) $matches as $match ) { $storage = $match['storage'] ?? $match['kind']; $id = 'rule' === $storage ? absint( $match['rule_id'] ?? 0 ) : absint( $match['id'] ?? 0 ); if ( ! $id ) { continue; } $locations[ $storage . ':' . $id ] = $match; } return array_values( $locations ); } private function apply( $key, $replacement, $locations, $mode ) { do_action( 'glovario_before_reference_batch', $locations, $mode ); $updated = 0; foreach ( $locations as $match ) { $result = $this->replace_location( $key, $replacement, $match, $mode ); if ( is_wp_error( $result ) ) { return $result; } $updated += $result ? 1 : 0; } $result = array( 'updated_locations' => $updated ); do_action( 'glovario_after_reference_batch', $locations, $mode ); return $result; } private function replace_location( $key, $replacement, $match, $mode = 'replace' ) { $storage = $match['storage'] ?? $match['kind']; if ( $this->is_post_storage( $storage ) ) { $post = get_post( $match['id'] ); if ( ! $post ) { return new \WP_Error( 'replacement_content_missing', __( 'A detected content item no longer exists.', 'glovario' ) ); } $field = 'content' === $storage ? 'post_content' : $storage; $source = (string) $post->{$field}; $content = 'post_content' === $field ? $this->replace_post_content( $source, $key, $replacement, $mode ) : $this->replace_references( $source, $key, $replacement, $mode ); if ( $content === $source ) { return false; } if ( \Glovario\Migration\ContentConversionService::would_lose_markup( $content, $field ) ) { return new \WP_Error( 'replacement_markup', /* translators: %s: title of the content item. */ sprintf( __( '“%s” contains markup that your account cannot save (such as an embedded frame or a script). Nothing was changed; a user who can save unfiltered HTML must make this change.', 'glovario' ), $post->post_title ) ); } // wp_update_post() unslashes what it is given: slash it, or every backslash is lost. $result = wp_update_post( wp_slash( array( 'ID' => $post->ID, $field => $content, ) ), true ); return is_wp_error( $result ) ? $result : true; } if ( 'widget_text' === $storage ) { $widgets = get_option( 'widget_text', array() ); $id = absint( $match['id'] ?? 0 ); if ( ! isset( $widgets[ $id ] ) || ! is_array( $widgets[ $id ] ) ) { return new \WP_Error( 'replacement_widget_missing', __( 'A detected text widget no longer exists.', 'glovario' ) ); } $source = (string) ( $widgets[ $id ]['text'] ?? '' ); $content = $this->replace_html_text( $source, $key, $replacement, $mode ); if ( $content === $source ) { return false; } $widgets[ $id ]['text'] = $content; return update_option( 'widget_text', $widgets ) ? true : new \WP_Error( 'replacement_widget_failed', __( 'A detected text widget could not be updated.', 'glovario' ) ); } if ( 'widget_block' === $storage ) { $widgets = get_option( 'widget_block', array() ); $id = absint( $match['id'] ?? 0 ); if ( ! isset( $widgets[ $id ] ) || ! is_array( $widgets[ $id ] ) ) { return new \WP_Error( 'replacement_widget_missing', __( 'A detected text widget no longer exists.', 'glovario' ) ); } $source = (string) ( $widgets[ $id ]['content'] ?? '' ); $content = $this->replace_post_content( $source, $key, $replacement, $mode ); if ( $content === $source ) { return false; } $widgets[ $id ]['content'] = $content; return update_option( 'widget_block', $widgets ) ? true : new \WP_Error( 'replacement_widget_failed', __( 'A detected text widget could not be updated.', 'glovario' ) ); } if ( 'rule' === $storage ) { $rule = $this->rules->find( $match['rule_id'] ?? 0 ); if ( ! $rule ) { return new \WP_Error( 'replacement_rule_missing', __( 'A detected conditional rule no longer exists.', 'glovario' ) ); } $rule['alternative_value'] = $this->replace_references( $rule['alternative_value'], $key, $replacement, $mode ); $result = $this->rules->save( $rule, $rule['id'] ); return is_wp_error( $result ) ? $result : true; } $source = $this->variables->find( $match['id'] ); if ( ! $source || $source['variable_key'] === $key ) { return false; } $source['value'] = $this->replace_references( $source['value'], $key, $replacement, $mode ); $result = $this->variables->update( $source['id'], $source ); return is_wp_error( $result ) ? $result : true; } private function source_content( $match ) { $storage = $match['storage'] ?? $match['kind']; if ( $this->is_post_storage( $storage ) ) { $post = get_post( $match['id'] ); $field = 'content' === $storage ? 'post_content' : $storage; return $post ? $post->{$field} : null; } if ( 'widget_text' === $storage ) { $widgets = get_option( 'widget_text', array() ); return isset( $widgets[ $match['id'] ]['text'] ) ? $widgets[ $match['id'] ]['text'] : null; } if ( 'widget_block' === $storage ) { $widgets = get_option( 'widget_block', array() ); return isset( $widgets[ $match['id'] ]['content'] ) ? $widgets[ $match['id'] ]['content'] : null; } if ( 'rule' === $storage ) { $rule = $this->rules->find( $match['rule_id'] ?? 0 ); return $rule ? $rule['alternative_value'] : null; } $variable = $this->variables->find( $match['id'] ); return $variable ? $variable['value'] : null; } private function replace_post_content( $content, $key, $replacement, $mode = 'replace' ) { $blocks = parse_blocks( $content ); $blocks = $this->replace_blocks( $blocks, $key, $replacement, $mode ); return serialize_blocks( $blocks ); } private function replace_blocks( $blocks, $key, $replacement, $mode = 'replace' ) { $result = array(); foreach ( $blocks as $block ) { if ( 'glovario/site-variable' === $block['blockName'] && sanitize_key( $block['attrs']['key'] ?? '' ) === $key ) { if ( 'key' === $mode ) { $block['attrs']['key'] = $replacement; $result[] = $block; continue; } $text = $this->escape_text_replacement( $replacement ); $result[] = array( 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), 'innerHTML' => $text, 'innerContent' => array( $text ), ); continue; } if ( 'key' === $mode && in_array( $block['blockName'], array( 'core/navigation', 'core/navigation-link', 'core/navigation-submenu', 'core/page-list' ), true ) ) { foreach ( (array) ( $block['attrs'] ?? array() ) as $attribute => $value ) { if ( is_string( $value ) ) { $block['attrs'][ $attribute ] = $this->replace_references( $value, $key, $replacement, $mode ); } } } if ( ! empty( $block['innerBlocks'] ) ) { $block['innerBlocks'] = $this->replace_blocks( $block['innerBlocks'], $key, $replacement, $mode ); } if ( ! empty( $block['innerContent'] ) ) { foreach ( $block['innerContent'] as $index => $inner ) { if ( is_string( $inner ) ) { $block['innerContent'][ $index ] = $this->replace_html_text( $inner, $key, $replacement, $mode ); } } } $block['innerHTML'] = $this->replace_html_text( $block['innerHTML'] ?? '', $key, $replacement, $mode ); $result[] = $block; } return $result; } private function replace_html_text( $content, $key, $replacement, $mode = 'replace' ) { $parts = preg_split( '~(<(?:script|style|pre|code)\b[^>]*>.*?||<[^>]+>)~is', (string) $content, -1, PREG_SPLIT_DELIM_CAPTURE ); foreach ( $parts as $index => $part ) { if ( 0 === $index % 2 ) { $parts[ $index ] = $this->replace_references( $part, $key, 'key' === $mode ? $replacement : $this->escape_text_replacement( $replacement ), $mode ); } } return implode( '', $parts ); } private function escape_text_replacement( $replacement ) { return str_replace( array( '"', ''' ), array( '"', "'" ), esc_html( (string) $replacement ) ); } private function replace_references( $content, $key, $replacement, $mode = 'replace' ) { $content = $this->tokens->replace( $content, static function ( $found, $original ) use ( $key, $replacement, $mode ) { if ( $found !== $key ) { return $original; } return 'key' === $mode ? str_replace( '{{glvr:' . $key, '{{glvr:' . $replacement, $original ) : $replacement; } ); if ( false === strpos( $content, '[glvr' ) ) { return $content; } $pattern = '/' . get_shortcode_regex( array( 'glvr' ) ) . '/s'; $result = preg_replace_callback( $pattern, static function ( $matches ) use ( $key, $replacement, $mode ) { if ( '[' === $matches[1] && ']' === $matches[6] ) { return $matches[0]; } $attributes = shortcode_parse_atts( $matches[3] ); $found = sanitize_key( is_array( $attributes ) ? ( $attributes['key'] ?? '' ) : '' ); if ( $found !== $key ) { return $matches[0]; } if ( 'key' !== $mode ) { return $replacement; } $attribute_pattern = '/(\bkey\s*=\s*)(["\']?)' . preg_quote( $key, '/' ) . '\\2/i'; $migrated = preg_replace( $attribute_pattern, '$1$2' . $replacement . '$2', $matches[0], 1 ); return null === $migrated ? $matches[0] : $migrated; }, $content ); return null === $result ? $content : $result; } private function is_post_storage( $storage ) { return in_array( $storage, array( 'content', 'post_content', 'post_title', 'post_excerpt' ), true ); } private function excerpt( $content, $key ) { $text = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $content ) ) ); if ( '' === $text && false !== strpos( (string) $content, 'glovario/site-variable' ) ) { $text = ''; } $marked_excerpt = $this->marked_excerpt( $text ); if ( null !== $marked_excerpt ) { return $marked_excerpt; } return wp_html_excerpt( $text, 220, '…' ); } private function marked_excerpt( $text ) { $start = strpos( $text, self::PREVIEW_START ); if ( false === $start ) { return null; } $value_start = $start + strlen( self::PREVIEW_START ); $end = strpos( $text, self::PREVIEW_END, $value_start ); if ( false === $end ) { return null; } $before = substr( $text, 0, $start ); $replacement = substr( $text, $value_start, $end - $value_start ); $after = substr( $text, $end + strlen( self::PREVIEW_END ) ); return $this->tail_excerpt( $before, 80 ) . self::PREVIEW_START . wp_html_excerpt( $replacement, 140, '…' ) . self::PREVIEW_END . wp_html_excerpt( $after, 60, '…' ); } private function tail_excerpt( $text, $length ) { $text_length = function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text ); if ( $text_length <= $length ) { return $text; } $tail = function_exists( 'mb_substr' ) ? mb_substr( $text, -$length ) : substr( $text, -$length ); return '…' . $tail; } }