Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
17.83% covered (danger)
17.83%
23 / 129
25.00% covered (danger)
25.00%
5 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Recommendations
17.83% covered (danger)
17.83%
23 / 129
25.00% covered (danger)
25.00%
5 / 20
2867.82
0.00% covered (danger)
0.00%
0 / 1
 is_enabled
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 is_banner_enabled
n/a
0 / 0
n/a
0 / 0
1
 init_conditional_recommendation_actions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 jetpack_module_activated
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 post_transition
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
110
 plugin_activated
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 plugin_auto_update_settings_changed
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 comment_added
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 jetpack_connected
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 recommend_videopress
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 should_recommend_videopress
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 enable_conditional_recommendation
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 disable_conditional_recommendation
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 is_conditional_recommendation_enabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_conditional_recommendations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_new_conditional_recommendations
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 initialize_jetpack_recommendations
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 get_recommendations_data
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 update_recommendations_data
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_recommendations_step
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 update_recommendations_step
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Utilities related to the Jetpack Recommendations
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
9use Automattic\Jetpack\Plugins_Installer;
10use Automattic\Jetpack\Status;
11use Automattic\Jetpack\Status\Host;
12
13/**
14 * Contains utilities related to the Jetpack Recommendations.
15 *
16 * @package automattic/jetpack
17 */
18
19/**
20 * Jetpack_Recommendations class
21 */
22class Jetpack_Recommendations {
23    const PUBLICIZE_RECOMMENDATION   = 'publicize';
24    const PROTECT_RECOMMENDATION     = 'protect';
25    const ANTI_SPAM_RECOMMENDATION   = 'anti-spam';
26    const VIDEOPRESS_RECOMMENDATION  = 'videopress';
27    const BACKUP_PLAN_RECOMMENDATION = 'backup-plan';
28    const BOOST_RECOMMENDATION       = 'boost';
29
30    const CONDITIONAL_RECOMMENDATIONS_OPTION = 'recommendations_conditional';
31    const CONDITIONAL_RECOMMENDATIONS        = array(
32        self::PUBLICIZE_RECOMMENDATION,
33        self::PROTECT_RECOMMENDATION,
34        self::ANTI_SPAM_RECOMMENDATION,
35        self::VIDEOPRESS_RECOMMENDATION,
36        self::BACKUP_PLAN_RECOMMENDATION,
37        self::BOOST_RECOMMENDATION,
38    );
39
40    const VIDEOPRESS_TIMED_ACTION = 'jetpack_recommend_videopress';
41
42    /**
43     * Returns a boolean indicating if the Jetpack Recommendations are enabled.
44     *
45     * @since 9.3.0
46     *
47     * @return bool
48     */
49    public static function is_enabled() {
50        // Shortcircuit early if Jetpack is not active or we are in offline mode.
51        if ( ! Jetpack::is_connection_ready() || ( new Status() )->is_offline_mode() ) {
52            return false;
53        }
54
55        // No recommendations for Atomic sites, they already get onboarded in Calypso.
56        if ( ( new Host() )->is_woa_site() ) {
57            return false;
58        }
59
60        self::initialize_jetpack_recommendations();
61
62        return true;
63    }
64
65    /**
66     * Returns a boolean indicating if the Jetpack Banner is enabled.
67     *
68     * @since 9.3.0
69     *
70     * @deprecated 13.2
71     *
72     * @return bool
73     */
74    public static function is_banner_enabled() {
75        _deprecated_function( __METHOD__, 'jetpack-13.2' );
76        return false;
77    }
78
79    /**
80     * Set up actions to monitor for things that trigger a recommendation.
81     *
82     * @return false|void
83     */
84    public static function init_conditional_recommendation_actions() {
85        // Check to make sure that recommendations are enabled.
86        if ( ! self::is_enabled() ) {
87            return false;
88        }
89
90        // Monitor for the publishing of a new post.
91        add_action( 'transition_post_status', array( static::class, 'post_transition' ), 10, 3 );
92        add_action( 'jetpack_activate_module', array( static::class, 'jetpack_module_activated' ), 10, 2 );
93
94        // Monitor for activating a new plugin.
95        add_action( 'activated_plugin', array( static::class, 'plugin_activated' ), 10 );
96
97        // Monitor for the addition of a new comment.
98        add_action( 'comment_post', array( static::class, 'comment_added' ), 10, 3 );
99
100        // Monitor for Jetpack connection success.
101        add_action( 'jetpack_authorize_ending_authorized', array( static::class, 'jetpack_connected' ) );
102        add_action( self::VIDEOPRESS_TIMED_ACTION, array( static::class, 'recommend_videopress' ) );
103
104        // Monitor for changes in plugins that have auto-updates enabled
105        add_action( 'update_site_option_auto_update_plugins', array( static::class, 'plugin_auto_update_settings_changed' ), 10, 3 );
106    }
107
108    /**
109     * Check when Jetpack modules are activated if some recommendations should be skipped.
110     *
111     * @param string $module Name of the module activated.
112     * @param bool   $success Whether the module activation was successful.
113     */
114    public static function jetpack_module_activated( $module, $success ) {
115        if ( 'publicize' === $module && $success ) {
116            self::disable_conditional_recommendation( self::PUBLICIZE_RECOMMENDATION );
117        } elseif ( 'videopress' === $module && $success ) {
118            // If VideoPress is enabled and a recommendation for it is scheduled, cancel that recommendation.
119            $recommendation_timestamp = wp_next_scheduled( self::VIDEOPRESS_TIMED_ACTION );
120            if ( false !== $recommendation_timestamp ) {
121                wp_unschedule_event( $recommendation_timestamp, self::VIDEOPRESS_TIMED_ACTION );
122            }
123        }
124    }
125
126    /**
127     * Hook for transition_post_status that checks for the publishing of a new post or page.
128     * Used to enable the publicize and boost recommendations.
129     *
130     * @param string  $new_status new status of post.
131     * @param string  $old_status old status of post.
132     * @param WP_Post $post the post object being updated.
133     */
134    public static function post_transition( $new_status, $old_status, $post ) {
135        // Check for condition when post has been published.
136        if ( 'post' === $post->post_type && 'publish' === $new_status && 'publish' !== $old_status && ! Jetpack::is_module_active( 'publicize' ) ) {
137            // Set the publicize recommendation to have met criteria to be shown.
138            self::enable_conditional_recommendation( self::PUBLICIZE_RECOMMENDATION );
139            return;
140        }
141        // A new page has been published
142        // Check to see if the boost plugin is active
143        if (
144            'page' === $post->post_type &&
145            'publish' === $new_status &&
146            'publish' !== $old_status &&
147            ! Plugins_Installer::is_plugin_active( 'boost/jetpack-boost.php' ) &&
148            ! Plugins_Installer::is_plugin_active( 'jetpack-boost/jetpack-boost.php' )
149        ) {
150            self::enable_conditional_recommendation( self::BOOST_RECOMMENDATION );
151        }
152    }
153
154    /**
155     * Runs when a plugin gets activated
156     *
157     * @param string $plugin Path to the plugins file relative to the plugins directory.
158     */
159    public static function plugin_activated( $plugin ) {
160        // If the plugin is in this list, don't enable the recommendation.
161        $plugin_whitelist = array(
162            'jetpack.php',
163            'akismet.php',
164            'creative-mail.php',
165            'jetpack-backup.php',
166            'jetpack-boost.php',
167            'jetpack-protect.php',
168            'crowdsignal.php',
169            'vaultpress.php',
170            'woocommerce.php',
171        );
172
173        $path_parts  = explode( '/', $plugin );
174        $plugin_file = array_pop( $path_parts );
175
176        if ( ! in_array( $plugin_file, $plugin_whitelist, true ) ) {
177            $products = array_column( Jetpack_Plan::get_products(), 'product_slug' );
178
179            // Check for a plan or product that enables scan.
180            $plan_supports_scan = Jetpack_Plan::supports( 'scan' );
181            $has_scan_product   = count( array_intersect( array( 'jetpack_scan', 'jetpack_scan_monthly' ), $products ) ) > 0;
182            $has_scan           = $plan_supports_scan || $has_scan_product;
183
184            // Check if Jetpack Protect plugin is already active.
185            $has_protect = Plugins_Installer::is_plugin_active( 'jetpack-protect/jetpack-protect.php' ) || Plugins_Installer::is_plugin_active( 'protect/jetpack-protect.php' );
186
187            if ( ! $has_scan && ! $has_protect ) {
188                self::enable_conditional_recommendation( self::PROTECT_RECOMMENDATION );
189            }
190        }
191    }
192
193    /**
194     * Runs when the auto_update_plugins option has been changed
195     *
196     * @param string $option_name - the name of the option updated ( always auto_update_plugins ).
197     * @param array  $new_auto_update_plugins - plugins that have auto update enabled following the change.
198     * @param array  $old_auto_update_plugins - plugins that had auto update enabled before the most recent change.
199     * @return void
200     */
201    public static function plugin_auto_update_settings_changed( $option_name, $new_auto_update_plugins, $old_auto_update_plugins ) {
202        if (
203            is_multisite() ||
204            self::is_conditional_recommendation_enabled( self::BACKUP_PLAN_RECOMMENDATION )
205        ) {
206            return;
207        }
208
209        // Look for plugins that have had auto-update enabled in this most recent update.
210        $enabled_auto_updates = array_diff( $new_auto_update_plugins, $old_auto_update_plugins );
211        if ( ! empty( $enabled_auto_updates ) ) {
212            // Check the backup state.
213            $rewind_state = get_transient( 'jetpack_rewind_state' );
214            $has_backup   = $rewind_state && in_array( $rewind_state->state, array( 'awaiting_credentials', 'provisioning', 'active' ), true );
215
216            if ( ! $has_backup ) {
217                self::enable_conditional_recommendation( self::BACKUP_PLAN_RECOMMENDATION );
218            }
219        }
220    }
221
222    /**
223     * Runs when a new comment is added.
224     *
225     * @param integer $comment_id The ID of the comment that was added.
226     * @param bool    $comment_approved Whether or not the comment is approved.
227     * @param array   $commentdata Comment data.
228     */
229    public static function comment_added( $comment_id, $comment_approved, $commentdata ) {
230        if ( self::is_conditional_recommendation_enabled( self::ANTI_SPAM_RECOMMENDATION ) ) {
231            return;
232        }
233
234        if ( Plugins_Installer::is_plugin_active( 'akismet/akismet.php' ) ) {
235            return;
236        }
237
238        // The site has anti-spam features already.
239        $site_products         = array_column( Jetpack_Plan::get_products(), 'product_slug' );
240        $has_anti_spam_product = count( array_intersect( array( 'jetpack_anti_spam', 'jetpack_anti_spam_monthly' ), $site_products ) ) > 0;
241
242        if ( Jetpack_Plan::supports( 'akismet' ) || Jetpack_Plan::supports( 'antispam' ) || $has_anti_spam_product ) {
243            return;
244        }
245
246        if ( isset( $commentdata['comment_post_ID'] ) ) {
247            $post_id = $commentdata['comment_post_ID'];
248        } else {
249            $comment = get_comment( $comment_id );
250            $post_id = $comment->comment_post_ID;
251        }
252        $comment_count = get_comments_number( $post_id );
253
254        if ( intval( $comment_count ) >= 5 ) {
255            self::enable_conditional_recommendation( self::ANTI_SPAM_RECOMMENDATION );
256        }
257    }
258
259    /**
260     * Runs after a successful connection is made.
261     */
262    public static function jetpack_connected() {
263        // Schedule a recommendation for VideoPress in 2 weeks.
264        if ( false === wp_next_scheduled( self::VIDEOPRESS_TIMED_ACTION ) ) {
265            $date = new DateTime();
266            $date->add( new DateInterval( 'P14D' ) );
267            wp_schedule_single_event( $date->getTimestamp(), self::VIDEOPRESS_TIMED_ACTION );
268        }
269    }
270
271    /**
272     * Enable a recommendation for VideoPress.
273     */
274    public static function recommend_videopress() {
275        // Check to see if the VideoPress recommendation is already enabled.
276        if ( self::is_conditional_recommendation_enabled( self::VIDEOPRESS_RECOMMENDATION ) ) {
277            return;
278        }
279
280        $site_plan     = Jetpack_Plan::get();
281        $site_products = array_column( Jetpack_Plan::get_products(), 'product_slug' );
282
283        if ( self::should_recommend_videopress( $site_plan, $site_products ) ) {
284            self::enable_conditional_recommendation( self::VIDEOPRESS_RECOMMENDATION );
285        }
286    }
287
288    /**
289     * Should we provide a recommendation for videopress?
290     * This method exists to facilitate unit testing
291     *
292     * @param array $site_plan A representation of the site's plan.
293     * @param array $site_products An array of product slugs.
294     * @return boolean
295     */
296    public static function should_recommend_videopress( $site_plan, $site_products ) {
297        // Does the site have the VideoPress module enabled?
298        if ( Jetpack::is_module_active( 'videopress' ) ) {
299            return false;
300        }
301
302        // Does the site plan have upgraded videopress features?
303        // For now, this just checks to see if the site has a free plan.
304        // Jetpack_Plan::supports('videopress') returns true for all plans, since there is a free tier.
305        $is_free_plan = 'free' === $site_plan['class'];
306        if ( ! $is_free_plan ) {
307            return false;
308        }
309
310        // Does this site already have a VideoPress product?
311        $has_videopress_product = count( array_intersect( array( 'jetpack_videopress', 'jetpack_videopress_monthly' ), $site_products ) ) > 0;
312        if ( $has_videopress_product ) {
313            return false;
314        }
315
316        return true;
317    }
318
319    /**
320     * Enable a recommendation.
321     *
322     * @param string $recommendation_name The name of the recommendation to enable.
323     * @return false|void
324     */
325    public static function enable_conditional_recommendation( $recommendation_name ) {
326        if ( ! in_array( $recommendation_name, self::CONDITIONAL_RECOMMENDATIONS, true ) ) {
327            return false;
328        }
329
330        $conditional_recommendations = Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
331        if ( ! in_array( $recommendation_name, $conditional_recommendations, true ) ) {
332            $conditional_recommendations[] = $recommendation_name;
333            Jetpack_Options::update_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, $conditional_recommendations );
334        }
335    }
336
337    /**
338     * Disable a recommendation.
339     *
340     * @param string $recommendation_name The name of the recommendation to disable.
341     * @return false|void
342     */
343    public static function disable_conditional_recommendation( $recommendation_name ) {
344        if ( ! in_array( $recommendation_name, self::CONDITIONAL_RECOMMENDATIONS, true ) ) {
345            return false;
346        }
347
348        $conditional_recommendations = Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
349        $recommendation_index        = array_search( $recommendation_name, $conditional_recommendations, true );
350
351        if ( false !== $recommendation_index ) {
352            array_splice( $conditional_recommendations, $recommendation_index, 1 );
353            Jetpack_Options::update_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, $conditional_recommendations );
354        }
355    }
356
357    /**
358     * Check to see if a recommendation is enabled or not.
359     *
360     * @param string $recommendation_name The name of the recommendation to check for.
361     * @return bool
362     */
363    public static function is_conditional_recommendation_enabled( $recommendation_name ) {
364        $conditional_recommendations = Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
365        return in_array( $recommendation_name, $conditional_recommendations, true );
366    }
367
368    /**
369     * Gets data for all conditional recommendations.
370     *
371     * @return mixed
372     */
373    public static function get_conditional_recommendations() {
374        return Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
375    }
376
377    /**
378     * Get an array of new conditional recommendations that have not been viewed.
379     *
380     * @return array
381     */
382    public static function get_new_conditional_recommendations() {
383        $conditional_recommendations = self::get_conditional_recommendations();
384        $recommendations_data        = Jetpack_Options::get_option( 'recommendations_data', array() );
385        $viewed_recommendations      = isset( $recommendations_data['viewedRecommendations'] ) ? $recommendations_data['viewedRecommendations'] : array();
386
387        // array_diff returns a keyed array - reduce to unique values.
388        return array_unique( array_values( array_diff( $conditional_recommendations, $viewed_recommendations ) ) );
389    }
390
391    /**
392     * Initializes the Recommendations step according to the Setup Wizard state.
393     */
394    private static function initialize_jetpack_recommendations() {
395        if ( Jetpack_Options::get_option( 'recommendations_step' ) ) {
396            return;
397        }
398
399        $setup_wizard_status = Jetpack_Options::get_option( 'setup_wizard_status' );
400        if ( 'completed' === $setup_wizard_status ) {
401            Jetpack_Options::update_option( 'recommendations_step', 'setup-wizard-completed' );
402        }
403    }
404
405    /**
406     * Get the data for the recommendations
407     *
408     * @return array Recommendations data
409     */
410    public static function get_recommendations_data() {
411        self::initialize_jetpack_recommendations();
412
413        return Jetpack_Options::get_option( 'recommendations_data', array() );
414    }
415
416    /**
417     * Update the data for the recommendations
418     *
419     * @param WP_REST_Request $data The data.
420     */
421    public static function update_recommendations_data( $data ) {
422        if ( ! empty( $data ) ) {
423            Jetpack_Options::update_option( 'recommendations_data', $data );
424        }
425    }
426
427    /**
428     * Get the data for the recommendations
429     *
430     * @return array Recommendations data
431     */
432    public static function get_recommendations_step() {
433        self::initialize_jetpack_recommendations();
434
435        return array(
436            'step' => Jetpack_Options::get_option( 'recommendations_step', 'not-started' ),
437        );
438    }
439
440    /**
441     * Update the step for the recommendations
442     *
443     * @param WP_REST_Request $step The step.
444     */
445    public static function update_recommendations_step( $step ) {
446        if ( ! empty( $step ) ) {
447            Jetpack_Options::update_option( 'recommendations_step', $step );
448        }
449    }
450}