• 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

91.43
/src/bulk-editor/user-interface/bulk-editor-integration.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 WPSEO_Admin_Asset_Manager;
7
use Yoast\WP\SEO\Bulk_Editor\Application\Content_Types\Content_Types_Repository;
8
use Yoast\WP\SEO\Bulk_Editor\Application\Endpoints\Endpoints_Repository;
9
use Yoast\WP\SEO\Bulk_Editor\Infrastructure\Nonces\Nonce_Repository;
10
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
11
use Yoast\WP\SEO\General\User_Interface\General_Page_Integration;
12
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
13
use Yoast\WP\SEO\Helpers\Options_Helper;
14
use Yoast\WP\SEO\Helpers\Product_Helper;
15
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
16
use Yoast\WP\SEO\Integrations\Integration_Interface;
17
use Yoast\WP\SEO\MyYoast_Client\User_Interface\Myyoast_Connection_Data_Presenter;
18

19
/**
20
 * Adds the bulk editor page to the Yoast admin menu.
21
 */
22
class Bulk_Editor_Integration implements Integration_Interface {
23

24
        /**
25
         * The page name.
26
         */
27
        public const PAGE = 'wpseo_page_bulk_edit';
28

29
        /**
30
         * The assets name.
31
         */
32
        public const ASSETS_NAME = 'bulk-editor-page';
33

34
        /**
35
         * Holds the WPSEO_Admin_Asset_Manager.
36
         *
37
         * @var WPSEO_Admin_Asset_Manager
38
         */
39
        private $asset_manager;
40

41
        /**
42
         * Holds the Current_Page_Helper.
43
         *
44
         * @var Current_Page_Helper
45
         */
46
        private $current_page_helper;
47

48
        /**
49
         * Holds the Product_Helper.
50
         *
51
         * @var Product_Helper
52
         */
53
        private $product_helper;
54

55
        /**
56
         * Holds the Short_Link_Helper.
57
         *
58
         * @var Short_Link_Helper
59
         */
60
        private $short_link_helper;
61

62
        /**
63
         * Holds the Content_Types_Repository.
64
         *
65
         * @var Content_Types_Repository
66
         */
67
        private $content_types_repository;
68

69
        /**
70
         * Holds the Nonce_Repository.
71
         *
72
         * @var Nonce_Repository
73
         */
74
        private $nonce_repository;
75

76
        /**
77
         * Holds the Endpoints_Repository.
78
         *
79
         * @var Endpoints_Repository
80
         */
81
        private $endpoints_repository;
82

83
        /**
84
         * Holds the Options_Helper.
85
         *
86
         * @var Options_Helper
87
         */
88
        private $options_helper;
89

90
        /**
91
         * Builds the MyYoast connection payload for script data.
92
         *
93
         * @var Myyoast_Connection_Data_Presenter
94
         */
95
        private $myyoast_connection_data_presenter;
96

97
        /**
98
         * Constructs the instance.
99
         *
100
         * @param WPSEO_Admin_Asset_Manager         $asset_manager                     The WPSEO_Admin_Asset_Manager.
101
         * @param Current_Page_Helper               $current_page_helper               The Current_Page_Helper.
102
         * @param Product_Helper                    $product_helper                    The Product_Helper.
103
         * @param Short_Link_Helper                 $short_link_helper                 The Short_Link_Helper.
104
         * @param Content_Types_Repository          $content_types_repository          The Content_Types_Repository.
105
         * @param Nonce_Repository                  $nonce_repository                  The Nonce_Repository.
106
         * @param Endpoints_Repository              $endpoints_repository              The Endpoints_Repository.
107
         * @param Options_Helper                    $options_helper                    The Options_Helper.
108
         * @param Myyoast_Connection_Data_Presenter $myyoast_connection_data_presenter The MyYoast connection data presenter.
109
         */
110
        public function __construct(
2✔
111
                WPSEO_Admin_Asset_Manager $asset_manager,
112
                Current_Page_Helper $current_page_helper,
113
                Product_Helper $product_helper,
114
                Short_Link_Helper $short_link_helper,
115
                Content_Types_Repository $content_types_repository,
116
                Nonce_Repository $nonce_repository,
117
                Endpoints_Repository $endpoints_repository,
118
                Options_Helper $options_helper,
119
                Myyoast_Connection_Data_Presenter $myyoast_connection_data_presenter
120
        ) {
121
                $this->asset_manager                     = $asset_manager;
2✔
122
                $this->current_page_helper               = $current_page_helper;
2✔
123
                $this->product_helper                    = $product_helper;
2✔
124
                $this->short_link_helper                 = $short_link_helper;
2✔
125
                $this->content_types_repository          = $content_types_repository;
2✔
126
                $this->nonce_repository                  = $nonce_repository;
2✔
127
                $this->endpoints_repository              = $endpoints_repository;
2✔
128
                $this->options_helper                    = $options_helper;
2✔
129
                $this->myyoast_connection_data_presenter = $myyoast_connection_data_presenter;
2✔
130
        }
131

132
        /**
133
         * Returns the conditionals based on which this loadable should be active.
134
         *
135
         * @return array<string> The conditionals.
136
         */
137
        public static function get_conditionals() {
2✔
138
                return [ Admin_Conditional::class ];
2✔
139
        }
140

141
        /**
142
         * Initializes the integration.
143
         *
144
         * This is the place to register hooks and filters.
145
         *
146
         * @return void
147
         */
148
        public function register_hooks() {
4✔
149
                \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] );
4✔
150

151
                // Hide the menu item without losing the page. See remove_menu_item() for why this runs on admin_head.
152
                \add_action( 'admin_head', [ $this, 'remove_menu_item' ] );
4✔
153

154
                // Are we on our page?
155
                if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) {
4✔
156
                        \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
2✔
157
                        \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX );
2✔
158
                }
159
        }
160

161
        /**
162
         * Removes the bulk editor's submenu item from the Yoast SEO menu while keeping the page reachable by URL.
163
         *
164
         * Runs on admin_head rather than admin_menu on purpose: by then WordPress has already resolved the page's
165
         * parent and capability (so the page stays accessible and keeps its `seo_page_wpseo_page_bulk_edit` body
166
         * class, which its styles depend on), but the menu HTML has not been rendered yet, so the item is hidden.
167
         * The page is opened from the Tools page instead of its own menu item.
168
         *
169
         * @return void
170
         */
171
        public function remove_menu_item() {
×
172
                \remove_submenu_page( 'wpseo_dashboard', self::PAGE );
×
173
        }
174

175
        /**
176
         * Adds the page to the (currently) last position in the array.
177
         *
178
         * @param array<array<string|callable|null>> $pages The pages.
179
         *
180
         * @return array<array<string|callable|null>> The pages.
181
         */
182
        public function add_page( $pages ) {
2✔
183
                $pages[] = [
2✔
184
                        'wpseo_dashboard',
2✔
185
                        '',
2✔
186
                        \__( 'Bulk editor', 'wordpress-seo' ),
2✔
187
                        'wpseo_manage_options',
2✔
188
                        self::PAGE,
2✔
189
                        [ $this, 'display_page' ],
2✔
190
                ];
2✔
191

192
                return $pages;
2✔
193
        }
194

195
        /**
196
         * Displays the page.
197
         *
198
         * @return void
199
         */
200
        public function display_page() {
2✔
201
                echo '<div id="yoast-seo-bulk-editor"></div>';
2✔
202
        }
203

204
        /**
205
         * Enqueues the assets.
206
         *
207
         * @return void
208
         */
209
        public function enqueue_assets() {
2✔
210
                // Remove the emoji script as it is incompatible with both React and any contenteditable fields.
211
                \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
2✔
212
                $this->asset_manager->enqueue_script( self::ASSETS_NAME );
2✔
213
                $this->asset_manager->enqueue_style( self::ASSETS_NAME );
2✔
214
                $this->asset_manager->localize_script( self::ASSETS_NAME, 'wpseoBulkEditorData', $this->get_script_data() );
2✔
215
        }
216

217
        /**
218
         * Creates the script data.
219
         *
220
         * @return array<string, string|array<string, string|bool|array<string, string>>> The script data.
221
         */
222
        public function get_script_data() {
18✔
223
                return [
18✔
224
                        'contentTypes'      => $this->content_types_repository->get_content_types(),
18✔
225
                        'endpoints'         => $this->endpoints_repository->get_all_endpoints()->to_array(),
18✔
226
                        // These must stay server-generated URLs: the bulk editor assigns them to window.location.href for its
227
                        // "Back to Tools" / logo navigation. If a link ever derives from request input, validate it with
228
                        // wp_validate_redirect() here before exposing it, to avoid an open redirect on the front-end.
229
                        'links'             => [
18✔
230
                                'dashboard' => \admin_url( 'admin.php?page=' . General_Page_Integration::PAGE ),
18✔
231
                                'tools'     => \admin_url( 'admin.php?page=wpseo_tools' ),
18✔
232
                        ],
18✔
233
                        'nonce'             => $this->nonce_repository->get_rest_nonce(),
18✔
234
                        'restRoot'          => \esc_url_raw( \rest_url() ),
18✔
235
                        'preferences'       => [
18✔
236
                                'isPremium'   => $this->product_helper->is_premium(),
18✔
237
                                'isAiEnabled' => $this->options_helper->get( 'enable_ai_generator' ) === true,
18✔
238
                                'isRtl'       => \is_rtl(),
18✔
239
                                'pluginUrl'   => \plugins_url( '', \WPSEO_FILE ),
18✔
240
                        ],
18✔
241
                        'linkParams'        => $this->short_link_helper->get_query_params(),
18✔
242
                        'analysis'          => [
18✔
243
                                'contentLocale'         => \get_locale(),
18✔
244
                                // Re-scoring only runs when SEO analysis is enabled, matching the post editor.
245
                                'keywordAnalysisActive' => $this->options_helper->get( 'keyword_analysis_active' ) === true,
18✔
246
                                // Used when collecting the AI prompt content, so shortcode delimiters are stripped from the
247
                                // text while the content they enclose is kept. Not gated on SEO analysis being enabled: the
248
                                // prompt content is collected for AI suggestions, which do not depend on the analysis.
249
                                'shortcodes'            => $this->get_valid_shortcode_tags(),
18✔
250
                        ],
18✔
251
                        'myyoastConnection' => $this->myyoast_connection_data_presenter->present(),
18✔
252
                ];
18✔
253
        }
254

255
        /**
256
         * Returns the tags of all registered shortcodes.
257
         *
258
         * Mirrors what the post editor passes to the analysis (see WPSEO_Metabox::get_valid_shortcode_tags()), so the
259
         * parse tree treats shortcodes the same on both pages: the delimiters are removed and the text an enclosing
260
         * shortcode wraps is kept. Without this list the raw brackets stay in the text and consume prompt tokens.
261
         *
262
         * @return array<string> The registered shortcode tags.
263
         */
NEW
264
        private function get_valid_shortcode_tags(): array {
×
265
                // The global is always set by WordPress, but stay defensive: an empty list only costs shortcode parity.
NEW
266
                if ( ! isset( $GLOBALS['shortcode_tags'] ) || ! \is_array( $GLOBALS['shortcode_tags'] ) ) {
×
NEW
267
                        return [];
×
268
                }
269

NEW
270
                return \array_keys( $GLOBALS['shortcode_tags'] );
×
271
        }
272

273
        /**
274
         * Removes all current WP notices.
275
         *
276
         * @return void
277
         */
278
        public function remove_notices() {
2✔
279
                \remove_all_actions( 'admin_notices' );
2✔
280
                \remove_all_actions( 'user_admin_notices' );
2✔
281
                \remove_all_actions( 'network_admin_notices' );
2✔
282
                \remove_all_actions( 'all_admin_notices' );
2✔
283
        }
284
}
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