Plugin Directory

Changeset 3750897


Ignore:
Timestamp:
10/16/2026 05:57:06 PM (4 weeks ago)
Author:
patrickposner
Message:

Update to version 3.8.18 from GitHub

Location:
simply-static
Files:
8 edited
1 copied

Legend:

Unmodified
Added
Removed
  • simply-static/tags/3.8.18/readme.txt

    r3734392 r3750897  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag:  3.8.17
     7Stable tag:  3.8.18
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    276276== Changelog ==
    277277
     278= 3.8.18 =
     279
     280* Fixed Oxygen and Breakdance home URLs left behind after Static Studio migrations
     281* Fixed bundled plugin assets when WordPress content directories use custom public names
     282
    278283= 3.8.17 =
    279284
  • simply-static/tags/3.8.18/simply-static.php

    r3734392 r3750897  
    99 * Plugin URI:        https://simplystatic.com
    1010 * Description:       A static site generator to create fast and secure static versions of your WordPress website.
    11  * Version:           3.8.17
     11 * Version:           3.8.18
    1212 * Requires at least:  6.2
    1313 * Author:            Patrick Posner
    … …  
    2121define( 'SIMPLY_STATIC_PATH', plugin_dir_path( __FILE__ ) );
    2222define( 'SIMPLY_STATIC_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) );
    23 define( 'SIMPLY_STATIC_VERSION', '3.8.17' );
     23define( 'SIMPLY_STATIC_VERSION', '3.8.18' );
    2424
    2525/**
  • simply-static/tags/3.8.18/src/class-ss-url-extractor.php

    r3734392 r3750897  
    487487         * The fetched page origin is included when WordPress does not already know
    488488         * about it. On Static Studio, also include managed WordPress aliases found in
    489          * the page. Migrations can leave a retired staticN.studio alias in post data
    490          * even though the page is now fetched through an onstatic.studio runtime.
     489         * the page. Migrations can leave a retired Static Studio alias in post data
     490         * even though the page is now fetched through a different managed runtime.
    491491         *
    492492         * @param string $source_origin Fetched page origin.
    … …  
    513513                // still operates on the original body and handles both representations.
    514514                $body = str_replace( '\\/', '/', $body );
    515                 $pattern = '~(?:https?:)?//(wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:onstatic|static\\d*)\\.studio)(?=$|[/?#\\s<>"\'])~i';
    516                 $matched = preg_match_all( $pattern, $body, $matches );
     515                // Retired onstatic/staticN aliases predate tenant-stable metadata and can
     516                // occur throughout migrated builder content.
     517                $retired_pattern = '~(?:https?:)?//(wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:onstatic|static\\d*)\\.studio)(?=$|[/?#\\s<>"\'])~i';
     518                $matched         = preg_match_all( $retired_pattern, $body, $matches );
     519                if ( false !== $matched && 0 !== $matched ) {
     520                        foreach ( $matches[1] as $host ) {
     521                                $origins[] = 'https://' . strtolower( $host );
     522                        }
     523                }
     524
     525                // Current ls-static aliases are valid tenant URLs. Limit their discovery to
     526                // the exact Oxygen/Breakdance configuration object which owns homeUrl; a
     527                // generic widget may legitimately point its own homeUrl at another tenant.
     528                foreach ( $this->get_builder_home_url_origins( $body ) as $builder_origin ) {
     529                        $origins[] = $builder_origin;
     530                }
     531
     532                return array_values( array_unique( array_filter( $origins ) ) );
     533        }
     534
     535        /**
     536         * Find managed homeUrl origins inside Oxygen and Breakdance assignments.
     537         *
     538         * @param string $body Page body with JSON slash escapes normalized.
     539         *
     540         * @return array
     541         */
     542        private function get_builder_home_url_origins( $body ) {
     543                $origins            = array();
     544                $assignment_pattern = '~(?:^|[^a-z0-9_$\\.])(?:window\\.)?(?:OxygenFrontend|BreakdanceFrontend)(\\s*\\.\\s*data)?\\s*=\\s*\\{~i';
     545                $matched            = preg_match_all( $assignment_pattern, $body, $assignments, PREG_SET_ORDER | PREG_OFFSET_CAPTURE );
    517546
    518547                if ( false === $matched || 0 === $matched ) {
    … …  
    520549                }
    521550
    522                 foreach ( $matches[1] as $host ) {
    523                         $origins[] = 'https://' . strtolower( $host );
    524                 }
    525 
    526                 return array_values( array_unique( array_filter( $origins ) ) );
     551                foreach ( $assignments as $assignment ) {
     552                        $assignment_text   = $assignment[0][0];
     553                        $assignment_offset = (int) $assignment[0][1];
     554                        $identifier_offset = 0;
     555                        if ( preg_match( '~(?:window\s*\.\s*)?(?:OxygenFrontend|BreakdanceFrontend)~i', $assignment_text, $identifier, PREG_OFFSET_CAPTURE ) ) {
     556                                $identifier_offset = (int) $identifier[0][1];
     557                        }
     558                        if ( ! $this->is_javascript_code_offset( $body, $assignment_offset + $identifier_offset ) ) {
     559                                continue;
     560                        }
     561                        $opening_brace     = $assignment_offset + strrpos( $assignment_text, '{' );
     562                        $object            = $this->get_javascript_object_at_offset( $body, $opening_brace );
     563
     564                        if ( '' === $object ) {
     565                                continue;
     566                        }
     567
     568                        $is_data_assignment = isset( $assignment[1][0] ) && '' !== trim( $assignment[1][0] );
     569                        $data_object        = $is_data_assignment ? $object : $this->get_top_level_javascript_object_property( $object, 'data' );
     570                        if ( '' === $data_object ) {
     571                                continue;
     572                        }
     573
     574                        foreach ( $this->get_top_level_managed_home_url_origins( $data_object ) as $home_url_origin ) {
     575                                $origins[] = $home_url_origin;
     576                        }
     577                }
     578
     579                return array_values( array_unique( $origins ) );
     580        }
     581
     582        /**
     583         * Confirm that a regex assignment candidate is executable script, not text.
     584         *
     585         * @param string $source Page or JavaScript source.
     586         * @param int    $offset Candidate byte offset.
     587         *
     588         * @return bool
     589         */
     590        private function is_javascript_code_offset( $source, $offset ) {
     591                $offset     = min( max( 0, (int) $offset ), strlen( $source ) );
     592                $scan_start = 0;
     593                $before     = substr( $source, 0, $offset );
     594                $script     = strripos( $before, '<script' );
     595                $end_script = strripos( $before, '</script' );
     596
     597                if ( false !== $script && ( false === $end_script || $script > $end_script ) ) {
     598                        $tag_end = strpos( $source, '>', $script );
     599                        if ( false === $tag_end || $tag_end >= $offset ) {
     600                                return false;
     601                        }
     602                        $scan_start = $tag_end + 1;
     603                }
     604
     605                $script_source = substr( $source, $scan_start, $offset - $scan_start );
     606
     607                return -1 !== $this->get_javascript_brace_depth_at_offset( $script_source, strlen( $script_source ) );
     608        }
     609
     610        /**
     611         * Get one top-level object-valued property from a JavaScript object.
     612         *
     613         * @param string $object JavaScript object source.
     614         * @param string $property Property name.
     615         *
     616         * @return string
     617         */
     618        private function get_top_level_javascript_object_property( $object, $property ) {
     619                $property_pattern = '~(?:["\']' . preg_quote( $property, '~' ) . '["\']|(?<![a-z0-9_$])' . preg_quote( $property, '~' ) . '(?![a-z0-9_$]))\\s*:\\s*\\{~i';
     620                $matched          = preg_match_all( $property_pattern, $object, $properties, PREG_OFFSET_CAPTURE );
     621
     622                if ( false === $matched || 0 === $matched ) {
     623                        return '';
     624                }
     625
     626                foreach ( $properties[0] as $candidate ) {
     627                        if ( 1 !== $this->get_javascript_brace_depth_at_offset( $object, (int) $candidate[1] ) ) {
     628                                continue;
     629                        }
     630
     631                        $opening_brace = (int) $candidate[1] + strrpos( $candidate[0], '{' );
     632
     633                        return $this->get_javascript_object_at_offset( $object, $opening_brace );
     634                }
     635
     636                return '';
     637        }
     638
     639        /**
     640         * Get a managed homeUrl declared directly on a builder data object.
     641         *
     642         * @param string $data_object Builder data object source.
     643         *
     644         * @return array
     645         */
     646        private function get_top_level_managed_home_url_origins( $data_object ) {
     647                $origins          = array();
     648                $home_url_pattern = '~["\']homeUrl["\']\\s*:\\s*["\'](?:https?:)?//(wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.ls-static\\.studio)(?=$|[/?#\\s<>"\'])~i';
     649                $matched          = preg_match_all( $home_url_pattern, $data_object, $home_urls, PREG_SET_ORDER | PREG_OFFSET_CAPTURE );
     650
     651                if ( false === $matched || 0 === $matched ) {
     652                        return $origins;
     653                }
     654
     655                foreach ( $home_urls as $home_url ) {
     656                        if ( 1 !== $this->get_javascript_brace_depth_at_offset( $data_object, (int) $home_url[0][1] ) ) {
     657                                continue;
     658                        }
     659
     660                        $origins[] = 'https://' . strtolower( $home_url[1][0] );
     661                }
     662
     663                return array_values( array_unique( $origins ) );
     664        }
     665
     666        /**
     667         * Calculate object-brace depth at one byte offset, ignoring quoted strings.
     668         *
     669         * @param string $source JavaScript source.
     670         * @param int    $offset Byte offset.
     671         *
     672         * @return int
     673         */
     674        private function get_javascript_brace_depth_at_offset( $source, $offset ) {
     675                $depth         = 0;
     676                $quote         = '';
     677                $escaped       = false;
     678                $line_comment  = false;
     679                $block_comment = false;
     680                $limit         = min( max( 0, (int) $offset ), strlen( $source ) );
     681
     682                for ( $index = 0; $index < $limit; $index ++ ) {
     683                        $character = $source[ $index ];
     684                        $next      = $index + 1 < $limit ? $source[ $index + 1 ] : '';
     685
     686                        if ( $line_comment ) {
     687                                if ( "\n" === $character || "\r" === $character ) {
     688                                        $line_comment = false;
     689                                }
     690                                continue;
     691                        }
     692
     693                        if ( $block_comment ) {
     694                                if ( '*' === $character && '/' === $next ) {
     695                                        $block_comment = false;
     696                                        $index ++;
     697                                }
     698                                continue;
     699                        }
     700
     701                        if ( '' !== $quote ) {
     702                                if ( $escaped ) {
     703                                        $escaped = false;
     704                                } elseif ( '\\' === $character ) {
     705                                        $escaped = true;
     706                                } elseif ( $quote === $character ) {
     707                                        $quote = '';
     708                                }
     709                                continue;
     710                        }
     711
     712                        if ( '/' === $character && '/' === $next ) {
     713                                $line_comment = true;
     714                                $index ++;
     715                        } elseif ( '/' === $character && '*' === $next ) {
     716                                $block_comment = true;
     717                                $index ++;
     718                        } elseif ( '"' === $character || "'" === $character || '`' === $character ) {
     719                                $quote = $character;
     720                        } elseif ( '{' === $character ) {
     721                                $depth ++;
     722                        } elseif ( '}' === $character ) {
     723                                $depth --;
     724                        }
     725                }
     726
     727                // A regex candidate inside a quoted payload is not an object member even
     728                // when the surrounding object's brace depth happens to be one.
     729                return '' === $quote && ! $line_comment && ! $block_comment ? $depth : -1;
     730        }
     731
     732        /**
     733         * Return one balanced JavaScript object starting at an opening brace.
     734         *
     735         * Builder frontend data is emitted as a JSON-compatible object. Tracking
     736         * quoted strings keeps braces within values from terminating the assignment.
     737         *
     738         * @param string $body Page body.
     739         * @param int    $opening_brace Opening-brace byte offset.
     740         *
     741         * @return string
     742         */
     743        private function get_javascript_object_at_offset( $body, $opening_brace ) {
     744                $length  = strlen( $body );
     745                $depth   = 0;
     746                $quote   = '';
     747                $escaped = false;
     748
     749                if ( $opening_brace < 0 || $opening_brace >= $length || '{' !== $body[ $opening_brace ] ) {
     750                        return '';
     751                }
     752
     753                for ( $index = $opening_brace; $index < $length; $index ++ ) {
     754                        $character = $body[ $index ];
     755
     756                        if ( '' !== $quote ) {
     757                                if ( $escaped ) {
     758                                        $escaped = false;
     759                                } elseif ( '\\' === $character ) {
     760                                        $escaped = true;
     761                                } elseif ( $quote === $character ) {
     762                                        $quote = '';
     763                                }
     764                                continue;
     765                        }
     766
     767                        if ( '"' === $character || "'" === $character ) {
     768                                $quote = $character;
     769                        } elseif ( '{' === $character ) {
     770                                $depth ++;
     771                        } elseif ( '}' === $character ) {
     772                                $depth --;
     773                                if ( 0 === $depth ) {
     774                                        return substr( $body, $opening_brace, $index - $opening_brace + 1 );
     775                                }
     776                        }
     777                }
     778
     779                return '';
    527780        }
    528781
    … …  
    544797                        $host  = is_array( $parts ) && ! empty( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : '';
    545798
    546                         if ( preg_match( '/^wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:onstatic|static\\d*)\\.studio$/', $host ) ) {
     799                        if ( preg_match( '/^wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:ls-static|onstatic|static\\d*)\\.studio$/', $host ) ) {
    547800                                return true;
    548801                        }
    … …  
    648901                }
    649902
    650                 $escaped_source      = addcslashes( untrailingslashit( $source_origin ), '/' );
    651903                $escaped_destination = addcslashes( untrailingslashit( $destination_url ), '/' );
    652                 $response_body       = str_ireplace( $escaped_source, $escaped_destination, $response_body );
    653                 $response_body       = str_ireplace( urlencode( $source_origin ), urlencode( $destination_url ), $response_body );
     904                // Match HTTPS, HTTP, and protocol-relative JSON strings without treating a
     905                // managed hostname as a prefix of an unrelated suffixed hostname.
     906                $escaped_pattern     = '~(?:(?:https?:)?\\\\/\\\\/)' . preg_quote( $authority, '~' ) . '(?=$|\\\\/|[/?#\s<>"\'])~i';
     907                $_result             = preg_replace_callback(
     908                        $escaped_pattern,
     909                        static function () use ( $escaped_destination ) {
     910                                return $escaped_destination;
     911                        },
     912                        $response_body
     913                );
     914                if ( null !== $_result ) {
     915                        $response_body = $_result;
     916                }
     917
     918                $encoded_authority = urlencode( $authority );
     919                // Encoded origins need the same authority boundary as plain and JSON URLs.
     920                $encoded_pattern   = '~(?:(?:https?%3A)?%2F%2F)' . preg_quote( $encoded_authority, '~' ) . '(?=$|%2F|%3F|%23|%22|%27|%20|%26|[/?#\s<>"\'])~i';
     921                $_result           = preg_replace_callback(
     922                        $encoded_pattern,
     923                        static function () use ( $destination_url ) {
     924                                return urlencode( $destination_url );
     925                        },
     926                        $response_body
     927                );
     928                if ( null !== $_result ) {
     929                        $response_body = $_result;
     930                }
    654931
    655932                $this->save_body( $response_body );
  • simply-static/tags/3.8.18/src/class-ss-util.php

    r3718928 r3750897  
    11891189         * origin_host() may include a subdirectory (for example example.com/wp).
    11901190         * Without a boundary, raw replacements can treat /wp-includes as though it
    1191          * started with the /wp base and leave broken suffixes like -includes.
     1191         * started with the /wp base and leave broken suffixes like -includes. Quotes
     1192         * and markup whitespace are valid boundaries when the URL is a complete
     1193         * JSON, JavaScript, or HTML string value.
    11921194         *
    11931195         * @param bool $encoded Whether the target text is URL-encoded.
    … …  
    12031205                        $pattern     = preg_quote( $encoded ? urlencode( $origin_host ) : $origin_host, '/' );
    12041206
    1205                         return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?#])' );
     1207                        return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?#\s<>"\'])' );
    12061208                }
    12071209
    … …  
    12221224                }
    12231225
    1224                 return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?#])';
     1226                return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?#\s<>"\'])';
    12251227        }
    12261228
    … …  
    12291231         *
    12301232         * WordPress often writes URLs in JSON as https:\/\/example.com\/wp\/path.
    1231          * A boundary prevents /wp from partially matching /wp-includes.
     1233         * A boundary prevents /wp from partially matching /wp-includes while still
     1234         * allowing an origin-only URL immediately followed by a closing quote.
    12321235         *
    12331236         * @return string Regex fragment.
    12341237         */
    12351238        public static function json_escaped_origin_url_pattern() {
    1236                 return preg_quote( addcslashes( untrailingslashit( self::origin_url() ), '/' ), '/' ) . '(?=$|\\\\\/|[?#])';
     1239                return preg_quote( addcslashes( untrailingslashit( self::origin_url() ), '/' ), '/' ) . '(?=$|\\\\\/|[\/?#\s<>"\'])';
    12371240        }
    12381241
  • simply-static/trunk/readme.txt

    r3734392 r3750897  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag:  3.8.17
     7Stable tag:  3.8.18
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    276276== Changelog ==
    277277
     278= 3.8.18 =
     279
     280* Fixed Oxygen and Breakdance home URLs left behind after Static Studio migrations
     281* Fixed bundled plugin assets when WordPress content directories use custom public names
     282
    278283= 3.8.17 =
    279284
  • simply-static/trunk/simply-static.php

    r3734392 r3750897  
    99 * Plugin URI:        https://simplystatic.com
    1010 * Description:       A static site generator to create fast and secure static versions of your WordPress website.
    11  * Version:           3.8.17
     11 * Version:           3.8.18
    1212 * Requires at least:  6.2
    1313 * Author:            Patrick Posner
    … …  
    2121define( 'SIMPLY_STATIC_PATH', plugin_dir_path( __FILE__ ) );
    2222define( 'SIMPLY_STATIC_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) );
    23 define( 'SIMPLY_STATIC_VERSION', '3.8.17' );
     23define( 'SIMPLY_STATIC_VERSION', '3.8.18' );
    2424
    2525/**
  • simply-static/trunk/src/class-ss-url-extractor.php

    r3734392 r3750897  
    487487         * The fetched page origin is included when WordPress does not already know
    488488         * about it. On Static Studio, also include managed WordPress aliases found in
    489          * the page. Migrations can leave a retired staticN.studio alias in post data
    490          * even though the page is now fetched through an onstatic.studio runtime.
     489         * the page. Migrations can leave a retired Static Studio alias in post data
     490         * even though the page is now fetched through a different managed runtime.
    491491         *
    492492         * @param string $source_origin Fetched page origin.
    … …  
    513513                // still operates on the original body and handles both representations.
    514514                $body = str_replace( '\\/', '/', $body );
    515                 $pattern = '~(?:https?:)?//(wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:onstatic|static\\d*)\\.studio)(?=$|[/?#\\s<>"\'])~i';
    516                 $matched = preg_match_all( $pattern, $body, $matches );
     515                // Retired onstatic/staticN aliases predate tenant-stable metadata and can
     516                // occur throughout migrated builder content.
     517                $retired_pattern = '~(?:https?:)?//(wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:onstatic|static\\d*)\\.studio)(?=$|[/?#\\s<>"\'])~i';
     518                $matched         = preg_match_all( $retired_pattern, $body, $matches );
     519                if ( false !== $matched && 0 !== $matched ) {
     520                        foreach ( $matches[1] as $host ) {
     521                                $origins[] = 'https://' . strtolower( $host );
     522                        }
     523                }
     524
     525                // Current ls-static aliases are valid tenant URLs. Limit their discovery to
     526                // the exact Oxygen/Breakdance configuration object which owns homeUrl; a
     527                // generic widget may legitimately point its own homeUrl at another tenant.
     528                foreach ( $this->get_builder_home_url_origins( $body ) as $builder_origin ) {
     529                        $origins[] = $builder_origin;
     530                }
     531
     532                return array_values( array_unique( array_filter( $origins ) ) );
     533        }
     534
     535        /**
     536         * Find managed homeUrl origins inside Oxygen and Breakdance assignments.
     537         *
     538         * @param string $body Page body with JSON slash escapes normalized.
     539         *
     540         * @return array
     541         */
     542        private function get_builder_home_url_origins( $body ) {
     543                $origins            = array();
     544                $assignment_pattern = '~(?:^|[^a-z0-9_$\\.])(?:window\\.)?(?:OxygenFrontend|BreakdanceFrontend)(\\s*\\.\\s*data)?\\s*=\\s*\\{~i';
     545                $matched            = preg_match_all( $assignment_pattern, $body, $assignments, PREG_SET_ORDER | PREG_OFFSET_CAPTURE );
    517546
    518547                if ( false === $matched || 0 === $matched ) {
    … …  
    520549                }
    521550
    522                 foreach ( $matches[1] as $host ) {
    523                         $origins[] = 'https://' . strtolower( $host );
    524                 }
    525 
    526                 return array_values( array_unique( array_filter( $origins ) ) );
     551                foreach ( $assignments as $assignment ) {
     552                        $assignment_text   = $assignment[0][0];
     553                        $assignment_offset = (int) $assignment[0][1];
     554                        $identifier_offset = 0;
     555                        if ( preg_match( '~(?:window\s*\.\s*)?(?:OxygenFrontend|BreakdanceFrontend)~i', $assignment_text, $identifier, PREG_OFFSET_CAPTURE ) ) {
     556                                $identifier_offset = (int) $identifier[0][1];
     557                        }
     558                        if ( ! $this->is_javascript_code_offset( $body, $assignment_offset + $identifier_offset ) ) {
     559                                continue;
     560                        }
     561                        $opening_brace     = $assignment_offset + strrpos( $assignment_text, '{' );
     562                        $object            = $this->get_javascript_object_at_offset( $body, $opening_brace );
     563
     564                        if ( '' === $object ) {
     565                                continue;
     566                        }
     567
     568                        $is_data_assignment = isset( $assignment[1][0] ) && '' !== trim( $assignment[1][0] );
     569                        $data_object        = $is_data_assignment ? $object : $this->get_top_level_javascript_object_property( $object, 'data' );
     570                        if ( '' === $data_object ) {
     571                                continue;
     572                        }
     573
     574                        foreach ( $this->get_top_level_managed_home_url_origins( $data_object ) as $home_url_origin ) {
     575                                $origins[] = $home_url_origin;
     576                        }
     577                }
     578
     579                return array_values( array_unique( $origins ) );
     580        }
     581
     582        /**
     583         * Confirm that a regex assignment candidate is executable script, not text.
     584         *
     585         * @param string $source Page or JavaScript source.
     586         * @param int    $offset Candidate byte offset.
     587         *
     588         * @return bool
     589         */
     590        private function is_javascript_code_offset( $source, $offset ) {
     591                $offset     = min( max( 0, (int) $offset ), strlen( $source ) );
     592                $scan_start = 0;
     593                $before     = substr( $source, 0, $offset );
     594                $script     = strripos( $before, '<script' );
     595                $end_script = strripos( $before, '</script' );
     596
     597                if ( false !== $script && ( false === $end_script || $script > $end_script ) ) {
     598                        $tag_end = strpos( $source, '>', $script );
     599                        if ( false === $tag_end || $tag_end >= $offset ) {
     600                                return false;
     601                        }
     602                        $scan_start = $tag_end + 1;
     603                }
     604
     605                $script_source = substr( $source, $scan_start, $offset - $scan_start );
     606
     607                return -1 !== $this->get_javascript_brace_depth_at_offset( $script_source, strlen( $script_source ) );
     608        }
     609
     610        /**
     611         * Get one top-level object-valued property from a JavaScript object.
     612         *
     613         * @param string $object JavaScript object source.
     614         * @param string $property Property name.
     615         *
     616         * @return string
     617         */
     618        private function get_top_level_javascript_object_property( $object, $property ) {
     619                $property_pattern = '~(?:["\']' . preg_quote( $property, '~' ) . '["\']|(?<![a-z0-9_$])' . preg_quote( $property, '~' ) . '(?![a-z0-9_$]))\\s*:\\s*\\{~i';
     620                $matched          = preg_match_all( $property_pattern, $object, $properties, PREG_OFFSET_CAPTURE );
     621
     622                if ( false === $matched || 0 === $matched ) {
     623                        return '';
     624                }
     625
     626                foreach ( $properties[0] as $candidate ) {
     627                        if ( 1 !== $this->get_javascript_brace_depth_at_offset( $object, (int) $candidate[1] ) ) {
     628                                continue;
     629                        }
     630
     631                        $opening_brace = (int) $candidate[1] + strrpos( $candidate[0], '{' );
     632
     633                        return $this->get_javascript_object_at_offset( $object, $opening_brace );
     634                }
     635
     636                return '';
     637        }
     638
     639        /**
     640         * Get a managed homeUrl declared directly on a builder data object.
     641         *
     642         * @param string $data_object Builder data object source.
     643         *
     644         * @return array
     645         */
     646        private function get_top_level_managed_home_url_origins( $data_object ) {
     647                $origins          = array();
     648                $home_url_pattern = '~["\']homeUrl["\']\\s*:\\s*["\'](?:https?:)?//(wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.ls-static\\.studio)(?=$|[/?#\\s<>"\'])~i';
     649                $matched          = preg_match_all( $home_url_pattern, $data_object, $home_urls, PREG_SET_ORDER | PREG_OFFSET_CAPTURE );
     650
     651                if ( false === $matched || 0 === $matched ) {
     652                        return $origins;
     653                }
     654
     655                foreach ( $home_urls as $home_url ) {
     656                        if ( 1 !== $this->get_javascript_brace_depth_at_offset( $data_object, (int) $home_url[0][1] ) ) {
     657                                continue;
     658                        }
     659
     660                        $origins[] = 'https://' . strtolower( $home_url[1][0] );
     661                }
     662
     663                return array_values( array_unique( $origins ) );
     664        }
     665
     666        /**
     667         * Calculate object-brace depth at one byte offset, ignoring quoted strings.
     668         *
     669         * @param string $source JavaScript source.
     670         * @param int    $offset Byte offset.
     671         *
     672         * @return int
     673         */
     674        private function get_javascript_brace_depth_at_offset( $source, $offset ) {
     675                $depth         = 0;
     676                $quote         = '';
     677                $escaped       = false;
     678                $line_comment  = false;
     679                $block_comment = false;
     680                $limit         = min( max( 0, (int) $offset ), strlen( $source ) );
     681
     682                for ( $index = 0; $index < $limit; $index ++ ) {
     683                        $character = $source[ $index ];
     684                        $next      = $index + 1 < $limit ? $source[ $index + 1 ] : '';
     685
     686                        if ( $line_comment ) {
     687                                if ( "\n" === $character || "\r" === $character ) {
     688                                        $line_comment = false;
     689                                }
     690                                continue;
     691                        }
     692
     693                        if ( $block_comment ) {
     694                                if ( '*' === $character && '/' === $next ) {
     695                                        $block_comment = false;
     696                                        $index ++;
     697                                }
     698                                continue;
     699                        }
     700
     701                        if ( '' !== $quote ) {
     702                                if ( $escaped ) {
     703                                        $escaped = false;
     704                                } elseif ( '\\' === $character ) {
     705                                        $escaped = true;
     706                                } elseif ( $quote === $character ) {
     707                                        $quote = '';
     708                                }
     709                                continue;
     710                        }
     711
     712                        if ( '/' === $character && '/' === $next ) {
     713                                $line_comment = true;
     714                                $index ++;
     715                        } elseif ( '/' === $character && '*' === $next ) {
     716                                $block_comment = true;
     717                                $index ++;
     718                        } elseif ( '"' === $character || "'" === $character || '`' === $character ) {
     719                                $quote = $character;
     720                        } elseif ( '{' === $character ) {
     721                                $depth ++;
     722                        } elseif ( '}' === $character ) {
     723                                $depth --;
     724                        }
     725                }
     726
     727                // A regex candidate inside a quoted payload is not an object member even
     728                // when the surrounding object's brace depth happens to be one.
     729                return '' === $quote && ! $line_comment && ! $block_comment ? $depth : -1;
     730        }
     731
     732        /**
     733         * Return one balanced JavaScript object starting at an opening brace.
     734         *
     735         * Builder frontend data is emitted as a JSON-compatible object. Tracking
     736         * quoted strings keeps braces within values from terminating the assignment.
     737         *
     738         * @param string $body Page body.
     739         * @param int    $opening_brace Opening-brace byte offset.
     740         *
     741         * @return string
     742         */
     743        private function get_javascript_object_at_offset( $body, $opening_brace ) {
     744                $length  = strlen( $body );
     745                $depth   = 0;
     746                $quote   = '';
     747                $escaped = false;
     748
     749                if ( $opening_brace < 0 || $opening_brace >= $length || '{' !== $body[ $opening_brace ] ) {
     750                        return '';
     751                }
     752
     753                for ( $index = $opening_brace; $index < $length; $index ++ ) {
     754                        $character = $body[ $index ];
     755
     756                        if ( '' !== $quote ) {
     757                                if ( $escaped ) {
     758                                        $escaped = false;
     759                                } elseif ( '\\' === $character ) {
     760                                        $escaped = true;
     761                                } elseif ( $quote === $character ) {
     762                                        $quote = '';
     763                                }
     764                                continue;
     765                        }
     766
     767                        if ( '"' === $character || "'" === $character ) {
     768                                $quote = $character;
     769                        } elseif ( '{' === $character ) {
     770                                $depth ++;
     771                        } elseif ( '}' === $character ) {
     772                                $depth --;
     773                                if ( 0 === $depth ) {
     774                                        return substr( $body, $opening_brace, $index - $opening_brace + 1 );
     775                                }
     776                        }
     777                }
     778
     779                return '';
    527780        }
    528781
    … …  
    544797                        $host  = is_array( $parts ) && ! empty( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : '';
    545798
    546                         if ( preg_match( '/^wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:onstatic|static\\d*)\\.studio$/', $host ) ) {
     799                        if ( preg_match( '/^wp(?:-|\\.)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.(?:ls-static|onstatic|static\\d*)\\.studio$/', $host ) ) {
    547800                                return true;
    548801                        }
    … …  
    648901                }
    649902
    650                 $escaped_source      = addcslashes( untrailingslashit( $source_origin ), '/' );
    651903                $escaped_destination = addcslashes( untrailingslashit( $destination_url ), '/' );
    652                 $response_body       = str_ireplace( $escaped_source, $escaped_destination, $response_body );
    653                 $response_body       = str_ireplace( urlencode( $source_origin ), urlencode( $destination_url ), $response_body );
     904                // Match HTTPS, HTTP, and protocol-relative JSON strings without treating a
     905                // managed hostname as a prefix of an unrelated suffixed hostname.
     906                $escaped_pattern     = '~(?:(?:https?:)?\\\\/\\\\/)' . preg_quote( $authority, '~' ) . '(?=$|\\\\/|[/?#\s<>"\'])~i';
     907                $_result             = preg_replace_callback(
     908                        $escaped_pattern,
     909                        static function () use ( $escaped_destination ) {
     910                                return $escaped_destination;
     911                        },
     912                        $response_body
     913                );
     914                if ( null !== $_result ) {
     915                        $response_body = $_result;
     916                }
     917
     918                $encoded_authority = urlencode( $authority );
     919                // Encoded origins need the same authority boundary as plain and JSON URLs.
     920                $encoded_pattern   = '~(?:(?:https?%3A)?%2F%2F)' . preg_quote( $encoded_authority, '~' ) . '(?=$|%2F|%3F|%23|%22|%27|%20|%26|[/?#\s<>"\'])~i';
     921                $_result           = preg_replace_callback(
     922                        $encoded_pattern,
     923                        static function () use ( $destination_url ) {
     924                                return urlencode( $destination_url );
     925                        },
     926                        $response_body
     927                );
     928                if ( null !== $_result ) {
     929                        $response_body = $_result;
     930                }
    654931
    655932                $this->save_body( $response_body );
  • simply-static/trunk/src/class-ss-util.php

    r3718928 r3750897  
    11891189         * origin_host() may include a subdirectory (for example example.com/wp).
    11901190         * Without a boundary, raw replacements can treat /wp-includes as though it
    1191          * started with the /wp base and leave broken suffixes like -includes.
     1191         * started with the /wp base and leave broken suffixes like -includes. Quotes
     1192         * and markup whitespace are valid boundaries when the URL is a complete
     1193         * JSON, JavaScript, or HTML string value.
    11921194         *
    11931195         * @param bool $encoded Whether the target text is URL-encoded.
    … …  
    12031205                        $pattern     = preg_quote( $encoded ? urlencode( $origin_host ) : $origin_host, '/' );
    12041206
    1205                         return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?#])' );
     1207                        return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?#\s<>"\'])' );
    12061208                }
    12071209
    … …  
    12221224                }
    12231225
    1224                 return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?#])';
     1226                return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?#\s<>"\'])';
    12251227        }
    12261228
    … …  
    12291231         *
    12301232         * WordPress often writes URLs in JSON as https:\/\/example.com\/wp\/path.
    1231          * A boundary prevents /wp from partially matching /wp-includes.
     1233         * A boundary prevents /wp from partially matching /wp-includes while still
     1234         * allowing an origin-only URL immediately followed by a closing quote.
    12321235         *
    12331236         * @return string Regex fragment.
    12341237         */
    12351238        public static function json_escaped_origin_url_pattern() {
    1236                 return preg_quote( addcslashes( untrailingslashit( self::origin_url() ), '/' ), '/' ) . '(?=$|\\\\\/|[?#])';
     1239                return preg_quote( addcslashes( untrailingslashit( self::origin_url() ), '/' ), '/' ) . '(?=$|\\\\\/|[\/?#\s<>"\'])';
    12371240        }
    12381241
Note: See TracChangeset for help on using the changeset viewer.