Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
26 / 26 |
|
100.00% |
12 / 12 |
CRAP | |
100.00% |
1 / 1 |
| Capabilities | |
100.00% |
26 / 26 |
|
100.00% |
12 / 12 |
26 | |
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 | |||
| dashboard_page_capability | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| current_user_can_open_stats | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
| is_site_connected | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| pre_connection_capability | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| 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 | use Automattic\Jetpack\Connection\Manager as Connection_Manager; |
| 14 | use Automattic\Jetpack\Status; |
| 15 | use Automattic\Jetpack\Status\Host; |
| 16 | |
| 17 | /** |
| 18 | * The dashboard's capability rules. |
| 19 | * |
| 20 | * @since 0.1.0 |
| 21 | */ |
| 22 | class 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 | } |