File "class-siteorigin-link-processor.php"
Full path: /home/xs854402/smcse.co.jp/public_html/test.smcse.co.jp/wp-content/plugins/broken-link-checker/app/integrations/siteorigin/class-siteorigin-link-processor.php
File
size: 6.86 B (6.86 KB bytes)
MIME-type: text/x-php
Charset: utf-8
Download Open Edit Advanced Editor Back
<?php
/**
* File that includes the SiteOrigin Builder link processor class.
*
* @package WPMUDEV_BLC\App\Integrations\SiteOrigin
*/
namespace WPMUDEV_BLC\App\Integrations\SiteOrigin;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class SiteOrigin_Link_Processor
*
* @package WPMUDEV_BLC\App\Integrations\SiteOrigin
*/
class SiteOrigin_Link_Processor {
const BUTTON_CLASS = 'SiteOrigin_Widget_Button_Widget';
const IMAGE_CLASS = 'WP_Widget_Media_Image';
/**
* URL keys to search for inside each widget's instance, in priority order.
* Button widgets use 'url'; Media Image widgets use 'link_url'.
*
* @var string[]
*/
const URL_KEYS = array( 'url', 'link_url' );
/**
* Execute the content processing logic for the given tag/attribute pair.
*
* Unlink implementations return an array: [ $search_markup => $replacement_str, ... ]
* Replace implementations return the modified content string.
* Replace_Wp_Native_Processor returns null when the WP native processor is unavailable.
*
* @param string $content The original content.
* @param string $link The old link URL.
* @param string $new_link The new link URL (empty for unlink).
* @param array $tags The HTML tags and attributes to search within.
* @return string The processed content.
*/
public function process(
string $content,
string $link,
string $new_link,
array $tags
) {
if ( empty( $content ) ) {
// No content to process, return as is.
return $content;
}
if ( empty( $tags['a'] ) || ! in_array( 'href', $tags['a'], true ) ) {
// No 'a' tags with 'href' attributes to process, return as is.
return $content;
}
// Work on the original (entity-encoded) content so attribute delimiters are
// never ambiguous. Real SiteOrigin output stores JSON with "-encoded
// double-quotes, so [^"]* / [^>]* patterns are safe here.
$offset = 0;
$closing_tag_len = strlen( '[/siteorigin_widget]' );
$widget_classes = array( self::BUTTON_CLASS, self::IMAGE_CLASS );
// Find the next occurrence of any supported widget class.
while ( false !== ( $button_class_pos = $this->find_next_class( $content, $widget_classes, $offset ) ) ) { // phpcs:ignore
$widget_start = $this->find_widget_start( $content, $button_class_pos );
if ( false === $widget_start ) {
// No widget start found, move offset past the current button class position to avoid infinite loop and continue searching.
$offset = $button_class_pos + strlen( self::BUTTON_CLASS );
continue;
}
$widget_end = $this->find_widget_end( $content, $widget_start );
if ( false === $widget_end ) {
// No closing tag exists anywhere after this point, so no further
// widget can be valid either. Stop searching.
break;
}
// Extract the widget content and check for the presence of a hidden input field.
$widget_content = substr( $content, $widget_start, $widget_end - $widget_start );
// Look for a hidden input field within the widget content.
$has_hidden_input = preg_match( '/<input[^>]+type=["\']hidden["\'][^>]*>/', $widget_content, $matches );
if ( ! $has_hidden_input || empty( $matches[0] ) ) {
// No hidden input found, move offset past the current widget to avoid infinite loop and continue searching.
$offset = $widget_end + $closing_tag_len;
continue;
}
// In the original content, inner double-quotes in the value are ",
// so [^"]* safely captures the entire JSON blob without ambiguity.
$has_value = preg_match( '/value="([^"]*)"/', $matches[0], $value_matches );
if ( ! $has_value || empty( $value_matches[1] ) ) {
// No value attribute found, move offset past the current widget to avoid infinite loop and continue searching.
$offset = $widget_end + $closing_tag_len;
continue;
}
// Replace URL keys directly in the entity-encoded value string.
// json_decode/encode cannot be used here: args values such as
// before_widget contain raw HTML with unescaped double-quotes that
// make the decoded string invalid JSON. The entity-encoded form is
// safe because every inner " is still stored as ".
$encoded_link = htmlspecialchars( $link, ENT_QUOTES | ENT_HTML5 );
$encoded_new_link = htmlspecialchars( $new_link, ENT_QUOTES | ENT_HTML5 );
// Match "KEY":"LINK" for each supported URL key.
$url_keys_pattern = implode( '|', array_map( 'preg_quote', self::URL_KEYS ) );
$url_pattern = '/("(?:' . $url_keys_pattern . ')":")' . preg_quote( $encoded_link, '/' ) . '(")/';
$new_value = preg_replace( $url_pattern, '${1}' . $encoded_new_link . '${2}', $value_matches[1] );
if ( null === $new_value || $new_value === $value_matches[1] ) {
// URL not matched or regex error, move offset past the current widget.
$offset = $widget_end + $closing_tag_len;
continue;
}
$updated_hidden_input = str_replace( $value_matches[1], $new_value, $matches[0] );
$content = str_replace( $matches[0], $updated_hidden_input, $content );
// Recalculate offset past the current widget's closing tag in the updated content.
$closing_tag_pos = strpos( $content, '[/siteorigin_widget]', $widget_start );
$offset = false !== $closing_tag_pos ? $closing_tag_pos + $closing_tag_len : $widget_end + $closing_tag_len;
}
return $content;
}
/**
* Find the position of the next occurrence of any widget class in $classes,
* starting from $offset. Returns the earliest match position, or false.
*
* @param string $content The content to search.
* @param string[] $classes List of widget class names to look for.
* @param int $offset Starting position for the search.
* @return int|false
*/
private function find_next_class( string $content, array $classes, int $offset ) {
$earliest = false;
foreach ( $classes as $class ) {
$pos = strpos( $content, $class, $offset );
if ( false !== $pos && ( false === $earliest || $pos < $earliest ) ) {
$earliest = $pos;
}
}
return $earliest;
}
/**
* Find the start position of the SiteOrigin widget containing the button.
*
* @param string $content The decoded content string.
* @param int $button_class_pos The position of the button class in the content.
* @return int|false The position of the widget start or false if not found.
*/
private function find_widget_start( string $content, int $button_class_pos ) {
return strrpos( $content, '[siteorigin_widget', -( strlen( $content ) - $button_class_pos ) );
}
/**
* Find the end position of the SiteOrigin widget containing the button.
*
* @param string $content The decoded content string.
* @param int $widget_start The position of the widget start in the content.
* @return int|false The position of the widget end or false if not found.
*/
private function find_widget_end( string $content, int $widget_start ) {
return strpos( $content, '[/siteorigin_widget]', $widget_start );
}
}