Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
Capabilities
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
8 / 8
17
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
 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
13/**
14 * The dashboard's capability rules.
15 *
16 * @since 0.1.0
17 */
18class Capabilities {
19
20    /**
21     * Meta capability for reading the dashboard.
22     */
23    const VIEW_ANALYTICS = 'jetpack_view_analytics';
24
25    /**
26     * Hooks the dashboard's meta capability mapping.
27     *
28     * Called from WordPress-aware entry points, never at load time: this class is autoloaded where
29     * WordPress — and add_filter() — isn't there. Idempotent, so overlapping callers may call it freely.
30     *
31     * @return void
32     */
33    public static function register() {
34        self::$has_available_section = array();
35        add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 );
36    }
37
38    /**
39     * Unhooks the mapping registered by register().
40     *
41     * Test tear-down needs this to drop the one filter: remove_all_filters(
42     * 'map_meta_cap' ) would also take out Stats' own `view_stats` mapping.
43     *
44     * @return void
45     */
46    public static function unregister() {
47        self::$has_available_section = array();
48        remove_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10 );
49    }
50
51    /**
52     * Whether a section's availability check is running, so a section gated on the dashboard
53     * capability itself does not recurse.
54     *
55     * @var bool
56     */
57    private static $resolving_sections = false;
58
59    /**
60     * Whether a section is available, per `<user id>:<blog id>`, for the rest of the request.
61     *
62     * @var array<string, bool>
63     */
64    private static $has_available_section = array();
65
66    /**
67     * Maps the dashboard capability: a reader needs at least one section available to them.
68     *
69     * Sections answer for the current user only, so checking anyone else is refused.
70     *
71     * @param string[] $caps    Primitive capabilities required of the user.
72     * @param string   $cap     Capability being checked.
73     * @param int      $user_id User being checked.
74     * @return string[] Primitives for the dashboard capability; anything else untouched.
75     */
76    public static function map_meta_caps( $caps, $cap, $user_id ) {
77        if ( self::VIEW_ANALYTICS !== $cap ) {
78            return $caps;
79        }
80
81        if ( (int) $user_id === get_current_user_id() && self::current_user_has_available_section() ) {
82            return array( 'read' );
83        }
84
85        return array( 'do_not_allow' );
86    }
87
88    /**
89     * Whether any dashboard section is available to the current user.
90     *
91     * @return bool
92     */
93    private static function current_user_has_available_section() {
94        // The registry hydrates only after init; the dashboard name comes with its loaded files.
95        if ( self::$resolving_sections || ! did_action( 'init' ) || ! defined( __NAMESPACE__ . '\\DASHBOARD_NAME' ) ) {
96            return false;
97        }
98
99        // add_menu_page() and the admin menu each check the capability on every admin screen.
100        $key = get_current_user_id() . ':' . get_current_blog_id();
101        if ( ! isset( self::$has_available_section[ $key ] ) ) {
102            self::$resolving_sections = true;
103            try {
104                self::$has_available_section[ $key ] = array() !== Dashboard_Section_Registry::get_instance()->get_available_sections( DASHBOARD_NAME );
105            } finally {
106                self::$resolving_sections = false;
107            }
108        }
109
110        return self::$has_available_section[ $key ];
111    }
112
113    /**
114     * Whether the current user may read the dashboard.
115     *
116     * @return bool
117     */
118    public static function current_user_can_view_analytics() {
119        return current_user_can( self::VIEW_ANALYTICS );
120    }
121
122    /**
123     * Whether the current user may read the Stats reports.
124     *
125     * "Stats reports" is everything the proxy serves under `view_stats`, mirroring what
126     * {@see \Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller} enforces there (pinned by Capabilities_Test).
127     *
128     * @since $$next-version$$
129     *
130     * @return bool
131     */
132    public static function current_user_can_view_stats() {
133        // `view_stats` alone would track Stats more closely, but it only works once Stats hooks its own
134        // `map_meta_cap` — which Analytics::init_wpcom_simple() never does, locking out administrators too.
135        return current_user_can( 'manage_options' ) || current_user_can( 'view_stats' );
136    }
137
138    /**
139     * Whether the current user may read the store reports.
140     *
141     * "Store reports" is everything the proxy serves from its `analytics` prefix, mirroring what
142     * {@see \Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller} enforces there (pinned by Capabilities_Test).
143     *
144     * @return bool
145     */
146    public static function current_user_can_view_store_reports() {
147        // The proxy accepts manage_options for every prefix.
148        return current_user_can( 'manage_options' ) || current_user_can( 'view_woocommerce_reports' );
149    }
150
151    /**
152     * Whether the current user may view ad reports.
153     *
154     * @since 0.4.0
155     *
156     * @return bool
157     */
158    public static function current_user_can_view_ad_reports() {
159        return current_user_can( 'manage_options' );
160    }
161}