PluginProbe ʕ •ᴥ•ʔ
Easy HTTPS Redirection (SSL) / trunk
Easy HTTPS Redirection (SSL) vtrunk
trunk 1.5 1.6 1.8 1.9.1 1.9.2 2.0.0 2.0.1
https-redirection / classes / ehssl-non-https-resources-scan-update.php
https-redirection / classes Last commit date
utilities 3 days ago ehssl-config.php 1 year ago ehssl-cronjob.php 1 year ago ehssl-custom-post-types.php 1 year ago ehssl-debug-logger.php 1 year ago ehssl-email-handler.php 1 year ago ehssl-init-time-tasks.php 3 days ago ehssl-installation.php 3 days ago ehssl-non-https-resources-scan-result-table.php 3 days ago ehssl-non-https-resources-scan-update.php 3 days ago ehssl-rules-helper.php 3 days ago ehssl-ssl-certificate.php 1 year ago index.php 1 year ago
ehssl-non-https-resources-scan-update.php
698 lines
1 <?php
2
3 class EHSSL_Non_HTTPS_Resources_Scan_Update {
4
5 public $batch_size = 100;
6
7 public $scan_results = array();
8
9 public $post_types;
10
11 public $other_tables;
12
13 public $flags;
14
15 private $scan_type = 'scan_static_resources_only';
16
17 public function __construct() {
18 add_action( 'wp_ajax_ehssl_non_https_resources_scan', array( $this, 'handle_non_https_resources_scan' ) );
19 add_action( 'wp_ajax_ehssl_get_scanned_resources_table', array( $this, 'handle_get_non_https_resources_table' ) );
20 add_action( 'wp_ajax_ehssl_load_static_resources_table_page', array( $this, 'handle_load_non_https_resources_table_page' ) );
21 add_action( 'wp_ajax_ehssl_update_http_urls', array( $this, 'handle_update_http_urls' ) );
22 }
23
24 public function handle_non_https_resources_scan() {
25 if ( ! check_ajax_referer( 'ehssl_non_https_resources_scan_form_nonce', false, false ) ) {
26 wp_send_json_error(
27 array(
28 'message' => __( 'Nonce verification failed!', 'https-redirection' ),
29 )
30 );
31 }
32
33 $post_types = isset( $_POST['ehssl_post_types'] ) ? $_POST['ehssl_post_types'] : array();
34 $this->post_types = $post_types;
35
36 $other_tables = isset( $_POST['ehssl_other_tables'] ) ? $_POST['ehssl_other_tables'] : array();
37 $this->other_tables = $other_tables;
38
39 $flags = isset( $_POST['ehssl_additional_flags'] ) ? $_POST['ehssl_additional_flags'] : array();
40 $this->flags = $flags;
41
42 if ( isset( $_POST['ehssl_scan_type'] ) ) {
43 $this->scan_type = sanitize_text_field( $_POST['ehssl_scan_type'] );
44 }
45
46 $total = isset( $_POST['total'] ) ? json_decode( ( stripslashes( $_POST['total'] ) ), true ) : array();
47
48 $offset = isset( $_POST['offset'] ) ? intval( $_POST['offset'] ) : 0;
49
50 // Check if this is the initial request.
51 if ( $offset == 0 ) {
52 // Clear old data.
53 $this->clear_scan_results();
54
55 // Get the scannable records count.
56 $total = $this->count_scannable_items();
57 }
58
59 try {
60 if ( ! empty( $post_types ) ) {
61 $this->scan_post_types( $offset );
62 }
63
64 if ( ! empty( $other_tables ) && in_array( 'wp_options_table', $other_tables ) ) {
65 $this->scan_wp_options( $offset );
66 }
67
68 // Debug purpose only. Uncomment to inspect found matches.
69 //EHSSL_Logger::log( $this->scan_results );
70
71 if ( ! empty( $this->scan_results ) ) {
72 $this->save_scan_result();
73 }
74
75 $next_offset = $offset + $this->batch_size;
76
77 $response = array(
78 'message' => __( 'URLs scanned successfully!', 'https-redirection' ),
79 'completed' => $next_offset >= $total['ehssl_post_types'] && $next_offset >= $total['ehssl_other_tables'],
80 'processed' => $next_offset,
81 'total' => $total,
82 'next_offset' => $next_offset,
83 );
84
85 wp_send_json_success( $response );
86
87 } catch ( \Exception $e ) {
88 EHSSL_Logger::log( $e->getMessage(), 4 );
89 wp_send_json_error( array(
90 'message' => $e->getMessage(),
91 ) );
92 }
93 }
94
95 public function count_scannable_items() {
96 global $wpdb;
97
98 $result = array(
99 'ehssl_post_types' => 0,
100 'ehssl_other_tables' => 0,
101 );
102
103 try {
104 $post_types = isset( $_POST['ehssl_post_types'] ) ? $_POST['ehssl_post_types'] : array();
105 if ( ! empty( $post_types ) ) {
106 $post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
107 $query = $wpdb->prepare(
108 'SELECT COUNT(*) FROM ' . $wpdb->posts . ' WHERE post_type IN (' . $post_types_placeholders . ')',
109 array( ...$post_types ) );
110
111 $count = $wpdb->get_var( $query );
112 if ( ! empty( $count ) ) {
113 $result['ehssl_post_types'] = (int) $count;
114 }
115 }
116
117
118 $other_tables = isset( $_POST['ehssl_other_tables'] ) ? $_POST['ehssl_other_tables'] : array();
119 if ( ! empty( $other_tables ) ) {
120 $query = $wpdb->prepare( "SELECT COUNT(*) FROM " . $wpdb->options . " WHERE option_name NOT LIKE %s AND option_name NOT LIKE %s ",
121 array( '_transient_%', '_site_transient_%' ),
122 );
123 $count = $wpdb->get_var( $query );
124 if ( ! empty( $count ) ) {
125 $result['ehssl_other_tables'] = (int) $count;
126 }
127 }
128
129 } catch ( \Exception $e ) {
130 EHSSL_Logger::log( $e->getMessage(), 4);
131 }
132
133 return $result;
134 }
135
136 public function handle_get_non_https_resources_table() {
137 try {
138 self::render_http_scan_result_table();
139 wp_die();
140 } catch ( Exception $e ) {
141 wp_die( $e->getMessage() );
142 }
143 }
144
145 public function scan_post_types( $offset = 0 ) {
146 global $wpdb;
147
148 $post_types = $this->post_types;
149 $flags = $this->flags;
150 $limit = $this->batch_size;
151
152 $post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
153 $query = $wpdb->prepare( 'SELECT ID, post_content, post_excerpt
154 FROM ' . $wpdb->posts . '
155 WHERE post_type IN (' . $post_types_placeholders . ')
156 LIMIT %d OFFSET %d',
157 array( ...$post_types, $limit, $offset ) );
158
159 $posts = $wpdb->get_results( $query );
160
161 $post_table_columns = array( 'post_content', 'post_excerpt' );
162
163 foreach ( $posts as $post ) {
164 $matches = array();
165
166 foreach ( $post_table_columns as $p_col ) {
167 $content = $post->$p_col;
168 $scan_static_resources_only = $this->scan_type == 'scan_static_resources_only';
169 $this->find_matches( $content, $matches[$p_col], $scan_static_resources_only );
170 }
171
172 $cols_map = array();
173
174 foreach ( $matches as $column => $url_matches ) {
175 if ( ! empty( $url_matches[0] ) ) {
176 $cols_map[ $column ] = array_unique( $url_matches[0] );
177 }
178 }
179
180 $found_post_urls = array();
181 if ( ! empty( $cols_map ) ) {
182 $found_post_urls = array(
183 'source_table' => $wpdb->posts,
184 'source_uid' => $post->ID,
185 'cols_map' => $cols_map,
186 'meta_map' => array(),
187 );
188 }
189
190 if ( ! empty( $flags ) && in_array( 'include_post_meta', $flags ) ) {
191 // Scan post meta
192 $found_urls_in_meta = array();
193 $all_post_meta = get_post_meta( $post->ID );
194
195 foreach ( $all_post_meta as $meta_key => $meta_values ) {
196 foreach ( $meta_values as $meta_value ) {
197 // Convert arrays/objects into searchable text
198 if ( is_array( $meta_value ) || is_object( $meta_value ) ) {
199 $meta_value = wp_json_encode( $meta_value );
200 }
201
202 if ( ! is_string( $meta_value ) ) {
203 continue;
204 }
205
206 $scan_static_resources_only = $this->scan_type == 'scan_static_resources_only';
207 $this->find_matches( $meta_value, $meta_matches, $scan_static_resources_only);
208
209 if ( empty( $meta_matches[0] ) ) {
210 continue;
211 }
212
213 foreach ( array_unique( $meta_matches[0] ) as $url ) {
214 $found_urls_in_meta[] = array(
215 'url' => $url,
216 'meta_key' => $meta_key,
217 );
218 }
219 }
220 }
221
222 if ( ! empty( $found_urls_in_meta ) ) {
223 if ( empty( $found_post_urls ) ) {
224 $found_post_urls = array(
225 'source_table' => $wpdb->posts,
226 'source_uid' => $post->ID,
227 'cols_map' => array(),
228 'meta_map' => array(),
229 );
230 }
231
232 foreach ( $found_urls_in_meta as $item ) {
233 $found_post_urls['meta_map'][ $item['meta_key'] ][] = $item['url'];
234 }
235 }
236 }
237
238 if ( ! empty( $found_post_urls ) ) {
239 $this->scan_results[] = $found_post_urls;
240 }
241 }
242 }
243
244 public function scan_wp_options( $offset = 0 ) {
245 global $wpdb;
246
247 $limit = $this->batch_size;
248
249 $query = $wpdb->prepare( "SELECT option_name, option_value
250 FROM " . $wpdb->options . "
251 WHERE option_name NOT LIKE %s AND option_name NOT LIKE %s
252 LIMIT %d OFFSET %d",
253 array( '_transient_%', '_site_transient_%', $limit, $offset ),
254 );
255 $options = $wpdb->get_results( $query );
256
257 $table_columns = array( 'option_value' );
258
259 foreach ( $options as $option ) {
260 $matches = array();
261
262 foreach ( $table_columns as $col ) {
263 $content = $option->$col;
264 $scan_static_resources_only = $this->scan_type == 'scan_static_resources_only';
265 $this->find_matches( $content, $matches[$col], $scan_static_resources_only );
266 }
267
268 $cols_map = array();
269
270 foreach ( $matches as $column => $url_matches ) {
271 if ( ! empty( $url_matches[0] ) ) {
272 $cols_map[ $column ] = array_unique( $url_matches[0] );
273 }
274 }
275
276 if ( ! empty( $cols_map ) ) {
277 $this->scan_results[] = array(
278 'source_table' => $wpdb->options,
279 'source_uid' => $option->option_name,
280 'cols_map' => $cols_map,
281 'meta_map' => array(),
282 );
283 }
284 }
285 }
286
287 public function find_matches( $haystack, &$result, $scan_static_resources_only ) {
288 if ( ! $scan_static_resources_only ) {
289 // Match any non-http urls
290 preg_match_all(
291 '#http://[^\s"\'<>{}|\\\\^`]+#i',
292 $haystack,
293 $result
294 );
295
296 return;
297 }
298
299 /*
300 * Match URLs from:
301 * - img[src]
302 * - script[src]
303 * - link[href]
304 * - iframe[src]
305 * - source[src]
306 * - video[src]
307 * - audio[src]
308 * - object[data]
309 * - video[poster]
310 */
311 preg_match_all(
312 '/<(?:img|script|link|iframe|source|video|audio|object)\b[^>]*\b(?:src|href|data|poster)\s*=\s*["\']\Khttp:\/\/[^"\']+/i',
313 $haystack,
314 $result
315 );
316
317 /*
318 * Match URLs from srcset attributes.
319 */
320 preg_match_all(
321 '/\bsrcset\s*=\s*["\']([^"\']+)["\']/i',
322 $haystack,
323 $srcset_matches
324 );
325
326 foreach ( $srcset_matches[1] as $srcset ) {
327 preg_match_all(
328 '#http://[^\s,]+#i',
329 $srcset,
330 $matches
331 );
332
333
334 array_push( $result[0], ...$matches[0] );
335 }
336
337 /*
338 * Match URLs from CSS url(...)
339 * Examples:
340 * background-image:url(http://example.com/bg.jpg)
341 * background:url('http://example.com/bg.jpg')
342 */
343 preg_match_all(
344 '#url\s*\(\s*[\'"]?\Khttp://[^)"\'\s]+#i',
345 $haystack,
346 $css_urls
347 );
348
349 array_push( $result[0], ...$css_urls[0] );
350 }
351
352 public function save_scan_result() {
353 $new_results = $this->scan_results;
354
355 global $wpdb;
356
357 $placeholders = array();
358 $values = array();
359
360 foreach ( $new_results as $row ) {
361 $placeholders[] = '(%s,%s,%s,%s)';
362 array_push(
363 $values,
364 $row['source_table'],
365 $row['source_uid'],
366 isset( $row['cols_map'] ) ? serialize( $row['cols_map'] ) : array(),
367 isset( $row['meta_map'] ) ? serialize( $row['meta_map'] ) : array(),
368 );
369 }
370
371 $query = "INSERT INTO {$wpdb->prefix}ehssl_resource_scan_tbl (source_table, source_uid, cols_map, meta_map) VALUES " . implode( ',', $placeholders );
372
373 $query = $wpdb->prepare( $query, $values );
374
375 return $wpdb->query( $query );
376 }
377
378 public static function get_scan_results_count( $skip_fixed = false ) {
379 global $wpdb;
380 $query = "SELECT COUNT(*) FROM {$wpdb->prefix}ehssl_resource_scan_tbl";
381
382 if ( $skip_fixed ) {
383 $query .= " WHERE fixed != %d";
384 $query = $wpdb->prepare( $query, 1 );
385 }
386
387 $count = $wpdb->get_var( $query );
388
389 return (int) $count;
390 }
391
392 public static function get_scan_results_chunk( $offset = 0, $limit = 10, $where = array(), $skip_fixed = false ) {
393 global $wpdb;
394
395 $sql = "SELECT * FROM {$wpdb->prefix}ehssl_resource_scan_tbl";
396 $where_parts = array();
397 $prepare_args = array();
398
399 if ($skip_fixed) {
400 $where_parts[] = "fixed != 1";
401 }
402
403 foreach ( $where as $column => $ids ) {
404
405 $column = sanitize_key( $column );
406
407 if ( empty( $ids ) || ! is_array( $ids ) ) {
408 continue;
409 }
410
411 $ids = array_map( 'absint', $ids );
412 $ids = array_filter( $ids );
413
414 if ( empty( $ids ) ) {
415 continue;
416 }
417
418 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
419
420 $where_parts[] = "`{$column}` IN ({$placeholders})";
421
422 $prepare_args = array_merge(
423 $prepare_args,
424 $ids
425 );
426 }
427
428 if ( ! empty( $where_parts ) ) {
429 $sql .= ' WHERE ' . implode( ' AND ', $where_parts );
430 }
431
432 $sql .= ' LIMIT %d OFFSET %d';
433
434 $prepare_args[] = (int) $limit;
435 $prepare_args[] = (int) $offset;
436
437 $query = $wpdb->prepare( $sql, $prepare_args );
438
439 return $wpdb->get_results( $query, ARRAY_A );
440 }
441
442 public function mark_items_fixed( &$batch ) {
443 if (empty($batch) || !is_array($batch)) {
444 return;
445 }
446
447 global $wpdb;
448
449 $ids = array_column( $batch, 'id' );
450 $ids = array_unique( $ids );
451
452 if ( empty( $ids ) ) {
453 return;
454 }
455
456 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
457
458 $sql = "UPDATE {$wpdb->prefix}ehssl_resource_scan_tbl SET `fixed` = 1 WHERE `id` IN ({$placeholders})";
459
460 $query = $wpdb->prepare( $sql, $ids );
461
462 $wpdb->query( $query );
463 }
464
465 public function clear_scan_results() {
466 global $wpdb;
467 $query = $wpdb->prepare( "DELETE FROM {$wpdb->prefix}ehssl_resource_scan_tbl;" );
468
469 return $wpdb->query( $query );
470 }
471
472 public function handle_load_non_https_resources_table_page() {
473 self::render_http_scan_result_table();
474
475 wp_die();
476 }
477
478 public static function render_http_scan_result_table() {
479 $table = new EHSSL_Static_Resources_Scan_Result_Table();
480 $table->prepare_items();
481 ?>
482 <div class="wrap" id="nhs-table-container">
483 <form method="post" id="ehssl_non_https_resources_table_form">
484 <?php $table->display(); ?>
485 <input type="hidden" name="ehssl_update_all_http_urls_nonce" value="<?php echo esc_attr( wp_create_nonce( 'ehssl_update_all_http_urls' ) ); ?>">
486 </form>
487 </div>
488 <?php
489 }
490
491 public function handle_update_http_urls() {
492 if ( ! check_ajax_referer( 'ehssl_update_all_http_urls', 'nonce', false ) ) {
493 wp_send_json_error(
494 array(
495 'message' => __( 'Nonce verification failed!', 'https-redirection' ),
496 )
497 );
498 }
499
500 $limit = $this->batch_size;
501 $offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
502 $selected_ids = isset( $_POST['selected_ids'] ) ? json_decode(stripslashes($_POST['selected_ids'])) : null;
503
504 $total = 0;
505
506 if ( is_null( $selected_ids ) ) {
507 // Need to update all items.
508 $total = self::get_scan_results_count();
509 } else {
510 // Need to update selected items.
511 $total = count( $selected_ids );
512 }
513
514 if ( empty( $total ) ) {
515 wp_send_json_success(
516 array(
517 'completed' => true,
518 'processed' => 0,
519 )
520 );
521 }
522
523 $batch = self::get_scan_results_chunk( $offset, $limit, array( 'id' => $selected_ids ) );
524
525 if ( empty( $batch ) ) {
526 wp_send_json_success(
527 array(
528 'completed' => true,
529 'processed' => 0,
530 )
531 );
532 }
533
534 try {
535 $this->update_urls( $batch );
536
537 $this->mark_items_fixed($batch);
538
539 $next_offset = $offset + count( $batch );
540
541 $response = array(
542 'message' => __( 'URLs update to https successfully.', 'https-redirection' ),
543 'completed' => $next_offset >= $total,
544 'processed' => $next_offset,
545 'total' => $total,
546 'next_offset' => $next_offset,
547 );
548
549 wp_send_json_success($response);
550
551 } catch ( \Exception $e ) {
552 EHSSL_Logger::log( $e->getMessage(), 4 );
553 wp_send_json_error( array(
554 'message' => $e->getMessage(),
555 ) );
556 }
557 }
558
559 public function update_urls( &$batch ) {
560 global $wpdb;
561 foreach ( $batch as $item ) {
562 switch ( $item['source_table'] ) {
563 case $wpdb->posts:
564 $this->update_urls_in_post_type_item( $item );
565 break;
566 case $wpdb->options:
567 $this->update_urls_in_option_table( $item );
568 break;
569 }
570 }
571 }
572
573 public function update_urls_in_post_type_item( &$item ) {
574 $cols_map = isset( $item['cols_map'] ) ? maybe_unserialize( $item['cols_map'] ) : array();
575 $meta_map = isset( $item['meta_map'] ) ? maybe_unserialize( $item['meta_map'] ) : array();
576
577 $post_id = (int) $item['source_uid'];
578
579 if (! empty( $cols_map ) ) {
580
581 global $wpdb;
582 $query = $wpdb->prepare( "SELECT ID, post_content, post_excerpt FROM {$wpdb->posts} WHERE ID = %d", $post_id );
583 $post = $wpdb->get_row( $query );
584 if ( empty( $post ) ) {
585 return;
586 }
587
588 $update_columns = array();
589
590 foreach ( $cols_map as $column => $urls ) {
591 if ( ! property_exists( $post, $column ) || empty( $urls ) ) {
592 continue;
593 }
594
595 $value = $post->$column;
596
597 // Check if the column data is serialized or not.
598 $was_serialized = is_serialized( $value );
599
600 $value = $this->replace_urls_recursively( $value, $urls );
601
602 if ( $was_serialized ) {
603 $value = maybe_serialize( $value );
604 }
605
606 $update_columns[ $column ] = $value;
607 }
608
609 if ( ! empty( $update_columns ) ) {
610 $wpdb->update(
611 $wpdb->posts,
612 $update_columns,
613 array(
614 'ID' => $post_id
615 )
616 );
617
618 clean_post_cache( $post_id );
619 }
620 }
621
622 if ( ! empty( $meta_map ) ) {
623 $meta_keys_to_update = array_keys( $meta_map );
624 $all_meta = get_post_meta( $post_id );
625 foreach ( $meta_keys_to_update as $meta_key ) {
626 $value = isset($all_meta[$meta_key][0]) ? $all_meta[$meta_key][0] : '';
627
628 $value = $this->replace_urls_recursively( $value, $meta_map[ $meta_key ] );
629
630 update_post_meta( $post_id, $meta_key, $value );
631 }
632 }
633
634 }
635
636 public function update_urls_in_option_table( &$item ) {
637 $cols_map = isset( $item['cols_map'] ) ? maybe_unserialize( $item['cols_map'] ) : '';
638
639 $option_name = isset( $item['source_uid'] ) ? sanitize_key( $item['source_uid'] ) : '';
640
641 if ( empty( $cols_map ) || empty( $option_name ) ) {
642 return;
643 }
644
645 foreach ( $cols_map as $urls ) {
646 if ( empty( $urls ) ) {
647 continue;
648 }
649
650 $option_value = get_option( $option_name, '' );
651
652 $option_value = $this->replace_urls_recursively( $option_value, $urls );
653
654 update_option( $option_name, $option_value );
655 }
656
657 }
658
659 public function replace_with_https( $http_url, $subject ) {
660 $https_url = preg_replace( '#^http://#i', 'https://', $http_url );
661
662 return str_replace( $http_url, $https_url, $subject );
663 }
664
665 /**
666 * Makes sure it properly updates serialized data.
667 */
668 public function replace_urls_recursively( $value_to_update, $urls ) {
669 $value_to_update = maybe_unserialize( $value_to_update );
670
671 if ( is_array( $value_to_update ) ) {
672 foreach ( $value_to_update as $key => $value ) {
673 $value_to_update[ $key ] = $this->replace_urls_recursively( $value, $urls );
674 }
675
676 return $value_to_update;
677 }
678
679 if ( is_object( $value_to_update ) ) {
680 foreach ( $value_to_update as $key => $value ) {
681 $value_to_update->$key = $this->replace_urls_recursively( $value, $urls );
682 }
683
684 return $value_to_update;
685 }
686
687 if ( is_string( $value_to_update ) ) {
688 foreach ( $urls as $url ) {
689 $value_to_update = $this->replace_with_https( $url, $value_to_update );
690 }
691 }
692
693 return $value_to_update;
694 }
695
696 }
697
698 new EHSSL_Non_HTTPS_Resources_Scan_Update();