Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.95% covered (warning)
54.95%
100 / 182
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Blaze
54.95% covered (warning)
54.95%
100 / 182
11.11% covered (danger)
11.11%
1 / 9
299.31
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 add_post_links_actions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_dashboard_enabled
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 enable_blaze_menu
50.00% covered (danger)
50.00%
13 / 26
0.00% covered (danger)
0.00%
0 / 1
6.00
 site_supports_blaze
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
10.06
 should_initialize
45.45% covered (danger)
45.45%
20 / 44
0.00% covered (danger)
0.00%
0 / 1
35.37
 get_campaign_management_url
56.52% covered (warning)
56.52%
13 / 23
0.00% covered (danger)
0.00%
0 / 1
2.33
 jetpack_blaze_row_action
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 enqueue_block_editor_assets
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Attract high-quality traffic to your site.
4 *
5 * @package automattic/jetpack-blaze
6 */
7
8namespace Automattic\Jetpack;
9
10use Automattic\Jetpack\Blaze\Dashboard as Blaze_Dashboard;
11use Automattic\Jetpack\Blaze\Dashboard_REST_Controller as Blaze_Dashboard_REST_Controller;
12use Automattic\Jetpack\Blaze\REST_Controller;
13use Automattic\Jetpack\Connection\Client;
14use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
15use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
16use Automattic\Jetpack\Status as Jetpack_Status;
17use Automattic\Jetpack\Status\Host;
18use Automattic\Jetpack\Sync\Settings as Sync_Settings;
19use WP_Post;
20
21/**
22 * Class for promoting posts.
23 */
24class 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     * Path of the JS file we enqueue in the post editor.
34     *
35     * @var string
36     */
37    public static $script_path = '../build/editor.js';
38
39    /**
40     * Initializer.
41     * Used to configure the blaze package, eg when called via the Config package.
42     *
43     * @return void
44     */
45    public static function init() {
46        // On the edit screen, add a row action to promote the post.
47        add_action( 'load-edit.php', array( __CLASS__, 'add_post_links_actions' ) );
48        // After the quick-edit screen is processed, ensure the blaze row action is still present
49        if ( 'edit.php' === $GLOBALS['pagenow'] ||
50            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is not needed here, we're not saving anything.
51            ( 'admin-ajax.php' === $GLOBALS['pagenow'] && ! empty( $_POST['post_view'] ) && 'list' === $_POST['post_view'] && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] ) ) {
52            self::add_post_links_actions();
53        }
54        // In the post editor, add a post-publish panel to allow promoting the post.
55        add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_editor_assets' ) );
56        // Add a Blaze Menu.
57        add_action( 'admin_menu', array( __CLASS__, 'enable_blaze_menu' ), 999 );
58        // Add Blaze dashboard app REST API endpoints.
59        add_action( 'rest_api_init', array( new Blaze_Dashboard_REST_Controller(), 'register_rest_routes' ) );
60        // Add general Blaze REST API endpoints.
61        add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) );
62    }
63
64    /**
65     * Add links under each published post in the wp-admin post list.
66     *
67     * @return void
68     */
69    public static function add_post_links_actions() {
70        if ( self::should_initialize()['can_init'] ) {
71            add_filter( 'post_row_actions', array( __CLASS__, 'jetpack_blaze_row_action' ), 10, 2 );
72            add_filter( 'page_row_actions', array( __CLASS__, 'jetpack_blaze_row_action' ), 10, 2 );
73        }
74    }
75
76    /**
77     * Is the wp-admin Dashboard enabled?
78     * That dashboard is not available or necessary on WordPress.com sites when the nav redesign is disabled.
79     *
80     * @return bool
81     */
82    public static function is_dashboard_enabled() {
83        $is_dashboard_enabled = true;
84
85        // On WordPress.com sites, the dashboard is not needed if the nav redesign is not enabled.
86        if ( get_option( 'wpcom_admin_interface' ) !== 'wp-admin' && ( new Host() )->is_wpcom_platform() ) {
87            $is_dashboard_enabled = false;
88        }
89
90        /**
91         * Enable a wp-admin dashboard for Blaze campaign management.
92         *
93         * @since 0.7.0
94         *
95         * @param bool $should_enable Should the dashboard be enabled?
96         */
97        return apply_filters( 'jetpack_blaze_dashboard_enable', $is_dashboard_enabled );
98    }
99
100    /**
101     * Enable the Blaze menu.
102     *
103     * @return void
104     */
105    public static function enable_blaze_menu() {
106        if ( ! self::should_initialize()['can_init'] ) {
107            return;
108        }
109
110        $blaze_dashboard = new Blaze_Dashboard();
111
112        if ( self::is_dashboard_enabled() ) {
113            $page_suffix = add_submenu_page(
114                'tools.php',
115                esc_attr__( 'Advertising', 'jetpack-blaze' ),
116                __( 'Advertising', 'jetpack-blaze' ),
117                'manage_options',
118                'advertising',
119                array( $blaze_dashboard, 'render' ),
120                1
121            );
122            add_action( 'load-' . $page_suffix, array( $blaze_dashboard, 'admin_init' ) );
123        } elseif ( ( new Host() )->is_wpcom_platform() ) {
124            $domain      = ( new Jetpack_Status() )->get_site_suffix();
125            $page_suffix = add_submenu_page(
126                'tools.php',
127                esc_attr__( 'Advertising', 'jetpack-blaze' ),
128                __( 'Advertising', 'jetpack-blaze' ),
129                'manage_options',
130                'https://wordpress.com/advertising/' . $domain,
131                null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539
132                1
133            );
134            add_action( 'load-' . $page_suffix, array( $blaze_dashboard, 'admin_init' ) );
135        }
136    }
137
138    /**
139     * Check the WordPress.com REST API
140     * to ensure that the site supports the Blaze feature.
141     *
142     * - If the site is on WordPress.com Simple, we do not query the API.
143     * - Results are cached for a day after getting response from API.
144     * - If the API returns an error, we cache the result for an hour.
145     *
146     * @param int $blog_id The blog ID to check.
147     *
148     * @return bool
149     */
150    public static function site_supports_blaze( $blog_id ) {
151        $transient_name = 'jetpack_blaze_site_supports_blaze_' . $blog_id;
152
153        /*
154         * On WordPress.com, we don't need to make an API request,
155         * we can query directly.
156         */
157        if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'blaze_is_site_eligible' ) ) {
158            return blaze_is_site_eligible( $blog_id );
159        }
160
161        $cached_result = get_transient( $transient_name );
162        if ( false !== $cached_result ) {
163            if ( is_array( $cached_result ) ) {
164                return $cached_result['approved'];
165            }
166
167            return (bool) $cached_result;
168        }
169
170        // Make the API request.
171        $url      = sprintf( '/sites/%d/blaze/status', $blog_id );
172        $response = Client::wpcom_json_api_request_as_blog(
173            $url,
174            '2',
175            array( 'method' => 'GET' ),
176            null,
177            'wpcom'
178        );
179
180        // If there was an error or malformed response, bail and save response for an hour.
181        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
182            set_transient( $transient_name, array( 'approved' => false ), HOUR_IN_SECONDS );
183            return false;
184        }
185
186        // Decode the results.
187        $result = json_decode( wp_remote_retrieve_body( $response ), true );
188
189        // Bail if there were no results returned.
190        if ( ! is_array( $result ) || ! isset( $result['approved'] ) ) {
191            return false;
192        }
193
194        // Cache the result for 24 hours.
195        set_transient( $transient_name, array( 'approved' => (bool) $result['approved'] ), DAY_IN_SECONDS );
196
197        return (bool) $result['approved'];
198    }
199
200    /**
201     * Determines if criteria is met to enable Blaze features.
202     * Keep in mind that this makes remote requests, so we want to avoid calling it when unnecessary, like in the frontend.
203     *
204     * @return array
205     */
206    public static function should_initialize() {
207        $is_wpcom   = defined( 'IS_WPCOM' ) && IS_WPCOM;
208        $connection = new Jetpack_Connection();
209        $site_id    = Jetpack_Connection::get_site_id();
210
211        // Only admins should be able to Blaze posts on a site.
212        if ( ! current_user_can( 'manage_options' ) ) {
213            return array(
214                'can_init' => false,
215                'reason'   => 'user_not_admin',
216            );
217        }
218
219        // Allow short-circuiting the Blaze initialization via a filter.
220        if ( has_filter( 'jetpack_blaze_enabled' ) ) {
221            /**
222             * Filter to disable all Blaze functionality.
223             *
224             * @since 0.3.0
225             *
226             * @param bool $should_initialize Whether Blaze should be enabled. Default to true.
227             */
228            $should_init = apply_filters( 'jetpack_blaze_enabled', true );
229
230            return array(
231                'can_init' => $should_init,
232                'reason'   => $should_init ? null : 'initialization_disabled',
233            );
234        }
235
236        // On self-hosted sites, we must do some additional checks.
237        if ( ! $is_wpcom ) {
238            /*
239            * These features currently only work on WordPress.com,
240            * so the site must be connected to WordPress.com, and the user as well for things to work.
241            */
242            if (
243                is_wp_error( $site_id )
244            ) {
245                return array(
246                    'can_init' => false,
247                    'reason'   => 'wp_error',
248                );
249            }
250
251            if ( ! $connection->is_connected() ) {
252                return array(
253                    'can_init' => false,
254                    'reason'   => 'site_not_connected',
255                );
256            }
257
258            if ( ! $connection->is_user_connected() ) {
259                return array(
260                    'can_init' => false,
261                    'reason'   => 'user_not_connected',
262                );
263            }
264
265            // The whole thing is powered by Sync!
266            if ( ! Sync_Settings::is_sync_enabled() ) {
267                return array(
268                    'can_init' => false,
269                    'reason'   => 'sync_disabled',
270                );
271            }
272        }
273
274        // Check if the site supports Blaze.
275        if ( is_numeric( $site_id ) && ! self::site_supports_blaze( $site_id ) ) {
276            return array(
277                'can_init' => false,
278                'reason'   => 'site_not_eligible',
279            );
280        }
281
282        // Final fallback.
283        return array(
284            'can_init' => true,
285            'reason'   => null,
286        );
287    }
288
289    /**
290     * Get URL to create a Blaze campaign for a specific post.
291     *
292     * This can return 2 different types of URL:
293     * - Calypso Links
294     * - wp-admin Links if access to the wp-admin Blaze Dashboard is enabled.
295     *
296     * @param int|string $post_id Post ID.
297     *
298     * @return array An array with the link, and whether this is a Calypso or a wp-admin link.
299     */
300    public static function get_campaign_management_url( $post_id ) {
301        if ( self::is_dashboard_enabled() ) {
302            $admin_url = admin_url( 'tools.php?page=advertising' );
303            $hostname  = wp_parse_url( get_site_url(), PHP_URL_HOST );
304            $blaze_url = sprintf(
305                '%1$s#!/advertising/posts/promote/post-%2$s/%3$s',
306                $admin_url,
307                esc_attr( $post_id ),
308                $hostname
309            );
310
311            return array(
312                'link'     => $blaze_url,
313                'external' => false,
314            );
315        }
316
317        // Default Calypso link.
318        $blaze_url = Redirect::get_url(
319            'jetpack-blaze',
320            array(
321                'query' => 'blazepress-widget=post-' . esc_attr( $post_id ),
322            )
323        );
324        return array(
325            'link'     => $blaze_url,
326            'external' => true,
327        );
328    }
329
330    /**
331     * Adds the Promote link to the posts list row action.
332     *
333     * @param array   $post_actions The current array of post actions.
334     * @param WP_Post $post The current post in the post list table.
335     *
336     * @return array
337     */
338    public static function jetpack_blaze_row_action( $post_actions, $post ) {
339        /**
340         * Allow third-party plugins to disable Blaze row actions.
341         *
342         * @since 0.16.0
343         *
344         * @param bool    $are_quick_links_enabled Should Blaze row actions be enabled.
345         * @param WP_Post $post                    The current post in the post list table.
346         */
347        $are_quick_links_enabled = apply_filters( 'jetpack_blaze_post_row_actions_enable', true, $post );
348
349        // Bail if we are not looking at one of the supported post types (post, page, or product).
350        if (
351            ! $are_quick_links_enabled
352            || ! in_array( $post->post_type, array( 'post', 'page', 'product' ), true )
353        ) {
354            return $post_actions;
355        }
356
357        // Bail if the post is not published.
358        if ( $post->post_status !== 'publish' ) {
359            return $post_actions;
360        }
361
362        // Bail if the post has a password.
363        if ( '' !== $post->post_password ) {
364            return $post_actions;
365        }
366
367        $blaze_url = self::get_campaign_management_url( $post->ID );
368        $text      = __( 'Promote with Blaze', 'jetpack-blaze' );
369        $title     = get_the_title( $post );
370        $label     = sprintf(
371            /* translators: post title */
372            __( 'Blaze &#8220;%s&#8221; to Tumblr and WordPress.com audiences.', 'jetpack-blaze' ),
373            $title
374        );
375
376        $post_actions['blaze'] = sprintf(
377            '<a href="%1$s" title="%2$s" aria-label="%2$s" %4$s>%3$s</a>',
378            esc_url( $blaze_url['link'] ),
379            esc_attr( $label ),
380            esc_html( $text ),
381            ( true === $blaze_url['external'] ? 'target="_blank" rel="noopener noreferrer"' : '' )
382        );
383
384        return $post_actions;
385    }
386
387    /**
388     * Enqueue block editor assets.
389     */
390    public static function enqueue_block_editor_assets() {
391        /*
392         * We do not want (nor need) Blaze in the site editor, or the widget editor, or the classic editor.
393         * We only want it in the post editor.
394         * Enqueueing the script in those editors would cause a fatal error.
395         * See #20357 for more info.
396        */
397        if ( ! function_exists( 'get_current_screen' ) ) { // When Gutenberg is loaded in the frontend.
398            return;
399        }
400        $current_screen = get_current_screen();
401        if (
402            empty( $current_screen )
403            || $current_screen->base !== 'post'
404            || ! $current_screen->is_block_editor()
405        ) {
406            return;
407        }
408        // Bail if criteria is not met to enable Blaze features.
409        if ( ! self::should_initialize()['can_init'] ) {
410            return;
411        }
412
413        Assets::register_script(
414            self::SCRIPT_HANDLE,
415            self::$script_path,
416            __FILE__,
417            array(
418                'enqueue'    => true,
419                'in_footer'  => true,
420                'textdomain' => 'jetpack-blaze',
421            )
422        );
423
424        // Adds Connection package initial state.
425        Connection_Initial_State::render_script( self::SCRIPT_HANDLE );
426
427        // Pass additional data to our script.
428        wp_localize_script(
429            self::SCRIPT_HANDLE,
430            'blazeInitialState',
431            array(
432                'blazeUrlTemplate' => self::get_campaign_management_url( '__POST_ID__' ),
433            )
434        );
435    }
436}