From 85b55809a704c503020779ff0d76fb3d2a7b48b4 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 8 Mar 2023 12:06:48 +0800 Subject: [PATCH 1/8] Properly catch fatal PHP errors during REST call --- rest/rest-api.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rest/rest-api.php b/rest/rest-api.php index cd527007..b7b8da0b 100644 --- a/rest/rest-api.php +++ b/rest/rest-api.php @@ -2,6 +2,7 @@ namespace WPCOMVIP\ContentApi; +use Error; use Exception; use WP_Error; @@ -38,13 +39,20 @@ public static function get_block_content( $params ) { Analytics::record_usage(); + $parser_error = false; $parse_time_start = microtime( true ); try { $content_parser = new ContentParser(); $parser_results = $content_parser->parse( $post->post_content, $post_id ); } catch ( Exception $exception ) { - $error_message = sprintf( 'Error parsing post ID %d: %s', $post_id, $exception ); + $parser_error = $exception; + } catch ( Error $error ) { + $parser_error = $error; + } + + if ( $parser_error ) { + $error_message = sprintf( 'Error parsing post ID %d: %s', $post_id, $parser_error ); Analytics::record_error( $error_message ); $exception_data = ''; @@ -52,12 +60,12 @@ public static function get_block_content( $params ) { if ( ! $is_production_site && true === WP_DEBUG ) { $exception_data = [ - 'stack_trace' => explode( "\n", $exception->getTraceAsString() ), + 'stack_trace' => explode( "\n", $parser_error->getTraceAsString() ), ]; } // Early return to skip parse time check - return new WP_Error( 'vip-content-api-parser-error', $exception->getMessage(), $exception_data ); + return new WP_Error( 'vip-content-api-parser-error', $parser_error->getMessage(), $exception_data ); } $parse_time = microtime( true ) - $parse_time_start; From 80a8cc569ab394b4edc86f364de7e04952803c6e Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 8 Mar 2023 14:27:26 +0800 Subject: [PATCH 2/8] Add doctype to ensure HTML5 parser is used --- parser/content-parser.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/parser/content-parser.php b/parser/content-parser.php index 39d342ff..2b6815fb 100644 --- a/parser/content-parser.php +++ b/parser/content-parser.php @@ -104,8 +104,10 @@ protected function source_block( $block, $registered_blocks ) { } } - $crawler = new Crawler( $block['innerHTML'] ); - // Enter the automatically-inserted tag from parser + // Specify a manual doctype so that the parser will use the HTML5 parser + $crawler = new Crawler( sprintf( '%s', $block['innerHTML'] ) ); + + // Enter the tag for block parsing $crawler = $crawler->filter( 'body' ); $attribute_value = $this->source_attribute( $crawler, $block_attribute_definition ); From 72048ee87e052371ccd44d7ab6f09482afe8684c Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 8 Mar 2023 16:38:45 +0800 Subject: [PATCH 3/8] Add children attribute source support --- parser/content-parser.php | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/parser/content-parser.php b/parser/content-parser.php index 2b6815fb..72c2dcbb 100644 --- a/parser/content-parser.php +++ b/parser/content-parser.php @@ -182,6 +182,8 @@ protected function source_attribute( $crawler, $block_attribute_definition ) { $attribute_value = $this->source_block_raw( $crawler, $block_attribute_definition ); } elseif ( 'query' === $attribute_source ) { $attribute_value = $this->source_block_query( $crawler, $block_attribute_definition ); + } elseif ( 'children' === $attribute_source ) { + $attribute_value = $this->source_block_children( $crawler, $block_attribute_definition ); } elseif ( 'meta' === $attribute_source ) { $attribute_value = $this->source_block_meta( $block_attribute_definition ); } @@ -310,6 +312,62 @@ protected function source_block_query( $crawler, $block_attribute_definition ) { return $attribute_values; } + /** + * @param Symfony\Component\DomCrawler\Crawler $crawler + * @param array $block_attribute_definition + * + * @return string|null + */ + protected function source_block_children( $crawler, $block_attribute_definition ) { + // 'children' attribute usage was removed from core in 2018, but not officically deprecated until WordPress 6.1: + // https://github.com/WordPress/gutenberg/pull/44265 + // Parsing code for 'children' can be found in these places: + // https://github.com/WordPress/gutenberg/blob/dd0504b/packages/blocks/src/api/parser/get-block-attributes.js#L215-L216 + // https://github.com/WordPress/gutenberg/blob/dd0504b/packages/blocks/src/api/children.js#L149 + + $attribute_values = []; + $selector = $block_attribute_definition['selector'] ?? null; + + if ( null !== $selector ) { + $crawler = $crawler->filter( $selector ); + } + + if ( $crawler->count() === 0 ) { + // If the selector doesn't exist, return a default empty array + return $attribute_values; + } + + $children = $crawler->children(); + + if ( $children->count() === 0 ) { + // 'children' attributes can be a single element. In this case, return the element value in an array. + $attribute_values = [ + $crawler->html(), + ]; + } else { + // Use DOMDocument childNodes directly to preserve text nodes. $crawler->children() will return only + // element nodes and omit text content. + $children_nodes = $crawler->getNode( 0 )->childNodes; + + foreach ( $children_nodes as $node ) { + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- external API calls + if ( XML_ELEMENT_NODE === $node->nodeType ) { + $attribute_values[] = $node->ownerDocument->saveHtml( $node ); + } elseif ( XML_TEXT_NODE === $node->nodeType ) { + $text = trim( $node->nodeValue ); + + // Exclude whitespace-only nodes + if ( ! empty( $text ) ) { + $attribute_values[] = $text; + } + } + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + } + + return $attribute_values; + } + /** * @param Symfony\Component\DomCrawler\Crawler $crawler * @param array $block_attribute_definition From d7351b040d7745a49b9f14256bde401bd1e71f0d Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 8 Mar 2023 16:38:58 +0800 Subject: [PATCH 4/8] Add tests for children sources --- tests/parser/sources/test-source-children.php | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/parser/sources/test-source-children.php diff --git a/tests/parser/sources/test-source-children.php b/tests/parser/sources/test-source-children.php new file mode 100644 index 00000000..68afaca0 --- /dev/null +++ b/tests/parser/sources/test-source-children.php @@ -0,0 +1,149 @@ +register_block_with_attributes( 'test/custom-list-children', [ + 'steps' => [ + 'type' => 'array', + 'source' => 'children', + 'selector' => '.steps', + ], + ] ); + + $html = ' + +
    +
  • Step 1
  • +
  • Step 2
  • +
+ + '; + + $expected_blocks = [ + [ + 'name' => 'test/custom-list-children', + 'attributes' => [ + 'steps' => [ + '
  • Step 1
  • ', + '
  • Step 2
  • ', + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); + } + + public function test_parse_children__with_single_child() { + $this->register_block_with_attributes( 'test/custom-block-with-title', [ + 'title' => [ + 'type' => 'array', + 'source' => 'children', + 'selector' => 'h2', + ], + ] ); + + $html = ' + +
    +

    Block title

    +
    + + '; + + $expected_blocks = [ + [ + 'name' => 'test/custom-block-with-title', + 'attributes' => [ + 'title' => [ + 'Block title', + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); + } + + public function test_parse_children__with_mixed_nodes_and_text() { + $this->register_block_with_attributes( 'test/custom-block', [ + 'instructions' => [ + 'type' => 'array', + 'source' => 'children', + 'selector' => '.instructions', + ], + ] ); + + $html = ' + +
    +
    Preheat oven to 200 degrees
    +
    + + '; + + $expected_blocks = [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'instructions' => [ + 'Preheat oven to', + '200 degrees', + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); + } + + public function test_parse_children__with_default_value() { + $this->register_block_with_attributes( 'test/custom-block', [ + 'unused-value' => [ + 'type' => 'array', + 'source' => 'children', + 'selector' => '.unused-class', + ], + ] ); + + $html = ' + +

    Unrelated content

    + + '; + + $expected_blocks = [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'unused-value' => [], + ], + ], + ]; + + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); + } +} From d683832901b37ee1dd0f1cc6317b460b423a2355 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Thu, 9 Mar 2023 12:16:24 +0800 Subject: [PATCH 5/8] Add node source, change 'children' source to reflect DOM tree --- parser/content-parser.php | 175 +++++++++++++++++++++++++------------- 1 file changed, 118 insertions(+), 57 deletions(-) diff --git a/parser/content-parser.php b/parser/content-parser.php index 72c2dcbb..84858dc6 100644 --- a/parser/content-parser.php +++ b/parser/content-parser.php @@ -182,10 +182,12 @@ protected function source_attribute( $crawler, $block_attribute_definition ) { $attribute_value = $this->source_block_raw( $crawler, $block_attribute_definition ); } elseif ( 'query' === $attribute_source ) { $attribute_value = $this->source_block_query( $crawler, $block_attribute_definition ); - } elseif ( 'children' === $attribute_source ) { - $attribute_value = $this->source_block_children( $crawler, $block_attribute_definition ); } elseif ( 'meta' === $attribute_source ) { $attribute_value = $this->source_block_meta( $block_attribute_definition ); + } elseif ( 'node' === $attribute_source ) { + $attribute_value = $this->source_block_node( $crawler, $block_attribute_definition ); + } elseif ( 'children' === $attribute_source ) { + $attribute_value = $this->source_block_children( $crawler, $block_attribute_definition ); } if ( null === $attribute_value ) { @@ -312,6 +314,78 @@ protected function source_block_query( $crawler, $block_attribute_definition ) { return $attribute_values; } + /** + * @param Symfony\Component\DomCrawler\Crawler $crawler + * @param array $block_attribute_definition + * + * @return string|null + */ + protected function source_block_tag( $crawler, $block_attribute_definition ) { + // The only current usage of the 'tag' attribute is Gutenberg core is the 'core/table' block: + // https://github.com/WordPress/gutenberg/blob/796b800/packages/block-library/src/table/block.json#L39 + // Also see tag attribute parsing in Gutenberg: + // https://github.com/WordPress/gutenberg/blob/6517008/packages/blocks/src/api/parser/get-block-attributes.js#L225 + + $attribute_value = null; + $selector = $block_attribute_definition['selector'] ?? null; + + if ( null !== $selector ) { + $crawler = $crawler->filter( $selector ); + } + + if ( $crawler->count() > 0 ) { + $attribute_value = strtolower( $crawler->nodeName() ); + } + + return $attribute_value; + } + + /** + * @param Symfony\Component\DomCrawler\Crawler $crawler + * @param array $block_attribute_definition + * + * @return string|null + */ + protected function source_block_raw( $crawler, $block_attribute_definition ) { + // The only current usage of the 'raw' attribute in Gutenberg core is the 'core/html' block: + // https://github.com/WordPress/gutenberg/blob/6517008/packages/block-library/src/html/block.json#L13 + // Also see tag attribute parsing in Gutenberg: + // https://github.com/WordPress/gutenberg/blob/6517008/packages/blocks/src/api/parser/get-block-attributes.js#L131 + + $attribute_value = null; + + if ( $crawler->count() > 0 ) { + $attribute_value = trim( $crawler->html() ); + } + + return $attribute_value; + } + + /** + * @param Symfony\Component\DomCrawler\Crawler $crawler + * @param array $block_attribute_definition + * + * @return string|null + */ + protected function source_block_meta( $block_attribute_definition ) { + // 'meta' sources: + // https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#meta-source + + $post = get_post( $this->post_id ); + if ( null === $post ) { + return null; + } + + $meta_key = $block_attribute_definition['meta']; + $is_metadata_present = metadata_exists( 'post', $post->ID, $meta_key ); + + if ( ! $is_metadata_present ) { + return null; + } else { + return get_post_meta( $post->ID, $meta_key, true ); + } + } + /** * @param Symfony\Component\DomCrawler\Crawler $crawler * @param array $block_attribute_definition @@ -321,8 +395,7 @@ protected function source_block_query( $crawler, $block_attribute_definition ) { protected function source_block_children( $crawler, $block_attribute_definition ) { // 'children' attribute usage was removed from core in 2018, but not officically deprecated until WordPress 6.1: // https://github.com/WordPress/gutenberg/pull/44265 - // Parsing code for 'children' can be found in these places: - // https://github.com/WordPress/gutenberg/blob/dd0504b/packages/blocks/src/api/parser/get-block-attributes.js#L215-L216 + // Parsing code for 'children' sources can be found here: // https://github.com/WordPress/gutenberg/blob/dd0504b/packages/blocks/src/api/children.js#L149 $attribute_values = []; @@ -342,7 +415,7 @@ protected function source_block_children( $crawler, $block_attribute_definition if ( $children->count() === 0 ) { // 'children' attributes can be a single element. In this case, return the element value in an array. $attribute_values = [ - $crawler->html(), + $crawler->getNode( 0 )->nodeValue, ]; } else { // Use DOMDocument childNodes directly to preserve text nodes. $crawler->children() will return only @@ -350,18 +423,11 @@ protected function source_block_children( $crawler, $block_attribute_definition $children_nodes = $crawler->getNode( 0 )->childNodes; foreach ( $children_nodes as $node ) { - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- external API calls - if ( XML_ELEMENT_NODE === $node->nodeType ) { - $attribute_values[] = $node->ownerDocument->saveHtml( $node ); - } elseif ( XML_TEXT_NODE === $node->nodeType ) { - $text = trim( $node->nodeValue ); - - // Exclude whitespace-only nodes - if ( ! empty( $text ) ) { - $attribute_values[] = $text; - } + $node_value = $this->from_dom_node( $node ); + + if ( $node_value ) { + $attribute_values[] = $node_value; } - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } } @@ -374,11 +440,11 @@ protected function source_block_children( $crawler, $block_attribute_definition * * @return string|null */ - protected function source_block_tag( $crawler, $block_attribute_definition ) { - // The only current usage of the 'tag' attribute is Gutenberg core is the 'core/table' block: - // https://github.com/WordPress/gutenberg/blob/796b800/packages/block-library/src/table/block.json#L39 - // Also see tag attribute parsing in Gutenberg: - // https://github.com/WordPress/gutenberg/blob/6517008/packages/blocks/src/api/parser/get-block-attributes.js#L225 + protected function source_block_node( $crawler, $block_attribute_definition ) { + // 'node' attribute usage was removed from core in 2018, but not officically deprecated until WordPress 6.1: + // https://github.com/WordPress/gutenberg/pull/44265 + // Parsing code for 'node' sources can be found here: + // https://github.com/WordPress/gutenberg/blob/dd0504bd34c29b5b2824d82c8d2bb3a8d0f071ec/packages/blocks/src/api/node.js#L125 $attribute_value = null; $selector = $block_attribute_definition['selector'] ?? null; @@ -387,57 +453,52 @@ protected function source_block_tag( $crawler, $block_attribute_definition ) { $crawler = $crawler->filter( $selector ); } - if ( $crawler->count() > 0 ) { - $attribute_value = strtolower( $crawler->nodeName() ); - } - - return $attribute_value; - } - - /** - * @param Symfony\Component\DomCrawler\Crawler $crawler - * @param array $block_attribute_definition - * - * @return string|null - */ - protected function source_block_raw( $crawler, $block_attribute_definition ) { - // The only current usage of the 'raw' attribute in Gutenberg core is the 'core/html' block: - // https://github.com/WordPress/gutenberg/blob/6517008/packages/block-library/src/html/block.json#L13 - // Also see tag attribute parsing in Gutenberg: - // https://github.com/WordPress/gutenberg/blob/6517008/packages/blocks/src/api/parser/get-block-attributes.js#L131 + $node = $crawler->getNode( 0 ); + $node_value = null; - $attribute_value = null; + if ( $node ) { + $node_value = $this->from_dom_node( $node ); + } - if ( $crawler->count() > 0 ) { - $attribute_value = trim( $crawler->html() ); + if ( null !== $node_value ) { + $attribute_value = $node_value; } return $attribute_value; } /** - * @param Symfony\Component\DomCrawler\Crawler $crawler - * @param array $block_attribute_definition + * Helper function to process markup used by the deprecated 'node' and 'children' sources. + * These sources can return a representation of the DOM tree and bypass the $crawler to access DOMNodes directly. * - * @return string|null + * @param \DOMNode $node + * + * @return array|string|null */ - protected function source_block_meta( $block_attribute_definition ) { - // 'meta' sources: - // https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#meta-source + protected function from_dom_node( $node ) { + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- external API calls - $post = get_post( $this->post_id ); - if ( null === $post ) { - return null; - } + if ( XML_TEXT_NODE === $node->nodeType ) { + // For plain text nodes, return the text directly + $text = trim( $node->nodeValue ); - $meta_key = $block_attribute_definition['meta']; - $is_metadata_present = metadata_exists( 'post', $post->ID, $meta_key ); + // Exclude whitespace-only nodes + if ( ! empty( $text ) ) { + return $text; + } + } elseif ( XML_ELEMENT_NODE === $node->nodeType ) { + $children = array_map( [ $this, 'from_dom_node' ], iterator_to_array( $node->childNodes ) ); - if ( ! $is_metadata_present ) { - return null; + // For element nodes, recurse and return an array of child nodes + return [ + 'type' => $node->nodeName, + 'children' => array_filter( $children ), + ]; } else { - return get_post_meta( $post->ID, $meta_key, true ); + return null; } + + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } protected function add_missing_block_warning( $block_name ) { From 14d38470505e3b5025fc55fc323352f7fa5c0d07 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Thu, 9 Mar 2023 12:16:38 +0800 Subject: [PATCH 6/8] Update children source tests --- tests/parser/sources/test-source-children.php | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/parser/sources/test-source-children.php b/tests/parser/sources/test-source-children.php index 68afaca0..7d492c25 100644 --- a/tests/parser/sources/test-source-children.php +++ b/tests/parser/sources/test-source-children.php @@ -35,17 +35,27 @@ public function test_parse_children__with_list_elements() { 'name' => 'test/custom-list-children', 'attributes' => [ 'steps' => [ - '
  • Step 1
  • ', - '
  • Step 2
  • ', + [ + 'type' => 'li', + 'children' => [ + 'Step 1', + ], + ], + [ + 'type' => 'li', + 'children' => [ + 'Step 2', + ], + ], ], ], ], ]; - $content_parser = new ContentParser( $this->registry ); - $blocks = $content_parser->parse( $html ); - $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); - $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); } public function test_parse_children__with_single_child() { @@ -105,7 +115,12 @@ public function test_parse_children__with_mixed_nodes_and_text() { 'attributes' => [ 'instructions' => [ 'Preheat oven to', - '200 degrees', + [ + 'type' => 'strong', + 'children' => [ + '200 degrees', + ], + ], ], ], ], @@ -121,6 +136,7 @@ public function test_parse_children__with_default_value() { $this->register_block_with_attributes( 'test/custom-block', [ 'unused-value' => [ 'type' => 'array', + 'default' => [], 'source' => 'children', 'selector' => '.unused-class', ], From 3f7ad0e8198c66140908a5342bb85ddb958524a1 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Thu, 9 Mar 2023 12:16:47 +0800 Subject: [PATCH 7/8] Add basic node source test --- tests/parser/sources/test-source-node.php | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/parser/sources/test-source-node.php diff --git a/tests/parser/sources/test-source-node.php b/tests/parser/sources/test-source-node.php new file mode 100644 index 00000000..7e83b22a --- /dev/null +++ b/tests/parser/sources/test-source-node.php @@ -0,0 +1,51 @@ +register_block_with_attributes( 'test/custom-block', [ + 'description' => [ + 'type' => 'object', + 'source' => 'node', + 'selector' => '.description p', + ], + ] ); + + $html = ' + +
    +

    Description text

    +
    + + '; + + $expected_blocks = [ + [ + 'name' => 'test/custom-block', + 'attributes' => [ + 'description' => [ + 'type' => 'p', + 'children' => [ + 'Description text', + ], + ], + ], + ], + ]; + + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArraySubset( $expected_blocks, $blocks['blocks'], true ); + } +} From 475dec5b86fd3212dec37e31b1f15843f3468b27 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Thu, 9 Mar 2023 12:17:23 +0800 Subject: [PATCH 8/8] Reduce parse time error threshold --- rest/rest-api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/rest-api.php b/rest/rest-api.php index b7b8da0b..34a20620 100644 --- a/rest/rest-api.php +++ b/rest/rest-api.php @@ -8,7 +8,7 @@ defined( 'ABSPATH' ) || die(); -defined( 'WPCOMVIP__CONTENT_API__PARSE_TIME_ERROR_THRESHOLD_MS' ) || define( 'WPCOMVIP__CONTENT_API__PARSE_TIME_ERROR_THRESHOLD_MS', 1000 ); +defined( 'WPCOMVIP__CONTENT_API__PARSE_TIME_ERROR_THRESHOLD_MS' ) || define( 'WPCOMVIP__CONTENT_API__PARSE_TIME_ERROR_THRESHOLD_MS', 500 ); class RestApi { public static function init() {