Passed
Push — master ( 9727f8...a3fb64 )
by Andreas
22:43
created

midcom_services_auth_acl   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Test Coverage

Coverage 79.74%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 147
dl 0
loc 420
ccs 122
cts 153
cp 0.7974
rs 3.6
c 3
b 0
f 0
wmc 60

13 Methods

Rating   Name   Duplication   Size   Complexity  
A get_default_privileges() 0 3 1
A _get_class_magic_privileges() 0 20 4
A register_default_privileges() 0 10 4
A __construct() 0 2 1
A get_owner_default_privileges() 0 3 1
A privilege_exists() 0 3 1
A get_user_id() 0 16 6
B can_do_byguid() 0 55 10
A get_parent_data() 0 9 1
B can_do_byclass() 0 50 9
C _load_content_privilege() 0 58 16
A _can_do_internal_sudo() 0 3 1
A _get_user_per_class_privileges() 0 22 5

How to fix   Complexity   

Complex Class

Complex classes like midcom_services_auth_acl often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use midcom_services_auth_acl, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package midcom.services
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * This class is responsible for ACL checks against classes and content objects.
11
 *
12
 * <b>Privilege definition</b>
13
 *
14
 * Privileges are represented by the class midcom_core_privilege and basically consist
15
 * of three parts: Name, Assignee and Value:
16
 *
17
 * The privilege name is a unique identifier for the privilege. The mRFC 15 defines the
18
 * syntax to be $component:$name, where $component is either the name of the component
19
 * or one of 'midgard' or 'midcom' for core privileges. Valid privilege names are for
20
 * example 'net.nehmer.static:do_something' or 'midgard:update'.
21
 *
22
 * The assignee is the entity to which the privilege applies, this can be one of several
23
 * things, depending on where the privilege is taken into effect, I'll explain this below
24
 * in more detail:
25
 *
26
 * On content objects (generally every object in the system used during 'normal operation'):
27
 *
28
 * - A Midgard User encapsulated by a midcom_core_user object.
29
 * - A Midgard Group encapsulated by a midcom_core_group object or subtype thereof.
30
 * - The magic assignee 'EVERYONE', which applies the privilege to every user unconditionally,
31
 *   even to unauthenticated users.
32
 * - The magic assignee 'USERS', which applies to all authenticated users.
33
 * - The magic assignee 'ANONYMOUS, which applies to all unauthenticated users.
34
 * - The magic assignee 'OWNER', which applies for all object owners.
35
 *
36
 * On users and groups during authentication (when building the basic privilege set for the user,
37
 * which applies generally):
38
 *
39
 * - The magic string 'SELF', which denotes that the privilege is set for the user in general for
40
 *   every content object. SELF privileges may be restricted to a class by using the classname
41
 *   property available at both midcom_core_privilege and various DBA interface functions.
42
 *
43
 * The value is one of MIDCOM_PRIVILEGE_ALLOW or MIDCOM_PRIVILEGE_DENY, which either grants or
44
 * revokes a privilege. Be aware, that unsetting a privilege does not set it to MIDCOM_PRIVILEGE_DENY,
45
 * but clears the entry completely, which means that the privilege value inherited from the parents
46
 * is now in effect.
47
 *
48
 * <b>How are privileges read and merged</b>
49
 *
50
 * First, you have to understand, that there are actually three distinct sources where a privilege
51
 * comes from: The systemwide defaults, the currently authenticated user and the content object
52
 * which is being operated on. We'll look into this distinction first, before we get on to the order
53
 * in which they are merged.
54
 *
55
 * <i>Systemwide default privileges</i>
56
 *
57
 * This is analogous to the MidCOM default configuration, they are taken into account globally to each
58
 * and every check whether a privilege is granted. Whenever a privilege is defined, there is also a
59
 * default value (either ALLOW or DENY) assigned to it. They serve as a basis for all privilege sets
60
 * and ensure that there is a value set for all privileges.
61
 *
62
 * These defaults are defined by the MidCOM core and the components respectively and are very restrictive,
63
 * basically granting read-only access to all non sensitive information.
64
 *
65
 * Currently, there is no way to influence these privileges unless you are a developer and writing new
66
 * components.
67
 *
68
 * <i>Class specific, systemwide default privileges (for magic assignees only)</i>
69
 *
70
 * Often you want to have a number of default privileges for certain classes in general. For regular
71
 * users/groups you can easily assign them to the corresponding users/groups, there is one special
72
 * case which cannot be covered there at this time: You cannot set defaults applicable for the magic
73
 * assignees EVERYONE, USERS and ANONYMOUS. This is normally only of interest for component authors,
74
 * which want to have some special privileges assigned for their objects, where the global defaults
75
 * do no longer suffice.
76
 *
77
 * These privileges are queried using a static callback of the DBA classes in question, see the following
78
 * example:
79
 *
80
 * <code>
81
 * public function get_class_magic_default_privileges()
82
 * {
83
 *     return Array (
84
 *         'EVERYONE' => [],
85
 *         'ANONYMOUS' => [],
86
 *         'USERS' => ['midcom:create' => MIDCOM_PRIVILEGE_ALLOW]
87
 *     );
88
 * }
89
 * </code>
90
 *
91
 * See also the documentation of the $_default_magic_class_privileges member for further details.
92
 *
93
 * <i>User / Group specific privileges</i>
94
 *
95
 * This kind of privileges are rights, assigned directly to a user. Similar to the systemwide defaults,
96
 * they too apply to any operation done by the user / group respectively throughout the system. The magic
97
 * assignee SELF is used to denote such privileges, which can obviously only be assigned to users or
98
 * groups. These privileges are loaded at the time of user authentication only.
99
 *
100
 * You should use these privileges carefully, due to their global nature. If you assign the privilege
101
 * midgard:delete to a user, this means that the user can now delete all objects he can read, unless
102
 * there are again restricting privileges set to content objects.
103
 *
104
 * To be more flexible in the control over the top level objects, you may add a classname which restricts
105
 * the validity of the privilege to a class and all of its descendants.
106
 *
107
 * <i>Content object privileges</i>
108
 *
109
 * This is the kind of privilege that will be used most often. They are associated with any content
110
 * object in the system, and are read on every access to a content object. As you can see in the
111
 * introduction, you have the most flexibility here.
112
 *
113
 * The basic idea is that you can assign privileges based on the combination of users/groups and
114
 * content objects. In other words, you can say the user x has the privilege midgard:update for
115
 * this object (and its descendants) only. This works with groups as well.
116
 *
117
 * The possible assignees here are either a user, a group or one of the magic assignees EVERYONE,
118
 * USERS or ANONYMOUS, as outlined above.
119
 *
120
 * Be aware, that persons and groups are treted as content objects when loaded from the database
121
 * in a tool like org.openpsa.user, as the groups are not used for authentication but for
122
 * regular site operation there. Therefore, the SELF privileges mentioned above are not taken into
123
 * account when determining the content object privileges!
124
 *
125
 * <i>Privilege merging</i>
126
 *
127
 * This is where we get to the guts of privilege system, as this is not trivial (but nevertheless
128
 * straight-forward I hope). The general idea is based on the scope of object a privilege applies:
129
 *
130
 * System default privileges obviously have the largest scope, they apply to everyone. The next
131
 * smaller scope are privileges which are assigned to groups in general, followed by privileges
132
 * assigned directly to a user.
133
 *
134
 * From this point on, the privileges of the content objects are next in line, starting at the
135
 * top-level objects again (for example a root topic). The smallest scope finally then has the
136
 * object that is being accessed itself.
137
 *
138
 * Let us visualize this a bit:
139
 *
140
 * <pre>
141
 * ^ larger scope     System default privileges
142
 * |                  Class specific magic assignee default privileges
143
 * |                  Root Midgard group
144
 * |                  ... more parent Midgard groups ...
145
 * |                  Direct Midgard group membership
146
 * |                  User
147
 * |                  SELF privileges limited to a class
148
 * |                  Root content object
149
 * |                  ... more parent objects ...
150
 * v smaller scope    Accessed content object
151
 * </pre>
152
 *
153
 * Privileges assigned to a specific user always override owner privileges; owner privileges are
154
 * calculated on a per-content-object bases, and are merged just before the final user privileges are
155
 * merged into the privilege set. It is of no importance from where you get ownership at that point.
156
 *
157
 * Implementation notes: Internally, MidCOM separates the "user privilege set" which is everything
158
 * down to the line User above, and the content object privileges, which constitutes the rest.
159
 * This separation has been done for performance reasons, as the user's privileges are loaded
160
 * immediately upon authentication of the user, and the privileges of the actual content objects
161
 * are merged into this set then. Normally, this should be of no importance for ACL users, but it
162
 * explains the more complex graph in the original mRFC.
163
 *
164
 * <b>Predefined Privileges</b>
165
 *
166
 * The MidCOM core defines a set of core privileges, which fall in two categories:
167
 *
168
 * <i>Midgard Core Privileges</i>
169
 *
170
 * These privileges are part of the MidCOM Database Abstraction layer (MidCOM DBA) and have been
171
 * originally proposed by me in a mail to the Midgard developers list. Unless otherwise noted,
172
 * all privileges are denied by default and no difference between owner and normal default privileges
173
 * is made.
174
 *
175
 * - <i>midgard:read</i> controls read access to the object, if denied, you cannot load the object
176
 *   from the database. This privilege is granted by default.
177
 * - <i>midgard:update</i> controls updating of objects. Be aware that you need to be able to read
178
 *   the object before updating it, it is granted by default only for owners.
179
 * - <i>midgard:delete</i> controls deletion of objects. Be aware that you need to be able to read
180
 *   the object before updating it, it is granted by default only for owners.
181
 * - <i>midgard:create</i> allows you to create new content objects as children on whatever content
182
 *   object that you have the create privilege for. This means that you can create an article if and only
183
 *   if you have create permission for either the parent article (if you create a so-called 'reply
184
 *   article') or the parent topic, it is granted by default only for owners.
185
 * - <i>midgard:parameters</i> allows the manipulation of parameters on the current object if and
186
 *   only if the user also has the midgard:update privilege on the object. This privileges is granted
187
 *   by default and covers the full set of parameter operations (create, update and delete).
188
 * - <i>midgard:attachments</i> is analogous to midgard:parameters but covers attachments instead
189
 *   and is also granted by default.
190
 * - <i>midgard:autoserve_attachment</i> controls whether an attachment may be autoserved using
191
 *   the midcom-serveattachmentguid handler. This is granted by default, allowing every attachment
192
 *   to be served using the default URL methods. Denying this right allows component authors to
193
 *   build more sophisticated access control restrictions to attachments.
194
 * - <i>midgard:privileges</i> allows the user to change the permissions on the objects they are
195
 *   granted for. You also need midgard:update and midgard:parameters to properly execute these
196
 *   operations.
197
 * - <i>midgard:owner</i> indicates that the user who has this privilege set is an owner of the
198
 *   given content object.
199
 *
200
 * <i>MidCOM Core Privileges</i>
201
 *
202
 * - <i>midcom:approve</i> grants the user the right to approve or unapprove objects.
203
 * - <i>midcom:component_config</i> grants the user access to configuration management system,
204
 *   it is granted by default only for owners.
205
 * - <i>midcom:isonline</i> is needed to see the online state of another user. It is not granted
206
 *   by default.
207
 *
208
 * <b>Assigning Privileges</b>
209
 *
210
 * See the documentation of the DBA layer for more information.
211
 *
212
 * @package midcom.services
213
 */
214
class midcom_services_auth_acl
215
{
216
    /**
217
     * This is an internal flag used to override all regular permission checks with a sort-of
218
     * read-only privilege set. While internal_sudo is enabled, the system automatically
219
     * grants all privileges except midgard:create, midgard:update, midgard:delete and
220
     * midgard:privileges, which will always be denied. These checks go after the basic checks
221
     * for not authenticated users or admin level users.
222
     *
223
     * @var boolean
224
     */
225
    private $_internal_sudo = false;
226
227
    /**
228
     * Internal listing of all default privileges currently registered in the system. This
229
     * is a privilege name/value map.
230
     *
231
     * @var array
232
     */
233
    private static $_default_privileges = [];
234
235
    /**
236
     * Internal listing of all default owner privileges currently registered in the system.
237
     * All privileges not set in this list will be inherited. This is a privilege name/value
238
     * map.
239
     *
240
     * @var array
241
     */
242
    private static $_owner_default_privileges = [];
243
244
    /**
245
     * This listing contains all magic privileges assigned to the existing classes. It is a
246
     * multi-level array, example entry:
247
     *
248
     * <pre>
249
     * 'class_name' => Array
250
     * (
251
     *     'EVERYONE' => [],
252
     *     'ANONYMOUS' => [],
253
     *     'USERS' => Array
254
     *     (
255
     *         'midcom:create' => MIDCOM_PRIVILEGE_ALLOW,
256
     *         'midcom:update' => MIDCOM_PRIVILEGE_ALLOW
257
     *     ),
258
     * )
259
     * </pre>
260
     *
261
     * @var array
262
     */
263
    private static $_default_magic_class_privileges = [];
264
265
    /**
266
    * Internal cache of the content privileges of users on content objects, this is
267
    * an associative array using a combination of the user identifier and the object's
268
    * guid as index. The privileges for the anonymous user use the magic
269
    * EVERYONE as user identifier.
270
    *
271
    * This must not be merged with the class-wide privileges_cache, because otherwise
272
    * class_default_privileges for child objects might be overridden by parent default
273
    * privileges
274
    *
275
    * @var Array
276
    */
277
    private static $_content_privileges_cache = [];
278
279
    /**
280
     * Constructor.
281
     */
282
    public function __construct()
283
    {
284
    }
285
286
    /**
287
     * Merges a new set of default privileges into the current set.
288
     * Existing keys will be silently overwritten.
289
     *
290
     * This is usually only called by the framework startup and the
291
     * component loader.
292
     *
293
     * If only a single default value is set (type integer), then this value is taken
294
     * for the default and the owner privilege is unset (meaning INHERIT). If two
295
     * values (type array of integers) is set, the first privilege value is used for
296
     * default, the second one for the owner privilege set.
297
     *
298
     * @param array $privileges An associative privilege_name => default_values listing.
299
     */
300
    public function register_default_privileges($privileges)
301
    {
302
        foreach ($privileges as $name => $values) {
303
            if (!is_array($values)) {
304
                $values = [$values, MIDCOM_PRIVILEGE_INHERIT];
305
            }
306
307
            self::$_default_privileges[$name] = $values[0];
308
            if ($values[1] != MIDCOM_PRIVILEGE_INHERIT) {
309
                self::$_owner_default_privileges[$name] = $values[1];
310
            }
311
        }
312
    }
313
314
    /**
315
     * Returns the system-wide basic privilege set.
316
     *
317
     * @return Array Privilege Name / Value map.
318
     */
319 3
    public function get_default_privileges() : array
320
    {
321 3
        return self::$_default_privileges;
322
    }
323
324
    /**
325
     * Returns the system-wide basic owner privilege set.
326
     *
327
     * @return Array Privilege Name / Value map.
328
     */
329 114
    public function get_owner_default_privileges() : array
330
    {
331 114
        return self::$_owner_default_privileges;
332
    }
333
334
    /**
335
     * Load and prepare the list of class magic privileges for usage.
336
     *
337
     * @param string $class The class name for which defaults should be loaded.
338
     * @param mixed $user The user to check
339
     */
340 79
    private function _get_class_magic_privileges(string $class, $user) : array
341
    {
342 79
        if (!array_key_exists($class, self::$_default_magic_class_privileges)) {
343
            $privs = [
344 17
                'EVERYONE' => [],
345
                'ANONYMOUS' => [],
346
                'USERS' => []
347
            ];
348
349 17
            if (method_exists($class, 'get_class_magic_default_privileges')) {
350 14
                $object = new $class();
351 14
                $privs = $object->get_class_magic_default_privileges();
352
            }
353
354 17
            self::$_default_magic_class_privileges[$class] = $privs;
355
        }
356 79
        $dmcp_user = $user === null ? 'ANONYMOUS' : 'USERS';
357 79
        return array_merge(
358 79
            self::$_default_magic_class_privileges[$class]['EVERYONE'],
359 79
            self::$_default_magic_class_privileges[$class][$dmcp_user]
360
        );
361
    }
362
363 13
    private function _get_user_per_class_privileges(string $classname, midcom_core_user $user) : array
364
    {
365 13
        static $cache = [];
366
367 13
        $cache_id = $user->id . '::' . $classname;
368
369 13
        if (!array_key_exists($cache_id, $cache)) {
370 12
            $cache[$cache_id] = [];
371 12
            if (is_subclass_of($classname, midcom_core_dbaobject::class)) {
372
                // in case of DBA classes we also want to match the mgd ones,
373
                // and for that dbafactory's is_a() needs an object
374 10
                $classname = new $classname;
375
            }
376
377 12
            foreach ($user->get_per_class_privileges() as $class => $privileges) {
378 1
                if (midcom::get()->dbfactory->is_a($classname, $class, true)) {
379 1
                    $cache[$cache_id] = array_merge($cache[$cache_id], $privileges);
380
                }
381
            }
382
        }
383
384 13
        return $cache[$cache_id];
385
    }
386
387
    /**
388
     * Determine the user identifier for accessing the privilege cache. This is the passed user's
389
     * identifier with the current user and anonymous as fallback
390
     *
391
     * @param mixed $user The user to check for as string or object.
392
     * @return string The identifier
393
     */
394 448
    public function get_user_id($user = null)
395
    {
396 448
        if ($user === null) {
397 448
            return midcom::get()->auth->user->id ?? 'ANONYMOUS';
398
        }
399 4
        if (is_string($user)) {
400 2
            if (mgd_is_guid($user) || is_numeric($user)) {
401
                return midcom::get()->auth->get_user($user)->id;
402
            }
403
            // Could be a magic assignee (?)
404 2
            return $user;
405
        }
406 2
        if (is_object($user)) {
407 2
            return $user->id;
408
        }
409
        return $user;
410
    }
411
412
    /**
413
     * Validate whether a given privilege exists by its name. Essentially this checks
414
     * if a corresponding default privilege has been registered in the system.
415
     *
416
     * @todo This call should load the component associated to the privilege on demand.
417
     * @param string $name The name of the privilege to check.
418
     */
419 117
    public function privilege_exists($name) : bool
420
    {
421 117
        return array_key_exists($name, self::$_default_privileges);
422
    }
423
424 7
    public function can_do_byclass($privilege, $user, $class) : bool
425
    {
426 7
        if ($this->_internal_sudo) {
427
            debug_add('INTERNAL SUDO mode is enabled. Generic Read-Only mode set.');
428
            return $this->_can_do_internal_sudo($privilege);
429
        }
430
431 7
        $default_magic_class_privileges = [];
432 7
        $user_privileges = [];
433 7
        $user_per_class_privileges = [];
434
435 7
        if ($user !== null) {
436 7
            $user_privileges = $user->get_privileges();
437
        }
438
439 7
        if ($class !== null) {
440 6
            if (is_object($class)) {
441
                $class = get_class($class);
442 6
            } elseif (!class_exists($class)) {
443
                debug_add("can_user_do check to undefined class '{$class}'.", MIDCOM_LOG_ERROR);
444
                return false;
445
            }
446
447 6
            $default_magic_class_privileges = $this->_get_class_magic_privileges($class, $user);
448 6
            if ($user !== null) {
449 6
                $user_per_class_privileges = $this->_get_user_per_class_privileges($class, $user);
450
            }
451
        }
452
453 7
        $full_privileges = array_merge(
454 7
            self::$_default_privileges,
455 7
            $default_magic_class_privileges,
456 7
            $user_privileges,
457 7
            $user_per_class_privileges
458
        );
459
460
        // Check for Ownership:
461 7
        if ($full_privileges['midgard:owner'] == MIDCOM_PRIVILEGE_ALLOW) {
462
            $full_privileges = array_merge(
463
                $full_privileges,
464
                $this->get_owner_default_privileges()
465
            );
466
        }
467
468 7
        if (!array_key_exists($privilege, $full_privileges)) {
469
            debug_add("Warning, the privilege {$privilege} is unknown at this point. Assuming not granted privilege.");
470
            return false;
471
        }
472
473 7
        return $full_privileges[$privilege] == MIDCOM_PRIVILEGE_ALLOW;
474
    }
475
476
    /**
477
     * Checks whether a user has a certain privilege on the given (via guid and class) content object.
478
     * Works on the currently authenticated user by default, but can take another
479
     * user as an optional argument.
480
     *
481
     * @param string $privilege The privilege to check for
482
     * @param string $object_guid A Midgard GUID pointing to an object
483
     * @param string $object_class Class of the object in question
484
     * @param string $user_id The user against which to check the privilege, defaults to the currently authenticated user.
485
     *     You may specify "EVERYONE" instead of an object to check what an anonymous user can do.
486
     */
487 447
    public function can_do_byguid($privilege, $object_guid, $object_class, $user_id) : bool
488
    {
489 447
        if ($this->_internal_sudo) {
490
            return $this->_can_do_internal_sudo($privilege);
491
        }
492
493 447
        if (midcom::get()->auth->is_component_sudo()) {
494 428
            return true;
495
        }
496 123
        static $cache = [];
497
498 123
        $cache_prefix = "{$user_id}::{$object_guid}";
499 123
        $cache_key = $cache_prefix . "::{$privilege}";
500
501 123
        if (isset($cache[$cache_key])) {
502 36
            return $cache[$cache_key];
503
        }
504
505 114
        if ($this->_load_content_privilege($privilege, $object_guid, $object_class, $user_id)) {
506 47
            $cache[$cache_key] = self::$_content_privileges_cache[$cache_prefix][$privilege];
507 47
            return $cache[$cache_key];
508
        }
509
510
        // user privileges
511 77
        if ($user = midcom::get()->auth->get_user($user_id)) {
512 10
            $user_per_class_privileges = $this->_get_user_per_class_privileges($object_class, $user);
513
514 10
            if (array_key_exists($privilege, $user_per_class_privileges)) {
515 1
                $cache[$cache_key] = ($user_per_class_privileges[$privilege] == MIDCOM_PRIVILEGE_ALLOW);
516 1
                return $cache[$cache_key];
517
            }
518
519 10
            $user_privileges = $user->get_privileges();
520
521 10
            if (array_key_exists($privilege, $user_privileges)) {
522
                $cache[$cache_key] = ($user_privileges[$privilege] == MIDCOM_PRIVILEGE_ALLOW);
523
                return $cache[$cache_key];
524
            }
525
        }
526
527
        // default magic class privileges user
528 77
        $dmcp = $this->_get_class_magic_privileges($object_class, midcom::get()->auth->user);
529
530 77
        if (array_key_exists($privilege, $dmcp)) {
531
            $cache[$cache_key] = ($dmcp[$privilege] == MIDCOM_PRIVILEGE_ALLOW);
532
            return $cache[$cache_key];
533
        }
534
535 77
        if (array_key_exists($privilege, self::$_default_privileges)) {
536 77
            $cache[$cache_key] = (self::$_default_privileges[$privilege] == MIDCOM_PRIVILEGE_ALLOW);
537 77
            return $cache[$cache_key];
538
        }
539
540
        debug_add("The privilege {$privilege} is unknown at this point. Assuming not granted privilege.", MIDCOM_LOG_WARN);
541
        return false;
542
    }
543
544
    /**
545
     * Look up a specific content privilege and cache the result.
546
     *
547
     * @param string $privilegename The privilege to check for
548
     * @param string $guid A Midgard GUID pointing to an object
549
     * @param string $class DBA Class of the object in question
550
     * @param string $user_id The user against which to check the privilege, defaults to the currently authenticated user.
551
     * @return boolean True when privilege was found, otherwise false
552
     */
553 114
    private function _load_content_privilege(string $privilegename, string $guid, string $class, $user_id) : bool
554
    {
555 114
        $cache_id = $user_id . '::' . $guid;
556
557 114
        if (!array_key_exists($cache_id, self::$_content_privileges_cache)) {
558 110
            self::$_content_privileges_cache[$cache_id] = [];
559
        }
560 114
        if (array_key_exists($privilegename, self::$_content_privileges_cache[$cache_id])) {
561 7
            return true;
562
        }
563
564 114
        $object_privileges = midcom_core_privilege::get_content_privileges($guid);
565
566 114
        $last_scope = -1;
567 114
        $content_privilege = null;
568
569 114
        foreach ($object_privileges as $privilege) {
570 75
            if ($privilege->privilegename == $privilegename) {
571 72
                $scope = $privilege->get_scope();
572 72
                if ($scope > $last_scope && $privilege->does_privilege_apply($user_id)) {
573 44
                    $last_scope = $scope;
574 44
                    $content_privilege = $privilege;
575
                }
576
            }
577
        }
578
579
        //owner privileges override everything but person privileges, so we have to cross-check those here
580 114
        if (   $privilegename != 'midgard:owner'
581 114
            && $last_scope < MIDCOM_PRIVILEGE_SCOPE_OWNER) {
582 114
            $owner_privileges = $this->get_owner_default_privileges();
583 114
            if (    array_key_exists($privilegename, $owner_privileges)
584 114
                 && $this->_load_content_privilege('midgard:owner', $guid, $class, $user_id)
585 114
                 && self::$_content_privileges_cache[$cache_id]['midgard:owner']) {
586 44
                self::$_content_privileges_cache[$cache_id][$privilegename] = ($owner_privileges[$privilegename] == MIDCOM_PRIVILEGE_ALLOW);
587 44
                return true;
588
            }
589
        }
590
591 113
        if ($content_privilege !== null) {
592 44
            self::$_content_privileges_cache[$cache_id][$privilegename] = ($content_privilege->value == MIDCOM_PRIVILEGE_ALLOW);
593 44
            return true;
594
        }
595
596
        //if nothing was found, we try to recurse to parent
597 77
        list ($parent_guid, $parent_class) = $this->get_parent_data($guid, $class);
598
599 77
        if (   $parent_guid == $guid
600 77
            || !mgd_is_guid($parent_guid)) {
601 77
            return false;
602
        }
603
604 25
        $parent_cache_id = $user_id . '::' . $parent_guid;
605 25
        if ($this->_load_content_privilege($privilegename, $parent_guid, $parent_class, $user_id)) {
606 1
            self::$_content_privileges_cache[$cache_id][$privilegename] = self::$_content_privileges_cache[$parent_cache_id][$privilegename];
607 1
            return true;
608
        }
609
610 25
        return false;
611
    }
612
613 77
    private function get_parent_data(string $guid, string $class) : array
614
    {
615
        // ==> into SUDO
616 77
        $previous_sudo = $this->_internal_sudo;
617 77
        $this->_internal_sudo = true;
618 77
        $parent_data = midcom::get()->dbfactory->get_parent_data($guid, $class);
619 77
        $this->_internal_sudo = $previous_sudo;
620
        // <== out of SUDO
621 77
        return [current($parent_data), key($parent_data)];
622
    }
623
624
    /**
625
     * This internal helper checks if a privilege is available during internal
626
     * sudo mode, as outlined in the corresponding variable.
627
     *
628
     * @param string $privilege The privilege to check for
629
     * @see $_internal_sudo
630
     */
631
    private function _can_do_internal_sudo(string $privilege) : bool
632
    {
633
        return !in_array($privilege, ['midgard:create', 'midgard:update', 'midgard:delete', 'midgard:privileges']);
634
    }
635
}
636