PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.3
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.3
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.3, at src/bulk-editor/user-interface/posts-route.php

232 lines 6.7 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\User_Helper;
16 use Yoast\WP\SEO\Main;
17 use Yoast\WP\SEO\Routes\Route_Interface;
18
19 /**
20 * Registers a route that returns a page of posts with their SEO and social meta.
21 */
22 class Posts_Route implements Route_Interface {
23
24 use No_Conditionals;
25
26 /**
27 * The namespace for this route.
28 *
29 * @var string
30 */
31 public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
32
33 /**
34 * The prefix for this route.
35 *
36 * @var string
37 */
38 public const ROUTE_PREFIX = '/bulk_editor/posts';
39
40 /**
41 * The default number of posts to return.
42 *
43 * @var int
44 */
45 public const DEFAULT_PER_PAGE = 20;
46
47 /**
48 * The maximum number of posts that can be requested in one page.
49 *
50 * @var int
51 */
52 public const MAX_PER_PAGE = 100;
53
54 /**
55 * The posts repository.
56 *
57 * @var Posts_Repository
58 */
59 private $posts_repository;
60
61 /**
62 * The content types repository.
63 *
64 * @var Content_Types_Repository
65 */
66 private $content_types_repository;
67
68 /**
69 * The content type access checker.
70 *
71 * @var Content_Type_Access_Checker_Interface
72 */
73 private $content_type_access_checker;
74
75 /**
76 * The user helper.
77 *
78 * @var User_Helper
79 */
80 private $user_helper;
81
82 /**
83 * The constructor.
84 *
85 * @param Posts_Repository $posts_repository The posts repository.
86 * @param Content_Types_Repository $content_types_repository The content types repository.
87 * @param Content_Type_Access_Checker_Interface $content_type_access_checker The content type access checker.
88 * @param User_Helper $user_helper The user helper.
89 */
90 public function __construct(
91 Posts_Repository $posts_repository,
92 Content_Types_Repository $content_types_repository,
93 Content_Type_Access_Checker_Interface $content_type_access_checker,
94 User_Helper $user_helper
95 ) {
96 $this->posts_repository = $posts_repository;
97 $this->content_types_repository = $content_types_repository;
98 $this->content_type_access_checker = $content_type_access_checker;
99 $this->user_helper = $user_helper;
100 }
101
102 /**
103 * Registers routes with WordPress.
104 *
105 * @return void
106 */
107 public function register_routes() {
108 \register_rest_route(
109 self::ROUTE_NAMESPACE,
110 self::ROUTE_PREFIX,
111 [
112 'methods' => 'GET',
113 'args' => [
114 'content_type' => [
115 'required' => true,
116 'type' => 'string',
117 'description' => 'The content type to fetch posts for.',
118 'sanitize_callback' => 'sanitize_text_field',
119 ],
120 'per_page' => [
121 'required' => false,
122 'type' => 'integer',
123 'default' => self::DEFAULT_PER_PAGE,
124 'minimum' => 1,
125 'maximum' => self::MAX_PER_PAGE,
126 'description' => 'The number of posts to fetch.',
127 'sanitize_callback' => 'absint',
128 ],
129 'page' => [
130 'required' => false,
131 'type' => 'integer',
132 'default' => 1,
133 'minimum' => 1,
134 'description' => 'The page of posts to fetch.',
135 'sanitize_callback' => 'absint',
136 ],
137 'search' => [
138 'required' => false,
139 'type' => 'string',
140 'default' => '',
141 'description' => 'The term to search posts by.',
142 'sanitize_callback' => 'sanitize_text_field',
143 ],
144 'status' => [
145 'required' => false,
146 'type' => 'array',
147 'default' => Posts_Collector_Interface::STATUSES,
148 'items' => [
149 'type' => 'string',
150 'enum' => Posts_Collector_Interface::STATUSES,
151 ],
152 'description' => 'The post statuses to include.',
153 ],
154 ],
155 'callback' => [ $this, 'get_posts' ],
156 'permission_callback' => [ $this, 'check_permissions' ],
157 ],
158 );
159 }
160
161 /**
162 * Returns a page of posts for the requested content type.
163 *
164 * @param WP_REST_Request $request The request object.
165 *
166 * @return WP_REST_Response|WP_Error The posts, or an error when the content type is not editable.
167 */
168 public function get_posts( WP_REST_Request $request ) {
169 $content_type = $request->get_param( 'content_type' );
170
171 if ( ! $this->is_valid_content_type( $content_type ) ) {
172 return new WP_Error(
173 'rest_invalid_content_type',
174 'The requested content type is not editable.',
175 [ 'status' => 400 ],
176 );
177 }
178
179 // An empty selection (no status filter) means all statuses, never zero results.
180 $statuses = (array) $request->get_param( 'status' );
181 if ( empty( $statuses ) ) {
182 $statuses = Posts_Collector_Interface::STATUSES;
183 }
184
185 // Narrow the query to the user's own posts when they cannot edit other authors' posts. This keeps
186 // pagination cheap; the exact per-post edit permission is still enforced when collecting the page.
187 $author_id = null;
188 if ( ! $this->content_type_access_checker->can_edit_others( $content_type ) ) {
189 $author_id = $this->user_helper->get_current_user_id();
190 }
191
192 $query = new Posts_Query(
193 $content_type,
194 (int) $request->get_param( 'page' ),
195 (int) $request->get_param( 'per_page' ),
196 (string) $request->get_param( 'search' ),
197 $statuses,
198 $author_id,
199 );
200
201 // Posts the current user cannot edit are returned locked and without their SEO data; the per-post
202 // permission is resolved while collecting the page.
203 return new WP_REST_Response( $this->posts_repository->get_posts( $query )->to_array() );
204 }
205
206 /**
207 * Checks whether the current user is allowed to use the bulk editor.
208 *
209 * @return bool Whether the current user is allowed to use the bulk editor.
210 */
211 public function check_permissions(): bool {
212 return \current_user_can( 'wpseo_manage_options' );
213 }
214
215 /**
216 * Checks whether the given content type is one the bulk editor can edit.
217 *
218 * @param string $content_type The content type to check.
219 *
220 * @return bool Whether the content type is editable.
221 */
222 private function is_valid_content_type( string $content_type ): bool {
223 foreach ( $this->content_types_repository->get_content_types() as $editable ) {
224 if ( $editable['name'] === $content_type ) {
225 return true;
226 }
227 }
228
229 return false;
230 }
231 }
232