PluginProbe
Parse.ly / 3.6.0
Parse.ly v3.6.0
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / src / Metadata / class-post-builder.php

class-post-builder.php in Parse.ly 3.6.0, at src/Metadata/class-post-builder.php

572 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post Page Metadata Builder class
4 *
5 * @package Parsely
6 * @since 3.4.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely\Metadata;
12
13 use Parsely\Parsely;
14 use WP_Post;
15 use WP_User;
16
17 /**
18 * Implements abstract Metadata Builder class to generate the metadata array
19 * for a post page.
20 *
21 * @since 3.4.0
22 */
23 class Post_Builder extends Metadata_Builder {
24 /**
25 * Post object to generate the metadata for.
26 *
27 * @var WP_Post
28 */
29 private $post;
30
31 /**
32 * Constructor.
33 *
34 * @param Parsely $parsely Instance of Parsely class.
35 * @param WP_Post $post Post object to generate the metadata for.
36 */
37 public function __construct( Parsely $parsely, WP_Post $post ) {
38 parent::__construct( $parsely );
39 $this->post = $post;
40 }
41
42 /**
43 * Generates the metadata object by calling the build_* methods and
44 * returns the value.
45 *
46 * @since 3.4.0
47 *
48 * @return array<string, mixed>
49 */
50 public function get_metadata(): array {
51 $this->build_basic();
52 $this->build_headline();
53 $this->build_url();
54
55 $this->build_type();
56 $this->build_main_entity();
57 $this->build_thumbnail_url();
58 $this->build_image();
59 $this->build_article_section();
60 $this->build_author();
61 $this->build_publisher();
62 $this->build_keywords();
63 $this->build_metadata_post_times();
64
65 return $this->metadata;
66 }
67
68 /**
69 * Populates the `headline` field in the metadata object.
70 *
71 * @since 3.4.0
72 */
73 private function build_headline(): void {
74 $this->metadata['headline'] = $this->clean_value( get_the_title( $this->post ) );
75 }
76
77 /**
78 * Populates the `url` field in the metadata object by getting the current page's URL.
79 *
80 * @since 3.4.0
81 */
82 protected function build_url(): void {
83 $this->metadata['url'] = $this->get_current_url( 'post', $this->post->ID );
84 }
85
86 /**
87 * Populates the `@type` field in the metadata object.
88 *
89 * @since 3.4.0
90 */
91 private function build_type(): void {
92 /**
93 * Filters the JSON-LD @type.
94 *
95 * @param array $jsonld_type JSON-LD @type value, default is NewsArticle.
96 * @param int $id Post ID.
97 * @param string $post_type The Post type in WordPress.
98 *
99 * @since 2.5.0
100 */
101 $type = (string) apply_filters( 'wp_parsely_post_type', 'NewsArticle', $this->post->ID, $this->post->post_type );
102
103 // TODO: Merge only once, not every execution.
104 $supported_types = array_merge( Parsely::SUPPORTED_JSONLD_POST_TYPES, Parsely::SUPPORTED_JSONLD_NON_POST_TYPES );
105
106 // Validate type before passing it further as an invalid type will not be recognized by Parse.ly.
107 if ( ! in_array( $type, $supported_types, true ) ) {
108 $error = sprintf(
109 /* translators: 1: JSON @type like NewsArticle, 2: URL */
110 __( '@type %1$s is not supported by Parse.ly. Please use a type mentioned in %2$s', 'wp-parsely' ),
111 $type,
112 'https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages'
113 );
114 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
115 trigger_error( esc_html( $error ), E_USER_WARNING );
116 $type = 'NewsArticle';
117 }
118
119 $this->metadata['@type'] = $type;
120 }
121
122 /**
123 * Populates the `mainEntityOfPage` field in the metadata object.
124 *
125 * @since 3.4.0
126 */
127 private function build_main_entity(): void {
128 $this->metadata['mainEntityOfPage'] = array(
129 '@type' => 'WebPage',
130 '@id' => $this->get_current_url( 'post' ),
131 );
132 }
133
134 /**
135 * Populates the `thumbnailUrl` field in the metadata object.
136 *
137 * @since 3.4.0
138 */
139 private function build_thumbnail_url(): void {
140 $thumb_url = get_the_post_thumbnail_url( $this->post, 'thumbnail' );
141 if ( ! is_string( $thumb_url ) ) {
142 $thumb_url = '';
143 }
144 $this->metadata['thumbnailUrl'] = $thumb_url;
145 }
146
147 /**
148 * Populates the `image` field in the metadata object.
149 *
150 * @since 3.4.0
151 */
152 private function build_image(): void {
153 $image_url = get_the_post_thumbnail_url( $this->post, 'full' );
154 if ( ! is_string( $image_url ) ) {
155 $image_url = '';
156 }
157 $this->metadata['image'] = array(
158 '@type' => 'ImageObject',
159 'url' => $image_url,
160 );
161 }
162
163 /**
164 * Populates the `articleSection` field in the metadata object.
165 *
166 * @since 3.4.0
167 */
168 private function build_article_section(): void {
169 $this->metadata['articleSection'] = $this->get_category_name( $this->post, $this->parsely->get_options() );
170 }
171
172 /**
173 * Populates the `author` and `creator` fields in the metadata object.
174 *
175 * @since 3.4.0
176 */
177 private function build_author(): void {
178 $authors = $this->get_author_names( $this->post );
179 $author_objects = array();
180 foreach ( $authors as $author ) {
181 $author_tag = array(
182 '@type' => 'Person',
183 'name' => $author,
184 );
185 $author_objects[] = $author_tag;
186 }
187 $this->metadata['author'] = $author_objects;
188 $this->metadata['creator'] = $authors;
189 }
190
191 /**
192 * Populates the `publisher` field in the metadata object.
193 *
194 * @since 3.4.0
195 */
196 private function build_publisher(): void {
197 $this->metadata['publisher'] = array(
198 '@type' => 'Organization',
199 'name' => get_bloginfo( 'name' ),
200 'logo' => $this->parsely->get_options()['logo'],
201 );
202 }
203
204 /**
205 * Populates the `keywords` field in the metadata object.
206 *
207 * @since 3.4.0
208 */
209 private function build_keywords(): void {
210 $options = $this->parsely->get_options();
211 $tags = $this->get_tags( $this->post->ID );
212 if ( $options['cats_as_tags'] ) {
213 $tags = array_merge( $tags, $this->get_categories( $this->post->ID ) );
214 // add custom taxonomy values.
215 $tags = array_merge( $tags, $this->get_custom_taxonomy_values( $this->post ) );
216 }
217 // The function 'mb_strtolower' is not enabled by default in php, so this check
218 // falls back to the native php function 'strtolower' if necessary.
219 if ( function_exists( 'mb_strtolower' ) ) {
220 $lowercase_callback = 'mb_strtolower';
221 } else {
222 $lowercase_callback = 'strtolower';
223 }
224 if ( $options['lowercase_tags'] ) {
225 $tags = array_map( $lowercase_callback, $tags );
226 }
227
228 /**
229 * Filters the post tags that are used as metadata keywords.
230 *
231 * @param array<string> $tags Post tags.
232 * @param int $ID Post ID.
233 *
234 * @since 1.8.0
235 */
236 $tags = apply_filters( 'wp_parsely_post_tags', $tags, $this->post->ID );
237 $tags = array_map( array( $this, 'clean_value' ), $tags );
238
239 $this->metadata['keywords'] = array_values( array_unique( $tags ) );
240 }
241
242 /**
243 * Sets all metadata values related to post time.
244 *
245 * @since 3.0.2
246 * @since 3.3.0 Moved to class-metadata
247 */
248 private function build_metadata_post_times(): void {
249 $date_format = 'Y-m-d\TH:i:s\Z';
250 $post_created_gmt = get_post_time( $date_format, true, $this->post );
251
252 if ( false === $post_created_gmt ) {
253 return;
254 }
255
256 $this->metadata['dateCreated'] = $post_created_gmt;
257 $this->metadata['datePublished'] = $post_created_gmt;
258 $this->metadata['dateModified'] = $post_created_gmt;
259
260 $post_modified_gmt = get_post_modified_time( $date_format, true, $this->post );
261
262 if ( false !== $post_modified_gmt && $post_modified_gmt > $post_created_gmt ) {
263 $this->metadata['dateModified'] = $post_modified_gmt;
264 }
265 }
266
267 /**
268 * Returns a properly cleaned category/taxonomy value and will optionally
269 * use the top-level category/taxonomy value, if so instructed via the
270 * `use_top_level_cats` option.
271 *
272 * @since 3.3.0 Moved to class-metadata
273 *
274 * @param WP_Post $post_obj The object for the post.
275 * @param array<string, mixed> $parsely_options The parsely options.
276 * @return string Cleaned category name for the post in question.
277 */
278 private function get_category_name( WP_Post $post_obj, array $parsely_options ): string {
279 $taxonomy_dropdown_choice = get_the_terms( $post_obj->ID, $parsely_options['custom_taxonomy_section'] );
280 // Get top-level taxonomy name for chosen taxonomy and assign to $parent_name; it will be used
281 // as the category value if 'use_top_level_cats' option is checked.
282 // Assign as the default category name if no value is checked for the chosen taxonomy.
283 $category_name = get_cat_name( get_option( 'default_category' ) );
284 if ( ! empty( $taxonomy_dropdown_choice ) && ! is_wp_error( $taxonomy_dropdown_choice ) ) {
285 if ( $parsely_options['use_top_level_cats'] ) {
286 $first_term = array_shift( $taxonomy_dropdown_choice );
287 $term_name = $this->get_top_level_term( $first_term->term_id, $first_term->taxonomy );
288 } else {
289 $term_name = $this->get_bottom_level_term( $post_obj->ID, $parsely_options['custom_taxonomy_section'] );
290 }
291
292 if ( is_string( $term_name ) && 0 < strlen( $term_name ) ) {
293 $category_name = $term_name;
294 }
295 }
296
297 /**
298 * Filters the constructed category name.
299 *
300 * @since 1.8.0
301 *
302 * @param string $category Category name.
303 * @param WP_Post $post_obj Post object.
304 * @param array<string, mixed> $parsely_options The Parsely options.
305 */
306 $category_name = apply_filters( 'wp_parsely_post_category', $category_name, $post_obj, $parsely_options );
307
308 return $this->clean_value( $category_name );
309 }
310
311 /**
312 * Returns the top-most category/taxonomy value in a hierarchy given a
313 * taxonomy value's ID.
314 *
315 * (WordPress calls taxonomy values 'terms').
316 *
317 * @since 3.3.0 Moved to class-metadata
318 *
319 * @param int $term_id The ID of the top level term.
320 * @param string $taxonomy_name The name of the taxonomy.
321 * @return string|false $parent The top level name of the category / taxonomy.
322 */
323 private function get_top_level_term( int $term_id, string $taxonomy_name ) {
324 $parent = get_term_by( 'id', $term_id, $taxonomy_name );
325 while ( false !== $parent && 0 !== $parent->parent ) {
326 $parent = get_term_by( 'id', $parent->parent, $taxonomy_name );
327 }
328 return $parent ? $parent->name : false;
329 }
330
331 /**
332 * Returns the bottom-most category/taxonomy value in a hierarchy given a
333 * post ID.
334 *
335 * (WordPress calls taxonomy values 'terms').
336 *
337 * @since 3.3.0 Moved to class-metadata
338 *
339 * @param int $post_id The post id you're interested in.
340 * @param string $taxonomy_name The name of the taxonomy.
341 * @return string Name of the custom taxonomy.
342 */
343 private function get_bottom_level_term( int $post_id, string $taxonomy_name ): string {
344 $terms = get_the_terms( $post_id, $taxonomy_name );
345
346 if ( ! is_array( $terms ) ) {
347 return '';
348 }
349
350 $term_ids = wp_list_pluck( $terms, 'term_id' );
351 $parents = array_filter( wp_list_pluck( $terms, 'parent' ) );
352
353 // Get array of IDs of terms which are not parents.
354 $term_ids_not_parents = array_diff( $term_ids, $parents );
355 // Get corresponding term objects, which are mapped to array index keys.
356 $terms_not_parents = array_intersect_key( $terms, $term_ids_not_parents );
357 // remove array index keys.
358 $terms_not_parents_cleaned = array_values( $terms_not_parents );
359
360 if ( ! empty( $terms_not_parents_cleaned ) ) {
361 // If you assign multiple child terms in a custom taxonomy, will only return the first.
362 return $terms_not_parents_cleaned[0]->name ?? '';
363 }
364
365 return '';
366 }
367
368 /**
369 * Retrieves all the authors for a post as an array. Can include multiple
370 * authors if the Co-Authors Plus plugin is in use.
371 *
372 * @since 3.3.0 Moved to class-metadata
373 *
374 * @param WP_Post $post The post object.
375 * @return array<string>
376 */
377 private function get_author_names( WP_Post $post ): array {
378 $authors = $this->get_coauthor_names( $post->ID );
379 if ( 0 === count( $authors ) ) {
380 $post_author = get_user_by( 'id', $post->post_author );
381 if ( false !== $post_author ) {
382 $authors = array( $post_author );
383 }
384 }
385
386 /**
387 * Filters the list of author WP_User objects for a post.
388 *
389 * @since 1.14.0
390 *
391 * @param array<WP_User> $authors One or more authors as WP_User objects.
392 * @param WP_Post $post Post object.
393 */
394 $authors = apply_filters( 'wp_parsely_pre_authors', $authors, $post );
395
396 // Getting the author name for each author.
397 $authors = array_map( array( $this, 'get_author_name' ), $authors );
398
399 /**
400 * Filters the list of author names for a post.
401 *
402 * @since 1.14.0
403 *
404 * @param array<string> $authors One or more author names.
405 * @param WP_Post $post Post object.
406 */
407 $authors = apply_filters( 'wp_parsely_post_authors', $authors, $post );
408
409 return array_map( array( $this, 'clean_value' ), $authors );
410 }
411
412 /**
413 * Returns a list of coauthors for a post assuming the Co-Authors Plus plugin
414 * is installed.
415 *
416 * Borrowed from
417 * https://github.com/Automattic/Co-Authors-Plus/blob/master/template-tags.php#L3-35
418 *
419 * @since 3.3.0 Moved to class-metadata
420 *
421 * @param int $post_id The ID of the post.
422 * @return array<WP_User> List of coauthors, or an empty array if the Co-Authors Plus plugin is not active.
423 */
424 private function get_coauthor_names( int $post_id ): array {
425 $coauthors = array();
426 if ( class_exists( 'coauthors_plus' ) ) {
427 global $post, $post_ID, $coauthors_plus;
428
429 if ( ! $post_id && $post_ID ) {
430 $post_id = $post_ID;
431 }
432
433 if ( ! $post_id && $post ) {
434 $post_id = $post->ID;
435 }
436
437 if ( $post_id ) {
438 $coauthor_terms = get_the_terms( $post_id, $coauthors_plus->coauthor_taxonomy );
439
440 if ( is_array( $coauthor_terms ) ) {
441 foreach ( $coauthor_terms as $coauthor ) {
442 $coauthor_slug = preg_replace( '#^cap-#', '', $coauthor->slug );
443 $post_author = $coauthors_plus->get_coauthor_by( 'user_nicename', $coauthor_slug );
444 // In case the user has been deleted while plugin was deactivated.
445 if ( ! empty( $post_author ) ) {
446 $coauthors[] = new WP_User( $post_author );
447 }
448 }
449 } elseif ( ! $coauthors_plus->force_guest_authors ) {
450 if ( $post && $post_id === $post->ID ) {
451 $post_author = get_userdata( $post->post_author );
452 }
453 if ( ! empty( $post_author ) ) {
454 $coauthors[] = $post_author;
455 }
456 }
457 // The empty else case is because if we force guest authors, we don't ever care what value wp_posts.post_author has.
458 }
459 }
460 return $coauthors;
461 }
462
463 /**
464 * Determines author name from display name, falling back to firstname
465 * lastname, then nickname and finally the nicename.
466 *
467 * @since 3.3.0 Moved to class-metadata
468 *
469 * @param ?WP_User $author The author of the post.
470 * @return string An author name.
471 */
472 private function get_author_name( ?WP_User $author ): string {
473 // Gracefully handle situation where no author is available.
474 if ( null === $author ) {
475 return '';
476 }
477
478 if ( ! empty( $author->display_name ) ) {
479 return $author->display_name;
480 }
481
482 $author_name = $author->user_firstname . ' ' . $author->user_lastname;
483 if ( ' ' !== $author_name ) {
484 return $author_name;
485 }
486
487 if ( ! empty( $author->nickname ) ) {
488 return $author->nickname;
489 }
490
491 if ( ! empty( $author->user_nicename ) ) {
492 return $author->user_nicename;
493 }
494
495 return '';
496 }
497
498 /**
499 * Returns the tags associated with this page or post.
500 *
501 * @since 3.3.0 Moved to class-metadata
502 *
503 * @param int $post_id The ID of the post you're trying to get tags for.
504 * @return array<string> The tags of the post represented by the post id.
505 */
506 private function get_tags( int $post_id ): array {
507 $tags = array();
508 $post_tags = wp_get_post_tags( $post_id );
509 if ( ! is_wp_error( $post_tags ) ) {
510 foreach ( $post_tags as $wp_tag ) {
511 $tags[] = $wp_tag->name;
512 }
513 }
514 return $tags;
515 }
516
517 /**
518 * Returns an array of all the child categories for the current post.
519 *
520 * @since 3.3.0 Moved to class-metadata
521 *
522 * @param int $post_id The ID of the post you're trying to get categories for.
523 * @param string $delimiter What character will delimit the categories.
524 * @return array<string> All the child categories of the current post.
525 */
526 private function get_categories( int $post_id, string $delimiter = '/' ): array {
527 $tags = array();
528 foreach ( get_the_category( $post_id ) as $category ) {
529 $hierarchy = get_category_parents( $category->term_id, false, $delimiter );
530 if ( ! is_wp_error( $hierarchy ) ) {
531 $tags[] = rtrim( $hierarchy, '/' );
532 }
533 }
534 // Take last element in the hierarchy, a string representing the full parent->child tree,
535 // and split it into individual category names.
536 $last_tag = end( $tags );
537 if ( false !== $last_tag ) {
538 $tags = explode( '/', $last_tag );
539 }
540
541 // Remove default category name from tags if needed.
542 $default_category_name = get_cat_name( get_option( 'default_category' ) );
543 return array_diff( $tags, array( $default_category_name ) );
544 }
545
546 /**
547 * Gets all term names from all custom taxonomies assigned to a post.
548 *
549 * @since 3.3.0 Moved to class-metadata
550 * @since 3.4.0 Moved to class-post-builder
551 *
552 * @param WP_Post $post_obj The post object to find the terms for.
553 * @return array<string> Term names.
554 */
555 private function get_custom_taxonomy_values( WP_Post $post_obj ): array {
556 // Filter out default WordPress taxonomies.
557 $all_taxonomies = array_diff( get_taxonomies(), array( 'post_tag', 'nav_menu', 'author', 'link_category', 'post_format' ) );
558 $all_values = array();
559
560 foreach ( $all_taxonomies as $taxonomy ) {
561 $custom_taxonomy_objects = get_the_terms( $post_obj->ID, $taxonomy );
562 if ( is_array( $custom_taxonomy_objects ) ) {
563 foreach ( $custom_taxonomy_objects as $custom_taxonomy_object ) {
564 $all_values[] = $custom_taxonomy_object->name;
565 }
566 }
567 }
568
569 return $all_values;
570 }
571 }
572