PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.6
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.6
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / integrations / front-end-integration.php

front-end-integration.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.6, at src/integrations/front-end-integration.php

479 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Integrations;
4
5 use WPSEO_Replace_Vars;
6 use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
7 use Yoast\WP\SEO\Context\Meta_Tags_Context;
8 use Yoast\WP\SEO\Helpers\Options_Helper;
9 use Yoast\WP\SEO\Helpers\Request_Helper;
10 use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
11 use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
12 use Yoast\WP\SEO\Presenters\Debug\Marker_Close_Presenter;
13 use Yoast\WP\SEO\Presenters\Debug\Marker_Open_Presenter;
14 use Yoast\WP\SEO\Presenters\Title_Presenter;
15 use Yoast\WP\SEO\Surfaces\Helpers_Surface;
16 use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
17
18 /**
19 * Class Front_End_Integration.
20 */
21 class Front_End_Integration implements Integration_Interface {
22
23 /**
24 * The memoizer for the meta tags context.
25 *
26 * @var Meta_Tags_Context_Memoizer
27 */
28 private $context_memoizer;
29
30 /**
31 * The container.
32 *
33 * @var ContainerInterface
34 */
35 protected $container;
36
37 /**
38 * Represents the options helper.
39 *
40 * @var Options_Helper
41 */
42 protected $options;
43
44 /**
45 * Represents the request helper.
46 *
47 * @var Request_Helper
48 */
49 protected $request;
50
51 /**
52 * The helpers surface.
53 *
54 * @var Helpers_Surface
55 */
56 protected $helpers;
57
58 /**
59 * The replace vars helper.
60 *
61 * @var WPSEO_Replace_Vars
62 */
63 protected $replace_vars;
64
65 /**
66 * The presenters we loop through on each page load.
67 *
68 * @var string[]
69 */
70 protected $base_presenters = [
71 'Title',
72 'Meta_Description',
73 'Robots',
74 ];
75
76 /**
77 * The presenters we loop through on each page load.
78 *
79 * @var string[]
80 */
81 protected $indexing_directive_presenters = [
82 'Canonical',
83 'Rel_Prev',
84 'Rel_Next',
85 ];
86
87 /**
88 * The Open Graph specific presenters.
89 *
90 * @var string[]
91 */
92 protected $open_graph_presenters = [
93 'Open_Graph\Locale',
94 'Open_Graph\Type',
95 'Open_Graph\Title',
96 'Open_Graph\Description',
97 'Open_Graph\Url',
98 'Open_Graph\Site_Name',
99 'Open_Graph\Article_Publisher',
100 'Open_Graph\Article_Author',
101 'Open_Graph\Article_Published_Time',
102 'Open_Graph\Article_Modified_Time',
103 'Open_Graph\Image',
104 ];
105
106 /**
107 * The Open Graph specific presenters that should be output on error pages.
108 *
109 * @var array
110 */
111 protected $open_graph_error_presenters = [
112 'Open_Graph\Locale',
113 'Open_Graph\Title',
114 'Open_Graph\Site_Name',
115 ];
116
117 /**
118 * The Twitter card specific presenters.
119 *
120 * @var string[]
121 */
122 protected $twitter_card_presenters = [
123 'Twitter\Card',
124 'Twitter\Title',
125 'Twitter\Description',
126 'Twitter\Image',
127 'Twitter\Creator',
128 'Twitter\Site',
129 ];
130
131 /**
132 * The Slack specific presenters.
133 *
134 * @var string[]
135 */
136 protected $slack_presenters = [
137 'Slack\Enhanced_Data',
138 ];
139
140 /**
141 * The Webmaster verification specific presenters.
142 *
143 * @var string[]
144 */
145 protected $webmaster_verification_presenters = [
146 'Webmaster\Baidu',
147 'Webmaster\Bing',
148 'Webmaster\Google',
149 'Webmaster\Pinterest',
150 'Webmaster\Yandex',
151 ];
152
153 /**
154 * Presenters that are only needed on singular pages.
155 *
156 * @var string[]
157 */
158 protected $singular_presenters = [
159 'Open_Graph\Article_Author',
160 'Open_Graph\Article_Publisher',
161 'Open_Graph\Article_Published_Time',
162 'Open_Graph\Article_Modified_Time',
163 'Twitter\Creator',
164 'Slack\Enhanced_Data',
165 ];
166
167 /**
168 * The presenters we want to be last in our output.
169 *
170 * @var string[]
171 */
172 protected $closing_presenters = [
173 'Schema',
174 ];
175
176 /**
177 * Returns the conditionals based on which this loadable should be active.
178 *
179 * @return array The conditionals.
180 */
181 public static function get_conditionals() {
182 return [ Front_End_Conditional::class ];
183 }
184
185 /**
186 * Front_End_Integration constructor.
187 *
188 * @codeCoverageIgnore It sets dependencies.
189 *
190 * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
191 * @param ContainerInterface $service_container The DI container.
192 * @param Options_Helper $options The options helper.
193 * @param Request_Helper $request The request helper.
194 * @param Helpers_Surface $helpers The helpers surface.
195 * @param WPSEO_Replace_Vars $replace_vars The replace vars helper.
196 */
197 public function __construct(
198 Meta_Tags_Context_Memoizer $context_memoizer,
199 ContainerInterface $service_container,
200 Options_Helper $options,
201 Request_Helper $request,
202 Helpers_Surface $helpers,
203 WPSEO_Replace_Vars $replace_vars
204 ) {
205 $this->container = $service_container;
206 $this->context_memoizer = $context_memoizer;
207 $this->options = $options;
208 $this->request = $request;
209 $this->helpers = $helpers;
210 $this->replace_vars = $replace_vars;
211 }
212
213 /**
214 * Registers the appropriate hooks to show the SEO metadata on the frontend.
215 *
216 * Removes some actions to remove metadata that WordPress shows on the frontend,
217 * to avoid duplicate and/or mismatched metadata.
218 */
219 public function register_hooks() {
220 \add_action( 'wp_head', [ $this, 'call_wpseo_head' ], 1 );
221 // Filter the title for compatibility with other plugins and themes.
222 \add_filter( 'wp_title', [ $this, 'filter_title' ], 15 );
223 // Filter the title for compatibility with block-based themes.
224 \add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
225
226 // Removes our robots presenter from the list when wp_robots is handling this.
227 \add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'filter_robots_presenter' ] );
228
229 \add_action( 'wpseo_head', [ $this, 'present_head' ], -9999 );
230
231 \remove_action( 'wp_head', 'rel_canonical' );
232 \remove_action( 'wp_head', 'index_rel_link' );
233 \remove_action( 'wp_head', 'start_post_rel_link' );
234 \remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );
235 \remove_action( 'wp_head', 'noindex', 1 );
236 \remove_action( 'wp_head', '_wp_render_title_tag', 1 );
237 \remove_action( 'wp_head', '_block_template_render_title_tag', 1 );
238 \remove_action( 'wp_head', 'gutenberg_render_title_tag', 1 );
239 }
240
241 /**
242 * Filters the title, mainly used for compatibility reasons.
243 *
244 * @return string
245 */
246 public function filter_title() {
247 $context = $this->context_memoizer->for_current_page();
248
249 $title_presenter = new Title_Presenter();
250
251 /** This filter is documented in src/integrations/front-end-integration.php */
252 $title_presenter->presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
253 $title_presenter->replace_vars = $this->replace_vars;
254 $title_presenter->helpers = $this->helpers;
255
256 \remove_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
257 $title = \esc_html( $title_presenter->get() );
258 \add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
259
260 return $title;
261 }
262
263 /**
264 * Filters our robots presenter, but only when wp_robots is attached to the wp_head action.
265 *
266 * @param array $presenters The presenters for current page.
267 *
268 * @return array The filtered presenters.
269 */
270 public function filter_robots_presenter( $presenters ) {
271 if ( ! \function_exists( 'wp_robots' ) ) {
272 return $presenters;
273 }
274
275 if ( ! \has_action( 'wp_head', 'wp_robots' ) ) {
276 return $presenters;
277 }
278
279 if ( $this->request->is_rest_request() ) {
280 return $presenters;
281 }
282
283 return \array_diff( $presenters, [ 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' ] );
284 }
285
286 /**
287 * Presents the head in the front-end. Resets wp_query if it's not the main query.
288 *
289 * @codeCoverageIgnore It just calls a WordPress function.
290 */
291 public function call_wpseo_head() {
292 global $wp_query;
293
294 $old_wp_query = $wp_query;
295 // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- Reason: The recommended function, wp_reset_postdata, doesn't reset wp_query.
296 \wp_reset_query();
297
298 \do_action( 'wpseo_head' );
299
300 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query.
301 $GLOBALS['wp_query'] = $old_wp_query;
302 }
303
304 /**
305 * Echoes all applicable presenters for a page.
306 */
307 public function present_head() {
308 $context = $this->context_memoizer->for_current_page();
309 $presenters = $this->get_presenters( $context->page_type, $context );
310
311 /**
312 * Filter 'wpseo_frontend_presentation' - Allow filtering the presentation used to output our meta values.
313 *
314 * @api Indexable_Presention The indexable presentation.
315 */
316 $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
317
318 echo \PHP_EOL;
319 foreach ( $presenters as $presenter ) {
320 $presenter->presentation = $presentation;
321 $presenter->helpers = $this->helpers;
322 $presenter->replace_vars = $this->replace_vars;
323
324 $output = $presenter->present();
325 if ( ! empty( $output ) ) {
326 // phpcs:ignore WordPress.Security.EscapeOutput -- Presenters are responsible for correctly escaping their output.
327 echo "\t" . $output . \PHP_EOL;
328 }
329 }
330 echo \PHP_EOL . \PHP_EOL;
331 }
332
333 /**
334 * Returns all presenters for this page.
335 *
336 * @param string $page_type The page type.
337 * @param Meta_Tags_Context|null $context The meta tags context for the current page.
338 *
339 * @return Abstract_Indexable_Presenter[] The presenters.
340 */
341 public function get_presenters( $page_type, $context = null ) {
342 if ( \is_null( $context ) ) {
343 $context = $this->context_memoizer->for_current_page();
344 }
345
346 $needed_presenters = $this->get_needed_presenters( $page_type );
347
348 $callback = static function( $presenter ) {
349 if ( ! \class_exists( $presenter ) ) {
350 return null;
351 }
352 return new $presenter();
353 };
354 $presenters = \array_filter( \array_map( $callback, $needed_presenters ) );
355
356 /**
357 * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request.
358 *
359 * @param array $presenters The presenters.
360 * @param Meta_Tags_Context $context The meta tags context for the current page.
361 *
362 * @api Abstract_Indexable_Presenter[] List of presenter instances.
363 */
364 $presenter_instances = \apply_filters( 'wpseo_frontend_presenters', $presenters, $context );
365
366 if ( ! \is_array( $presenter_instances ) ) {
367 $presenter_instances = $presenters;
368 }
369
370 $is_presenter_callback = static function ( $presenter_instance ) {
371 return $presenter_instance instanceof Abstract_Indexable_Presenter;
372 };
373 $presenter_instances = \array_filter( $presenter_instances, $is_presenter_callback );
374
375 return \array_merge(
376 [ new Marker_Open_Presenter() ],
377 $presenter_instances,
378 [ new Marker_Close_Presenter() ]
379 );
380 }
381
382 /**
383 * Generate the array of presenters we need for the current request.
384 *
385 * @param string $page_type The page type we're retrieving presenters for.
386 *
387 * @return string[] The presenters.
388 */
389 private function get_needed_presenters( $page_type ) {
390 $presenters = $this->get_presenters_for_page_type( $page_type );
391
392 $presenters = $this->maybe_remove_title_presenter( $presenters );
393
394 $callback = static function ( $presenter ) {
395 return "Yoast\WP\SEO\Presenters\\{$presenter}_Presenter";
396 };
397 $presenters = \array_map( $callback, $presenters );
398
399 /**
400 * Filter 'wpseo_frontend_presenter_classes' - Allow filtering presenters in or out of the request.
401 *
402 * @api array List of presenters.
403 */
404 $presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters );
405
406 return $presenters;
407 }
408
409 /**
410 * Filters the presenters based on the page type.
411 *
412 * @param string $page_type The page type.
413 *
414 * @return string[] The presenters.
415 */
416 private function get_presenters_for_page_type( $page_type ) {
417 if ( $page_type === 'Error_Page' ) {
418 $presenters = $this->base_presenters;
419 if ( $this->options->get( 'opengraph' ) === true ) {
420 $presenters = \array_merge( $presenters, $this->open_graph_error_presenters );
421 }
422 return \array_merge( $presenters, $this->closing_presenters );
423 }
424
425 $presenters = $this->get_all_presenters();
426 if ( \in_array( $page_type, [ 'Static_Home_Page', 'Home_Page' ], true ) ) {
427 $presenters = \array_merge( $presenters, $this->webmaster_verification_presenters );
428 }
429
430 // Filter out the presenters only needed for singular pages on non-singular pages.
431 if ( ! \in_array( $page_type, [ 'Post_Type', 'Static_Home_Page' ], true ) ) {
432 $presenters = \array_diff( $presenters, $this->singular_presenters );
433 }
434
435 return $presenters;
436 }
437
438 /**
439 * Returns a list of all available presenters based on settings.
440 *
441 * @return string[] The presenters.
442 */
443 private function get_all_presenters() {
444 $presenters = \array_merge( $this->base_presenters, $this->indexing_directive_presenters );
445 if ( $this->options->get( 'opengraph' ) === true ) {
446 $presenters = \array_merge( $presenters, $this->open_graph_presenters );
447 }
448 if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) {
449 $presenters = \array_merge( $presenters, $this->twitter_card_presenters );
450 }
451 if ( $this->options->get( 'enable_enhanced_slack_sharing' ) === true && \apply_filters( 'wpseo_output_enhanced_slack_data', true ) !== false ) {
452 $presenters = \array_merge( $presenters, $this->slack_presenters );
453 }
454
455 return \array_merge( $presenters, $this->closing_presenters );
456 }
457
458 /**
459 * Checks if the Title presenter needs to be removed.
460 *
461 * @param string[] $presenters The presenters.
462 *
463 * @return string[] The presenters.
464 */
465 private function maybe_remove_title_presenter( $presenters ) {
466 // Do not remove the title if we're on a REST request.
467 if ( $this->request->is_rest_request() ) {
468 return $presenters;
469 }
470
471 // Remove the title presenter if the theme is hardcoded to output a title tag so we don't have two title tags.
472 if ( ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false ) ) {
473 $presenters = \array_diff( $presenters, [ 'Title' ] );
474 }
475
476 return $presenters;
477 }
478 }
479