Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add icw cli command to fix insecure content URLs #39

Merged
merged 3 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
288 changes: 288 additions & 0 deletions includes/wp-cli/insecure-content-warning.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
<?php
/**
* Insecure content warning wp-cli commands.
*
* @package ICW
*/

namespace ICW\CLI;

use WP_CLI;
use WP_CLI\Utils;
use WP_CLI_Command;

/**
* Custom command to fix insecure content in post.
*
* ## EXAMPLES
*
* # Fix single post content.
* $ wp icw fix 42
* Checking post content...
* Success: Fixed 4/4 insecure URLs in post 42.
*
* # Fix multiple post content.
* $ wp icw fix --include=10,42
* Checking post content...
* Success: Fixed 4/4 insecure URLs in post 10.
* Success: Fixed 4/4 insecure URLs in post 42.
*
* # Fix all post content.
* $ wp icw fix --all
* Checking post content...
* Success: Fixed 4/4 insecure URLs in post 22.
* Warning: Fixed 3/4 insecure URLs in post 34.
*
* # Fix all page content.
* $ wp icw fix --all --post_type=page
* Checking post content...
* Success: Fixed 2/4 insecure URLs in post 96.
* Warning: Fixed 5/7 insecure URLs in post 49.
*
* @when after_wp_load
* @package ICW\CLI
*/
class InsecureContentWarning_CLI_Command extends \WP_CLI_Command {

/**
* Is current run a dry run?
*
* @var bool
*/
private $dry_run;

/**
* Fix insecure content.
*
* ## OPTIONS
*
* [<id>]
* : ID of post if fixing specific post content.
*
* [--include]
* : Comma separated IDs of post if fixing post content for multiple posts.
*
* [--all]
* : If set iterate through all post content, fix insecure media and report. This argument overrides targeted fix args.
*
* [--post_type]
* : The post type. Default 'post'.
*
* [--limit]
* : Count of posts to process in batch. Default 10.
*
* [--offset]
* : Allows skipping n number of posts. Default 0.
*
* [--dry-run]
* : Run the command without making updates to post contnet.
*
* ## EXAMPLES
*
* # Fix single post content.
* $ wp icw fix 42
* Checking post content...
* Success: Fixed 4/4 insecure URLs in post 42.
*
* # Fix multiple post content.
* $ wp icw fix --include=10,42
* Checking post content...
* Success: Fixed 4/4 insecure URLs in post 10.
* Success: Fixed 4/4 insecure URLs in post 42.
*
* # Fix all post content.
* $ wp icw fix --all
* Checking post content...
* Success: Fixed 4/4 insecure URLs in post 22.
* Warning: Fixed 3/4 insecure URLs in post 34.
*
* # Fix all page content.
* $ wp icw fix --all --post_type=page
* Checking post content...
* Success: Fixed 2/4 insecure URLs in post 96.
* Warning: Fixed 5/7 insecure URLs in post 49.
*
* @param array $args arguments.
* @param array $assoc_args associate arguments.
*/
public function fix( $args, $assoc_args ) {
$include = Utils\get_flag_value( $assoc_args, 'include', false );
$all = Utils\get_flag_value( $assoc_args, 'all', false );
$post_type = Utils\get_flag_value( $assoc_args, 'post_type', 'post' );
$posts_per_page = Utils\get_flag_value( $assoc_args, 'limit', 10 );
$post_offset = Utils\get_flag_value( $assoc_args, 'offset', 0 );
$this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false );

WP_CLI::log( 'Checking post content...' );

if ( false === $all && ! empty( $args[0] ) ) {
$this->success_or_failure( $this->fix_insecure_content( $args[0] ) );
} elseif ( false === $all && empty( $args ) && false !== $include ) {
$post_ids_list = explode( ',', $include );
foreach ( $post_ids_list as $icw_post_id ) {
$this->success_or_failure( $this->fix_insecure_content( $icw_post_id ) );
}
} else {
$total = 0;

// Loop through all posts and fix content.
while ( true ) {
$args = array(
'posts_per_page' => $posts_per_page,
'post_type' => $post_type,
'post_status' => 'publish',
'offset' => $post_offset,
);
$query = new \WP_Query( $args );

if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$this->success_or_failure( $this->fix_insecure_content( get_the_ID() ) );
}
} else {
break;
}

// Set offset for next page.
$post_offset += $posts_per_page;
$total += $query->post_count;

// Wait for a while before fixing the rest.
usleep( 1000 );
}
}
}

/**
* Fix insecure content for given Post ID.
*
* @param int $post_id Post ID.
*
* @return array|string
*/
protected function fix_insecure_content( $post_id ) {
$current_post = get_post( $post_id );

// Check and make sure post exists.
if ( empty( $current_post ) ) {
WP_CLI::warning( "Unable to fetch details for post ${post_id}" );

return '';
}

// Loop through post content and get HTTPS URLs wherever possible.
$post_content = $current_post->post_content;
$urls = $this->parse_content_for_insecure_urls( $post_content );
$count = 0;

// Check if post content has insecure URLs.
if ( empty( $urls ) ) {
WP_CLI::log( "No insecure content URL found in post ${post_id}" );

return '';
}

foreach ( $urls as $url ) {
if ( $this->does_secure_content_exist( $url ) ) {
$ssl_url = preg_replace( '/^http:/i', 'https:', $url );
$post_content = str_replace( $url, $ssl_url, $post_content );
$count ++;
}
}

if ( false === $this->dry_run ) {
// Update post content with HTTPS URLs.
wp_update_post(
array(
'ID' => $post_id,
'post_content' => $post_content,
)
);
}

if ( $this->dry_run ) {
return array(
'success',
sprintf( '%s/%s insecure URLs will be fixed in post %s.', $count, count( $urls ), $post_id ),
);
} else {
return array(
'success',
sprintf( '%s/%s insecure URLs fixed in post %s.', $count, count( $urls ), $post_id ),
);
}

}

/**
* Check if a SSL version of the URL exists.
*
* @param string $url URL to check for SSL version.
*
* @return bool
*/
protected function does_secure_content_exist( $url ) {
// Check if a https version of the URL exists.
$secure_version_exists = false;
$ssl = preg_replace( '/^http:/i', 'https:', $url );
$response = wp_remote_get( $ssl );
$response_code = wp_remote_retrieve_response_code( $response );

if ( 200 === $response_code ) {
$secure_version_exists = true;
}

return $secure_version_exists;
}

/**
* Create list of URLs.
*
* @param string $post_content Post Content.
*
* @return array
*/
protected function parse_content_for_insecure_urls( $post_content ) {
// Check all src and create an orray of URLs to check https URL availability.
$src_urls = array();
$src_regex = '/src="([^"]+)"/';
preg_match_all( $src_regex, $post_content, $matches, PREG_SET_ORDER, 0 );

// If we have matches, loop through and get clean URL.
if ( ! empty( $matches ) ) {
foreach ( $matches as $match ) {
$matched_url = $match[1];
$base_url = explode( '?', $matched_url )[0];
if ( 'https://' !== substr( $base_url, 0, 8 ) ) {
$src_urls[] = $base_url;
}
}
}

return $src_urls;
}

/**
* Display success or warning based on response; return proper exit code.
*
* @param array $response Response from processing insecure content.
*
* @return int $status
*/
protected function success_or_failure( $response ) {
if ( empty( $response ) ) {
return;
}

list( $type, $msg ) = $response;
if ( 'success' === $type ) {
WP_CLI::success( $msg );
} else {
WP_CLI::warning( $msg );
}
}
}

// Register migration command.
\WP_CLI::add_command( 'icw', __NAMESPACE__ . '\\InsecureContentWarning_CLI_Command' );
4 changes: 4 additions & 0 deletions insecure-content-warning.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@
require_once INSECURE_CONTENT_INC . 'assets.php';
require_once INSECURE_CONTENT_INC . 'rest.php';

if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once INSECURE_CONTENT_INC . 'wp-cli/insecure-content-warning.php';
}

Assets\setup();
Rest\setup();