Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ jobs:
- name: Setup Environment
run: |
rm composer.lock
npm run setup
npm run start
npm run composer -- install --no-security-blocking

- name: Test
run: npm run test
45 changes: 42 additions & 3 deletions lib/class-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ public function export( $args ) {
}

/**
* Read a JSON file containing the PHPDoc markup, convert it into WordPress posts, and insert into DB.
* Imports an exported parser document into WordPress.
*
* The command validates the parsed-file envelope before invoking the importer
* and preserves setup Blueprint object and list shapes while decoding JSON.
*
* @synopsis <file> [--quick] [--import-internal]
*
Expand All @@ -57,11 +60,47 @@ public function import( $args, $assoc_args ) {
exit;
}

$phpdoc = json_decode( $phpdoc, true );
if ( is_null( $phpdoc ) ) {
$phpdoc = json_decode( $phpdoc );
if ( JSON_ERROR_NONE !== json_last_error() ) {
WP_CLI::error( sprintf( "JSON in %1\$s can't be decoded :(", $file ) );
exit;
}
if ( ! is_array( $phpdoc ) ) {
WP_CLI::error( sprintf( 'JSON in %1$s must contain a top-level list of parsed files.', $file ) );
exit;
}
preserve_json_object_shapes( $phpdoc );

// The importer dereferences these fields before it can report malformed
// input. Validate the file envelope here so bad JSON data produces one
// actionable CLI error instead of array-offset warnings or a type error.
foreach ( $phpdoc as $index => $parsed_file ) {
if (
! is_array( $parsed_file ) ||
! isset( $parsed_file['path'], $parsed_file['file'] ) ||
! is_string( $parsed_file['path'] ) ||
'' === $parsed_file['path'] ||
! is_array( $parsed_file['file'] ) ||
! isset(
$parsed_file['file']['description'],
$parsed_file['file']['long_description'],
$parsed_file['file']['tags']
) ||
! is_string( $parsed_file['file']['description'] ) ||
! is_string( $parsed_file['file']['long_description'] ) ||
! is_array( $parsed_file['file']['tags'] ) ||
array_values( $parsed_file['file']['tags'] ) !== $parsed_file['file']['tags']
) {
WP_CLI::error(
sprintf(
'JSON in %1$s entry %2$d must contain a parsed file object with a path and file metadata.',
$file,
$index + 1
)
);
exit;
}
}

// Import data
$this->_do_import( $phpdoc, isset( $assoc_args['quick'] ), isset( $assoc_args['import-internal'] ) );
Expand Down
90 changes: 90 additions & 0 deletions lib/class-file-reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,77 @@ class File_Reflector extends FileReflector {
*/
protected $last_doc = null;

/**
* Whether complete fence bodies were blanked before parsing the file DocBlock.
*
* @var bool
*/
protected $docblock_was_sanitized = false;

/**
* Indicates whether complete file-DocBlock fence bodies were blanked.
*
* @return bool Whether phpDocumentor parsed a sanitized comment.
*/
public function wasDocBlockSanitized() {
return $this->docblock_was_sanitized;
}

/**
* Lets phpDocumentor identify a file DocBlock without parsing fenced PHP as tags.
*
* `FileReflector` consumes the file comment before visiting its nodes, so
* `export_docblock()` cannot sanitize it later. Complete fence bodies are
* temporarily blanked for the parent traversal and restored afterward; the
* untouched file contents remain available for snippet extraction.
*
* @param PHPParser_Node[] $nodes
*
* @return PHPParser_Node[]
*/
public function beforeTraverse( array $nodes ) {
$source_docblock = null;
$docblock = null;

foreach ( $nodes as $node ) {
if ( $node instanceof \PhpParser\Node\Stmt\InlineHTML ) {
continue;
}

foreach ( (array) $node->getAttribute( 'comments' ) as $comment ) {
if ( $comment instanceof \PhpParser\Comment\Doc ) {
$docblock = $comment;
$source_docblock = (string) $comment;
break;
}
}
break;
}

if ( null !== $source_docblock && false !== strpos( $source_docblock, '```' ) ) {
$sanitized_docblock = sanitize_docblock_fenced_contents( $source_docblock );
if ( $sanitized_docblock !== $source_docblock ) {
$docblock->setText( $sanitized_docblock );
$this->docblock_was_sanitized = true;
}
}

try {
$nodes = parent::beforeTraverse( $nodes );
} catch ( \Exception $exception ) {
if ( null !== $source_docblock ) {
$docblock->setText( $source_docblock );
}
throw $exception;
}

if ( null !== $source_docblock ) {
$docblock->setText( $source_docblock );
}

return $nodes;
}

/**
* Add hooks to the queue and update the node stack when we enter a node.
*
Expand Down Expand Up @@ -144,6 +215,25 @@ public function leaveNode( \PHPParser_Node $node ) {
parent::leaveNode( $node );

switch ( $node->getType() ) {
case 'Stmt_Property':
/*
* PropertyReflector::getDocBlock() reads the declaration node, while
* getNode() returns one PropertyProperty child. Copy the declaration's
* comment to each child after it has been visited so export_docblock() can
* recover the raw fenced text through getNode(). Copying it earlier would
* make enterNode() queue the non-documentable child's comment for the next
* hook.
*/
$docblock = $node->getDocComment();
if ( $docblock ) {
foreach ( $node->props as $property ) {
$comments = (array) $property->getAttribute( 'comments' );
$comments[] = $docblock;
$property->setAttribute( 'comments', $comments );
}
}
break;

case 'Stmt_Class':
$class = end( $this->classes );
if ( ! empty( $this->method_uses_queue ) ) {
Expand Down
8 changes: 8 additions & 0 deletions lib/class-importer.php
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,14 @@ public function import_item( array $data, $parent_post_id = 0, $import_ignored =
$anything_updated[] = update_post_meta( $post_id, '_wp-parser_line_num', (string) $data['line'] );
$anything_updated[] = update_post_meta( $post_id, '_wp-parser_end_line_num', (string) $data['end_line'] );
$anything_updated[] = update_post_meta( $post_id, '_wp-parser_tags', $data['doc']['tags'] );

// Metadata APIs unslash their input. map_deep() reaches retained JSON
// objects as well as arrays, preserving backslashes in PHP and Blueprint
// source where wp_slash() alone would leave object properties untouched.
$code_snippets = isset( $data['doc']['code_snippets'] ) ? $data['doc']['code_snippets'] : array();
$setup_blueprints = isset( $data['doc']['setup_blueprints'] ) ? $data['doc']['setup_blueprints'] : array();
$anything_updated[] = update_post_meta( $post_id, '_wp-parser_code_snippets', map_deep( $code_snippets, 'wp_slash' ) );
$anything_updated[] = update_post_meta( $post_id, '_wp-parser_setup_blueprints', map_deep( $setup_blueprints, 'wp_slash' ) );
$anything_updated[] = update_post_meta( $post_id, '_wp-parser_last_parsed_wp_version', $this->version );

// If the post didn't need to be updated, but meta or tax changed, update it to bump last modified.
Expand Down
Loading
Loading