Completed
Push — master ( 309357...7ad46f )
by Andreas
18:00
created

lib/midcom/helper/reflector/nameresolver.php (2 issues)

Labels
Severity
1
<?php
2
/**
3
 * @package midcom.helper.reflector
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
/**
10
 * Helper class for object name handling
11
 *
12
 * @package midcom.helper.reflector
13
 */
14
class midcom_helper_reflector_nameresolver
15
{
16
    /**
17
     * The object we're working with
18
     *
19
     * @var midcom_core_dbaobject
20
     */
21
    private $_object;
22
23 147
    public function __construct($object)
24
    {
25 147
        $this->_object = $object;
26 147
    }
27
28
    /**
29
     * Resolves the "name" of given object
30
     *
31
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
32
     * @return string value of name property or boolean false on failure
33
     */
34 147
    public function get_object_name($name_property = null)
35
    {
36 147
        if (is_null($name_property)) {
37 147
            $name_property = midcom_helper_reflector::get_name_property($this->_object);
38
        }
39 147
        if (    empty($name_property)
40 147
            || !midcom_helper_reflector::get($this->_object)->property_exists($name_property)) {
41
            // Could not resolve valid property
42 1
            return false;
43
        }
44
        // Make copy via typecast, very important or we might accidentally manipulate the given object
45 147
        return (string)$this->_object->{$name_property};
46
    }
47
48
    /**
49
     * Checks for "clean" URL name
50
     *
51
     * @see http://trac.midgard-project.org/ticket/809
52
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
53
     * @return boolean indicating cleanliness
54
     */
55 1
    public function name_is_clean($name_property = null)
56
    {
57 1
        $name_copy = $this->get_object_name($name_property);
58 1
        if (empty($name_copy)) {
59
            // empty name is not "clean"
60
            return false;
61
        }
62 1
        $generator = midcom::get()->serviceloader->load(midcom_core_service_urlgenerator::class);
63 1
        return ($name_copy === $generator->from_string($name_copy));
64
    }
65
66
    /**
67
     * Checks for URL-safe name
68
     *
69
     * @see http://trac.midgard-project.org/ticket/809
70
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
71
     * @return boolean indicating safety
72
     */
73 89
    public function name_is_safe($name_property = null)
74
    {
75 89
        $name_copy = $this->get_object_name($name_property);
76
77 89
        if (empty($name_copy)) {
78
            // empty name is not url-safe
79
            return false;
80
        }
81 89
        return ($name_copy === rawurlencode($name_copy));
82
    }
83
84
    /**
85
     * Checks for URL-safe name, this variant accepts empty name
86
     *
87
     * @see http://trac.midgard-project.org/ticket/809
88
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
89
     * @return boolean indicating safety
90
     */
91 143
    public function name_is_safe_or_empty($name_property = null)
92
    {
93 143
        $name_copy = $this->get_object_name($name_property);
94 143
        if ($name_copy === false) {
95
            //get_object_name failed
96
            return false;
97
        }
98 143
        if (empty($name_copy)) {
99 67
            return true;
100
        }
101 88
        return $this->name_is_safe($name_property);
102
    }
103
104
    /**
105
     * Checks for "clean" URL name, this variant accepts empty name
106
     *
107
     * @see http://trac.midgard-project.org/ticket/809
108
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
109
     * @return boolean indicating cleanliness
110
     */
111
    public function name_is_clean_or_empty($name_property = null)
112
    {
113
        $name_copy = $this->get_object_name($name_property);
114
        if ($name_copy === false) {
115
            //get_object_name failed
116
            return false;
117
        }
118
        if (empty($name_copy)) {
119
            return true;
120
        }
121
        return $this->name_is_clean($name_property);
122
    }
123
124
    /**
125
     * Check that none of given objects siblings have same name, or the name is empty.
126
     *
127
     * @return boolean indicating uniqueness
128
     */
129 143
    public function name_is_unique_or_empty()
130
    {
131 143
        $name_copy = $this->get_object_name();
132 143
        if (   empty($name_copy)
133 143
            && $name_copy !== false) {
134
            // Allow empty string name
135 67
            return true;
136
        }
137 88
        return $this->name_is_unique();
138
    }
139
140
    /**
141
     * Check that none of given object's siblings have same name.
142
     *
143
     * @return boolean indicating uniqueness
144
     */
145 88
    public function name_is_unique()
146
    {
147
        // Get current name and sanity-check
148 88
        $name_copy = $this->get_object_name();
149 88
        if (empty($name_copy)) {
150
            // We do not check for empty names, and do not consider them to be unique
151 4
            return false;
152
        }
153
154
        // Start the magic
155 88
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
156 88
        $parent = midcom_helper_reflector_tree::get_parent($this->_object);
157 88
        if (!empty($parent->guid)) {
158
            // We have parent, check siblings
159 84
            $parent_resolver = new midcom_helper_reflector_tree($parent);
0 ignored issues
show
It seems like $parent can also be of type false; however, parameter $src of midcom_helper_reflector_tree::__construct() does only seem to accept midgard_object|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

159
            $parent_resolver = new midcom_helper_reflector_tree(/** @scrutinizer ignore-type */ $parent);
Loading history...
160 84
            $sibling_classes = $parent_resolver->get_child_classes();
161 84
            if (!in_array('midgard_attachment', $sibling_classes)) {
162 84
                $sibling_classes[] = 'midgard_attachment';
163
            }
164 84
            if (!$this->_name_is_unique_check_siblings($sibling_classes, $parent)) {
165 1
                midcom::get()->auth->drop_sudo();
166 84
                return false;
167
            }
168
        } else {
169
            // No parent, we might be a root level class
170 12
            $is_root_class = false;
171 12
            $root_classes = midcom_helper_reflector_tree::get_root_classes();
172 12
            foreach ($root_classes as $classname) {
173 12
                if (midcom::get()->dbfactory->is_a($this->_object, $classname)) {
174 8
                    $is_root_class = true;
175 8
                    if (!$this->_name_is_unique_check_roots($root_classes)) {
176
                        midcom::get()->auth->drop_sudo();
177 12
                        return false;
178
                    }
179
                }
180
            }
181 12
            if (!$is_root_class) {
182
                // This should not happen, logging error and returning true (even though it's potentially dangerous)
183 4
                midcom::get()->auth->drop_sudo();
184 4
                debug_add("Object " . get_class($this->_object) . " #" . $this->_object->id . " has no valid parent but is not listed in the root classes, don't know what to do, returning true and supposing user knows what he is doing", MIDCOM_LOG_ERROR);
185 4
                return true;
186
            }
187
        }
188
189 85
        midcom::get()->auth->drop_sudo();
190
        // If we get this far we know we don't have name clashes
191 85
        return true;
192
    }
193
194
    /**
195
     * Check uniqueness for each sibling
196
     *
197
     * @param array $sibling_classes array of classes to check
198
     * @return boolean true means no clashes, false means clash.
199
     */
200 84
    private function _name_is_unique_check_siblings($sibling_classes, $parent)
201
    {
202 84
        $name_copy = $this->get_object_name();
203
204 84
        foreach ($sibling_classes as $schema_type) {
205 84
            $qb = $this->_get_sibling_qb($schema_type, $parent);
206 84
            if (!$qb) {
207 84
                continue;
208
            }
209 84
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
210
211 84
            $qb->add_constraint($child_name_property, '=', $name_copy);
212 84
            if ($qb->count()) {
213 1
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path($this->_object, '/') . "')" );
214 84
                return false;
215
            }
216
        }
217 84
        return true;
218
    }
219
220
    /**
221
     * Check uniqueness for each root level class
222
     *
223
     * @param array $sibling_classes array of classes to check
224
     * @return boolean true means no clashes, false means clash.
225
     */
226 8
    private function _name_is_unique_check_roots($sibling_classes)
227
    {
228 8
        if (!$sibling_classes) {
229
            // We don't know about siblings, allow this to happen.
230
            // Note: This also happens with the "neverchild" types like midgard_attachment and midgard_parameter
231
            return true;
232
        }
233 8
        $name_copy = $this->get_object_name();
234
235 8
        foreach ($sibling_classes as $schema_type) {
236 8
            $qb = $this->_get_root_qb($schema_type);
237 8
            if (!$qb) {
238 8
                continue;
239
            }
240 8
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
241
242 8
            $qb->add_constraint($child_name_property, '=', $name_copy);
243 8
            if ($qb->count()) {
244
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path($this->_object, '/') . "')" );
245 8
                return false;
246
            }
247
        }
248 8
        return true;
249
    }
250
251
    /**
252
     * Generates an unique name for the given object.
253
     *
254
     * 1st IF name is empty, we generate one from title (if title is empty too, we return false)
255
     * Then we check if it's unique, if not we add an incrementing
256
     * number to it (before this we make some educated guesses about a
257
     * good starting value)
258
     *
259
     * @param string $title_property Property of the object to use at title, if null will be reflected (see midcom_helper_reflector::get_object_title())
260
     * @param string $extension The file extension, when working with attachments
261
     * @return string string usable as name or boolean false on critical failures
262
     */
263 72
    public function generate_unique_name($title_property = null, $extension = '')
264
    {
265
        // Get current name and sanity-check
266 72
        $original_name = $this->get_object_name();
267 72
        if ($original_name === false) {
268
            // Fatal error with name resolution
269
            debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for name resolution, aborting", MIDCOM_LOG_WARN);
270
            return false;
271
        }
272
273
        // We need the name of the "name" property later
274 72
        $name_prop = midcom_helper_reflector::get_name_property($this->_object);
275
276 72
        if (!empty($original_name)) {
277
            $current_name = (string)$original_name;
278
        } else {
279
            // Empty name, try to generate from title
280 72
            $title_copy = midcom_helper_reflector::get_object_title($this->_object, $title_property);
281 72
            if ($title_copy === false) {
282
                // Fatal error with title resolution
283
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for title resolution when name was empty, aborting", MIDCOM_LOG_WARN);
284
                return false;
285
            }
286 72
            if (empty($title_copy)) {
287 68
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} has empty name and title, aborting", MIDCOM_LOG_WARN);
288 68
                return false;
289
            }
290 8
            $generator = midcom::get()->serviceloader->load(midcom_core_service_urlgenerator::class);
291 8
            $current_name = $generator->from_string($title_copy);
292 8
            unset($title_copy);
293
        }
294
295
        // incrementer, the number to add as suffix and the base name. see _generate_unique_name_resolve_i()
296 8
        list($i, $base_name) = $this->_generate_unique_name_resolve_i($current_name, $extension);
297
298 8
        $this->_object->name = $base_name;
299
        // decrementer, do not try more than this many times (the incrementer can raise above this if we start high enough.
300 8
        $d = 100;
301
302
        // The loop, usually we *should* hit gold in first try
303
        do {
304 8
            if ($i > 1) {
305
                // Start suffixes from -002
306 4
                $this->_object->{$name_prop} = $base_name . sprintf('-%03d', $i) . $extension;
307
            }
308
309
            // Handle the decrementer
310 8
            --$d;
311 8
            if ($d < 1) {
312
                // Decrementer underflowed
313
                debug_add("Maximum number of tries exceeded, current name was: " . $this->_object->{$name_prop}, MIDCOM_LOG_ERROR);
314
                $this->_object->{$name_prop} = $original_name;
315
                return false;
316
            }
317
            // and the incrementer
318 8
            ++$i;
319 8
        } while (!$this->name_is_unique());
320
321
        // Get a copy of the current, usable name
322 8
        $ret = (string)$this->_object->{$name_prop};
323
        // Restore the original name
324 8
        $this->_object->{$name_prop} = $original_name;
325 8
        return $ret;
326
    }
327
328 84
    private function _get_sibling_qb($schema_type, $parent)
329
    {
330 84
        $dummy = new $schema_type();
331 84
        $child_name_property = midcom_helper_reflector::get_name_property($dummy);
332 84
        if (empty($child_name_property)) {
333
            // This sibling class does not use names
334 84
            return false;
335
        }
336 84
        $resolver = midcom_helper_reflector_tree::get($schema_type);
337 84
        $qb = $resolver->_child_objects_type_qb($schema_type, $parent, false);
338 84
        if (!is_object($qb)) {
339
            return false;
340
        }
341
        // Do not include current object in results, this is the easiest way
342 84
        if (!empty($this->_object->guid)) {
343 15
            $qb->add_constraint('guid', '<>', $this->_object->guid);
344
        }
345 84
        $qb->add_order($child_name_property, 'DESC');
346
        // One result should be enough
347 84
        $qb->set_limit(1);
348 84
        return $qb;
349
    }
350
351 8
    private function _get_root_qb($schema_type)
352
    {
353 8
        $dummy = new $schema_type();
354 8
        $child_name_property = midcom_helper_reflector::get_name_property($dummy);
355 8
        if (empty($child_name_property)) {
356
            // This sibling class does not use names
357 8
            return false;
358
        }
359 8
        $qb = midcom_helper_reflector_tree::get($schema_type)->_root_objects_qb(false);
360 8
        if (!$qb) {
361
            return false;
362
        }
363
364
        // Do not include current object in results, this is the easiest way
365 8
        if (!empty($this->_object->guid)) {
366
            $qb->add_constraint('guid', '<>', $this->_object->guid);
367
        }
368 8
        $qb->add_order($child_name_property, 'DESC');
369
        // One result should be enough
370 8
        $qb->set_limit(1);
371 8
        return $qb;
372
    }
373
374 8
    private function _parse_filename($name, $extension, $default = 0)
375
    {
376 8
        if (preg_match('/(.*?)-([0-9]{3,})' . $extension . '$/', $name, $name_matches)) {
377
            // Name already has i and base parts, split them.
378
            return [(int) $name_matches[2], (string) $name_matches[1]];
379
        }
380
        // Defaults
381 8
        return [$default, $name];
382
    }
383
384
    /**
385
     * Resolve the base value for the incrementing suffix and for the name.
386
     *
387
     * @see midcom_helper_reflector_nameresolver::generate_unique_name()
388
     * @param string $current_name the "current name" of the object (might not be the actual name value see the title logic in generate_unique_name())
389
     * @param string $extension The file extension, when working with attachments
390
     * @return array first key is the resolved $i second is the $base_name, which is $current_name without numeric suffix
391
     */
392 8
    private function _generate_unique_name_resolve_i($current_name, $extension)
393
    {
394 8
        list($i, $base_name) = $this->_parse_filename($current_name, $extension, 1);
395
396
        // Look for siblings with similar names and see if they have higher i.
397 8
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
398 8
        $parent = midcom_helper_reflector_tree::get_parent($this->_object);
399 8
        if (!empty($parent->guid)) {
400
            // We have parent, check siblings
401 6
            $parent_resolver = new midcom_helper_reflector_tree($parent);
0 ignored issues
show
It seems like $parent can also be of type false; however, parameter $src of midcom_helper_reflector_tree::__construct() does only seem to accept midgard_object|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

401
            $parent_resolver = new midcom_helper_reflector_tree(/** @scrutinizer ignore-type */ $parent);
Loading history...
402 6
            $sibling_classes = $parent_resolver->get_child_classes();
403 6
            if (!in_array('midgard_attachment', $sibling_classes)) {
404 6
                $sibling_classes[] = 'midgard_attachment';
405
            }
406 6
            foreach ($sibling_classes as $schema_type) {
407 6
                $i = $this->process_schema_type($this->_get_sibling_qb($schema_type, $parent), $i, $schema_type, $base_name, $extension);
408
            }
409
        } else {
410
            // No parent, we might be a root level class
411 2
            $is_root_class = false;
412 2
            $root_classes = midcom_helper_reflector_tree::get_root_classes();
413 2
            foreach ($root_classes as $schema_type) {
414 2
                if (midcom::get()->dbfactory->is_a($this->_object, $schema_type)) {
415 1
                    $is_root_class = true;
416 2
                    break;
417
                }
418
            }
419 2
            if (!$is_root_class) {
420
                // This should not happen, logging error and returning true (even though it's potentially dangerous)
421 1
                midcom::get()->auth->drop_sudo();
422 1
                debug_add("Object " . get_class($this->_object) . " #" . $this->_object->id . " has no valid parent but is not listed in the root classes, don't know what to do, letting higher level decide", MIDCOM_LOG_ERROR);
423 1
                return [$i, $base_name];
424
            }
425 1
            foreach ($root_classes as $schema_type) {
426 1
                $i = $this->process_schema_type($this->_get_root_qb($schema_type), $i, $schema_type, $base_name, $extension);
427
            }
428
        }
429 7
        midcom::get()->auth->drop_sudo();
430
431 7
        return [$i, $base_name];
432
    }
433
434 7
    private function process_schema_type($qb, $i, $schema_type, $base_name, $extension)
435
    {
436 7
        if (!$qb) {
437 7
            return $i;
438
        }
439 7
        $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
440
441 7
        $qb->add_constraint($child_name_property, 'LIKE', "{$base_name}-%" . $extension);
442 7
        $siblings = $qb->execute();
443 7
        if (!empty($siblings)) {
444
            $sibling = $siblings[0];
445
            $sibling_name = $sibling->{$child_name_property};
446
447
            $sibling_i = $this->_parse_filename($sibling_name, $extension)[0];
448
            if ($sibling_i >= $i) {
449
                $i = $sibling_i + 1;
450
            }
451
        }
452 7
        return $i;
453
    }
454
}
455