Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
Capabilities
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
5 / 5
9
100.00% covered (success)
100.00%
1 / 1
 register
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unregister
100.00% covered (success)
100.00%
1 / 1
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_can_view_analytics
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 current_user_can_view_store_reports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Who may see the Premium Analytics dashboard.
4 *
5 * Jetpack Stats lets a site grant non-administrators access through the
6 * `view_stats` meta capability, and this dashboard replaces that UI, so it has
7 * to honour the same grant. `add_menu_page()` takes a single capability string,
8 * so the "manage_options OR view_stats" rule lives in a meta capability of our
9 * own rather than being spelled out at each call site.
10 *
11 * A class rather than a function file so every consumer reaches it through the
12 * autoloader: gating lives in files loaded on several different paths, and a
13 * `require_once` in each of them is a dependency to keep in sync (and an
14 * unmeasurable line of coverage) for no benefit.
15 *
16 * @package automattic/jetpack-premium-analytics
17 */
18
19namespace Automattic\Jetpack\PremiumAnalytics;
20
21/**
22 * The dashboard's capability rules.
23 *
24 * @since 0.1.0
25 */
26class Capabilities {
27
28    /**
29     * Meta capability for reading the dashboard.
30     */
31    const VIEW_ANALYTICS = 'jetpack_view_analytics';
32
33    /**
34     * Hooks the dashboard's meta capability mapping.
35     *
36     * Called from WordPress-aware entry points, never at load time: this class is
37     * autoloaded in contexts where WordPress — and add_filter() — isn't there.
38     * Idempotent, so overlapping callers are free to call it.
39     *
40     * @return void
41     */
42    public static function register() {
43        add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 );
44    }
45
46    /**
47     * Unhooks the mapping registered by register().
48     *
49     * Test tear-down needs this to drop the one filter: remove_all_filters(
50     * 'map_meta_cap' ) would also take out Stats' own `view_stats` mapping.
51     *
52     * @return void
53     */
54    public static function unregister() {
55        remove_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10 );
56    }
57
58    /**
59     * Maps the dashboard capability to the primitives that grant it.
60     *
61     * `view_stats` alone would track Stats more closely, but it only means
62     * anything once the Stats package has hooked its own `map_meta_cap` — which
63     * `Analytics::init_wpcom_simple()` never does. Without the `manage_options`
64     * arm, an unmapped `view_stats` would take the dashboard away from
65     * administrators too, which is worse than the gap this closes.
66     *
67     * @param string[] $caps    Primitive capabilities required of the user.
68     * @param string   $cap     Capability being checked.
69     * @param int      $user_id User being checked.
70     * @return string[] Primitives for the dashboard capability; anything else untouched.
71     */
72    public static function map_meta_caps( $caps, $cap, $user_id ) {
73        if ( self::VIEW_ANALYTICS !== $cap ) {
74            return $caps;
75        }
76
77        if ( user_can( $user_id, 'manage_options' ) || user_can( $user_id, 'view_stats' ) ) {
78            return array( 'read' );
79        }
80
81        return array( 'do_not_allow' );
82    }
83
84    /**
85     * Whether the current user may read the dashboard.
86     *
87     * @return bool
88     */
89    public static function current_user_can_view_analytics() {
90        return current_user_can( self::VIEW_ANALYTICS );
91    }
92
93    /**
94     * Whether the current user may read the store reports.
95     *
96     * "Store reports" is everything the proxy serves from its `analytics` prefix —
97     * WooCommerce's own reporting data. Mirrors the capability
98     * {@see \Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller} enforces
99     * there (Capabilities_Test pins the two together); surfaces backed by the prefix
100     * are hidden from readers who fail it, since all they could collect is 403s.
101     *
102     * @return bool
103     */
104    public static function current_user_can_view_store_reports() {
105        // The proxy accepts manage_options for every prefix.
106        return current_user_can( 'manage_options' ) || current_user_can( 'view_woocommerce_reports' );
107    }
108}