Passed
Push — master ( 34dd63...33989f )
by Andreas
22:47
created

midcom_baseclasses_core_dbobject::purge()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.8476

Importance

Changes 0
Metric Value
cc 4
eloc 16
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 28
ccs 7
cts 16
cp 0.4375
crap 6.8476
rs 9.7333

1 Method

Rating   Name   Duplication   Size   Complexity  
A midcom_baseclasses_core_dbobject::delete_pre_checks() 0 14 3
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
use midgard\portable\api\mgdobject;
11
12
/**
13
 * This class only contains static functions which are there to hook into
14
 * the classes you derive from the midcom_core_dbaobject.
15
 *
16
 * The static members will invoke a number of callback methods so that you should
17
 * normally never have to override the base midgard methods like update or the like.
18
 *
19
 * @package midcom.baseclasses
20
 */
21
class midcom_baseclasses_core_dbobject
22
{
23
    /**
24
     * "Pre-flight" checks for update method
25
     *
26
     * Separated so that dbfactory->import() can reuse the code
27
     */
28 114
    public static function update_pre_checks(midcom_core_dbaobject $object) : bool
29
    {
30 114
        if (!$object->can_do('midgard:update')) {
31 2
            debug_add("Failed to load object, update privilege on the " . get_class($object) . " {$object->id} not granted for the current user.",
32 2
                MIDCOM_LOG_ERROR);
33 2
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
34 2
            return false;
35
        }
36 113
        if (!$object->_on_updating()) {
37
            debug_add("The _on_updating event handler returned false.");
38
            return false;
39
        }
40
        // Still check name uniqueness
41 113
        return self::_pre_check_name($object);
42
    }
43
44
    /**
45
     * Execute a DB update of the object passed. This will call the corresponding
46
     * event handlers. Calling sequence with method signatures:
47
     *
48
     * 1. Validate privileges using can_do. The user needs midgard:update privilege on the content object.
49
     * 2. bool $object->_on_updating() is executed. If it returns false, update is aborted.
50
     * 3. bool $object->__object->update() is executed to do the actual DB update. This has to execute parent::update()
51
     *    and return its value, nothing else.
52
     * 4. void $object->_on_updated() is executed to notify the class from a successful DB update.
53
     */
54 114
    public static function update(midcom_core_dbaobject $object) : bool
55
    {
56 114
        if (!self::update_pre_checks($object)) {
57 2
            debug_add('Pre-flight check returned false', MIDCOM_LOG_ERROR);
58 2
            return false;
59
        }
60
61 113
        if (!$object->__object->update()) {
62
            debug_add("Failed to update the record, last Midgard error: " . midcom_connection::get_error_string());
63
            return false;
64
        }
65
66 113
        self::update_post_ops($object);
67
68 113
        return true;
69
    }
70
71
    /**
72
     * Post object creation operations for create
73
     *
74
     * Separated so that dbfactory->import() can reuse the code
75
     */
76 113
    public static function update_post_ops(midcom_core_dbaobject $object)
77
    {
78 113
        if ($object->_use_rcs) {
79 51
            midcom::get()->rcs->update($object, $object->get_rcs_message());
80
        }
81
82 113
        $object->_on_updated();
83
84 113
        midcom::get()->dispatcher->dispatch(new dbaevent($object), dbaevent::UPDATE);
85 113
    }
86
87
    /**
88
     * Add full privileges to the owner of the object.
89
     * This is essentially sets the midgard:owner privilege for the current user.
90
     */
91 287
    private static function _set_owner_privileges(midcom_core_dbaobject $object)
92
    {
93 287
        if (!midcom::get()->auth->user) {
94 166
            debug_add("Could not retrieve the midcom_core_user instance for the creator of " . get_class($object) . " {$object->guid}, skipping owner privilege assignment.",
95 166
                MIDCOM_LOG_INFO);
96 166
            return;
97
        }
98
99
        // Circumvent the main privilege class as we need full access here regardless of
100
        // the actual circumstances.
101 146
        $privilege = new midcom_core_privilege_db();
102 146
        $privilege->assignee = midcom::get()->auth->user->id;
103 146
        $privilege->privilegename = 'midgard:owner';
104 146
        $privilege->objectguid = $object->guid;
105 146
        $privilege->value = MIDCOM_PRIVILEGE_ALLOW;
106
107 146
        if (!$privilege->create()) {
108
            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(),
109
                MIDCOM_LOG_WARN);
110
        }
111 146
    }
112
113
    /**
114
     * "Pre-flight" checks for create method
115
     *
116
     * Separated so that dbfactory->import() can reuse the code
117
     */
118 287
    public static function create_pre_checks(midcom_core_dbaobject $object) : bool
119
    {
120 287
        $parent = $object->get_parent();
121
122 287
        if ($parent !== null) {
123
            // Attachments are a special case
124 163
            if ($object instanceof midcom_db_attachment) {
125 11
                if (   !$parent->can_do('midgard:attachments')
126 11
                    || !$parent->can_do('midgard:update')) {
127
                    debug_add("Failed to create attachment, update or attachments privilege on the parent " . get_class($parent) . " {$parent->guid} not granted for the current user.",
128
                        MIDCOM_LOG_ERROR);
129
                    midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
130 11
                    return false;
131
                }
132 153
            } elseif (   !$parent->can_do('midgard:create')
133 153
                      && !midcom::get()->auth->can_user_do('midgard:create', null, get_class($object))) {
134
                debug_add("Failed to create object, create privilege on the parent " . get_class($parent) . " {$parent->guid} or the actual object class not granted for the current user.",
135
                    MIDCOM_LOG_ERROR);
136
                midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
137 163
                return false;
138
            }
139 213
        } elseif (!midcom::get()->auth->can_user_do('midgard:create', null, get_class($object))) {
140
            debug_add("Failed to create object, general create privilege not granted for the current user.", MIDCOM_LOG_ERROR);
141
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
142
            return false;
143
        }
144
145 287
        if (!$object->_on_creating()) {
146 4
            debug_add("The _on_creating event handler returned false.");
147 4
            return false;
148
        }
149
150
        // Still check name uniqueness
151 287
        return self::_pre_check_name($object);
152
    }
153
154
    /**
155
     * Helper method to call in the _xxx_pre_checks, handles the API
156
     * level checks and automatic operations as specified in ticket #809
157
     *
158
     * @see http://trac.midgard-project.org/ticket/809
159
     * Quoting the ticket API-level section:
160
     * <pre>
161
     *      1. Checks will be done in the pre-flight check phase (ie just after _on_creating/_on_updating)
162
     *      2. If name is not unique false is returned for pre-flight check, preventing create/update
163
     *          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.
164
     *      3. if name is empty unique name is generated from title property (unless title is empty too)
165
     *      4. if name is not URL-safe false is returned
166
     * </pre>
167
     *
168
     * @see midcom_helper_reflector_nameresolver::name_is_safe()
169
     * @see midcom_helper_reflector_nameresolver::name_is_unique()
170
     * @see midcom_helper_reflector_nameresolver::generate_unique_name()
171
     */
172 294
    private static function _pre_check_name(midcom_core_dbaobject $object) : bool
173
    {
174
        // Make sure name is empty of unique if the object has such property
175 294
        $name_property = midcom_helper_reflector::get_name_property($object);
176 294
        if (empty($name_property)) {
177
            // This object has no name property, return early
178 214
            return true;
179
        }
180
181 140
        $resolver = new midcom_helper_reflector_nameresolver($object);
182
183
        /**
184
         * If name is empty, try to generate new, unique one
185
         *
186
         * @see http://trac.midgard-project.org/ticket/809
187
         */
188 140
        if (empty($object->{$name_property})) {
189
            // name is empty, try to generate
190 72
            $object->{$name_property} = (string) $resolver->generate_unique_name();
191 72
            if (empty($object->{$name_property})) {
192 68
                return true;
193
            }
194
        }
195
196
        // Enforce URL-safe names
197 89
        if (!$resolver->name_is_safe()) {
198 1
            midcom_connection::set_error(MGD_ERR_INVALID_NAME);
199 1
            return false;
200
        }
201
202
        // Enforce unique (or empty) names
203 89
        if (!$resolver->name_is_unique()) {
204
            if ($object->allow_name_catenate) {
205
                // Transparent catenation allowed, let's try again.
206
                if ($new_name = $resolver->generate_unique_name()) {
207
                    $object->{$name_property} = $new_name;
208
                    return true;
209
                }
210
                debug_add('allow_name_catenate was set but midcom_helper_reflector_nameresolver::generate_unique_name() returned empty value, falling through', MIDCOM_LOG_WARN);
211
            }
212
            midcom_connection::set_error(MGD_ERR_OBJECT_NAME_EXISTS);
213
            return false;
214
        }
215
216
        // All checks ok, we're fine.
217 89
        return true;
218
    }
219
220
    /**
221
     * Execute a DB create of the object passed. This will call the corresponding
222
     * event handlers. Calling sequence with method signatures:
223
     *
224
     * 1. Validate privileges using can_do. The user needs midgard:create privilege to the parent object or in general, if there is no parent.
225
     * 2. bool $object->_on_creating() is executed. If it returns false, create is aborted.
226
     * 3. bool $object->__object->create() is executed to do the actual DB create. This has to execute parent::create()
227
     *    and return its value, nothing else.
228
     * 4. void $object->_on_created() is executed to notify the class from a successful DB creation.
229
     */
230 287
    public static function create(midcom_core_dbaobject $object) : bool
231
    {
232 287
        if (!self::create_pre_checks($object)) {
233 5
            debug_add('Pre-flight check returned false', MIDCOM_LOG_ERROR);
234 5
            return false;
235
        }
236
237 287
        if (midcom::get()->auth->user !== null) {
238
            // Default the authors to current user
239 146
            if (empty($object->metadata->authors)) {
240 146
                $object->metadata->set('authors', "|" . midcom::get()->auth->user->guid . "|");
241
            }
242
243
            // Default the owner to first group of current user
244 146
            if (   empty($object->metadata->owner)
245 146
                && $first_group = midcom::get()->auth->user->get_first_group_guid()) {
246
                $object->metadata->set('owner', $first_group);
247
            }
248
        }
249
        // Default the publication time to current date/time
250 287
        if (empty($object->metadata->published)) {
251 287
            $object->metadata->set('published', time());
252
        }
253
254 287
        if (!$object->__object->create()) {
255 6
            debug_add("Failed to create " . get_class($object) . ", last Midgard error: " . midcom_connection::get_error_string());
256 6
            return false;
257
        }
258
259 287
        self::create_post_ops($object);
260
261 287
        return true;
262
    }
263
264
    /**
265
     * Post object creation operations for create
266
     *
267
     * Separated so that dbfactory->import() can reuse the code
268
     */
269 287
    public static function create_post_ops(midcom_core_dbaobject $object)
270
    {
271 287
        self::_set_owner_privileges($object);
272
273 287
        $object->_on_created();
274 287
        midcom::get()->dispatcher->dispatch(new dbaevent($object), dbaevent::CREATE);
275
276 287
        if ($object->_use_rcs) {
277 35
            midcom::get()->rcs->update($object, $object->get_rcs_message());
278
        }
279 287
    }
280
281
    /**
282
     * Execute a DB delete of the object passed. This will call the corresponding
283
     * event handlers. Calling sequence with method signatures:
284
     *
285
     * 1. Validate privileges using can_do. The user needs midgard:delete privilege on the content object.
286
     * 2. bool $object->_on_deleting() is executed. If it returns false, delete is aborted.
287
     * 3. All extensions of the object are deleted
288
     * 4. bool $object->__object->delete() is executed to do the actual DB delete. This has to execute parent::delete()
289
     *    and return its value, nothing else.
290
     * 5. void $object->_on_deleted() is executed to notify the class from a successful DB deletion.
291
     */
292 186
    public static function delete(midcom_core_dbaobject $object) : bool
293
    {
294 186
        if (!self::delete_pre_checks($object)) {
295
            debug_add('Pre-flight check returned false', MIDCOM_LOG_ERROR);
296
            return false;
297
        }
298
299
        // Delete all extensions:
300
        // Attachments can't have attachments so no need to query those
301 186
        if (!$object instanceof midcom_db_attachment) {
302 181
            foreach ($object->list_attachments() as $attachment) {
303 4
                if (!$attachment->delete()) {
304
                    debug_add("Failed to delete attachment ID {$attachment->id}", MIDCOM_LOG_ERROR);
305
                    return false;
306
                }
307
            }
308
        }
309
310 186
        $query = new midgard_query_builder('midgard_parameter');
311 186
        $query->add_constraint('parentguid', '=', $object->guid);
312 186
        foreach ($query->execute() as $parameter) {
313 30
            if (!$parameter->delete()) {
314
                debug_add("Failed to delete parameter ID {$parameter->id}", MIDCOM_LOG_ERROR);
315
                return false;
316
            }
317
        }
318
319 186
        if (!self::_delete_privileges($object)) {
320
            debug_add('Failed to delete the object privileges.', MIDCOM_LOG_INFO);
321
            return false;
322
        }
323
324
        // Finally, delete the object itself
325 186
        if (!$object->__object->delete()) {
326 34
            debug_add("Failed to delete " . get_class($object) . ", last Midgard error: " . midcom_connection::get_error_string(), MIDCOM_LOG_INFO);
327 34
            return false;
328
        }
329
330
        // Explicitly set this in case someone needs to check against it
331 186
        self::delete_post_ops($object);
332
333 186
        return true;
334
    }
335
336
    /**
337
     * Unconditionally drop all privileges assigned to the given object.
338
     * Called upon successful delete
339
     */
340 186
    private static function _delete_privileges(midcom_core_dbaobject $object) : bool
341
    {
342 186
        $qb = new midgard_query_builder('midcom_core_privilege_db');
343 186
        $qb->add_constraint('objectguid', '=', $object->guid);
344
345 186
        foreach ($qb->execute() as $dbpriv) {
346 98
            if (!$dbpriv->purge()) {
347
                return false;
348
            }
349
        }
350 186
        return true;
351
    }
352
353
    /**
354
     * Execute a DB delete of the object passed and delete its descendants. This will call the corresponding
355
     * event handlers. Calling sequence with method signatures:
356
     *
357
     * 1. Get all of the child objects
358
     * 2. Delete them recursively starting from the top, working towards the root
359
     * 3. Finally delete the root object
360
     */
361 1
    public static function delete_tree(midcom_core_dbaobject $object) : bool
362
    {
363 1
        foreach (midcom_helper_reflector_tree::get_child_objects($object) as $children) {
364
            // Delete first the descendants
365
            foreach ($children as $child) {
366
                //Inherit RCS status (so that f.x. large tree deletions can run faster)
367
                $child->_use_rcs = $object->_use_rcs;
368
                if (!self::delete_tree($child)) {
369
                    debug_print_r('Failed to delete the children of this object:', $object, MIDCOM_LOG_INFO);
370
                    return false;
371
                }
372
            }
373
        }
374 1
        if (!self::delete($object)) {
375
            debug_print_r('Failed to delete the object', $object, MIDCOM_LOG_ERROR);
376
            return false;
377
        }
378
379 1
        return true;
380
    }
381
382
    /**
383
     * Post object creation operations for delete
384
     *
385
     * Separated so that dbfactory->import() can reuse the code
386
     */
387 186
    public static function delete_post_ops(midcom_core_dbaobject $object)
388
    {
389 186
        $object->_on_deleted();
390 186
        midcom::get()->dispatcher->dispatch(new dbaevent($object), dbaevent::DELETE);
391 186
        if ($object->_use_rcs) {
392 14
            midcom::get()->rcs->update($object, $object->get_rcs_message());
393
        }
394 186
    }
395
396
    /**
397
     * After we instantiated the midgard object do some post processing and ACL checks
398
     *
399
     * @see load()
400
     */
401 387
    public static function post_db_load_checks(midcom_core_dbaobject $object)
402
    {
403 387
        if (!$object->can_do('midgard:read')) {
404
            debug_add("Failed to load object, read privilege on the " . get_class($object) . " {$object->guid} not granted for the current user.");
405
            throw new midcom_error_forbidden();
406
        }
407 387
        $object->_on_loaded();
408
409
        // Register the GUID as loaded in this request
410 387
        if (isset(midcom::get()->cache->content)) {
411 387
            midcom::get()->cache->content->register($object->guid);
412
        }
413 387
    }
414
415
    /**
416
     * This is a simple wrapper with (currently) no additional functionality
417
     * over get_by_id that resynchronizes the object state with the database.
418
     * Use this if you think that your current object is stale. It does full
419
     * access control.
420
     *
421
     * On any failure, the object is cleared.
422
     */
423 188
    public static function refresh(midcom_core_dbaobject $object) : bool
424
    {
425
        /**
426
         * Use try/catch here since the object might have been deleted...
427
         * @see http://trac.midgard-project.org/ticket/927
428
         */
429
        try {
430 188
            return $object->get_by_id($object->id);
431 62
        } catch (Exception $e) {
432 62
            return false;
433
        }
434
    }
435
436
    /**
437
     * This call wraps the original get_by_id call to provide access control.
438
     * The calling sequence is as with the corresponding constructor.
439
     */
440 188
    public static function get_by_id(midcom_core_dbaobject $object, int $id) : bool
441
    {
442 188
        if (!$id) {
443
            debug_add("Failed to load " . get_class($object) . " object, incorrect ID provided.", MIDCOM_LOG_ERROR);
444
            return false;
445
        }
446
447 188
        $object->__object->get_by_id((int) $id);
448
449 172
        if ($object->id != 0) {
450 172
            if (!$object->can_do('midgard:read')) {
451
                debug_add("Failed to load object, read privilege on the " . get_class($object) . " {$object->guid} not granted for the current user.",
452
                    MIDCOM_LOG_ERROR);
453
                $object->__object = new $object->__mgdschema_class_name__;
454
                return false;
455
            }
456
457 172
            $object->_on_loaded();
458 172
            return true;
459
        }
460
        debug_add("Failed to load the record identified by {$id}, last Midgard error was:" . midcom_connection::get_error_string(), MIDCOM_LOG_INFO);
461
        return false;
462
    }
463
464
    /**
465
     * This call wraps the original get_by_guid call to provide access control.
466
     * The calling sequence is as with the corresponding constructor.
467
     */
468
    public static function get_by_guid(midcom_core_dbaobject $object, string $guid) : bool
469
    {
470
        if (   !midcom::get()->auth->admin
471
            && !midcom::get()->auth->acl->can_do_byguid('midgard:read', $guid, get_class($object), midcom::get()->auth->acl->get_user_id())) {
472
            debug_add("Failed to load object, read privilege on the " . get_class($object) . " {$guid} not granted for the current user.", MIDCOM_LOG_ERROR);
473
            return false;
474
        }
475
        $object->__object->get_by_guid($guid);
476
477
        if ($object->id != 0) {
478
            $object->_on_loaded();
479
            return true;
480
        }
481
        debug_add("Failed to load the record identified by {$guid}, last Midgard error was: " . midcom_connection::get_error_string(), MIDCOM_LOG_INFO);
482
        return false;
483
    }
484
485
    /**
486
     * This call wraps the original get_by_guid call to provide access control.
487
     * The calling sequence is as with the corresponding constructor.
488
     */
489 1
    public static function get_by_path(midcom_core_dbaobject $object, string $path) : bool
490
    {
491 1
        $object->__object->get_by_path($path);
492
493 1
        if ($object->id != 0) {
494 1
            if (!$object->can_do('midgard:read')) {
495
                $object->__object = new $object->__mgdschema_class_name__;
496
                return false;
497
            }
498
499 1
            $object->_on_loaded();
500 1
            return true;
501
        }
502
        return false;
503
    }
504
505
    /**
506
     * "Pre-flight" checks for delete method
507
     *
508
     * Separated so that dbfactory->import() can reuse the code
509
     */
510 186
    public static function delete_pre_checks(midcom_core_dbaobject $object) : bool
511
    {
512 186
        if (!$object->id) {
513
            debug_add("Failed to delete object, object " . get_class($object) . " is non-persistent (empty ID).", MIDCOM_LOG_ERROR);
514
            return false;
515
        }
516
517 186
        if (!$object->can_do('midgard:delete')) {
518
            debug_add("Failed to delete object, delete privilege on the " . get_class($object) . " {$object->guid} not granted for the current user.",
519
                MIDCOM_LOG_ERROR);
520
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
521
            return false;
522
        }
523 186
        return $object->_on_deleting();
524
    }
525
}
526