PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / 22.7.0
WPSSO Core – Complete Schema Markup and Meta Tags v22.7.0
22.7.0 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 / post.php

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

2,911 lines 82.7 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( 'WpssoAbstractWpMeta' ) ) {
19
20 require_once WPSSO_PLUGINDIR . 'lib/abstract/wp-meta.php';
21 }
22
23 /*
24 * Extended by the WpssoOpmPost class.
25 */
26 if ( ! class_exists( 'WpssoPost' ) ) {
27
28 class WpssoPost extends WpssoAbstractWpMeta {
29
30 private static $saved_shortlink_url = null; // Used by get_canonical_shortlink() and maybe_restore_shortlink().
31 private static $cache_shortlinks = array(); // Used by get_canonical_shortlink() and maybe_restore_shortlink().
32
33 public function __construct( &$plugin ) {
34
35 $this->p =& $plugin;
36
37 if ( $this->p->debug->enabled ) {
38
39 $this->p->debug->mark();
40 }
41
42 /*
43 * Maybe enable excerpts for pages.
44 */
45 if ( ! empty( $this->p->options[ 'plugin_page_excerpt' ] ) ) {
46
47 add_post_type_support( 'page', array( 'excerpt' ) );
48 }
49
50 /*
51 * Maybe register tags for pages.
52 */
53 if ( $page_tag_taxonomy = SucomUtil::get_const( 'WPSSO_PAGE_TAG_TAXONOMY' ) ) {
54
55 if ( ! empty( $this->p->options[ 'plugin_page_tags' ] ) ) {
56
57 if ( ! taxonomy_exists( $page_tag_taxonomy ) ) {
58
59 WpssoRegister::register_taxonomy_page_tag();
60 }
61
62 } elseif ( taxonomy_exists( $page_tag_taxonomy ) ) {
63
64 unregister_taxonomy( $page_tag_taxonomy );
65 }
66 }
67
68 /*
69 * This hook is fired once WordPress, plugins, and the theme are fully loaded and instantiated.
70 */
71 add_action( 'wp_loaded', array( $this, 'add_wp_callbacks' ) );
72 }
73
74 /*
75 * Add WordPress action and filter callbacks.
76 */
77 public function add_wp_callbacks() {
78
79 if ( $this->p->debug->enabled ) {
80
81 $this->p->debug->mark();
82 }
83
84 /*
85 * Since WPSSO Core v17.0.0.
86 *
87 * Register our post meta.
88 */
89 $this->register_meta( $object_type = 'post', WPSSO_META_NAME );
90
91 $is_admin = is_admin(); // Only check once.
92
93 $doing_ajax = SucomUtilWP::doing_ajax();
94
95 if ( $is_admin ) {
96
97 $metabox_id = $this->p->cf[ 'meta' ][ 'id' ];
98
99 add_action( 'wp_ajax_wpsso_get_metabox_postbox_id_' . $metabox_id . '_inside', array( $this, 'ajax_get_metabox_sso' ) );
100
101 add_action( 'wp_ajax_wpsso_get_validate_submenu', array( $this, 'ajax_get_validate_submenu' ) );
102
103 if ( ! empty( $_GET ) || 'post-new' === basename( $_SERVER[ 'PHP_SELF' ], '.php' ) ) { // Skip some action hooks if no query argument(s).
104
105 /*
106 * load_meta_page() priorities: 100 post, 200 user, 300 term.
107 *
108 * Sets the parent::$head_tags and parent::$head_info class properties.
109 */
110 add_action( 'current_screen', array( $this, 'load_meta_page' ), 100, 1 );
111
112 /*
113 * The 'add_meta_boxes' action fires after all built-in meta boxes have been added.
114 */
115 add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 10, 2 );
116 }
117
118 add_action( 'wp_after_insert_post', array( $this, 'after_insert_post' ), 10, 4 );
119
120 /*
121 * The 'save_post' action is run after other post type specific actions, so we can use it to save
122 * post meta for any post type. Don't hook the 'clean_post_cache' action since 'save_post' is run
123 * after 'clean_post_cache' and our custom post meta has not been saved yet.
124 */
125 add_action( 'save_post', array( $this, 'save_options' ), WPSSO_META_SAVE_PRIORITY );
126 add_action( 'save_post', array( $this, 'clear_cache' ), WPSSO_META_CLEAR_PRIORITY );
127 add_action( 'save_post', array( $this, 'refresh_cache' ), WPSSO_META_REFRESH_PRIORITY );
128
129 /*
130 * The wp_insert_post() function returns after running the 'edit_attachment' action, so the
131 * 'save_post' action is never run for attachments.
132 */
133 add_action( 'edit_attachment', array( $this, 'save_options' ), WPSSO_META_SAVE_PRIORITY );
134 add_action( 'edit_attachment', array( $this, 'clear_cache' ), WPSSO_META_CLEAR_PRIORITY );
135 add_action( 'edit_attachment', array( $this, 'refresh_cache' ), WPSSO_META_REFRESH_PRIORITY );
136 }
137
138 /*
139 * Add the columns when doing AJAX as well to allow Quick Edit to add the required columns.
140 */
141 if ( $is_admin || $doing_ajax ) {
142
143 /*
144 * Add edit table columns.
145 */
146 if ( $this->p->debug->enabled ) {
147
148 $this->p->debug->log( 'adding column filters for posts' );
149 }
150
151 add_filter( 'manage_pages_columns', array( $this, 'add_page_column_headings' ), WPSSO_ADD_COLUMN_PRIORITY, 1 );
152 add_filter( 'manage_posts_columns', array( $this, 'add_post_column_headings' ), WPSSO_ADD_COLUMN_PRIORITY, 2 );
153 add_filter( 'manage_media_columns', array( $this, 'add_media_column_headings' ), WPSSO_ADD_COLUMN_PRIORITY, 1 );
154
155 add_action( 'manage_pages_custom_column', array( $this, 'show_column_content' ), 10, 2 );
156 add_action( 'manage_posts_custom_column', array( $this, 'show_column_content' ), 10, 2 );
157 add_action( 'manage_media_custom_column', array( $this, 'show_column_content' ), 10, 2 );
158
159 /*
160 * The 'parse_query' action is hooked once in the WpssoPost class to set the column orderby for
161 * post, term, and user edit tables.
162 */
163 add_action( 'parse_query', array( $this, 'set_column_orderby' ), 10, 1 );
164 }
165
166 if ( ! empty( $this->p->options[ 'plugin_wp_shortlink' ] ) ) { // Use Short URL for WP Shortlink.
167
168 if ( ! empty( $this->p->options[ 'plugin_shortener' ] ) && $this->p->options[ 'plugin_shortener' ] !== 'none' ) {
169
170 if ( $this->p->debug->enabled ) {
171
172 $this->p->debug->log( 'adding pre_get_shortlink filters to shorten the url' );
173 }
174
175 add_filter( 'pre_get_shortlink', array( $this, 'get_canonical_shortlink' ), PHP_INT_MIN, 4 );
176 add_filter( 'pre_get_shortlink', array( $this, 'maybe_restore_shortlink' ), PHP_INT_MAX, 4 );
177
178 if ( function_exists( 'wpme_get_shortlink_handler' ) ) {
179
180 if ( $this->p->debug->enabled ) {
181
182 $this->p->debug->log( 'removing the jetpack pre_get_shortlink filter hook' );
183 }
184
185 remove_filter( 'pre_get_shortlink', 'wpme_get_shortlink_handler', 1 );
186 }
187 }
188 }
189
190 /*
191 * Maybe create or update the post column content.
192 */
193 add_filter( 'get_post_metadata', array( $this, 'check_sortable_meta' ), 1000, 4 );
194
195 /*
196 * Maybe inherit a featured image ID from the post/page parent, if the 'plugin_inherit_featured' option is
197 * enabled, and ignore saving the same featured image ID.
198 */
199 add_filter( 'get_post_metadata', array( $this, 'get_post_metadata_thumbnail_id' ), PHP_INT_MAX, 4 );
200 add_filter( 'update_post_metadata', array( $this, 'update_post_metadata_thumbnail_id' ), PHP_INT_MAX, 5 );
201 }
202
203 /*
204 * Get the $mod object for a post id.
205 */
206 public function get_mod( $post_id ) {
207
208 if ( $this->p->debug->enabled ) {
209
210 $this->p->debug->mark_caller();
211
212 $this->p->debug->log_args( array(
213 'post_id' => $post_id,
214 ) );
215 }
216
217 static $local_fifo = array();
218
219 /*
220 * Maybe return the array from the local cache.
221 */
222 if ( isset( $local_fifo[ $post_id ] ) ) {
223
224 if ( ! $this->md_cache_disabled ) {
225
226 if ( $this->p->debug->enabled ) {
227
228 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' mod array from local cache' );
229 }
230
231 return $local_fifo[ $post_id ];
232
233 } else unset( $local_fifo[ $post_id ] );
234 }
235
236 /*
237 * Maybe limit the number of array elements.
238 */
239 $local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );
240
241 $mod = self::get_mod_defaults();
242
243 /*
244 * Common elements.
245 */
246 $mod[ 'id' ] = is_numeric( $post_id ) ? (int) $post_id : 0; // Cast as integer.
247 $mod[ 'name' ] = 'post';
248 $mod[ 'name_transl' ] = _x( 'post', 'module name', 'wpsso' );
249 $mod[ 'obj' ] =& $this;
250
251 /*
252 * WpssoPost elements.
253 */
254 $mod[ 'is_post' ] = true;
255 $mod[ 'is_home_page' ] = SucomUtilWP::is_home_page( $post_id ); // Static front page (singular post).
256 $mod[ 'is_home_posts' ] = $mod[ 'is_home_page' ] ? false : SucomUtilWP::is_home_posts( $post_id ); // Static posts page or blog archive page.
257 $mod[ 'is_home' ] = $mod[ 'is_home_page' ] || $mod[ 'is_home_posts' ] ? true : false; // Home page (static or blog archive).
258
259 if ( $mod[ 'id' ] ) { // Just in case.
260
261 $mod[ 'wp_obj' ] = SucomUtilWP::get_post_object( $mod[ 'id' ] );
262
263 if ( $mod[ 'wp_obj' ] instanceof WP_Post ) { // Just in case.
264
265 $mod[ 'post_slug' ] = get_post_field( 'post_name', $mod[ 'wp_obj' ] ); // Post name (aka slug).
266 $mod[ 'post_type' ] = get_post_type( $mod[ 'wp_obj' ] ); // Post type name.
267 $mod[ 'post_mime_type' ] = get_post_mime_type( $mod[ 'wp_obj' ] ); // Post mime type (ie. image/jpg).
268 $mod[ 'post_status' ] = get_post_status( $mod[ 'wp_obj' ] ); // Post status name.
269 $mod[ 'post_author' ] = (int) get_post_field( 'post_author', $mod[ 'wp_obj' ] ); // Post author id.
270 $mod[ 'post_coauthors' ] = array();
271 $mod[ 'post_time' ] = get_post_time( 'c', $gmt = true, $mod[ 'wp_obj' ] ); // ISO 8601 date or false.
272 $mod[ 'post_timestamp' ] = get_post_time( 'U', $gmt = true, $mod[ 'wp_obj' ] ); // Unix timestamp or false.
273 $mod[ 'post_modified_time' ] = get_post_modified_time( 'c', $gmt = true, $mod[ 'wp_obj' ] ); // ISO 8601 date or false.
274 $mod[ 'post_modified_timestamp' ] = get_post_modified_time( 'U', $gmt = true, $mod[ 'wp_obj' ] ); // Unix timestamp or false.
275
276 if ( is_array( $mod[ 'post_type' ] ) ) { // Just in case.
277
278 $notice_msg = sprintf( __( 'The <a href="%1$s">WordPress get_post_type() function</a> returned an array for WP_Post object ID %2$s.', 'wpsso' ), __( 'https://developer.wordpress.org/reference/functions/get_post_type/', 'wpsso' ), $mod[ 'id' ] ) . ' ';
279
280 $notice_msg .= __( 'This function must not return an array, it must return false or a post type string.', 'wpsso' ) . ' ';
281
282 $notice_msg .= '<pre><code>' . print_r( $mod[ 'post_type' ], true ) . '</code></pre>';
283
284 if ( $this->p->notice->is_admin_pre_notices() ) {
285
286 $this->p->notice->err( $notice_msg );
287 }
288
289 $error_pre = sprintf( '%s error:', __METHOD__ );
290
291 SucomUtil::safe_error_log( $error_pre . ' ' . $notice_msg, $strip_html = true );
292
293 if ( $this->p->debug->enabled ) {
294
295 $this->p->debug->log( 'get_post_type() returned an array for WP_Post object id ' . $mod[ 'id' ] );
296
297 $this->p->debug->log_arr( 'post_type', $mod[ 'post_type' ] );
298 }
299 }
300
301 /*
302 * See WpssoIntegEcomWooCommerce->filter_get_post_type().
303 */
304 $mod[ 'post_type' ] = apply_filters( 'wpsso_get_post_type', $mod[ 'post_type' ], $post_id );
305
306 if ( ! empty( $mod[ 'post_type' ] ) && is_string( $mod[ 'post_type' ] ) ) { // Not false or empty string.
307
308 $mod[ 'is_attachment' ] = 'attachment' === $mod[ 'post_type' ] ? true : false; // Post type is 'attachment'.
309
310 $post_type_obj = get_post_type_object( $mod[ 'post_type' ] );
311
312 if ( $post_type_obj instanceof WP_Post_Type ) { // Just in case.
313
314 if ( isset( $post_type_obj->labels->name ) ) {
315
316 $mod[ 'post_type_label_plural' ] = $post_type_obj->labels->name;
317 }
318
319 if ( isset( $post_type_obj->labels->singular_name ) ) {
320
321 $mod[ 'post_type_label_single' ] = $post_type_obj->labels->singular_name;
322 }
323
324 if ( isset( $post_type_obj->public ) ) {
325
326 $mod[ 'is_public' ] = $post_type_obj->public ? true : false;
327 }
328
329 $mod[ 'is_post_type_archive' ] = SucomUtilWP::is_post_type_archive( $post_type_obj, $mod[ 'post_slug' ] );
330
331 $mod[ 'is_archive' ] = $mod[ 'is_post_type_archive' ];
332 }
333
334 unset( $post_type_obj ); // Done with $post_type_obj.
335 }
336
337 /*
338 * If we have a post with content (ie. singular), then check for paging in the content.
339 */
340 if ( ! $mod[ 'is_post_type_archive' ] && ! $mod[ 'is_home_posts' ] ) {
341
342 $mod[ 'paged_total' ] = substr_count( $mod[ 'wp_obj' ]->post_content, '<!--nextpage-->' ) + 1;
343 }
344
345 /*
346 * If the post has a parent, save the parent ID.
347 */
348 if ( ! empty( $mod[ 'wp_obj' ]->post_parent ) ) {
349
350 $mod[ 'post_parent' ] = $mod[ 'wp_obj' ]->post_parent; // Post parent id.
351 }
352
353 /*
354 * The post type might be public, but if the post itself is private, then mark the post as not public.
355 *
356 * See https://wordpress.org/support/article/post-status/#default-statuses.
357 */
358 if ( 'private' === $mod[ 'post_status' ] ) {
359
360 $mod[ 'is_public' ] = false;
361 }
362
363 /*
364 * Find the post mime type group and subgroup values.
365 *
366 * See wp_post_mime_type_where() in wordpress/wp-includes/post.php.
367 */
368 if ( ! empty( $mod[ 'post_mime_type' ] ) ) {
369
370 if ( false !== $slashpos = strpos( $mod[ 'post_mime_type' ], '/' ) ) {
371
372 $mod[ 'post_mime_group' ] = preg_replace( '/[^-*.a-zA-Z0-9]/', '',
373 substr( $mod[ 'post_mime_type' ], 0, $slashpos ) );
374
375 $mod[ 'post_mime_subgroup' ] = preg_replace( '/[^-*.+a-zA-Z0-9]/', '',
376 substr( $mod[ 'post_mime_type' ], $slashpos + 1 ) );
377
378 } else {
379
380 $mod[ 'post_mime_group' ] = preg_replace( '/[^-*.a-zA-Z0-9]/', '', $mod[ 'post_mime_type' ] );
381
382 $mod[ 'post_mime_subgroup' ] = '*';
383 }
384 }
385
386 $mod[ 'post_taxonomies' ] = get_post_taxonomies( $mod[ 'wp_obj' ] );
387
388 if ( is_array( $mod[ 'post_taxonomies' ] ) ) { // Just in case.
389
390 if ( ! in_array( $mod[ 'post_primary_tax_slug' ], $mod[ 'post_taxonomies' ] ) ) {
391
392 $mod[ 'post_primary_tax_slug' ] = ''; // Default value is not valid.
393
394 foreach ( $mod[ 'post_taxonomies' ] as $tax_slug ) {
395
396 if ( preg_match( '/_(cat|category)$/', $tax_slug ) ) { // Matches WooCommerce 'product_cat' for example.
397
398 if ( $this->p->debug->enabled ) {
399
400 $this->p->debug->log( 'using ' . $tax_slug . ' as the primary taxonomy' );
401 }
402
403 $mod[ 'post_primary_tax_slug' ] = $tax_slug;
404
405 unset( $tax_slug );
406
407 break;
408 }
409 }
410 }
411
412 } else $mod[ 'post_taxonomies' ] = array();
413
414 } else $mod[ 'wp_obj' ] = false;
415 }
416
417 /*
418 * Filter the post mod array.
419 *
420 * See WpssoIntegUserCoAuthors->filter_get_post_mod().
421 */
422 $filter_name = 'wpsso_get_post_mod';
423
424 if ( $this->p->debug->enabled ) {
425
426 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
427 }
428
429 $mod = apply_filters( $filter_name, $mod, $post_id );
430
431 $filter_name = 'wpsso_primary_tax_slug';
432
433 if ( $this->p->debug->enabled ) {
434
435 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
436 }
437
438 $mod[ 'post_primary_tax_slug' ] = apply_filters( $filter_name, $mod[ 'post_primary_tax_slug' ], $mod );
439
440 if ( $this->p->debug->enabled ) {
441
442 $this->p->debug->log_arr( 'mod', $mod );
443 }
444
445 /*
446 * Maybe save the array to the local cache.
447 */
448 if ( ! $this->md_cache_disabled ) {
449
450 $local_fifo[ $post_id ] = $mod;
451
452 if ( $this->p->debug->enabled ) {
453
454 $this->p->debug->log_size( 'local_fifo', $local_fifo );
455 }
456 }
457
458 return $mod;
459 }
460
461 public function get_mod_wp_object( array $mod ) {
462
463 if ( $mod[ 'wp_obj' ] instanceof WP_Post ) {
464
465 return $mod[ 'wp_obj' ];
466 }
467
468 return SucomUtilWP::get_post_object( $mod[ 'id' ] );
469 }
470
471 /*
472 * Check if the post type matches a pre-defined Open Graph type.
473 *
474 * For example, a post type of 'organization' would return 'website' for the Open Graph type.
475 *
476 * Returns false or an Open Graph type string.
477 */
478 public function get_post_type_og_type( $mod ) {
479
480 if ( ! empty( $mod[ 'post_type' ] ) && is_string( $mod[ 'post_type' ] ) ) { // Not false or empty string.
481
482 if ( ! empty( $this->p->cf[ 'head' ][ 'og_type_by_post_type' ][ $mod[ 'post_type' ] ] ) ) {
483
484 return $this->p->cf[ 'head' ][ 'og_type_by_post_type' ][ $mod[ 'post_type' ] ];
485 }
486 }
487
488 return false;
489 }
490
491 /*
492 * Option handling methods:
493 *
494 * get_defaults()
495 * get_options()
496 * save_options()
497 * delete_options()
498 */
499 public function get_options( $post_id, $md_key = false, $filter_opts = true, $merge_defs = false ) {
500
501 if ( $this->p->debug->enabled ) {
502
503 $this->p->debug->mark_caller();
504
505 $this->p->debug->log_args( array(
506 'post_id' => $post_id,
507 'md_key' => $md_key,
508 'filter_opts' => $filter_opts,
509 'merge_defs' => $merge_defs,
510 ) );
511 }
512
513 static $local_fifo = array();
514
515 /*
516 * Use $post_id and $filter_opts to create the cache ID string, but DO NOT ADD $merge_defs.
517 */
518 $cache_id = SucomUtil::get_assoc_salt( array( 'id' => $post_id, 'filter' => $filter_opts ) );
519
520 /*
521 * Maybe initialize a new local cache element. Use isset() instead of empty() to allow for an empty array.
522 */
523 if ( ! isset( $local_fifo[ $cache_id ] ) ) {
524
525 /*
526 * Maybe limit the number of array elements.
527 */
528 $local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );
529
530 $local_fifo[ $cache_id ] = null; // Create an element to reference.
531 }
532
533 $md_opts =& $local_fifo[ $cache_id ]; // Reference the local cache element.
534
535 if ( null === $md_opts ) { // Maybe read metadata into a new local cache element.
536
537 if ( $this->p->debug->enabled ) {
538
539 $this->p->debug->log( 'getting metadata for post id ' . $post_id );
540 }
541
542 $md_opts = self::get_meta( $post_id, WPSSO_META_NAME, $single = true );
543
544 if ( ! is_array( $md_opts ) ) {
545
546 $md_opts = array(); // WPSSO_META_NAME not found.
547 }
548
549 unset( $md_opts[ 'opt_filtered' ] ); // Just in case.
550
551 /*
552 * Check if options need to be upgraded and saved.
553 */
554 if ( $this->p->opt->is_upgrade_required( $md_opts ) ) {
555
556 $md_opts = $this->upgrade_options( $md_opts, $post_id );
557
558 self::update_meta( $post_id, WPSSO_META_NAME, $md_opts );
559 }
560 }
561
562 if ( $filter_opts ) {
563
564 if ( ! empty( $md_opts[ 'opt_filtered' ] ) ) { // Set before calling filters to prevent recursion.
565
566 if ( $this->p->debug->enabled ) {
567
568 $this->p->debug->log( 'skipping filters: options already filtered' );
569 }
570
571 } else {
572
573 if ( $this->p->debug->enabled ) {
574
575 $this->p->debug->log( 'setting opt_filtered to 1' );
576 }
577
578 $md_opts[ 'opt_filtered' ] = 1; // Set before calling filters to prevent recursion.
579
580 if ( $this->p->debug->enabled ) {
581
582 $this->p->debug->log( 'required call to WpssoPost->get_mod() for post ID ' . $post_id );
583 }
584
585 $mod = $this->get_mod( $post_id );
586
587 /*
588 * See WpssoUtilBlocks->filter_import_content_blocks().
589 */
590 if ( isset( $mod[ 'wp_obj' ]->post_content ) ) {
591
592 $filter_name = 'wpsso_import_content_blocks';
593
594 if ( $this->p->debug->enabled ) {
595
596 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
597 }
598
599 $md_opts = apply_filters( $filter_name, $md_opts, $mod[ 'wp_obj' ]->post_content );
600
601 } elseif ( $this->p->debug->enabled ) {
602
603 $this->p->debug->log( 'content property missing in post id ' . $post_id . ' object' );
604 }
605
606 /*
607 * The 'import_custom_fields' filter is executed before the 'wpsso_get_md_options' and
608 * 'wpsso_get_post_options' filters, custom field values may get overwritten by these
609 * filters.
610 *
611 * The 'import_custom_fields' filter is also executed before the 'wpsso_get_md_defaults'
612 * and 'wpsso_get_post_defaults' filters, so submitted form values that are identical to
613 * their defaults can be removed before saving the options array.
614 *
615 * See WpssoPost->get_options().
616 * See WpssoAbstractWpMeta->get_defaults().
617 * See WpssoUtilCustomFields->filter_import_custom_fields().
618 * See WpssoIntegEcomWooCommerce->add_mt_product().
619 * See WpssoIntegEcomWooAddGtin->filter_wc_variation_alt_options().
620 */
621 $filter_name = 'wpsso_import_custom_fields';
622
623 if ( $this->p->debug->enabled ) {
624
625 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
626 }
627
628 $md_opts = apply_filters( $filter_name, $md_opts, $mod, self::get_meta( $post_id ) );
629
630 /*
631 * Since WPSSO Core v14.2.0.
632 *
633 * See WpssoIntegEcomWooCommerce->add_mt_product().
634 */
635 $filter_name = 'wpsso_import_product_attributes';
636
637 if ( $this->p->debug->enabled ) {
638
639 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
640 }
641
642 $md_opts = apply_filters( $filter_name, $md_opts, $mod, $mod[ 'wp_obj' ] );
643
644 /*
645 * Since WPSSO Core v9.5.0.
646 *
647 * Overwrite parent options with those of the child, allowing only undefined child options
648 * to be inherited from the parent.
649 */
650 if ( $this->p->debug->enabled ) {
651
652 $this->p->debug->log( 'inheriting parent metadata options for post id ' . $post_id );
653 }
654
655 $parent_opts = $this->get_inherited_md_opts( $mod );
656
657 if ( ! empty( $parent_opts ) ) {
658
659 $md_opts = array_merge( $parent_opts, $md_opts );
660 }
661
662 $filter_name = 'wpsso_get_md_options';
663
664 if ( $this->p->debug->enabled ) {
665
666 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
667 }
668
669 $md_opts = apply_filters( $filter_name, $md_opts, $mod );
670
671 /*
672 * Hooked by several integration modules to provide information about the current content.
673 * e-Commerce integration modules will provide information on their product (price,
674 * condition, etc.) and disable these options in the Document SSO metabox.
675 */
676 $filter_name = 'wpsso_get_' . $mod[ 'name' ] . '_options';
677
678 if ( $this->p->debug->enabled ) {
679
680 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
681 }
682
683 $md_opts = apply_filters( $filter_name, $md_opts, $post_id, $mod );
684
685 $filter_name = 'wpsso_sanitize_md_options';;
686
687 if ( $this->p->debug->enabled ) {
688
689 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
690 }
691
692 $md_opts = apply_filters( $filter_name, $md_opts, $mod );
693 }
694 }
695
696 /*
697 * Maybe save the array to the local cache.
698 */
699 if ( $this->md_cache_disabled ) {
700
701 $deref_md_opts = $local_fifo[ $cache_id ]; // Dereference.
702
703 unset( $local_fifo, $md_opts );
704
705 return $this->return_options( $post_id, $deref_md_opts, $md_key, $merge_defs );
706 }
707
708 return $this->return_options( $post_id, $md_opts, $md_key, $merge_defs );
709 }
710
711 /*
712 * Use $rel = false to extend WpssoAbstractWpMeta->save_options().
713 */
714 public function save_options( $post_id, $rel = false ) {
715
716 if ( $this->p->debug->enabled ) {
717
718 $this->p->debug->log_args( array(
719 'post_id' => $post_id,
720 ) );
721 }
722
723 if ( empty( $post_id ) ) { // Just in case.
724
725 if ( $this->p->debug->enabled ) {
726
727 $this->p->debug->log( 'exiting early: post id is empty' );
728 }
729
730 return;
731
732 } elseif ( ! $this->verify_submit_nonce() ) {
733
734 if ( $this->p->debug->enabled ) {
735
736 $this->p->debug->log( 'exiting early: verify_submit_nonce failed' );
737 }
738
739 return;
740
741 /*
742 * WpssoPost->post_can_have_meta() returns false:
743 *
744 * If the $post argument is not numeric or an instance of WP_Post.
745 * If the post ID is empty.
746 * If the post type is empty.
747 * If the post status is empty.
748 * If the post type is 'revision'.
749 * If the post status is 'trash'.
750 */
751 } elseif ( ! $this->post_can_have_meta( $post_id ) ) {
752
753 if ( $this->p->debug->enabled ) {
754
755 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' invalid for metadata' );
756 }
757
758 return;
759
760 /*
761 * Check user capability for the post id.
762 */
763 } elseif ( ! $this->user_can_edit( $post_id ) ) {
764
765 if ( $this->p->debug->enabled ) {
766
767 $user_id = get_current_user_id();
768
769 $this->p->debug->log( 'exiting early: user id ' . $user_id . ' cannot edit post id ' . $post_id );
770 }
771
772 return;
773 }
774
775 $this->md_cache_disable(); // Disable the local cache.
776
777 $mod = $this->get_mod( $post_id );
778
779 $md_opts = $this->get_submit_opts( $mod ); // Merge previous + submitted options and then sanitize.
780
781 $this->md_cache_enable(); // Re-enable the local cache.
782
783 if ( false === $md_opts ) {
784
785 if ( $this->p->debug->enabled ) {
786
787 $this->p->debug->log( 'exiting early: returned submit options is false' );
788 }
789
790 return;
791 }
792
793 $md_opts = apply_filters( 'wpsso_save_md_options', $md_opts, $mod );
794 $md_opts = apply_filters( 'wpsso_save_' . $mod[ 'name' ] . '_options', $md_opts, $post_id, $mod );
795
796 return self::update_meta( $post_id, WPSSO_META_NAME, $md_opts );
797 }
798
799 /*
800 * Use $rel = false to extend WpssoAbstractWpMeta->delete_options().
801 */
802 public function delete_options( $post_id, $rel = false ) {
803
804 return self::delete_meta( $post_id, WPSSO_META_NAME );
805 }
806
807 public function after_insert_post( $post_id, $post_obj, $update, $post_before ) {
808
809 if ( null === $post_before ) {
810
811 if ( false === $update ) {
812
813 if ( isset( $post_obj->post_status ) && 'auto-draft' === $post_obj->post_status ) {
814
815 if ( ! empty( $this->p->options[ 'plugin_add_to_' . $post_obj->post_type ] ) ) {
816
817 $mod = $this->get_mod( $post_id );
818
819 list(
820 parent::$head_tags, // Used by WpssoAbstractWpMeta->is_meta_page().
821 parent::$head_info // Used by WpssoAbstractWpMeta->check_head_info().
822 ) = $this->p->util->cache->refresh_mod_head_meta( $mod );
823 }
824 }
825 }
826 }
827 }
828
829 /*
830 * Get all publicly accessible post ids using WP_Query->query().
831 */
832 public static function get_public_ids( array $posts_args = array() ) {
833
834 $wpsso =& Wpsso::get_instance();
835
836 /*
837 * The WPML integration module returns true so WPML can filter posts for the current language.
838 *
839 * See WpssoAbstractWpMeta->get_column_meta_query_og_type().
840 * See WpssoIntegLangWpml->__construct().
841 * See WpssoIntegLangQtranslateXt->__construct().
842 */
843 $filter_name = 'wpsso_post_public_ids_suppress_filters';
844
845 if ( $wpsso->debug->enabled ) {
846
847 $wpsso->debug->log( 'applying filters "' . $filter_name . '"' );
848 }
849
850 $suppress_filters = apply_filters( $filter_name, true );
851
852 $posts_args = array_merge( array(
853 'has_password' => false,
854 'order' => 'DESC', // Newest first.
855 'orderby' => 'date',
856 'post_status' => 'publish', // Only 'publish' (not 'auto-draft', 'draft', 'future', 'inherit', 'pending', 'private', or 'trash').
857 'post_type' => 'any', // Return any post, page, or custom post type.
858 'paged' => false,
859 'posts_per_page' => -1, // The number of posts to query for. -1 to request all posts.
860 'no_found_rows' => true, // Skip counting total rows found - should be enabled when pagination is not needed.
861 'suppress_filters' => $suppress_filters,
862 ), $posts_args, array( 'fields' => 'ids' ) ); // Return an array of post ids.
863
864 /*
865 * See WpssoIntegLangQtranslateXt->filter_post_public_ids_posts_args().
866 */
867 $filter_name = 'wpsso_post_public_ids_posts_args';
868
869 if ( $wpsso->debug->enabled ) {
870
871 $wpsso->debug->log( 'applying filters "' . $filter_name . '"' );
872 }
873
874 $posts_args = apply_filters( $filter_name, $posts_args );
875
876 if ( $wpsso->debug->enabled ) {
877
878 $wpsso->debug->log_arr( 'posts_args', $posts_args );
879 }
880
881 $mtime_start = microtime( $get_float = true );
882
883 $public_ids = SucomUtilWP::get_posts( $posts_args );
884
885 $mtime_total = microtime( $get_float = true ) - $mtime_start;
886
887 if ( $wpsso->debug->enabled ) {
888
889 $wpsso->debug->log( count( $public_ids ) . ' post IDs returned in ' . sprintf( '%0.3f secs', $mtime_total ) );
890 }
891
892 $filter_name = 'wpsso_post_public_ids';
893
894 if ( $wpsso->debug->enabled ) {
895
896 $wpsso->debug->log( 'applying filters "' . $filter_name . '"' );
897 }
898
899 return apply_filters( $filter_name, $public_ids, $posts_args );
900 }
901
902 /*
903 * Get the children for a post.
904 *
905 * Returns an array of post ids for a given $mod object.
906 *
907 * The 'posts_per_page' value should be set for an archive page before calling this method.
908 *
909 * See WpssoPost->clear_cache().
910 * See WpssoAbstractWpMeta->get_posts_mods().
911 */
912 public function get_posts_ids( array $mod ) {
913
914 if ( $this->p->debug->enabled ) {
915
916 $this->p->debug->mark();
917 }
918
919 if ( empty( $mod[ 'is_post' ] ) ) {
920
921 if ( $this->p->debug->enabled ) {
922
923 $this->p->debug->log( 'exiting early: ' . $mod[ 'name' ] . ' ID ' . $mod[ 'id' ] . ' is not a post' );
924 }
925
926 return array();
927 }
928
929 $posts_args = array_merge( array(
930 'has_password' => false,
931 'order' => 'DESC', // Newest first.
932 'orderby' => 'date',
933 'post_status' => 'publish', // Only 'publish', not 'auto-draft', 'draft', 'future', 'inherit', 'pending', 'private', or 'trash'.
934 'post_type' => 'any', // Return posts, pages, or any custom post type.
935 'post_parent' => $mod[ 'id' ],
936 'child_of' => $mod[ 'id' ], // Only include direct children.
937 ), $mod[ 'posts_args' ], array( 'fields' => 'ids' ) ); // Return an array of post ids.
938
939 if ( $this->p->debug->enabled ) {
940
941 $this->p->debug->log( 'getting posts for direct children of ' . $mod[ 'name' ] . ' ID ' . $mod[ 'id' ] );
942
943 $this->p->debug->log_arr( 'posts_args', $posts_args );
944 }
945
946 $mtime_start = microtime( $get_float = true );
947
948 $posts_ids = SucomUtilWP::get_posts( $posts_args ); // Alternative to get_posts() that does not exclude sticky posts.
949
950 $mtime_total = microtime( $get_float = true ) - $mtime_start;
951
952 if ( $this->p->debug->enabled ) {
953
954 $this->p->debug->log( count( $posts_ids ) . ' post IDs returned in ' . sprintf( '%0.3f secs', $mtime_total ) );
955 }
956
957 return $posts_ids;
958 }
959
960 /*
961 * This method is hooked to the 'manage_pages_columns' filter.
962 *
963 * See https://core.trac.wordpress.org/browser/tags/5.8.1/src/wp-admin/includes/class-wp-posts-list-table.php#L711.
964 */
965 public function add_page_column_headings( $columns ) {
966
967 add_filter( 'manage_edit-page_sortable_columns', array( $this, 'add_sortable_columns' ), 10, 1 );
968
969 return $this->add_column_headings( $columns, $post_type = 'page' );
970 }
971
972 /*
973 * This method is hooked to the 'manage_posts_columns' filter.
974 *
975 * Some plugins have been known to call the 'manage_posts_columns' filter with only one argument, so include a
976 * default value for the second argument to avoid a PHP fatal error.
977 *
978 * See https://core.trac.wordpress.org/browser/tags/5.8.1/src/wp-admin/includes/class-wp-posts-list-table.php#L722.
979 */
980 public function add_post_column_headings( $columns, $post_type = 'post' ) {
981
982 add_filter( 'manage_edit-' . $post_type . '_sortable_columns', array( $this, 'add_sortable_columns' ), 10, 1 );
983
984 return $this->add_column_headings( $columns, $post_type );
985 }
986
987 /*
988 * This method is hooked to the 'manage_media_columns' filter.
989 *
990 * See https://core.trac.wordpress.org/browser/tags/5.8.1/src/wp-admin/includes/class-wp-media-list-table.php#L366.
991 */
992 public function add_media_column_headings( $columns ) {
993
994 add_filter( 'manage_upload_sortable_columns', array( $this, 'add_sortable_columns' ), 10, 1 );
995
996 return $this->add_column_headings( $columns, $post_type = 'attachment' );
997 }
998
999 /*
1000 * Hooked to the 'manage_pages_custom_column' action.
1001 * Hooked to the 'manage_posts_custom_column' action.
1002 * Hooked to the 'manage_media_custom_column' action.
1003 */
1004 public function show_column_content( $column_name, $post_id ) {
1005
1006 echo $this->get_column_content( '', $column_name, $post_id );
1007 }
1008
1009 public function get_update_meta_cache( $post_id ) {
1010
1011 return SucomUtilWP::get_update_meta_cache( $post_id, $meta_type = 'post' );
1012 }
1013
1014 /*
1015 * Hooked into the current_screen action.
1016 *
1017 * Sets the parent::$head_tags and parent::$head_info class properties.
1018 */
1019 public function load_meta_page( $screen = false ) {
1020
1021 if ( $this->p->debug->enabled ) {
1022
1023 $this->p->debug->mark();
1024 }
1025
1026 /*
1027 * All meta modules set this property, so use it to optimize code execution.
1028 */
1029 if ( false !== parent::$head_tags || ! isset( $screen->id ) ) {
1030
1031 return;
1032 }
1033
1034 if ( $this->p->debug->enabled ) {
1035
1036 $this->p->debug->log( 'screen id = ' . $screen->id );
1037 }
1038
1039 switch ( $screen->id ) {
1040
1041 case 'upload':
1042 case ( 0 === strpos( $screen->id, 'edit-' ) ? true : false ): // Posts list table.
1043
1044 if ( $this->p->debug->enabled ) {
1045
1046 $this->p->debug->log( 'exiting early: not a recognized post page' );
1047 }
1048
1049 return;
1050 }
1051
1052 /*
1053 * Get the post object for sanity checks.
1054 */
1055 if ( $this->p->debug->enabled ) {
1056
1057 $this->p->debug->log( 'calling get_post_object( $use_post = true )' );
1058 }
1059
1060 $post_obj = SucomUtilWP::get_post_object( $use_post = true );
1061 $post_id = empty( $post_obj->ID ) ? 0 : $post_obj->ID;
1062
1063 if ( ! $post_obj instanceof WP_Post ) {
1064
1065 if ( $this->p->debug->enabled ) {
1066
1067 if ( is_object( $post_obj ) ) {
1068
1069 $this->p->debug->log( 'exiting early: ' . get_class( $post_obj ) . ' object is not an instance of WP_Post' );
1070
1071 } else $this->p->debug->log( 'exiting early: post object is not an object' );
1072 }
1073
1074 return;
1075 }
1076
1077 /*
1078 * Define parent::$head_tags and signal to other 'current_screen' actions that this is a post page.
1079 */
1080 parent::$head_tags = array(); // Used by WpssoAbstractWpMeta->is_meta_page().
1081
1082 /*
1083 * WpssoPost->post_can_have_meta() returns false:
1084 *
1085 * If the $post argument is not numeric or an instance of WP_Post.
1086 * If the post ID is empty.
1087 * If the post type is empty.
1088 * If the post status is empty.
1089 * If the post type is 'revision'.
1090 * If the post status is 'trash'.
1091 */
1092 if ( ! $this->post_can_have_meta( $post_obj ) ) {
1093
1094 if ( $this->p->debug->enabled ) {
1095
1096 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' invalid for metadata' );
1097 }
1098
1099 return;
1100
1101 } elseif ( isset( $_REQUEST[ 'action' ] ) && 'trash' === $_REQUEST[ 'action' ] ) {
1102
1103 if ( $this->p->debug->enabled ) {
1104
1105 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' is being trashed' );
1106 }
1107
1108 return;
1109
1110 } elseif ( isset( $_REQUEST[ 'action' ] ) && 'delete' === $_REQUEST[ 'action' ] ) {
1111
1112 if ( $this->p->debug->enabled ) {
1113
1114 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' is being deleted' );
1115 }
1116
1117 return;
1118 }
1119
1120 $mod = $this->get_mod( $post_id );
1121
1122 if ( $this->p->debug->enabled ) {
1123
1124 $this->p->debug->log( 'post id = ' . $post_id );
1125 $this->p->debug->log( 'home url = ' . get_option( 'home' ) );
1126 $this->p->debug->log( 'locale current = ' . SucomUtilWP::get_locale() );
1127 $this->p->debug->log( 'locale default = ' . SucomUtilWP::get_locale( 'default' ) );
1128 $this->p->debug->log( 'locale mod = ' . SucomUtilWP::get_locale( $mod ) );
1129 $this->p->debug->log( SucomUtil::get_array_pretty( $mod ) );
1130 }
1131
1132 if ( SucomUtilWP::doing_block_editor() && ( ! empty( $_REQUEST[ 'meta-box-loader' ] ) || ! empty( $_REQUEST[ 'meta_box' ] ) ) ) {
1133
1134 if ( $this->p->debug->enabled ) {
1135
1136 $this->p->debug->log( 'skipping head: doing block editor for metabox' );
1137 }
1138
1139 } elseif ( empty( $this->p->options[ 'plugin_add_to_' . $post_obj->post_type ] ) ) {
1140
1141 if ( $this->p->debug->enabled ) {
1142
1143 $this->p->debug->log( 'skipping head: metabox not enabled for post type ' . $post_obj->post_type );
1144 }
1145
1146 } else {
1147
1148 /*
1149 * Hooked by woocommerce module to load front-end libraries and start a session.
1150 */
1151 do_action( 'wpsso_admin_post_head', $mod );
1152
1153 list(
1154 parent::$head_tags, // Used by WpssoAbstractWpMeta->is_meta_page().
1155 parent::$head_info // Used by WpssoAbstractWpMeta->check_head_info().
1156 ) = $this->p->util->cache->refresh_mod_head_meta( $mod );
1157
1158 /*
1159 * Check for missing open graph image and description values.
1160 */
1161 if ( $mod[ 'id' ] && $mod[ 'is_public' ] && 'publish' === $mod[ 'post_status' ] ) {
1162
1163 $this->check_head_info( $mod );
1164
1165 /*
1166 * Check duplicates only when the post is available publicly and we have a valid permalink.
1167 */
1168 if ( current_user_can( 'manage_options' ) ) {
1169
1170 $check_head = empty( $this->p->options[ 'plugin_check_head' ] ) ? false : true;
1171
1172 if ( apply_filters( 'wpsso_check_post_head', $check_head, $post_id, $post_obj ) ) {
1173
1174 if ( $this->p->debug->enabled ) {
1175
1176 $this->p->debug->log( 'checking post head' );
1177 }
1178
1179 $this->check_post_head( $post_id, $post_obj );
1180 }
1181 }
1182 }
1183 }
1184
1185 $action_query = 'wpsso-action';
1186
1187 if ( ! empty( $_GET[ $action_query ] ) ) {
1188
1189 $action_name = SucomUtil::sanitize_hookname( $_GET[ $action_query ] );
1190
1191 if ( $this->p->debug->enabled ) {
1192
1193 $this->p->debug->log( 'found action query: ' . $action_name );
1194 }
1195
1196 if ( empty( $_GET[ WPSSO_NONCE_NAME ] ) ) { // WPSSO_NONCE_NAME is an md5() string
1197
1198 if ( $this->p->debug->enabled ) {
1199
1200 $this->p->debug->log( 'nonce token query field missing' );
1201 }
1202
1203 } elseif ( ! wp_verify_nonce( $_GET[ WPSSO_NONCE_NAME ], WpssoAdmin::get_nonce_action() ) ) {
1204
1205 $this->p->notice->err( sprintf( __( 'Nonce token validation failed for %1$s action "%2$s".', 'wpsso' ), 'post', $action_name ) );
1206
1207 } else {
1208
1209 $_SERVER[ 'REQUEST_URI' ] = remove_query_arg( array( $action_query, WPSSO_NONCE_NAME ) );
1210
1211 switch ( $action_name ) {
1212
1213 default:
1214
1215 do_action( 'wpsso_load_meta_page_post_' . $action_name, $post_id, $post_obj );
1216
1217 break;
1218 }
1219 }
1220 }
1221 }
1222
1223 public function check_post_head( $post_id = true, $post_obj = false ) {
1224
1225 if ( $this->p->debug->enabled ) {
1226
1227 $this->p->debug->mark();
1228 }
1229
1230 if ( empty( $post_id ) ) {
1231
1232 $post_id = true;
1233 }
1234
1235 if ( ! is_object( $post_obj ) ) {
1236
1237 $post_obj = SucomUtilWP::get_post_object( $post_id );
1238
1239 if ( empty( $post_obj ) ) {
1240
1241 if ( $this->p->debug->enabled ) {
1242
1243 $this->p->debug->log( 'exiting early: unable to get the post object');
1244 }
1245
1246 return; // Stop here.
1247 }
1248 }
1249
1250 if ( ! is_numeric( $post_id ) ) { // Just in case the post_id is true/false.
1251
1252 if ( empty( $post_obj->ID ) ) {
1253
1254 if ( $this->p->debug->enabled ) {
1255
1256 $this->p->debug->log( 'exiting early: post id in post object is empty');
1257 }
1258
1259 return; // Stop here.
1260 }
1261
1262 $post_id = $post_obj->ID;
1263 }
1264
1265 static $do_once = array();
1266
1267 if ( isset( $do_once[ $post_id ] ) ) return; // Stop here.
1268
1269 $do_once[ $post_id ] = true;
1270
1271 /*
1272 * Only check publicly available posts.
1273 */
1274 if ( ! isset( $post_obj->post_status ) || 'publish' !== $post_obj->post_status ) {
1275
1276 if ( $this->p->debug->enabled ) {
1277
1278 $this->p->debug->log( 'exiting early: post_status "' . $post_obj->post_status . '" is not publish' );
1279 }
1280
1281 return; // Stop here.
1282 }
1283
1284 if ( empty( $post_obj->post_type ) || SucomUtilWP::is_post_type_public( $post_obj->post_type ) ) {
1285
1286 if ( $this->p->debug->enabled ) {
1287
1288 $this->p->debug->log( 'exiting early: post_type "' . $post_obj->post_type . '" not public' );
1289 }
1290
1291 return; // Stop here.
1292 }
1293
1294 $exec_count = $this->p->debug->enabled ? 0 : (int) get_option( WPSSO_POST_CHECK_COUNT_NAME, $default = 0 );
1295 $max_count = SucomUtil::get_const( 'WPSSO_DUPE_CHECK_HEADER_COUNT', 3 );
1296
1297 if ( $exec_count >= $max_count ) {
1298
1299 if ( $this->p->debug->enabled ) {
1300
1301 $this->p->debug->log( 'exiting early: exec_count of ' . $exec_count . ' exceeds max_count of ' . $max_count );
1302 }
1303
1304 return; // Stop here.
1305 }
1306
1307 if ( ini_get( 'open_basedir' ) ) {
1308
1309 $check_url = $this->p->util->get_canonical_url( $post_id );
1310
1311 } else $check_url = $this->p->util->get_shortlink( $post_id, $context = 'post' );
1312
1313 $check_url_htmlenc = SucomUtil::encode_html_emoji( urldecode( $check_url ) ); // Does not double-encode.
1314
1315 if ( empty( $check_url ) ) {
1316
1317 if ( $this->p->debug->enabled ) {
1318
1319 $this->p->debug->log( 'exiting early: invalid shortlink' );
1320 }
1321
1322 return; // Stop here.
1323 }
1324
1325 /*
1326 * Fetch the post HTML.
1327 */
1328 $is_admin = is_admin(); // Call the function only once.
1329
1330 if ( $this->p->debug->enabled ) {
1331
1332 $this->p->debug->log( 'getting html for ' . $check_url );
1333 }
1334
1335 if ( $is_admin ) {
1336
1337 $this->p->notice->inf( sprintf( __( 'Checking %1$s for duplicate meta tags...', 'wpsso' ),
1338 '<a href="' . $check_url . '">' . $check_url_htmlenc . '</a>' ) );
1339 }
1340
1341 /*
1342 * Use the Facebook user agent to get Open Graph meta tags.
1343 */
1344 $curl_opts = array(
1345 'CURLOPT_USERAGENT' => WPSSO_PHP_CURL_USERAGENT_FACEBOOK,
1346 );
1347
1348 $this->p->cache->clear( $check_url ); // Clear the cached webpage.
1349
1350 $exp_secs = $this->p->debug->enabled ? false : null;
1351 $webpage_html = $this->p->cache->get( $check_url, $format = 'raw', $cache_type = 'transient', $exp_secs, $pre_ext = '', $curl_opts );
1352 $url_mtime = $this->p->cache->get_url_mtime( $check_url );
1353 $html_size = strlen( $webpage_html );
1354 $error_size = (int) SucomUtil::get_const( 'WPSSO_DUPE_CHECK_ERROR_SIZE', 2500000 );
1355 $warning_time = (int) SucomUtil::get_const( 'WPSSO_DUPE_CHECK_WARNING_TIME', 2.5 );
1356 $timeout_time = (int) SucomUtil::get_const( 'WPSSO_DUPE_CHECK_TIMEOUT_TIME', 3.0 );
1357
1358 if ( $html_size > $error_size ) {
1359
1360 if ( $this->p->debug->enabled ) {
1361
1362 $this->p->debug->log( 'size of ' . $check_url . ' is ' . $html_size . ' bytes' );
1363 }
1364
1365 /*
1366 * If debug is enabled, the webpage may be larger than normal, so skip this warning.
1367 */
1368 if ( $is_admin && ! $this->p->debug->enabled ) {
1369
1370 $notice_msg = sprintf( __( 'The webpage HTML retrieved from %1$s is %2$s bytes.', 'wpsso' ),
1371 '<a href="' . $check_url . '">' . $check_url_htmlenc . '</a>', $html_size ) . ' ';
1372
1373 $notice_msg .= sprintf( __( 'This exceeds the maximum limit of %1$s bytes imposed by the Google crawler.', 'wpsso' ),
1374 $error_size ) . ' ';
1375
1376 $notice_msg .= __( 'If you do not reduce the webpage HTML size, Google will refuse to crawl this webpage.', 'wpsso' );
1377
1378 $this->p->notice->err( $notice_msg );
1379 }
1380 }
1381
1382 if ( true === $url_mtime ) {
1383
1384 if ( $this->p->debug->enabled ) {
1385
1386 $this->p->debug->log( 'fetched ' . $check_url . ' from transient cache' );
1387 }
1388
1389 } elseif ( false === $url_mtime ) {
1390
1391 if ( $this->p->debug->enabled ) {
1392
1393 $this->p->debug->log( 'fetched ' . $check_url . ' returned a failure' );
1394 }
1395
1396 } else {
1397
1398 if ( $this->p->debug->enabled ) {
1399
1400 $this->p->debug->log( 'fetched ' . $check_url . ' in ' . $url_mtime . ' secs' );
1401 }
1402
1403 if ( $is_admin && $url_mtime > $warning_time ) {
1404
1405 $this->p->notice->warn(
1406 sprintf( __( 'Retrieving the webpage HTML for %1$s took %2$s seconds.', 'wpsso' ),
1407 '<a href="' . $check_url . '">' . $check_url_htmlenc . '</a>', $url_mtime ) . ' ' .
1408 sprintf( __( 'This exceeds the recommended limit of %1$s seconds (crawlers often time-out after %2$s seconds).',
1409 'wpsso' ), $warning_time, $timeout_time ) . ' ' .
1410 __( 'Please consider improving the speed of your site.', 'wpsso' ) . ' ' .
1411 __( 'As an added benefit, a faster site will also improve ranking in search results.', 'wpsso' ) . ' ;-)'
1412 );
1413 }
1414 }
1415
1416 if ( empty( $webpage_html ) ) {
1417
1418 if ( $this->p->debug->enabled ) {
1419
1420 $this->p->debug->log( 'exiting early: error retrieving content from ' . $check_url );
1421 }
1422
1423 if ( $is_admin ) {
1424
1425 $this->p->notice->err( sprintf( __( 'Error retrieving content from <a href="%1$s">%1$s</a>.', 'wpsso' ), $check_url ) );
1426 }
1427
1428 return; // Stop here.
1429
1430 } elseif ( stripos( $webpage_html, '<html' ) === false ) { // Webpage must have an <html> tag.
1431
1432 if ( $this->p->debug->enabled ) {
1433
1434 $this->p->debug->log( 'exiting early: <html> tag not found in ' . $check_url );
1435 }
1436
1437 if ( $is_admin ) {
1438
1439 $this->p->notice->err( sprintf( __( 'An %1$s tag was not found in <a href="%2$s">%2$s</a>.', 'wpsso' ),
1440 '&lt;html&gt;', $check_url ) );
1441 }
1442
1443 return; // Stop here
1444
1445 } elseif ( ! preg_match( '/<meta[ \n]/i', $webpage_html ) ) { // Webpage must have one or more <meta/> tags.
1446
1447 if ( $this->p->debug->enabled ) {
1448
1449 $this->p->debug->log( 'exiting early: No <meta/> HTML tags were found in ' . $check_url );
1450 }
1451
1452 if ( $is_admin ) {
1453
1454 $this->p->notice->err( sprintf( __( 'No %1$s HTML tags were found in <a href="%2$s">%2$s</a>.', 'wpsso' ),
1455 '&lt;meta/&gt;', $check_url ) );
1456 }
1457
1458 return; // Stop here.
1459
1460 } elseif ( false === strpos( $webpage_html, WPSSO_DATA_ID . ' begin' ) ) { // Webpage should include our own meta tags.
1461
1462 if ( $this->p->debug->enabled ) {
1463
1464 $this->p->debug->log( 'exiting early: ' . WPSSO_DATA_ID . ' not found in ' . $check_url );
1465 }
1466
1467 if ( $is_admin ) {
1468
1469 $short_name = $this->p->cf[ 'plugin' ][ 'wpsso' ][ 'short' ];
1470
1471 $notice_msg = sprintf( __( 'The %1$s meta tags and Schema markup section was not found in <a href="%2$s">%2$s</a>.',
1472 'wpsso' ), $short_name, $check_url ) . ' ';
1473
1474 $notice_msg .= __( 'Does a caching plugin or service need to be refreshed?', 'wpsso' );
1475
1476 $this->p->notice->err( $notice_msg );
1477 }
1478
1479 return; // Stop here.
1480 }
1481
1482 /*
1483 * Remove the WPSSO meta tags and Schema markup section from the webpage to check for duplicate meta tags and markup.
1484 */
1485 if ( $this->p->debug->enabled ) {
1486
1487 $this->p->debug->log( 'removing the wpsso meta tag section from the webpage html' );
1488 }
1489
1490 $mt_mark_preg = $this->p->head->get_mt_data( 'preg' );
1491
1492 $count = null;
1493
1494 $html_stripped = preg_replace( $mt_mark_preg, '', $webpage_html, $limit = -1, $count );
1495
1496 if ( ! $count ) {
1497
1498 if ( $this->p->debug->enabled ) {
1499
1500 $this->p->debug->log( 'exiting early: preg_replace() function failed to remove the meta tag section' );
1501 }
1502
1503 if ( $is_admin ) {
1504
1505 $short_name = $this->p->cf[ 'plugin' ][ 'wpsso' ][ 'short' ];
1506
1507 $notice_msg = sprintf( __( 'The PHP %1$s function failed to remove the %2$s meta tag section.', 'wpsso' ), '<code>preg_replace()</code>', $short_name ) . ' ';
1508
1509 $notice_msg .= __( 'This could indicate a problem with PHP\'s PCRE library, or an optimization plugin / service corrupting the webpage HTML.', 'wpsso' ) . ' ';
1510
1511 $notice_msg .= __( 'You should consider updating or having your hosting provider update your PHP installation and its PCRE library.', 'wpsso' );
1512
1513 $this->p->notice->err( $notice_msg );
1514 }
1515
1516 return; // Stop here.
1517 }
1518
1519 /*
1520 * Check the stripped webpage HTML for duplicate html tags.
1521 */
1522 if ( $this->p->debug->enabled ) {
1523
1524 $this->p->debug->log( 'checking the stripped webpage html for duplicates' );
1525 }
1526
1527 $metas = $this->p->util->get_html_head_meta( $html_stripped, $query = '/html/head/link|/html/head/meta', $libxml_errors = true );
1528
1529 $check_opts = SucomUtil::preg_grep_keys( '/^add_/', $this->p->options, $invert = false, $replace = '' );
1530
1531 $conflicts_msg = __( 'Conflict detected - your theme or another plugin is adding %1$s to the head section of this webpage.', 'wpsso' );
1532
1533 $conflicts_found = 0;
1534
1535 if ( is_array( $metas ) ) {
1536
1537 if ( empty( $metas ) ) { // No link or meta tags found.
1538
1539 if ( $this->p->debug->enabled ) {
1540
1541 $this->p->debug->log( 'error parsing head meta for ' . $check_url );
1542 }
1543
1544 if ( $is_admin ) {
1545
1546 $validator_url = 'https://validator.w3.org/nu/?doc=' . urlencode( $check_url );
1547
1548 $settings_page_url = $this->p->util->get_admin_url( 'general#sucom-tabset_social_search-tab_pinterest' );
1549
1550 $notice_msg = sprintf( __( 'An error occured parsing the head meta tags from <a href="%1$s">%1$s</a> (no "link" or "meta" HTML tags were found).', 'wpsso' ), $check_url ) . ' ';
1551
1552 $notice_msg .= __( 'The webpage may contain HTML syntax errors preventing PHP from successfully parsing the HTML document.',
1553 'wpsso' ) . ' ';
1554
1555 $notice_msg .= sprintf( __( 'Please review the <a href="%1$s">W3C Markup Validator</a> results and correct any syntax errors.',
1556 'wpsso' ), $validator_url ) . ' ';
1557
1558 $notice_key = 'possible-html-syntax-errors-for-' . $check_url;
1559
1560 $this->p->notice->err( $notice_msg, null, $notice_key );
1561 }
1562
1563 } else {
1564
1565 foreach( array(
1566 'link' => array( 'rel' ),
1567 'meta' => array( 'name', 'property', 'itemprop' ),
1568 ) as $tag => $types ) {
1569
1570 if ( isset( $metas[ $tag ] ) ) {
1571
1572 foreach( $metas[ $tag ] as $meta ) {
1573
1574 foreach( $types as $type ) {
1575
1576 if ( isset( $meta[ $type ] ) && $meta[ $type ] !== 'generator' &&
1577 ! empty( $check_opts[ $tag . '_' . $type . '_' . $meta[ $type ] ] ) ) {
1578
1579 $conflicts_found++;
1580
1581 $conflicts_tag = '<code>' . $tag . ' ' . $type . '="' . $meta[ $type ] . '"</code>';
1582
1583 $this->p->notice->err( sprintf( $conflicts_msg, $conflicts_tag ) );
1584 }
1585 }
1586 }
1587 }
1588 }
1589
1590 if ( $is_admin ) {
1591
1592 $exec_count++;
1593
1594 if ( $conflicts_found ) {
1595
1596 $notice_key = 'duplicate-meta-tags-found';
1597
1598 $notice_msg = sprintf( __( '%1$d duplicate meta tags found.', 'wpsso' ), $conflicts_found ) . ' ';
1599
1600 $notice_msg .= sprintf( __( 'Check %1$d of %2$d failed (will retry)...', 'wpsso' ), $exec_count, $max_count );
1601
1602 $this->p->notice->warn( $notice_msg, null, $notice_key );
1603
1604 } else {
1605
1606 $notice_key = 'no-duplicate-meta-tags-found';
1607
1608 $notice_msg = __( 'Awesome! No duplicate meta tags found.', 'wpsso' ) . ' :-) ';
1609
1610 if ( $this->p->debug->enabled ) {
1611
1612 $notice_msg .= __( 'Debug option is enabled - will keep repeating duplicate check...', 'wpsso' );
1613
1614 } else {
1615
1616 $notice_msg .= sprintf( __( 'Check %1$d of %2$d successful...', 'wpsso' ), $exec_count, $max_count );
1617 }
1618
1619 update_option( WPSSO_POST_CHECK_COUNT_NAME, $exec_count, $autoload = false );
1620
1621 $this->p->notice->inf( $notice_msg, null, $notice_key );
1622 }
1623 }
1624 }
1625 }
1626 }
1627
1628 /*
1629 * Use $post_obj = false to extend WpssoAbstractWpMeta->add_meta_boxes().
1630 *
1631 * The Organization and Place post types do not have a Document SSO metabox as they have their own metabox.
1632 *
1633 * See WpssoOpmIntegAdminPost->add_meta_boxes().
1634 */
1635 public function add_meta_boxes( $post_type, $post_obj = false ) {
1636
1637 if ( $this->p->debug->enabled ) {
1638
1639 $this->p->debug->mark();
1640 }
1641
1642 $post_id = empty( $post_obj->ID ) ? 0 : $post_obj->ID;
1643
1644 if ( empty( $this->p->options[ 'plugin_add_to_' . $post_type ] ) ) {
1645
1646 if ( $this->p->debug->enabled ) {
1647
1648 $this->p->debug->log( 'exiting early: cannot add metabox to post type "' . $post_type . '"' );
1649 }
1650
1651 return;
1652 }
1653
1654 $capability = 'page' === $post_type ? 'edit_page' : 'edit_post';
1655
1656 if ( ! current_user_can( $capability, $post_id ) ) {
1657
1658 if ( $this->p->debug->enabled ) {
1659
1660 $this->p->debug->log( 'exiting early: cannot ' . $capability . ' for post id ' . $post_id );
1661 }
1662
1663 return;
1664 }
1665
1666 $metabox_id = $this->p->cf[ 'meta' ][ 'id' ];
1667 $metabox_title = _x( $this->p->cf[ 'meta' ][ 'title' ], 'metabox title', 'wpsso' );
1668 $metabox_screen = $post_type;
1669 $metabox_context = 'normal';
1670 $metabox_prio = 'default';
1671 $callback_args = array( // Second argument passed to the callback function / method.
1672 'metabox_id' => $metabox_id,
1673 'metabox_title' => $metabox_title,
1674 '__block_editor_compatible_meta_box' => true,
1675 );
1676
1677 if ( $this->p->debug->enabled ) {
1678
1679 $this->p->debug->log( 'adding metabox id wpsso_' . $metabox_id . ' for screen ' . $metabox_screen );
1680 }
1681
1682 add_meta_box( 'wpsso_' . $metabox_id, $metabox_title, array( $this, 'show_metabox_' . $metabox_id ),
1683 $metabox_screen, $metabox_context, $metabox_prio, $callback_args );
1684 }
1685
1686 public function ajax_get_validate_submenu() {
1687
1688 $doing_ajax = SucomUtilWP::doing_ajax();
1689
1690 if ( ! $doing_ajax ) { // Just in case.
1691
1692 return;
1693 }
1694
1695 $post_obj = $this->die_or_get_ajax_post_obj();
1696
1697 require_once ABSPATH . WPINC . '/class-wp-admin-bar.php';
1698
1699 $admin_bar_class = apply_filters( 'wp_admin_bar_class', 'WP_Admin_Bar' );
1700 $ajax_admin_bar = new $admin_bar_class;
1701 $parent_id = $this->p->page->add_admin_bar_menu_validate( $ajax_admin_bar, $post_obj->ID );
1702 $metabox_html = '';
1703
1704 if ( empty( $parent_id ) ) {
1705
1706 die( $metabox_html );
1707 }
1708
1709 $nodes = $ajax_admin_bar->get_nodes();
1710 $parent_node = $nodes[ $parent_id ];
1711 $menu_class = 'ab-submenu' . ( empty( $parent_node->meta[ 'class' ] ) ? '' : $parent_node->meta[ 'class' ] );
1712
1713 $metabox_html .= '<ul id="' . esc_attr( 'wp-admin-bar-' . $parent_node->id . '-default' ) . '"';
1714 $metabox_html .= $menu_class ? ' class="' . esc_attr( trim( $menu_class ) ) . '"' : '';
1715 $metabox_html .= '>';
1716
1717 foreach ( $nodes as $key => $node ) {
1718
1719 if ( $parent_id !== $node->parent ) {
1720
1721 continue;
1722 }
1723
1724 $has_link = ! empty( $node->href );
1725 $is_parent = ! empty( $node->children );
1726 $is_root_top_item = 'root-default' === $node->parent;
1727 $is_top_secondary_item = 'top-secondary' === $node->parent;
1728 $tabindex = isset( $node->meta[ 'tabindex' ] ) && is_numeric( $node->meta[ 'tabindex' ] ) ? (int) $node->meta[ 'tabindex' ] : '';
1729 $aria_attributes = '' !== $tabindex ? ' tabindex="' . $tabindex . '"' : '';
1730 $menu_class = empty( $node->meta[ 'class' ] ) ? '' : $node->meta[ 'class' ];
1731 $arrow = '';
1732
1733 if ( ! $is_root_top_item && ! $is_top_secondary_item && $is_parent ) {
1734
1735 $arrow = '<span class="wp-admin-bar-arrow" aria-hidden="true"></span>';
1736 }
1737
1738 $link = $has_link ?
1739 '<a class="ab-item"' . $aria_attributes . ' href="' . esc_url( $node->href ) . '"' :
1740 '<div class="ab-item ab-empty-item"' . $aria_attributes;
1741
1742 $attributes = array( 'onclick', 'target', 'title', 'rel', 'lang', 'dir' );
1743
1744 foreach ( $attributes as $attribute ) {
1745
1746 if ( empty( $node->meta[ $attribute ] ) ) {
1747
1748 continue;
1749 }
1750
1751 $link .= ' ' . $attribute . '="';
1752 $link .= 'onclick' === $attribute ? esc_js( $node->meta[ $attribute ] ) : esc_attr( $node->meta[ $attribute ] );
1753 $link .= '"';
1754 }
1755
1756 $link .= '>' . $arrow . $node->title;
1757 $link .= $has_link ? '</a>' : '</div>';
1758
1759 $metabox_html .= '<li id="' . esc_attr( 'wp-admin-bar-' . $node->id ) . '"';
1760 $metabox_html .= $menu_class ? ' class="' . esc_attr( trim( $menu_class ) ) . '"' : '';
1761 $metabox_html .= '>' . $link;
1762 $metabox_html .= empty( $node->meta[ 'html' ] ) ? '' : $node->meta[ 'html' ];
1763 $metabox_html .= '</li>' . "\n";
1764 }
1765
1766 $metabox_html .= '</ul>';
1767
1768 die( $metabox_html );
1769 }
1770
1771 public function ajax_get_metabox_sso() {
1772
1773 $doing_ajax = SucomUtilWP::doing_ajax();
1774
1775 if ( ! $doing_ajax ) { // Just in case.
1776
1777 return;
1778 }
1779
1780 $post_obj = $this->die_or_get_ajax_post_obj();
1781
1782 if ( ! empty( $this->p->options[ 'plugin_add_to_' . $post_obj->post_type ] ) ) {
1783
1784 $mod = $this->get_mod( $post_obj->ID );
1785
1786 list(
1787 parent::$head_tags, // Used by WpssoAbstractWpMeta->is_meta_page().
1788 parent::$head_info // Used by WpssoAbstractWpMeta->check_head_info().
1789 ) = $this->p->util->cache->refresh_mod_head_meta( $mod );
1790
1791 /*
1792 * Check for missing open graph image and description values.
1793 */
1794 if ( $mod[ 'id' ] && $mod[ 'is_public' ] && 'publish' === $mod[ 'post_status' ] ) {
1795
1796 $this->check_head_info( $mod );
1797 }
1798 }
1799
1800 $metabox_html = $this->get_metabox_sso( $post_obj );
1801
1802 die( $metabox_html );
1803 }
1804
1805 private function die_or_get_ajax_post_obj() {
1806
1807 $error_msg = '';
1808
1809 if ( SucomUtil::get_const( 'DOING_AUTOSAVE' ) ) {
1810
1811 die( -1 );
1812
1813 } elseif ( ! check_ajax_referer( WPSSO_NONCE_NAME, '_ajax_nonce', $die = false ) ) {
1814
1815 $error_msg = __( 'ajax request invalid nonce', 'wpsso' );
1816 }
1817
1818 $post_id = isset( $_POST[ 'post_id' ] ) ? SucomUtil::sanitize_int( $_POST[ 'post_id' ] ) : 0; // Returns integer or null.
1819
1820 /*
1821 * WpssoPost->post_can_have_meta() returns false:
1822 *
1823 * If the $post argument is not numeric or an instance of WP_Post.
1824 * If the post ID is empty.
1825 * If the post type is empty.
1826 * If the post status is empty.
1827 * If the post type is 'revision'.
1828 * If the post status is 'trash'.
1829 */
1830 if ( ! $this->post_can_have_meta( $post_id ) ) {
1831
1832 $error_msg = sprintf( __( 'post id %s invalid for metadata', 'wpsso' ), $post_id );
1833
1834 /*
1835 * Check user capability for the post id.
1836 */
1837 } elseif ( ! $this->user_can_edit( $post_id ) ) {
1838
1839 $user_id = get_current_user_id();
1840
1841 $error_msg = sprintf( __( 'user id %s cannot edit post id %s', 'wpsso' ), $user_id, $post_id );
1842 }
1843
1844 if ( $error_msg ) {
1845
1846 $stack = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
1847 $from = '';
1848 $class_seq = 1;
1849 $func_seq = 1;
1850
1851 if ( ! empty( $stack[ $class_seq ][ 'class' ] ) ) {
1852
1853 $from .= $stack[ $class_seq ][ 'class' ] . '::';
1854 }
1855
1856 if ( ! empty( $stack[ $func_seq ][ 'function' ] ) ) {
1857
1858 $from .= $stack[ $func_seq ][ 'function' ];
1859 }
1860
1861 $error_pre = trim( sprintf( __( '%s error:', 'wpsso' ), $from ) );
1862
1863 SucomUtil::safe_error_log( $error_pre . ' ' . $error_msg );
1864
1865 die( -1 );
1866 }
1867
1868 return SucomUtilWP::get_post_object( $post_id );
1869 }
1870
1871 /*
1872 * Use $rel = false to extend WpssoAbstractWpMeta->clear_cache().
1873 *
1874 * See WpssoIntegEcomWooCommerce->clear_product_cache().
1875 */
1876 public function clear_cache( $post_id, $rel = false ) {
1877
1878 if ( $this->p->debug->enabled ) {
1879
1880 $this->p->debug->log_args( array(
1881 'post_id' => $post_id,
1882 ) );
1883 }
1884
1885 static $do_once = array(); // Just in case - prevent recursion.
1886
1887 if ( isset( $do_once[ $post_id ] ) ) return; // Stop here.
1888
1889 $do_once[ $post_id ] = true;
1890
1891 /*
1892 * WpssoPost->post_can_have_meta() returns false:
1893 *
1894 * If the $post argument is not numeric or an instance of WP_Post.
1895 * If the post ID is empty.
1896 * If the post type is empty.
1897 * If the post status is empty.
1898 * If the post type is 'revision'.
1899 * If the post status is 'trash'.
1900 */
1901 if ( ! $this->post_can_have_meta( $post_id ) ) {
1902
1903 if ( $this->p->debug->enabled ) {
1904
1905 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' invalid for metadata' );
1906 }
1907
1908 return;
1909 }
1910
1911 /*
1912 * Clear the post meta, content, and head caches.
1913 */
1914 $mod = $this->get_mod( $post_id );
1915
1916 $this->clear_mod_cache( $mod );
1917
1918 /*
1919 * Clear the permalink, canonical / shortlink url cache.
1920 */
1921 $permalink = get_permalink( $mod[ 'wp_obj' ] );
1922
1923 $this->p->cache->clear( $permalink );
1924
1925 if ( ini_get( 'open_basedir' ) ) {
1926
1927 $other_url = $this->p->util->get_canonical_url( $mod );
1928
1929 } else $other_url = $this->p->util->get_shortlink( $mod, $context = 'post' );
1930
1931 if ( $permalink !== $other_url ) {
1932
1933 $this->p->cache->clear( $other_url );
1934 }
1935
1936 /*
1937 * Clear post date, term, and author archive pages.
1938 */
1939 $post_year = get_the_time( 'Y', $mod[ 'wp_obj' ] );
1940 $post_month = get_the_time( 'm', $mod[ 'wp_obj' ] );
1941 $post_day = get_the_time( 'd', $mod[ 'wp_obj' ] );
1942
1943 $home_url = home_url( '/' );
1944 $post_year_url = get_year_link( $post_year );
1945 $post_month_url = get_month_link( $post_year, $post_month );
1946 $post_day_url = get_day_link( $post_year, $post_month, $post_day );
1947
1948 foreach ( array( $home_url, $post_year_url, $post_month_url, $post_day_url ) as $url ) {
1949
1950 $this->p->head->clear_head_array( $archive_page_mod = false, $url );
1951 }
1952
1953 /*
1954 * Clear the post terms (categories, tags, etc.) for published (aka public) posts.
1955 */
1956 if ( 'publish' === $mod[ 'post_status' ] ) {
1957
1958 foreach ( $mod[ 'post_taxonomies' ] as $tax_slug ) {
1959
1960 $post_terms = wp_get_post_terms( $post_id, $tax_slug ); // Returns WP_Error if taxonomy does not exist.
1961
1962 if ( is_array( $post_terms ) ) { // Just in case.
1963
1964 foreach ( $post_terms as $term_obj ) {
1965
1966 $this->p->term->clear_cache( $term_obj->term_id, $tax_slug );
1967 }
1968
1969 unset( $post_terms, $term_obj );
1970 }
1971 }
1972
1973 unset( $tax_slug );
1974 }
1975
1976 /*
1977 * Clear the post author archive page.
1978 */
1979 $this->p->user->clear_cache( $mod[ 'post_author' ] );
1980
1981 /*
1982 * The WPSSO FAQ add-on question shortcode attaches the post id to the question so the post cache can be
1983 * cleared if/when a question is updated.
1984 */
1985 $attached_ids = self::get_attached_ids( $post_id, 'post' );
1986
1987 foreach ( $attached_ids as $attach_id => $bool ) {
1988
1989 if ( $bool ) $this->clear_cache( $attach_id );
1990 }
1991
1992 unset( $attached_ids, $attach_id, $bool );
1993
1994 /*
1995 * Clear the cache for any direct children as well.
1996 */
1997 $children_ids = $this->get_posts_ids( $mod );
1998
1999 foreach ( $children_ids as $child_id ) {
2000
2001 $this->clear_cache( $child_id, $rel = false );
2002 }
2003
2004 unset( $children_ids, $child_id );
2005
2006 do_action( 'wpsso_clear_post_cache', $post_id, $mod );
2007 }
2008
2009 /*
2010 * Refresh the cache for a single post ID.
2011 *
2012 * This method will only execute once per post ID per page load.
2013 *
2014 * Use $rel = false to extend WpssoAbstractWpMeta->refresh_cache().
2015 *
2016 * See WpssoIntegEcomWooCommerce->refresh_product_cache().
2017 */
2018 public function refresh_cache( $post_id, $rel = false ) {
2019
2020 if ( $this->p->debug->enabled ) {
2021
2022 $this->p->debug->log_args( array(
2023 'post_id' => $post_id,
2024 ) );
2025 }
2026
2027 static $do_once = array(); // Just in case - prevent recursion.
2028
2029 if ( isset( $do_once[ $post_id ] ) ) return; // Stop here.
2030
2031 $do_once[ $post_id ] = true;
2032
2033 /*
2034 * WpssoPost->post_can_have_meta() returns false:
2035 *
2036 * If the $post argument is not numeric or an instance of WP_Post.
2037 * If the post ID is empty.
2038 * If the post type is empty.
2039 * If the post status is empty.
2040 * If the post type is 'revision'.
2041 * If the post status is 'trash'.
2042 */
2043 if ( ! $this->post_can_have_meta( $post_id ) ) {
2044
2045 if ( $this->p->debug->enabled ) {
2046
2047 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' invalid for metadata' );
2048 }
2049
2050 return;
2051 }
2052
2053 $mod = $this->get_mod( $post_id );
2054
2055 $this->p->util->cache->refresh_mod_head_meta( $mod );
2056
2057 /*
2058 * See WpssoCmcfActions->action_refresh_post_cache().
2059 * See WpssoGmfActions->action_refresh_post_cache().
2060 */
2061 do_action( 'wpsso_refresh_post_cache', $post_id, $mod );
2062 }
2063
2064 /*
2065 * Check user capability for the post id.
2066 *
2067 * Use $rel = false to extend WpssoAbstractWpMeta->user_can_edit().
2068 */
2069 public function user_can_edit( $post_id, $rel = false ) {
2070
2071 if ( ! $post_type = SucomUtil::get_request_value( 'post_type', 'POST' ) ) { // Uses sanitize_text_field.
2072
2073 $post_type = 'post';
2074 }
2075
2076 $capability = 'page' === $post_type ? 'edit_page' : 'edit_post';
2077
2078 if ( ! current_user_can( $capability, $post_id ) ) {
2079
2080 if ( $this->p->debug->enabled ) {
2081
2082 $this->p->debug->log( 'exiting early: cannot ' . $capability . ' for post id ' . $post_id );
2083 }
2084
2085 /*
2086 * Add notice only if the admin notices have not already been shown.
2087 */
2088 if ( $this->p->notice->is_admin_pre_notices() ) {
2089
2090 $this->p->notice->err( sprintf( __( 'Insufficient privileges to edit %1$s ID %2$s.', 'wpsso' ), $post_type, $post_id ) );
2091 }
2092
2093 return false;
2094 }
2095
2096 return true;
2097 }
2098
2099 public function get_mt_reviews( $post_id, $rating_meta = 'rating', $worst_rating = 1, $best_rating = 5 ) {
2100
2101 $reviews = array();
2102
2103 if ( empty( $post_id ) ) {
2104
2105 return $reviews;
2106 }
2107
2108 $comments = get_comments( array(
2109 'post_id' => $post_id,
2110 'status' => 'approve',
2111 'parent' => 0, // Parent ID of comment to retrieve children of (0 = don't get replies).
2112 'order' => 'DESC', // Newest first.
2113 'orderby' => 'date',
2114 'number' => WPSSO_SCHEMA_REVIEWS_MAX,
2115 ) );
2116
2117 if ( is_array( $comments ) ) {
2118
2119 if ( $this->p->debug->enabled ) {
2120
2121 $this->p->debug->log( count( $comments ) . ' comment objects' );
2122 }
2123
2124 foreach( $comments as $num => $comment_obj ) {
2125
2126 $comment_review = $this->get_mt_comment_review( $comment_obj, $rating_meta, $worst_rating, $best_rating );
2127
2128 if ( ! empty( $comment_review ) ) { // Just in case.
2129
2130 $reviews[] = $comment_review;
2131 }
2132 }
2133 }
2134
2135 if ( $this->p->debug->enabled ) {
2136
2137 $this->p->debug->log_arr( 'reviews', $reviews );
2138 }
2139
2140 return $reviews;
2141 }
2142
2143 /*
2144 * WpssoPost class specific methods.
2145 *
2146 * Filters the wp shortlink for a post - returns the shortened canonical URL.
2147 *
2148 * The wp_shortlink_wp_head() function calls wp_get_shortlink( 0, 'query' );
2149 */
2150 public function get_canonical_shortlink( $shortlink = false, $post_id = 0, $context = 'post', $allow_slugs = true ) {
2151
2152 if ( $this->p->debug->enabled ) {
2153
2154 $this->p->debug->log_args( array(
2155 'shortlink' => $shortlink,
2156 'post_id' => $post_id,
2157 'context' => $context,
2158 'allow_slugs' => $allow_slugs,
2159 ) );
2160 }
2161
2162 self::$saved_shortlink_url = null; // Just in case.
2163
2164 if ( isset( self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ] ) ) {
2165
2166 if ( $this->p->debug->enabled ) {
2167
2168 $this->p->debug->log( 'returning shortlink from static cache = ' . self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ] );
2169 }
2170
2171 return self::$saved_shortlink_url = self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ];
2172 }
2173
2174 /*
2175 * Check to make sure we have a plugin shortener selected.
2176 */
2177 if ( empty( $this->p->options[ 'plugin_shortener' ] ) || $this->p->options[ 'plugin_shortener' ] === 'none' ) {
2178
2179 if ( $this->p->debug->enabled ) {
2180
2181 $this->p->debug->log( 'exiting early: no shortening service defined' );
2182 }
2183
2184 return $shortlink; // Return original shortlink.
2185 }
2186
2187 /*
2188 * The WordPress link-template.php functions call wp_get_shortlink() with a post id of 0.
2189 *
2190 * Use the same WordPress code to get a real post id and create a default shortlink (if required).
2191 */
2192 if ( 0 === $post_id ) {
2193
2194 if ( $this->p->debug->enabled ) {
2195
2196 $this->p->debug->log( 'provided post id is 0 (current post)' );
2197 }
2198
2199 if ( 'query' === $context && is_singular() ) { // wp_get_shortlink() uses the same logic.
2200
2201 $post_id = get_queried_object_id();
2202
2203 if ( $this->p->debug->enabled ) {
2204
2205 $this->p->debug->log( 'setting post id ' . $post_id . ' from queried object' );
2206 }
2207
2208 } elseif ( 'post' === $context ) {
2209
2210 $post_obj = get_post();
2211
2212 if ( empty( $post_obj->ID ) ) {
2213
2214 if ( $this->p->debug->enabled ) {
2215
2216 $this->p->debug->log( 'exiting early: post object ID is empty' );
2217 }
2218
2219 return $shortlink; // Return original shortlink.
2220
2221 } else {
2222
2223 $post_id = $post_obj->ID;
2224
2225 if ( $this->p->debug->enabled ) {
2226
2227 $this->p->debug->log( 'setting post id ' . $post_id . ' from post object' );
2228 }
2229 }
2230 }
2231
2232 if ( empty( $post_id ) ) {
2233
2234 if ( $this->p->debug->enabled ) {
2235
2236 $this->p->debug->log( 'exiting early: unable to determine the post id' );
2237 }
2238
2239 return $shortlink; // Return original shortlink.
2240 }
2241
2242 if ( empty( $shortlink ) ) {
2243
2244 if ( 'page' === get_post_type( $post_id ) &&
2245 (int) $post_id === (int) get_option( 'page_on_front' ) &&
2246 'page' === get_option( 'show_on_front' ) ) {
2247
2248 $shortlink = home_url( '/' );
2249
2250 } else $shortlink = home_url( '?p=' . $post_id );
2251 }
2252
2253 /*
2254 * WpssoPost->post_can_have_meta() returns false:
2255 *
2256 * If the $post argument is not numeric or an instance of WP_Post.
2257 * If the post ID is empty.
2258 * If the post type is empty.
2259 * If the post status is empty.
2260 * If the post type is 'revision'.
2261 * If the post status is 'trash'.
2262 */
2263 } elseif ( ! $this->post_can_have_meta( $post_id ) ) {
2264
2265 if ( $this->p->debug->enabled ) {
2266
2267 $this->p->debug->log( 'exiting early: post id ' . $post_id . ' invalid for metadata' );
2268 }
2269
2270 return $shortlink; // Return original shortlink.
2271 }
2272
2273 $mod = $this->get_mod( $post_id );
2274
2275 if ( 'auto-draft' === $mod[ 'post_status' ] ) {
2276
2277 if ( $this->p->debug->enabled ) {
2278
2279 $this->p->debug->log( 'exiting early: post status is ' . $mod[ 'post_status' ] );
2280 }
2281
2282 return $shortlink; // Return original shortlink.
2283 }
2284
2285 /*
2286 * Shorten URL using the selected shortening service.
2287 */
2288 $canonical_url = $this->p->util->get_canonical_url( $mod );
2289 $short_url = $this->p->util->shorten_url( $canonical_url, $mod );
2290
2291 if ( $short_url === $canonical_url ) {
2292
2293 if ( $this->p->debug->enabled ) {
2294
2295 $this->p->debug->log( 'exiting early: shortened URL same as canonical URL' );
2296 }
2297
2298 return $shortlink; // Return original shortlink.
2299 }
2300
2301 if ( $this->p->debug->enabled ) {
2302
2303 $this->p->debug->log( 'returning shortlink = ' . $short_url );
2304 }
2305
2306 return self::$saved_shortlink_url = self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ] = $short_url;
2307 }
2308
2309 public function maybe_restore_shortlink( $shortlink = false, $post_id = 0, $context = 'post', $allow_slugs = true ) {
2310
2311 if ( self::$saved_shortlink_url === $shortlink ) { // Shortlink value has not changed.
2312
2313 self::$saved_shortlink_url = null; // Just in case.
2314
2315 if ( $this->p->debug->enabled ) {
2316
2317 $this->p->debug->log( 'exiting early: shortlink value has not changed' );
2318 }
2319
2320 return $shortlink;
2321 }
2322
2323 self::$saved_shortlink_url = null; // Just in case.
2324
2325 if ( isset( self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ] ) ) {
2326
2327 if ( $this->p->debug->enabled ) {
2328
2329 $this->p->debug->log( 'restoring shortlink ' . $shortlink . ' to ' .
2330 self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ] );
2331 }
2332
2333 return self::$cache_shortlinks[ $post_id ][ $context ][ $allow_slugs ];
2334 }
2335
2336 return $shortlink;
2337 }
2338
2339 /*
2340 * Maybe inherit a featured image ID from the post/page parent, if the 'plugin_inherit_featured' option is enabled.
2341 *
2342 * See get_metadata_raw() in wordpress/wp-includes/meta.php.
2343 * See metadata_exists() in wordpress/wp-includes/meta.php.
2344 */
2345 public function get_post_metadata_thumbnail_id( $check, $post_id, $meta_key, $single ) {
2346
2347 if ( '_thumbnail_id' !== $meta_key ) { // Inherit only the featured image (aka '_thumbnail_id').
2348
2349 return $check; // Null by default.
2350 }
2351
2352 if ( $this->p->debug->enabled ) {
2353
2354 $this->p->debug->log( 'required call to WpssoPost->get_mod() for post ID ' . $post_id );
2355 }
2356
2357 $mod = $this->get_mod( $post_id ); // Uses a local cache.
2358
2359 if ( $mod[ 'is_attachment' ] ) { // Attachments do not inherit metadata.
2360
2361 if ( $this->p->debug->enabled ) {
2362
2363 $this->p->debug->log( 'exiting early: attachments do not inherit metadata' );
2364 }
2365
2366 return $check; // Null by default.
2367 }
2368
2369 $inherit_featured = empty( $this->p->options[ 'plugin_inherit_featured' ] ) ? false : true;
2370
2371 $inherit_featured = (bool) apply_filters( 'wpsso_inherit_featured_image', $inherit_featured, $mod );
2372
2373 if ( $inherit_featured ) {
2374
2375 if ( $this->p->debug->enabled ) {
2376
2377 $this->p->debug->log( 'inherit featured image is enabled' );
2378 }
2379
2380 } else {
2381
2382 if ( $this->p->debug->enabled ) {
2383
2384 $this->p->debug->log( 'exiting early: inherit featured image is disabled' );
2385 }
2386
2387 return $check; // Null by default.
2388 }
2389
2390 $metadata = $this->get_update_meta_cache( $post_id );
2391
2392 /*
2393 * If the meta key already has a value, then no need to check the parents.
2394 */
2395 if ( ! empty( $metadata[ $meta_key ][ 0 ] ) ) {
2396
2397 if ( $this->p->debug->enabled ) {
2398
2399 $this->p->debug->log( 'exiting early: featured image = ' . $metadata[ $meta_key ][ 0 ] );
2400 }
2401
2402 return $check; // Null by default.
2403 }
2404
2405 /*
2406 * Make sure the post type is not false, empty string, or array.
2407 */
2408 if ( empty( $mod[ 'post_type' ] ) || ! is_string( $mod[ 'post_type' ] ) ) {
2409
2410 if ( $this->p->debug->enabled ) {
2411
2412 $this->p->debug->log( 'exiting early: invalid post type' );
2413 }
2414
2415 return $check; // Null by default.
2416 }
2417
2418 /*
2419 * Start with the parent and work our way up - return the first value found.
2420 */
2421 if ( $this->p->debug->enabled ) {
2422
2423 $this->p->debug->log( 'getting ancestors for post type = ' . $mod[ 'post_type' ] );
2424 }
2425
2426 $ancestor_ids = get_ancestors( $mod[ 'id' ], $mod[ 'post_type' ], $resource_type = 'post_type' );
2427
2428 if ( empty( $ancestor_ids ) ) {
2429
2430 if ( $this->p->debug->enabled ) {
2431
2432 $this->p->debug->log( 'no ancestors for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
2433 }
2434
2435 } elseif ( is_array( $ancestor_ids ) ) { // Just in case.
2436
2437 foreach ( $ancestor_ids as $parent_id ) {
2438
2439 /*
2440 * Get the parent's metadata array.
2441 */
2442 if ( $this->p->debug->enabled ) {
2443
2444 $this->p->debug->log( 'getting metadata for parent id ' . $parent_id );
2445 }
2446
2447 $metadata = $this->get_update_meta_cache( $parent_id );
2448
2449 if ( ! empty( $metadata[ $meta_key ][ 0 ] ) ) { // Parent has a meta key value.
2450
2451 if ( $this->p->debug->enabled ) {
2452
2453 $this->p->debug->log( 'found parent ID ' . $parent_id . ' featured image = ' . $metadata[ $meta_key ][ 0 ] );
2454 }
2455
2456 /*
2457 * Return the parent's metadata single value or its array.
2458 */
2459 if ( $single ) {
2460
2461 return maybe_unserialize( $metadata[ $meta_key ][ 0 ] );
2462 }
2463
2464 return array_map( 'maybe_unserialize', $metadata[ $meta_key ] );
2465 }
2466 }
2467 }
2468
2469 return $check; // Null by default.
2470 }
2471
2472 /*
2473 * Maybe inherit a featured image ID from the post/page parent, if the 'plugin_inherit_featured' option is enabled,
2474 * and ignore saving the same featured image ID.
2475 *
2476 * See update_metadata() in wordpress/wp-includes/meta.php.
2477 */
2478 public function update_post_metadata_thumbnail_id( $check, $post_id, $meta_key, $meta_value, $prev_value ) {
2479
2480 if ( '_thumbnail_id' !== $meta_key ) { // Inherit only the featured image (aka '_thumbnail_id').
2481
2482 return $check; // Null by default.
2483 }
2484
2485 if ( $this->p->debug->enabled ) {
2486
2487 $this->p->debug->log( 'required call to WpssoPost->get_mod() for post ID ' . $post_id );
2488 }
2489
2490 $mod = $this->get_mod( $post_id ); // Uses a local cache.
2491
2492 if ( $mod[ 'is_attachment' ] ) { // Attachments do not inherit metadata.
2493
2494 if ( $this->p->debug->enabled ) {
2495
2496 $this->p->debug->log( 'exiting early: attachments do not inherit metadata' );
2497 }
2498
2499 return $check; // Null by default.
2500 }
2501
2502 $inherit_featured = empty( $this->p->options[ 'plugin_inherit_featured' ] ) ? false : true;
2503
2504 $inherit_featured = (bool) apply_filters( 'wpsso_inherit_featured_image', $inherit_featured, $mod );
2505
2506 if ( $inherit_featured ) {
2507
2508 if ( $this->p->debug->enabled ) {
2509
2510 $this->p->debug->log( 'inherit featured image is enabled' );
2511 }
2512
2513 } else {
2514
2515 if ( $this->p->debug->enabled ) {
2516
2517 $this->p->debug->log( 'exiting early: inherit featured image is disabled' );
2518 }
2519
2520 return $check; // Null by default.
2521 }
2522
2523 if ( '' === $prev_value ) { // No existing previous value.
2524
2525 /*
2526 * Make sure the post type is not false, empty string, or array.
2527 */
2528 if ( empty( $mod[ 'post_type' ] ) || ! is_string( $mod[ 'post_type' ] ) ) {
2529
2530 if ( $this->p->debug->enabled ) {
2531
2532 $this->p->debug->log( 'exiting early: invalid post type' );
2533 }
2534
2535 return $check; // Null by default.
2536 }
2537
2538 if ( $this->p->debug->enabled ) {
2539
2540 $this->p->debug->log( 'getting ancestors for post type = ' . $mod[ 'post_type' ] );
2541 }
2542
2543 $ancestor_ids = get_ancestors( $mod[ 'id' ], $mod[ 'post_type' ], $resource_type = 'post_type' );
2544
2545 if ( empty( $ancestor_ids ) ) {
2546
2547 if ( $this->p->debug->enabled ) {
2548
2549 $this->p->debug->log( 'no ancestors for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
2550 }
2551
2552 } elseif ( is_array( $ancestor_ids ) ) { // Just in case.
2553
2554 foreach ( $ancestor_ids as $parent_id ) {
2555
2556 /*
2557 * Get the parent's metadata array.
2558 */
2559 if ( $this->p->debug->enabled ) {
2560
2561 $this->p->debug->log( 'getting metadata for parent id ' . $parent_id );
2562 }
2563
2564 $metadata = $this->get_update_meta_cache( $parent_id );
2565
2566 if ( ! empty( $metadata[ $meta_key ][ 0 ] ) ) { // Parent has a meta key value.
2567
2568 $parent_value = maybe_unserialize( $metadata[ $meta_key ][ 0 ] );
2569
2570 if ( $meta_value == $parent_value ) { // Allow integer to numeric string comparison.
2571
2572 return false; // Do not save the meta key value.
2573 }
2574 }
2575 }
2576 }
2577 }
2578
2579 if ( $this->p->debug->enabled ) {
2580
2581 $this->p->debug->log_arr( 'check', $check );
2582 }
2583
2584 return $check; // Null by default.
2585 }
2586
2587 /*
2588 * Returns a custom or default term ID, or false if a term for the $tax_slug is not found.
2589 */
2590 public function get_primary_term_id( array $mod, $tax_slug = 'category' ) {
2591
2592 if ( $this->p->debug->enabled ) {
2593
2594 $this->p->debug->mark();
2595 }
2596
2597 $primary_term_id = false;
2598
2599 if ( $mod[ 'is_post' ] ) { // Just in case.
2600
2601 static $local_fifo = array();
2602
2603 $post_id = $mod[ 'id' ];
2604
2605 if ( isset( $local_fifo[ $post_id ][ $tax_slug ] ) ) {
2606
2607 return $local_fifo[ $post_id ][ $tax_slug ]; // Return value from local cache.
2608 }
2609
2610 /*
2611 * Maybe limit the number of array elements.
2612 */
2613 $local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );
2614
2615 /*
2616 * Returns null if a custom primary term ID has not been selected.
2617 */
2618 $primary_term_id = $this->get_options( $post_id, $md_key = 'primary_term_id' );
2619
2620 /*
2621 * Make sure the term is not null or false, and still exists.
2622 *
2623 * Note that term_exists() requires an integer ID, not a string ID, so cast as integer.
2624 */
2625 $is_custom = $primary_term_id && term_exists( (int) $primary_term_id ) ? true : false;
2626
2627 $filter_name = 'wpsso_primary_term_id_is_custom';
2628
2629 if ( $this->p->debug->enabled ) {
2630
2631 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
2632 }
2633
2634 $is_custom = apply_filters( $filter_name, $is_custom, $mod, $tax_slug, $primary_term_id );
2635
2636 if ( $is_custom ) {
2637
2638 $filter_name = 'wpsso_primary_term_id';
2639
2640 if ( $this->p->debug->enabled ) {
2641
2642 $this->p->debug->log( 'applying filters "' . $filter_name . '"' );
2643 }
2644
2645 $primary_term_id = apply_filters( $filter_name, $primary_term_id, $mod, $tax_slug, $is_custom );
2646
2647 } else {
2648
2649 /*
2650 * WpssoPost->get_default_term_id() applies the 'wpsso_primary_term_id' filter with $is_custom = false.
2651 */
2652 if ( $this->p->debug->enabled ) {
2653
2654 $this->p->debug->log( 'calling WpssoPost->get_default_term_id()' );
2655 }
2656
2657 $primary_term_id = $this->get_default_term_id( $mod, $tax_slug );
2658 }
2659
2660 $local_fifo[ $post_id ][ $tax_slug ] = empty( $primary_term_id ) ? false : (int) $primary_term_id;
2661 }
2662
2663 return $primary_term_id;
2664 }
2665
2666 /*
2667 * Returns the first taxonomy term ID, , or false if a term for the $tax_slug is not found.
2668 */
2669 public function get_default_term_id( array $mod, $tax_slug = 'category' ) {
2670
2671 if ( $this->p->debug->enabled ) {
2672
2673 $this->p->debug->mark();
2674 }
2675
2676 $default_term_id = false;
2677
2678 if ( $mod[ 'is_post' ] ) { // Just in case.
2679
2680 $post_terms = wp_get_post_terms( $mod[ 'id' ], $tax_slug, $args = array( 'number' => 1 ) );
2681
2682 if ( ! empty( $post_terms ) && is_array( $post_terms ) ) { // Have one or more terms and taxonomy exists.
2683
2684 foreach ( $post_terms as $term_obj ) {
2685
2686 $default_term_id = (int) $term_obj->term_id; // Use the first term ID found.
2687
2688 break;
2689 }
2690 }
2691
2692 /*
2693 * Apply the 'wpsso_primary_term_id' filter to get the Yoast SEO primary category (for example) and
2694 * use it as the default value.
2695 */
2696 $filter_name = 'wpsso_primary_term_id';
2697
2698 if ( $this->p->debug->enabled ) {
2699
2700 $this->p->debug->log( 'applying filters "' . $filter_name . '" with $is_custom = false' );
2701 }
2702
2703 $default_term_id = apply_filters( $filter_name, $default_term_id, $mod, $tax_slug, $is_custom = false );
2704 }
2705
2706 return $default_term_id;
2707 }
2708
2709 /*
2710 * Returns an associative array of term IDs and their names or objects.
2711 *
2712 * If the custom primary or default term ID exists in the post terms array, it will be moved to the top.
2713 */
2714 public function get_primary_terms( array $mod, $tax_slug = 'category', $output = 'objects' ) {
2715
2716 $primary_terms = array();
2717
2718 if ( $mod[ 'is_post' ] ) { // Just in case.
2719
2720 $post_id = $mod[ 'id' ];
2721
2722 /*
2723 * Returns a custom or default term ID, or false if a term for the $tax_slug is not found.
2724 */
2725 if ( $this->p->debug->enabled ) {
2726
2727 $this->p->debug->log( 'calling WpssoPost->get_primary_term_id()' );
2728 }
2729
2730 $primary_term_id = $this->p->post->get_primary_term_id( $mod, $tax_slug ); // Returns false or term ID.
2731
2732 if ( $primary_term_id ) {
2733
2734 $primary_term_obj = get_term_by( 'id', $primary_term_id, $tax_slug, OBJECT, 'raw' );
2735
2736 if ( $primary_term_obj ) {
2737
2738 $post_terms = wp_get_post_terms( $post_id, $tax_slug );
2739
2740 if ( ! empty( $post_terms ) && is_array( $post_terms ) ) { // Have one or more terms and taxonomy exists.
2741
2742 /*
2743 * If the primary or default term ID exists in the post terms array, move it to the top.
2744 */
2745 foreach ( $post_terms as $num => $term_obj ) {
2746
2747 if ( $primary_term_obj->term_id === $term_obj->term_id ) {
2748
2749 unset( $post_terms[ $num ] );
2750
2751 $post_terms = array_merge( array( $primary_term_obj ), $post_terms );
2752
2753 break; // No need to continue.
2754 }
2755 }
2756
2757 } else {
2758
2759 $post_terms = array( $primary_term_obj );
2760 }
2761
2762 foreach ( $post_terms as $term_obj ) {
2763
2764 switch ( $output ) {
2765
2766 case 'ids':
2767 case 'term_ids':
2768
2769 $primary_terms[ $term_obj->term_id ] = (int) $term_obj->term_id;
2770
2771 break;
2772
2773 case 'names':
2774
2775 $primary_terms[ $term_obj->term_id ] = (string) $term_obj->name;
2776
2777 break;
2778
2779 case 'objects':
2780
2781 $primary_terms[ $term_obj->term_id ] = $term_obj;
2782
2783 break;
2784 }
2785 }
2786 }
2787 }
2788 }
2789
2790 return apply_filters( 'wpsso_primary_terms', $primary_terms, $mod, $tax_slug, $output );
2791 }
2792
2793 /*
2794 * Since WPSSO Core v15.15.1.
2795 *
2796 * WpssoPost->post_can_have_meta() returns false:
2797 *
2798 * If the $post argument is not numeric or an instance of WP_Post.
2799 * If the post ID is empty.
2800 * If the post type is empty.
2801 * If the post status is empty.
2802 * If the post type is 'revision'.
2803 * If the post status is 'trash'.
2804 */
2805 public function post_can_have_meta( $post ) {
2806
2807 if ( $post instanceof WP_Post ) {
2808
2809 $post_id = isset( $post->ID ) ? $post->ID : 0;
2810 $post_type = isset( $post->post_type ) ? $post->post_type : '';
2811 $post_status = isset( $post->post_status ) ? $post->post_status : '';
2812
2813 } elseif ( is_numeric( $post ) && $post > 0 ) {
2814
2815 $post_id = $post;
2816 $post_type = get_post_type( $post_id );
2817 $post_status = get_post_status( $post_id );
2818
2819 } else {
2820
2821 if ( $this->p->debug->enabled ) {
2822
2823 $this->p->debug->log( 'exiting early: post argument is not a valid number or an instance of WP_Post' );
2824 }
2825
2826 return false;
2827 }
2828
2829 if ( empty( $post_id ) ) {
2830
2831 if ( $this->p->debug->enabled ) {
2832
2833 $this->p->debug->log( 'exiting early: post id is empty' );
2834 }
2835
2836 return false;
2837
2838 } elseif ( empty( $post_type ) ) {
2839
2840 if ( $this->p->debug->enabled ) {
2841
2842 $this->p->debug->log( 'exiting early: post type is empty' );
2843 }
2844
2845 return false;
2846
2847 } elseif ( empty( $post_status ) ) {
2848
2849 if ( $this->p->debug->enabled ) {
2850
2851 $this->p->debug->log( 'exiting early: post status is empty' );
2852 }
2853
2854 return false;
2855 }
2856
2857 if ( $this->p->debug->enabled ) {
2858
2859 $this->p->debug->log( 'post id ' . $post_id . ' post type is ' . $post_type );
2860 $this->p->debug->log( 'post id ' . $post_id . ' post status is ' . $post_status );
2861 }
2862
2863 switch ( $post_type ) {
2864
2865 case 'revision':
2866
2867 return false;
2868 }
2869
2870 switch ( $post_status ) {
2871
2872 case 'trash':
2873
2874 return false;
2875 }
2876
2877 return true;
2878 }
2879
2880 /*
2881 * If $meta_key is en empty string, retrieves all metadata for the specified object ID.
2882 *
2883 * Use get_metadata() instead of get_post_meta() as the WordPress get_post_meta() function does not check
2884 * wp_is_post_revision(), so it retrieves the revision metadata instead of the post metadata.
2885 *
2886 * See https://developer.wordpress.org/reference/functions/get_metadata/.
2887 */
2888 public static function get_meta( $post_id, $meta_key = '', $single = false ) {
2889
2890 return get_metadata( 'post', $post_id, $meta_key, $single );
2891 }
2892
2893 public static function update_meta( $post_id, $meta_key, $value ) {
2894
2895 $wpsso =& Wpsso::get_instance();
2896
2897 if ( $wpsso->debug->enabled ) {
2898
2899 $wpsso->debug->log( 'calling update_metadata() for post id ' . $post_id . ' meta key ' . $meta_key . ' value = ' . print_r( $value, true ) );
2900 }
2901
2902 return update_metadata( 'post', $post_id, $meta_key, $value );
2903 }
2904
2905 public static function delete_meta( $post_id, $meta_key ) {
2906
2907 return delete_metadata( 'post', $post_id, $meta_key );
2908 }
2909 }
2910 }
2911