<?php
use WP_CLI\Utils;
use WP_CLI\WpOrgApi;
/**
* Verifies core file integrity by comparing to published checksums.
*
* @package wp-cli
*/
class Checksum_Core_Command extends Checksum_Base_Command {
/**
* Whether or not to verify contents of the root directory.
*
* @var boolean
*/
private $include_root = false;
/**
* Files to exclude from the verification.
*
* @var array
*/
private $exclude_files = [];
/**
* Verifies WordPress files against WordPress.org's checksums.
*
* Downloads md5 checksums for the current version from WordPress.org, and
* compares those checksums against the currently installed files.
*
* For security, avoids loading WordPress when verifying checksums.
*
* If you experience issues verifying from this command, ensure you are
* passing the relevant `--locale` and `--version` arguments according to
* the values from the `Dashboard->Updates` menu in the admin area of the
* site.
*
* ## OPTIONS
*
* [--include-root]
* : Verify all files and folders in the root directory, and warn if any non-WordPress items are found.
*
* [--version=<version>]
* : Verify checksums against a specific version of WordPress.
*
* [--locale=<locale>]
* : Verify checksums against a specific locale of WordPress.
*
* [--insecure]
* : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
*
* [--exclude=<files>]
* : Exclude specific files from the checksum verification. Provide a comma-separated list of file paths.
*
* ## EXAMPLES
*
* # Verify checksums
* $ wp core verify-checksums
* Success: WordPress installation verifies against checksums.
*
* # Verify checksums for given WordPress version
* $ wp core verify-checksums --version=4.0
* Success: WordPress installation verifies against checksums.
*
* # Verify checksums for given locale
* $ wp core verify-checksums --locale=en_US
* Success: WordPress installation verifies against checksums.
*
* # Verify checksums for given locale
* $ wp core verify-checksums --locale=ja
* Warning: File doesn't verify against checksum: wp-includes/version.php
* Warning: File doesn't verify against checksum: readme.html
* Warning: File doesn't verify against checksum: wp-config-sample.php
* Error: WordPress installation doesn't verify against checksums.
*
* # Verify checksums and exclude files
* $ wp core verify-checksums --exclude="readme.html"
* Success: WordPress installation verifies against checksums.
*
* @when before_wp_load
*/
public function __invoke( $args, $assoc_args ) {
$wp_version = '';
$locale = '';
if ( ! empty( $assoc_args['version'] ) ) {
$wp_version = $assoc_args['version'];
}
if ( ! empty( $assoc_args['locale'] ) ) {
$locale = $assoc_args['locale'];
}
if ( ! empty( $assoc_args['include-root'] ) ) {
$this->include_root = true;
}
if ( ! empty( $assoc_args['exclude'] ) ) {
$this->exclude_files = explode( ',', Utils\get_flag_value( $assoc_args, 'exclude', '' ) );
}
if ( empty( $wp_version ) ) {
$details = self::get_wp_details();
$wp_version = $details['wp_version'];
if ( empty( $locale ) ) {
$locale = $details['wp_local_package'];
}
}
$insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false );
$wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] );
try {
$checksums = $wp_org_api->get_core_checksums( $wp_version, empty( $locale ) ? 'en_US' : $locale );
} catch ( Exception $exception ) {
WP_CLI::error( $exception );
}
if ( ! is_array( $checksums ) ) {
WP_CLI::error( "Couldn't get checksums from WordPress.org." );
}
$has_errors = false;
foreach ( $checksums as $file => $checksum ) {
// Skip files which get updated
if ( 'wp-content' === substr( $file, 0, 10 ) ) {
continue;
}
if ( in_array( $file, $this->exclude_files, true ) ) {
continue;
}
if ( ! file_exists( ABSPATH . $file ) ) {
WP_CLI::warning( "File doesn't exist: {$file}" );
$has_errors = true;
continue;
}
$md5_file = md5_file( ABSPATH . $file );
if ( $md5_file !== $checksum ) {
WP_CLI::warning( "File doesn't verify against checksum: {$file}" );
$has_errors = true;
}
}
$core_checksums_files = array_filter( array_keys( $checksums ), [ $this, 'filter_file' ] );
$core_files = $this->get_files( ABSPATH );
$additional_files = array_diff( $core_files, $core_checksums_files );
if ( ! empty( $additional_files ) ) {
foreach ( $additional_files as $additional_file ) {
if ( in_array( $additional_file, $this->exclude_files, true ) ) {
continue;
}
WP_CLI::warning( "File should not exist: {$additional_file}" );
}
}
if ( ! $has_errors ) {
WP_CLI::success( 'WordPress installation verifies against checksums.' );
} else {
WP_CLI::error( "WordPress installation doesn't verify against checksums." );
}
}
/**
* Whether to include the file in the verification or not.
*
* @param string $filepath Path to a file.
*
* @return bool
*/
protected function filter_file( $filepath ) {
if ( true === $this->include_root ) {
return ( 1 !== preg_match( '/^(\.htaccess$|\.maintenance$|wp-config\.php$|wp-content\/)/', $filepath ) );
}
return ( 0 === strpos( $filepath, 'wp-admin/' )
|| 0 === strpos( $filepath, 'wp-includes/' )
|| 1 === preg_match( '/^wp-(?!config\.php)([^\/]*)$/', $filepath )
);
}
/**
* Gets version information from `wp-includes/version.php`.
*
* @return array {
* @type string $wp_version The WordPress version.
* @type int $wp_db_version The WordPress DB revision.
* @type string $tinymce_version The TinyMCE version.
* @type string $wp_local_package The TinyMCE version.
* }
*/
private static function get_wp_details() {
$versions_path = ABSPATH . 'wp-includes/version.php';
if ( ! is_readable( $versions_path ) ) {
WP_CLI::error(
"This does not seem to be a WordPress install.\n" .
'Pass --path=`path/to/wordpress` or run `wp core download`.'
);
}
$version_content = file_get_contents( $versions_path, false, null, 6, 2048 );
$vars = [ 'wp_version', 'wp_db_version', 'tinymce_version', 'wp_local_package' ];
$result = [];
foreach ( $vars as $var_name ) {
$result[ $var_name ] = self::find_var( $var_name, $version_content );
}
return $result;
}
/**
* Searches for the value assigned to variable `$var_name` in PHP code `$code`.
*
* This is equivalent to matching the `\$VAR_NAME = ([^;]+)` regular expression and returning
* the first match either as a `string` or as an `integer` (depending if it's surrounded by
* quotes or not).
*
* @param string $var_name Variable name to search for.
* @param string $code PHP code to search in.
*
* @return int|string|null
*/
private static function find_var( $var_name, $code ) {
$start = strpos( $code, '$' . $var_name . ' = ' );
if ( ! $start ) {
return null;
}
$start = $start + strlen( $var_name ) + 3;
$end = strpos( $code, ';', $start );
$value = substr( $code, $start, $end - $start );
return trim( $value, " '" );
}
}