Changeset 3750897
- Timestamp:
- 10/16/2026 05:57:06 PM (4 weeks ago)
- Location:
- simply-static
- Files:
-
- 8 edited
- 1 copied
-
tags/3.8.18 (copied) (copied from simply-static/trunk )
-
tags/3.8.18/readme.txt (modified) (2 diffs)
-
tags/3.8.18/simply-static.php (modified) (2 diffs)
-
tags/3.8.18/src/class-ss-url-extractor.php (modified) (5 diffs)
-
tags/3.8.18/src/class-ss-util.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/simply-static.php (modified) (2 diffs)
-
trunk/src/class-ss-url-extractor.php (modified) (5 diffs)
-
trunk/src/class-ss-util.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
simply-static/tags/3.8.18/readme.txt
r3734392 r3750897 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 3.8.1 77 Stable tag: 3.8.18 8 8 License: GPLv2 or later 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 276 276 == Changelog == 277 277 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 278 283 = 3.8.17 = 279 284 -
simply-static/tags/3.8.18/simply-static.php
r3734392 r3750897 9 9 * Plugin URI: https://simplystatic.com 10 10 * Description: A static site generator to create fast and secure static versions of your WordPress website. 11 * Version: 3.8.1 711 * Version: 3.8.18 12 12 * Requires at least: 6.2 13 13 * Author: Patrick Posner … … 21 21 define( 'SIMPLY_STATIC_PATH', plugin_dir_path( __FILE__ ) ); 22 22 define( 'SIMPLY_STATIC_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) ); 23 define( 'SIMPLY_STATIC_VERSION', '3.8.1 7' );23 define( 'SIMPLY_STATIC_VERSION', '3.8.18' ); 24 24 25 25 /** -
simply-static/tags/3.8.18/src/class-ss-url-extractor.php
r3734392 r3750897 487 487 * The fetched page origin is included when WordPress does not already know 488 488 * 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 data490 * even though the page is now fetched through a n onstatic.studioruntime.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. 491 491 * 492 492 * @param string $source_origin Fetched page origin. … … 513 513 // still operates on the original body and handles both representations. 514 514 $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 ); 517 546 518 547 if ( false === $matched || 0 === $matched ) { … … 520 549 } 521 550 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 ''; 527 780 } 528 781 … … 544 797 $host = is_array( $parts ) && ! empty( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : ''; 545 798 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 ) ) { 547 800 return true; 548 801 } … … 648 901 } 649 902 650 $escaped_source = addcslashes( untrailingslashit( $source_origin ), '/' );651 903 $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 } 654 931 655 932 $this->save_body( $response_body ); -
simply-static/tags/3.8.18/src/class-ss-util.php
r3718928 r3750897 1189 1189 * origin_host() may include a subdirectory (for example example.com/wp). 1190 1190 * 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. 1192 1194 * 1193 1195 * @param bool $encoded Whether the target text is URL-encoded. … … 1203 1205 $pattern = preg_quote( $encoded ? urlencode( $origin_host ) : $origin_host, '/' ); 1204 1206 1205 return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?# ])' );1207 return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?#\s<>"\'])' ); 1206 1208 } 1207 1209 … … 1222 1224 } 1223 1225 1224 return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?# ])';1226 return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?#\s<>"\'])'; 1225 1227 } 1226 1228 … … 1229 1231 * 1230 1232 * 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. 1232 1235 * 1233 1236 * @return string Regex fragment. 1234 1237 */ 1235 1238 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<>"\'])'; 1237 1240 } 1238 1241 -
simply-static/trunk/readme.txt
r3734392 r3750897 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 3.8.1 77 Stable tag: 3.8.18 8 8 License: GPLv2 or later 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 276 276 == Changelog == 277 277 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 278 283 = 3.8.17 = 279 284 -
simply-static/trunk/simply-static.php
r3734392 r3750897 9 9 * Plugin URI: https://simplystatic.com 10 10 * Description: A static site generator to create fast and secure static versions of your WordPress website. 11 * Version: 3.8.1 711 * Version: 3.8.18 12 12 * Requires at least: 6.2 13 13 * Author: Patrick Posner … … 21 21 define( 'SIMPLY_STATIC_PATH', plugin_dir_path( __FILE__ ) ); 22 22 define( 'SIMPLY_STATIC_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) ); 23 define( 'SIMPLY_STATIC_VERSION', '3.8.1 7' );23 define( 'SIMPLY_STATIC_VERSION', '3.8.18' ); 24 24 25 25 /** -
simply-static/trunk/src/class-ss-url-extractor.php
r3734392 r3750897 487 487 * The fetched page origin is included when WordPress does not already know 488 488 * 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 data490 * even though the page is now fetched through a n onstatic.studioruntime.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. 491 491 * 492 492 * @param string $source_origin Fetched page origin. … … 513 513 // still operates on the original body and handles both representations. 514 514 $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 ); 517 546 518 547 if ( false === $matched || 0 === $matched ) { … … 520 549 } 521 550 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 ''; 527 780 } 528 781 … … 544 797 $host = is_array( $parts ) && ! empty( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : ''; 545 798 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 ) ) { 547 800 return true; 548 801 } … … 648 901 } 649 902 650 $escaped_source = addcslashes( untrailingslashit( $source_origin ), '/' );651 903 $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 } 654 931 655 932 $this->save_body( $response_body ); -
simply-static/trunk/src/class-ss-util.php
r3718928 r3750897 1189 1189 * origin_host() may include a subdirectory (for example example.com/wp). 1190 1190 * 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. 1192 1194 * 1193 1195 * @param bool $encoded Whether the target text is URL-encoded. … … 1203 1205 $pattern = preg_quote( $encoded ? urlencode( $origin_host ) : $origin_host, '/' ); 1204 1206 1205 return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?# ])' );1207 return $pattern . ( $encoded ? '(?=$|%2F|%3F|%23)' : '(?=$|[\/?#\s<>"\'])' ); 1206 1208 } 1207 1209 … … 1222 1224 } 1223 1225 1224 return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?# ])';1226 return preg_quote( $host, '/' ) . $port_pattern . preg_quote( $path, '/' ) . '(?=$|[\/?#\s<>"\'])'; 1225 1227 } 1226 1228 … … 1229 1231 * 1230 1232 * 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. 1232 1235 * 1233 1236 * @return string Regex fragment. 1234 1237 */ 1235 1238 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<>"\'])'; 1237 1240 } 1238 1241
Note:
See TracChangeset
for help on using the changeset viewer.