diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 4bfbd06261..656be8196e 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -12,6 +12,7 @@ use WordPressdotorg\Plugin_Directory\Tools; use WordPressdotorg\Plugin_Directory\Tools\Filesystem; use WordPressdotorg\Plugin_Directory\Tools\SVN; +use WordPressdotorg\Plugin_Directory\Tools\Tokenisation_Helpers; use WordPressdotorg\Plugin_Directory\Zip\Builder; /** @@ -497,6 +498,9 @@ public function import_from_svn( $plugin_slug, $svn_changed_tags = array( 'trunk delete_post_meta( $plugin->ID, 'dashboard_widget_name' ); foreach ( $dashboard_widgets as $widget_name ) { + if ( '' === $widget_name ) { + continue; + } add_post_meta( $plugin->ID, 'dashboard_widget_name', $widget_name, false ); } } else { @@ -1190,30 +1194,23 @@ static function find_blocks_in_file( $filename ) { } if ( 'php' === $ext ) { - // Parse a php-style register_block_type() call. - // Again this assumes literal strings, and only parses the name and title. + // Parse register_block_type() calls and `new WP_Block_Type()` constructor calls. + // Block names must be literal strings of the form "namespace/name"; the optional + // 'title' entry inside the second-arg options array is captured when present. $contents = file_get_contents( $filename ); - - // Search out register_block_type() calls. - if ( $contents && preg_match_all( "#register_block_type\s*[(]\s*['\"]([-\w]+/[-\w]+)['\"](?!\s*[.])#ms", $contents, $matches, PREG_SET_ORDER ) ) { - foreach ( $matches as $match ) { - $blocks[] = (object) [ - 'name' => $match[1], - 'title' => null, - ]; - } - } - - // Search out WP_Block_Type() instances. - if ( $contents && preg_match_all( "#new\s+WP_Block_Type\s*[(]\s*['\"]([-\w]+\/[-\w]+)['\"](?!\s*[.])(\s*,[^;]{0,500}['\"]title['\"]\s*=>\s*['\"]([^'\"]+)['\"](?!\s*[.]))?#ms", $contents, $matches, PREG_SET_ORDER ) ) { - foreach ( $matches as $match ) { - $blocks[] = (object) [ - 'name' => $match[1], - 'title' => $match[3] ?? null, - ]; + if ( $contents ) { + foreach ( array( 'register_block_type', 'new WP_Block_Type' ) as $needle ) { + foreach ( Tokenisation_Helpers::find_function_call_first_arg_and_array_value( $contents, $needle, 1, 'title' ) as $name => $title ) { + if ( ! preg_match( '#^[-\w]+/[-\w]+$#', $name ) ) { + continue; + } + $blocks[] = (object) array( + 'name' => $name, + 'title' => $title, + ); + } } } - } if ( 'block.json' === basename( $filename ) ) { @@ -1259,8 +1256,8 @@ function( $is_valid, $prop ) use ( $error ) { * Look for wp_add_dashboard_widget() calls within a single PHP file. * * The second argument is the widget label, often wrapped in __(), _x(), - * esc_html__(), etc. We extract the first quoted string literal from - * inside the second argument. + * esc_html__(), etc. The first literal-string value reachable through any + * such wrapping call is returned. * * @param string $filename Pathname of the file. * @return string[] List of widget label strings. @@ -1275,24 +1272,7 @@ public static function find_dashboard_widgets_in_file( $filename ) { return array(); } - $widgets = array(); - - // Match wp_add_dashboard_widget( , ). - if ( preg_match_all( - '#wp_add_dashboard_widget\s*\(\s*[^,]{1,200},\s*([^;]{1,500}?)(?:,|\))#ms', - $contents, - $matches, - PREG_SET_ORDER - ) ) { - foreach ( $matches as $match ) { - // Pull the first quoted string out of the second argument. - if ( preg_match( '#[\'"]([^\'"]+)[\'"]#', $match[1], $title_match ) ) { - $widgets[] = $title_match[1]; - } - } - } - - return array_unique( $widgets ); + return array_unique( Tokenisation_Helpers::find_function_call_arg_strings( $contents, 'wp_add_dashboard_widget', 1 ) ); } /** diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/Tokenisation_Helpers_Test.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/Tokenisation_Helpers_Test.php new file mode 100644 index 0000000000..785d95e7b1 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/Tokenisation_Helpers_Test.php @@ -0,0 +1,400 @@ +assertSame( + array( 'Plain' ), + $this->find_args( "wp_add_dashboard_widget( 'id', 'Plain', 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_translation_wrapper_double_underscore() { + $this->assertSame( + array( 'Translated' ), + $this->find_args( "wp_add_dashboard_widget( 'id', __( 'Translated', 'td' ), 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_translation_wrapper_x() { + $this->assertSame( + array( 'Contextual' ), + $this->find_args( "wp_add_dashboard_widget( 'id', _x( 'Contextual', 'ctx', 'td' ), 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_translation_wrapper_esc_html() { + $this->assertSame( + array( 'Escaped' ), + $this->find_args( "wp_add_dashboard_widget( 'id', esc_html__( 'Escaped', 'td' ), 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_double_quoted_string() { + $this->assertSame( + array( 'Double' ), + $this->find_args( 'wp_add_dashboard_widget( "id", "Double", "cb" );', 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_escaped_single_quote_in_label() { + $this->assertSame( + array( "Bob's Widget" ), + $this->find_args( "wp_add_dashboard_widget( 'id', __( 'Bob\\'s Widget', 'td' ), 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_class_constant_id_with_inline_doc_comment() { + // Common pattern: class constant for the ID, doc comment between args, literal label. + $src = <<<'PHP' +wp_add_dashboard_widget( + My_Widget_Class::DASHBOARD_WIDGET_ID, + /** This is a comment */ + 'My Widget', + array( $instance, 'render' ) +); +PHP; + $this->assertSame( + array( 'My Widget' ), + $this->find_args( $src, 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_multiline_call() { + $src = "wp_add_dashboard_widget(\n\t'id',\n\t__( 'Multi', 'td' ),\n\t'cb'\n);"; + $this->assertSame( array( 'Multi' ), $this->find_args( $src, 'wp_add_dashboard_widget', 1 ) ); + } + + /** + * False-positive prevention: comments, strings, method calls, declarations. + */ + public function test_call_inside_line_comment_is_ignored() { + $this->assertSame( + array(), + $this->find_args( "// wp_add_dashboard_widget( 'id', 'X', 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_call_inside_block_comment_is_ignored() { + $this->assertSame( + array(), + $this->find_args( "/* wp_add_dashboard_widget( 'id', 'X', 'cb' ); */", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_call_inside_string_literal_is_ignored() { + $this->assertSame( + array(), + $this->find_args( "\$x = \"wp_add_dashboard_widget( 'id', 'X', 'cb' );\";", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_method_call_is_ignored() { + $this->assertSame( + array(), + $this->find_args( "\$obj->wp_add_dashboard_widget( 'id', 'X', 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_static_method_call_is_ignored() { + $this->assertSame( + array(), + $this->find_args( "Foo::wp_add_dashboard_widget( 'id', 'X', 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_function_declaration_is_ignored() { + $this->assertSame( + array(), + $this->find_args( 'function wp_add_dashboard_widget( $id, $name, $cb ) {}', 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_function_exists_argument_is_ignored() { + $this->assertSame( + array(), + $this->find_args( "if ( function_exists( 'wp_add_dashboard_widget' ) ) {}", 'wp_add_dashboard_widget', 1 ) + ); + } + + /** + * Calls without a literal at the target position still count: they yield + * an empty string so callers can detect that the function was used (and + * e.g. apply a section term) even when the label is not parseable. + */ + public function test_arg_with_only_variable_yields_empty_string() { + $this->assertSame( + array( '' ), + $this->find_args( "wp_add_dashboard_widget( 'id', \$label, 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_arg_with_only_class_constant_yields_empty_string() { + $this->assertSame( + array( '' ), + $this->find_args( "wp_add_dashboard_widget( 'id', Foo::LABEL, 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + /** + * Block-registration calls: register_block_type, new WP_Block_Type, namespaced. + */ + public function test_register_block_type_literal_name() { + $this->assertSame( + array( 'my-plugin/foo' ), + $this->find_args( "register_block_type( 'my-plugin/foo' );", 'register_block_type', 0 ) + ); + } + + public function test_new_wp_block_type_constructor() { + $this->assertSame( + array( 'my-plugin/baz' ), + $this->find_args( "new WP_Block_Type( 'my-plugin/baz' );", 'new WP_Block_Type', 0 ) + ); + } + + public function test_register_block_type_with_leading_backslash() { + // Plugins occasionally call the global function via `\register_block_type(...)`. + $this->assertSame( + array( 'my-plugin/leading-slash' ), + $this->find_args( "\\register_block_type( 'my-plugin/leading-slash' );", 'register_block_type', 0 ) + ); + } + + /** + * Title-style metadata via find_function_call_first_arg_and_array_value(). + */ + public function test_array_value_long_array_form() { + $this->assertSame( + array( 'my-plugin/foo' => 'Foo Title' ), + $this->find_arg_and_array( + "register_block_type( 'my-plugin/foo', array( 'title' => 'Foo Title' ) );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + public function test_array_value_short_array_form() { + $this->assertSame( + array( 'my-plugin/bar' => 'Bar Title' ), + $this->find_arg_and_array( + "register_block_type( 'my-plugin/bar', [ 'title' => 'Bar Title' ] );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + public function test_array_value_with_translation_wrapper() { + $this->assertSame( + array( 'my-plugin/wrap' => 'Translated Title' ), + $this->find_arg_and_array( + "new WP_Block_Type( 'my-plugin/wrap', array( 'title' => __( 'Translated Title', 'td' ) ) );", + 'new WP_Block_Type', + 1, + 'title' + ) + ); + } + + public function test_array_value_with_other_keys_present() { + $this->assertSame( + array( 'my-plugin/multi' => 'Real Title' ), + $this->find_arg_and_array( + "register_block_type( 'my-plugin/multi', array( 'category' => 'widgets', 'title' => 'Real Title', 'icon' => 'star' ) );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + public function test_array_value_missing_key_yields_null() { + $this->assertSame( + array( 'my-plugin/no-title' => null ), + $this->find_arg_and_array( + "register_block_type( 'my-plugin/no-title', array( 'category' => 'widgets' ) );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + public function test_array_value_with_no_options_array_yields_null() { + $this->assertSame( + array( 'my-plugin/bare' => null ), + $this->find_arg_and_array( + "register_block_type( 'my-plugin/bare' );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + public function test_array_value_with_variable_value_yields_null() { + $this->assertSame( + array( 'my-plugin/dyn' => null ), + $this->find_arg_and_array( + "register_block_type( 'my-plugin/dyn', array( 'title' => \$dynamic ) );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + public function test_array_value_skips_calls_with_non_literal_first_arg() { + // First arg must be literal for this method to emit an entry. + $this->assertSame( + array(), + $this->find_arg_and_array( + "register_block_type( \$name, array( 'title' => 'X' ) );", + 'register_block_type', + 1, + 'title' + ) + ); + } + + /** + * Shortcut (to reduce complexity): a concatenated literal-plus-expression is + * captured as the leading literal alone. We do not detect that the value is + * actually composed of multiple parts. Consumers that require a clean literal + * must validate the captured value (e.g. block detection requires `\w+/\w+`). + */ + public function test_shortcut_concatenated_literal_returns_leading_part() { + $this->assertSame( + array( 'prefix-' ), + $this->find_args( "register_block_type( 'prefix-' . \$name );", 'register_block_type', 0 ) + ); + } + + /** + * Shortcut (to reduce complexity): only `\register_block_type` (T_NAME_FULLY_QUALIFIED + * with a leading backslash) is treated as the global function. Calls inside an + * arbitrary namespace, like `Foo\Bar\register_block_type(...)`, are NOT matched + * — they are assumed to be unrelated functions that just happen to share the name. + */ + public function test_shortcut_namespaced_call_is_not_matched() { + $this->assertSame( + array(), + $this->find_args( "Foo\\Bar\\register_block_type( 'ns/inside' );", 'register_block_type', 0 ) + ); + } + + /** + * Shortcut (to reduce complexity): the helper takes the FIRST literal-string + * reachable inside the target arg position, regardless of how it nests. A call + * that wraps something other than a translation function (e.g. a method call + * that happens to take a string literal) will still produce a value — we do + * not validate that the surrounding wrapper is a known i18n helper. + */ + public function test_shortcut_arbitrary_wrapping_call_still_captures_inner_literal() { + $this->assertSame( + array( 'Inner' ), + $this->find_args( "wp_add_dashboard_widget( 'id', \$obj->method( 'Inner' ), 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + /** + * Shortcut (to reduce complexity): when the target arg is an array literal, + * the helper's positional-string method returns the first inner string it + * finds — typically a key, not the value. Real callers don't pass arrays + * for the labels we extract; for the registration pattern (name + options + * array), use find_function_call_first_arg_and_array_value() instead. + */ + public function test_shortcut_array_arg_captures_first_inner_string() { + $this->assertSame( + array( 'title' ), + $this->find_args( "register_block_type( array( 'title' => 'Block Title' ) );", 'register_block_type', 0 ) + ); + } + + /** + * Multiple matches in one source. + */ + public function test_multiple_calls_in_one_file() { + // Each call yields an entry, even when the label arg has no literal — + // callers can still detect the function's presence in such cases. + $src = "wp_add_dashboard_widget( 'a', 'A', 'cb' );\n" + . "wp_add_dashboard_widget( 'b', __( 'B', 'td' ), 'cb' );\n" + . "wp_add_dashboard_widget( 'c', \$variable, 'cb' );\n" + . "wp_add_dashboard_widget( 'd', 'D', 'cb' );"; + $this->assertSame( + array( 'A', 'B', '', 'D' ), + $this->find_args( $src, 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_multiple_block_registrations_with_titles() { + $src = "register_block_type( 'p/a', array( 'title' => 'A' ) );\n" + . "register_block_type( 'p/b' );\n" + . "register_block_type( 'p/c', array( 'title' => __( 'C', 'td' ) ) );"; + $this->assertSame( + array( + 'p/a' => 'A', + 'p/b' => null, + 'p/c' => 'C', + ), + $this->find_arg_and_array( $src, 'register_block_type', 1, 'title' ) + ); + } + + public function test_returns_empty_for_no_matches() { + $this->assertSame( + array(), + $this->find_args( "do_something_else( 'foo' );", 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_invalid_php_returns_empty_array() { + $this->assertSame( + array(), + Tokenisation_Helpers::find_function_call_arg_strings( 'this is not php', 'wp_add_dashboard_widget', 1 ) + ); + } + + public function test_function_name_match_is_case_insensitive() { + // PHP function names are case-insensitive; mirror that. + $this->assertSame( + array( 'X' ), + $this->find_args( "WP_Add_Dashboard_Widget( 'id', 'X', 'cb' );", 'wp_add_dashboard_widget', 1 ) + ); + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tools/class-tokenisation-helpers.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tools/class-tokenisation-helpers.php new file mode 100644 index 0000000000..fc917d0899 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tools/class-tokenisation-helpers.php @@ -0,0 +1,274 @@ + value_at_$array_key ]`, where the value is the + * literal-string entry at `$array_key` inside the inline `array(...)`/`[...]` + * literal at `$array_arg_index`. The value is null when the arg is not an + * inline array, the key is missing, or the matched value is not a literal. + * + * Use this for the "registration call" pattern where the first arg is the + * identifier and a second-arg options array carries inline metadata, e.g. + * `register_block_type( 'foo/bar', array( 'title' => 'Foo' ) )`. + * + * @param string $contents PHP source code. + * @param string $function_name Function name to match. Prefix with `'new '` + * for a constructor (e.g. `'new WP_Block_Type'`). + * @param int $array_arg_index Zero-based index of the array argument. + * @param string $array_key Key to look up inside the array literal. + * @return array + */ + public static function find_function_call_first_arg_and_array_value( $contents, $function_name, $array_arg_index, $array_key ) { + $results = array(); + foreach ( self::walk_calls( $contents, $function_name ) as $args ) { + $name = self::first_literal( $args[0] ?? array() ); + if ( null === $name || '' === $name ) { + continue; + } + $results[ $name ] = self::array_string_value( $args[ $array_arg_index ] ?? array(), $array_key ); + } + return $results; + } + + /** + * Walk PHP tokens for calls to `$function_name` and yield each call's + * arg-list tokens, split into per-arg slices at top-level commas. + * + * @return array[] One entry per matched call: [ arg0_tokens, arg1_tokens, ... ]. + */ + private static function walk_calls( $contents, $function_name ) { + $tokens = @token_get_all( $contents ); + if ( ! $tokens ) { + return array(); + } + + $is_new = str_starts_with( $function_name, 'new ' ); + $needle = $is_new ? substr( $function_name, 4 ) : $function_name; + $global_form = '\\' . $needle; + $skip = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); + $out = array(); + $count = count( $tokens ); + + for ( $i = 0; $i < $count; $i++ ) { + $tok = $tokens[ $i ]; + if ( ! is_array( $tok ) ) { + continue; + } + $matches_simple = ( T_STRING === $tok[0] && 0 === strcasecmp( $tok[1], $needle ) ); + $matches_global = ( T_NAME_FULLY_QUALIFIED === $tok[0] && 0 === strcasecmp( $tok[1], $global_form ) ); + if ( ! $matches_simple && ! $matches_global ) { + continue; + } + + // Skip method/property access, function declarations, and constructor mismatches. + $prev_id = null; + for ( $p = $i - 1; $p >= 0; $p-- ) { + $pt = $tokens[ $p ]; + if ( is_array( $pt ) && in_array( $pt[0], $skip, true ) ) { + continue; + } + $prev_id = is_array( $pt ) ? $pt[0] : null; + break; + } + if ( in_array( $prev_id, array( T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_FUNCTION, T_NULLSAFE_OBJECT_OPERATOR ), true ) ) { + continue; + } + if ( $is_new ? T_NEW !== $prev_id : T_NEW === $prev_id ) { + continue; + } + + // Find the opening paren of the call. + $j = $i + 1; + while ( $j < $count && is_array( $tokens[ $j ] ) && in_array( $tokens[ $j ][0], $skip, true ) ) { + $j++; + } + if ( $j >= $count || '(' !== $tokens[ $j ] ) { + continue; + } + + // Collect arg-list tokens, splitting at top-level commas. + $args = array(); + $cur = array(); + $depth = 1; + for ( $k = $j + 1; $k < $count; $k++ ) { + $t = $tokens[ $k ]; + if ( is_array( $t ) ) { + if ( in_array( $t[0], $skip, true ) ) { + continue; + } + $cur[] = $t; + continue; + } + if ( '(' === $t || '[' === $t || '{' === $t ) { + $depth++; + $cur[] = $t; + continue; + } + if ( ')' === $t || ']' === $t || '}' === $t ) { + $depth--; + if ( 0 === $depth ) { + if ( $cur ) { + $args[] = $cur; + } + break; + } + $cur[] = $t; + continue; + } + if ( ',' === $t && 1 === $depth ) { + $args[] = $cur; + $cur = array(); + continue; + } + $cur[] = $t; + } + $out[] = $args; + $i = $j; + } + return $out; + } + + /** + * Return the unescaped value of the first `T_CONSTANT_ENCAPSED_STRING` in + * the given token list, regardless of nesting depth. null if none. + */ + private static function first_literal( array $tokens ) { + foreach ( $tokens as $t ) { + if ( is_array( $t ) && T_CONSTANT_ENCAPSED_STRING === $t[0] ) { + return self::unescape_string_token( $t[1] ); + } + } + return null; + } + + /** + * Inside an arg-token list that forms an `array(...)` or `[...]` literal, + * return the literal-string value of the entry whose key equals `$array_key`. + * null when the arg is not an inline array, the key is absent, or the value + * isn't a literal string (recursing through wrappers like `__()`). + */ + private static function array_string_value( array $tokens, $array_key ) { + $inner = self::array_inner_tokens( $tokens ); + if ( null === $inner ) { + return null; + } + + $depth = 0; + $count = count( $inner ); + for ( $i = 0; $i < $count; $i++ ) { + $t = $inner[ $i ]; + if ( ! is_array( $t ) ) { + if ( '(' === $t || '[' === $t || '{' === $t ) { + $depth++; + continue; + } + if ( ')' === $t || ']' === $t || '}' === $t ) { + $depth--; + continue; + } + continue; + } + if ( 0 !== $depth || T_CONSTANT_ENCAPSED_STRING !== $t[0] ) { + continue; + } + if ( self::unescape_string_token( $t[1] ) !== $array_key ) { + continue; + } + + // Found the key; the next significant token must be `=>`. + for ( $j = $i + 1; $j < $count; $j++ ) { + $u = $inner[ $j ]; + if ( is_array( $u ) && T_DOUBLE_ARROW === $u[0] ) { + // Capture the value tokens until the next top-level comma. + $value_tokens = array(); + $vd = 0; + for ( $k = $j + 1; $k < $count; $k++ ) { + $v = $inner[ $k ]; + if ( ! is_array( $v ) ) { + if ( '(' === $v || '[' === $v || '{' === $v ) { + $vd++; + } elseif ( ')' === $v || ']' === $v || '}' === $v ) { + $vd--; + } elseif ( ',' === $v && 0 === $vd ) { + break; + } + } + $value_tokens[] = $v; + } + return self::first_literal( $value_tokens ); + } + break; + } + } + return null; + } + + /** + * Return the inner tokens of an `array(...)` or `[...]` literal, or null + * when the given tokens are not such a literal. + */ + private static function array_inner_tokens( array $tokens ) { + $count = count( $tokens ); + if ( $count < 2 ) { + return null; + } + $first = $tokens[0]; + $last = $tokens[ $count - 1 ]; + if ( '[' === $first && ']' === $last ) { + return array_slice( $tokens, 1, $count - 2 ); + } + if ( $count >= 3 && is_array( $first ) && T_ARRAY === $first[0] && '(' === $tokens[1] && ')' === $last ) { + return array_slice( $tokens, 2, $count - 3 ); + } + return null; + } + + /** + * Decode a PHP string literal token (with surrounding quotes) to its value. + */ + private static function unescape_string_token( $literal ) { + if ( strlen( $literal ) < 2 ) { + return $literal; + } + $body = substr( $literal, 1, -1 ); + if ( "'" === $literal[0] ) { + return str_replace( array( "\\'", '\\\\' ), array( "'", '\\' ), $body ); + } + return stripcslashes( $body ); + } +}