PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / trunk
WPSSO Core – Complete Schema Markup and Meta Tags vtrunk
22.6.1 22.6.0 22.5.3 22.5.2 22.5.1 22.4.0 22.3.0 22.2.1 22.2.0 22.1.3 22.1.2 22.1.1 22.1.0 22.0.0 21.13.3 21.13.2 trunk 21.13.1
wpsso / lib / util.php

util.php in WPSSO Core – Complete Schema Markup and Meta Tags trunk, at lib/util.php

4,211 lines 110.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * License: GPLv3
4 * License URI: https://www.gnu.org/licenses/gpl.txt
5 * Copyright 2012-2026 Jean-Sebastien Morisset (https://wpsso.com/)
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9
10 die( 'These aren\'t the droids you\'re looking for.' );
11 }
12
13 if ( ! defined( 'WPSSO_PLUGINDIR' ) ) {
14
15 die( 'Do. Or do not. There is no try.' );
16 }
17
18 if ( ! class_exists( 'SucomUtil' ) ) {
19
20 require_once WPSSO_PLUGINDIR . 'lib/com/util.php';
21 }
22
23 if ( ! class_exists( 'WpssoUtil' ) ) {
24
25 class WpssoUtil extends SucomUtil {
26
27 private $p; // Wpsso class object.
28
29 private $cache_size_labels = array(); // Array for image size labels.
30 private $cache_size_opts = array(); // Array for image size option prefix.
31 private $cache_uniq_urls = array(); // Array to detect duplicate image URLs.
32
33 private $is_functions = array(
34 'is_404',
35 'is_admin',
36 'is_archive',
37 'is_attachment',
38 'is_author',
39 'is_category',
40 'is_customize_preview',
41 'is_embed',
42 'is_feed',
43 'is_front_page',
44 'is_home',
45 'is_multisite',
46 'is_page',
47 'is_post_type_archive',
48 'is_preview',
49 'is_rtl',
50 'is_search',
51 'is_single',
52 'is_singular',
53 'is_ssl',
54 'is_tag',
55 'is_tax',
56
57 /*
58 * Common e-commerce functions.
59 */
60 'is_account_page',
61 'is_cart',
62 'is_checkout',
63 'is_checkout_pay_page',
64 'is_product',
65 'is_product_category',
66 'is_product_tag',
67 'is_shop',
68
69 /*
70 * Other functions.
71 */
72 'is_amp_endpoint',
73 'is_sitemap',
74 'is_sitemap_stylesheet',
75 'wp_doing_ajax',
76 'wp_doing_cron',
77 'wp_using_ext_object_cache',
78 );
79
80 public $blocks; // WpssoUtilBlocks.
81 public $cache; // WpssoUtilCache.
82 public $cf; // WpssoUtilCustomFields.
83 public $inline; // WpssoUtilInline.
84 public $metabox; // WpssoUtilMetabox.
85 public $reg; // WpssoUtilReg.
86 public $robots; // WpssoUtilRobots.
87 public $units; // WpssoUtilUnits.
88 public $wc; // WpssoUtilWooCommerce.
89
90 public function __construct( &$plugin ) {
91
92 $this->p =& $plugin;
93
94 if ( $this->p->debug->enabled ) {
95
96 $this->p->debug->mark();
97 }
98
99 $this->set_util_instances();
100
101 $this->add_plugin_actions( $this, array( 'scheduled_task_started' => 1 ), $prio = -1000 );
102
103 /*
104 * Log the locale change and clear the SucomUtilWP::get_locale() cache.
105 */
106 add_action( 'change_locale', array( $this, 'locale_changed' ), -100, 1 );
107 add_action( 'switch_locale', array( $this, 'locale_switched' ), -100, 1 );
108 add_action( 'restore_previous_locale', array( $this, 'locale_restored' ), -100, 2 );
109
110 /*
111 * Add our image sizes on the front-end, back-end, AJAX calls, and REST API calls.
112 */
113 if ( $this->p->debug->enabled ) {
114
115 $this->p->debug->log( 'adding WpssoUtil->add_plugin_image_sizes() action hooks' );
116 }
117
118 add_action( 'wp', array( $this, 'add_plugin_image_sizes' ), -100 ); // Front-end compatibility.
119 add_action( 'admin_init', array( $this, 'add_plugin_image_sizes' ), -100 ); // Back-end + AJAX compatibility.
120 add_action( 'rest_api_init', array( $this, 'add_plugin_image_sizes' ), -100 ); // REST API compatibility.
121 }
122
123 public function set_util_instances() {
124
125 /*
126 * Instantiate WpssoUtilBlocks.
127 */
128 if ( ! class_exists( 'WpssoUtilBlocks' ) ) {
129
130 require_once WPSSO_PLUGINDIR . 'lib/util-blocks.php';
131 }
132
133 $this->blocks = new WpssoUtilBlocks( $this->p, $this );
134
135 /*
136 * Instantiate WpssoUtilCache.
137 */
138 if ( ! class_exists( 'WpssoUtilCache' ) ) {
139
140 require_once WPSSO_PLUGINDIR . 'lib/util-cache.php';
141 }
142
143 $this->cache = new WpssoUtilCache( $this->p, $this );
144
145 /*
146 * Instantiate WpssoUtilCustomFields.
147 */
148 if ( ! class_exists( 'WpssoUtilCustomFields' ) ) {
149
150 require_once WPSSO_PLUGINDIR . 'lib/util-custom-fields.php';
151 }
152
153 $this->cf = new WpssoUtilCustomFields( $this->p, $this );
154
155 /*
156 * Instantiate WpssoUtilInline.
157 */
158 if ( ! class_exists( 'WpssoUtilInline' ) ) {
159
160 require_once WPSSO_PLUGINDIR . 'lib/util-inline.php';
161 }
162
163 $this->inline = new WpssoUtilInline( $this->p, $this );
164
165 /*
166 * Instantiate WpssoUtilMetabox.
167 */
168 if ( ! class_exists( 'WpssoUtilMetabox' ) ) {
169
170 require_once WPSSO_PLUGINDIR . 'lib/util-metabox.php';
171 }
172
173 $this->metabox = new WpssoUtilMetabox( $this->p );
174
175 /*
176 * Instantiate WpssoUtilReg.
177 */
178 if ( ! class_exists( 'WpssoUtilReg' ) ) {
179
180 require_once WPSSO_PLUGINDIR . 'lib/util-reg.php';
181 }
182
183 $this->reg = new WpssoUtilReg( $this->p );
184
185 /*
186 * Instantiate WpssoUtilReg.
187 */
188 if ( ! class_exists( 'WpssoUtilRobots' ) ) {
189
190 require_once WPSSO_PLUGINDIR . 'lib/util-robots.php';
191 }
192
193 $this->robots = new WpssoUtilRobots( $this->p );
194
195 /*
196 * Instantiate WpssoUtilUnits.
197 */
198 if ( ! class_exists( 'WpssoUtilUnits' ) ) {
199
200 require_once WPSSO_PLUGINDIR . 'lib/util-units.php';
201 }
202
203 $this->units = new WpssoUtilUnits( $this->p );
204
205 /*
206 * Instantiate WpssoUtilWooCommerce.
207 */
208 if ( ! empty( $this->p->avail[ 'ecom' ][ 'woocommerce' ] ) ) {
209
210 if ( ! class_exists( 'WpssoUtilWooCommerce' ) ) {
211
212 require_once WPSSO_PLUGINDIR . 'lib/util-woocommerce.php';
213 }
214
215 $this->wc = new WpssoUtilWooCommerce( $this->p );
216 }
217 }
218
219 /*
220 * Add plugin image sizes before starting a background task, like refreshing the plugin cache.
221 */
222 public function action_scheduled_task_started( $user_id ) {
223
224 $this->add_plugin_image_sizes();
225 }
226
227 /*
228 * Monitor the WordPress 'change_locale' action for locale changes.
229 *
230 * Log the locale change and clear the SucomUtilWP::get_locale() cache.
231 */
232 public function locale_changed( $locale ) {
233
234 if ( $this->p->debug->enabled ) {
235
236 $this->p->debug->log( 'wp locale changed to ' . $locale );
237 }
238
239 SucomUtilWP::clear_locale_cache();
240 }
241
242 /*
243 * Cache is cleared by WpssoUtil->locale_changed().
244 */
245 public function locale_switched( $locale ) {
246
247 if ( $this->p->debug->enabled ) {
248
249 $this->p->debug->log( 'wp locale switched to ' . $locale );
250 }
251 }
252
253 /*
254 * Cache is cleared by WpssoUtil->locale_changed().
255 */
256 public function locale_restored( $locale, $previous_locale ) {
257
258 if ( $this->p->debug->enabled ) {
259
260 $this->p->debug->log( 'wp locale restored to ' . $locale . ' from ' . $previous_locale );
261 }
262 }
263
264 /*
265 * Can be called directly and from the "wp", "rest_api_init", and "current_screen" actions.
266 *
267 * This method does not return a value, so do not use it as a filter. ;-)
268 */
269 public function add_plugin_image_sizes() {
270
271 /*
272 * Allow various plugin add-ons to provide their image names, labels, etc. The first dimension array key is
273 * the option name prefix by default. You can also include the width, height, crop, crop_x, and crop_y
274 * values.
275 *
276 * Array (
277 * [og] => Array (
278 * [name] => opengraph
279 * [label] => Open Graph // Pre-translated.
280 * )
281 * )
282 */
283 if ( $this->p->debug->enabled ) {
284
285 $this->p->debug->mark( 'define image sizes' ); // Begin timer.
286 }
287
288 /*
289 * Get the default options only once.
290 */
291 static $defs = null;
292
293 $image_sizes = apply_filters( 'wpsso_plugin_image_sizes', array() );
294
295 foreach( $image_sizes as $opt_prefix => $size_info ) {
296
297 if ( ! is_array( $size_info ) ) { // Just in case.
298
299 continue;
300 }
301
302 foreach ( array( 'width', 'height', 'crop', 'crop_x', 'crop_y' ) as $opt_suffix ) {
303
304 /*
305 * Value provided by filters.
306 */
307 if ( isset( $size_info[ $opt_suffix ] ) ) {
308
309 continue;
310
311 /*
312 * Plugin settings.
313 */
314 } elseif ( isset( $this->p->options[ $opt_prefix . '_img_' . $opt_suffix ] ) ) {
315
316 $size_info[ $opt_suffix ] = $this->p->options[ $opt_prefix . '_img_' . $opt_suffix ];
317
318 /*
319 * Default settings.
320 */
321 } else {
322
323 if ( null === $defs ) {
324
325 if ( $this->p->debug->enabled ) {
326
327 $this->p->debug->log( 'getting default option values' );
328 }
329
330 $defs = $this->p->opt->get_defaults();
331 }
332
333 if ( isset( $defs[ $opt_prefix . '_img_' . $opt_suffix ] ) ) { // Just in case.
334
335 $size_info[ $opt_suffix ] = $defs[ $opt_prefix . '_img_' . $opt_suffix ];
336
337 } else $size_info[ $opt_suffix ] = null;
338 }
339 }
340
341 if ( empty( $size_info[ 'crop' ] ) ) {
342
343 $size_info[ 'crop' ] = false;
344
345 } else {
346
347 $size_info[ 'crop' ] = true;
348
349 $new_crop = array( 'center', 'center' );
350
351 foreach ( array( 'crop_x', 'crop_y' ) as $crop_key => $opt_suffix ) {
352
353 if ( ! empty( $size_info[ $opt_suffix ] ) && $size_info[ $opt_suffix ] !== 'none' ) {
354
355 $new_crop[ $crop_key ] = $size_info[ $opt_suffix ];
356 }
357 }
358
359 if ( $new_crop !== array( 'center', 'center' ) ) {
360
361 $size_info[ 'crop' ] = $new_crop;
362 }
363 }
364
365 if ( $size_info[ 'width' ] > 0 && $size_info[ 'height' ] > 0 ) {
366
367 if ( isset( $size_info[ 'label_transl' ] ) ) {
368
369 $size_label = $size_info[ 'label_transl' ];
370
371 } elseif ( isset( $size_info[ 'label' ] ) ) {
372
373 $size_label = $size_info[ 'label' ];
374
375 } else {
376
377 $size_label = $size_info[ 'name' ];
378 }
379
380 $this->cache_size_labels[ 'wpsso-' . $size_info[ 'name' ] ] = $size_label;
381
382 $this->cache_size_opts[ 'wpsso-' . $size_info[ 'name' ] ] = $opt_prefix;
383
384 /*
385 * Add the image size.
386 */
387 add_image_size( 'wpsso-' . $size_info[ 'name' ], $size_info[ 'width' ], $size_info[ 'height' ], $size_info[ 'crop' ] );
388
389 if ( $this->p->debug->enabled ) {
390
391 $this->p->debug->log( 'added image size wpsso-' . $size_info[ 'name' ] . ' ' .
392 $size_info[ 'width' ] . 'x' . $size_info[ 'height' ] . ' ' . ( empty( $size_info[ 'crop' ] ) ?
393 'uncropped' : 'cropped ' . $size_info[ 'crop_x' ] . '/' . $size_info[ 'crop_y' ] ) );
394 }
395 }
396 }
397
398 /*
399 * Reset the default options variable.
400 */
401 $defs = null;
402
403 if ( $this->p->debug->enabled ) {
404
405 $this->p->debug->mark( 'define image sizes' ); // End timer.
406 }
407 }
408
409 /*
410 * Get the width, height and crop value for all image sizes.
411 *
412 * Returns an associative array with the image size name as the array key.
413 */
414 public function get_image_sizes( $attachment_id = false ) {
415
416 $image_sizes = array();
417
418 foreach ( get_intermediate_image_sizes() as $size_name ) {
419
420 $image_sizes[ $size_name ] = $this->get_size_info( $size_name, $attachment_id );
421 }
422
423 return $image_sizes;
424 }
425
426 public function is_size_cropped( $size_name = 'thumbnail', $attachment_id = false ) {
427
428 /*
429 * Example $size_info = Array (
430 * [size_name] => wpsso-opengraph,
431 * [attachment_id] => false,
432 * [width] => 1200,
433 * [height] => 630,
434 * [crop] => 1,
435 * [is_cropped] => 1,
436 * [dims_transl] => 1200x630 cropped,
437 * [label_transl] => Open Graph (Facebook and oEmbed),
438 * [opt_prefix] => og,
439 * );
440 */
441 $size_info = $this->get_size_info( $size_name, $attachment_id );
442
443 return empty( $size_info[ 'is_cropped' ] ) ? false : true;
444 }
445
446 /*
447 * Get the width, height and crop value for a specific image size.
448 */
449 public function get_size_info( $size_name = 'thumbnail', $attachment_id = false ) {
450
451 if ( ! is_string( $size_name ) ) { // Just in case.
452
453 return false;
454 }
455
456 static $local_fifo = array();
457
458 if ( isset( $local_fifo[ $size_name ][ $attachment_id ] ) ) {
459
460 return $local_fifo[ $size_name ][ $attachment_id ];
461
462 } elseif ( isset( $local_fifo[ $size_name ] ) ) {
463
464 /*
465 * Maybe limit the number of array elements.
466 */
467 $local_fifo[ $size_name ] = SucomUtil::array_slice_fifo( $local_fifo[ $size_name ], WPSSO_CACHE_ARRAY_FIFO_MAX );
468
469 } else $local_fifo[ $size_name ] = array();
470
471 global $_wp_additional_image_sizes;
472
473 if ( isset( $_wp_additional_image_sizes[ $size_name ][ 'width' ] ) ) {
474
475 $width = intval( $_wp_additional_image_sizes[ $size_name ][ 'width' ] );
476
477 } else $width = get_option( $size_name . '_size_w' );
478
479 if ( isset( $_wp_additional_image_sizes[ $size_name ][ 'height' ] ) ) {
480
481 $height = intval( $_wp_additional_image_sizes[ $size_name ][ 'height' ] );
482
483 } else $height = get_option( $size_name . '_size_h' );
484
485 if ( isset( $_wp_additional_image_sizes[ $size_name ][ 'crop' ] ) ) {
486
487 $crop = $_wp_additional_image_sizes[ $size_name ][ 'crop' ];
488
489 } else $crop = get_option( $size_name . '_crop' );
490
491 /*
492 * Standardize to true, false, or non-empty array.
493 */
494 if ( empty( $crop ) ) { // 0, false, null, or empty array.
495
496 $crop = false;
497
498 } elseif ( ! is_array( $crop ) ) { // 1, or true.
499
500 $crop = true;
501 }
502
503 /*
504 * If the image size is cropped, then check the image metadata for a custom crop area.
505 */
506 if ( $crop && $attachment_id && is_numeric( $attachment_id ) ) {
507
508 $new_crop = is_array( $crop ) ? $crop : array( 'center', 'center' );
509
510 foreach ( array( 'attach_img_crop_x', 'attach_img_crop_y' ) as $crop_key => $md_key ) {
511
512 $value = $this->p->post->get_options( $attachment_id, $md_key );
513
514 if ( $value && $value !== 'none' ) { // Custom crop value found.
515
516 $new_crop[ $crop_key ] = $value; // Adjust the crop value.
517
518 $crop = $new_crop; // Update the crop array.
519 }
520 }
521 }
522
523 if ( $crop === array( 'center', 'center' ) ) {
524
525 $crop = true;
526 }
527
528 $is_cropped = empty( $crop ) ? false : true;
529
530 /*
531 * Crop can be true, false, or an array.
532 */
533 $local_fifo[ $size_name ][ $attachment_id ] = array(
534 'size_name' => $size_name,
535 'attachment_id' => $attachment_id,
536 'width' => $width,
537 'height' => $height,
538 'crop' => $crop,
539 'is_cropped' => $is_cropped,
540 'dims_transl' => $width . 'x' . $height . ' ' . ( $is_cropped ? __( 'cropped', 'wpsso' ) : __( 'uncropped', 'wpsso' ) ),
541 'label_transl' => $this->get_image_size_label( $size_name ),
542 'opt_prefix' => $this->get_image_size_opt( $size_name ),
543 );
544
545 if ( $this->p->debug->enabled ) {
546
547 $this->p->debug->log_size( 'local_fifo', $local_fifo );
548 }
549
550 return $local_fifo[ $size_name ][ $attachment_id ];
551 }
552
553 /*
554 * Example $size_name = 'wpsso-opengraph' returns 'Open Graph' pre-translated.
555 */
556 public function get_image_size_label( $size_name ) {
557
558 if ( ! empty( $this->cache_size_labels[ $size_name ] ) ) {
559
560 return $this->cache_size_labels[ $size_name ];
561
562 }
563
564 return $size_name;
565 }
566
567 /*
568 * Example $size_name = 'wpsso-opengraph' returns 'og'.
569 */
570 public function get_image_size_opt( $size_name ) {
571
572 if ( isset( $this->cache_size_opts[ $size_name ] ) ) {
573
574 return $this->cache_size_opts[ $size_name ];
575
576 }
577
578 return '';
579 }
580
581 /*
582 * See WpssoMedia->get_all_images().
583 * See WpssoMedia->get_mt_pid_images().
584 * See WpssoUtil->clear_uniq_urls().
585 * See WpssoSscShortcodeSchema->do_shortcode().
586 */
587 public function get_image_size_names( $mixed = null, $sanitize = true ) {
588
589 $size_names = array_keys( $this->cache_size_labels );
590
591 if ( null === $mixed ) {
592
593 return $size_names;
594
595 } elseif ( is_array( $mixed ) ) {
596
597 return $sanitize ? array_intersect( $size_names, $mixed ) : $mixed; // Sanitize and return.
598
599 } elseif ( is_string( $mixed ) ) {
600
601 switch ( $mixed ) {
602
603 case 'og':
604 case 'opengraph':
605
606 return $this->get_obj_image_size_names( $this->p->og );
607
608 case 'pin':
609 case 'pinterest':
610
611 return $this->get_obj_image_size_names( $this->p->pinterest );
612
613 case 'thumb':
614 case 'thumbnail':
615
616 return array( 'wpsso-' . $mixed );
617
618 case 'tc':
619 case 'twitter':
620 case 'twittercard':
621
622 return $this->get_obj_image_size_names( $this->p->tc );
623
624 case 'schema':
625
626 return $this->get_obj_image_size_names( $this->p->schema );
627
628 default:
629
630 return $sanitize ? array_intersect( $size_names, array( $mixed ) ) : array( $mixed );
631 }
632 }
633
634 return array();
635 }
636
637 private function get_obj_image_size_names( object $obj ) {
638
639 $names = array();
640
641 if ( is_callable( array( $obj, 'filter_plugin_image_sizes' ) ) ) { // Just in case.
642
643 $sizes = $obj->filter_plugin_image_sizes( array() );
644
645 foreach ( $sizes as $opt_prefix => $arr ) {
646
647 if ( ! empty( $arr[ 'name' ] ) ) {
648
649 $names[] = 'wpsso-' . $arr[ 'name' ];
650 }
651 }
652 }
653
654 return $names;
655 }
656
657 /*
658 * If this is an '_img_url' option, add the image dimensions and unset the '_img_id' option.
659 */
660 public function maybe_add_img_url_size( array &$opts, $opt_key ) {
661
662 /*
663 * Only process option keys with '_img_url' in their name.
664 */
665 if ( false === strpos( $opt_key, '_img_url' ) ) {
666
667 return;
668 }
669
670 $this->add_image_url_size( $opts, $opt_key );
671
672 $count = null;
673 $img_id_key = str_replace( '_img_url', '_img_id', $opt_key, $count ); // Image ID key.
674 $img_id_lib_key = str_replace( '_img_url', '_img_id_lib', $opt_key ); // Image ID media library prefix key.
675
676 if ( $count ) { // Just in case.
677
678 unset( $opts[ $img_id_key ] );
679 unset( $opts[ $img_id_lib_key ] );
680 }
681 }
682
683 /*
684 * If this is a '_value' option, add the '_units' option.
685 */
686 public function maybe_add_md_key_units( array &$md_opts, $md_key ) {
687
688 if ( false !== strpos( $md_key, '_value' ) ) {
689
690 $count = null;
691
692 $md_units_key = preg_replace( '/_value$/', '_units', $md_key, $limit = -1, $count );
693
694 if ( $count ) { // Just in case.
695
696 $md_opts[ $md_units_key ] = WpssoUtilUnits::get_mixed_text( $md_units_key );
697
698 $md_opts[ $md_units_key . ':disabled' ] = true;
699 }
700 }
701 }
702
703 /*
704 * See WpssoIntegEcomWooCommerce->filter_import_product_attributes().
705 * See WpssoUtilCustomFields->filter_import_custom_fields().
706 */
707 public function maybe_renum_md_key( array &$md_opts, $md_key, array $values, $is_disabled = true ) {
708
709 /*
710 * Remove any old values from the options array.
711 */
712 $md_opts = SucomUtil::preg_grep_keys( '/^' . $md_key . '_[0-9]+$/', $md_opts, $invert = true );
713
714 /*
715 * Renumber the options starting from 0.
716 */
717 foreach ( $values as $num => $val ) {
718
719 $md_num_key = $md_key . '_' . $num;
720
721 $md_opts[ $md_num_key ] = $val;
722
723 if ( $is_disabled ) {
724
725 $md_opts[ $md_num_key . ':disabled' ] = true;
726 }
727
728 if ( $this->p->debug->enabled ) {
729
730 $this->p->debug->log( 'option ' . $md_num_key . ' = ' . print_r( $md_opts[ $md_num_key ], true ) );
731 }
732 }
733 }
734
735 /*
736 * $media_prefixes can be a single key name or an array of key names.
737 *
738 * Uses a reference variable to modify the $opts array directly.
739 */
740 public function add_image_url_size( array &$opts, $media_prefixes = 'og:image' ) {
741
742 if ( $this->p->debug->enabled ) {
743
744 $this->p->debug->mark();
745 }
746
747 if ( ! is_array( $media_prefixes ) ) {
748
749 $media_prefixes = array( $media_prefixes );
750 }
751
752 foreach ( $media_prefixes as $media_pre ) {
753
754 $opt_suffix = '';
755
756 if ( preg_match( '/^(.*)(#.*)$/', $media_pre, $matches ) ) { // Language.
757
758 $media_pre = $matches[ 1 ];
759 $opt_suffix = $matches[ 2 ] . $opt_suffix;
760 }
761
762 if ( preg_match( '/^(.*)(_[0-9]+)$/', $media_pre, $matches ) ) { // Multi-option.
763
764 $media_pre = $matches[ 1 ];
765 $opt_suffix = $matches[ 2 ] . $opt_suffix;
766 }
767
768 $media_url = self::get_first_mt_media_url( $opts, $media_pre . $opt_suffix );
769
770 if ( ! empty( $media_url ) ) {
771
772 list(
773 $opts[ $media_pre . ':width' . $opt_suffix ], // Example: place_img_url:width_1.
774 $opts[ $media_pre . ':height' . $opt_suffix ], // Example: place_img_url:height_1.
775 $image_type,
776 $image_attr
777 ) = $this->get_image_url_info( $media_url );
778 }
779 }
780
781 return $opts;
782 }
783
784 /*
785 * Always returns an array.
786 *
787 * Note that WebP is only supported since PHP v7.1.
788 */
789 public function get_image_url_info( $image_url ) {
790
791 if ( $this->p->debug->enabled ) {
792
793 $this->p->debug->mark();
794 }
795
796 static $local_fifo = array(); // Optimize and get image size for a given URL only once.
797
798 $image_url = trim( $image_url ); // Just in case.
799
800 if ( isset( $local_fifo[ $image_url ] ) ) {
801
802 if ( $this->p->debug->enabled ) {
803
804 $this->p->debug->log( 'exiting early: returning image info from static cache' );
805 }
806
807 return $local_fifo[ $image_url ];
808 }
809
810 /*
811 * Maybe limit the number of array elements.
812 */
813 $local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );
814
815 $def_image_info = array( WPSSO_UNDEF, WPSSO_UNDEF, '', '' );
816
817 if ( empty( $image_url ) ) {
818
819 if ( $this->p->debug->enabled ) {
820
821 $this->p->debug->log( 'exiting early: image URL is empty' );
822 }
823
824 return $local_fifo[ $image_url ] = $def_image_info; // Stop here.
825
826 } elseif ( false === filter_var( $image_url, FILTER_VALIDATE_URL ) ) {
827
828 if ( $this->p->debug->enabled ) {
829
830 $this->p->debug->log( 'exiting early: invalid image url "' . $image_url . '"' );
831 }
832
833 return $local_fifo[ $image_url ] = $def_image_info; // Stop here.
834 }
835
836 $cache_md5_pre = 'wpsso_i_'; // Transient prefix for image URL info.
837 $cache_exp_secs = $this->get_cache_exp_secs( $cache_md5_pre );
838
839 if ( $cache_exp_secs > 0 ) {
840
841 /*
842 * Note that cache_id is a unique identifier for the cached data and should be 45 characters or
843 * less in length. If using a site transient, it should be 40 characters or less in length.
844 */
845 $cache_salt = __METHOD__ . '(url:' . $image_url . ')';
846 $cache_id = $cache_md5_pre . md5( $cache_salt );
847
848 if ( $this->p->debug->enabled ) {
849
850 $this->p->debug->log( 'transient cache salt ' . $cache_salt );
851 }
852
853 $image_info = get_transient( $cache_id );
854
855 if ( is_array( $image_info ) ) {
856
857 if ( $this->p->debug->enabled ) {
858
859 $this->p->debug->log( 'exiting early: returning image info from transient' );
860 }
861
862 /*
863 * Optimize and save the transient cache value to local cache.
864 */
865 return $local_fifo[ $image_url ] = $image_info;
866 }
867
868 } elseif ( $this->p->debug->enabled ) {
869
870 $this->p->debug->log( 'transient cache for image info is disabled' );
871 }
872
873 $mtime_start = microtime( $get_float = true );
874
875 /*
876 * Example $image_info:
877 *
878 * Array (
879 * [0] => 2048
880 * [1] => 2048
881 * [2] => 3
882 * [3] => width="2048" height="2048"
883 * [bits] => 8
884 * [mime] => image/png
885 * )
886 */
887 $image_info = $this->p->cache->get_image_size( $image_url, $exp_secs = 300, $curl_opts = array(), $error_handler = 'wpsso_error_handler' );
888 $mtime_total = microtime( $get_float = true ) - $mtime_start;
889 $mtime_max = WPSSO_PHP_GETIMGSIZE_MAX_TIME;
890
891 if ( $mtime_total > $mtime_max ) {
892
893 $func_name = 'getimagesize';
894 $current_url = isset( $_SERVER[ 'REQUEST_URI' ] ) ? $_SERVER[ 'REQUEST_URI' ] : '';
895 $php_ref_url = sprintf( __( 'https://www.php.net/manual/en/function.%s.php', 'wpsso' ), $func_name );
896 $php_ref_link = sprintf( '<a href="%1$s">%2$s</a>', $php_ref_url, $func_name );
897 $error_pre = sprintf( __( '%s warning:', 'wpsso' ), __METHOD__ );
898 $rec_max_msg = sprintf( __( 'longer than recommended max of %1$.3f secs', 'wpsso' ), $mtime_max );
899 $notice_msg = sprintf( __( 'Slow PHP function detected - %1$s took %2$.3f secs for %3$s (%4$s).', 'wpsso' ),
900 '<code>' . $func_name . '()</code>', $mtime_total, $image_url, $rec_max_msg );
901
902 if ( $this->p->notice->is_admin_pre_notices() ) {
903
904 $this->p->notice->warn( $notice_msg );
905 }
906
907 /*
908 * Add extra information for the debug log.
909 */
910 if ( $php_ref_url ) $notice_msg .= ' ' . sprintf( __( 'See %s for more information.', 'wpsso' ), $php_ref_url );
911 if ( $current_url ) $notice_msg .= ' ' . sprintf( __( 'Server request URI = %s', 'wpsso' ), $current_url );
912
913 self::safe_error_log( $error_pre . ' ' . $notice_msg, $strip_html = true );
914
915 if ( $this->p->debug->enabled ) {
916
917 $this->p->debug->log( sprintf( 'slow PHP function detected - %1$s took %2$.3f secs for %3$s',
918 $func_name . '()', $mtime_total, $image_url ) );
919 }
920 }
921
922 if ( is_array( $image_info ) ) {
923
924 if ( $this->p->debug->enabled ) {
925
926 $this->p->debug->log( 'PHP getimagesize() image info: ' . $image_info[ 0 ] . 'x' . $image_info[ 1 ] );
927 }
928
929 } else {
930
931 if ( $this->p->debug->enabled ) {
932
933 $this->p->debug->log( 'PHP getimagesize() did not return an array - using defaults' );
934 }
935
936 $image_info = $def_image_info;
937 }
938
939 if ( $cache_exp_secs > 0 ) {
940
941 set_transient( $cache_id, $image_info, $cache_exp_secs );
942
943 if ( $this->p->debug->enabled ) {
944
945 $this->p->debug->log( 'image info saved to transient cache for ' . $cache_exp_secs . ' seconds' );
946 }
947 }
948
949 return $local_fifo[ $image_url ] = $image_info;
950 }
951
952 /*
953 * Called by several class __construct() methods to hook their filters.
954 */
955 public function add_plugin_filters( $class, $filters, $prio = 10, $ext = '' ) {
956
957 $this->add_plugin_callbacks( $type = 'filter', $class, $filters, $prio, $ext );
958 }
959
960 /*
961 * Called by several class __construct() methods to hook their actions.
962 */
963 public function add_plugin_actions( $class, $actions, $prio = 10, $ext = '' ) {
964
965 $this->add_plugin_callbacks( $type = 'action', $class, $actions, $prio, $ext );
966 }
967
968 /*
969 * Called by WpssoUtil->add_plugin_filters().
970 * Called by WpssoUtil->add_plugin_actions().
971 *
972 * $type = 'filter' | 'action'.
973 */
974 private function add_plugin_callbacks( $type, $class, $hook_list, $prio, $ext = '' ) {
975
976 $ext = $ext === '' ? $this->p->id : $ext;
977
978 foreach ( $hook_list as $name => $val ) {
979
980 if ( ! is_string( $name ) ) {
981
982 if ( $this->p->debug->enabled ) {
983
984 $this->p->debug->log( $name . ' => ' . $val . ' ' . $type . ' skipped: filter name must be a string' );
985 }
986
987 continue;
988 }
989
990 /*
991 * Example:
992 *
993 * 'json_data_https_schema_org_website' => 5
994 */
995 if ( is_int( $val ) ) {
996
997 $arg_nums = $val;
998 $hook_name = self::sanitize_hookname( $ext . '_' . $name );
999 $method_name = self::sanitize_hookname( $type . '_' . $name );
1000
1001 if ( is_callable( array( &$class, $method_name ) ) ) {
1002
1003 call_user_func( 'add_' . $type, $hook_name, array( &$class, $method_name ), $prio, $arg_nums );
1004
1005 if ( $this->p->debug->enabled ) {
1006
1007 $this->p->debug->log( 'added ' . $method_name . ' method ' . $type, $class_seq = 3 );
1008 }
1009
1010 } else {
1011
1012 $error_pre = sprintf( __( '%s warning:', 'wpsso' ), __METHOD__ );
1013
1014 self::safe_error_log( $error_pre . ' ' . $method_name . ' method ' . $type . ' is not callable' );
1015
1016 if ( $this->p->debug->enabled ) {
1017
1018 $this->p->debug->log( $method_name . ' method ' . $type . ' is not callable' );
1019 }
1020 }
1021
1022 /*
1023 * Example:
1024 *
1025 * 'wpsso_filter_hook_name' => '__return_false'
1026 */
1027 } elseif ( is_string( $val ) ) {
1028
1029 $arg_nums = 1;
1030 $hook_name = self::sanitize_hookname( $ext . '_' . $name );
1031 $function_name = self::sanitize_hookname( $val );
1032
1033 if ( is_callable( $function_name ) ) {
1034
1035 call_user_func( 'add_' . $type, $hook_name, $function_name, $prio, $arg_nums );
1036
1037 if ( $this->p->debug->enabled ) {
1038
1039 $this->p->debug->log( 'added ' . $function_name . ' function ' . $type . ' for ' . $hook_name, $class_seq = 3 );
1040 }
1041
1042 } else {
1043
1044 $error_pre = sprintf( __( '%s warning:', 'wpsso' ), __METHOD__ );
1045
1046 self::safe_error_log( $error_pre . ' ' . $method_name . ' method ' . $type . ' for ' . $hook_name . ' is not callable' );
1047
1048 if ( $this->p->debug->enabled ) {
1049
1050 $this->p->debug->log( $function_name . ' function ' . $type . ' for ' . $hook_name . ' is not callable' );
1051 }
1052 }
1053
1054 /*
1055 * Example:
1056 *
1057 * 'json_data_https_schema_org_article' => array(
1058 * 'json_data_https_schema_org_article' => 5,
1059 * 'json_data_https_schema_org_newsarticle' => 5,
1060 * 'json_data_https_schema_org_techarticle' => 5,
1061 * )
1062 */
1063 } elseif ( is_array( $val ) ) {
1064
1065 $method_name = self::sanitize_hookname( $type . '_' . $name );
1066
1067 foreach ( $val as $hook_name => $arg_nums ) {
1068
1069 $hook_name = self::sanitize_hookname( $ext . '_' . $hook_name );
1070
1071 if ( is_callable( array( &$class, $method_name ) ) ) {
1072
1073 call_user_func( 'add_' . $type, $hook_name, array( &$class, $method_name ), $prio, $arg_nums );
1074
1075 if ( $this->p->debug->enabled ) {
1076
1077 $this->p->debug->log( 'added ' . $method_name . ' method ' . $type . ' for ' . $hook_name, $class_seq = 3 );
1078 }
1079
1080 } else {
1081
1082 $error_pre = sprintf( __( '%s warning:', 'wpsso' ), __METHOD__ );
1083
1084 self::safe_error_log( $error_pre . ' ' . $method_name . ' method ' . $type . ' for ' . $hook_name . ' is not callable' );
1085
1086 if ( $this->p->debug->enabled ) {
1087
1088 $this->p->debug->log( $method_name . ' method ' . $type . ' for ' . $hook_name . ' is not callable' );
1089 }
1090 }
1091 }
1092 }
1093 }
1094 }
1095
1096 /*
1097 * Add options using a key prefix array and post type names.
1098 */
1099 public function add_post_type_names( array &$opts, array $opt_prefixes, $args = null ) {
1100
1101 foreach ( $opt_prefixes as $opt_prefix => $def_val ) {
1102
1103 $type_names = SucomUtilWP::get_post_types( $output = 'names', $sort = false, $args );
1104
1105 foreach ( $type_names as $opt_suffix ) {
1106
1107 $opt_key = $opt_prefix . '_' . $opt_suffix;
1108
1109 if ( ! isset( $opts[ $opt_key ] ) ) {
1110
1111 $opts[ $opt_key ] = $def_val;
1112 }
1113 }
1114 }
1115
1116 return $opts;
1117 }
1118
1119 /*
1120 * Add options using a key prefix array and post type archive names.
1121 *
1122 * Note that 'has_archive' = 1 will not match post types registered with a string in 'has_archive'.
1123 *
1124 * Use 'has_archive' = true to include the WooCommerce product archive page (ie. 'has_archive' = 'shop').
1125 */
1126 public function add_post_type_archive_names( array &$opts, array $opt_prefixes ) {
1127
1128 $args = array( 'has_archive' => true );
1129
1130 return $this->add_post_type_names( $opts, $opt_prefixes, $args );
1131 }
1132
1133 /*
1134 * Add options using a key prefix array and taxonomy names.
1135 */
1136 public function add_taxonomy_names( array &$opts, array $opt_prefixes ) {
1137
1138 foreach ( $opt_prefixes as $opt_prefix => $def_val ) {
1139
1140 $tax_names = SucomUtilWP::get_taxonomies( $output = 'names' );
1141
1142 foreach ( $tax_names as $opt_suffix ) {
1143
1144 $opt_key = $opt_prefix . '_' . $opt_suffix;
1145
1146 if ( ! isset( $opts[ $opt_key ] ) ) {
1147
1148 $opts[ $opt_key ] = $def_val;
1149 }
1150 }
1151 }
1152
1153 return $opts;
1154 }
1155
1156 public function get_pkg_info( $get_ext = null, $get_key = null ) {
1157
1158 static $local_cache = array();
1159
1160 if ( empty( $local_cache ) ) {
1161
1162 foreach ( $this->p->cf[ 'plugin' ] as $ext => $info ) {
1163
1164 if ( empty( $info[ 'name' ] ) ) continue; // Just in case.
1165
1166 $local_cache[ $ext ] = array();
1167
1168 $ext_pdir = $this->p->check->pp( $ext, $li = false );
1169 $ext_auth_id = $this->p->check->get_ext_auth_id( $ext );
1170 $ext_pkg_std = ! $ext_pdir || ( isset( $GLOBALS[ $ext . '_dist' ] ) && 'std' === $GLOBALS[ $ext . '_dist' ] ) ? true : false;
1171 $ext_pp = ! $ext_pkg_std && $ext_auth_id && $this->p->check->pp( $ext, $li = true, WPSSO_UNDEF ) === WPSSO_UNDEF ? true : false;
1172 $ext_status = ( $ext_pp ? 'L' : ( $ext_pdir ? 'U' : 'S' ) ) . ( $ext_auth_id ? '*' : '' );
1173 $ext_name_transl = _x( $info[ 'name' ], 'plugin name', 'wpsso' );
1174 $pkg_pro_transl = _x( $this->p->cf[ 'packages' ][ 'pro' ], 'package name', 'wpsso' );
1175 $pkg_std_transl = _x( $this->p->cf[ 'packages' ][ 'std' ], 'package name', 'wpsso' );
1176
1177 $local_cache[ $ext ][ 'gen' ] = $info[ 'short' ] . ( isset( $info[ 'version' ] ) ? ' ' . $info[ 'version' ] . '/' . $ext_status : '' );
1178 $local_cache[ $ext ][ 'name' ] = $ext_name_transl;
1179 $local_cache[ $ext ][ 'name_pkg' ] = $this->get_pkg_name( $ext_name_transl, $ext_pp ? $pkg_pro_transl : $pkg_std_transl );
1180 $local_cache[ $ext ][ 'name_pro' ] = $this->get_pkg_name( $ext_name_transl, $pkg_pro_transl );
1181 $local_cache[ $ext ][ 'name_std' ] = $this->get_pkg_name( $ext_name_transl, $pkg_std_transl );
1182 $local_cache[ $ext ][ 'pdir' ] = $ext_pdir;
1183 $local_cache[ $ext ][ 'pkg' ] = $ext_pp ? $pkg_pro_transl : $pkg_std_transl;
1184 $local_cache[ $ext ][ 'pp' ] = $ext_pp;
1185 $local_cache[ $ext ][ 'short' ] = $info[ 'short' ];
1186 $local_cache[ $ext ][ 'short_pkg' ] = $info[ 'short' ] . ' ' . $ext_pp ? $pkg_pro_transl : $pkg_std_transl;
1187 $local_cache[ $ext ][ 'short_pro' ] = $info[ 'short' ] . ' ' . $pkg_pro_transl;
1188 $local_cache[ $ext ][ 'short_std' ] = $info[ 'short' ] . ' ' . $pkg_std_transl;
1189 $local_cache[ $ext ][ 'slug' ] = $info[ 'slug' ];
1190 }
1191 }
1192
1193 return isset( $local_cache[ $get_ext ][ $get_key ] ) ? $local_cache[ $get_ext ][ $get_key ] : $local_cache;
1194 }
1195
1196 public function get_pkg_name( $name, $pkg ) {
1197
1198 if ( false !== strpos( $name, $pkg ) ) {
1199
1200 $name = preg_replace( '/^(.*) ' . $pkg . '( [\[\(].+[\)\]])?$/U', '$1$2', $name );
1201 }
1202
1203 return preg_replace( '/^(.*)( [\[\(].+[\)\]])?$/U', '$1 ' . $pkg . '$2', $name );
1204 }
1205
1206 public function get_form_cache( $name, $add_none = false, $excl_keys = array() ) {
1207
1208 if ( $this->p->debug->enabled ) {
1209
1210 $this->p->debug->log_args( array(
1211 'name' => $name,
1212 'add_none' => $add_none,
1213 ) );
1214 }
1215
1216 static $local_cache = array();
1217
1218 $filter_key = self::sanitize_key( $name );
1219 $filter_name = self::sanitize_hookname( 'wpsso_form_cache_' . $filter_key );
1220
1221 if ( ! isset( $local_cache[ $filter_key ] ) ) {
1222
1223 $local_cache[ $filter_key ] = array(); // Initialize a default value.
1224
1225 switch ( $filter_key ) {
1226
1227 case 'contact_types': // Returns a multi-dimentional array.
1228
1229 $this->get_form_cache( 'schema_types' );
1230
1231 $local_cache[ $filter_key ] =& $local_cache[ 'schema_types' ][ 'thing' ][ 'intangible' ][ 'structured.value' ][ 'contact.point' ];
1232
1233 break;
1234
1235 case 'contact_types_select':
1236
1237 $this->get_form_cache( 'contact_types' ); // Sets $local_cache[ 'contact_types' ].
1238
1239 $local_cache[ $filter_key ] = $this->p->schema->get_schema_types_select( $local_cache[ 'contact_types' ] );
1240
1241 break;
1242
1243 case 'og_types':
1244
1245 $local_cache[ $filter_key ] = $this->p->og->get_og_types();
1246
1247 break;
1248
1249 case 'og_types_select':
1250
1251 $local_cache[ $filter_key ] = $this->p->og->get_og_types_select();
1252
1253 break;
1254
1255 case 'org_names':
1256
1257 $local_cache[ $filter_key ] = array( 'site' => $this->p->cf[ 'form' ][ 'org_select' ][ 'site' ] );
1258
1259 break;
1260
1261 case 'org_types': // Returns a multi-dimentional array.
1262
1263 $this->get_form_cache( 'schema_types' );
1264
1265 $local_cache[ $filter_key ] =& $local_cache[ 'schema_types' ][ 'thing' ][ 'organization' ];
1266
1267 break;
1268
1269 case 'org_types_select':
1270 case 'org_types_select_strict': // Use strict for Google.
1271
1272 $this->get_form_cache( 'org_types' ); // Sets $local_cache[ 'org_types' ].
1273
1274 $local_cache[ $filter_key ] = $this->p->schema->get_schema_types_select( $local_cache[ 'org_types' ] );
1275
1276 /*
1277 * Remove Schema types that are also places.
1278 */
1279 if ( 'org_types_select_strict' === $filter_key ) {
1280
1281 foreach ( $this->get_form_cache( 'place_types_select' ) as $key => $val ) {
1282
1283 unset ( $local_cache[ $filter_key ][ $key ] );
1284 }
1285 }
1286
1287 break;
1288
1289 case 'person_names':
1290
1291 $local_cache[ $filter_key ] = WpssoUser::get_persons_names();
1292
1293 break;
1294
1295 case 'place_types': // Returns a multi-dimentional array.
1296
1297 $this->get_form_cache( 'schema_types' );
1298
1299 $local_cache[ $filter_key ] =& $local_cache[ 'schema_types' ][ 'thing' ][ 'place' ];
1300
1301 break;
1302
1303 case 'place_types_select':
1304 case 'place_types_select_strict':
1305
1306 $this->get_form_cache( 'place_types' );
1307
1308 $local_cache[ $filter_key ] = $this->p->schema->get_schema_types_select( $local_cache[ 'place_types' ] );
1309
1310 /*
1311 * Remove Schema types that are also organizations.
1312 */
1313 if ( 'place_types_select_strict' === $filter_key ) {
1314
1315 foreach ( $this->get_form_cache( 'org_types_select' ) as $key => $val ) {
1316
1317 unset ( $local_cache[ $filter_key ][ $key ] );
1318 }
1319 }
1320
1321 break;
1322
1323 case 'schema_types': // Returns a multi-dimentional array.
1324
1325 $local_cache[ $filter_key ] = $this->p->schema->get_schema_types( $flatten = false );
1326
1327 break;
1328
1329 case 'schema_types_select':
1330
1331 $local_cache[ $filter_key ] = $this->p->schema->get_schema_types_select();
1332
1333 break;
1334 }
1335
1336 if ( $this->p->debug->enabled ) {
1337
1338 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
1339 }
1340
1341 $local_cache[ $filter_key ] = apply_filters( $filter_name, $local_cache[ $filter_key ] );
1342
1343 } elseif ( $this->p->debug->enabled ) {
1344
1345 $this->p->debug->log( 'using existing form cache entry for ' . $filter_key );
1346 }
1347
1348 if ( isset( $local_cache[ $filter_key ][ 'none' ] ) ) { // Just in case.
1349
1350 unset( $local_cache[ $filter_key ][ 'none' ] );
1351 }
1352
1353 /*
1354 * Maybe modify the returned array.
1355 */
1356 if ( empty( $add_none ) && empty( $excl_keys ) ) {
1357
1358 if ( $this->p->debug->enabled ) {
1359
1360 $this->p->debug->log( 'returning cached array' );
1361 }
1362
1363 return $local_cache[ $filter_key ];
1364 }
1365
1366 $ret = $local_cache[ $filter_key ];
1367
1368 if ( $add_none ) {
1369
1370 if ( $this->p->debug->enabled ) {
1371
1372 $this->p->debug->log( 'adding "none" as first element' );
1373 }
1374
1375 $ret = array( 'none' => '[None]' ) + $ret;
1376 }
1377
1378 if ( ! empty( $excl_keys ) ) {
1379
1380 foreach ( $excl_keys as $key ) {
1381
1382 unset( $ret[ $key ] );
1383 }
1384 }
1385
1386 if ( $this->p->debug->enabled ) {
1387
1388 $this->p->debug->log_arr( 'returning modified array', $ret );
1389 }
1390
1391 return $ret;
1392 }
1393
1394 /*
1395 * Returns an associative array, with 'none' as the first element.
1396 */
1397 public function get_article_sections() {
1398
1399 if ( $this->p->debug->enabled ) {
1400
1401 $this->p->debug->mark();
1402 }
1403
1404 $sections = array();
1405
1406 if ( ! defined( 'WPSSO_ARTICLE_SECTIONS_LIST' ) || empty( WPSSO_ARTICLE_SECTIONS_LIST ) ) {
1407
1408 return $sections;
1409 }
1410
1411 $text_list_file = self::get_file_path_locale( WPSSO_ARTICLE_SECTIONS_LIST );
1412 $cache_md5_pre = 'wpsso_f_';
1413 $cache_exp_secs = MONTH_IN_SECONDS;
1414 $cache_salt = __METHOD__ . '(' . $text_list_file . ')';
1415 $cache_id = $cache_md5_pre . md5( $cache_salt );
1416
1417 if ( $cache_exp_secs > 0 ) {
1418
1419
1420 if ( $this->p->debug->enabled ) {
1421
1422 $this->p->debug->log( 'transient cache salt ' . $cache_salt );
1423 }
1424
1425 $sections = get_transient( $cache_id );
1426
1427 if ( is_array( $sections ) ) {
1428
1429 if ( $this->p->debug->enabled ) {
1430
1431 $this->p->debug->log( 'article sections retrieved from transient ' . $cache_id );
1432 }
1433
1434 return $sections;
1435 }
1436 }
1437
1438 $raw_sections = file( $text_list_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); // Returns false on error.
1439
1440 if ( ! is_array( $raw_sections ) ) {
1441
1442 $error_pre = sprintf( '%s error:', __METHOD__ );
1443 $notice_msg = sprintf( __( 'Error reading the %s file for the article sections list.', 'wpsso' ), $text_list_file );
1444
1445 if ( $this->p->notice->is_admin_pre_notices() ) {
1446
1447 $this->p->notice->err( $notice_msg );
1448 }
1449
1450 self::safe_error_log( $error_pre . ' ' . $notice_msg );
1451
1452 if ( $this->p->debug->enabled ) {
1453
1454 $this->p->debug->log( 'error reading %s article sections list file' );
1455 }
1456
1457 return array();
1458 }
1459
1460 $sections = array();
1461
1462 foreach ( $raw_sections as $num => $section_name ) {
1463
1464 if ( 0 === strpos( $section_name, '#' ) ) { // Skip comment lines.
1465
1466 continue;
1467 }
1468
1469 $sections[ $section_name ] = $section_name;
1470
1471 unset( $sections[ $num ] ); // Save memory and unset as we go.
1472 }
1473
1474 unset( $raw_sections );
1475
1476 $sections = apply_filters( 'wpsso_article_sections', $sections );
1477
1478 asort( $sections, SORT_NATURAL );
1479
1480 $sections = array( 'none' => '[None]' ) + $sections; // After sorting the array, put 'none' first.
1481
1482 if ( $cache_exp_secs > 0 ) {
1483
1484 set_transient( $cache_id, $sections, $cache_exp_secs );
1485
1486 if ( $this->p->debug->enabled ) {
1487
1488 $this->p->debug->log( 'article sections saved to transient cache for ' . $cache_exp_secs . ' seconds' );
1489 }
1490 }
1491
1492 return $sections;
1493 }
1494
1495 /*
1496 * Format the product category list from https://www.google.com/basepages/producttype/taxonomy-with-ids.en-US.txt.
1497 *
1498 * Returns an associative array, with 'none' as the first element.
1499 */
1500 public function get_google_product_categories() {
1501
1502 if ( $this->p->debug->enabled ) {
1503
1504 $this->p->debug->mark();
1505 }
1506
1507 $categories = array();
1508
1509 if ( ! defined( 'WPSSO_PRODUCT_CATEGORIES_LIST' ) || empty( WPSSO_PRODUCT_CATEGORIES_LIST ) ) {
1510
1511 return $categories;
1512 }
1513
1514 $text_list_file = self::get_file_path_locale( WPSSO_PRODUCT_CATEGORIES_LIST );
1515 $cache_md5_pre = 'wpsso_f_';
1516 $cache_exp_secs = MONTH_IN_SECONDS;
1517 $cache_salt = __METHOD__ . '(' . $text_list_file . ')';
1518 $cache_id = $cache_md5_pre . md5( $cache_salt );
1519
1520 if ( $cache_exp_secs > 0 ) {
1521
1522 if ( $this->p->debug->enabled ) {
1523
1524 $this->p->debug->log( 'transient cache salt ' . $cache_salt );
1525 }
1526
1527 $categories = get_transient( $cache_id );
1528
1529 if ( is_array( $categories ) ) {
1530
1531 if ( $this->p->debug->enabled ) {
1532
1533 $this->p->debug->log( 'product categories retrieved from transient ' . $cache_id );
1534 }
1535
1536 return $categories;
1537 }
1538 }
1539
1540 $raw_categories = file( $text_list_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); // Returns false on error.
1541
1542 if ( ! is_array( $raw_categories ) ) {
1543
1544 $error_pre = sprintf( '%s error:', __METHOD__ );
1545 $notice_msg = sprintf( __( 'Error reading the %s file for the product categories list.', 'wpsso' ), $text_list_file );
1546
1547 if ( $this->p->notice->is_admin_pre_notices() ) {
1548
1549 $this->p->notice->err( $notice_msg );
1550 }
1551
1552 self::safe_error_log( $error_pre . ' ' . $notice_msg );
1553
1554 if ( $this->p->debug->enabled ) {
1555
1556 $this->p->debug->log( 'error reading %s product categories list file' );
1557 }
1558
1559 return array();
1560 }
1561
1562 $categories = array();
1563
1564 foreach ( $raw_categories as $num => $category_id_name ) {
1565
1566 if ( 0 === strpos( $category_id_name, '#' ) ) { // Skip comment lines.
1567
1568 continue;
1569 }
1570
1571 if ( preg_match( '/^([0-9]+) - (.*)$/', $category_id_name, $match ) ) {
1572
1573 $categories[ $match[ 1 ] ] = $match[ 2 ];
1574 }
1575
1576 unset( $raw_categories[ $num ] ); // Save memory and unset as we go.
1577 }
1578
1579 unset( $raw_categories );
1580
1581 $categories = apply_filters( 'wpsso_google_product_categories', $categories );
1582
1583 asort( $categories, SORT_NATURAL );
1584
1585 $categories = array( 'none' => '[None]' ) + $categories; // After sorting the array, put 'none' first.
1586
1587 if ( $cache_exp_secs > 0 ) {
1588
1589 set_transient( $cache_id, $categories, $cache_exp_secs );
1590
1591 if ( $this->p->debug->enabled ) {
1592
1593 $this->p->debug->log( 'product categories saved to transient cache for ' . $cache_exp_secs . ' seconds' );
1594 }
1595 }
1596
1597 return $categories;
1598 }
1599
1600 /*
1601 * See WpssoMedia->add_og_video_from_url().
1602 */
1603 public function is_html_head_meta_url_cached( $url, $cache_exp_secs = 0 ) {
1604
1605 if ( empty( $url ) ) return false; // Just in case.
1606
1607 if ( false !== stripos( $url, '<html' ) ) return false; // Request contains html.
1608
1609 if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) return false; // Request is an invalid url.
1610
1611 $cache_format = 'url';
1612 $cache_type = 'file';
1613 $cache_file_ext = '.html';
1614
1615 $cache_url = $this->p->cache->is_cached( $url, $cache_format, $cache_type, $cache_exp_secs, $cache_file_ext );
1616
1617 return $cache_url;
1618 }
1619
1620 /*
1621 * Query argument examples:
1622 *
1623 * /html/head/link|/html/head/meta
1624 * /html/head/link[@rel="canonical"]
1625 * /html/head/meta[starts-with(@property, "og:video:")]
1626 *
1627 * See WpssoMedia->add_og_video_from_url().
1628 * See WpssoPost->check_post_head().
1629 * See WpssoProMediaSoundcloud->filter_video_details().
1630 * See WpssoProMediaWistia->filter_video_details().
1631 */
1632 public function get_html_head_meta( $request, $query = '/html/head/meta', $libxml_errors = false, $cache_exp_secs = 0, $curl_opts = array() ) {
1633
1634 if ( $this->p->debug->enabled ) {
1635
1636 $this->p->debug->mark();
1637 }
1638
1639 $is_admin = is_admin(); // Optimize and call once.
1640
1641 if ( ! function_exists( 'mb_convert_encoding' ) ) {
1642
1643 $this->php_function_missing( 'mb_convert_encoding()', __METHOD__ );
1644
1645 return false;
1646 }
1647
1648 if ( ! class_exists( 'DOMDocument' ) ) {
1649
1650 $this->php_class_missing( 'DOMDocument', __METHOD__ );
1651
1652 return false;
1653 }
1654
1655 if ( empty( $request ) ) { // Just in case.
1656
1657 if ( $this->p->debug->enabled ) {
1658
1659 $this->p->debug->log( 'exiting early: the request argument is empty' );
1660 }
1661
1662 return false;
1663 }
1664
1665 if ( empty( $query ) ) { // Just in case.
1666
1667 if ( $this->p->debug->enabled ) {
1668
1669 $this->p->debug->log( 'exiting early: the query argument is empty' );
1670 }
1671
1672 return false;
1673 }
1674
1675 if ( false !== stripos( $request, '<html' ) ) { // Request contains html.
1676
1677 if ( $this->p->debug->enabled ) {
1678
1679 $this->p->debug->log( 'using the html submitted as the request argument' );
1680 }
1681
1682 $html = $request;
1683
1684 $request = false; // Just in case.
1685
1686 } elseif ( false === filter_var( $request, FILTER_VALIDATE_URL ) ) { // Request is an invalid url.
1687
1688 if ( $this->p->debug->enabled ) {
1689
1690 $this->p->debug->log( 'exiting early: request argument is not html or a valid url' );
1691 }
1692
1693 if ( $is_admin ) {
1694
1695 $this->p->notice->err( sprintf( __( 'The <code>%1$s</code> request argument is not HTML or a valid URL.',
1696 'wpsso' ), __METHOD__ . '()' ) );
1697 }
1698
1699 return false;
1700
1701 } else {
1702
1703 if ( $this->p->debug->enabled ) {
1704
1705 $this->p->debug->log( 'getting HTML for ' . $request );
1706 }
1707
1708 $cache_format = 'raw';
1709 $cache_type = 'file';
1710 $cache_file_ext = '.html';
1711
1712 $html = $this->p->cache->get( $request, $cache_format, $cache_type, $cache_exp_secs, $cache_file_ext, $curl_opts );
1713
1714 if ( empty( $html ) ) {
1715
1716 if ( $this->p->debug->enabled ) {
1717
1718 $this->p->debug->log( 'exiting early: error getting HTML from ' . $request );
1719 }
1720
1721 if ( $is_admin ) {
1722
1723 $this->p->notice->err( sprintf( __( 'Error getting HTML from <a href="%1$s">%1$s</a>.', 'wpsso' ), $request ) );
1724 }
1725
1726 return false;
1727 }
1728 }
1729
1730 if ( function_exists( 'mb_convert_encoding' ) ) { // Just in case.
1731
1732 /*
1733 * Use call_user_func_array() to pass $html by reference.
1734 */
1735 call_user_func_array( 'mb_convert_encoding', array( &$html, $to_encoding = 'HTML-ENTITIES', $from_encoding = 'UTF-8' ) );
1736 }
1737
1738 /*
1739 * U = Invert greediness of quantifiers, so they are NOT greedy by default, but become greedy if followed by ?.
1740 * m = The "^" and "$" constructs match newlines and the complete subject string.
1741 * s = A dot metacharacter in the pattern matches all characters, including newlines.
1742 */
1743 $html = preg_replace( '/<!--.*-->/Ums', '', $html );
1744
1745 if ( empty( $html ) ) { // Returned html for url is empty.
1746
1747 if ( $request ) {
1748
1749 if ( $this->p->debug->enabled ) {
1750
1751 $this->p->debug->log( 'exiting early: html for ' . $request . ' is empty' );
1752 }
1753
1754 if ( $is_admin ) {
1755
1756 $this->p->notice->err( sprintf( __( 'Webpage retrieved from <a href="%1$s">%1$s</a> is empty.', 'wpsso' ), $request ) );
1757 }
1758
1759 } elseif ( $this->p->debug->enabled ) {
1760
1761 $this->p->debug->log( 'exiting early: submitted html is empty' );
1762 }
1763
1764 return false;
1765
1766 }
1767
1768 $doc = new DOMDocument(); // Since PHP v4.1.
1769
1770 if ( function_exists( 'libxml_use_internal_errors' ) ) { // Since PHP v5.1.
1771
1772 $libxml_prev_state = libxml_use_internal_errors( true ); // Enable user error handling.
1773
1774 if ( ! $doc->loadHTML( $html ) ) { // loadXML() is too strict for most webpages.
1775
1776 if ( $this->p->debug->enabled ) {
1777
1778 $this->p->debug->log( 'loadHTML returned error(s)' );
1779 }
1780
1781 /*
1782 * libXMLError {
1783 * public int $level;
1784 * public int $code;
1785 * public int $column;
1786 * public string $message;
1787 * public string $file;
1788 * public int $line;
1789 * }
1790 */
1791 foreach ( libxml_get_errors() as $error ) {
1792
1793 if ( $this->p->debug->enabled ) {
1794
1795 $this->p->debug->log( 'libxml error: ' . $error->message );
1796 }
1797
1798 if ( $libxml_errors ) {
1799
1800 if ( $is_admin ) {
1801
1802 $this->p->notice->err( 'PHP libXML error: ' . $error->message );
1803 }
1804 }
1805 }
1806
1807 libxml_clear_errors(); // Clear any HTML parsing errors.
1808
1809 } elseif ( $this->p->debug->enabled ) {
1810
1811 $this->p->debug->log( 'loadHTML was successful' );
1812 }
1813
1814 libxml_use_internal_errors( $libxml_prev_state ); // Restore previous error handling.
1815
1816 } else @$doc->loadHTML( $html ); // Load HTML and ignore errors.
1817
1818 unset( $html ); // No longer required.
1819
1820 $xpath = new DOMXPath( $doc );
1821 $metas = $xpath->query( $query );
1822 $mt_ret = array();
1823
1824 foreach ( $metas as $m ) {
1825
1826 $m_atts = array(); // Put all attributes in a single array.
1827
1828 foreach ( $m->attributes as $a ) {
1829
1830 $m_atts[ $a->name ] = $a->value;
1831 }
1832
1833 if ( isset( $m->textContent ) ) {
1834
1835 $m_atts[ 'textContent' ] = $m->textContent;
1836 }
1837
1838 $mt_ret[ $m->tagName ][] = $m_atts;
1839 }
1840
1841 if ( $this->p->debug->enabled ) {
1842
1843 if ( empty( $mt_ret ) ) { // Empty array.
1844
1845 if ( false === $request ) { // $request argument is html
1846
1847 $this->p->debug->log( 'meta tags found in submitted html' );
1848
1849 } else $this->p->debug->log( 'no meta tags found in ' . $request );
1850
1851 } else $this->p->debug->log( 'returning array of ' . count( $mt_ret ) . ' meta tags' );
1852 }
1853
1854 return $mt_ret;
1855 }
1856
1857 public function php_class_missing( $class, $method = null ) {
1858
1859 if ( $this->p->debug->enabled ) {
1860
1861 $this->p->debug->log( $class . ' PHP class is missing' );
1862 }
1863
1864 if ( is_admin() ) {
1865
1866 // translators: %1$s is the class name.
1867 $this->p->notice->err( sprintf( __( 'The PHP %s class is missing &ndash; contact your hosting provider to have the missing class installed.', 'wpsso' ), '<code>' . $class . '</code>' ) );
1868 }
1869 }
1870
1871 public function php_function_missing( $function, $method = null ) {
1872
1873 if ( $this->p->debug->enabled ) {
1874
1875 $this->p->debug->log( $function . ' PHP function is missing' );
1876 }
1877
1878 if ( is_admin() ) {
1879
1880 // translators: %1$s is the function name.
1881 $this->p->notice->err( sprintf( __( 'The PHP %s function is missing &ndash; contact your hosting provider to have the missing function installed.', 'wpsso' ), '<code>' . $function . '</code>' ) );
1882 }
1883 }
1884
1885 /*
1886 * Used by WpssoHead->show_head() and WpssoAdminHead->__construct().
1887 */
1888 public function log_is_functions() {
1889
1890 if ( ! $this->p->debug->enabled ) { // Nothing to do.
1891
1892 return;
1893 }
1894
1895 $function_info = $this->get_is_functions();
1896
1897 foreach ( $function_info as $function => $info ) {
1898
1899 $this->p->debug->log( $info[ 0 ] );
1900 }
1901 }
1902
1903 public function get_is_functions() {
1904
1905 $function_info = array();
1906
1907 foreach ( apply_filters( 'wpsso_is_functions', $this->is_functions ) as $function ) {
1908
1909 if ( is_callable( $function ) ) {
1910
1911 $mtime_start = microtime( $get_float = true );
1912 $ret_val = $function();
1913 $mtime_total = microtime( $get_float = true ) - $mtime_start;
1914
1915 $function_info[ $function ] = array(
1916 sprintf( '%-40s (%f secs)', $function . '() = ' . ( $ret_val ? 'TRUE' : 'false' ), $mtime_total ),
1917 $ret_val, $mtime_total,
1918 );
1919
1920 } else $function_info[ $function ] = array( $function . '() not found', null, 0 );
1921 }
1922
1923 return $function_info;
1924 }
1925
1926 /*
1927 * Some themes and plugins have been known to hook the WordPress 'get_shortlink' filter and return an empty URL to
1928 * disable the WordPress shortlink meta tag. This breaks the WordPress wp_get_shortlink() function and is a
1929 * violation of the WordPress theme guidelines.
1930 *
1931 * This method calls the WordPress wp_get_shortlink() function, and if an empty string is returned, calls an
1932 * unfiltered version of the same function.
1933 *
1934 * $context = 'blog', 'post' (default), 'media', or 'query'
1935 */
1936 public function get_shortlink( $mod, $context = 'post', $allow_slugs = true ) {
1937
1938 /*
1939 * The $mod array argument is preferred but not required.
1940 */
1941 if ( ! is_array( $mod ) ) {
1942
1943 if ( $this->p->debug->enabled ) {
1944
1945 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
1946 }
1947
1948 $mod = $this->p->page->get_mod( $mod );
1949 }
1950
1951 $shortlink = '';
1952
1953 if ( $mod[ 'is_post' ] && $mod[ 'id' ] ) { // Just in case.
1954
1955 $shortlink = wp_get_shortlink( $mod[ 'id' ], $context, $allow_slugs );
1956
1957 if ( empty( $shortlink ) || ! is_string( $shortlink) || false === filter_var( $shortlink, FILTER_VALIDATE_URL ) ) {
1958
1959 $shortlink = SucomUtilWP::raw_wp_get_shortlink( $mod[ 'id' ], $context, $allow_slugs );
1960 }
1961 }
1962
1963 return $shortlink;
1964 }
1965
1966 /*
1967 * Shorten URL using the selected shortening service.
1968 */
1969 public function shorten_url( $long_url, $mod = false ) {
1970
1971 $shortener = isset( $this->p->options[ 'plugin_shortener' ] ) ? $this->p->options[ 'plugin_shortener' ] : 'none';
1972
1973 if ( ! empty( $shortener ) && 'none' !== $shortener ) {
1974
1975 $short_url = apply_filters( 'wpsso_get_short_url', $long_url, $shortener, $mod );
1976
1977 if ( false !== filter_var( $short_url, FILTER_VALIDATE_URL ) ) { // Make sure the returned URL is valid.
1978
1979 return $short_url;
1980 }
1981 }
1982
1983 return $long_url;
1984 }
1985
1986 public function json_format( array $data, $options = 0, $depth = 32 ) {
1987
1988 if ( 0 === $options ) {
1989
1990 $options = JSON_UNESCAPED_SLASHES;
1991 }
1992
1993 if ( $this->is_json_pretty() ) {
1994
1995 $options = $options|JSON_PRETTY_PRINT;
1996 }
1997
1998 return wp_json_encode( $data, $options, $depth );
1999 }
2000
2001 /*
2002 * Get the sitemaps alternates array.
2003 *
2004 * Example:
2005 *
2006 * $alternates = array(
2007 * array(
2008 * 'href' => 'https://example.com/en/page-1/',
2009 * 'hreflang' => 'en_US',
2010 * ),
2011 * array(
2012 * 'href' => 'https://example.com/fr/page-1/',
2013 * 'hreflang' => 'fr_FR',
2014 * ),
2015 * array(
2016 * 'href' => 'https://example.com/es/page-1/',
2017 * 'hreflang' => 'es_ES',
2018 * ),
2019 * );
2020 */
2021 public function get_sitemaps_alternates( array $mod ) {
2022
2023 $alternates = apply_filters( 'wpsso_sitemaps_alternates', array(), $mod );
2024
2025 if ( $this->p->debug->enabled ) {
2026
2027 $this->p->debug->log_arr( 'alternates', $alternates );
2028 }
2029
2030 return $alternates;
2031 }
2032
2033 /*
2034 * See https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps.
2035 * See WpssoWpsmSitemapsFilters->wp_sitemaps_posts_entry().
2036 * See WpssoWpsmSitemapsFilters->wp_sitemaps_taxonomies_entry().
2037 * See WpssoWpsmSitemapsFilters->wp_sitemaps_users_entry().
2038 */
2039 public function get_sitemaps_images( array $mod, $num = 1 ) {
2040
2041 $images = array();
2042 $size_names = 'schema';
2043 $md_pre = array( 'schema', 'og' );
2044 $mt_images = $this->p->media->get_all_images( $num, $size_names, $mod, $md_pre );
2045
2046 foreach ( $mt_images as $mt_single_image ) {
2047
2048 if ( $image_url = SucomUtil::get_first_og_image_url( $mt_single_image ) ) {
2049
2050 $images[] = array(
2051 'image:loc' => $image_url,
2052 );
2053 }
2054 }
2055
2056 return $images;
2057 }
2058
2059 /*
2060 * See https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap.
2061 * See WpssoWpsmSitemapsFilters->wp_sitemaps_posts_entry().
2062 */
2063 public function get_sitemaps_news( array $mod, $news_pub_time, $news_pub_name ) {
2064
2065 $news = array();
2066
2067 $is_recent = $mod[ 'post_timestamp' ] > time() - $news_pub_time ? true : false;
2068
2069 if ( $is_recent ) {
2070
2071 $schema_lang = $this->p->schema->get_schema_lang( $mod, $trim_lang = true );
2072 $schema_title = $this->p->page->get_title( $mod, $md_key = 'schema_title', $max_len = 'schema_title' );
2073
2074 $news[] = array(
2075 'news:publication' => array(
2076 array(
2077 'news:name' => $news_pub_name,
2078 'news:language' => $schema_lang,
2079 ),
2080 ),
2081 'news:title' => $schema_title,
2082 'news:publication_date' => $mod[ 'post_time' ],
2083 );
2084 }
2085
2086 return $news;
2087 }
2088
2089 /*
2090 * See https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps.
2091 * See WpssoWpsmSitemapsFilters->wp_sitemaps_posts_entry().
2092 */
2093 public function get_sitemaps_videos( array $mod, $num = 1 ) {
2094
2095 $videos = array();
2096 $md_pre = array( 'schema', 'og' );
2097 $max_nums = $this->p->util->get_max_nums( $mod );
2098 $mt_videos = $this->p->media->get_all_videos( $max_nums[ 'og_vid_max' ], $mod, $md_pre, $force_prev = true );
2099 $added_urls = array();
2100
2101 foreach ( $mt_videos as $mt_single_video ) {
2102
2103 $video_url = SucomUtil::get_first_mt_media_url( $mt_single_video, $media_pre = 'og:video',
2104 $mt_suffixes = array( ':embed_url', ':stream_url' ) );
2105
2106 if ( $video_url ) {
2107
2108 if ( isset( $added_urls[ $video_url ] ) ) { // Prevent duplicates from 'text/html' and 'video/mp4' videos.
2109
2110 continue;
2111 }
2112
2113 $added_urls[ $video_url ] = true;
2114
2115 $image_url = SucomUtil::get_first_og_image_url( $mt_single_video );
2116
2117 $video = array(
2118 'video:thumbnail_loc' => $image_url,
2119 );
2120
2121 WpssoSchema::add_data_itemprop_from_assoc( $video, $mt_single_video, array(
2122 'video:player_loc' => 'og:video:embed_url',
2123 'video:content_loc' => 'og:video:stream_url',
2124 'video:title' => 'og:video:title',
2125 'video:description' => 'og:video:description',
2126 'video:duration' => 'og:video:duration',
2127 'video:publication_date' => 'og:video:published_date',
2128 'video:family_friendly' => 'og:video:family_friendly',
2129 'video:restriction_allow' => 'og:video:regions_allowed',
2130 ) );
2131
2132 if ( isset( $video[ 'video:duration' ] ) ) {
2133
2134 $video[ 'video:duration' ] = SucomUtil::maybe_iso8601_to_seconds( $video[ 'video:duration' ] );
2135 }
2136
2137 if ( isset( $video[ 'video:restriction_allow' ] ) ) {
2138
2139 if ( is_array( $video[ 'video:restriction_allow' ] ) ) {
2140
2141 $video[ 'video:restriction_allow' ] = implode( ' ', $video[ 'video:restriction_allow' ] );
2142 }
2143 }
2144
2145 $videos[] = $video;
2146 }
2147 }
2148
2149 return $videos;
2150 }
2151
2152 /*
2153 * Shorten the canonical URL using the selected shortening service.
2154 *
2155 * $mod = true | false | post_id | $mod array ($mod array is preferred but not required).
2156 *
2157 * $md_key = 'canonical_url' | '' (empty value ignores custom URL).
2158 */
2159 public function get_canonical_short_url( $mod = false, $add_page = false, $md_key = 'canonical_url' ) {
2160
2161 $url = $this->get_canonical_url( $mod, $add_page, $md_key );
2162
2163 return $this->shorten_url( $url, $mod );
2164 }
2165
2166 /*
2167 * $mod = true | false | post_id | $mod array ($mod array is preferred but not required).
2168 *
2169 * $md_key = 'canonical_url' | '' (empty value ignores custom URL)
2170 */
2171 public function get_canonical_url( $mod = false, $add_page = false, $md_key = 'canonical_url' ) {
2172
2173 if ( $this->p->debug->enabled ) {
2174
2175 $this->p->debug->log_args( array(
2176 'mod' => $mod,
2177 'add_page' => $add_page,
2178 ) );
2179 }
2180
2181 /*
2182 * The $mod array argument is preferred but not required.
2183 */
2184 if ( ! is_array( $mod ) ) {
2185
2186 if ( $this->p->debug->enabled ) {
2187
2188 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
2189 }
2190
2191 $mod = $this->p->page->get_mod( $mod );
2192 }
2193
2194 /*
2195 * Prevent loops and optimize by returning the URL from local cache if possible.
2196 */
2197 static $local_cache = array();
2198
2199 $url = null;
2200 $is_custom = false;
2201 $cache_salt = false;
2202
2203 if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
2204
2205 /*
2206 * Note that the sort order, page number, locale, amp and embed checks are provided by
2207 * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
2208 */
2209 $cache_salt = self::get_mod_salt( $mod ) . '_add_page:' . (string) $add_page;
2210
2211 if ( $this->p->debug->enabled ) {
2212
2213 $this->p->debug->log( 'cache salt = ' . $cache_salt );
2214 }
2215
2216 if ( isset( $local_cache[ $cache_salt ] ) ) {
2217
2218 return $local_cache[ $cache_salt ];
2219 }
2220
2221 /*
2222 * WpssoUtil->is_canonical_disabled() returns true if:
2223 *
2224 * - An SEO plugin is active.
2225 * - The 'add_link_rel_canonical' option is unchecked.
2226 * - The 'wpsso_add_link_rel_canonical' filter returns false.
2227 * - The 'wpsso_canonical_disabled' filter returns true.
2228 */
2229 if ( $this->is_canonical_disabled() ) { // Just in case.
2230
2231 if ( $this->p->debug->enabled ) {
2232
2233 $this->p->debug->log( 'skipped custom canonical url: canonical is disabled' );
2234 }
2235
2236 } elseif ( ! empty( $md_key ) ) {
2237
2238 $url = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $md_key ); // Returns null if an index key is not found.
2239
2240 if ( ! empty( $url ) ) {
2241
2242 if ( $this->p->debug->enabled ) {
2243
2244 $this->p->debug->log( 'custom canonical url = ' . $url );
2245 }
2246
2247 $is_custom = true;
2248 }
2249 }
2250 }
2251
2252 if ( ! $is_custom ) { // No custom canonical url from the post, term, or user meta.
2253
2254 /*
2255 * Similar module type logic can be found in the following methods:
2256 *
2257 * See WpssoOpenGraph->get_mod_og_type().
2258 * See WpssoPage->get_description().
2259 * See WpssoPage->get_the_title().
2260 * See WpssoSchema->get_mod_schema_type().
2261 * See WpssoUtil->get_canonical_url().
2262 */
2263 if ( $mod[ 'is_home' ] ) {
2264
2265 $url = SucomUtilWP::get_home_url( $this->p->options, $mod );
2266
2267 $url = apply_filters( 'wpsso_home_url', $url, $mod );
2268
2269 } elseif ( $mod[ 'is_comment' ] ) {
2270
2271 if ( $mod[ 'id' ] ) { // Just in case.
2272
2273 $url = get_comment_link( $mod[ 'id' ] );
2274
2275 if ( $this->p->debug->enabled ) {
2276
2277 $this->p->debug->log( 'get_comment_link() returned "' . $url . '"' );
2278 }
2279
2280 $url = $this->is_string_url( $url, 'comment link' ); // Check for WP_Error.
2281 }
2282
2283 $url = apply_filters( 'wpsso_comment_url', $url, $mod );
2284
2285 } elseif ( $mod[ 'is_post' ] ) {
2286
2287 if ( $mod[ 'post_type' ] ) { // Just in case.
2288
2289 if ( $mod[ 'is_post_type_archive' ] ) { // The post id may be 0.
2290
2291 if ( $this->p->debug->enabled ) {
2292
2293 $this->p->debug->log( 'post type is archive' );
2294 }
2295
2296 $url = get_post_type_archive_link( $mod[ 'post_type' ] );
2297
2298 if ( $this->p->debug->enabled ) {
2299
2300 $this->p->debug->log( 'get_post_type_archive_link() returned "' . $url . '"' );
2301 }
2302
2303 $url = $this->is_string_url( $url, 'post type archive link' ); // Check for WP_Error.
2304
2305 } elseif ( $mod[ 'id' ] ) { // Just in case.
2306
2307 /*
2308 * Get the canonical URL of the published post.
2309 */
2310 if ( 'publish' !== $mod[ 'post_status' ] ) {
2311
2312 if ( $this->p->debug->enabled ) {
2313
2314 $this->p->debug->log( 'post status is not published' );
2315 }
2316
2317 if ( $mod[ 'wp_obj' ] ) { // Just in case.
2318
2319 $mod[ 'wp_obj' ]->post_status = 'publish';
2320
2321 if ( empty( $mod[ 'wp_obj' ]->post_name ) ) {
2322
2323 $mod[ 'wp_obj' ]->post_name = sanitize_title( $mod[ 'wp_obj' ]->post_title );
2324 }
2325
2326 $url = get_permalink( $mod[ 'wp_obj' ] );
2327
2328 if ( $this->p->debug->enabled ) {
2329
2330 $this->p->debug->log( 'get_permalink() returned "' . $url . '"' );
2331 }
2332 }
2333 }
2334
2335 if ( empty( $url ) ) { // Just in case.
2336
2337 if ( $this->p->debug->enabled ) {
2338
2339 $this->p->debug->log( 'getting permalink for post id ' . $mod[ 'id' ] );
2340 }
2341
2342 $url = get_permalink( $mod[ 'id' ] );
2343
2344 if ( $this->p->debug->enabled ) {
2345
2346 $this->p->debug->log( 'get_permalink() returned "' . $url . '"' );
2347 }
2348 }
2349
2350 $url = $this->is_string_url( $url, 'post permalink' ); // Check for WP_Error.
2351
2352 } elseif ( $this->p->debug->enabled ) {
2353
2354 $this->p->debug->log( 'no post id' );
2355 }
2356
2357 } elseif ( $this->p->debug->enabled ) {
2358
2359 $this->p->debug->log( 'no post type' );
2360 }
2361
2362 $url = apply_filters( 'wpsso_post_url', $url, $mod );
2363
2364 } elseif ( $mod[ 'is_term' ] ) {
2365
2366 if ( $mod[ 'id' ] ) { // Just in case.
2367
2368 $url = get_term_link( $mod[ 'id' ], $mod[ 'tax_slug' ] );
2369
2370 if ( $this->p->debug->enabled ) {
2371
2372 $this->p->debug->log( 'get_term_link() returned "' . $url . '"' );
2373 }
2374
2375 $url = $this->is_string_url( $url, 'term link' ); // Check for WP_Error.
2376 }
2377
2378 $url = apply_filters( 'wpsso_term_url', $url, $mod );
2379
2380 } elseif ( $mod[ 'is_user' ] ) {
2381
2382 if ( $mod[ 'id' ] ) { // Just in case.
2383
2384 $url = get_author_posts_url( $mod[ 'id' ] );
2385
2386 if ( $this->p->debug->enabled ) {
2387
2388 $this->p->debug->log( 'get_author_posts_url() returned "' . $url . '"' );
2389 }
2390
2391 $url = $this->is_string_url( $url, 'author posts url' ); // Check for WP_Error.
2392 }
2393
2394 $url = apply_filters( 'wpsso_user_url', $url, $mod );
2395
2396 } elseif ( $mod[ 'is_search' ] ) {
2397
2398 $url = get_search_link( $mod[ 'query_vars' ][ 's' ] );
2399
2400 if ( $this->p->debug->enabled ) {
2401
2402 $this->p->debug->log( 'get_search_link() returned "' . $url . '"' );
2403 }
2404
2405 $url = $this->is_string_url( $url, 'search link' ); // Check for WP_Error.
2406
2407 $url = apply_filters( 'wpsso_search_url', $url, $mod );
2408
2409 } elseif ( $mod[ 'is_archive' ] ) {
2410
2411 if ( $mod[ 'is_date' ] ) {
2412
2413 if ( $mod[ 'is_year' ] ) {
2414
2415 $url = get_year_link( $mod[ 'query_vars' ][ 'year' ] );
2416
2417 if ( $this->p->debug->enabled ) {
2418
2419 $this->p->debug->log( 'get_year_link() returned "' . $url . '"' );
2420 }
2421
2422 $url = $this->is_string_url( $url, 'year link' ); // Check for WP_Error.
2423
2424 } elseif ( $mod[ 'is_month' ] ) {
2425
2426 $url = get_month_link( $mod[ 'query_vars' ][ 'year' ], $mod[ 'query_vars' ][ 'monthnum' ] );
2427
2428 if ( $this->p->debug->enabled ) {
2429
2430 $this->p->debug->log( 'get_month_link() returned "' . $url . '"' );
2431 }
2432
2433 $url = $this->is_string_url( $url, 'month link' ); // Check for WP_Error.
2434
2435 } elseif ( $mod[ 'is_day' ] ) {
2436
2437 $url = get_day_link( $mod[ 'query_vars' ][ 'year' ], $mod[ 'query_vars' ][ 'monthnum' ], $mod[ 'query_vars' ][ 'day' ] );
2438
2439 if ( $this->p->debug->enabled ) {
2440
2441 $this->p->debug->log( 'get_day_link() returned "' . $url . '"' );
2442 }
2443
2444 $url = $this->is_string_url( $url, 'day link' ); // Check for WP_Error.
2445 }
2446 }
2447
2448 $url = apply_filters( 'wpsso_archive_page_url', $url, $mod );
2449 }
2450
2451 /*
2452 * Use the current URL as a fallback for themes and plugins that create public content and don't use the
2453 * standard WordPress functions / variables and/or are not properly integrated with WordPress (ie. they do
2454 * not use custom post types, taxonomies, terms, etc.).
2455 */
2456 if ( empty ( $url ) ) {
2457
2458 if ( $this->p->debug->enabled ) {
2459
2460 $this->p->debug->log( 'falling back to server request url' );
2461 }
2462
2463 $url = self::get_url( $remove_ignored_args = true ); // Uses a local cache.
2464
2465 $url = apply_filters( 'wpsso_server_request_url', $url );
2466 }
2467 }
2468
2469 /*
2470 * Maybe enforce the FORCE_SSL constant.
2471 */
2472 if ( strpos( $url, '://' ) ) { // Only check URLs with a protocol.
2473
2474 if ( self::get_const( 'FORCE_SSL' ) && ! self::is_https( $url ) ) {
2475
2476 $url = set_url_scheme( $url, 'https' );
2477 }
2478 }
2479
2480 $url = $this->get_url_paged( $url, $mod, $add_page );
2481
2482 $url = apply_filters( 'wpsso_canonical_url', $url, $mod, $add_page, $is_custom );
2483
2484 if ( ! empty( $cache_salt ) ) {
2485
2486 $local_cache[ $cache_salt ] = $url;
2487 }
2488
2489 return $url;
2490 }
2491
2492 public function get_oembed_url( $mod = false, $format = 'json' ) {
2493
2494 if ( $this->p->debug->enabled ) {
2495
2496 $this->p->debug->mark();
2497 }
2498
2499 $url = '';
2500
2501 /*
2502 * The $mod array argument is preferred but not required.
2503 */
2504 if ( ! is_array( $mod ) ) {
2505
2506 if ( $this->p->debug->enabled ) {
2507
2508 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
2509 }
2510
2511 $mod = $this->p->page->get_mod( $mod );
2512 }
2513
2514 if ( function_exists( 'get_oembed_endpoint_url' ) ) {
2515
2516 if ( $mod[ 'is_post' ] && $mod[ 'id' ] ) { // Just in case.
2517
2518 $url = $this->get_canonical_url( $mod );
2519
2520 $url = get_oembed_endpoint_url( $url, $format );
2521 }
2522 }
2523
2524 return apply_filters( 'wpsso_oembed_url', $url, $mod, $format );
2525 }
2526
2527 public function get_oembed_data( $mod = false, $width = '600' ) {
2528
2529 if ( $this->p->debug->enabled ) {
2530
2531 $this->p->debug->mark();
2532 }
2533
2534 $data = false;
2535
2536 /*
2537 * The $mod array argument is preferred but not required.
2538 */
2539 if ( ! is_array( $mod ) ) {
2540
2541 if ( $this->p->debug->enabled ) {
2542
2543 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
2544 }
2545
2546 $mod = $this->p->page->get_mod( $mod );
2547 }
2548
2549 if ( function_exists( 'get_oembed_response_data' ) ) {
2550
2551 if ( $mod[ 'is_post' ] && $mod[ 'id' ] ) { // Just in case.
2552
2553 $data = get_oembed_response_data( $mod[ 'id' ], $width ); // Returns false on error.
2554 }
2555 }
2556
2557 return apply_filters( 'wpsso_oembed_data', $data, $mod, $width );
2558 }
2559
2560 /*
2561 * $mixed = true | false | post_id | $mod array ($mod array is preferred but not required).
2562 *
2563 * $md_key = 'redirect_url' | '' (empty value ignores custom URL).
2564 */
2565 public function get_redirect_url( $mixed, $mod_id = null, $md_key = 'redirect_url' ) {
2566
2567 if ( $this->p->debug->enabled ) {
2568
2569 $this->p->debug->mark();
2570 }
2571
2572 $mod = false;
2573
2574 if ( ! empty( $mixed[ 'obj' ] ) ) {
2575
2576 $mod =& $mixed;
2577
2578 } elseif ( is_string( $mixed ) && isset( $this->p->$mixed ) && $mod_id ) { // Just in case.
2579
2580 $mod = $this->p->$mixed->get_mod( $mod_id );
2581
2582 } elseif ( ! is_array( $mod ) ) {
2583
2584 if ( $this->p->debug->enabled ) {
2585
2586 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
2587 }
2588
2589 $mod = $this->p->page->get_mod( $mixed );
2590 }
2591
2592 $url = null;
2593 $is_custom = false;
2594
2595 if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
2596
2597 /*
2598 * WpssoUtil->is_redirect_disabled() returns true if:
2599 *
2600 * - An SEO plugin is active.
2601 * - The 'wpsso_redirect_disabled' filter returns true.
2602 */
2603 if ( $this->is_redirect_disabled() ) { // Just in case.
2604
2605 if ( $this->p->debug->enabled ) {
2606
2607 $this->p->debug->log( 'skipped custom redirect url: redirect is disabled' );
2608 }
2609
2610 } elseif ( ! empty( $md_key ) ) {
2611
2612 $url = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $md_key ); // Returns null if an index key is not found.
2613
2614 if ( ! empty( $url ) ) {
2615
2616 if ( $this->p->debug->enabled ) {
2617
2618 $this->p->debug->log( 'custom redirect url = ' . $url );
2619 }
2620
2621 $is_custom = true;
2622 }
2623 }
2624 }
2625
2626 $url = apply_filters( 'wpsso_redirect_url', $url, $mod, $is_custom );
2627
2628 return $url;
2629 }
2630
2631 public function get_sharing_url( $mod = false, $add_page = false, $atts = array() ) {
2632
2633 if ( $this->p->debug->enabled ) {
2634
2635 $this->p->debug->log_args( array(
2636 'mod' => $mod,
2637 'add_page' => $add_page,
2638 'atts' => $atts,
2639 ) );
2640 }
2641
2642 $url = $this->get_canonical_url( $mod, $add_page );
2643
2644 $utm = array();
2645
2646 foreach ( array(
2647 'utm_medium',
2648 'utm_source',
2649 'utm_campaign',
2650 'utm_content',
2651 'utm_term',
2652 ) as $q ) {
2653
2654 /*
2655 * Ignore 0, false, null, and '' (empty string) values.
2656 */
2657 $utm[ $q ] = empty( $atts[ $q ] ) ? false : $atts[ $q ];
2658 }
2659
2660 $utm = apply_filters( 'wpsso_sharing_utm_args', $utm, $mod );
2661
2662 /*
2663 * To add UTM tracking query arguments we need at least 'utm_source', 'utm_medium', and 'utm_campaign'.
2664 */
2665 if ( ! empty( $utm[ 'utm_source' ] ) && ! empty( $utm[ 'utm_medium' ] ) && ! empty( $utm[ 'utm_campaign' ] ) ) {
2666
2667 $url = add_query_arg( array(
2668 'utm_medium' => $utm[ 'utm_medium' ], // Example: 'social'.
2669 'utm_source' => $utm[ 'utm_source' ], // Example: 'facebook'.
2670 'utm_campaign' => $utm[ 'utm_campaign' ], // Example: 'book-launch'
2671 'utm_content' => $utm[ 'utm_content' ],
2672 'utm_term' => $utm[ 'utm_term' ],
2673 ), $url );
2674 }
2675
2676 return apply_filters( 'wpsso_sharing_url', $url, $mod, $add_page );
2677 }
2678
2679 /*
2680 * Shorten the sharing URL using the selected shortening service.
2681 */
2682 public function get_sharing_short_url( $mod = false, $add_page = false, $atts = array() ) {
2683
2684 if ( $this->p->debug->enabled ) {
2685
2686 $this->p->debug->mark();
2687 }
2688
2689 $url = $this->get_sharing_url( $mod, $add_page, $atts );
2690
2691 return $this->shorten_url( $url, $mod );
2692 }
2693
2694 public function get_url_paged( $url, array $mod, $add_page = false ) {
2695
2696 if ( $this->p->debug->enabled ) {
2697
2698 $this->p->debug->mark();
2699 }
2700
2701 if ( empty( $url ) ) { // Just in case.
2702
2703 if ( $this->p->debug->enabled ) {
2704
2705 $this->p->debug->log( 'exiting early: url is empty' );
2706 }
2707
2708 return $url;
2709
2710 } elseif ( ! $add_page ) { // Just in case.
2711
2712 if ( $this->p->debug->enabled ) {
2713
2714 $this->p->debug->log( 'exiting early: add_page is false' );
2715 }
2716
2717 return $url;
2718 }
2719
2720 global $wp_rewrite;
2721
2722 if ( $this->p->debug->enabled ) {
2723
2724 $this->p->debug->log( 'pagination base = ' . $wp_rewrite->pagination_base );
2725 }
2726
2727 $using_permalinks = $wp_rewrite->using_permalinks();
2728 $have_query_args = false === strpos( $url, '?' ) ? false : true;
2729 $page_number = $this->get_page_number( $mod, $add_page );
2730
2731 if ( $mod[ 'is_archive' ] || $mod[ 'is_home_posts' ] ) {
2732
2733 if ( $this->p->debug->enabled ) {
2734
2735 $this->p->debug->log( 'is archive or home posts page' );
2736 }
2737
2738 if ( $page_number > 1 ) {
2739
2740 if ( ! $using_permalinks || $have_query_args ) {
2741
2742 $url = add_query_arg( 'paged', $page_number, $url );
2743
2744 } else {
2745
2746 $url = user_trailingslashit( trailingslashit( $url ) . trailingslashit( $wp_rewrite->pagination_base ) . $page_number );
2747 }
2748 }
2749
2750 } elseif ( ! $using_permalinks || $have_query_args ) {
2751
2752 if ( $this->p->debug->enabled ) {
2753
2754 $this->p->debug->log( 'not using permalinks or have query args' );
2755 }
2756
2757 /*
2758 * Note that the singular page query argument is named 'page' not 'paged'.
2759 */
2760 if ( $page_number > 1 ) {
2761
2762 $url = add_query_arg( 'page', $page_number, $url );
2763 }
2764
2765 if ( $mod[ 'comment_paged' ] > 1 ) {
2766
2767 $url = add_query_arg( 'cpage', $mod[ 'comment_paged' ], $url );
2768 }
2769
2770 } else {
2771
2772 if ( $page_number > 1 ) {
2773
2774 $url = user_trailingslashit( trailingslashit( $url ) . $page_number );
2775 }
2776
2777 if ( $mod[ 'comment_paged' ] > 1 ) {
2778
2779 $url = user_trailingslashit( trailingslashit( $url ) . $wp_rewrite->comments_pagination_base . '-' . $mod[ 'comment_paged' ] );
2780 }
2781 }
2782
2783 if ( $this->p->debug->enabled ) {
2784
2785 $this->p->debug->log( 'get url paged = ' . $url );
2786 }
2787
2788 return $url;
2789 }
2790
2791 /*
2792 * See WpssoUtil->get_url_paged().
2793 * See WpssoUtilInline->get_defaults().
2794 */
2795 public function get_page_number( array $mod, $add_page = false ) {
2796
2797 $page_number = 1;
2798
2799 if ( is_numeric( $add_page ) ) {
2800
2801 $page_number = $add_page > 1 ? $add_page : 1;
2802
2803 } else $page_number = $mod[ 'paged' ]; // False or a number.
2804
2805 return $page_number;
2806 }
2807
2808 /*
2809 * Returns for example "#sso-post-123", #sso-term-123-tax-faq-category with a $mod array or "#sso-" without.
2810 *
2811 * Called by WpssoFaqShortcodeFaq->do_shortcode().
2812 * Called by WpssoFaqShortcodeQuestion->do_shortcode().
2813 * Called by WpssoJsonTypeThing->filter_json_data_https_schema_org_thing().
2814 */
2815 public function get_fragment_anchor( $mod = null ) {
2816
2817 return '#sso-' . ( $mod ? SucomUtilWP::get_mod_css_id( $mod ) : '' );
2818 }
2819
2820 /*
2821 * Called by scheduled tasks to check the user ID value and possibly load a different textdomain language.
2822 *
2823 * See WpssoUser->schedule_add_person_role().
2824 * See WpssoUser->add_person_role().
2825 * See WpssoUser->schedule_remove_person_role().
2826 * See WpssoUser->remove_person_role().
2827 * See WpssoUtilCache->show_refresh_pending().
2828 * See WpssoUtilCache->schedule_refresh().
2829 * See WpssoUtilCache->refresh().
2830 */
2831 public function maybe_change_user_id( $user_id ) {
2832
2833 $current_user_id = get_current_user_id(); // Always returns an integer.
2834 $user_id = is_numeric( $user_id ) ? (int) $user_id : $current_user_id; // User ID can be true, false, null, or a number.
2835
2836 if ( empty( $user_id ) ) { // User ID is 0 (cron user, for example).
2837
2838 return $user_id;
2839
2840 } elseif ( $user_id === $current_user_id ) { // Nothing to do.
2841
2842 return $user_id;
2843 }
2844
2845 /*
2846 * The user ID is different than the current / effective user ID, so check if the user locale is different
2847 * to the current WordPress locale and load the user locale if required.
2848 */
2849 $current_locale = get_locale(); // Use the WordPress locale.
2850 $user_locale = get_metadata( 'user', $user_id, 'locale', $single = true );
2851
2852 $this->maybe_load_textdomain( $current_locale, $user_locale, $plugin_slug = 'wpsso' );
2853
2854 return $user_id;
2855 }
2856
2857 public function maybe_load_textdomain( $current_locale, $new_locale, $plugin_slug ) {
2858
2859 static $local_cache = array();
2860
2861 if ( isset( $local_cache[ $new_locale ][ $plugin_slug ] ) ) {
2862
2863 return $local_cache[ $new_locale ][ $plugin_slug ];
2864 }
2865
2866 if ( $new_locale && $new_locale !== $current_locale ) {
2867
2868 $rel_lang_path = $plugin_slug . '/languages/';
2869 $mofile = $plugin_slug . '-' . $new_locale . '.mo';
2870 $wp_mopath = defined( 'WP_LANG_DIR' ) ? WP_LANG_DIR . '/plugins/' . $mofile : false;
2871 $mu_plugin_mopath = defined( 'WPMU_PLUGIN_DIR' ) ? WPMU_PLUGIN_DIR . '/' . $rel_lang_path . $mofile : false;
2872 $plugin_mopath = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR . '/' . $rel_lang_path . $mofile : false;
2873
2874 /*
2875 * Try to load from the WordPress languages directory first.
2876 */
2877 if ( ( $wp_mopath && load_textdomain( $plugin_slug, $wp_mopath ) ) ||
2878 ( $mu_plugin_mopath && load_textdomain( $plugin_slug, $mu_plugin_mopath ) ) ||
2879 ( $plugin_mopath && load_textdomain( $plugin_slug, $plugin_mopath ) ) ) {
2880
2881 $this->p->notice->set_label_transl(); // Update the notice label.
2882
2883 return $local_cache[ $new_locale ][ $plugin_slug ] = true;
2884 }
2885 }
2886
2887 return $local_cache[ $new_locale ][ $plugin_slug ] = false;
2888 }
2889
2890 /*
2891 * Used by WpssoMedia get_content_images() and get_attachment_image_src().
2892 */
2893 public function fix_relative_url( $url ) {
2894
2895 if ( empty( $url ) || false !== strpos( $url, '://' ) ) {
2896
2897 return $url;
2898 }
2899
2900 if ( $this->p->debug->enabled ) {
2901
2902 $this->p->debug->log( 'relative url found = ' . $url );
2903 }
2904
2905 if ( 0 === strpos( $url, '//' ) ) { // Example: //host.com/dir/file/
2906
2907 $url = self::get_prot() . ':' . $url;
2908
2909 } elseif ( 0 === strpos( $url, '/' ) ) { // Example: /dir/file/
2910
2911 $url = home_url( $url );
2912
2913 } else { // Example: file/
2914
2915 $base = self::get_url( $remove_ignored_args = true ); // Uses a local cache.
2916
2917 if ( false !== strpos( $base, '?' ) ) {
2918
2919 $base_parts = explode( '?', $base );
2920
2921 $base = reset( $base_parts );
2922 }
2923
2924 $url = trailingslashit( $base, false ) . $url;
2925 }
2926
2927 if ( $this->p->debug->enabled ) {
2928
2929 $this->p->debug->log( 'relative url fixed = ' . $url );
2930 }
2931
2932 return $url;
2933 }
2934
2935 /*
2936 * WpssoUtil->is_canonical_disabled() returns true if:
2937 *
2938 * - An SEO plugin is active.
2939 * - The 'add_link_rel_canonical' option is unchecked.
2940 * - The 'wpsso_add_link_rel_canonical' filter returns false.
2941 * - The 'wpsso_canonical_disabled' filter returns true.
2942 */
2943 public function is_canonical_disabled() {
2944
2945 $disabled = false;
2946
2947 if ( ! empty( $this->p->avail[ 'seo' ][ 'any' ] ) ) {
2948
2949 $disabled = true;
2950
2951 } elseif ( empty( $this->p->options[ 'add_link_rel_canonical' ] ) ) {
2952
2953 $disabled = true;
2954 }
2955
2956 $disabled = apply_filters( 'wpsso_add_link_rel_canonical', ( $disabled ? false : true ) ) ? false : true;
2957
2958 $disabled = apply_filters( 'wpsso_canonical_disabled', $disabled );
2959
2960 return $disabled;
2961 }
2962
2963 /*
2964 * WpssoUtil->is_redirect_disabled() returns true if:
2965 *
2966 * - An SEO plugin is active.
2967 * - The 'wpsso_redirect_disabled' filter returns true.
2968 */
2969 public function is_redirect_disabled() {
2970
2971 $disabled = false;
2972
2973 if ( ! empty( $this->p->avail[ 'seo' ][ 'any' ] ) ) {
2974
2975 $disabled = true;
2976 }
2977
2978 $disabled = apply_filters( 'wpsso_redirect_disabled', $disabled );
2979
2980 return $disabled;
2981 }
2982
2983 /*
2984 * WpssoUtil->is_shortlink_disabled() returns true if:
2985 *
2986 * - The 'add_link_rel_shortlink' option is unchecked.
2987 * - The 'wpsso_add_link_rel_shortlink' filter returns false.
2988 * - The 'wpsso_shortlink_disabled' filter returns true.
2989 */
2990 public function is_shortlink_disabled() {
2991
2992 $disabled = false;
2993
2994 if ( empty( $this->p->options[ 'add_link_rel_shortlink' ] ) ) {
2995
2996 $disabled = true;
2997 }
2998
2999 $disabled = apply_filters( 'wpsso_add_link_rel_shortlink', ( $disabled ? false : true ) ) ? false : true;
3000
3001 $disabled = apply_filters( 'wpsso_shortlink_disabled', $disabled );
3002
3003 return $disabled;
3004 }
3005
3006 /*
3007 * WpssoUtil->is_title_tag_disabled() returns true if:
3008 *
3009 * - The theme does not support the 'title-tag' feature.
3010 * - The WPSSO_TITLE_TAG_DISABLE constant is true.
3011 *
3012 * See WpssoMessages->maybe_doc_title_disabled().
3013 */
3014 public function is_title_tag_disabled() {
3015
3016 $disabled = false;
3017
3018 if ( ! current_theme_supports( 'title-tag' ) ) {
3019
3020 $disabled = true;
3021
3022 } elseif ( SucomUtil::get_const( 'WPSSO_TITLE_TAG_DISABLE' ) ) {
3023
3024 $disabled = true;
3025 }
3026
3027 return $disabled;
3028 }
3029
3030 /*
3031 * WpssoUtil->is_seo_title_disabled() returns true if:
3032 *
3033 * - An SEO plugin is active.
3034 * - The theme does not support the 'title-tag' feature.
3035 * - The WPSSO_TITLE_TAG_DISABLE constant is true.
3036 * - The 'plugin_title_tag' option is not 'seo_title'.
3037 */
3038 public function is_seo_title_disabled() {
3039
3040 $disabled = false;
3041
3042 if ( ! empty( $this->p->avail[ 'seo' ][ 'any' ] ) ) {
3043
3044 $disabled = true;
3045
3046 /*
3047 * WpssoUtil->is_title_tag_disabled() returns true if:
3048 *
3049 * - The theme does not support the 'title-tag' feature.
3050 * - The WPSSO_TITLE_TAG_DISABLE constant is true.
3051 */
3052 } elseif ( $this->is_title_tag_disabled() ) {
3053
3054 $disabled = true;
3055
3056 } elseif ( 'seo_title' !== $this->p->options[ 'plugin_title_tag' ] ) {
3057
3058 $disabled = true;
3059 }
3060
3061 return $disabled;
3062 }
3063
3064 /*
3065 * WpssoUtil->is_seo_desc_disabled() returns true if:
3066 *
3067 * - An SEO plugin is active.
3068 * - The 'add_meta_name_description' option is unchecked.
3069 */
3070 public function is_seo_desc_disabled() {
3071
3072 $disabled = false;
3073
3074 if ( ! empty( $this->p->avail[ 'seo' ][ 'any' ] ) ) {
3075
3076 $disabled = true;
3077
3078 } elseif ( empty( $this->p->options[ 'add_meta_name_description' ] ) ) {
3079
3080 $disabled = true;
3081 }
3082
3083 return $disabled;
3084 }
3085
3086 /*
3087 * WpssoUtilRobots->is_disabled() returns true if:
3088 *
3089 * - An SEO plugin is active.
3090 * - The 'add_meta_name_robots' option is unchecked.
3091 * - The 'wpsso_robots_disabled' filter returns true.
3092 */
3093 public function is_robots_disabled() {
3094
3095 return $this->robots->is_disabled();
3096 }
3097
3098 public function is_schema_disabled() {
3099
3100 $disabled = false;
3101
3102 /*
3103 * Array element is false when the WPSSO_SCHEMA_MARKUP_DISABLE constant is true.
3104 *
3105 * See WpssoCheck->get_avail().
3106 */
3107 if ( isset( $this->p->avail[ 'p' ][ 'schema' ] ) && empty( $this->p->avail[ 'p' ][ 'schema' ] ) ) {
3108
3109 $disabled = true;
3110 }
3111
3112 return $disabled;
3113 }
3114
3115 public function is_sitemaps_disabled() {
3116
3117 return apply_filters( 'wp_sitemaps_enabled', true ) ? false : true;
3118 }
3119
3120 public function is_pin_img_disabled() {
3121
3122 return empty( $this->p->options[ 'pin_add_img_html' ] ) ? true : false;
3123 }
3124
3125 public function is_json_pretty() {
3126
3127 if ( $this->p->debug->enabled ) { // Always pretty JSON when debug is enabled.
3128
3129 return true;
3130 }
3131
3132 $is_pretty = empty( $this->p->options[ 'plugin_schema_json_min' ] ) ? true : false;
3133
3134 $is_pretty = (bool) apply_filters( 'wpsso_json_pretty_print', $is_pretty );
3135
3136 return $is_pretty ? true : false;
3137 }
3138
3139 /*
3140 * Note that WebP is only supported since PHP v7.1.
3141 */
3142 public function is_image_url( $image_url ) {
3143
3144 if ( $this->p->debug->enabled ) {
3145
3146 $this->p->debug->mark();
3147 }
3148
3149 /*
3150 * Example $image_info:
3151 *
3152 * Array (
3153 * [0] => 2048
3154 * [1] => 2048
3155 * [2] => 3
3156 * [3] => width="2048" height="2048"
3157 * [bits] => 8
3158 * [mime] => image/png
3159 * )
3160 */
3161 $image_info = $this->get_image_url_info( $image_url );
3162
3163 if ( empty( $image_info[ 2 ] ) ) { // Make sure we have an image type integer.
3164
3165 return false;
3166 }
3167
3168 return true;
3169 }
3170
3171 public function is_string_url( $url, $context ) {
3172
3173 if ( is_string( $url ) ) {
3174
3175 if ( $this->p->debug->enabled ) {
3176
3177 $this->p->debug->log( $context . ' url = ' . $url );
3178 }
3179
3180 return $url; // Stop here.
3181 }
3182
3183 if ( $this->p->debug->enabled ) {
3184
3185 $this->p->debug->log( $context . ' url is ' . gettype( $url ) );
3186
3187 if ( is_wp_error( $url ) ) {
3188
3189 $this->p->debug->log( $context . ' url error: ' . $url->get_error_message() );
3190 }
3191 }
3192
3193 return false;
3194 }
3195
3196 public function clear_uniq_urls( $image_sizes = 'default', $mod = false ) {
3197
3198 $cleared = 0; // Default value to return.
3199
3200 if ( ! is_array( $image_sizes ) ) {
3201
3202 $image_sizes = $this->get_image_size_names( $image_sizes, $sanitize = false ); // Always returns an array.
3203 }
3204
3205 /*
3206 * Note that the sort order, page number, locale, amp and embed checks are provided by
3207 * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
3208 */
3209 $cache_salt = SucomUtil::get_mod_salt( $mod );
3210
3211 foreach ( $image_sizes as $num => $uniq_context ) {
3212
3213 $uniq_context = preg_replace( '/-[0-9]+x[0-9]+$/', '', $uniq_context ); // Change 'wpsso-schema-1x1' to 'wpsso-schema'.
3214
3215 $image_sizes[ $num ] = $cache_salt ? $cache_salt . '_' . $uniq_context : $uniq_context;
3216 }
3217
3218 foreach ( array_unique( $image_sizes ) as $uniq_context ) {
3219
3220 if ( isset( $this->cache_uniq_urls[ $uniq_context ] ) ) { // Array to detect duplicate image URLs.
3221
3222 $cleared += count( $this->cache_uniq_urls[ $uniq_context ] );
3223 }
3224
3225 $this->cache_uniq_urls[ $uniq_context ] = array();
3226
3227 if ( $this->p->debug->enabled ) {
3228
3229 $this->p->debug->log( 'cleared uniq url cache for context ' . $uniq_context );
3230 }
3231 }
3232
3233 return $cleared;
3234 }
3235
3236 public function is_dupe_url( $url, $image_sizes = 'default', $mod = false ) {
3237
3238 return $this->is_uniq_url( $url, $image_sizes, $mod ) ? false : true;
3239 }
3240
3241 public function is_uniq_url( $url, $image_sizes = 'default', $mod = false ) {
3242
3243 $is_uniq = true; // Default value to return.
3244
3245 if ( empty( $url ) ) return false; // Just in case.
3246
3247 $url = $this->fix_relative_url( $url ); // Just in case.
3248
3249 if ( ! is_array( $image_sizes ) ) $image_sizes = array( $image_sizes );
3250
3251 /*
3252 * Note that the sort order, page number, locale, amp and embed checks are provided by
3253 * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
3254 */
3255 $cache_salt = SucomUtil::get_mod_salt( $mod );
3256
3257 foreach ( $image_sizes as $num => $uniq_context ) {
3258
3259 $uniq_context = preg_replace( '/-[0-9]+x[0-9]+$/', '', $uniq_context ); // Change 'wpsso-schema-1x1' to 'wpsso-schema'.
3260
3261 $image_sizes[ $num ] = $cache_salt ? $cache_salt . '_' . $uniq_context : $uniq_context;
3262 }
3263
3264 foreach ( array_unique( $image_sizes ) as $uniq_context ) {
3265
3266 if ( isset( $this->cache_uniq_urls[ $uniq_context ][ $url ] ) ) { // Array to detect duplicate image URLs.
3267
3268 if ( $this->p->debug->enabled ) {
3269
3270 $this->p->debug->log( 'duplicate url found for context ' . $uniq_context . ': ' . $url );
3271 }
3272
3273 $is_uniq = false; // Duplicate found.
3274
3275 } else {
3276
3277 if ( $this->p->debug->enabled ) {
3278
3279 $this->p->debug->log( 'unique url saved for context ' . $uniq_context . ': ' . $url );
3280 }
3281
3282 $this->cache_uniq_urls[ $uniq_context ][ $url ] = true;
3283 }
3284 }
3285
3286 return $is_uniq;
3287 }
3288
3289 public function is_maxed( &$arr, $num = 0 ) {
3290
3291 if ( ! is_array( $arr ) ) {
3292
3293 return false;
3294 }
3295
3296 if ( $num > 0 && count( $arr ) >= $num ) {
3297
3298 return true;
3299 }
3300
3301 return false;
3302 }
3303
3304 public function merge_max( &$dst, &$src, $num = 0 ) {
3305
3306 if ( ! is_array( $dst ) || ! is_array( $src ) ) {
3307
3308 return false;
3309 }
3310
3311 if ( ! empty( $src ) && array_filter( $src ) ) {
3312
3313 $dst = array_merge( $dst, $src );
3314 }
3315
3316 return $this->slice_max( $dst, $num ); // Returns true or false.
3317 }
3318
3319 public function push_max( &$dst, &$src, $num = 0 ) {
3320
3321 if ( ! is_array( $dst ) || ! is_array( $src ) ) {
3322
3323 return false;
3324 }
3325
3326 if ( ! empty( $src ) && array_filter( $src ) ) {
3327
3328 array_push( $dst, $src );
3329 }
3330
3331 return $this->slice_max( $dst, $num ); // Returns true or false.
3332 }
3333
3334 public function slice_max( &$arr, $num = 0 ) {
3335
3336 if ( ! is_array( $arr ) ) {
3337
3338 return false;
3339 }
3340
3341 $has_count = count( $arr );
3342
3343 if ( $num > 0 ) {
3344
3345 if ( $has_count == $num ) {
3346
3347 if ( $this->p->debug->enabled ) {
3348
3349 $this->p->debug->log( 'max values reached (' . $has_count . ' == ' . $num . ')' );
3350 }
3351
3352 return true;
3353
3354 } elseif ( $has_count > $num ) {
3355
3356 if ( $this->p->debug->enabled ) {
3357
3358 $this->p->debug->log( 'max values reached (' . $has_count . ' > ' . $num . ') - slicing array' );
3359 }
3360
3361 $arr = array_slice( $arr, 0, $num );
3362
3363 return true;
3364 }
3365 }
3366
3367 return false;
3368 }
3369
3370 /*
3371 * Get maximum media values from custom meta or plugin settings.
3372 */
3373 public function get_max_nums( array $mod, $opt_prefix = 'og' ) {
3374
3375 $max_nums = array();
3376 $max_opt_keys = array( $opt_prefix . '_vid_max', $opt_prefix . '_img_max' );
3377
3378 foreach ( $max_opt_keys as $opt_key ) {
3379
3380 if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
3381
3382 $max_val = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $opt_key ); // Returns null if an index key is not found.
3383
3384 } else $max_val = null; // Default value if index key is missing.
3385
3386 /*
3387 * Quick sanitation of returned value.
3388 */
3389 if ( $max_val !== null & is_numeric( $max_val ) && $max_val >= 0 ) {
3390
3391 $max_nums[ $opt_key ] = $max_val;
3392
3393 if ( $this->p->debug->enabled ) {
3394
3395 $this->p->debug->log( 'found custom meta ' . $opt_key . ' = ' . $max_val );
3396 }
3397
3398 } else $max_nums[ $opt_key ] = isset( $this->p->options[ $opt_key ] ) ? $this->p->options[ $opt_key ] : 0;
3399 }
3400
3401 return $max_nums;
3402 }
3403
3404 public function safe_apply_filters( array $args, array $mod = array(), $mtime_max = 0, $use_bfo = false ) {
3405
3406 if ( $this->p->debug->enabled ) {
3407
3408 $this->p->debug->mark();
3409 }
3410
3411 /*
3412 * Check for required apply_filters() arguments.
3413 */
3414 if ( empty( $args[ 0 ] ) ) {
3415
3416 if ( $this->p->debug->enabled ) {
3417
3418 $this->p->debug->log( 'exiting early: filter name missing from parameter array' );
3419 }
3420
3421 return '';
3422
3423 } elseif ( ! isset( $args[ 1 ] ) ) {
3424
3425 if ( $this->p->debug->enabled ) {
3426
3427 $this->p->debug->log( 'exiting early: filter value missing from parameter array' );
3428 }
3429
3430 return '';
3431 }
3432
3433 $filter_name = $args[ 0 ];
3434 $filter_value = $args[ 1 ];
3435
3436 if ( false === has_filter( $filter_name ) ) { // Skip if no filters.
3437
3438 if ( $this->p->debug->enabled ) {
3439
3440 $this->p->debug->log( 'exiting early: ' . $filter_name . ' has no filter hooks' );
3441 }
3442
3443 return $filter_value;
3444 }
3445
3446 /*
3447 * Prevent recursive loops - the global variable is defined before applying the filters.
3448 */
3449 if ( ! empty( $GLOBALS[ 'wpsso_doing_filter_' . $filter_name ] ) ) {
3450
3451 if ( $this->p->debug->enabled ) {
3452
3453 $this->p->debug->log( 'exiting early: global variable "wpsso_doing_filter_' . $filter_name . '" is true' );
3454 }
3455
3456 return $filter_value;
3457 }
3458
3459 /*
3460 * Hooked by some modules to perform actions before/after filtering the content.
3461 */
3462 do_action( 'wpsso_pre_apply_filters_text', $filter_name );
3463
3464 /*
3465 * Load the Block Filter Output (BFO) filters to block and show an error for incorrectly coded filters.
3466 */
3467 if ( $use_bfo ) {
3468
3469 $classname = apply_filters( 'wpsso_load_lib', false, 'com/bfo', 'SucomBFO' );
3470
3471 if ( is_string( $classname ) && class_exists( $classname ) ) {
3472
3473 $bfo_obj = new $classname( $this->p );
3474
3475 $bfo_obj->add_start_hooks( array( $filter_name ) );
3476 }
3477 }
3478
3479 /*
3480 * Save original post object, in case some filters modify the global $post.
3481 */
3482 global $post, $wp_query;
3483
3484 if ( $this->p->debug->enabled ) {
3485
3486 $this->p->debug->log( 'saving original post object ' . ( isset( $post->ID ) ? 'id ' . $post->ID : '(no post id)' ) );
3487 }
3488
3489 $post_pre_filter = $post; // Save original global post object.
3490 $wp_query_pre_filter = $wp_query; // Save original global wp_query.
3491
3492 /*
3493 * Make sure the $post object is correct before filtering.
3494 */
3495 if ( ! empty( $mod[ 'is_post' ] ) ) {
3496
3497 if ( ! empty( $mod[ 'id' ] ) ) { // Just in case.
3498
3499 if ( ! isset( $post->ID ) || (int) $post->ID !== (int) $mod[ 'id' ] ) {
3500
3501 if ( $this->p->debug->enabled ) {
3502
3503 $this->p->debug->log( 'resetting post object from mod id ' . $mod[ 'id' ] );
3504 }
3505
3506 $post = SucomUtilWP::get_post_object( $mod[ 'id' ] ); // Redefine the $post global.
3507
3508 } elseif ( $this->p->debug->enabled ) {
3509
3510 $this->p->debug->log( 'post object id matches the post mod id' );
3511 }
3512 }
3513 }
3514
3515 if ( $this->p->debug->enabled ) {
3516
3517 $this->p->debug->log( 'setting post data for template functions' );
3518 }
3519
3520 setup_postdata( $post );
3521
3522 /*
3523 * Prevent recursive loops and signal to other methods that the content filter is being applied to create a
3524 * description text - this avoids the addition of unnecessary HTML which will be removed anyway (social
3525 * sharing buttons, for example).
3526 */
3527 if ( $this->p->debug->enabled ) {
3528
3529 $this->p->debug->log( 'setting global wpsso_doing_filter_' . $filter_name );
3530 }
3531
3532 $GLOBALS[ 'wpsso_doing_filter_' . $filter_name ] = true; // Prevent recursive loops.
3533
3534 /*
3535 * Apply the filters.
3536 */
3537 if ( $this->p->debug->enabled ) {
3538
3539 $this->p->debug->mark( 'applying filters "' . $filter_name . '"' ); // Begin timer.
3540 }
3541
3542 $mtime_start = microtime( $get_float = true );
3543 $filter_value = call_user_func_array( 'apply_filters', $args );
3544 $mtime_total = microtime( $get_float = true ) - $mtime_start;
3545
3546 if ( $this->p->debug->enabled ) {
3547
3548 $this->p->debug->mark( 'applying filters "' . $filter_name . '"' ); // End timer.
3549 }
3550
3551 /*
3552 * Unset the recursive loop check.
3553 */
3554 if ( $this->p->debug->enabled ) {
3555
3556 $this->p->debug->log( 'unsetting global wpsso_doing_filter_' . $filter_name );
3557 }
3558
3559 unset( $GLOBALS[ 'wpsso_doing_filter_' . $filter_name ] ); // Un-prevent recursive loops.
3560
3561 /*
3562 * Issue warning for slow filter performance.
3563 */
3564 if ( $mtime_max > 0 && $mtime_total > $mtime_max ) {
3565
3566 $is_wp_filter = false;
3567
3568 switch ( $filter_name ) {
3569
3570 case 'get_the_excerpt':
3571 case 'the_content':
3572 case 'the_excerpt':
3573 case 'wp_title':
3574
3575 $is_wp_filter = true;
3576
3577 break;
3578 }
3579
3580 $current_url = isset( $_SERVER[ 'REQUEST_URI' ] ) ? $_SERVER[ 'REQUEST_URI' ] : '';
3581 $wp_ref_url = $is_wp_filter ? sprintf( __( 'https://developer.wordpress.org/reference/hooks/%s/', 'wpsso' ), $filter_name ) : '';
3582 $wp_ref_link = sprintf( '<a href="%1$s">%2$s</a>', $wp_ref_url, $filter_name );
3583 $error_pre = sprintf( __( '%s warning:', 'wpsso' ), __METHOD__ );
3584 $rec_max_msg = sprintf( __( 'longer than recommended max of %1$.3f secs', 'wpsso' ), $mtime_max );
3585 $notice_msg = sprintf( __( 'Slow filter hook(s) detected - WordPress took %1$.3f secs to execute the "%2$s" filter (%3$s).', 'wpsso' ),
3586 $mtime_total, $filter_name, $rec_max_msg );
3587
3588 if ( $wp_ref_url ) $notice_msg .= ' ' . sprintf( __( 'See %s for more information.', 'wpsso' ), $wp_ref_url );
3589 if ( $current_url ) $notice_msg .= ' ' . sprintf( __( 'Server request URI = %s', 'wpsso' ), $current_url );
3590
3591 self::safe_error_log( $error_pre . ' ' . $notice_msg );
3592
3593 if ( $this->p->debug->enabled ) {
3594
3595 $this->p->debug->log( sprintf( 'slow filter hook(s) detected - WordPress took %1$.3f secs to execute the "%2$s" filter',
3596 $mtime_total, $filter_name ) );
3597 }
3598
3599 if ( $this->p->notice->is_admin_pre_notices() ) {
3600
3601 /*
3602 * If this is a known WordPress filter, show a different and more complete notification message.
3603 */
3604 if ( $is_wp_filter ) {
3605
3606 $qm_plugin_link = '<a href="https://wordpress.org/plugins/query-monitor/">Query Monitor</a>';
3607 $option_label = _x( 'Disable Cache for Debugging', 'option label', 'wpsso' );
3608 $option_link = $this->p->util->get_admin_url( 'advanced#sucom-tabset_plugin-tab_settings', $option_label );
3609
3610 /*
3611 * Start a new notice message.
3612 */
3613 $notice_msg = sprintf( __( 'Slow filter hook(s) detected - the WordPress %1$s filter took %2$.3f seconds to execute.', 'wpsso' ), $wp_ref_link, $mtime_total ) . ' ';
3614
3615 $notice_msg .= sprintf( __( 'This is longer than the recommended maximum of %1$.3f seconds and may affect page load time.', 'wpsso' ), $mtime_max ) . ' ';
3616
3617 $notice_msg .= sprintf( __( 'You should consider reviewing active plugin and theme functions hooked into the WordPress %1$s filter for slow and/or sub-optimal PHP code.', 'wpsso' ), $wp_ref_link ) . ' ';
3618
3619 $notice_msg .= sprintf( __( 'Activating the %1$s plugin and enabling the the %2$s option (to apply the filter consistently) may provide more information on the specific hooks or PHP code affecting performance.', 'wpsso' ), $qm_plugin_link, $option_link );
3620 }
3621
3622 $notice_key = 'slow-filter-hooks-detected-' . $filter_name;
3623
3624 $this->p->notice->warn( $notice_msg, null, $notice_key, $dismiss_time = DAY_IN_SECONDS );
3625 }
3626 }
3627
3628 /*
3629 * Restore original post object.
3630 */
3631 if ( $this->p->debug->enabled ) {
3632
3633 $this->p->debug->log( 'restoring original post object ' . ( isset( $post_pre_filter->ID ) ? 'id ' . $post_pre_filter->ID : '(no post id)' ) );
3634 }
3635
3636 $post = $post_pre_filter; // Restore original global post object.
3637 $wp_query = $wp_query_pre_filter; // Restore original global wp_query.
3638
3639 if ( $this->p->debug->enabled ) {
3640
3641 $this->p->debug->log( 'restoring post data for template functions' );
3642 }
3643
3644 setup_postdata( $post );
3645
3646 /*
3647 * Remove the Block Filter Output (BFO) filters.
3648 */
3649 if ( $use_bfo ) {
3650
3651 $bfo_obj->remove_all_hooks( array( $filter_name ) );
3652 }
3653
3654 /*
3655 * Hooked by some modules to perform actions before/after filtering the content.
3656 */
3657 do_action( 'wpsso_after_apply_filters_text', $filter_name );
3658
3659 if ( $this->p->debug->enabled ) {
3660
3661 $this->p->debug->log( 'returning filtered value' );
3662 }
3663
3664 return $filter_value;
3665 }
3666
3667 public function get_admin_url( $menu_id = '', $link_text = '', $menu_lib = '' ) {
3668
3669 $hash = '';
3670 $query = '';
3671 $admin_url = '';
3672 $current_url = isset( $_SERVER[ 'REQUEST_URI' ] ) ? $_SERVER[ 'REQUEST_URI' ] : '';
3673
3674 /*
3675 * $menu_id may start with a hash or query, so parse before checking its value.
3676 */
3677 if ( false !== strpos( $menu_id, '#' ) ) {
3678
3679 list( $menu_id, $hash ) = explode( '#', $menu_id );
3680 }
3681
3682 if ( false !== strpos( $menu_id, '?' ) ) {
3683
3684 list( $menu_id, $query ) = explode( '?', $menu_id );
3685 }
3686
3687 if ( empty( $menu_id ) ) {
3688
3689 if ( preg_match( '/^.*\?page=wpsso-([^&]*).*$/', $current_url, $match ) ) {
3690
3691 $menu_id = $match[ 1 ];
3692
3693 } else $menu_id = key( $this->p->cf[ '*' ][ 'lib' ][ 'submenu' ] ); // Default to first submenu.
3694 }
3695
3696 /*
3697 * Find the menu_lib value for this menu_id.
3698 */
3699 if ( empty( $menu_lib ) ) {
3700
3701 foreach ( $this->p->cf[ '*' ][ 'lib' ] as $menu_lib => $menu ) {
3702
3703 if ( isset( $menu[ $menu_id ] ) ) {
3704
3705 break;
3706
3707 } else {
3708
3709 $menu_lib = '';
3710 }
3711 }
3712 }
3713
3714 if ( empty( $menu_lib ) || empty( $this->p->cf[ 'wp' ][ 'admin' ][ $menu_lib ][ 'page' ] ) ) {
3715
3716 return;
3717 }
3718
3719 $parent_slug = $this->p->cf[ 'wp' ][ 'admin' ][ $menu_lib ][ 'page' ] . '?page=wpsso-' . $menu_id;
3720
3721 switch ( $menu_lib ) {
3722
3723 case 'sitesubmenu':
3724
3725 $admin_url = network_admin_url( $parent_slug );
3726
3727 break;
3728
3729 default:
3730
3731 $admin_url = admin_url( $parent_slug );
3732
3733 break;
3734 }
3735
3736 if ( ! empty( $query ) ) {
3737
3738 $admin_url .= '&' . $query;
3739 }
3740
3741 if ( ! empty( $hash ) ) {
3742
3743 /*
3744 * If we have anchor text, force a page reload for the anchor, in case we're on the same page.
3745 *
3746 * Use the same random query for all admin URLs during a single page load so the SucomNotice "show
3747 * once" feature will work correctly.
3748 */
3749 static $rand_arg = null;
3750
3751 if ( null === $rand_arg ) {
3752
3753 $rand_arg = wp_rand( 100000, 999999 );
3754 }
3755
3756 $admin_url .= '&' . $rand_arg;
3757
3758 $admin_url .= '#' . $hash;
3759 }
3760
3761 if ( empty( $link_text ) ) {
3762
3763 return $admin_url;
3764
3765 }
3766
3767 $html = '<a href="' . $admin_url . '">' . $link_text . '</a>';
3768
3769 return $html;
3770 }
3771
3772 /*
3773 * Rename options array keys, preserving the option modifiers (ie. '_[0-9]', ':disabled', ':use', and '#.*').
3774 */
3775 public function rename_options_by_ext( array $opts, array $version_keys ) {
3776
3777 foreach ( $this->p->cf[ 'plugin' ] as $ext => $info ) {
3778
3779 if ( ! isset( $version_keys[ $ext ] ) ) { // Nothing to do.
3780
3781 continue;
3782
3783 } elseif ( ! is_array( $version_keys[ $ext ] ) ) { // Must be an array of option keys.
3784
3785 continue;
3786
3787 } elseif ( ! isset( $info[ 'opt_version' ] ) ) { // Nothing to compare to.
3788
3789 continue;
3790
3791 } elseif ( empty( $opts[ 'opt_versions' ][ $ext ] ) ) { // No previous options.
3792
3793 continue;
3794 }
3795
3796 $prev_version = $opts[ 'opt_versions' ][ $ext ];
3797
3798 foreach ( $version_keys[ $ext ] as $max_version => $opt_keys ) {
3799
3800 if ( is_numeric( $max_version ) && is_array( $opt_keys ) && $prev_version <= $max_version ) {
3801
3802 $opts = $this->rename_options_keys( $opts, $opt_keys );
3803
3804 $opts[ 'opt_versions' ][ $ext ] = $info[ 'opt_version' ]; // Mark as current.
3805 }
3806 }
3807 }
3808
3809 return $opts;
3810 }
3811
3812 public function rename_options_keys( array $opts, array $opt_keys ) {
3813
3814 foreach ( $opt_keys as $old_key => $new_key ) {
3815
3816 if ( empty( $old_key ) ) { // Just in case.
3817
3818 continue;
3819 }
3820
3821 $old_key_preg = '/^' . $old_key . '(:disabled|:use|#.*|_[0-9]+)?$/';
3822
3823 foreach ( preg_grep( $old_key_preg, array_keys( $opts ) ) as $old_key_matched ) {
3824
3825 if ( ! empty( $new_key ) ) { // Can be empty to remove the option.
3826
3827 $new_key_matched = preg_replace( $old_key_preg, $new_key . '$1', $old_key_matched );
3828
3829 $opts[ $new_key_matched ] = $opts[ $old_key_matched ]; // Preserve the old option value.
3830 }
3831
3832 unset( $opts[ $old_key_matched ] );
3833 }
3834 }
3835
3836 return $opts;
3837 }
3838
3839 /*
3840 * limit_text_length() uses PHP's multibyte functions (mb_strlen and mb_substr) for UTF8.
3841 */
3842 public function limit_text_length( $text, $maxlen = 300, $trailing = '', $cleanup_html = true ) {
3843
3844 static $charset = null;
3845
3846 if ( null === $charset ) {
3847
3848 $charset = get_bloginfo( $show = 'charset', $filter = 'raw' );
3849 }
3850
3851 if ( $cleanup_html ) {
3852
3853 $text = $this->cleanup_html_tags( $text ); // Remove any remaining html tags.
3854 }
3855
3856 $text = html_entity_decode( self::decode_utf8( $text ), ENT_QUOTES, $charset );
3857
3858 if ( $maxlen > 0 && function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
3859
3860 if ( mb_strlen( $trailing ) > $maxlen ) { // Just in case.
3861
3862 $trailing = mb_substr( $trailing, 0, $maxlen ); // Trim the trailing string, if too long.
3863 }
3864
3865 if ( mb_strlen( $text ) > $maxlen ) {
3866
3867 $adj_max_len = $maxlen - mb_strlen( $trailing );
3868
3869 $text = mb_substr( $text, 0, $adj_max_len );
3870 $text = trim( preg_replace( '/[^ ]*$/', '', $text ) ); // Remove trailing bits of words.
3871 $text = preg_replace( '/[,\.]*$/', '', $text ); // Remove trailing puntuation.
3872
3873 } else {
3874
3875 $trailing = ''; // Truncate trailing string if text is less than maxlen.
3876 }
3877
3878 $text = $text . $trailing; // Trim and add trailing string (if provided).
3879 }
3880
3881 $text = preg_replace( '/&nbsp;/', ' ', $text ); // Just in case.
3882
3883 return $text;
3884 }
3885
3886 public function cleanup_html_tags( $text, $strip_tags = true, $use_img_alt = false ) {
3887
3888 $text = SucomUtil::strip_shortcodes( $text ); // Remove any remaining shortcodes.
3889 $text = preg_replace( '/[\s\n\r]+/s', ' ', $text ); // Put everything on one line.
3890 $text = preg_replace( '/<\?.*\?'.'>/U', ' ', $text ); // Remove php.
3891 $text = preg_replace( '/<script\b[^>]*>(.*)<\/script>/Ui', ' ', $text ); // Remove javascript.
3892 $text = preg_replace( '/<style\b[^>]*>(.*)<\/style>/Ui', ' ', $text ); // Remove inline stylesheets.
3893
3894 /*
3895 * Maybe remove text between ignore markers.
3896 */
3897 if ( false !== strpos( $text, 'wpsso-ignore' ) ) {
3898
3899 $text = preg_replace( '/<!-- *wpsso-ignore *-->.*<!-- *\/wpsso-ignore *-->/U', ' ', $text );
3900 }
3901
3902 /*
3903 * Similar to SucomUtil::strip_html(), but includes image alt tags.
3904 */
3905 if ( $strip_tags ) {
3906
3907 /*
3908 * Add missing dot to buttons, headers, lists, etc.
3909 */
3910 $text = preg_replace( '/([\w])<\/(button|dt|h[0-9]+|li|th)>/i', '$1. ', $text );
3911
3912 /*
3913 * Replace list and paragraph tags with a space.
3914 */
3915 $text = preg_replace( '/(<li[^>]*>|<p[^>]*>|<\/p>)/i', ' ', $text );
3916
3917 /*
3918 * Remove remaining html tags.
3919 */
3920 $text_stripped = trim( strip_tags( $text ) );
3921
3922 /*
3923 * Possibly use img alt strings if no text.
3924 */
3925 if ( '' === $text_stripped && $use_img_alt && false !== strpos( $text, '<img ' ) ) {
3926
3927 $alt_text = '';
3928 $alt_prefix = $this->p->opt->get_text( 'plugin_img_alt_prefix' );
3929
3930 if ( preg_match_all( '/<img [^>]*alt=["\']([^"\'>]*)["\']/Ui', $text, $all_matches, PREG_PATTERN_ORDER ) ) {
3931
3932 foreach ( $all_matches[ 1 ] as $alt ) {
3933
3934 $alt = trim( $alt );
3935
3936 if ( ! empty( $alt ) ) {
3937
3938 $alt = empty( $alt_prefix ) ? $alt : $alt_prefix . ' ' . $alt;
3939
3940 /*
3941 * Maybe add a period after the image alt text.
3942 */
3943 $alt_text .= ( strpos( $alt, '.' ) + 1 ) === strlen( $alt ) ? $alt . ' ' : $alt . '. ';
3944 }
3945 }
3946
3947 if ( $this->p->debug->enabled ) {
3948
3949 $this->p->debug->log( 'img alt text: ' . $alt_text );
3950 }
3951 }
3952
3953 $text = $alt_text;
3954
3955 } else {
3956
3957 $text = $text_stripped;
3958 }
3959 }
3960
3961 /*
3962 * Replace 1+ spaces to a single space.
3963 */
3964 $text = preg_replace( '/(\xC2\xA0|\s)+/s', ' ', $text );
3965
3966 return trim( $text );
3967 }
3968
3969 public function get_validators( array $mod, $form = null ) {
3970
3971 /*
3972 * We do not want to validate settings pages in the back-end, so validators are only provided for known
3973 * modules (comment, post, term, and user). If we're on the front-end, validating the current webpage URL
3974 * is fine.
3975 */
3976 if ( is_admin() ) {
3977
3978 if ( empty( $mod[ 'obj' ] ) ) {
3979
3980 return array();
3981 }
3982 }
3983
3984 $can_crawl_url = true;
3985 $canonical_url = $this->p->util->get_canonical_url( $mod );
3986
3987 if ( empty( $canonical_url ) ) {
3988
3989 $can_crawl_url = false;
3990
3991 } elseif ( ! $mod[ 'is_public' ] ) {
3992
3993 $can_crawl_url = false;
3994
3995 } elseif ( $mod[ 'is_post' ] ) {
3996
3997 if ( 'publish' !== $mod[ 'post_status' ] ) {
3998
3999 $can_crawl_url = false;
4000 }
4001 }
4002
4003 $have_amp = $mod[ 'is_post' ] && $mod[ 'id' ] && function_exists( 'amp_get_permalink' ) ? true : false;
4004 $have_schema = $this->p->avail[ 'p' ][ 'schema' ] ? true : false;
4005 $amp_url_enc = $have_amp ? urlencode( amp_get_permalink( $mod[ 'id' ] ) ) : '';
4006 $canonical_url_enc = urlencode( $canonical_url );
4007
4008 $validators = array(
4009 'facebook-debugger' => array(
4010 'title' => _x( 'Facebook Sharing Debugger', 'option label', 'wpsso' ),
4011 'type' => _x( 'Open Graph', 'validator type', 'wpsso' ),
4012 'url' => 'https://developers.facebook.com/tools/debug/?q=' . $canonical_url_enc,
4013 ),
4014 'facebook-microdata' => array(
4015 'title' => _x( 'Facebook Microdata Debug Tool', 'option label', 'wpsso' ),
4016 'type' => _x( 'Microdata', 'validator type', 'wpsso' ),
4017 'url' => 'https://business.facebook.com/ads/microdata/debug?url=' . $canonical_url_enc,
4018 ),
4019 'google-amp' => array(
4020 'title' => _x( 'Google AMP Test', 'option label', 'wpsso' ),
4021 'type' => _x( 'AMP Markup', 'validator type', 'wpsso' ) . ( $have_amp ? '' : ' *' ),
4022 'url' => $have_amp ? 'https://search.google.com/test/amp/result?url=' . $amp_url_enc : '',
4023 ),
4024 'google-page-speed' => array(
4025 'title' => _x( 'Google PageSpeed Insights', 'option label', 'wpsso' ),
4026 'type' => _x( 'PageSpeed', 'validator type', 'wpsso' ),
4027 'url' => 'https://pagespeed.web.dev/report?url=' . $canonical_url_enc,
4028 ),
4029 'google-rich-results' => array(
4030 'title' => _x( 'Google Rich Results Test', 'option label', 'wpsso' ),
4031 'type' => _x( 'Rich Results', 'validator type', 'wpsso' ) . ( $have_schema ? '' : ' **' ),
4032 'url' => $have_schema ? 'https://search.google.com/test/rich-results?url=' . $canonical_url_enc : '',
4033 ),
4034 'linkedin' => array(
4035 'title' => _x( 'LinkedIn Post Inspector', 'option label', 'wpsso' ),
4036 'type' => _x( 'oEmbed Data', 'validator type', 'wpsso' ),
4037 'url' => 'https://www.linkedin.com/post-inspector/inspect/' . $canonical_url_enc,
4038 ),
4039 'pinterest' => array(
4040 'title' => _x( 'Pinterest Rich Pins Validator', 'option label', 'wpsso' ),
4041 'type' => _x( 'Rich Pins', 'validator type', 'wpsso' ),
4042 'url' => 'https://developers.pinterest.com/tools/url-debugger/?link=' . $canonical_url_enc,
4043 ),
4044 'schema-markup-validator' => array(
4045 'title' => _x( 'Schema Markup Validator', 'option label', 'wpsso' ),
4046 'type' => _x( 'Schema Markup', 'validator type', 'wpsso' ) . ( $have_schema ? '' : ' *' ),
4047 'url' => $have_schema ? 'https://validator.schema.org/#url=' . $canonical_url_enc : '',
4048 ),
4049 'twitter' => array(
4050 'title' => _x( 'X (Twitter) Card Validator', 'option label', 'wpsso' ),
4051 'type' => _x( 'X (Twitter) Card', 'validator type', 'wpsso' ),
4052 'url' => is_object( $form ) ? 'https://cards-dev.twitter.com/validator' : '',
4053 'extra_msg' => is_object( $form ) ? $form->get_no_input_clipboard( $canonical_url ) : '',
4054 ),
4055 'w3c' => array(
4056 'title' => _x( 'W3C Markup Validator', 'option label', 'wpsso' ),
4057 'type' => _x( 'HTML Markup', 'validator type', 'wpsso' ),
4058 'url' => 'https://validator.w3.org/nu/?doc=' . $canonical_url_enc,
4059 ),
4060 );
4061
4062 if ( ! $can_crawl_url ) {
4063
4064 foreach ( $validators as $key => $arr ) {
4065
4066 $validators[ $key ][ 'url' ] = '';
4067 }
4068 }
4069
4070 return $validators;
4071 }
4072
4073 /*
4074 * Maybe set the notice reference URL and translated message.
4075 *
4076 * Example messages, depending on the $mod array:
4077 *
4078 * adding schema organization
4079 * adding schema organization for this page
4080 * adding schema organization for page ID 123
4081 */
4082 public function maybe_set_ref( $canonical_url = null, $mod = false, $msg_transl = '' ) {
4083
4084 static $is_admin = null;
4085
4086 if ( null === $is_admin ) {
4087
4088 $is_admin = is_admin();
4089 }
4090
4091 if ( ! $is_admin ) {
4092
4093 return false;
4094 }
4095
4096 if ( empty( $canonical_url ) ) {
4097
4098 $canonical_url = $this->get_canonical_url( $mod );
4099 }
4100
4101 if ( empty( $msg_transl ) ) {
4102
4103 return $this->p->notice->set_ref( $canonical_url, $mod );
4104 }
4105
4106 if ( empty( $mod[ 'id' ] ) ) {
4107
4108 return $this->p->notice->set_ref( $canonical_url, $mod, $msg_transl );
4109 }
4110
4111 if ( $mod[ 'is_post' ] && $mod[ 'post_type_label_single' ] ) {
4112
4113 $name_transl = mb_strtolower( $mod[ 'post_type_label_single' ] );
4114
4115 } elseif ( $mod[ 'is_term' ] && $mod[ 'tax_label_single' ] ) {
4116
4117 $name_transl = mb_strtolower( $mod[ 'tax_label_single' ] );
4118
4119 } else {
4120
4121 $name_transl = $mod[ 'name_transl' ]; // Translated module name.
4122 }
4123
4124 if ( SucomUtilWP::is_mod_screen_obj( $mod ) ) {
4125
4126 // translators: %1$s is an action message, %2$s is the module or post type name.
4127 $msg_transl = sprintf( __( '%1$s for this %2$s', 'wpsso' ), $msg_transl, $name_transl );
4128
4129 /*
4130 * Exclude the $mod array to avoid adding an 'Edit' link to the notice message.
4131 */
4132 return $this->p->notice->set_ref( $canonical_url, false, $msg_transl );
4133
4134 }
4135
4136 // translators: %1$s is an action message, %2$s is the module or post type name and %3$d is the object ID.
4137 $msg_transl = sprintf( __( '%1$s for %2$s ID %3$d', 'wpsso' ), $msg_transl, $name_transl, $mod[ 'id' ] );
4138
4139 return $this->p->notice->set_ref( $canonical_url, $mod, $msg_transl );
4140 }
4141
4142 public function maybe_unset_ref( $canonical_url ) {
4143
4144 static $is_admin = null;
4145
4146 if ( null === $is_admin ) {
4147
4148 $is_admin = is_admin();
4149 }
4150
4151 if ( ! $is_admin ) {
4152
4153 return false;
4154 }
4155
4156 return $this->p->notice->unset_ref( $canonical_url );
4157 }
4158
4159 public function count_cron_jobs() {
4160
4161 $count = 0;
4162
4163 $cron_jobs = get_option( 'cron' );
4164
4165 if ( is_array( $cron_jobs ) ) {
4166
4167 foreach ( $cron_jobs as $cron_id => $cron_el ) {
4168
4169 if ( is_array( $cron_el ) ) {
4170
4171 foreach ( $cron_el as $sched_id => $sched_el ) {
4172
4173 $count++;
4174 }
4175 }
4176 }
4177 }
4178
4179 return $count;
4180 }
4181
4182 /*
4183 * See WpssoCmcfXml->get().
4184 * See WpssoGmfXml->get().
4185 * See WpssoAdmin->settings_sanitation().
4186 * See WpssoAdmin->show_metabox_cache_status().
4187 * See WpssoHead->get_head_array().
4188 * See WpssoMessagesTooltipPlugin->get().
4189 * See WpssoPage->get_the_content().
4190 * See WpssoProMediaFacebook->filter_video_details().
4191 * See WpssoProMediaSlideshare->filter_video_details().
4192 * See WpssoProMediaVimeo->filter_video_details().
4193 * See WpssoProMediaWistia->filter_video_details().
4194 * See WpssoProMediaYoutube->filter_video_details().
4195 * See WpssoProReviewJudgeme->filter_og().
4196 * See WpssoProReviewShopperApproved->filter_og().
4197 * See WpssoProReviewStamped->filter_og().
4198 * See WpssoProUtilShorten->get_short_url().
4199 * See WpssoSchema->get_schema_types().
4200 * See WpssoSchema->get_schema_type_child_family().
4201 * See WpssoSchema->get_schema_type_children().
4202 * See WpssoSchema->get_schema_type_row_class().
4203 * See WpssoUtil->get_image_url_info().
4204 */
4205 public function get_cache_exp_secs( $cache_key, $cache_type = 'transient', $mod = false ) {
4206
4207 return $this->cache->get_cache_exp_secs( $cache_key, $cache_type, $mod );
4208 }
4209 }
4210 }
4211