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 / bulk-editor / user-interface / posts-route.php

posts-route.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at src/bulk-editor/user-interface/posts-route.php

287 lines 8.9 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\Bulk_Editor\User_Interface;
5
6 use WP_Error;
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use Yoast\WP\SEO\Bulk_Editor\Application\Content_Types\Content_Type_Access_Checker_Interface;
10 use Yoast\WP\SEO\Bulk_Editor\Application\Content_Types\Content_Types_Repository;
11 use Yoast\WP\SEO\Bulk_Editor\Application\Posts\Posts_Collector_Interface;
12 use Yoast\WP\SEO\Bulk_Editor\Application\Posts\Posts_Repository;
13 use Yoast\WP\SEO\Bulk_Editor\Domain\Posts\Posts_Query;
14 use Yoast\WP\SEO\Conditionals\No_Conditionals;
15 use Yoast\WP\SEO\Helpers\Options_Helper;
16 use Yoast\WP\SEO\Helpers\Post_Type_Helper;
17 use Yoast\WP\SEO\Helpers\User_Helper;
18 use Yoast\WP\SEO\Main;
19 use Yoast\WP\SEO\Routes\Route_Interface;
20
21 /**
22 * Registers a route that returns a page of posts with their SEO and social meta.
23 */
24 class Posts_Route implements Route_Interface {
25
26 use No_Conditionals;
27
28 /**
29 * The namespace for this route.
30 *
31 * @var string
32 */
33 public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
34
35 /**
36 * The prefix for this route.
37 *
38 * @var string
39 */
40 public const ROUTE_PREFIX = '/bulk_editor/posts';
41
42 /**
43 * The default number of posts to return.
44 *
45 * @var int
46 */
47 public const DEFAULT_PER_PAGE = 20;
48
49 /**
50 * The maximum number of posts that can be requested in one page.
51 *
52 * @var int
53 */
54 public const MAX_PER_PAGE = 100;
55
56 /**
57 * The posts repository.
58 *
59 * @var Posts_Repository
60 */
61 private $posts_repository;
62
63 /**
64 * The content types repository.
65 *
66 * @var Content_Types_Repository
67 */
68 private $content_types_repository;
69
70 /**
71 * The content type access checker.
72 *
73 * @var Content_Type_Access_Checker_Interface
74 */
75 private $content_type_access_checker;
76
77 /**
78 * The user helper.
79 *
80 * @var User_Helper
81 */
82 private $user_helper;
83
84 /**
85 * The options helper.
86 *
87 * @var Options_Helper
88 */
89 private $options_helper;
90
91 /**
92 * The post type helper.
93 *
94 * @var Post_Type_Helper
95 */
96 private $post_type_helper;
97
98 /**
99 * The constructor.
100 *
101 * @param Posts_Repository $posts_repository The posts repository.
102 * @param Content_Types_Repository $content_types_repository The content types repository.
103 * @param Content_Type_Access_Checker_Interface $content_type_access_checker The content type access checker.
104 * @param User_Helper $user_helper The user helper.
105 * @param Options_Helper $options_helper The options helper.
106 * @param Post_Type_Helper $post_type_helper The post type helper.
107 */
108 public function __construct(
109 Posts_Repository $posts_repository,
110 Content_Types_Repository $content_types_repository,
111 Content_Type_Access_Checker_Interface $content_type_access_checker,
112 User_Helper $user_helper,
113 Options_Helper $options_helper,
114 Post_Type_Helper $post_type_helper
115 ) {
116 $this->posts_repository = $posts_repository;
117 $this->content_types_repository = $content_types_repository;
118 $this->content_type_access_checker = $content_type_access_checker;
119 $this->user_helper = $user_helper;
120 $this->options_helper = $options_helper;
121 $this->post_type_helper = $post_type_helper;
122 }
123
124 /**
125 * Registers routes with WordPress.
126 *
127 * @return void
128 */
129 public function register_routes() {
130 \register_rest_route(
131 self::ROUTE_NAMESPACE,
132 self::ROUTE_PREFIX,
133 [
134 'methods' => 'GET',
135 'args' => [
136 'content_type' => [
137 'required' => true,
138 'type' => 'string',
139 'description' => 'The content type to fetch posts for.',
140 'sanitize_callback' => 'sanitize_text_field',
141 ],
142 'per_page' => [
143 'required' => false,
144 'type' => 'integer',
145 'default' => self::DEFAULT_PER_PAGE,
146 'minimum' => 1,
147 'maximum' => self::MAX_PER_PAGE,
148 'description' => 'The number of posts to fetch.',
149 'sanitize_callback' => 'absint',
150 ],
151 'page' => [
152 'required' => false,
153 'type' => 'integer',
154 'default' => 1,
155 'minimum' => 1,
156 'description' => 'The page of posts to fetch.',
157 'sanitize_callback' => 'absint',
158 ],
159 'search' => [
160 'required' => false,
161 'type' => 'string',
162 'default' => '',
163 'description' => 'The term to search posts by.',
164 'sanitize_callback' => 'sanitize_text_field',
165 ],
166 'status' => [
167 'required' => false,
168 'type' => 'array',
169 'default' => Posts_Collector_Interface::STATUSES,
170 'items' => [
171 'type' => 'string',
172 'enum' => Posts_Collector_Interface::STATUSES,
173 ],
174 'description' => 'The post statuses to include.',
175 ],
176 'needs_improvement' => [
177 'required' => false,
178 'type' => 'array',
179 'default' => [],
180 'items' => [
181 'type' => 'string',
182 'enum' => Posts_Collector_Interface::NEEDS_IMPROVEMENT_FIELDS,
183 ],
184 'description' => 'The fields to filter posts by; a field matches when it is empty, or (for search fields, while SEO analysis is enabled and the content type shows Yoast\'s controls and assessments) when its score needs improvement.',
185 ],
186 'include' => [
187 'required' => false,
188 'type' => 'array',
189 'default' => [],
190 'maxItems' => self::MAX_PER_PAGE,
191 'items' => [
192 'type' => 'integer',
193 'minimum' => 1,
194 ],
195 'description' => 'Limits the posts to these post IDs, e.g. a selection carried over from the posts overview.',
196 ],
197 ],
198 'callback' => [ $this, 'get_posts' ],
199 'permission_callback' => [ $this, 'check_permissions' ],
200 ],
201 );
202 }
203
204 /**
205 * Returns a page of posts for the requested content type.
206 *
207 * @param WP_REST_Request $request The request object.
208 *
209 * @return WP_REST_Response|WP_Error The posts, or an error when the content type is not editable.
210 */
211 public function get_posts( WP_REST_Request $request ) {
212 $content_type = $request->get_param( 'content_type' );
213
214 if ( ! $this->is_valid_content_type( $content_type ) ) {
215 return new WP_Error(
216 'rest_invalid_content_type',
217 'The requested content type is not editable.',
218 [ 'status' => 400 ],
219 );
220 }
221
222 // An empty selection (no status filter) means all statuses, never zero results.
223 $statuses = (array) $request->get_param( 'status' );
224 if ( empty( $statuses ) ) {
225 $statuses = Posts_Collector_Interface::STATUSES;
226 }
227
228 // Narrow the query to the user's own posts when they cannot edit other authors' posts. This keeps
229 // pagination cheap; the exact per-post edit permission is still enforced when collecting the page.
230 $author_id = null;
231 if ( ! $this->content_type_access_checker->can_edit_others( $content_type ) ) {
232 $author_id = $this->user_helper->get_current_user_id();
233 }
234
235 // The per-field scores only back the filter while SEO analysis is on globally and the content type
236 // shows Yoast's controls and assessments; otherwise they go stale and the filter falls back to the
237 // empty-field check.
238 $scores_enabled = $this->options_helper->get( 'keyword_analysis_active' ) === true
239 && $this->post_type_helper->has_metabox( $content_type );
240
241 // The schema already coerces the items to positive integers; deduplicate on top of that.
242 $include = \array_values( \array_unique( \array_map( 'intval', (array) $request->get_param( 'include' ) ) ) );
243
244 $query = new Posts_Query(
245 $content_type,
246 (int) $request->get_param( 'page' ),
247 (int) $request->get_param( 'per_page' ),
248 (string) $request->get_param( 'search' ),
249 $statuses,
250 $author_id,
251 (array) $request->get_param( 'needs_improvement' ),
252 $scores_enabled,
253 $include,
254 );
255
256 // Posts the current user cannot edit are returned locked and without their SEO data; the per-post
257 // permission is resolved while collecting the page.
258 return new WP_REST_Response( $this->posts_repository->get_posts( $query )->to_array() );
259 }
260
261 /**
262 * Checks whether the current user is allowed to use the bulk editor.
263 *
264 * @return bool Whether the current user is allowed to use the bulk editor.
265 */
266 public function check_permissions(): bool {
267 return \current_user_can( 'wpseo_manage_options' );
268 }
269
270 /**
271 * Checks whether the given content type is one the bulk editor can edit.
272 *
273 * @param string $content_type The content type to check.
274 *
275 * @return bool Whether the content type is editable.
276 */
277 private function is_valid_content_type( string $content_type ): bool {
278 foreach ( $this->content_types_repository->get_content_types() as $editable ) {
279 if ( $editable['name'] === $content_type ) {
280 return true;
281 }
282 }
283
284 return false;
285 }
286 }
287