Passed
Push — master ( 02d2e0...f9fe83 )
by Andreas
28:12
created

midcom_helper_metadata::is_object_visible_onsite()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 5
nop 0
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 4
rs 10
c 0
b 0
f 0
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
     * @var midgard\portable\api\metadata
77
     */
78
    private $__metadata;
79
80
    /**
81
     * @var array
82
     */
83
    private $_cache = [];
84
85
    private $field_config = [
86
        'readonly' => ['creator', 'created', 'revisor', 'revised', 'locker', 'locked', 'revision', 'size', 'deleted', 'exported', 'imported'],
87
        'timebased' => ['created', 'revised', 'published', 'locked', 'approved', 'schedulestart', 'scheduleend', 'exported', 'imported'],
88
        'person' => ['creator', 'revisor', 'locker', 'approver'],
89
        'other' => ['authors', 'owner', 'hidden', 'navnoentry', 'score', 'revision', 'size', 'deleted']
90
    ];
91
92
    /**
93
     * This will construct a new metadata object for an existing content object.
94
     */
95 404
    public function __construct(midcom_core_dbaobject $object)
96
    {
97 404
        $this->__metadata = $object->__object->metadata;
98 404
        $this->__object = $object;
99 404
    }
100
101
    /* ------- BASIC METADATA INTERFACE --------- */
102
103
    /**
104
     * Return a single metadata key from the object. The return
105
     * type depends on the metadata key that is requested (see the class introduction).
106
     *
107
     * You will not get the data from the datamanager using this calls, but the only
108
     * slightly post-processed metadata values. See _retrieve_value for post processing.
109
     *
110
     * @see midcom_helper_metadata::_retrieve_value()
111
     * @return mixed The key's value.
112
     */
113 416
    public function get(string $key)
114
    {
115 416
        if (!isset($this->_cache[$key])) {
116 291
            $this->_cache[$key] = $this->_retrieve_value($key);
117
        }
118
119 416
        return $this->_cache[$key];
120
    }
121
122 372
    public function __get($key)
123
    {
124 372
        if ($key == 'object') {
125 78
            return $this->__object;
126
        }
127 372
        return $this->get($key);
128
    }
129
130 321
    public function __isset($key)
131
    {
132 321
        if (!isset($this->_cache[$key])) {
133 321
            $this->_cache[$key] = $this->_retrieve_value($key);
134
        }
135
136 321
        return isset($this->_cache[$key]);
137
    }
138
139
    /**
140
     * Return a Datamanager instance for the current object.
141
     *
142
     * Also, whenever the containing datamanager stores its data, you
143
     * <b>must</b> call the on_update() method of this class. This is
144
     * very important or backwards compatibility will be broken.
145
     *
146
     * @see midcom_helper_metadata::on_update()
147
     */
148 98
    public function get_datamanager() : datamanager
149
    {
150 98
        static $schemadb;
151 98
        if ($schemadb === null) {
152
            $schemadb = schemadb::from_path(midcom::get()->config->get('metadata_schema'));
0 ignored issues
show
Bug introduced by
It seems like midcom::get()->config->get('metadata_schema') can also be of type null; however, parameter $path of midcom\datamanager\schemadb::from_path() does only seem to accept string, 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

152
            $schemadb = schemadb::from_path(/** @scrutinizer ignore-type */ midcom::get()->config->get('metadata_schema'));
Loading history...
153
        }
154
155
        // Check if we have metadata schema defined in the schemadb specific for the object's schema or component
156 98
        $object_schema = $this->__object->get_parameter('midcom.helper.datamanager2', 'schema_name');
157 98
        if (!$object_schema || !$schemadb->has($object_schema)) {
158 98
            $component_schema = str_replace('.', '_', midcom_core_context::get()->get_key(MIDCOM_CONTEXT_COMPONENT));
159 98
            if ($schemadb->has($component_schema)) {
160
                // No specific metadata schema for object, fall back to component-specific metadata schema
161
                $object_schema = $component_schema;
162
            } else {
163
                // No metadata schema for component, fall back to default
164 98
                $object_schema = 'metadata';
165
            }
166
        }
167 98
        $dm = new datamanager($schemadb);
168 98
        return $dm->set_storage($this->__object, $object_schema);
169
    }
170
171
    /**
172
     * Frontend for setting a single metadata option
173
     */
174 302
    public function set(string $key, $value) : bool
175
    {
176
        // Store the RCS mode
177 302
        $rcs_mode = $this->__object->_use_rcs;
178
179 302
        if ($return = $this->_set_property($key, $value)) {
180 301
            if ($this->__object->guid) {
181 9
                $return = $this->__object->update();
182
            }
183
184
            // Update the corresponding cache variable
185 301
            $this->on_update($key);
186
        }
187
        // Return the original RCS mode
188 302
        $this->__object->_use_rcs = $rcs_mode;
189 302
        return $return;
190
    }
191
192 15
    public function __set($key, $value)
193
    {
194 15
        $this->set($key, $value);
195 15
    }
196
197
    /**
198
     * Directly set a metadata option.
199
     *
200
     * The passed value will be stored using the follow transformations:
201
     *
202
     * - Storing into the approver field will automatically recognize Person Objects and simple
203
     *   IDs and transform them into a GUID.
204
     * - created can only be set with articles.
205
     * - creator, editor and edited cannot be set.
206
     *
207
     * Any error will trigger midcom_error.
208
     */
209 302
    private function _set_property(string $key, $value) : bool
210
    {
211 302
        if (is_object($value)) {
212
            $classname = get_class($value);
213
            debug_add("Can not set metadata '{$key}' property with '{$classname}' object as value", MIDCOM_LOG_WARN);
214
            return false;
215
        }
216
217 302
        if (in_array($key, $this->field_config['readonly'])) {
218
            midcom_connection::set_error(MGD_ERR_ACCESS_DENIED);
219
            return false;
220
        }
221
222 302
        if (in_array($key, ['approver', 'approved'])) {
223
            // Prevent lock changes from creating new revisions
224
            $this->__object->_use_rcs = false;
225
        }
226
227 302
        if (in_array($key, $this->field_config['timebased'])) {
228 300
            if (!is_numeric($value) || $value == 0) {
229 3
                $value = null;
230
            } else {
231 300
                $value = new midgard_datetime(gmstrftime('%Y-%m-%d %T', $value));
232
            }
233 153
        } elseif (!in_array($key, $this->field_config['other']) && $key !== 'approver') {
234
            // Fall-back for non-core properties
235 4
            return $this->__object->set_parameter('midcom.helper.metadata', $key, $value);
236
        }
237
238 301
        $this->__metadata->$key = $value;
239 301
        return true;
240
    }
241
242
    /**
243
     * This is the update event handler for the Metadata system. It must be called
244
     * whenever metadata changes to synchronize the various backwards-compatibility
245
     * values in place throughout the system.
246
     *
247
     * @param string $key The key that was updated. Leave empty for a complete update by the Datamanager.
248
     */
249 301
    private function on_update(string $key = null)
250
    {
251 301
        if ($key) {
252 301
            unset($this->_cache[$key]);
253
        } else {
254
            $this->_cache = [];
255
        }
256
257 301
        if (!empty($this->__object->guid)) {
258 9
            midcom::get()->cache->invalidate($this->__object->guid);
259
        }
260 301
    }
261
262
    /* ------- METADATA I/O INTERFACE -------- */
263
264
    /**
265
     * Retrieves a given metadata key, postprocesses it where necessary
266
     * and stores it into the local cache.
267
     *
268
     * - Person references (both guid and id) get resolved into the corresponding
269
     *   Person object.
270
     * - created, creator, edited and editor are taken from the corresponding
271
     *   MidgardObject fields.
272
     * - Parameters are accessed using $object->get_parameter directly
273
     *
274
     * Note, that we hide any errors from not existent properties explicitly,
275
     * as a few of the MidCOM objects do not support all of the predefined meta
276
     * data fields, PHP will default to "0" in these cases. For Person IDs, this
277
     * "0" is rewritten to "1" to use the MidgardAdministrator account instead.
278
     */
279 416
    private function _retrieve_value(string $key)
280
    {
281 416
        if (in_array($key, $this->field_config['timebased'])) {
282
            // This is ugly, but seems the only possible way...
283 383
            if (   isset($this->__metadata->$key)
284 383
                && (string) $this->__metadata->$key !== "0001-01-01T00:00:00+00:00") {
285 172
                return (int) $this->__metadata->$key->format('U');
286
            }
287 335
            return 0;
288
        }
289 281
        if (in_array($key, $this->field_config['person'])) {
290 108
            if (!$this->__metadata->$key) {
291
                // Fall back to "Midgard root user" if person is not found
292 80
                static $root_user_guid = null;
293 80
                if (!$root_user_guid) {
294
                    $mc = new midgard_collector('midgard_person', 'id', 1);
295
                    $mc->set_key_property('guid');
296
                    $mc->execute();
297
                    $root_user_guid = key($mc->list_keys()) ?: 'f6b665f1984503790ed91f39b11b5392';
298
                }
299
300 80
                return $root_user_guid;
301
            }
302 65
            return $this->__metadata->$key;
303
        }
304 270
        if (!in_array($key, $this->field_config['other'])) {
305
            // Fall-back for non-core properties
306 97
            $dm = $this->get_datamanager();
307 97
            if (!$dm->get_schema()->has_field($key)) {
308
                // Fall back to the parameter reader for non-core MidCOM metadata params
309 95
                return $this->__object->get_parameter('midcom.helper.metadata', $key);
310
            }
311 2
            return $dm->get_content_csv()[$key];
312
        }
313 254
        return $this->__metadata->$key;
314
    }
315
316
    /* ------- CONVENIENCE METADATA INTERFACE --------- */
317
318
    /**
319
     * Checks whether the object has been approved since its last editing.
320
     */
321
    public function is_approved() : bool
322
    {
323
        return $this->__object->is_approved();
324
    }
325
326
    /**
327
     * Checks the object's visibility regarding scheduling and the hide flag.
328
     *
329
     * This does not check approval, use is_approved for that.
330
     *
331
     * @see midcom_helper_metadata::is_approved()
332
     */
333
    public function is_visible() : bool
334
    {
335
        if ($this->get('hidden')) {
336
            return false;
337
        }
338
339
        $now = time();
340
        if (   $this->get('schedulestart')
341
            && $this->get('schedulestart') > $now) {
342
            return false;
343
        }
344
        if (   $this->get('scheduleend')
345
            && $this->get('scheduleend') < $now) {
346
            return false;
347
        }
348
        return true;
349
    }
350
351
    /**
352
     * This is a helper function which indicates whether a given object may be shown onsite
353
     * taking approval, scheduling and visibility settings into account. The important point
354
     * here is that it also checks the global configuration defaults, so that this is
355
     * basically the same base on which NAP decides whether to show an item or not.
356
     */
357 141
    public function is_object_visible_onsite() : bool
358
    {
359
        return
360 141
        (   (   midcom::get()->config->get('show_hidden_objects')
361 141
             || $this->is_visible())
362 141
         && (   midcom::get()->config->get('show_unapproved_objects')
363 141
             || $this->is_approved())
364
        );
365
    }
366
367
    /**
368
     * Approves the object.
369
     *
370
     * This sets the approved timestamp to the current time and the
371
     * approver person GUID to the GUID of the person currently
372
     * authenticated.
373
     */
374 1
    public function approve() : bool
375
    {
376 1
        midcom::get()->auth->require_do('midcom:approve', $this->__object);
377 1
        midcom::get()->auth->require_do('midgard:update', $this->__object);
378 1
        return $this->__object->approve();
379
    }
380
381
    /**
382
     * Unapproves the object.
383
     *
384
     * This resets the approved timestamp and sets the
385
     * approver person GUID to the GUID of the person currently
386
     * authenticated.
387
     */
388 1
    public function unapprove() : bool
389
    {
390 1
        midcom::get()->auth->require_do('midcom:approve', $this->__object);
391 1
        midcom::get()->auth->require_do('midgard:update', $this->__object);
392 1
        return $this->__object->unapprove();
393
    }
394
395
    /* ------- CLASS MEMBER FUNCTIONS ------- */
396
397
    /**
398
     * Returns a metadata object for a given content object.
399
     *
400
     * You may pass any one of the following arguments to the function:
401
     *
402
     * - Any class derived from MidgardObject, you must only ensure, that the parameter
403
     *   and guid member functions stays available.
404
     * - Any valid GUID
405
     *
406
     * @param mixed $source The object to attach to, this may be either a MidgardObject or a GUID.
407
     */
408 51
    public static function retrieve($source) : ?self
409
    {
410 51
        $object = null;
411
412 51
        if (is_object($source)) {
413 51
            $object = $source;
414 51
            $guid = $source->guid;
415
        } else {
416
            $guid = $source;
417
        }
418
419 51
        if (   $object === null
420 51
            && mgd_is_guid($guid)) {
421
            try {
422
                $object = midcom::get()->dbfactory->get_object_by_guid($guid);
423
            } catch (midcom_error $e) {
424
                debug_add("Failed to create a metadata instance for the GUID {$guid}: " . $e->getMessage(), MIDCOM_LOG_WARN);
425
                debug_print_r("Source was:", $source);
426
427
                return null;
428
            }
429
        }
430
431
        // $object is now populated, too
432 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

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