PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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 / ai / content-planner / user-interface / get-outline-route.php

get-outline-route.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at src/ai/content-planner/user-interface/get-outline-route.php

243 lines 7.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\AI\Content_Planner\User_Interface;
5
6 use RuntimeException;
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use Yoast\WP\SEO\AI\Content_Planner\Application\Content_Outline_Command;
10 use Yoast\WP\SEO\AI\Content_Planner\Application\Content_Outline_Command_Handler;
11 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
12 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
13 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
14 use Yoast\WP\SEO\Conditionals\AI_Conditional;
15 use Yoast\WP\SEO\Main;
16 use Yoast\WP\SEO\Routes\Route_Interface;
17
18 /**
19 * Registers a route to get a content outline from the AI API.
20 *
21 * @internal This route powers the Yoast SEO admin UI's Content Planner feature. It is not part of the plugin's public REST API surface, requires the capability to edit posts of the requested post type (see {@see self::check_permissions()}), and may change at any time without notice.
22 *
23 * @makePublic
24 *
25 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
26 */
27 class Get_Outline_Route implements Route_Interface {
28
29 /**
30 * The namespace for this route.
31 *
32 * @var string
33 */
34 public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
35
36 /**
37 * The prefix for this route.
38 *
39 * @var string
40 */
41 public const ROUTE_PREFIX = '/ai_content_planner/get_outline';
42
43 /**
44 * The command handler instance.
45 *
46 * @var Content_Outline_Command_Handler
47 */
48 private $command_handler;
49
50 /**
51 * Returns the conditionals based in which this loadable should be active.
52 *
53 * @return array<string> The conditionals.
54 */
55 public static function get_conditionals() {
56 return [ AI_Conditional::class ];
57 }
58
59 /**
60 * Class constructor.
61 *
62 * @param Content_Outline_Command_Handler $command_handler The command handler instance.
63 */
64 public function __construct( Content_Outline_Command_Handler $command_handler ) {
65 $this->command_handler = $command_handler;
66 }
67
68 /**
69 * Registers routes with WordPress.
70 *
71 * @return void
72 */
73 public function register_routes() {
74 \register_rest_route(
75 self::ROUTE_NAMESPACE,
76 self::ROUTE_PREFIX,
77 [
78 'methods' => 'POST',
79 'args' => [
80 'post_type' => [
81 'required' => true,
82 'type' => 'string',
83 'description' => 'The post type to get a content outline for.',
84 ],
85 'language' => [
86 'required' => true,
87 'type' => 'string',
88 'description' => 'The language the content is written in.',
89 ],
90 'editor' => [
91 'required' => true,
92 'type' => 'string',
93 'enum' => [
94 'classic',
95 'elementor',
96 'gutenberg',
97 ],
98 'description' => 'The current editor.',
99 ],
100 'title' => [
101 'required' => true,
102 'type' => 'string',
103 'description' => 'The title of the chosen content suggestion.',
104 ],
105 'intent' => [
106 'required' => true,
107 'type' => 'string',
108 'description' => 'The intent of the chosen content suggestion.',
109 ],
110 'explanation' => [
111 'required' => true,
112 'type' => 'string',
113 'description' => 'The explanation of the chosen content suggestion.',
114 ],
115 'keyphrase' => [
116 'required' => true,
117 'type' => 'string',
118 'description' => 'The keyphrase of the chosen content suggestion.',
119 ],
120 'meta_description' => [
121 'required' => true,
122 'type' => 'string',
123 'description' => 'The meta description of the chosen content suggestion.',
124 ],
125 'category' => [
126 'required' => true,
127 'type' => 'object',
128 'properties' => [
129 'name' => [
130 'type' => 'string',
131 'required' => true,
132 ],
133 'id' => [
134 'type' => 'integer',
135 'required' => true,
136 ],
137 ],
138 'description' => 'The category of the chosen content suggestion. Use name "" and id -1 to indicate no category.',
139 ],
140 'recent_content' => [
141 'required' => true,
142 'type' => 'array',
143 'maxItems' => 100,
144 'items' => [
145 'type' => 'object',
146 'properties' => [
147 'title' => [
148 'type' => 'string',
149 'required' => true,
150 'maxLength' => 500,
151 ],
152 'description' => [
153 'type' => 'string',
154 'required' => true,
155 'maxLength' => 1000,
156 ],
157 ],
158 'additionalProperties' => false,
159 ],
160 'description' => 'The recent content returned by the get_suggestions response.',
161 ],
162 ],
163 'callback' => [ $this, 'get_outline' ],
164 'permission_callback' => [ $this, 'check_permissions' ],
165 ],
166 );
167 }
168
169 /**
170 * Runs the callback to get an AI-generated content outline.
171 *
172 * @param WP_REST_Request $request The request object.
173 *
174 * @return WP_REST_Response The response of the get_outline action.
175 */
176 public function get_outline( WP_REST_Request $request ): WP_REST_Response {
177 try {
178 $user = \wp_get_current_user();
179
180 $category_param = $request->get_param( 'category' );
181
182 $command = new Content_Outline_Command(
183 $user,
184 $request->get_param( 'post_type' ),
185 $request->get_param( 'language' ),
186 $request->get_param( 'editor' ),
187 $request->get_param( 'title' ),
188 $request->get_param( 'intent' ),
189 $request->get_param( 'explanation' ),
190 $request->get_param( 'keyphrase' ),
191 $request->get_param( 'meta_description' ),
192 $category_param['name'],
193 (int) $category_param['id'],
194 $request->get_param( 'recent_content' ),
195 );
196 $data = $this->command_handler->handle( $command );
197 } catch ( Remote_Request_Exception $e ) {
198 $message = [
199 'message' => $e->getMessage(),
200 'errorIdentifier' => $e->get_error_identifier(),
201 ];
202 if ( $e instanceof Payment_Required_Exception || $e instanceof Too_Many_Requests_Exception ) {
203 $message['missingLicenses'] = $e->get_missing_licenses();
204 }
205 return new WP_REST_Response(
206 $message,
207 $e->getCode(),
208 );
209 } catch ( RuntimeException $e ) {
210 return new WP_REST_Response( 'Failed to get content outline.', 500 );
211 }
212
213 return new WP_REST_Response( $data->to_array() );
214 }
215
216 /**
217 * Checks if the user is logged in and can edit posts of the requested post type.
218 *
219 * The requested post_type is caller-controlled, so the permission must be evaluated
220 * against that post type's own edit_posts meta-capability — not the generic
221 * 'edit_posts' string, which would let a user with edit_posts but no edit_pages
222 * (e.g. an Author) trigger outline generation for pages or arbitrary CPTs.
223 *
224 * @param WP_REST_Request $request The request object.
225 *
226 * @return bool Whether the user can edit posts of the requested post type.
227 */
228 public function check_permissions( WP_REST_Request $request ): bool {
229 $user = \wp_get_current_user();
230 if ( $user === null || $user->ID < 1 ) {
231 return false;
232 }
233
234 $post_type = $request->get_param( 'post_type' );
235 $post_type_object = \get_post_type_object( $post_type );
236 if ( $post_type_object === null ) {
237 return false;
238 }
239
240 return \user_can( $user, $post_type_object->cap->edit_posts );
241 }
242 }
243