Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
Menu_Visibility
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
5 / 5
12
100.00% covered (success)
100.00%
1 / 1
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 forget_resolved_gates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolve
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 is_module_activated
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 is_product_activated
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Resolves whether a Jetpack admin menu item's feature is active.
4 *
5 * @package automattic/my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\Modules;
12
13/**
14 * The activation half of the sidebar visibility contract.
15 *
16 * Admin_Menu owns when the question is asked and what a host may do with the answer; this
17 * class owns the answer itself, because the product classes it reads live here. A menu item
18 * declares its gate at registration and this resolves it from the same product class its
19 * My Jetpack card reads.
20 */
21class Menu_Visibility {
22
23    /**
24     * Gates already resolved this registration pass, keyed by type and slug.
25     *
26     * @var array<string, bool|null>
27     */
28    private static $resolved = array();
29
30    /**
31     * Registers this class as Admin_Menu's visibility resolver.
32     *
33     * @return void
34     */
35    public static function init() {
36        // An older admin-ui, loaded first by another plugin, may predate the resolver; items then stay visible.
37        if ( ! method_exists( Admin_Menu::class, 'set_visibility_resolver' ) ) {
38            return;
39        }
40
41        Admin_Menu::set_visibility_resolver( array( __CLASS__, 'resolve' ) );
42
43        // Memoized answers last one registration pass, which is as long as they can stay true.
44        add_action( 'admin_menu', array( __CLASS__, 'forget_resolved_gates' ), 0 );
45        add_action( 'network_admin_menu', array( __CLASS__, 'forget_resolved_gates' ), 0 );
46    }
47
48    /**
49     * Drops the gates resolved during the previous registration pass.
50     *
51     * @return void
52     */
53    public static function forget_resolved_gates() {
54        self::$resolved = array();
55    }
56
57    /**
58     * Answers whether a menu item's declared gate is satisfied.
59     *
60     * Answers are memoized for one registration pass: resolving a product rebuilds the product
61     * class map and rescans the installed plugins, and the sidebar asks once per gated item.
62     *
63     * @param array $args The item's visibility declaration, as passed to Admin_Menu::add_menu().
64     * @return bool|null True or false, or null when the gate cannot be resolved here.
65     */
66    public static function resolve( $args ) {
67        if ( ! empty( $args['product'] ) ) {
68            $key = 'product:' . $args['product'];
69
70            if ( ! array_key_exists( $key, self::$resolved ) ) {
71                self::$resolved[ $key ] = self::is_product_activated( $args['product'] );
72            }
73
74            return self::$resolved[ $key ];
75        }
76
77        if ( ! empty( $args['module'] ) ) {
78            $key = 'module:' . $args['module'];
79
80            if ( ! array_key_exists( $key, self::$resolved ) ) {
81                self::$resolved[ $key ] = self::is_module_activated( $args['module'] );
82            }
83
84            return self::$resolved[ $key ];
85        }
86
87        return null;
88    }
89
90    /**
91     * Whether a Jetpack module gating an item is switched on.
92     *
93     * A name this site has no module for is unanswerable rather than off, so a typo in a gate
94     * fails open like an unknown product slug does instead of silently removing the item. Off
95     * the Jetpack plugin that covers any module a standalone plugin did not declare through
96     * `jetpack_get_available_standalone_modules`.
97     *
98     * @param string $module_name A Jetpack module name.
99     * @return bool|null Null when this site has no such module.
100     */
101    private static function is_module_activated( $module_name ) {
102        $modules = new Modules();
103
104        if ( ! in_array( $module_name, $modules->get_available(), true ) ) {
105            return null;
106        }
107
108        return $modules->is_active( $module_name );
109    }
110
111    /**
112     * Whether the site has switched a My Jetpack product on.
113     *
114     * Reads is_activated(), not is_active(): a lapsed plan is not "off", and resolving plans
115     * costs WordPress.com requests on every admin page load. No connection check is added.
116     *
117     * @param string $product_slug A My Jetpack product slug.
118     * @return bool|null Null when no product class is registered under that slug.
119     */
120    private static function is_product_activated( $product_slug ) {
121        $product_class = Products::get_product_class( $product_slug );
122
123        if ( ! $product_class ) {
124            return null;
125        }
126
127        return (bool) $product_class::is_activated();
128    }
129}