PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.9
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.9
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 / application / content-outline-command-handler.php

content-outline-command-handler.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.9, at src/ai/content-planner/application/content-outline-command-handler.php

183 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3
4 namespace Yoast\WP\SEO\AI\Content_Planner\Application;
5
6 use Yoast\WP\SEO\AI\Authorization\Application\Token_Manager;
7 use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
8 use Yoast\WP\SEO\AI\Content_Planner\Domain\Section;
9 use Yoast\WP\SEO\AI\Content_Planner\Domain\Section_List;
10 use Yoast\WP\SEO\AI\Content_Planner\Infrastructure\Recent_Content\Recent_Content_Collector;
11 use Yoast\WP\SEO\AI\HTTP_Request\Application\Request_Handler;
12 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
13 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
14 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
15 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Response;
16
17 /**
18 * Handles the content outline command.
19 */
20 class Content_Outline_Command_Handler {
21
22 /**
23 * The recent content collector.
24 *
25 * @var Recent_Content_Collector
26 */
27 private $recent_content_collector;
28
29 /**
30 * The token manager.
31 *
32 * @var Token_Manager
33 */
34 private $token_manager;
35
36 /**
37 * The request handler.
38 *
39 * @var Request_Handler
40 */
41 private $request_handler;
42
43 /**
44 * The consent handler.
45 *
46 * @var Consent_Handler
47 */
48 private $consent_handler;
49
50 /**
51 * The constructor.
52 *
53 * @param Recent_Content_Collector $recent_content_collector The recent content collector.
54 * @param Token_Manager $token_manager The token manager.
55 * @param Request_Handler $request_handler The request handler.
56 * @param Consent_Handler $consent_handler The consent handler.
57 */
58 public function __construct(
59 Recent_Content_Collector $recent_content_collector,
60 Token_Manager $token_manager,
61 Request_Handler $request_handler,
62 Consent_Handler $consent_handler
63 ) {
64 $this->recent_content_collector = $recent_content_collector;
65 $this->token_manager = $token_manager;
66 $this->request_handler = $request_handler;
67 $this->consent_handler = $consent_handler;
68 }
69
70 /**
71 * Handles the content outline command by collecting recent content and requesting an outline from the AI API.
72 *
73 * @param Content_Outline_Command $command The content outline command.
74 * @param bool $retry_on_unauthorized Whether to retry on unauthorized response.
75 *
76 * @throws Unauthorized_Exception When the API returns an unauthorized response and retry is exhausted.
77 * @throws Forbidden_Exception When consent has been revoked.
78 *
79 * @return Section_List A list of outline sections.
80 */
81 public function handle(
82 Content_Outline_Command $command,
83 bool $retry_on_unauthorized = true
84 ): Section_List {
85 $recent_content = $command->get_recent_content();
86 $about_page = $this->recent_content_collector->collect_about_page( $command->get_post_type() );
87 try {
88 $token = $this->token_manager->get_or_request_access_token( $command->get_user() );
89 } catch ( Forbidden_Exception $exception ) {
90 // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
91 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
92 $this->consent_handler->revoke_consent( $command->get_user()->ID );
93 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
94 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
95 }
96
97 $existing_posts = \array_map(
98 static function ( $post ) {
99 return [
100 'title' => $post['title'],
101 'description' => $post['description'],
102 ];
103 },
104 $recent_content,
105 );
106
107 $content = [
108 'new_post_metadata' => [
109 'title' => $command->get_title(),
110 'intent' => $command->get_intent(),
111 'explanation' => $command->get_explanation(),
112 'keyphrase' => $command->get_keyphrase(),
113 'meta_description' => $command->get_meta_description(),
114 'category' => $command->get_category()->to_array(),
115 ],
116 'existing_posts' => $existing_posts,
117 ];
118 if ( $about_page ) {
119 $content['about_page'] = $about_page;
120 }
121
122 $request_body = [
123 'subject' => [
124 'language' => $command->get_language(),
125 'content' => $content,
126 ],
127 ];
128
129 $request_headers = [
130 'Authorization' => "Bearer $token",
131 'X-Yst-Cohort' => $command->get_editor(),
132 ];
133
134 try {
135 $response = $this->request_handler->handle( new Request( '/content-planner/next-post-outline', $request_body, $request_headers ) );
136 } catch ( Unauthorized_Exception $exception ) {
137 // Delete the stored JWT tokens, as they appear to be no longer valid.
138 $this->token_manager->clear_tokens( (string) $command->get_user()->ID );
139
140 if ( ! $retry_on_unauthorized ) {
141 throw $exception;
142 }
143
144 // Try again once more by fetching a new set of tokens and trying the outline endpoint again.
145 return $this->handle( $command, false );
146 } catch ( Forbidden_Exception $exception ) {
147 // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
148 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
149 $this->consent_handler->revoke_consent( $command->get_user()->ID );
150 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
151 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
152 }
153
154 return $this->build_outline( $response );
155 }
156
157 /**
158 * Builds a list of outline sections from the API response.
159 *
160 * @param Response $response The API response.
161 *
162 * @return Section_List The list of outline sections.
163 */
164 private function build_outline( Response $response ): Section_List {
165 $section_list = new Section_List();
166 $json = \json_decode( $response->get_body() );
167
168 if ( $json === null || ! isset( $json->choices ) ) {
169 return $section_list;
170 }
171 foreach ( $json->choices as $choice ) {
172 $section_list->add(
173 new Section(
174 ( $choice->content_notes ?? [] ),
175 ( $choice->subheading_text ?? null ),
176 ),
177 );
178 }
179
180 return $section_list;
181 }
182 }
183