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-ability.php

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

264 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abstract base class for Stream abilities (WordPress Abilities API integration).
4 *
5 * @package WP_Stream
6 */
7
8 namespace WP_Stream;
9
10 /**
11 * Class - Ability
12 *
13 * Subclasses define a single Stream operation that is exposed via the
14 * WordPress Abilities API. Each subclass declares a namespaced name,
15 * input/output JSON Schemas, a permission callback, and an execute
16 * callback. Subclasses are instantiated and registered by Abilities.
17 */
18 abstract class Ability {
19
20 /**
21 * Holds instance of plugin object.
22 *
23 * @var Plugin
24 */
25 protected $plugin;
26
27 /**
28 * Class constructor.
29 *
30 * @param Plugin $plugin Instance of plugin object.
31 */
32 public function __construct( Plugin $plugin ) {
33 $this->plugin = $plugin;
34 }
35
36 /**
37 * Namespaced ability name (e.g. "stream/get-records").
38 *
39 * @return string
40 */
41 abstract public function get_name();
42
43 /**
44 * Short human-readable label.
45 *
46 * @return string
47 */
48 abstract public function get_label();
49
50 /**
51 * Description of what the ability does.
52 *
53 * @return string
54 */
55 abstract public function get_description();
56
57 /**
58 * JSON Schema for ability input. Return an empty array for no input.
59 *
60 * @return array
61 */
62 abstract public function get_input_schema();
63
64 /**
65 * JSON Schema for ability output.
66 *
67 * @return array
68 */
69 abstract public function get_output_schema();
70
71 /**
72 * Execute the ability.
73 *
74 * The default value is `null` to match WP core's invoke_callback() contract:
75 * when an ability has no input_schema, core calls the callback with zero
76 * arguments. PHP's ArgumentCountError would otherwise fatal here. Subclasses
77 * that declare a non-empty input_schema can rely on $input being a parsed
78 * array (core enforces this via rest_validate_value_from_schema()).
79 *
80 * @param mixed $input Validated input matching get_input_schema(), or null
81 * when the ability declares no input_schema.
82 * @return mixed|\WP_Error Result conforming to get_output_schema(), or WP_Error.
83 */
84 abstract public function execute( $input = null );
85
86 /**
87 * Permission check. Defaults to the Stream settings capability so write
88 * abilities are safe by default; read-only abilities should override with
89 * the view capability (e.g. 'view_stream') to follow least-privilege.
90 *
91 * @param array $input Input that will be passed to execute().
92 * @return bool|\WP_Error
93 */
94 public function permission_callback( $input = array() ) {
95 unset( $input );
96 return current_user_can( WP_STREAM_SETTINGS_CAPABILITY );
97 }
98
99 /**
100 * Permission check for abilities that persist Stream settings.
101 *
102 * On network-activated multisite, Settings::update_all_setting_values()
103 * writes the authoritative network option (wp_stream_network), so the write
104 * affects every site on the network. WP_STREAM_SETTINGS_CAPABILITY defaults
105 * to 'manage_options', which every single-site administrator holds -- on its
106 * own it is not sufficient authority for a network-wide change. Require a
107 * network capability whenever the write is going to be network-scoped.
108 *
109 * The network capability is required in addition to the settings
110 * capability, not instead of it: on multisite a super admin passes every
111 * capability check by definition, so returning the network check alone
112 * would silently ignore a deployment that narrowed
113 * WP_STREAM_SETTINGS_CAPABILITY.
114 *
115 * @return bool
116 */
117 protected function can_write_settings() {
118 $can_write_settings = current_user_can( WP_STREAM_SETTINGS_CAPABILITY );
119
120 // Mirrors the branch in Settings::update_all_setting_values(): when the
121 // write lands on the network option it additionally requires network
122 // authority. This is an extra requirement, never a substitute -- the
123 // settings capability is a site-overridable constant, so a deployment
124 // that restricts or revokes it must keep being honoured for super
125 // admins too.
126 if ( is_multisite() && $this->plugin->is_network_activated() ) {
127 return $can_write_settings && current_user_can( 'manage_network_options' );
128 }
129
130 return $can_write_settings;
131 }
132
133 /**
134 * Alert meta keys holding reusable credentials rather than configuration.
135 *
136 * Alert destinations are configured by users with the Stream settings
137 * capability, but alerts are readable through get-alerts by anyone with
138 * `view_stream`. These values are bearer credentials -- a Slack incoming
139 * webhook URL is sufficient on its own to post into the target channel, and
140 * an IFTTT Maker key is sufficient to fire that account's applets -- so they
141 * must not cross that privilege boundary.
142 *
143 * @const array
144 */
145 const SECRET_ALERT_META_KEYS = array(
146 'webhook',
147 'maker_key',
148 );
149
150 /**
151 * Replace credential values in an alert_meta array with a boolean marker.
152 *
153 * Callers still need to know whether a destination is configured, so each
154 * secret key is replaced by `{key}_configured` rather than dropped. The
155 * value itself never leaves the site.
156 *
157 * Returns stdClass for empty input so wp_json_encode() emits `{}` and
158 * satisfies the `alert_meta: object` output schema (an empty PHP array
159 * encodes as `[]`).
160 *
161 * @param mixed $alert_meta Raw alert meta, usually an array.
162 * @return array|\stdClass
163 */
164 protected function redact_alert_meta( $alert_meta ) {
165 if ( ! is_array( $alert_meta ) || empty( $alert_meta ) ) {
166 return new \stdClass();
167 }
168
169 /**
170 * Filters the alert_meta keys treated as credentials and withheld from
171 * ability output.
172 *
173 * Third-party alert types registered via `wp_stream_alert_types` may
174 * store their own destination secrets under names Stream cannot know
175 * about; add them here so they are redacted too.
176 *
177 * @param array $keys Meta keys to redact.
178 * @param array $alert_meta The alert meta being redacted.
179 */
180 $secret_keys = (array) apply_filters(
181 'wp_stream_secret_alert_meta_keys',
182 self::SECRET_ALERT_META_KEYS,
183 $alert_meta
184 );
185
186 foreach ( $secret_keys as $key ) {
187 if ( ! array_key_exists( $key, $alert_meta ) ) {
188 continue;
189 }
190
191 $alert_meta[ $key . '_configured' ] = ! empty( $alert_meta[ $key ] );
192 unset( $alert_meta[ $key ] );
193 }
194
195 return empty( $alert_meta ) ? new \stdClass() : $alert_meta;
196 }
197
198 /**
199 * Annotation flags for the ability (readonly, destructive, idempotent).
200 *
201 * @return array
202 */
203 public function get_annotations() {
204 return array();
205 }
206
207 /**
208 * Meta passed to wp_register_ability(). Sets REST exposure and (optionally) annotations.
209 *
210 * @return array
211 */
212 public function get_meta() {
213 $meta = array(
214 'show_in_rest' => true,
215 // Mark every Stream ability as MCP-discoverable. When the
216 // WordPress MCP Adapter (wordpress/mcp-adapter) is installed,
217 // the default MCP server exposes abilities with this flag via
218 // its `mcp-adapter/discover-abilities` and `execute-ability`
219 // tools. The flag is harmless if mcp-adapter is not present
220 // (unused meta key). permission_callback still gates execution,
221 // so destructive abilities aren't auto-callable by anonymous
222 // MCP clients.
223 'mcp' => array(
224 'public' => true,
225 ),
226 );
227
228 $annotations = $this->get_annotations();
229 if ( ! empty( $annotations ) ) {
230 $meta['annotations'] = $annotations;
231 }
232
233 return $meta;
234 }
235
236 /**
237 * Register the ability with the Abilities API.
238 *
239 * @return void
240 */
241 final public function register() {
242 if ( ! function_exists( 'wp_register_ability' ) ) {
243 return;
244 }
245
246 $args = array(
247 'label' => $this->get_label(),
248 'description' => $this->get_description(),
249 'category' => Abilities::CATEGORY_SLUG,
250 'output_schema' => $this->get_output_schema(),
251 'execute_callback' => array( $this, 'execute' ),
252 'permission_callback' => array( $this, 'permission_callback' ),
253 'meta' => $this->get_meta(),
254 );
255
256 $input_schema = $this->get_input_schema();
257 if ( ! empty( $input_schema ) ) {
258 $args['input_schema'] = $input_schema;
259 }
260
261 wp_register_ability( $this->get_name(), $args );
262 }
263 }
264