Completed
Push — master ( 527212...df338f )
by Andreas
39:48
created

midcom_helper_metadata   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 453
Duplicated Lines 0 %

Test Coverage

Coverage 80.26%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 145
c 1
b 0
f 0
dl 0
loc 453
ccs 122
cts 152
cp 0.8026
rs 3.04
wmc 67

20 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 7 2
A __isset() 0 7 2
A __get() 0 6 2
A __construct() 0 4 1
A is_approved() 0 3 1
A is_object_visible_onsite() 0 7 4
A __set() 0 3 1
A is_visible() 0 16 6
A on_update() 0 10 3
A get_datamanager() 0 18 4
A set() 0 16 3
B _retrieve_value() 0 40 10
B _set_property() 0 31 9
A is_locked() 0 21 5
A unapprove() 0 5 1
A can_unlock() 0 4 2
A lock() 0 10 2
A approve() 0 5 1
A retrieve() 0 25 5
A unlock() 0 9 3

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * @package midcom.helper
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\datamanager\schemadb;
10
use midcom\datamanager\datamanager;
11
12
/**
13
 * This class is an interface to the metadata of MidCOM objects.
14
 *
15
 * It will use an internal mechanism to cache repeated accesses to the same
16
 * metadata key during its lifetime. (Invalidating this cache will be possible
17
 * though.)
18
 *
19
 * <b>Metadata Key Reference</b>
20
 *
21
 * See the schema in /midcom/config/metadata_default.inc
22
 *
23
 * <b>Example Usage, Metadata Retrieval</b>
24
 *
25
 * <code>
26
 * <?php
27
 * $nap = new midcom_helper_nav();
28
 * $node = $nap->get_node($nap->get_current_node());
29
 *
30
 * $meta = $node[MIDCOM_NAV_OBJECT]->metadata;
31
 * echo "Visible : " . $meta->is_visible() . "</br>";
32
 * echo "Approved : " . $meta->is_approved() . "</br>";
33
 * echo "Keywords: " . $meta->get('keywords') . "</br>";
34
 * </code>
35
 *
36
 * <b>Example Usage, Approval</b>
37
 *
38
 * <code>
39
 * <?php
40
 * $article = new midcom_db_article($my_article_created_id);
41
 *
42
 * $article->metadata->approve();
43
 * </code>
44
 *
45
 * @property integer $schedulestart The time upon which the object should be made visible. 0 for no restriction.
46
 * @property integer $scheduleend The time upon which the object should be made invisible. 0 for no restriction.
47
 * @property boolean $navnoentry Set this to true if you do not want this object to appear in the navigation without it being completely hidden.
48
 * @property boolean $hidden Set this to true to hide the object on-site, overriding scheduling.
49
 * @property integer $published The publication time of the object.
50
 * @property string $publisher The person that published the object (i.e. author), read-only except on articles and pages.
51
 * @property string $authors The persons that worked on the object, pipe-separated list of GUIDs
52
 * @property string $owner The group that owns the object.
53
 * @property-read integer $created The creation time of the object.
54
 * @property-read string $creator The person that created the object.
55
 * @property-read integer $revised The last-modified time of the object.
56
 * @property-read string $revisor The person that modified the object.
57
 * @property-read integer $revision The object's revision.
58
 * @property-read integer $locked The lock time of the object.
59
 * @property-read string $locker The person that locked the object.
60
 * @property-read integer $size The object's size in bytes.
61
 * @property-read boolean $deleted Is the object deleted.
62
 * @property integer $approved The time of approval of the object, or 0 if not approved. Set automatically through approve/unapprove.
63
 * @property string $approver The person that approved/unapproved the object. Set automatically through approve/unapprove.
64
 * @property integer $score The object's score for sorting.
65
 * @property midcom_core_dbaobject $object Object to which we are attached.
66
 * @package midcom.helper
67
 */
68
class midcom_helper_metadata
69
{
70
    /**
71
     * @var midcom_core_dbaobject
72
     */
73
    private $__object;
74
75
    /**
76
     * Metadata object of the current object
77
     *
78
     * @var midgard\portable\api\metadata
79
     */
80
    private $__metadata;
81
82
    /**
83
     * Holds the values already read from the database.
84
     *
85
     * @var Array
86
     */
87
    private $_cache = [];
88
89
    private $field_config = [
90
        'readonly' => ['creator', 'created', 'revisor', 'revised', 'locker', 'locked', 'revision', 'size', 'deleted', 'exported', 'imported'],
91
        'timebased' => ['created', 'revised', 'published', 'locked', 'approved', 'schedulestart', 'scheduleend', 'exported', 'imported'],
92
        'person' => ['creator', 'revisor', 'locker', 'approver'],
93
        'other' => ['authors', 'owner', 'hidden', 'navnoentry', 'score', 'revision', 'size', 'deleted']
94
    ];
95
96
    /**
97
     * This will construct a new metadata object for an existing content object.
98
     *
99
     * @param midcom_core_dbaobject $object The object to attach to.
100
     */
101 390
    public function __construct(midcom_core_dbaobject $object)
102
    {
103 390
        $this->__metadata = $object->__object->metadata;
104 390
        $this->__object = $object;
105 390
    }
106
107
    /* ------- BASIC METADATA INTERFACE --------- */
108
109
    /**
110
     * Return a single metadata key from the object. The return
111
     * type depends on the metadata key that is requested (see the class introduction).
112
     *
113
     * You will not get the data from the datamanager using this calls, but the only
114
     * slightly post-processed metadata values. See _retrieve_value for post processing.
115
     *
116
     * @see midcom_helper_metadata::_retrieve_value()
117
     * @param string $key The key to retrieve
118
     * @return mixed The key's value.
119
     */
120 402
    public function get($key)
121
    {
122 402
        if (!isset($this->_cache[$key])) {
123 290
            $this->_cache[$key] = $this->_retrieve_value($key);
124
        }
125
126 402
        return $this->_cache[$key];
127
    }
128
129 358
    public function __get($key)
130
    {
131 358
        if ($key == 'object') {
132 78
            return $this->__object;
133
        }
134 358
        return $this->get($key);
135
    }
136
137 307
    public function __isset($key)
138
    {
139 307
        if (!isset($this->_cache[$key])) {
140 307
            $this->_cache[$key] = $this->_retrieve_value($key);
141
        }
142
143 307
        return isset($this->_cache[$key]);
144
    }
145
146
    /**
147
     * Return a Datamanager instance for the current object.
148
     *
149
     * Also, whenever the containing datamanager stores its data, you
150
     * <b>must</b> call the on_update() method of this class. This is
151
     * very important or backwards compatibility will be broken.
152
     *
153
     * @see midcom_helper_metadata::on_update()
154
     */
155 98
    public function get_datamanager() : datamanager
156
    {
157 98
        $schemadb = schemadb::from_path(midcom::get()->config->get('metadata_schema'));
158
159
        // Check if we have metadata schema defined in the schemadb specific for the object's schema or component
160 98
        $object_schema = $this->__object->get_parameter('midcom.helper.datamanager2', 'schema_name');
161 98
        if (!$object_schema || !$schemadb->has($object_schema)) {
162 98
            $component_schema = str_replace('.', '_', midcom_core_context::get()->get_key(MIDCOM_CONTEXT_COMPONENT));
163 98
            if ($schemadb->has($component_schema)) {
164
                // No specific metadata schema for object, fall back to component-specific metadata schema
165
                $object_schema = $component_schema;
166
            } else {
167
                // No metadata schema for component, fall back to default
168 98
                $object_schema = 'metadata';
169
            }
170
        }
171 98
        $dm = new datamanager($schemadb);
172 98
        return $dm->set_storage($this->__object, $object_schema);
173
    }
174
175
    /**
176
     * Frontend for setting a single metadata option
177
     *
178
     * @param string $key The key to set.
179
     * @param mixed $value The value to set.
180
     */
181 288
    public function set($key, $value) : bool
182
    {
183
        // Store the RCS mode
184 288
        $rcs_mode = $this->__object->_use_rcs;
185
186 288
        if ($return = $this->_set_property($key, $value)) {
187 287
            if ($this->__object->guid) {
188 8
                $return = $this->__object->update();
189
            }
190
191
            // Update the corresponding cache variable
192 287
            $this->on_update($key);
193
        }
194
        // Return the original RCS mode
195 288
        $this->__object->_use_rcs = $rcs_mode;
196 288
        return $return;
197
    }
198
199 13
    public function __set($key, $value)
200
    {
201 13
        $this->set($key, $value);
202 13
    }
203
204
    /**
205
     * Directly set a metadata option.
206
     *
207
     * The passed value will be stored using the follow transformations:
208
     *
209
     * - Storing into the approver field will automatically recognize Person Objects and simple
210
     *   IDs and transform them into a GUID.
211
     * - created can only be set with articles.
212
     * - creator, editor and edited cannot be set.
213
     *
214
     * Any error will trigger midcom_error.
215
     *
216
     * @param string $key The key to set.
217
     * @param mixed $value The value to set.
218
     */
219 288
    private function _set_property(string $key, $value) : bool
220
    {
221 288
        if (is_object($value)) {
222
            $classname = get_class($value);
223
            debug_add("Can not set metadata '{$key}' property with '{$classname}' object as value", MIDCOM_LOG_WARN);
224
            return false;
225
        }
226
227 288
        if (in_array($key, $this->field_config['readonly'])) {
228
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
229
            return false;
230
        }
231
232 288
        if (in_array($key, ['approver', 'approved'])) {
233
            // Prevent lock changes from creating new revisions
234
            $this->__object->_use_rcs = false;
235
        }
236
237 288
        if (in_array($key, $this->field_config['timebased'])) {
238 286
            if (!is_numeric($value) || $value == 0) {
239 2
                $value = null;
240
            } else {
241 286
                $value = new midgard_datetime(gmstrftime('%Y-%m-%d %T', $value));
242
            }
243 151
        } elseif (!in_array($key, $this->field_config['other']) && $key !== 'approver') {
244
            // Fall-back for non-core properties
245 4
            return $this->__object->set_parameter('midcom.helper.metadata', $key, $value);
246
        }
247
248 287
        $this->__metadata->$key = $value;
249 287
        return true;
250
    }
251
252
    /**
253
     * This is the update event handler for the Metadata system. It must be called
254
     * whenever metadata changes to synchronize the various backwards-compatibility
255
     * values in place throughout the system.
256
     *
257
     * @param string $key The key that was updated. Leave empty for a complete update by the Datamanager.
258
     */
259 287
    private function on_update($key = null)
260
    {
261 287
        if ($key) {
262 287
            unset($this->_cache[$key]);
263
        } else {
264
            $this->_cache = [];
265
        }
266
267 287
        if (!empty($this->__object->guid)) {
268 8
            midcom::get()->cache->invalidate($this->__object->guid);
269
        }
270 287
    }
271
272
    /* ------- METADATA I/O INTERFACE -------- */
273
274
    /**
275
     * Retrieves a given metadata key, postprocesses it where necessary
276
     * and stores it into the local cache.
277
     *
278
     * - Person references (both guid and id) get resolved into the corresponding
279
     *   Person object.
280
     * - created, creator, edited and editor are taken from the corresponding
281
     *   MidgardObject fields.
282
     * - Parameters are accessed using $object->get_parameter directly
283
     *
284
     * Note, that we hide any errors from not existent properties explicitly,
285
     * as a few of the MidCOM objects do not support all of the predefined meta
286
     * data fields, PHP will default to "0" in these cases. For Person IDs, this
287
     * "0" is rewritten to "1" to use the MidgardAdministrator account instead.
288
     *
289
     * @param string $key The key to retrieve.
290
     */
291 402
    private function _retrieve_value(string $key)
292
    {
293 402
        if (in_array($key, $this->field_config['timebased'])) {
294
            // This is ugly, but seems the only possible way...
295 369
            if (   isset($this->__metadata->$key)
296 369
                && (string) $this->__metadata->$key !== "0001-01-01T00:00:00+00:00") {
297 172
                return (int) $this->__metadata->$key->format('U');
298
            }
299 321
            return 0;
300
        }
301 278
        if (in_array($key, $this->field_config['person'])) {
302 108
            if (!$this->__metadata->$key) {
303
                // Fall back to "Midgard root user" if person is not found
304 80
                static $root_user_guid = null;
305 80
                if (!$root_user_guid) {
306 1
                    $mc = new midgard_collector('midgard_person', 'id', 1);
307 1
                    $mc->set_key_property('guid');
308 1
                    $mc->execute();
309 1
                    $guids = $mc->list_keys();
310 1
                    if (empty($guids)) {
311
                        $root_user_guid = 'f6b665f1984503790ed91f39b11b5392';
312
                    } else {
313 1
                        $root_user_guid = key($guids);
314
                    }
315
                }
316
317 80
                return $root_user_guid;
318
            }
319 64
            return $this->__metadata->$key;
320
        }
321 267
        if (!in_array($key, $this->field_config['other'])) {
322
            // Fall-back for non-core properties
323 97
            $dm = $this->get_datamanager();
324 97
            if (!$dm->get_schema()->has_field($key)) {
325
                // Fall back to the parameter reader for non-core MidCOM metadata params
326 95
                return $this->__object->get_parameter('midcom.helper.metadata', $key);
327
            }
328 2
            return $dm->get_content_csv()[$key];
329
        }
330 251
        return $this->__metadata->$key;
331
    }
332
333
    /* ------- CONVENIENCE METADATA INTERFACE --------- */
334
335
    /**
336
     * Checks whether the object has been approved since its last editing.
337
     */
338
    public function is_approved() : bool
339
    {
340
        return $this->__object->is_approved();
341
    }
342
343
    /**
344
     * Checks the object's visibility regarding scheduling and the hide flag.
345
     *
346
     * This does not check approval, use is_approved for that.
347
     *
348
     * @see midcom_helper_metadata::is_approved()
349
     */
350
    public function is_visible() : bool
351
    {
352
        if ($this->get('hidden')) {
353
            return false;
354
        }
355
356
        $now = time();
357
        if (   $this->get('schedulestart')
358
            && $this->get('schedulestart') > $now) {
359
            return false;
360
        }
361
        if (   $this->get('scheduleend')
362
            && $this->get('scheduleend') < $now) {
363
            return false;
364
        }
365
        return true;
366
    }
367
368
    /**
369
     * This is a helper function which indicates whether a given object may be shown onsite
370
     * taking approval, scheduling and visibility settings into account. The important point
371
     * here is that it also checks the global configuration defaults, so that this is
372
     * basically the same base on which NAP decides whether to show an item or not.
373
     */
374 128
    public function is_object_visible_onsite() : bool
375
    {
376
        return
377 128
        (   (   midcom::get()->config->get('show_hidden_objects')
378 128
             || $this->is_visible())
379 128
         && (   midcom::get()->config->get('show_unapproved_objects')
380 128
             || $this->is_approved())
381
        );
382
    }
383
384
    /**
385
     * Approves the object.
386
     *
387
     * This sets the approved timestamp to the current time and the
388
     * approver person GUID to the GUID of the person currently
389
     * authenticated.
390
     */
391 1
    public function approve() : bool
392
    {
393 1
        midcom::get()->auth->require_do('midcom:approve', $this->__object);
394 1
        midcom::get()->auth->require_do('midgard:update', $this->__object);
395 1
        return $this->__object->approve();
396
    }
397
398
    /**
399
     * Unapproves the object.
400
     *
401
     * This resets the approved timestamp and sets the
402
     * approver person GUID to the GUID of the person currently
403
     * authenticated.
404
     */
405 1
    public function unapprove() : bool
406
    {
407 1
        midcom::get()->auth->require_do('midcom:approve', $this->__object);
408 1
        midcom::get()->auth->require_do('midgard:update', $this->__object);
409 1
        return $this->__object->unapprove();
410
    }
411
412
    /* ------- CLASS MEMBER FUNCTIONS ------- */
413
414
    /**
415
     * Returns a metadata object for a given content object.
416
     *
417
     * You may bass any one of the following arguments to the function:
418
     *
419
     * - Any class derived from MidgardObject, you must only ensure, that the parameter
420
     *   and guid member functions stays available.
421
     * - Any valid GUID
422
     *
423
     * @param mixed $source The object to attach to, this may be either a MidgardObject or a GUID.
424
     * @return midcom_helper_metadata The created metadata object.
425
     */
426 51
    public static function retrieve($source)
427
    {
428 51
        $object = null;
429
430 51
        if (is_object($source)) {
431 51
            $object = $source;
432 51
            $guid = $source->guid;
433
        } else {
434
            $guid = $source;
435
        }
436
437 51
        if (   $object === null
438 51
            && mgd_is_guid($guid)) {
439
            try {
440
                $object = midcom::get()->dbfactory->get_object_by_guid($guid);
441
            } catch (midcom_error $e) {
442
                debug_add("Failed to create a metadata instance for the GUID {$guid}: " . $e->getMessage(), MIDCOM_LOG_WARN);
443
                debug_print_r("Source was:", $source);
444
445
                return false;
446
            }
447
        }
448
449
        // $object is now populated, too
450 51
        return new self($object);
0 ignored issues
show
Bug introduced by
It seems like $object can also be of type null; however, parameter $object of midcom_helper_metadata::__construct() does only seem to accept midcom_core_dbaobject, maybe add an additional type check? ( Ignorable by Annotation )

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

450
        return new self(/** @scrutinizer ignore-type */ $object);
Loading history...
451
    }
452
453
    /**
454
     * Check if the requested object is locked
455
     */
456 89
    public function is_locked() : bool
457
    {
458
        // Object hasn't been marked to be edited
459 89
        if ($this->get('locked') == 0) {
460 58
            return false;
461
        }
462
463 31
        if (($this->get('locked') + (midcom::get()->config->get('metadata_lock_timeout') * 60)) < time()) {
464
            // lock expired, explicitly clear lock
465
            $this->unlock();
466
            return false;
467
        }
468
469
        // Lock was created by the user, return "not locked"
470 31
        if (   !empty(midcom::get()->auth->user->guid)
471 31
            && $this->get('locker') === midcom::get()->auth->user->guid) {
472 30
            return false;
473
        }
474
475
        // Unlocked states checked and none matched, consider locked
476 1
        return $this->__object->is_locked();
477
    }
478
479
    /**
480
     * Set the object lock
481
     *
482
     * @return boolean       Indicating success
483
     */
484 44
    public function lock() : bool
485
    {
486 44
        midcom::get()->auth->require_do('midgard:update', $this->__object);
487
488 44
        if ($this->__object->lock()) {
489 32
            $this->_cache = [];
490 32
            return true;
491
        }
492
493 12
        return false;
494
    }
495
496
    /**
497
     * Check whether current user can unlock the object
498
     *
499
     * @todo enable specifying user ?
500
     */
501 14
    public function can_unlock() : bool
502
    {
503 14
        return (   $this->__object->can_do('midcom:unlock')
504 14
                || midcom::get()->auth->can_user_do('midcom:unlock', null, midcom_services_auth::class));
505
    }
506
507
    /**
508
     * Unlock the object
509
     *
510
     * @return boolean    Indicating success
511
     */
512 13
    public function unlock() : bool
513
    {
514 13
        if (   $this->can_unlock()
515 13
            && $this->__object->unlock()) {
516 12
            $this->_cache = [];
517 12
            return true;
518
        }
519
520 1
        return false;
521
    }
522
}
523