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

Yoast / wordpress-seo / c905cd28ff546151fcfc5c40343d53642f3a5bde

30 Jul 2026 10:22AM UTC coverage: 55.677% (+0.02%) from 55.653%
c905cd28ff546151fcfc5c40343d53642f3a5bde

Pull #23524

github

web-flow
Merge 0c187d05a into 9bc3dc8ba
Pull Request #23524: 1376 bulk editor collect ai prompt content client side via yoastseo instead of the php mirror

10658 of 18902 branches covered (56.39%)

Branch coverage included in aggregate %.

80 of 96 new or added lines in 8 files covered. (83.33%)

40537 of 73048 relevant lines covered (55.49%)

41200.55 hits per line

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

95.12
/src/bulk-editor/user-interface/posts-content-route.php
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_REST_Request;
7
use WP_REST_Response;
8
use Yoast\WP\SEO\Bulk_Editor\Application\Updates\Post_Access_Checker_Interface;
9
use Yoast\WP\SEO\Conditionals\No_Conditionals;
10
use Yoast\WP\SEO\Main;
11
use Yoast\WP\SEO\Routes\Route_Interface;
12

13
/**
14
 * Registers a route that returns the raw content of the requested posts.
15
 *
16
 * Serves the AI bulk suggestions flow, which collects each post's prompt content in the browser (so it runs through
17
 * the same analysis engine as the in-editor AI generator) and therefore needs the unrendered `post_content`. It is
18
 * deliberately separate from the posts route: the table listing does not need content, and keeping it out of that
19
 * payload keeps page loads lean.
20
 */
21
class Posts_Content_Route implements Route_Interface {
22

23
        use No_Conditionals;
24

25
        /**
26
         * The namespace for this route.
27
         *
28
         * @var string
29
         */
30
        public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
31

32
        /**
33
         * The prefix for this route.
34
         *
35
         * @var string
36
         */
37
        public const ROUTE_PREFIX = '/bulk_editor/posts_content';
38

39
        /**
40
         * The maximum number of posts whose content can be requested at once.
41
         *
42
         * Matches the maximum number of posts a single AI bulk suggestions request accepts, since that is the only
43
         * consumer: there is never a reason to ask for more in one call.
44
         *
45
         * @var int
46
         */
47
        public const MAX_IDS = 20;
48

49
        /**
50
         * The post access checker.
51
         *
52
         * @var Post_Access_Checker_Interface
53
         */
54
        private $post_access_checker;
55

56
        /**
57
         * The constructor.
58
         *
59
         * @param Post_Access_Checker_Interface $post_access_checker The post access checker.
60
         */
NEW
61
        public function __construct( Post_Access_Checker_Interface $post_access_checker ) {
×
NEW
62
                $this->post_access_checker = $post_access_checker;
×
63
        }
64

65
        /**
66
         * Registers routes with WordPress.
67
         *
68
         * @return void
69
         */
70
        public function register_routes() {
2✔
71
                \register_rest_route(
2✔
72
                        self::ROUTE_NAMESPACE,
2✔
73
                        self::ROUTE_PREFIX,
2✔
74
                        [
2✔
75
                                'methods'             => 'GET',
2✔
76
                                'args'                => [
2✔
77
                                        'ids' => [
2✔
78
                                                'required'    => true,
2✔
79
                                                'type'        => 'array',
2✔
80
                                                'minItems'    => 1,
2✔
81
                                                'maxItems'    => self::MAX_IDS,
2✔
82
                                                'items'       => [
2✔
83
                                                        'type'    => 'integer',
2✔
84
                                                        'minimum' => 1,
2✔
85
                                                ],
2✔
86
                                                'description' => 'The IDs of the posts to return the content of. Accepts a comma separated list.',
2✔
87
                                        ],
2✔
88
                                ],
2✔
89
                                'callback'            => [ $this, 'get_posts_content' ],
2✔
90
                                'permission_callback' => [ $this, 'check_permissions' ],
2✔
91
                        ],
2✔
92
                );
2✔
93
        }
94

95
        /**
96
         * Returns the raw content of the requested posts.
97
         *
98
         * Posts that no longer exist, are not of an editable type, or that the current user may not edit are omitted
99
         * from the response rather than failing the request: one inaccessible post out of a selection should not cost
100
         * the caller the whole batch. The caller treats an absent ID as "no content available" for that row.
101
         *
102
         * @param WP_REST_Request $request The request object.
103
         *
104
         * @return WP_REST_Response The posts and their raw content.
105
         */
106
        public function get_posts_content( WP_REST_Request $request ): WP_REST_Response {
16✔
107
                $post_ids = \array_unique( \array_map( '\intval', (array) $request->get_param( 'ids' ) ) );
16✔
108

109
                $posts = [];
16✔
110
                foreach ( $post_ids as $post_id ) {
16✔
111
                        if ( ! $this->post_access_checker->exists( $post_id )
16✔
112
                                || ! $this->post_access_checker->is_supported_type( $post_id )
14✔
113
                                || ! $this->post_access_checker->can_edit( $post_id )
16✔
114
                        ) {
115
                                continue;
8✔
116
                        }
117

118
                        $post = \get_post( $post_id );
14✔
119

120
                        $posts[] = [
14✔
121
                                'id'      => $post_id,
14✔
122
                                // The stored content, unrendered: the client parses it with the analysis engine, exactly as the
123
                                // post editor does, so blocks and shortcodes are handled there rather than by render filters here.
124
                                'content' => (string) $post->post_content,
14✔
125
                        ];
14✔
126
                }
127

128
                return new WP_REST_Response( [ 'posts' => $posts ] );
16✔
129
        }
130

131
        /**
132
         * Checks whether the current user is allowed to use the bulk editor.
133
         *
134
         * The per-post edit capability is enforced per requested ID in {@see get_posts_content()}.
135
         *
136
         * @return bool Whether the current user is allowed to use the bulk editor.
137
         */
138
        public function check_permissions(): bool {
4✔
139
                return \current_user_can( 'wpseo_manage_options' );
4✔
140
        }
141
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc