[--output=] * wp hooktrail compare [--format=] [--fail-on-change] * wp hooktrail hooks [--format=] * * @package Hooktrail */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Record, compare and document WordPress hooks. */ class Hooktrail_CLI { /** * Record the hook map of a page on this site, as a logged-out visitor sees it. * * Requests the page once with a single-use token that switches recording * on for that request only, and saves the map as JSON. * * ## OPTIONS * * * : Page on this site, e.g. https://example.com/about/ or /about/. * * [--output=] * : Save the map here. Without it, a short summary is printed. * * [--insecure] * : Skip SSL certificate checks (for local sites with self-signed certificates). * * ## EXAMPLES * * wp hooktrail record / --output=home-before.json * * @param array $args Positional arguments. * @param array $assoc_args Named arguments. */ public function record( $args, $assoc_args ) { $url = (string) $args[0]; if ( 0 === strpos( $url, '/' ) ) { $url = home_url( $url ); } $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( wp_parse_url( $url, PHP_URL_HOST ) !== $home_host ) { WP_CLI::error( 'Only pages on this site can be recorded.' ); } $token = Hooktrail_Panel::create_cli_token(); $response = wp_remote_get( add_query_arg( Hooktrail_Panel::CLI_PARAM, $token, $url ), array( 'timeout' => 60, 'redirection' => 0, 'sslverify' => empty( $assoc_args['insecure'] ), 'headers' => array( 'Cache-Control' => 'no-cache' ), ) ); if ( is_wp_error( $response ) ) { WP_CLI::error( 'Request failed: ' . $response->get_error_message() ); } $map = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! Hooktrail_Diff::is_map( $map ) ) { WP_CLI::error( sprintf( 'No hook map came back (HTTP %d). Check that Hooktrail is active and the URL is a page on this site that is not served from a page cache.', (int) wp_remote_retrieve_response_code( $response ) ) ); } $summary = sprintf( '%s: %d hooks fired, %d callbacks, %d queries, %d PHP warnings (page status %d).', $url, $map['summary']['hooks'], $map['summary']['callbacks'], $map['summary']['queries'], $map['summary']['errors'], isset( $map['status'] ) ? (int) $map['status'] : 0 ); if ( ! empty( $assoc_args['output'] ) ) { $file = (string) $assoc_args['output']; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $filesystem = new WP_Filesystem_Direct( null ); if ( ! $filesystem->put_contents( $file, wp_json_encode( $map, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ) ) { WP_CLI::error( 'Could not write ' . $file ); } WP_CLI::success( $summary . ' Saved to ' . $file ); return; } WP_CLI::success( $summary ); } /** * Compare two hook maps. * * ## OPTIONS * * * : The earlier map (JSON). * * * : The later map (JSON). * * [--format=] * : Output format. * --- * default: text * options: * - text * - json * --- * * [--fail-on-change] * : Exit with status 1 if anything differs. Useful in deploy checks. * * ## EXAMPLES * * wp hooktrail compare home-before.json home-after.json --fail-on-change * * @param array $args Positional arguments. * @param array $assoc_args Named arguments. */ public function compare( $args, $assoc_args ) { $maps = array(); foreach ( array( $args[0], $args[1] ) as $file ) { if ( ! is_readable( $file ) ) { WP_CLI::error( 'Cannot read ' . $file ); } $map = json_decode( (string) file_get_contents( $file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local file the CLI user named. if ( ! Hooktrail_Diff::is_map( $map ) ) { WP_CLI::error( $file . ' is not a Hooktrail map.' ); } $maps[] = $map; } $diff = Hooktrail_Diff::compare( $maps[0], $maps[1] ); if ( isset( $assoc_args['format'] ) && 'json' === $assoc_args['format'] ) { WP_CLI::line( wp_json_encode( $diff, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } else { WP_CLI::line( Hooktrail_Diff::to_text( $diff ) ); } if ( Hooktrail_Diff::has_changes( $diff ) ) { if ( ! empty( $assoc_args['fail-on-change'] ) ) { WP_CLI::halt( 1 ); } WP_CLI::warning( 'The maps differ.' ); return; } WP_CLI::success( 'No differences.' ); } /** * List the hooks a plugin offers to other developers. * * ## OPTIONS * * * : Plugin folder name, e.g. woocommerce. * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * - csv * - markdown * --- * * ## EXAMPLES * * wp hooktrail hooks contact-form-7 --format=markdown > cf7-hooks.md * * @param array $args Positional arguments. * @param array $assoc_args Named arguments. */ public function hooks( $args, $assoc_args ) { $scanner = new Hooktrail_Hook_Scanner(); $result = $scanner->scan_plugin( $args[0] ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); } $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table'; if ( 'markdown' === $format ) { $choices = Hooktrail_Hook_Scanner::plugin_choices(); WP_CLI::line( Hooktrail_Hook_Scanner::to_markdown( $choices[ $args[0] ], $result ) ); return; } $rows = array(); foreach ( $result['hooks'] as $hook ) { $rows[] = array( 'hook' => $hook['hook'], 'type' => $hook['type'] . ( $hook['deprecated'] ? ' (deprecated)' : '' ), 'dynamic' => $hook['dynamic'] ? 'yes' : 'no', 'file' => $hook['file'] . ':' . $hook['line'], 'description' => $hook['doc'], ); } WP_CLI\Utils\format_items( $format, $rows, array( 'hook', 'type', 'dynamic', 'file', 'description' ) ); if ( 'table' === $format ) { WP_CLI::log( sprintf( '%d hooks in %d files.%s', count( $rows ), $result['files'], $result['truncated'] ? ' Stopped early: too many files.' : '' ) ); } } }