PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
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 / abilities / infrastructure / post-identifier-resolver.php

post-identifier-resolver.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at src/abilities/infrastructure/post-identifier-resolver.php

202 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4 namespace Yoast\WP\SEO\Abilities\Infrastructure;
5
6 use WP_Error;
7 use Yoast\WP\SEO\Models\Indexable;
8 use Yoast\WP\SEO\Repositories\Indexable_Repository;
9
10 /**
11 * Resolves a user-friendly post identifier (ID, permalink, or title keywords) into post indexables.
12 *
13 * Lets callers find a post without knowing its numeric ID: by URL, or by a few
14 * remembered title keywords. Shared by the read ability, the write ability, and
15 * the write permission check.
16 */
17 class Post_Identifier_Resolver {
18
19 /**
20 * The indexable repository.
21 *
22 * @var Indexable_Repository
23 */
24 private $indexable_repository;
25
26 /**
27 * Constructor.
28 *
29 * @param Indexable_Repository $indexable_repository The indexable repository.
30 */
31 public function __construct(
32 Indexable_Repository $indexable_repository
33 ) {
34 $this->indexable_repository = $indexable_repository;
35 }
36
37 /**
38 * Resolves the input to exactly one post indexable.
39 *
40 * Used by operations that have to target a single, unambiguous post.
41 * Title keywords are deliberately not accepted here: they can match several
42 * posts, so only an exact identifier (post ID or permalink) is allowed.
43 *
44 * @param array<string, int|string|bool|null> $input The input containing one of 'post_id' or 'permalink'.
45 *
46 * @return Indexable|WP_Error The matching indexable, or an error (missing or unknown identifier).
47 */
48 public function resolve_one( array $input ) {
49 if ( $this->has( $input, 'post_id' ) ) {
50 return $this->by_id( (int) $input['post_id'] );
51 }
52
53 if ( $this->has( $input, 'permalink' ) ) {
54 return $this->by_permalink( (string) $input['permalink'] );
55 }
56
57 return new WP_Error(
58 'yoast_seo_missing_identifier',
59 \__( 'Provide a post_id or a permalink to identify the post.', 'wordpress-seo' ),
60 [ 'status' => 400 ],
61 );
62 }
63
64 /**
65 * Resolves the input to all matching post indexables.
66 *
67 * Used by the read path. A title search may match several posts; with no
68 * identifier at all, an error is returned.
69 *
70 * @param array<string, int|string|bool|null> $input The input containing a 'post_id', 'permalink', or 'title', plus an optional 'page' for a title search.
71 *
72 * @return Indexable[]|WP_Error The matching indexables, or an error for a missing or unknown id/permalink.
73 */
74 public function resolve_many( array $input ) {
75 if ( $this->has( $input, 'post_id' ) ) {
76 $indexable = $this->by_id( (int) $input['post_id'] );
77
78 return ( $indexable instanceof WP_Error ) ? $indexable : [ $indexable ];
79 }
80
81 if ( $this->has( $input, 'permalink' ) ) {
82 $indexable = $this->by_permalink( (string) $input['permalink'] );
83
84 return ( $indexable instanceof WP_Error ) ? $indexable : [ $indexable ];
85 }
86
87 if ( $this->has( $input, 'title' ) ) {
88 $page = ( isset( $input['page'] ) ) ? (int) $input['page'] : 1;
89
90 return $this->by_title( (string) $input['title'], $page );
91 }
92
93 return new WP_Error(
94 'yoast_seo_missing_identifier',
95 \__( 'Provide a post_id, a permalink, or title keywords to identify the post.', 'wordpress-seo' ),
96 [ 'status' => 400 ],
97 );
98 }
99
100 /**
101 * Resolves a post by its ID.
102 *
103 * @param int $post_id The post ID.
104 *
105 * @return Indexable|WP_Error The matching indexable, or a not-found error.
106 */
107 private function by_id( int $post_id ) {
108 $indexable = $this->indexable_repository->find_by_id_and_type( $post_id, 'post', false );
109
110 if ( ! $indexable ) {
111 return $this->invalid_identifier( 'post_id' );
112 }
113
114 return $indexable;
115 }
116
117 /**
118 * Resolves a post by its permalink.
119 *
120 * Matches the permalink exactly against the indexable table; no WordPress URL
121 * resolution is performed, so the exact stored permalink must be provided.
122 *
123 * @param string $permalink The permalink.
124 *
125 * @return Indexable|WP_Error The matching indexable, or a not-found error.
126 */
127 private function by_permalink( string $permalink ) {
128 $indexable = $this->indexable_repository->find_by_permalink( $permalink );
129
130 // The permalink lookup is not scoped by object type, so a term, author, or
131 // archive URL can match too; only posts are valid targets here.
132 if ( ! $indexable || $indexable->object_type !== 'post' ) {
133 return $this->invalid_identifier( 'permalink' );
134 }
135
136 return $indexable;
137 }
138
139 /**
140 * Resolves posts by title keywords.
141 *
142 * A title search may match several posts; no match on the first page (or an
143 * unexpected result that is not a list of indexables) is treated as not-found,
144 * mirroring the by-id and by-permalink resolvers. An empty later page is valid
145 * pagination past the last result, as the page parameter's schema documents.
146 *
147 * @param string $title The title keywords.
148 * @param int $page The 1-based result page.
149 *
150 * @return Indexable[]|WP_Error The matching indexables, or a not-found error.
151 */
152 private function by_title( string $title, int $page ) {
153 $indexables = $this->indexable_repository->find_posts_by_title_keywords( $title, $page );
154
155 if ( $indexables === [] ) {
156 // An empty later page means the client paged past the last result, not a bad identifier.
157 if ( $page > 1 ) {
158 return [];
159 }
160
161 return $this->invalid_identifier( 'title' );
162 }
163
164 foreach ( $indexables as $indexable ) {
165 if ( ! ( $indexable instanceof Indexable ) ) {
166 return $this->invalid_identifier( 'title' );
167 }
168 }
169
170 return $indexables;
171 }
172
173 /**
174 * Builds the error for an identifier that does not resolve to a post.
175 *
176 * Coded yoast_seo_invalid_<field> so a caller knows which identifier to correct and retry.
177 *
178 * @param string $field The identifier that failed to resolve: 'post_id', 'permalink', or 'title'.
179 *
180 * @return WP_Error The error for the unresolvable identifier.
181 */
182 private function invalid_identifier( string $field ): WP_Error {
183 return new WP_Error(
184 'yoast_seo_invalid_' . $field,
185 \__( 'No post could be found for the given identifier.', 'wordpress-seo' ),
186 [ 'status' => 404 ],
187 );
188 }
189
190 /**
191 * Checks whether a non-empty identifier of the given key is present in the input.
192 *
193 * @param array<string, int|string|bool|null> $input The input.
194 * @param string $key The identifier key.
195 *
196 * @return bool Whether the identifier is present and non-empty.
197 */
198 private function has( array $input, string $key ): bool {
199 return ( isset( $input[ $key ] ) && $input[ $key ] !== '' );
200 }
201 }
202