Issues (806)

lib/midcom/helper/metadata.php (2 issues)

Labels
Severity
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
    private midcom_core_dbaobject $__object;
71
72
    private midgard\portable\api\metadata $__metadata;
73
74
    private array $field_config = [
75
        'readonly' => ['creator', 'created', 'revisor', 'revised', 'locker', 'locked', 'revision', 'size', 'deleted', 'exported', 'imported', 'islocked', 'isapproved'],
76
        'timebased' => ['created', 'revised', 'published', 'locked', 'approved', 'schedulestart', 'scheduleend', 'exported', 'imported'],
77
        'person' => ['creator', 'revisor', 'locker', 'approver'],
78
        'other' => ['authors', 'owner', 'hidden', 'navnoentry', 'score', 'revision', 'size', 'deleted'],
79
        'functions' => [
80
            'islocked' => 'is_locked',
81
            'isapproved' => 'is_approved'
82
        ]
83
    ];
84
85
    /**
86
     * This will construct a new metadata object for an existing content object.
87
     */
88 418
    public function __construct(midcom_core_dbaobject $object)
89
    {
90 418
        $this->__metadata = $object->__object->metadata;
91 418
        $this->__object = $object;
92
    }
93
94
    /* ------- BASIC METADATA INTERFACE --------- */
95
96
    /**
97
     * Return a single metadata key from the object. The return
98
     * type depends on the metadata key that is requested (see the class introduction).
99
     *
100
     * You will not get the data from the datamanager using this calls, but the only
101
     * slightly post-processed metadata values. See _retrieve_value for post processing.
102
     *
103
     * @see midcom_helper_metadata::_retrieve_value()
104
     * @return mixed The key's value.
105
     */
106 433
    public function get(string $key)
107
    {
108 433
        return $this->_retrieve_value($key);
109
    }
110
111 389
    public function __get($key)
112
    {
113 389
        if ($key == 'object') {
114 239
            return $this->__object;
115
        }
116 389
        return $this->get($key);
117
    }
118
119 320
    public function __isset($key)
120
    {
121 320
        return $this->_retrieve_value($key) !== null;
122
    }
123
124
    /**
125
     * Return a Datamanager instance for the current object.
126
     *
127
     * Also, whenever the containing datamanager stores its data, you
128
     * <b>must</b> call the on_update() method of this class. This is
129
     * very important or backwards compatibility will be broken.
130
     *
131
     * @see midcom_helper_metadata::on_update()
132
     */
133 10
    public function get_datamanager() : datamanager
134
    {
135 10
        static $schemadb;
136 10
        $schemadb ??= schemadb::from_path(midcom::get()->config->get('metadata_schema'));
0 ignored issues
show
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

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

410
        return new self(/** @scrutinizer ignore-type */ $object);
Loading history...
411
    }
412
413
    /**
414
     * Check if the requested object is locked
415
     */
416 286
    public function is_locked() : bool
417
    {
418
        // Object hasn't been marked to be edited
419 286
        if ($this->get('locked') == 0) {
420 271
            return false;
421
        }
422
423 33
        if (($this->get('locked') + (midcom::get()->config->get('metadata_lock_timeout') * 60)) < time()) {
424
            // lock expired, explicitly clear lock
425
            $this->unlock();
426
            return false;
427
        }
428
429
        // Lock was created by the user, return "not locked"
430 33
        if ($this->get('locker') === midcom::get()->auth->user?->guid) {
431 32
            return false;
432
        }
433
434
        // Unlocked states checked and none matched, consider locked
435 1
        return $this->__object->is_locked();
436
    }
437
438
    /**
439
     * Set the object lock
440
     *
441
     * @return boolean       Indicating success
442
     */
443 45
    public function lock() : bool
444
    {
445 45
        midcom::get()->auth->require_do('midgard:update', $this->__object);
446 45
        return $this->__object->lock();
447
    }
448
449
    /**
450
     * Check whether current user can unlock the object
451
     *
452
     * @todo enable specifying user ?
453
     */
454 15
    public function can_unlock() : bool
455
    {
456 15
        return (   midcom::get()->auth->user?->guid == $this->__object->metadata->locker
457 15
                || $this->__object->can_do('midcom:unlock')
458 15
                || midcom::get()->auth->can_user_do('midcom:unlock', class: midcom_services_auth::class));
459
    }
460
461
    /**
462
     * Unlock the object
463
     *
464
     * @return boolean    Indicating success
465
     */
466 14
    public function unlock() : bool
467
    {
468 14
        return $this->can_unlock() && $this->__object->unlock();
469
    }
470
}
471