PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.3
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.3
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-blaze / src / class-blaze.php
jetpack / jetpack_vendor / automattic / jetpack-blaze / src Last commit date
class-blaze.php 1 month ago class-dashboard-config-data.php 1 month ago class-dashboard-rest-controller.php 1 month ago class-dashboard.php 2 weeks ago class-rest-controller.php 1 month ago
class-blaze.php
887 lines
1 <?php
2 /**
3 * Attract high-quality traffic to your site.
4 *
5 * @package automattic/jetpack-blaze
6 */
7
8 namespace Automattic\Jetpack;
9
10 use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 use Automattic\Jetpack\Blaze\Dashboard as Blaze_Dashboard;
12 use Automattic\Jetpack\Blaze\Dashboard_REST_Controller as Blaze_Dashboard_REST_Controller;
13 use Automattic\Jetpack\Blaze\REST_Controller;
14 use Automattic\Jetpack\Connection\Client;
15 use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
16 use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
17 use Automattic\Jetpack\Status\Host;
18 use Automattic\Jetpack\Sync\Settings as Sync_Settings;
19 use WP_Post;
20
21 /**
22 * Class for promoting posts.
23 */
24 class Blaze {
25 /**
26 * Script handle for the JS file we enqueue in the post editor.
27 *
28 * @var string
29 */
30 const SCRIPT_HANDLE = 'jetpack-promote-editor';
31
32 /**
33 * Transient prefix for active campaign status checks.
34 *
35 * @var string
36 */
37 const ACTIVE_CAMPAIGNS_STATUS_TRANSIENT_PREFIX = 'jetpack_blaze_active_campaigns_status_';
38
39 /**
40 * Transient TTL for active campaign status checks that find campaigns.
41 *
42 * @var int
43 */
44 const ACTIVE_CAMPAIGNS_STATUS_TRANSIENT_TTL = HOUR_IN_SECONDS;
45
46 /**
47 * Transient TTL for active campaign status checks that cannot determine campaign status.
48 *
49 * @var int
50 */
51 const UNKNOWN_ACTIVE_CAMPAIGNS_STATUS_TRANSIENT_TTL = 5 * MINUTE_IN_SECONDS;
52
53 /**
54 * Minimum campaign statuses that should trigger a warning.
55 *
56 * @var string[]
57 */
58 const ACTIVE_CAMPAIGN_STATUSES = array( 'active' );
59
60 /**
61 * Path of the JS file we enqueue in the post editor.
62 *
63 * @var string
64 */
65 public static $script_path = '../build/editor.js';
66
67 /**
68 * Initializer.
69 * Used to configure the blaze package, eg when called via the Config package.
70 *
71 * @return void
72 */
73 public static function init() {
74 // On the edit screen, add a row action to promote the post.
75 add_action( 'load-edit.php', array( __CLASS__, 'add_post_links_actions' ) );
76 // After the quick-edit screen is processed, ensure the blaze row action is still present
77 if ( 'edit.php' === $GLOBALS['pagenow'] ||
78 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is not needed here, we're not saving anything.
79 ( 'admin-ajax.php' === $GLOBALS['pagenow'] && ! empty( $_POST['post_view'] ) && 'list' === $_POST['post_view'] && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] ) ) {
80 self::add_post_links_actions();
81 }
82 // In the post editor, add a post-publish panel to allow promoting the post.
83 add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_editor_assets' ) );
84 // Add a Blaze Menu.
85 add_action( 'admin_menu', array( __CLASS__, 'enable_blaze_menu' ), 999 );
86 // Redirect legacy/duplicate advertising URLs (the old tools.php location, or the
87 // admin.php?page=advertising slug when the standalone Blaze Ads plugin owns the
88 // menu). Runs late (after WooCommerce and core register their menus, so
89 // get_menu_parent() resolves the real parent) but still before WordPress
90 // validates the page parameter.
91 add_action( 'admin_menu', array( __CLASS__, 'redirect_legacy_advertising_url' ), 999 );
92 // Add Blaze dashboard app REST API endpoints.
93 add_action( 'rest_api_init', array( Blaze_Dashboard_REST_Controller::class, 'register' ) );
94 // Add general Blaze REST API endpoints.
95 add_action( 'rest_api_init', array( REST_Controller::class, 'register' ) );
96 }
97
98 /**
99 * Add links under each published post in the wp-admin post list.
100 *
101 * @return void
102 */
103 public static function add_post_links_actions() {
104 if ( self::should_initialize()['can_init'] ) {
105 add_filter( 'post_row_actions', array( __CLASS__, 'jetpack_blaze_row_action' ), 10, 2 );
106 add_filter( 'page_row_actions', array( __CLASS__, 'jetpack_blaze_row_action' ), 10, 2 );
107 }
108 }
109
110 /**
111 * Is the wp-admin Dashboard enabled?
112 * That dashboard is not available or necessary on WordPress.com sites when the nav redesign is disabled.
113 *
114 * @return bool
115 */
116 public static function is_dashboard_enabled() {
117 $is_dashboard_enabled = true;
118
119 // On WordPress.com sites, the dashboard is not needed if the nav redesign is not enabled.
120 if ( get_option( 'wpcom_admin_interface' ) !== 'wp-admin' && ( new Host() )->is_wpcom_platform() ) {
121 $is_dashboard_enabled = false;
122 }
123
124 /**
125 * Enable a wp-admin dashboard for Blaze campaign management.
126 *
127 * @since 0.7.0
128 *
129 * @param bool $should_enable Should the dashboard be enabled?
130 */
131 return apply_filters( 'jetpack_blaze_dashboard_enable', $is_dashboard_enabled );
132 }
133
134 /**
135 * Enable the Blaze menu.
136 *
137 * @return void
138 */
139 public static function enable_blaze_menu() {
140 if ( ! self::should_initialize()['can_init'] ) {
141 return;
142 }
143
144 /**
145 * Filter the menu page slug used for the Blaze dashboard.
146 *
147 * @since 0.28.0
148 *
149 * @param string $menu_slug The menu page slug. Default 'advertising'.
150 */
151 $menu_slug = apply_filters( 'jetpack_blaze_menu_slug', 'advertising' );
152
153 // Avoid a duplicate Blaze Ads menu when the standalone Blaze Ads plugin
154 // (Automattic/blaze-ads) is also active. Releases <= 0.9.0 register their own
155 // menu and leave our slug at the default 'advertising', so we bail to avoid a
156 // second entry; the standalone owns the menu under 'wp-blaze' and
157 // redirect_legacy_advertising_url() forwards 'advertising' links there. When the
158 // standalone instead delegates registration to us by filtering the slug (e.g. to
159 // 'wp-blaze'), we are the only registrant and must register normally -- the
160 // non-default slug is what tells the two cases apart.
161 if ( 'advertising' === $menu_slug && self::is_standalone_blaze_ads_active() ) {
162 return;
163 }
164
165 /**
166 * Filter the menu label for the Blaze dashboard menu item.
167 *
168 * @since 0.28.0
169 *
170 * @param string $menu_label The menu label. Default 'Blaze Ads'.
171 */
172 $menu_label = apply_filters( 'jetpack_blaze_menu_label', 'Blaze Ads' ); // Product name, do not translate.
173
174 /**
175 * Filter the CSS class prefix for the Blaze dashboard.
176 *
177 * @since 0.28.0
178 *
179 * @param string $css_prefix The CSS class prefix. Default 'jp-blaze'.
180 */
181 $css_prefix = apply_filters( 'jetpack_blaze_dashboard_css_prefix', 'jp-blaze' );
182
183 $parent_slug = self::get_menu_parent();
184 $blaze_dashboard = new Blaze_Dashboard( 'admin.php', $menu_slug, $css_prefix );
185
186 if ( self::is_dashboard_enabled() || ( new Host() )->is_wpcom_platform() ) {
187 if ( 'jetpack' === $parent_slug ) {
188 // Register through Admin_Menu: on WordPress.com Simple sites the Jetpack
189 // parent menu does not exist yet at this priority, and registering the
190 // submenu before its parent produces a broken menu URL.
191 $page_suffix = Admin_Menu::add_menu(
192 esc_attr( $menu_label ),
193 $menu_label,
194 'manage_options',
195 $menu_slug,
196 array( $blaze_dashboard, 'render' ),
197 1
198 );
199 } else {
200 // Other parents already exist at this priority, so add_submenu_page is safe.
201 $page_suffix = add_submenu_page(
202 $parent_slug,
203 esc_attr( $menu_label ),
204 $menu_label,
205 'manage_options',
206 $menu_slug,
207 array( $blaze_dashboard, 'render' ),
208 1
209 );
210 }
211 add_action( 'load-' . $page_suffix, array( $blaze_dashboard, 'admin_init' ) );
212
213 // Temporary entry at the old Tools location; remove ~1 month after the move ships.
214 if ( 'tools.php' !== $parent_slug ) {
215 self::add_migration_notice_menu();
216 }
217 }
218 }
219
220 /**
221 * Detect whether the standalone Blaze Ads plugin (Automattic/blaze-ads) is active.
222 *
223 * Both this package and the standalone plugin can register a Blaze Ads menu, which
224 * would result in a duplicate entry when both are active on the same site (for
225 * example a WooCommerce store running Jetpack alongside the standalone plugin). When
226 * the standalone plugin is present it owns the menu, so this package defers to it.
227 *
228 * Detection relies on a class/constant defined by the standalone plugin rather than
229 * on any cooperation from it, so it works with every released version of the plugin,
230 * including ones that predate the jetpack_blaze_menu_* filters.
231 *
232 * @return bool True if the standalone Blaze Ads plugin is active.
233 */
234 public static function is_standalone_blaze_ads_active() {
235 $is_active = defined( 'BLAZEADS_PLUGIN_FILE' )
236 || defined( 'BLAZE_ADS_VERSION_NUMBER' )
237 || class_exists( 'Blaze_Ads' );
238
239 /**
240 * Filter whether the standalone Blaze Ads plugin (Automattic/blaze-ads) is
241 * considered active. When active and this package is still using the default
242 * menu slug, the package skips registering its own Blaze Ads menu to avoid a
243 * duplicate entry.
244 *
245 * @since 0.28.0
246 *
247 * @param bool $is_active Whether the standalone Blaze Ads plugin is active.
248 */
249 return (bool) apply_filters( 'jetpack_blaze_standalone_active', $is_active );
250 }
251
252 /**
253 * Determine the appropriate parent menu slug based on the installation context.
254 *
255 * - WooCommerce active (Marketing menu registered): parent is 'woocommerce-marketing'
256 * - WordPress.com platform or Jetpack connected: parent is 'jetpack'
257 * - Otherwise: parent is 'tools.php'
258 *
259 * @return string The parent menu slug.
260 */
261 public static function get_menu_parent() {
262 if ( class_exists( 'WooCommerce' ) ) {
263 // Only use woocommerce-marketing if the menu is actually registered.
264 global $menu;
265 foreach ( (array) $menu as $item ) {
266 if ( isset( $item[2] ) && 'woocommerce-marketing' === $item[2] ) {
267 /**
268 * Filter the parent menu slug for the Blaze dashboard submenu item.
269 *
270 * @since 0.28.0
271 *
272 * @param string $parent The parent menu slug.
273 */
274 return apply_filters( 'jetpack_blaze_menu_parent', 'woocommerce-marketing' );
275 }
276 }
277 }
278
279 if ( ( new Host() )->is_wpcom_platform() ) {
280 /** This filter is documented above. */
281 return apply_filters( 'jetpack_blaze_menu_parent', 'jetpack' );
282 }
283
284 if ( ( new Jetpack_Connection() )->is_connected() ) {
285 /** This filter is documented above. */
286 return apply_filters( 'jetpack_blaze_menu_parent', 'jetpack' );
287 }
288
289 /** This filter is documented above. */
290 return apply_filters( 'jetpack_blaze_menu_parent', 'tools.php' );
291 }
292
293 /**
294 * Register a temporary "moved" notice page at the old Tools menu location.
295 *
296 * The menu keeps its old "Advertising" label so users who knew it by that
297 * name still recognize it; the page itself explains the move to Blaze Ads.
298 *
299 * @return void
300 */
301 public static function add_migration_notice_menu() {
302 /**
303 * Filter whether to show the temporary migration notice at the old
304 * Tools > Advertising location.
305 *
306 * @since 0.28.0
307 *
308 * @param bool $show_notice Whether to show the migration notice. Default true.
309 */
310 if ( ! apply_filters( 'jetpack_blaze_show_migration_notice', true ) ) {
311 return;
312 }
313
314 add_submenu_page(
315 'tools.php',
316 esc_attr__( 'Advertising', 'jetpack-blaze' ),
317 __( 'Advertising', 'jetpack-blaze' ),
318 'manage_options',
319 'advertising-moved',
320 array( __CLASS__, 'render_migration_notice' ),
321 1
322 );
323 }
324
325 /**
326 * Render the temporary migration notice shown at the old Tools menu location.
327 *
328 * @return void
329 */
330 public static function render_migration_notice() {
331 /** This filter is documented in Blaze::enable_blaze_menu() */
332 $menu_slug = apply_filters( 'jetpack_blaze_menu_slug', 'advertising' );
333
334 $parent_slug = self::get_menu_parent();
335 $parent_label = 'woocommerce-marketing' === $parent_slug
336 ? __( 'Marketing', 'jetpack-blaze' )
337 : 'Jetpack';
338
339 $dashboard_url = admin_url( 'admin.php?page=' . $menu_slug );
340 $image_file = 'woocommerce-marketing' === $parent_slug
341 ? 'blaze-ads-moved-woo.webp'
342 : 'blaze-ads-moved.webp';
343 $image_path = dirname( __DIR__ ) . '/assets/images/' . $image_file;
344 $image_url = plugins_url( 'assets/images/' . $image_file, __DIR__ );
345
346 ?>
347 <div class="wrap blaze-ads-migration">
348 <div class="blaze-ads-migration__header">
349 <svg class="blaze-ads-migration__logo" xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
350 <path d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z" fill="#069e08" />
351 <polygon points="15,19 7,19 15,3" fill="#fff" />
352 <polygon points="17,29 17,13 25,13" fill="#fff" />
353 </svg>
354 <span class="blaze-ads-migration__brand">Blaze Ads</span>
355 </div>
356 <h1 class="blaze-ads-migration__title">
357 <?php
358 printf(
359 /* translators: %s is the product name (not translatable). */
360 esc_html__( '%s has moved', 'jetpack-blaze' ),
361 'Blaze Ads'
362 );
363 ?>
364 </h1>
365 <p class="blaze-ads-migration__subtitle">
366 <?php
367 printf(
368 /* translators: %s is the menu path where the section lives now, e.g. "Jetpack → Blaze Ads" or "Marketing → Blaze Ads". */
369 esc_html__( "Now it's part of %s", 'jetpack-blaze' ),
370 esc_html( $parent_label . ' → Blaze Ads' )
371 );
372 ?>
373 </p>
374 <p>
375 <a class="button button-primary button-hero" href="<?php echo esc_url( $dashboard_url ); ?>">
376 <?php
377 printf(
378 /* translators: %s is the product name (not translatable). */
379 esc_html__( 'Check new %s', 'jetpack-blaze' ),
380 'Blaze Ads'
381 );
382 ?>
383 </a>
384 </p>
385 <?php if ( file_exists( $image_path ) ) : ?>
386 <p class="blaze-ads-migration__image">
387 <img
388 src="<?php echo esc_url( $image_url ); ?>"
389 alt="<?php echo esc_attr( sprintf( /* translators: %s is the product name (not translatable). */ __( '%s has moved', 'jetpack-blaze' ), 'Blaze Ads' ) ); ?>"
390 />
391 </p>
392 <?php endif; ?>
393 </div>
394 <style>
395 .blaze-ads-migration__header { display: flex; align-items: center; gap: 8px; margin: 16px 0 24px; }
396 .blaze-ads-migration__brand { font-size: 1.65em; font-weight: 500; line-height: 1; }
397 .blaze-ads-migration__title { font-size: 2.5em; margin-bottom: 0.25em; padding: 0; }
398 .blaze-ads-migration__subtitle { font-size: 1.4em; margin-top: 0; color: #50575e; }
399 .blaze-ads-migration__image { margin-top: 3em; }
400 .blaze-ads-migration__image img { max-width: 100%; height: auto; }
401 </style>
402 <?php
403 }
404
405 /**
406 * Redirect legacy/duplicate Blaze dashboard URLs to wherever the menu actually lives.
407 *
408 * Runs early on admin_menu, before WordPress validates the ?page= parameter against
409 * registered submenus. The target is computed by get_legacy_advertising_redirect_target();
410 * this method only performs the redirect.
411 *
412 * @return void
413 */
414 public static function redirect_legacy_advertising_url() {
415 $target = self::get_legacy_advertising_redirect_target();
416 if ( null !== $target ) {
417 wp_safe_redirect( $target, 302 );
418 exit;
419 }
420 }
421
422 /**
423 * Compute where a legacy/duplicate `page=advertising` request should be redirected, or
424 * null if it should be left alone. Pure (no side effects) so it is unit-testable.
425 *
426 * Two cases produce a redirect:
427 *
428 * - Standard move: `tools.php?page=advertising` -> `admin.php?page=advertising`, once the
429 * menu has moved away from Tools.
430 * - Standalone Blaze Ads plugin present: this package does not register the 'advertising'
431 * page (the standalone owns the menu under 'wp-blaze'), so BOTH the tools.php and the
432 * admin.php `page=advertising` entry points are forwarded to the standalone's page. We
433 * forward both ourselves rather than relying on the standalone shipping its own
434 * redirect, so correctness never depends on a particular standalone release.
435 *
436 * @return string|null The redirect URL, or null if no redirect should happen.
437 */
438 public static function get_legacy_advertising_redirect_target() {
439 global $pagenow;
440
441 if (
442 ! isset( $_GET['page'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check to redirect, no data is processed.
443 || 'advertising' !== $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check to redirect, no data is processed.
444 ) {
445 return null;
446 }
447
448 if ( ! self::should_initialize()['can_init'] ) {
449 return null;
450 }
451
452 if ( self::is_standalone_blaze_ads_active() ) {
453 // Only the two URLs that can carry the dashboard page parameter.
454 if ( 'admin.php' !== $pagenow && 'tools.php' !== $pagenow ) {
455 return null;
456 }
457 /**
458 * Filter the standalone Blaze Ads plugin's menu slug used as the redirect
459 * target when this package defers to it.
460 *
461 * @since 0.28.0
462 *
463 * @param string $standalone_slug The standalone menu slug. Default 'wp-blaze'.
464 */
465 $standalone_slug = apply_filters( 'jetpack_blaze_standalone_menu_slug', 'wp-blaze' );
466 return admin_url( 'admin.php?page=' . $standalone_slug );
467 }
468
469 if ( 'tools.php' !== $pagenow || 'tools.php' === self::get_menu_parent() ) {
470 return null;
471 }
472
473 /** This filter is documented in Blaze::enable_blaze_menu() */
474 $menu_slug = apply_filters( 'jetpack_blaze_menu_slug', 'advertising' );
475 return admin_url( 'admin.php?page=' . $menu_slug );
476 }
477
478 /**
479 * Check the WordPress.com REST API
480 * to ensure that the site supports the Blaze feature.
481 *
482 * - If the site is on WordPress.com Simple, we do not query the API.
483 * - Results are cached for a day after getting response from API.
484 * - If the API returns an error, we cache the result for an hour.
485 *
486 * @param int $blog_id The blog ID to check.
487 *
488 * @return bool
489 */
490 public static function site_supports_blaze( $blog_id ) {
491 $transient_name = 'jetpack_blaze_site_supports_blaze_' . $blog_id;
492
493 /*
494 * On WordPress.com, we don't need to make an API request,
495 * we can query directly.
496 */
497 if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'blaze_is_site_eligible' ) ) {
498 return blaze_is_site_eligible( $blog_id );
499 }
500
501 $cached_result = get_transient( $transient_name );
502 if ( false !== $cached_result ) {
503 if ( is_array( $cached_result ) ) {
504 return $cached_result['approved'];
505 }
506
507 return (bool) $cached_result;
508 }
509
510 // Make the API request.
511 $url = sprintf( '/sites/%d/blaze/status', $blog_id );
512 $response = Client::wpcom_json_api_request_as_blog(
513 $url,
514 '2',
515 array( 'method' => 'GET' ),
516 null,
517 'wpcom'
518 );
519
520 // If there was an error or malformed response, bail and save response for an hour.
521 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
522 set_transient( $transient_name, array( 'approved' => false ), HOUR_IN_SECONDS );
523 return false;
524 }
525
526 // Decode the results.
527 $result = json_decode( wp_remote_retrieve_body( $response ), true );
528
529 // Bail if there were no results returned.
530 if ( ! is_array( $result ) || ! isset( $result['approved'] ) ) {
531 return false;
532 }
533
534 // Cache the result for 24 hours.
535 set_transient( $transient_name, array( 'approved' => (bool) $result['approved'] ), DAY_IN_SECONDS );
536
537 return (bool) $result['approved'];
538 }
539
540 /**
541 * Get the active campaign status for a site.
542 *
543 * @param int $blog_id The blog ID to check.
544 *
545 * @return array Active campaign status.
546 */
547 public static function get_active_campaigns_status( $blog_id ) {
548 $blog_id = absint( $blog_id );
549
550 if ( empty( $blog_id ) ) {
551 return self::get_unknown_active_campaigns_status();
552 }
553
554 $transient_name = self::ACTIVE_CAMPAIGNS_STATUS_TRANSIENT_PREFIX . $blog_id;
555 $cached_result = get_transient( $transient_name );
556
557 if ( false !== $cached_result ) {
558 return $cached_result;
559 }
560
561 /**
562 * Filter the campaign statuses that should trigger the active campaign warning.
563 *
564 * @since 0.27.23
565 *
566 * @param string[] $statuses Campaign statuses to check.
567 * @param int $blog_id The blog ID being checked.
568 */
569 $statuses = apply_filters( 'jetpack_blaze_active_campaign_statuses', self::ACTIVE_CAMPAIGN_STATUSES, $blog_id );
570 $statuses = array_filter( array_map( 'sanitize_key', (array) $statuses ) );
571
572 if ( empty( $statuses ) ) {
573 $statuses = self::ACTIVE_CAMPAIGN_STATUSES;
574 }
575
576 $url = add_query_arg(
577 array(
578 'status' => implode( ',', $statuses ),
579 'limit' => 1,
580 ),
581 sprintf( '/sites/%d/wordads/dsp/api/v1/search/campaigns/site/%d', $blog_id, $blog_id )
582 );
583
584 $response = Client::wpcom_json_api_request_as_user(
585 $url,
586 '2',
587 array( 'method' => 'GET' ),
588 null,
589 'wpcom'
590 );
591
592 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
593 return self::get_unknown_active_campaigns_status( $transient_name );
594 }
595
596 $result = json_decode( wp_remote_retrieve_body( $response ), true );
597
598 if ( ! is_array( $result ) || ! isset( $result['campaigns'] ) || ! is_array( $result['campaigns'] ) ) {
599 return self::get_unknown_active_campaigns_status( $transient_name );
600 }
601
602 $has_active_campaigns = ! empty( $result['campaigns'] );
603 $status = array(
604 'has_active_campaigns' => $has_active_campaigns,
605 'status' => $has_active_campaigns ? 'active' : 'none',
606 );
607
608 if ( $has_active_campaigns ) {
609 set_transient( $transient_name, $status, self::ACTIVE_CAMPAIGNS_STATUS_TRANSIENT_TTL );
610 }
611
612 return $status;
613 }
614
615 /**
616 * Get the unknown active campaign status response.
617 *
618 * @param string|null $transient_name Optional transient name used to cache the unknown status.
619 * @return array Unknown active campaign status.
620 */
621 private static function get_unknown_active_campaigns_status( $transient_name = null ) {
622 $status = array(
623 'has_active_campaigns' => false,
624 'status' => 'unknown',
625 );
626
627 if ( $transient_name ) {
628 set_transient( $transient_name, $status, self::UNKNOWN_ACTIVE_CAMPAIGNS_STATUS_TRANSIENT_TTL );
629 }
630
631 return $status;
632 }
633
634 /**
635 * Determines if criteria is met to enable Blaze features.
636 * Keep in mind that this makes remote requests, so we want to avoid calling it when unnecessary, like in the frontend.
637 *
638 * @return array
639 */
640 public static function should_initialize() {
641 $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
642 $connection = new Jetpack_Connection();
643 $site_id = Jetpack_Connection::get_site_id();
644
645 // Only admins should be able to Blaze posts on a site.
646 if ( ! current_user_can( 'manage_options' ) ) {
647 return array(
648 'can_init' => false,
649 'reason' => 'user_not_admin',
650 );
651 }
652
653 // Allow short-circuiting the Blaze initialization via a filter.
654 if ( has_filter( 'jetpack_blaze_enabled' ) ) {
655 /**
656 * Filter to disable all Blaze functionality.
657 *
658 * @since 0.3.0
659 *
660 * @param bool $should_initialize Whether Blaze should be enabled. Default to true.
661 */
662 $should_init = apply_filters( 'jetpack_blaze_enabled', true );
663
664 return array(
665 'can_init' => $should_init,
666 'reason' => $should_init ? null : 'initialization_disabled',
667 );
668 }
669
670 // On self-hosted sites, we must do some additional checks.
671 if ( ! $is_wpcom ) {
672 /*
673 * These features currently only work on WordPress.com,
674 * so the site must be connected to WordPress.com, and the user as well for things to work.
675 */
676 if (
677 is_wp_error( $site_id )
678 ) {
679 return array(
680 'can_init' => false,
681 'reason' => 'wp_error',
682 );
683 }
684
685 if ( ! $connection->is_connected() ) {
686 return array(
687 'can_init' => false,
688 'reason' => 'site_not_connected',
689 );
690 }
691
692 if ( ! $connection->is_user_connected() ) {
693 return array(
694 'can_init' => false,
695 'reason' => 'user_not_connected',
696 );
697 }
698
699 // The whole thing is powered by Sync!
700 if ( ! Sync_Settings::is_sync_enabled() ) {
701 return array(
702 'can_init' => false,
703 'reason' => 'sync_disabled',
704 );
705 }
706 }
707
708 // Check if the site supports Blaze.
709 if ( is_numeric( $site_id ) && ! self::site_supports_blaze( $site_id ) ) {
710 return array(
711 'can_init' => false,
712 'reason' => 'site_not_eligible',
713 );
714 }
715
716 // Final fallback.
717 return array(
718 'can_init' => true,
719 'reason' => null,
720 );
721 }
722
723 /**
724 * Get URL to create a Blaze campaign for a specific post.
725 *
726 * This can return 2 different types of URL:
727 * - Calypso Links
728 * - wp-admin Links if access to the wp-admin Blaze Dashboard is enabled.
729 *
730 * @param int|string $post_id Post ID.
731 *
732 * @return array An array with the link, and whether this is a Calypso or a wp-admin link.
733 */
734 public static function get_campaign_management_url( $post_id ) {
735 if ( self::is_dashboard_enabled() ) {
736 /** This filter is documented in Blaze::enable_blaze_menu() */
737 $menu_slug = apply_filters( 'jetpack_blaze_menu_slug', 'advertising' );
738
739 // When the standalone Blaze Ads plugin owns the menu under its own slug and
740 // this package did not register 'advertising', point the link straight at the
741 // standalone's page. Relying on the admin.php?page=advertising redirect would
742 // drop the #! route fragment and land the user on the dashboard home instead
743 // of the specific post's promotion flow.
744 if ( 'advertising' === $menu_slug && self::is_standalone_blaze_ads_active() ) {
745 /** This filter is documented in Blaze::redirect_legacy_advertising_url() */
746 $menu_slug = apply_filters( 'jetpack_blaze_standalone_menu_slug', 'wp-blaze' );
747 }
748
749 $admin_url = admin_url( 'admin.php?page=' . $menu_slug );
750 $hostname = wp_parse_url( get_site_url(), PHP_URL_HOST );
751 // The dashboard SPA routes under its menu slug (see Dashboard), so the #! path
752 // prefix must match the slug rather than being hardcoded to 'advertising'.
753 $blaze_url = sprintf(
754 '%1$s#!/%2$s/posts/promote/post-%3$s/%4$s',
755 $admin_url,
756 $menu_slug,
757 esc_attr( $post_id ),
758 $hostname
759 );
760
761 return array(
762 'link' => $blaze_url,
763 'external' => false,
764 );
765 }
766
767 // Default Calypso link.
768 $blaze_url = Redirect::get_url(
769 'jetpack-blaze',
770 array(
771 'query' => 'blazepress-widget=post-' . esc_attr( $post_id ),
772 )
773 );
774 return array(
775 'link' => $blaze_url,
776 'external' => true,
777 );
778 }
779
780 /**
781 * Adds the Promote link to the posts list row action.
782 *
783 * @param array $post_actions The current array of post actions.
784 * @param WP_Post $post The current post in the post list table.
785 *
786 * @return array
787 */
788 public static function jetpack_blaze_row_action( $post_actions, $post ) {
789 /**
790 * Allow third-party plugins to disable Blaze row actions.
791 *
792 * @since 0.16.0
793 *
794 * @param bool $are_quick_links_enabled Should Blaze row actions be enabled.
795 * @param WP_Post $post The current post in the post list table.
796 */
797 $are_quick_links_enabled = apply_filters( 'jetpack_blaze_post_row_actions_enable', true, $post );
798
799 // Bail if we are not looking at one of the supported post types (post, page, or product).
800 if (
801 ! $are_quick_links_enabled
802 || ! in_array( $post->post_type, array( 'post', 'page', 'product' ), true )
803 ) {
804 return $post_actions;
805 }
806
807 // Bail if the post is not published.
808 if ( $post->post_status !== 'publish' ) {
809 return $post_actions;
810 }
811
812 // Bail if the post has a password.
813 if ( '' !== $post->post_password ) {
814 return $post_actions;
815 }
816
817 $blaze_url = self::get_campaign_management_url( $post->ID );
818 $text = __( 'Promote with Blaze', 'jetpack-blaze' );
819 $title = get_the_title( $post );
820 $label = sprintf(
821 /* translators: post title */
822 __( 'Blaze &#8220;%s&#8221; to Tumblr and WordPress.com audiences.', 'jetpack-blaze' ),
823 $title
824 );
825
826 $post_actions['blaze'] = sprintf(
827 '<a href="%1$s" title="%2$s" aria-label="%2$s" %4$s>%3$s</a>',
828 esc_url( $blaze_url['link'] ),
829 esc_attr( $label ),
830 esc_html( $text ),
831 ( true === $blaze_url['external'] ? 'target="_blank" rel="noopener noreferrer"' : '' )
832 );
833
834 return $post_actions;
835 }
836
837 /**
838 * Enqueue block editor assets.
839 */
840 public static function enqueue_block_editor_assets() {
841 /*
842 * We do not want (nor need) Blaze in the site editor, or the widget editor, or the classic editor.
843 * We only want it in the post editor.
844 * Enqueueing the script in those editors would cause a fatal error.
845 * See #20357 for more info.
846 */
847 if ( ! function_exists( 'get_current_screen' ) ) { // When Gutenberg is loaded in the frontend.
848 return;
849 }
850 $current_screen = get_current_screen();
851 if (
852 empty( $current_screen )
853 || $current_screen->base !== 'post'
854 || ! $current_screen->is_block_editor()
855 ) {
856 return;
857 }
858 // Bail if criteria is not met to enable Blaze features.
859 if ( ! self::should_initialize()['can_init'] ) {
860 return;
861 }
862
863 Assets::register_script(
864 self::SCRIPT_HANDLE,
865 self::$script_path,
866 __FILE__,
867 array(
868 'enqueue' => true,
869 'in_footer' => true,
870 'textdomain' => 'jetpack-blaze',
871 )
872 );
873
874 // Adds Connection package initial state.
875 Connection_Initial_State::render_script( self::SCRIPT_HANDLE );
876
877 // Pass additional data to our script.
878 wp_localize_script(
879 self::SCRIPT_HANDLE,
880 'blazeInitialState',
881 array(
882 'blazeUrlTemplate' => self::get_campaign_management_url( '__POST_ID__' ),
883 )
884 );
885 }
886 }
887