Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
21 / 21 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
1 / 1 |
| Capabilities | |
100.00% |
21 / 21 |
|
100.00% |
8 / 8 |
17 | |
100.00% |
1 / 1 |
| register | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| unregister | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| map_meta_caps | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| current_user_has_available_section | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
| current_user_can_view_analytics | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| current_user_can_view_stats | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| current_user_can_view_store_reports | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| current_user_can_view_ad_reports | |
100.00% |
1 / 1 |
|
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 | |
| 11 | namespace Automattic\Jetpack\PremiumAnalytics; |
| 12 | |
| 13 | /** |
| 14 | * The dashboard's capability rules. |
| 15 | * |
| 16 | * @since 0.1.0 |
| 17 | */ |
| 18 | class 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 | } |