midcom_baseclasses_core_dbobject   F
last analyzed

Complexity

Total Complexity 72

Size/Duplication

Total Lines 488
Duplicated Lines 0 %

Test Coverage

Coverage 66.83%

Importance

Changes 0
Metric Value
eloc 183
dl 0
loc 488
ccs 133
cts 199
cp 0.6683
rs 2.64
c 0
b 0
f 0
wmc 72

18 Methods

Rating   Name   Duplication   Size   Complexity  
A update() 0 15 3
A update_pre_checks() 0 14 3
A update_post_ops() 0 5 1
B create_pre_checks() 0 32 9
A post_db_load_checks() 0 10 2
B _pre_check_name() 0 46 8
A refresh() 0 10 2
B create() 0 32 8
A _set_owner_privileges() 0 19 3
A delete_pre_checks() 0 14 3
A get_by_guid() 0 15 4
A get_by_path() 0 14 3
A delete_post_ops() 0 4 1
A get_by_id() 0 22 4
A create_post_ops() 0 6 1
B delete() 0 42 9
A _delete_privileges() 0 11 3
A delete_tree() 0 19 5

How to fix   Complexity   

Complex Class

Complex classes like midcom_baseclasses_core_dbobject 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_baseclasses_core_dbobject, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package midcom.baseclasses
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
use midcom\events\dbaevent;
10
11
/**
12
 * This class only contains static functions which are there to hook into
13
 * the classes you derive from the midcom_core_dbaobject.
14
 *
15
 * The static members will invoke a number of callback methods so that you should
16
 * normally never have to override the base midgard methods like update or the like.
17
 *
18
 * @package midcom.baseclasses
19
 */
20
class midcom_baseclasses_core_dbobject
21
{
22
    /**
23
     * "Pre-flight" checks for update method
24
     *
25
     * Separated so that dbfactory->import() can reuse the code
26
     */
27 130
    public static function update_pre_checks(midcom_core_dbaobject $object) : bool
28
    {
29 130
        if (!$object->can_do('midgard:update')) {
30 12
            debug_add("Failed to update object, update privilege on the " . $object::class . " {$object->id} not granted for the current user.",
31 12
                MIDCOM_LOG_ERROR);
32 12
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
33 12
            return false;
34
        }
35 119
        if (!$object->_on_updating()) {
36
            debug_add("The _on_updating event handler returned false.");
37
            return false;
38
        }
39
        // Still check name uniqueness
40 119
        return self::_pre_check_name($object);
41
    }
42
43
    /**
44
     * Execute a DB update of the object passed. This will call the corresponding
45
     * event handlers. Calling sequence with method signatures:
46
     *
47
     * 1. Validate privileges using can_do. The user needs midgard:update privilege on the content object.
48
     * 2. bool $object->_on_updating() is executed. If it returns false, update is aborted.
49
     * 3. bool $object->__object->update() is executed to do the actual DB update. This has to execute parent::update()
50
     *    and return its value, nothing else.
51
     * 4. void $object->_on_updated() is executed to notify the class from a successful DB update.
52
     */
53 130
    public static function update(midcom_core_dbaobject $object) : bool
54
    {
55 130
        if (!self::update_pre_checks($object)) {
56 12
            debug_add('Pre-flight check returned false', MIDCOM_LOG_ERROR);
57 12
            return false;
58
        }
59
60 119
        if (!$object->__object->update()) {
61
            debug_add("Failed to update the record, last Midgard error: " . midcom_connection::get_error_string());
62
            return false;
63
        }
64
65 119
        self::update_post_ops($object);
66
67 119
        return true;
68
    }
69
70
    /**
71
     * Post object creation operations for create
72
     *
73
     * Separated so that dbfactory->import() can reuse the code
74
     */
75 119
    public static function update_post_ops(midcom_core_dbaobject $object)
76
    {
77 119
        $object->_on_updated();
78
79 119
        midcom::get()->dispatcher->dispatch(new dbaevent($object), dbaevent::UPDATE);
80
    }
81
82
    /**
83
     * Add full privileges to the owner of the object.
84
     * This is essentially sets the midgard:owner privilege for the current user.
85
     */
86 314
    private static function _set_owner_privileges(midcom_core_dbaobject $object)
87
    {
88 314
        if (!midcom::get()->auth->user) {
89 188
            debug_add("Could not retrieve the midcom_core_user instance for the creator of " . $object::class . " {$object->guid}, skipping owner privilege assignment.",
90 188
                MIDCOM_LOG_INFO);
91 188
            return;
92
        }
93
94
        // Circumvent the main privilege class as we need full access here regardless of
95
        // the actual circumstances.
96 152
        $privilege = new midcom_core_privilege_db();
97 152
        $privilege->assignee = midcom::get()->auth->user->id;
98 152
        $privilege->privilegename = 'midgard:owner';
99 152
        $privilege->objectguid = $object->guid;
100 152
        $privilege->value = MIDCOM_PRIVILEGE_ALLOW;
101
102 152
        if (!$privilege->create()) {
103
            debug_add("Could not set the owner privilege {$privilege->privilegename} for {$object->guid}, see debug level log for details. Last Midgard Error: " . midcom_connection::get_error_string(),
104
                MIDCOM_LOG_WARN);
105
        }
106
    }
107
108
    /**
109
     * "Pre-flight" checks for create method
110
     *
111
     * Separated so that dbfactory->import() can reuse the code
112
     */
113 314
    public static function create_pre_checks(midcom_core_dbaobject $object) : bool
114
    {
115 314
        if ($parent = $object->get_parent()) {
116
            // Attachments are a special case
117 180
            if ($object instanceof midcom_db_attachment) {
118 21
                if (   !$parent->can_do('midgard:attachments')
119 21
                    || !$parent->can_do('midgard:update')) {
120
                    debug_add("Failed to create attachment, update or attachments privilege on the parent " . $parent::class . " {$parent->guid} not granted for the current user.",
121
                        MIDCOM_LOG_ERROR);
122
                    midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
123 21
                    return false;
124
                }
125 160
            } elseif (   !$parent->can_do('midgard:create')
126 160
                      && !midcom::get()->auth->can_user_do('midgard:create', class: $object::class)) {
127
                debug_add("Failed to create object, create privilege on the parent " . $parent::class . " {$parent->guid} or the actual object class not granted for the current user.",
128
                    MIDCOM_LOG_ERROR);
129
                midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
130 180
                return false;
131
            }
132 224
        } elseif (!midcom::get()->auth->can_user_do('midgard:create', class: $object::class)) {
133
            debug_add("Failed to create object, general create privilege not granted for the current user.", MIDCOM_LOG_ERROR);
134
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
135
            return false;
136
        }
137
138 314
        if (!$object->_on_creating()) {
139 1
            debug_add("The _on_creating event handler returned false.");
140 1
            return false;
141
        }
142
143
        // Still check name uniqueness
144 314
        return self::_pre_check_name($object);
145
    }
146
147
    /**
148
     * Helper method to call in the _xxx_pre_checks, handles the API
149
     * level checks and automatic operations as specified in ticket #809
150
     *
151
     * @see http://trac.midgard-project.org/ticket/809
152
     * Quoting the ticket API-level section:
153
     * <pre>
154
     *      1. Checks will be done in the pre-flight check phase (ie just after _on_creating/_on_updating)
155
     *      2. If name is not unique false is returned for pre-flight check, preventing create/update
156
     *          2.2 UNLESS a property in the object ('allow_name_catenate') is set to true in which case unique one is generated by catenating an incrementing number to the name.
157
     *      3. if name is empty unique name is generated from title property (unless title is empty too)
158
     *      4. if name is not URL-safe false is returned
159
     * </pre>
160
     *
161
     * @see midcom_helper_reflector_nameresolver::name_is_safe()
162
     * @see midcom_helper_reflector_nameresolver::name_is_unique()
163
     * @see midcom_helper_reflector_nameresolver::generate_unique_name()
164
     */
165 322
    private static function _pre_check_name(midcom_core_dbaobject $object) : bool
166
    {
167
        // Make sure name is empty of unique if the object has such property
168 322
        $name_property = midcom_helper_reflector::get_name_property($object);
169 322
        if (empty($name_property)) {
170
            // This object has no name property, return early
171 227
            return true;
172
        }
173
174 155
        $resolver = new midcom_helper_reflector_nameresolver($object);
175
176
        /**
177
         * If name is empty, try to generate new, unique one
178
         *
179
         * @see http://trac.midgard-project.org/ticket/809
180
         */
181 155
        if (empty($object->{$name_property})) {
182
            // name is empty, try to generate
183 89
            $object->{$name_property} = (string) $resolver->generate_unique_name();
184 89
            if (empty($object->{$name_property})) {
185 75
                return true;
186
            }
187
        }
188
189
        // Enforce URL-safe names
190 97
        if (!$resolver->name_is_safe()) {
191 1
            midcom_connection::set_error(MGD_ERR_INVALID_NAME);
192 1
            return false;
193
        }
194
195
        // Enforce unique (or empty) names
196 97
        if (!$resolver->name_is_unique()) {
197 1
            if ($object->allow_name_catenate) {
198
                // Transparent catenation allowed, let's try again.
199
                if ($new_name = $resolver->generate_unique_name()) {
200
                    $object->{$name_property} = $new_name;
201
                    return true;
202
                }
203
                debug_add('allow_name_catenate was set but midcom_helper_reflector_nameresolver::generate_unique_name() returned empty value, falling through', MIDCOM_LOG_WARN);
204
            }
205 1
            midcom_connection::set_error(MGD_ERR_OBJECT_NAME_EXISTS);
206 1
            return false;
207
        }
208
209
        // All checks ok, we're fine.
210 97
        return true;
211
    }
212
213
    /**
214
     * Execute a DB create of the object passed. This will call the corresponding
215
     * event handlers. Calling sequence with method signatures:
216
     *
217
     * 1. Validate privileges using can_do. The user needs midgard:create privilege to the parent object or in general, if there is no parent.
218
     * 2. bool $object->_on_creating() is executed. If it returns false, create is aborted.
219
     * 3. bool $object->__object->create() is executed to do the actual DB create. This has to execute parent::create()
220
     *    and return its value, nothing else.
221
     * 4. void $object->_on_created() is executed to notify the class from a successful DB creation.
222
     */
223 314
    public static function create(midcom_core_dbaobject $object) : bool
224
    {
225 314
        if (!self::create_pre_checks($object)) {
226 3
            debug_add('Pre-flight check returned false', MIDCOM_LOG_ERROR);
227 3
            return false;
228
        }
229
230 314
        if (midcom::get()->auth->user !== null) {
231
            // Default the authors to current user
232 152
            if (empty($object->metadata->authors)) {
233 152
                $object->metadata->set('authors', "|" . midcom::get()->auth->user->guid . "|");
234
            }
235
236
            // Default the owner to first group of current user
237 152
            if (   empty($object->metadata->owner)
238 152
                && $first_group = midcom::get()->auth->user->get_first_group_guid()) {
0 ignored issues
show
Bug introduced by
The method get_first_group_guid() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

238
                && $first_group = midcom::get()->auth->user->/** @scrutinizer ignore-call */ get_first_group_guid()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
239
                $object->metadata->set('owner', $first_group);
240
            }
241
        }
242
        // Default the publication time to current date/time
243 314
        if (empty($object->metadata->published)) {
244 314
            $object->metadata->set('published', time());
245
        }
246
247 314
        if (!$object->__object->create()) {
248 7
            debug_add("Failed to create " . $object::class . ", last Midgard error: " . midcom_connection::get_error_string());
249 7
            return false;
250
        }
251
252 314
        self::create_post_ops($object);
253
254 314
        return true;
255
    }
256
257
    /**
258
     * Post object creation operations for create
259
     *
260
     * Separated so that dbfactory->import() can reuse the code
261
     */
262 314
    public static function create_post_ops(midcom_core_dbaobject $object)
263
    {
264 314
        self::_set_owner_privileges($object);
265
266 314
        $object->_on_created();
267 314
        midcom::get()->dispatcher->dispatch(new dbaevent($object), dbaevent::CREATE);
268
    }
269
270
    /**
271
     * Execute a DB delete of the object passed. This will call the corresponding
272
     * event handlers. Calling sequence with method signatures:
273
     *
274
     * 1. Validate privileges using can_do. The user needs midgard:delete privilege on the content object.
275
     * 2. bool $object->_on_deleting() is executed. If it returns false, delete is aborted.
276
     * 3. All extensions of the object are deleted
277
     * 4. bool $object->__object->delete() is executed to do the actual DB delete. This has to execute parent::delete()
278
     *    and return its value, nothing else.
279
     * 5. void $object->_on_deleted() is executed to notify the class from a successful DB deletion.
280
     */
281 211
    public static function delete(midcom_core_dbaobject $object) : bool
282
    {
283 211
        if (!self::delete_pre_checks($object)) {
284
            debug_add('Pre-flight check returned false', MIDCOM_LOG_ERROR);
285
            return false;
286
        }
287
288
        // Delete all extensions:
289
        // Attachments can't have attachments so no need to query those
290 211
        if (!$object instanceof midcom_db_attachment) {
291 196
            foreach ($object->list_attachments() as $attachment) {
292 4
                if (!$attachment->delete()) {
293
                    debug_add("Failed to delete attachment ID {$attachment->id}", MIDCOM_LOG_ERROR);
294
                    return false;
295
                }
296
            }
297
        }
298
299 211
        $query = new midgard_query_builder('midgard_parameter');
300 211
        $query->add_constraint('parentguid', '=', $object->guid);
301 211
        foreach ($query->execute() as $parameter) {
302 31
            if (!$parameter->delete()) {
303
                debug_add("Failed to delete parameter ID {$parameter->id}", MIDCOM_LOG_ERROR);
304
                return false;
305
            }
306
        }
307
308 211
        if (!self::_delete_privileges($object)) {
309
            debug_add('Failed to delete the object privileges.', MIDCOM_LOG_INFO);
310
            return false;
311
        }
312
313
        // Finally, delete the object itself
314 211
        if (!$object->__object->delete()) {
315 40
            debug_add("Failed to delete " . $object::class . ", last Midgard error: " . midcom_connection::get_error_string(), MIDCOM_LOG_INFO);
316 40
            return false;
317
        }
318
319
        // Explicitly set this in case someone needs to check against it
320 211
        self::delete_post_ops($object);
321
322 211
        return true;
323
    }
324
325
    /**
326
     * Unconditionally drop all privileges assigned to the given object.
327
     * Called upon successful delete
328
     */
329 211
    private static function _delete_privileges(midcom_core_dbaobject $object) : bool
330
    {
331 211
        $qb = new midgard_query_builder('midcom_core_privilege_db');
332 211
        $qb->add_constraint('objectguid', '=', $object->guid);
333
334 211
        foreach ($qb->execute() as $dbpriv) {
335 107
            if (!$dbpriv->purge()) {
336
                return false;
337
            }
338
        }
339 211
        return true;
340
    }
341
342
    /**
343
     * Execute a DB delete of the object passed and delete its descendants. This will call the corresponding
344
     * event handlers. Calling sequence with method signatures:
345
     *
346
     * 1. Get all of the child objects
347
     * 2. Delete them recursively starting from the top, working towards the root
348
     * 3. Finally delete the root object
349
     */
350 1
    public static function delete_tree(midcom_core_dbaobject $object) : bool
351
    {
352 1
        foreach (midcom_helper_reflector_tree::get_child_objects($object) as $children) {
353
            // Delete first the descendants
354
            foreach ($children as $child) {
355
                //Inherit RCS status (so that f.x. large tree deletions can run faster)
356
                $child->_use_rcs = $object->_use_rcs;
357
                if (!self::delete_tree($child)) {
358
                    debug_print_r('Failed to delete the children of this object:', $object, MIDCOM_LOG_INFO);
359
                    return false;
360
                }
361
            }
362
        }
363 1
        if (!self::delete($object)) {
364
            debug_print_r('Failed to delete the object', $object, MIDCOM_LOG_ERROR);
365
            return false;
366
        }
367
368 1
        return true;
369
    }
370
371
    /**
372
     * Post object creation operations for delete
373
     *
374
     * Separated so that dbfactory->import() can reuse the code
375
     */
376 211
    public static function delete_post_ops(midcom_core_dbaobject $object)
377
    {
378 211
        $object->_on_deleted();
379 211
        midcom::get()->dispatcher->dispatch(new dbaevent($object), dbaevent::DELETE);
380
    }
381
382
    /**
383
     * After we instantiated the midgard object do some post processing and ACL checks
384
     *
385
     * @see load()
386
     */
387 403
    public static function post_db_load_checks(midcom_core_dbaobject $object)
388
    {
389 403
        if (!$object->can_do('midgard:read')) {
390
            debug_add("Failed to load object, read privilege on the " . $object::class . " {$object->guid} not granted for the current user.");
391
            throw new midcom_error_forbidden();
392
        }
393 403
        $object->_on_loaded();
394
395
        // Register the GUID as loaded in this request
396 403
        midcom::get()->cache->content->register($object->guid);
397
    }
398
399
    /**
400
     * This is a simple wrapper with (currently) no additional functionality
401
     * over get_by_id that resynchronizes the object state with the database.
402
     * Use this if you think that your current object is stale. It does full
403
     * access control.
404
     *
405
     * On any failure, the object is cleared.
406
     */
407 213
    public static function refresh(midcom_core_dbaobject $object) : bool
408
    {
409
        /**
410
         * Use try/catch here since the object might have been deleted...
411
         * @see http://trac.midgard-project.org/ticket/927
412
         */
413
        try {
414 213
            return $object->get_by_id($object->id);
415 66
        } catch (Exception) {
416 66
            return false;
417
        }
418
    }
419
420
    /**
421
     * This call wraps the original get_by_id call to provide access control.
422
     * The calling sequence is as with the corresponding constructor.
423
     */
424 213
    public static function get_by_id(midcom_core_dbaobject $object, int $id) : bool
425
    {
426 213
        if (!$id) {
427
            debug_add("Failed to load " . $object::class . " object, incorrect ID provided.", MIDCOM_LOG_ERROR);
428
            return false;
429
        }
430
431 213
        $object->__object->get_by_id($id);
432
433 195
        if ($object->id == 0) {
434
            debug_add("Failed to load the record identified by {$id}, last Midgard error was:" . midcom_connection::get_error_string(), MIDCOM_LOG_INFO);
435
            return false;
436
        }
437 195
        if (!$object->can_do('midgard:read')) {
438
            debug_add("Failed to load object, read privilege on the " . $object::class . " {$object->guid} not granted for the current user.",
439
            MIDCOM_LOG_ERROR);
440
            $object->__object = new $object->__mgdschema_class_name__;
441
            return false;
442
        }
443
444 195
        $object->_on_loaded();
445 195
        return true;
446
    }
447
448
    /**
449
     * This call wraps the original get_by_guid call to provide access control.
450
     * The calling sequence is as with the corresponding constructor.
451
     */
452
    public static function get_by_guid(midcom_core_dbaobject $object, string $guid) : bool
453
    {
454
        if (   !midcom::get()->auth->admin
455
            && !midcom::get()->auth->acl->can_do_byguid('midgard:read', $guid, $object::class, midcom::get()->auth->acl->get_user_id())) {
456
            debug_add("Failed to load object, read privilege on the " . $object::class . " {$guid} not granted for the current user.", MIDCOM_LOG_ERROR);
457
            return false;
458
        }
459
        $object->__object->get_by_guid($guid);
460
461
        if ($object->id == 0) {
462
            debug_add("Failed to load the record identified by {$guid}, last Midgard error was: " . midcom_connection::get_error_string(), MIDCOM_LOG_INFO);
463
            return false;
464
        }
465
        $object->_on_loaded();
466
        return true;
467
    }
468
469
    /**
470
     * This call wraps the original get_by_guid call to provide access control.
471
     * The calling sequence is as with the corresponding constructor.
472
     */
473 1
    public static function get_by_path(midcom_core_dbaobject $object, string $path) : bool
474
    {
475 1
        $object->__object->get_by_path($path);
476
477 1
        if ($object->id == 0) {
478
            return false;
479
        }
480 1
        if (!$object->can_do('midgard:read')) {
481
            $object->__object = new $object->__mgdschema_class_name__;
482
            return false;
483
        }
484
485 1
        $object->_on_loaded();
486 1
        return true;
487
    }
488
489
    /**
490
     * "Pre-flight" checks for delete method
491
     *
492
     * Separated so that dbfactory->import() can reuse the code
493
     */
494 211
    public static function delete_pre_checks(midcom_core_dbaobject $object) : bool
495
    {
496 211
        if (!$object->id) {
497
            debug_add("Failed to delete object, object " . $object::class . " is non-persistent (empty ID).", MIDCOM_LOG_ERROR);
498
            return false;
499
        }
500
501 211
        if (!$object->can_do('midgard:delete')) {
502
            debug_add("Failed to delete object, delete privilege on the " . $object::class . " {$object->guid} not granted for the current user.",
503
                MIDCOM_LOG_ERROR);
504
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
505
            return false;
506
        }
507 211
        return $object->_on_deleting();
508
    }
509
}
510