Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Capabilities
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
12 / 12
26
100.00% covered (success)
100.00%
1 / 1
 register
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 unregister
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 map_meta_caps
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 current_user_has_available_section
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 current_user_can_view_analytics
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dashboard_page_capability
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 current_user_can_open_stats
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 is_site_connected
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 pre_connection_capability
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 current_user_can_view_stats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 current_user_can_view_store_reports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 current_user_can_view_ad_reports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Who may see the Premium Analytics dashboard.
4 *
5 * Opening it takes one section available to the reader, by whatever rule that section registered,
6 * and add_menu_page() takes one capability string — hence a meta capability of our own.
7 *
8 * @package automattic/jetpack-premium-analytics
9 */
10
11namespace Automattic\Jetpack\PremiumAnalytics;
12
13use Automattic\Jetpack\Connection\Manager as Connection_Manager;
14use Automattic\Jetpack\Status;
15use Automattic\Jetpack\Status\Host;
16
17/**
18 * The dashboard's capability rules.
19 *
20 * @since 0.1.0
21 */
22class Capabilities {
23
24    /**
25     * Meta capability for reading the dashboard.
26     */
27    const VIEW_ANALYTICS = 'jetpack_view_analytics';
28
29    /**
30     * Hooks the dashboard's meta capability mapping.
31     *
32     * Called from WordPress-aware entry points, never at load time: this class is autoloaded where
33     * WordPress — and add_filter() — isn't there. Idempotent, so overlapping callers may call it freely.
34     *
35     * @return void
36     */
37    public static function register() {
38        self::$has_available_section = array();
39        add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 );
40    }
41
42    /**
43     * Unhooks the mapping registered by register().
44     *
45     * Test tear-down needs this to drop the one filter: remove_all_filters(
46     * 'map_meta_cap' ) would also take out Stats' own `view_stats` mapping.
47     *
48     * @return void
49     */
50    public static function unregister() {
51        self::$has_available_section = array();
52        remove_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10 );
53    }
54
55    /**
56     * Whether a section's availability check is running, so a section gated on the dashboard
57     * capability itself does not recurse.
58     *
59     * @var bool
60     */
61    private static $resolving_sections = false;
62
63    /**
64     * Whether a section is available, per `<user id>:<blog id>`, for the rest of the request.
65     *
66     * @var array<string, bool>
67     */
68    private static $has_available_section = array();
69
70    /**
71     * Maps the dashboard capability: a reader needs at least one section available to them.
72     *
73     * Sections answer for the current user only, so checking anyone else is refused.
74     *
75     * @param string[] $caps    Primitive capabilities required of the user.
76     * @param string   $cap     Capability being checked.
77     * @param int      $user_id User being checked.
78     * @return string[] Primitives for the dashboard capability; anything else untouched.
79     */
80    public static function map_meta_caps( $caps, $cap, $user_id ) {
81        if ( self::VIEW_ANALYTICS !== $cap ) {
82            return $caps;
83        }
84
85        if ( (int) $user_id === get_current_user_id() && self::current_user_has_available_section() ) {
86            return array( 'read' );
87        }
88
89        return array( 'do_not_allow' );
90    }
91
92    /**
93     * Whether any dashboard section is available to the current user.
94     *
95     * @return bool
96     */
97    private static function current_user_has_available_section() {
98        // The registry hydrates only after init; the dashboard name comes with its loaded files.
99        if ( self::$resolving_sections || ! did_action( 'init' ) || ! defined( __NAMESPACE__ . '\\DASHBOARD_NAME' ) ) {
100            return false;
101        }
102
103        // add_menu_page() and the admin menu each check the capability on every admin screen.
104        $key = get_current_user_id() . ':' . get_current_blog_id();
105        if ( ! isset( self::$has_available_section[ $key ] ) ) {
106            self::$resolving_sections = true;
107            try {
108                self::$has_available_section[ $key ] = array() !== Dashboard_Section_Registry::get_instance()->get_available_sections( DASHBOARD_NAME );
109            } finally {
110                self::$resolving_sections = false;
111            }
112        }
113
114        return self::$has_available_section[ $key ];
115    }
116
117    /**
118     * Whether the current user may read the dashboard.
119     *
120     * @return bool
121     */
122    public static function current_user_can_view_analytics() {
123        return current_user_can( self::VIEW_ANALYTICS );
124    }
125
126    /**
127     * Capability the dashboard's menu and page need.
128     *
129     * @return string
130     */
131    public static function dashboard_page_capability() {
132        return self::is_site_connected() ? self::VIEW_ANALYTICS : self::pre_connection_capability();
133    }
134
135    /**
136     * Whether the current user can open the dashboard's Stats views, which links to them are gated on.
137     *
138     * @return bool
139     */
140    public static function current_user_can_open_stats() {
141        return self::current_user_can_view_stats()
142            && ( self::is_site_connected() || current_user_can( self::pre_connection_capability() ) );
143    }
144
145    /**
146     * Whether the site is connected to WordPress.com, as WordPress.com Simple always is.
147     *
148     * @return bool
149     */
150    private static function is_site_connected() {
151        return ( new Host() )->is_wpcom_simple() || ( new Connection_Manager() )->is_connected();
152    }
153
154    /**
155     * Until the site is connected the dashboard only offers to connect it, so like Stats it is for
156     * whoever can (`jetpack_connect`), or for administrators in offline mode, where nobody can.
157     *
158     * @return string
159     */
160    private static function pre_connection_capability() {
161        return ( new Status() )->is_offline_mode() ? 'manage_options' : 'jetpack_connect';
162    }
163
164    /**
165     * Whether the current user may read the Stats reports.
166     *
167     * "Stats reports" is everything the proxy serves under `view_stats`, mirroring what
168     * {@see \Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller} enforces there (pinned by Capabilities_Test).
169     *
170     * @since 0.12.0
171     *
172     * @return bool
173     */
174    public static function current_user_can_view_stats() {
175        // `view_stats` alone would track Stats more closely, but it only works once Stats hooks its own
176        // `map_meta_cap` — which Analytics::init_wpcom_simple() never does, locking out administrators too.
177        return current_user_can( 'manage_options' ) || current_user_can( 'view_stats' );
178    }
179
180    /**
181     * Whether the current user may read the store reports.
182     *
183     * "Store reports" is everything the proxy serves from its `analytics` prefix, mirroring what
184     * {@see \Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller} enforces there (pinned by Capabilities_Test).
185     *
186     * @return bool
187     */
188    public static function current_user_can_view_store_reports() {
189        // The proxy accepts manage_options for every prefix.
190        return current_user_can( 'manage_options' ) || current_user_can( 'view_woocommerce_reports' );
191    }
192
193    /**
194     * Whether the current user may view ad reports.
195     *
196     * @since 0.4.0
197     *
198     * @return bool
199     */
200    public static function current_user_can_view_ad_reports() {
201        return current_user_can( 'manage_options' );
202    }
203}