PluginProbe
ActivityPub / 9.0.2
ActivityPub v9.0.2
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / oauth / class-scope.php

class-scope.php in ActivityPub 9.0.2, at includes/oauth/class-scope.php

266 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OAuth 2.0 Scope definitions for ActivityPub C2S.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\OAuth;
9
10 /**
11 * Scope class for OAuth 2.0 scope management.
12 *
13 * Defines available scopes and provides validation methods.
14 */
15 class Scope {
16 /**
17 * Read access scope - read actor profile, collections, and objects.
18 */
19 const READ = 'read';
20
21 /**
22 * Write access scope - create activities via POST to outbox.
23 */
24 const WRITE = 'write';
25
26 /**
27 * Follow access scope - manage following relationships.
28 */
29 const FOLLOW = 'follow';
30
31 /**
32 * Push access scope - subscribe to SSE streams.
33 */
34 const PUSH = 'push';
35
36 /**
37 * Profile access scope - edit actor profile.
38 */
39 const PROFILE = 'profile';
40
41 /**
42 * All available scopes.
43 *
44 * @var array
45 */
46 const ALL = array(
47 self::READ,
48 self::WRITE,
49 self::FOLLOW,
50 self::PUSH,
51 self::PROFILE,
52 );
53
54 /**
55 * SWICG ActivityPub API Basic Profile canonical scope aliases.
56 *
57 * Advertised in OAuth metadata so Basic Profile clients can discover them,
58 * and accepted in scope requests (any `activitypub:read:*` collapses to
59 * `read`, any `activitypub:write:*` collapses to `write`). Enforcement
60 * stays coarse: there is no per-activity-type access control yet.
61 *
62 * @since 9.0.0
63 *
64 * @var array
65 */
66 const CANONICAL_ALIASES = array(
67 'activitypub:read:all',
68 'activitypub:write:all',
69 );
70
71 /**
72 * Human-readable descriptions for each scope.
73 *
74 * @var array
75 */
76 const DESCRIPTIONS = array(
77 self::READ => 'Read actor profile, collections, and objects',
78 self::WRITE => 'Create activities via POST to outbox',
79 self::FOLLOW => 'Manage following relationships',
80 self::PUSH => 'Subscribe to real-time event streams',
81 self::PROFILE => 'Edit actor profile',
82 );
83
84 /**
85 * Default scopes when none are requested.
86 *
87 * Defaults to read-only to prevent granting write access without
88 * explicit scope request (fail-closed on access control).
89 *
90 * @var array
91 */
92 const DEFAULT_SCOPES = array(
93 self::READ,
94 );
95
96 /**
97 * Validate and filter requested scopes.
98 *
99 * Canonical SWICG ActivityPub API Basic Profile scope names of the form
100 * `activitypub:read:*` and `activitypub:write:*` are normalized to the
101 * plugin's internal `read` and `write` scopes before validation.
102 *
103 * @param string|array $scopes The requested scopes (space-separated string or array).
104 * @return array Valid scopes.
105 */
106 public static function validate( $scopes ) {
107 if ( is_string( $scopes ) ) {
108 $scopes = self::parse( $scopes );
109 }
110
111 if ( ! is_array( $scopes ) ) {
112 return self::DEFAULT_SCOPES;
113 }
114
115 $scopes = self::normalize( $scopes );
116 $valid_scopes = array_intersect( $scopes, self::ALL );
117
118 if ( empty( $valid_scopes ) ) {
119 return self::DEFAULT_SCOPES;
120 }
121
122 return array_values( array_unique( $valid_scopes ) );
123 }
124
125 /**
126 * Normalize canonical Basic Profile scope names to internal scopes.
127 *
128 * Maps any `activitypub:read:*` to {@see self::READ} and any
129 * `activitypub:write:*` to {@see self::WRITE}. Unknown values pass through
130 * unchanged so they can be filtered out by the caller.
131 *
132 * @since 9.0.0
133 *
134 * @param array $scopes Requested scope strings.
135 * @return array Normalized scope strings.
136 */
137 public static function normalize( $scopes ) {
138 if ( ! is_array( $scopes ) ) {
139 return array();
140 }
141
142 $normalized = array();
143 foreach ( $scopes as $scope ) {
144 if ( ! is_string( $scope ) || '' === $scope ) {
145 continue;
146 }
147
148 if ( 0 === strpos( $scope, 'activitypub:read:' ) ) {
149 $normalized[] = self::READ;
150 continue;
151 }
152
153 if ( 0 === strpos( $scope, 'activitypub:write:' ) ) {
154 $normalized[] = self::WRITE;
155 continue;
156 }
157
158 $normalized[] = $scope;
159 }
160
161 return $normalized;
162 }
163
164 /**
165 * Return the scope identifiers advertised in OAuth authorization-server metadata.
166 *
167 * Includes the plugin's internal scopes plus the SWICG Basic Profile
168 * canonical aliases so spec-aware clients can discover them.
169 *
170 * @since 9.0.0
171 *
172 * @return array Scope identifiers.
173 */
174 public static function supported() {
175 return array_merge( self::ALL, self::CANONICAL_ALIASES );
176 }
177
178 /**
179 * Parse a space-separated scope string to array.
180 *
181 * @param string $scope_string Space-separated scopes.
182 * @return array Scope array.
183 */
184 public static function parse( $scope_string ) {
185 if ( empty( $scope_string ) || ! is_string( $scope_string ) ) {
186 return array();
187 }
188
189 $scopes = preg_split( '/\s+/', trim( $scope_string ) );
190
191 return array_filter( array_map( 'trim', $scopes ) );
192 }
193
194 /**
195 * Convert scopes array to space-separated string.
196 *
197 * @param array $scopes The scopes array.
198 * @return string Space-separated scope string.
199 */
200 public static function to_string( $scopes ) {
201 if ( ! is_array( $scopes ) ) {
202 return '';
203 }
204
205 return implode( ' ', $scopes );
206 }
207
208 /**
209 * Check if a scope is valid.
210 *
211 * @param string $scope The scope to check.
212 * @return bool True if valid, false otherwise.
213 */
214 public static function is_valid( $scope ) {
215 return in_array( $scope, self::ALL, true );
216 }
217
218 /**
219 * Get the description for a scope.
220 *
221 * @param string $scope The scope.
222 * @return string The description or empty string if not found.
223 */
224 public static function get_description( $scope ) {
225 return self::DESCRIPTIONS[ $scope ] ?? '';
226 }
227
228 /**
229 * Get all scopes with their descriptions.
230 *
231 * @return array Associative array of scope => description.
232 */
233 public static function get_all_with_descriptions() {
234 return self::DESCRIPTIONS;
235 }
236
237 /**
238 * Check if scopes contain a specific scope.
239 *
240 * @param array $scopes The scopes to check.
241 * @param string $scope The scope to look for.
242 * @return bool True if the scope is present.
243 */
244 public static function contains( $scopes, $scope ) {
245 return is_array( $scopes ) && in_array( $scope, $scopes, true );
246 }
247
248 /**
249 * Sanitize callback for scope storage.
250 *
251 * @param mixed $value The value to sanitize.
252 * @return array Sanitized scopes array.
253 */
254 public static function sanitize( $value ) {
255 if ( is_string( $value ) ) {
256 $value = self::parse( $value );
257 }
258
259 if ( ! is_array( $value ) ) {
260 return array();
261 }
262
263 return self::validate( $value );
264 }
265 }
266