PluginProbe
Stream – Activity Log & Audit Trail / trunk
Stream – Activity Log & Audit Trail vtrunk
4.4.0 4.3.0 4.2.2 4.2.1 trunk 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.1 3.1.1 3.10.0 3.2.0 3.2.1 3.2.2 3.2.3 All 50 releases
stream / classes / class-abilities.php

class-abilities.php in Stream – Activity Log & Audit Trail trunk, at classes/class-abilities.php

281 lines 7.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Loads and registers Stream abilities with the WordPress Abilities API.
4 *
5 * @package WP_Stream
6 */
7
8 namespace WP_Stream;
9
10 /**
11 * Class - Abilities
12 *
13 * Gates the Abilities API integration on:
14 * 1. WordPress 6.9+ (presence of WP_Ability class).
15 * 2. The "Enable Abilities API" setting in the Advanced section.
16 *
17 * When both gates pass, hooks wp_abilities_api_init to register all
18 * Stream abilities under the `stream/` namespace.
19 */
20 class Abilities {
21
22 /**
23 * Category slug used in ability meta.
24 *
25 * @const string
26 */
27 const CATEGORY_SLUG = 'stream';
28
29 /**
30 * Settings field name (under the Advanced section).
31 *
32 * @const string
33 */
34 const SETTING_NAME = 'enable_abilities_api';
35
36 /**
37 * Holds instance of plugin object.
38 *
39 * @var Plugin
40 */
41 public $plugin;
42
43 /**
44 * Registered ability objects keyed by namespaced name.
45 *
46 * @var array<string, Ability>
47 */
48 public $abilities = array();
49
50 /**
51 * Class constructor.
52 *
53 * @param Plugin $plugin Instance of plugin object.
54 */
55 public function __construct( Plugin $plugin ) {
56 $this->plugin = $plugin;
57
58 if ( ! $this->is_available() ) {
59 return;
60 }
61
62 if ( ! $this->is_enabled() ) {
63 return;
64 }
65
66 add_action( 'wp_abilities_api_categories_init', array( $this, 'register_category' ) );
67 add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
68
69 // REST requests don't load Admin, so the dynamic view_stream cap filter
70 // isn't registered. Register an equivalent here so read abilities can
71 // authorize editors / other allowed roles consistently with the admin UI.
72 if ( ! isset( $this->plugin->admin ) ) {
73 add_filter( 'user_has_cap', array( $this, 'filter_user_caps' ), 10, 4 );
74 }
75
76 // MCP exposure is handled by the WordPress MCP Adapter when it's
77 // installed alongside Stream (as a plugin or via Composer). The
78 // adapter boots its own singleton and picks up Stream abilities
79 // because each one advertises meta.mcp.public via Ability::get_meta().
80 // Stream does NOT load or initialize the adapter -- that's the
81 // adapter plugin's responsibility once activated.
82 }
83
84 /**
85 * Grant the dynamic view_stream cap to users whose role appears in
86 * general_role_access. Mirrors Admin::filter_user_caps() for REST contexts
87 * where Admin isn't instantiated.
88 *
89 * @param array $allcaps All capabilities.
90 * @param array $caps Required caps.
91 * @param array $args Unused.
92 * @param \WP_User $user User.
93 * @return array
94 */
95 public function filter_user_caps( $allcaps, $caps, $args, $user = null ) {
96 unset( $args );
97
98 if ( ! in_array( 'view_stream', (array) $caps, true ) ) {
99 return $allcaps;
100 }
101
102 // Read through Settings::get_setting_value() so the network-level option
103 // (wp_stream_network) is consulted on network-activated multisite.
104 // In REST contexts is_network_admin() is always false, so a direct
105 // $plugin->settings->options read would hit the empty per-site option
106 // and silently drop view_stream for users granted access via the
107 // network admin's Role Access setting.
108 $role_access = (array) $this->plugin->settings->get_setting_value( 'general_role_access', array() );
109
110 if ( empty( $role_access ) ) {
111 return $allcaps;
112 }
113
114 $user = is_a( $user, '\WP_User' ) ? $user : wp_get_current_user();
115 if ( ! $user || ! $user->exists() ) {
116 return $allcaps;
117 }
118
119 global $wp_roles;
120 $_wp_roles = isset( $wp_roles ) ? $wp_roles : new \WP_Roles();
121
122 $roles = array_unique(
123 array_merge(
124 $user->roles,
125 array_filter(
126 array_keys( $user->caps ),
127 array( $_wp_roles, 'is_role' )
128 )
129 )
130 );
131
132 foreach ( $roles as $role ) {
133 if ( in_array( $role, $role_access, true ) ) {
134 $allcaps['view_stream'] = true;
135 break;
136 }
137 }
138
139 return $allcaps;
140 }
141
142 /**
143 * Hooked to wp_abilities_api_categories_init. Registers the "stream" ability category.
144 *
145 * @return void
146 */
147 public function register_category() {
148 if ( ! function_exists( 'wp_register_ability_category' ) ) {
149 return;
150 }
151
152 // Skip when the category is already registered. Without this guard,
153 // re-running the bootstrap (e.g. multiple loader instances in tests)
154 // triggers a core _doing_it_wrong notice. Mirrors the idempotency
155 // pattern in register_abilities().
156 if ( function_exists( 'wp_has_ability_category' ) && wp_has_ability_category( self::CATEGORY_SLUG ) ) {
157 return;
158 }
159
160 wp_register_ability_category(
161 self::CATEGORY_SLUG,
162 array(
163 'label' => __( 'Stream', 'stream' ),
164 'description' => __( 'Abilities that read or modify Stream activity logs and configuration.', 'stream' ),
165 )
166 );
167 }
168
169 /**
170 * Whether the WordPress Abilities API is available (WP 6.9+).
171 *
172 * @return bool
173 */
174 public function is_available() {
175 return class_exists( '\WP_Ability' );
176 }
177
178 /**
179 * Whether the integration is enabled in Stream settings.
180 *
181 * Delegates to Settings::get_setting_value() so the multisite/network
182 * fallback logic lives next to the settings storage.
183 *
184 * @return bool
185 */
186 public function is_enabled() {
187 if ( ! isset( $this->plugin->settings ) ) {
188 return false;
189 }
190
191 return ! empty(
192 $this->plugin->settings->get_setting_value( 'advanced_' . self::SETTING_NAME )
193 );
194 }
195
196 /**
197 * List of ability slugs to load. Each maps to abilities/class-ability-{slug}.php
198 * and class WP_Stream\Ability_{Slug_With_Underscores}.
199 *
200 * @return array
201 */
202 public function get_ability_slugs() {
203 return array(
204 // Read-only.
205 'get-records',
206 'get-record',
207 'get-settings',
208 'get-alerts',
209 'get-connectors',
210 'get-exclusion-rules',
211
212 // Write.
213 'create-alert',
214 'update-settings',
215 'create-exclusion-rule',
216
217 // Destructive.
218 'purge-records',
219 'delete-alert',
220 );
221 }
222
223 /**
224 * Require ability files and instantiate their classes.
225 *
226 * @return void
227 */
228 public function load_abilities() {
229 $dir = trailingslashit( $this->plugin->locations['dir'] ) . 'abilities/';
230
231 // Load shared trait once before any ability file is included. The
232 // read abilities `use Trait_View_Stream_Permission` and PHP needs the
233 // trait declared before the class declaration is parsed. Doing it
234 // here keeps the require centralized -- new read abilities don't
235 // have to remember to require the trait themselves.
236 require_once $dir . 'trait-view-stream-permission.php';
237
238 foreach ( $this->get_ability_slugs() as $slug ) {
239 $file = $dir . 'class-ability-' . $slug . '.php';
240 if ( ! is_readable( $file ) ) {
241 continue;
242 }
243 include_once $file;
244
245 $class_part = implode( '_', array_map( 'ucfirst', explode( '-', $slug ) ) );
246 $class = '\WP_Stream\Ability_' . $class_part;
247 if ( ! class_exists( $class ) ) {
248 continue;
249 }
250
251 $ability = new $class( $this->plugin );
252 if ( ! $ability instanceof Ability ) {
253 continue;
254 }
255
256 $this->abilities[ $ability->get_name() ] = $ability;
257 }
258 }
259
260 /**
261 * Hooked to wp_abilities_api_init. Loads and registers all abilities.
262 *
263 * @return void
264 */
265 public function register_abilities() {
266 if ( empty( $this->abilities ) ) {
267 $this->load_abilities();
268 }
269
270 foreach ( $this->abilities as $ability ) {
271 // Defensive: skip if another loader instance already registered this
272 // ability (e.g. duplicate plugin instances in tests, multisite hooks).
273 // Re-registering would emit a _doing_it_wrong notice from core.
274 if ( function_exists( 'wp_has_ability' ) && wp_has_ability( $ability->get_name() ) ) {
275 continue;
276 }
277 $ability->register();
278 }
279 }
280 }
281