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
← All changes | lib/post.php +2910 -0 22.5.222.7.0 View file →
@@ -1,0 +1,2910 @@
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 +}