• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / da4efbc5b07d9422360e8da09157e996aa6d4c8e

13 May 2026 09:45AM UTC coverage: 50.158%. First build
da4efbc5b07d9422360e8da09157e996aa6d4c8e

Pull #23265

github

web-flow
Merge c231415c9 into b17f347fb
Pull Request #23265: feat: authenticate yoast-ai requests with MyYoast OAuth tokens

129 of 246 new or added lines in 16 files covered. (52.44%)

20769 of 41407 relevant lines covered (50.16%)

4.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

32.08
/src/ai/content-planner/application/content-suggestion-command-handler.php
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\Authentication\Application\AI_Request_Sender_Factory;
7
use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
8
use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion;
9
use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion_List;
10
use Yoast\WP\SEO\AI\Content_Planner\Infrastructure\Recent_Content\Recent_Content_Collector;
11
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
12
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Insufficient_Scope_Exception;
13
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\OAuth_Forbidden_Exception;
14
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
15
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
16
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Response;
17

18
/**
19
 * Handles the content suggestion command.
20
 */
21
class Content_Suggestion_Command_Handler {
22

23
        /**
24
         * The recent content collector.
25
         *
26
         * @var Recent_Content_Collector
27
         */
28
        private $recent_content_collector;
29

30
        /**
31
         * The auth strategy factory.
32
         *
33
         * @var AI_Request_Sender_Factory
34
         */
35
        private $ai_request_sender_factory;
36

37
        /**
38
         * The consent handler.
39
         *
40
         * @var Consent_Handler
41
         */
42
        private $consent_handler;
43

44
        /**
45
         * The category repository.
46
         *
47
         * @var Category_Repository_Interface
48
         */
49
        private $category_repository;
50

51
        /**
52
         * The constructor.
53
         *
54
         * @param Recent_Content_Collector      $recent_content_collector  The recent content collector.
55
         * @param AI_Request_Sender_Factory     $ai_request_sender_factory The auth strategy factory.
56
         * @param Consent_Handler               $consent_handler           The consent handler.
57
         * @param Category_Repository_Interface $category_repository       The category repository.
58
         */
59
        public function __construct(
×
60
                Recent_Content_Collector $recent_content_collector,
61
                AI_Request_Sender_Factory $ai_request_sender_factory,
62
                Consent_Handler $consent_handler,
63
                Category_Repository_Interface $category_repository
64
        ) {
NEW
65
                $this->recent_content_collector  = $recent_content_collector;
×
NEW
66
                $this->ai_request_sender_factory = $ai_request_sender_factory;
×
NEW
67
                $this->consent_handler           = $consent_handler;
×
NEW
68
                $this->category_repository       = $category_repository;
×
69
        }
70

71
        // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't track exceptions thrown by called services.
72

73
        /**
74
         * Handles the content suggestion command by collecting recent content and requesting suggestions from the AI API.
75
         *
76
         * @param Content_Suggestion_Command $command The content suggestion command.
77
         *
78
         * @throws Unauthorized_Exception        When the API returns an unauthorized response and retry is exhausted.
79
         * @throws Forbidden_Exception           When consent has been revoked.
80
         * @throws Insufficient_Scope_Exception  When the OAuth path's token is missing the required scope.
81
         * @throws OAuth_Forbidden_Exception     When yoast-ai returns a non-scope 403 on the OAuth wire.
82
         *
83
         * @return Content_Suggestion_List A list of content suggestions.
84
         */
NEW
85
        public function handle( Content_Suggestion_Command $command ): Content_Suggestion_List {
×
86
                $recent_content = $this->recent_content_collector->collect( $command->get_post_type() );
×
87
                $about_page     = $this->recent_content_collector->collect_about_page( $command->get_post_type() );
×
88
                $recent_content = $recent_content->to_array();
×
89

90
                $content = [
×
91
                        'posts' => $recent_content,
×
92
                ];
×
93
                if ( $about_page ) {
×
94
                        $content['about_page'] = $about_page;
×
95
                }
96
                $request_body = [
×
97
                        'subject' => [
×
98
                                'language' => $command->get_language(),
×
99
                                'content'  => $content,
×
100
                        ],
×
101
                ];
×
102

103
                try {
NEW
104
                        $sender   = $this->ai_request_sender_factory->create( $command->get_user() );
×
NEW
105
                        $response = $sender->send(
×
NEW
106
                                new Request(
×
NEW
107
                                        '/content-planner/next-post-suggestions',
×
NEW
108
                                        $request_body,
×
NEW
109
                                        [ 'X-Yst-Cohort' => $command->get_editor() ],
×
NEW
110
                                ),
×
NEW
111
                                $command->get_user(),
×
NEW
112
                        );
×
NEW
113
                } catch ( Insufficient_Scope_Exception | OAuth_Forbidden_Exception $exception ) {
×
114
                        // OAuth-side 4xxs are deployment/policy problems, not consent revocation.
NEW
115
                        throw $exception;
×
116
                } catch ( Forbidden_Exception $exception ) {
×
117
                        // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
118
                        // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
119
                        $this->consent_handler->revoke_consent( $command->get_user()->ID );
×
120
                        throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
×
121
                        // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
122
                }
123

124
                return $this->build_suggestions_array( $response );
×
125
        }
126

127
        /**
128
         * Builds a list of content suggestions from the API response.
129
         *
130
         * @param Response $response The API response.
131
         *
132
         * @return Content_Suggestion_List The list of content suggestions.
133
         */
134
        public function build_suggestions_array( Response $response ): Content_Suggestion_List {
6✔
135
                $content_suggestion_list = new Content_Suggestion_List();
6✔
136
                $json                    = \json_decode( $response->get_body() );
6✔
137

138
                if ( $json === null || ! isset( $json->choices ) ) {
6✔
139
                        return $content_suggestion_list;
×
140
                }
141
                foreach ( $json->choices as $suggestion ) {
6✔
142
                        $category = $this->category_repository->find_by_name( $suggestion->category->name );
6✔
143

144
                        $content_suggestion_list->add(
6✔
145
                                new Content_Suggestion(
6✔
146
                                        $suggestion->title,
6✔
147
                                        $suggestion->intent,
6✔
148
                                        $suggestion->explanation,
6✔
149
                                        $suggestion->keyphrase,
6✔
150
                                        $suggestion->meta_description,
6✔
151
                                        $category,
6✔
152
                                ),
6✔
153
                        );
6✔
154
                }
155

156
                return $content_suggestion_list;
6✔
157
        }
158
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc