Passed
Push — master ( 75fb19...387f96 )
by Andreas
19:36
created

__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 140
    public function __construct($object)
24
    {
25 140
        $this->_object = $object;
26 140
    }
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 null on failure
33
     */
34 140
    public function get_object_name($name_property = null) : ?string
35
    {
36 140
        if ($name_property === null) {
37 140
            $name_property = midcom_helper_reflector::get_name_property($this->_object);
38
        }
39 140
        if (    empty($name_property)
40 140
            || !midcom_helper_reflector::get($this->_object)->property_exists($name_property)) {
41
            // Could not resolve valid property
42 1
            return null;
43
        }
44 140
        return $this->_object->{$name_property};
45
    }
46
47
    /**
48
     * Checks for "clean" URL name
49
     *
50
     * @see http://trac.midgard-project.org/ticket/809
51
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
52
     */
53 1
    public function name_is_clean($name_property = null) : bool
54
    {
55 1
        if ($name_copy = $this->get_object_name($name_property)) {
56 1
            return $name_copy === midcom_helper_misc::urlize($name_copy);
57
        }
58
        // empty name is not "clean"
59
        return false;
60
    }
61
62
    /**
63
     * Checks for URL-safe name
64
     *
65
     * @see http://trac.midgard-project.org/ticket/809
66
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
67
     */
68 90
    public function name_is_safe($name_property = null) : bool
69
    {
70 90
        if ($name_copy = $this->get_object_name($name_property)) {
71 90
            return $name_copy === rawurlencode($name_copy);
72
        }
73
        // empty name is not url-safe
74
        return false;
75
    }
76
77
    /**
78
     * Checks for URL-safe name, this variant accepts empty name
79
     *
80
     * @see http://trac.midgard-project.org/ticket/809
81
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
82
     */
83 136
    public function name_is_safe_or_empty($name_property = null) : bool
84
    {
85 136
        $name_copy = $this->get_object_name($name_property);
86 136
        if ($name_copy === null) {
87
            //get_object_name failed
88
            return false;
89
        }
90 136
        if (empty($name_copy)) {
91 64
            return true;
92
        }
93 89
        return $this->name_is_safe($name_property);
94
    }
95
96
    /**
97
     * Checks for "clean" URL name, this variant accepts empty name
98
     *
99
     * @see http://trac.midgard-project.org/ticket/809
100
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
101
     */
102
    public function name_is_clean_or_empty($name_property = null) : bool
103
    {
104
        $name_copy = $this->get_object_name($name_property);
105
        if ($name_copy === null) {
106
            //get_object_name failed
107
            return false;
108
        }
109
        if (empty($name_copy)) {
110
            return true;
111
        }
112
        return $this->name_is_clean($name_property);
113
    }
114
115
    /**
116
     * Check that none of given objects siblings have same name, or the name is empty.
117
     */
118 136
    public function name_is_unique_or_empty() : bool
119
    {
120 136
        $name_copy = $this->get_object_name();
121 136
        if (empty($name_copy)) {
122
            // Allow empty string, but return false if get_object_name failed
123 64
            return $name_copy !== null;
124
        }
125 89
        return $this->name_is_unique();
126
    }
127
128
    /**
129
     * Check that none of given object's siblings have same name.
130
     */
131 89
    public function name_is_unique() : bool
132
    {
133
        // Get current name and sanity-check
134 89
        if (empty($this->get_object_name())) {
135
            // We do not check for empty names, and do not consider them to be unique
136 4
            return false;
137
        }
138
139
        // Start the magic
140 89
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
141 89
        $parent = midcom_helper_reflector_tree::get_parent($this->_object);
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::get_parent(). ( Ignorable by Annotation )

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

141
        $parent = midcom_helper_reflector_tree::get_parent(/** @scrutinizer ignore-type */ $this->_object);
Loading history...
142 89
        if (!empty($parent->guid)) {
143
            // We have parent, check siblings
144 84
            $parent_resolver = new midcom_helper_reflector_tree($parent);
145 84
            $sibling_classes = $parent_resolver->get_child_classes();
146 84
            if (!in_array('midgard_attachment', $sibling_classes)) {
147 84
                $sibling_classes[] = 'midgard_attachment';
148
            }
149 84
            if (!$this->_name_is_unique_check_siblings($sibling_classes, $parent)) {
150 1
                midcom::get()->auth->drop_sudo();
151 84
                return false;
152
            }
153
        } else {
154
            // No parent, we might be a root level class
155 10
            $is_root_class = false;
156 10
            $root_classes = midcom_helper_reflector_tree::get_root_classes();
157 10
            foreach ($root_classes as $classname) {
158 10
                if (midcom::get()->dbfactory->is_a($this->_object, $classname)) {
159 6
                    $is_root_class = true;
160 6
                    if (!$this->_name_is_unique_check_roots($root_classes)) {
161
                        midcom::get()->auth->drop_sudo();
162
                        return false;
163
                    }
164
                }
165
            }
166 10
            if (!$is_root_class) {
167
                // This should not happen, logging error and returning true (even though it's potentially dangerous)
168 4
                midcom::get()->auth->drop_sudo();
169 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);
170 4
                return true;
171
            }
172
        }
173
174 86
        midcom::get()->auth->drop_sudo();
175
        // If we get this far we know we don't have name clashes
176 86
        return true;
177
    }
178
179
    /**
180
     * Check uniqueness for each sibling
181
     */
182 84
    private function _name_is_unique_check_siblings(array $sibling_classes, $parent) : bool
183
    {
184 84
        $name_copy = $this->get_object_name();
185
186 84
        foreach ($sibling_classes as $schema_type) {
187 84
            $qb = $this->_get_sibling_qb($schema_type, $parent);
188 84
            if (!$qb) {
189 84
                continue;
190
            }
191 84
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
192
193 84
            $qb->add_constraint($child_name_property, '=', $name_copy);
194 84
            if ($qb->count()) {
195 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, '/') . "')" );
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::resolve_path(). ( Ignorable by Annotation )

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

195
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path(/** @scrutinizer ignore-type */ $this->_object, '/') . "')" );
Loading history...
196 1
                return false;
197
            }
198
        }
199 84
        return true;
200
    }
201
202
    /**
203
     * Check uniqueness for each root level class
204
     */
205 6
    private function _name_is_unique_check_roots(array $sibling_classes) : bool
206
    {
207 6
        $name_copy = $this->get_object_name();
208
209 6
        foreach ($sibling_classes as $schema_type) {
210 6
            $qb = $this->_get_root_qb($schema_type);
211 6
            if (!$qb) {
212 6
                continue;
213
            }
214 6
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
215
216 6
            $qb->add_constraint($child_name_property, '=', $name_copy);
217 6
            if ($qb->count()) {
218
                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, '/') . "')" );
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::resolve_path(). ( Ignorable by Annotation )

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

218
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path(/** @scrutinizer ignore-type */ $this->_object, '/') . "')" );
Loading history...
219
                return false;
220
            }
221
        }
222 6
        return true;
223
    }
224
225
    /**
226
     * Generates an unique name for the given object.
227
     *
228
     * 1st IF name is empty, we generate one from title (if title is empty too, we return false)
229
     * Then we check if it's unique, if not we add an incrementing
230
     * number to it (before this we make some educated guesses about a
231
     * good starting value)
232
     *
233
     * @param string $title_property Property of the object to use at title, if null will be reflected (see midcom_helper_reflector::get_object_title())
234
     * @param string $extension The file extension, when working with attachments
235
     * @return string string usable as name or boolean false on critical failures
236
     */
237 69
    public function generate_unique_name($title_property = null, $extension = '')
238
    {
239
        // Get current name and sanity-check
240 69
        $original_name = $this->get_object_name();
241 69
        if ($original_name === null) {
242
            // Fatal error with name resolution
243
            debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for name resolution, aborting", MIDCOM_LOG_WARN);
244
            return false;
245
        }
246
247
        // We need the name of the "name" property later
248 69
        $name_prop = midcom_helper_reflector::get_name_property($this->_object);
249
250 69
        if (!empty($original_name)) {
251
            $current_name = $original_name;
252
        } else {
253
            // Empty name, try to generate from title
254 69
            $title_copy = midcom_helper_reflector::get_object_title($this->_object, $title_property);
255 69
            if ($title_copy === false) {
0 ignored issues
show
introduced by
The condition $title_copy === false is always false.
Loading history...
256
                // Fatal error with title resolution
257
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for title resolution when name was empty, aborting", MIDCOM_LOG_WARN);
258
                return false;
259
            }
260 69
            if (empty($title_copy)) {
261 65
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} has empty name and title, aborting", MIDCOM_LOG_WARN);
262 65
                return false;
263
            }
264 8
            $current_name = midcom_helper_misc::urlize($title_copy);
265 8
            unset($title_copy);
266
        }
267
268
        // incrementer, the number to add as suffix and the base name. see _generate_unique_name_resolve_i()
269 8
        list($i, $base_name) = $this->_generate_unique_name_resolve_i($current_name, $extension);
270
271 8
        $this->_object->name = $base_name;
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
272
        // decrementer, do not try more than this many times (the incrementer can raise above this if we start high enough.
273 8
        $d = 100;
274
275
        // The loop, usually we *should* hit gold in first try
276
        do {
277 8
            if ($i > 1) {
278
                // Start suffixes from -002
279 4
                $this->_object->{$name_prop} = $base_name . sprintf('-%03d', $i) . $extension;
280
            }
281
282
            // Handle the decrementer
283 8
            --$d;
284 8
            if ($d < 1) {
285
                // Decrementer underflowed
286
                debug_add("Maximum number of tries exceeded, current name was: " . $this->_object->{$name_prop}, MIDCOM_LOG_ERROR);
287
                $this->_object->{$name_prop} = $original_name;
288
                return false;
289
            }
290
            // and the incrementer
291 8
            ++$i;
292 8
        } while (!$this->name_is_unique());
293
294
        // Get a copy of the current, usable name
295 8
        $ret = (string)$this->_object->{$name_prop};
296
        // Restore the original name
297 8
        $this->_object->{$name_prop} = $original_name;
298 8
        return $ret;
299
    }
300
301 84
    private function _get_sibling_qb(string $schema_type, $parent)
302
    {
303 84
        $dummy = new $schema_type();
304 84
        $child_name_property = midcom_helper_reflector::get_name_property($dummy);
305 84
        if (empty($child_name_property)) {
306
            // This sibling class does not use names
307 84
            return false;
308
        }
309 84
        $resolver = midcom_helper_reflector_tree::get($schema_type);
310 84
        $qb = $resolver->_child_objects_type_qb($schema_type, $parent, false);
311 84
        if (!is_object($qb)) {
312
            return false;
313
        }
314
        // Do not include current object in results, this is the easiest way
315 84
        if (!empty($this->_object->guid)) {
316 17
            $qb->add_constraint('guid', '<>', $this->_object->guid);
317
        }
318 84
        $qb->add_order($child_name_property, 'DESC');
319
        // One result should be enough
320 84
        $qb->set_limit(1);
321 84
        return $qb;
322
    }
323
324 6
    private function _get_root_qb(string $schema_type)
325
    {
326 6
        $dummy = new $schema_type();
327 6
        $child_name_property = midcom_helper_reflector::get_name_property($dummy);
328 6
        if (empty($child_name_property)) {
329
            // This sibling class does not use names
330 6
            return false;
331
        }
332 6
        $qb = midcom_helper_reflector_tree::get($schema_type)->_root_objects_qb(false);
333 6
        if (!$qb) {
334
            return false;
335
        }
336
337
        // Do not include current object in results, this is the easiest way
338 6
        if (!empty($this->_object->guid)) {
339
            $qb->add_constraint('guid', '<>', $this->_object->guid);
340
        }
341 6
        $qb->add_order($child_name_property, 'DESC');
342
        // One result should be enough
343 6
        $qb->set_limit(1);
344 6
        return $qb;
345
    }
346
347 8
    private function _parse_filename(string $name, string $extension, $default = 0) : array
348
    {
349 8
        if (preg_match('/(.*?)-([0-9]{3,})' . $extension . '$/', $name, $name_matches)) {
350
            // Name already has i and base parts, split them.
351
            return [(int) $name_matches[2], (string) $name_matches[1]];
352
        }
353
        // Defaults
354 8
        return [$default, $name];
355
    }
356
357
    /**
358
     * Resolve the base value for the incrementing suffix and for the name.
359
     *
360
     * @see midcom_helper_reflector_nameresolver::generate_unique_name()
361
     * @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())
362
     * @param string $extension The file extension, when working with attachments
363
     * @return array first key is the resolved $i second is the $base_name, which is $current_name without numeric suffix
364
     */
365 8
    private function _generate_unique_name_resolve_i(string $current_name, string $extension) : array
366
    {
367 8
        list($i, $base_name) = $this->_parse_filename($current_name, $extension, 1);
368
369
        // Look for siblings with similar names and see if they have higher i.
370 8
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
371 8
        $parent = midcom_helper_reflector_tree::get_parent($this->_object);
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::get_parent(). ( Ignorable by Annotation )

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

371
        $parent = midcom_helper_reflector_tree::get_parent(/** @scrutinizer ignore-type */ $this->_object);
Loading history...
372 8
        if (!empty($parent->guid)) {
373
            // We have parent, check siblings
374 6
            $parent_resolver = new midcom_helper_reflector_tree($parent);
375 6
            $sibling_classes = $parent_resolver->get_child_classes();
376 6
            if (!in_array('midgard_attachment', $sibling_classes)) {
377 6
                $sibling_classes[] = 'midgard_attachment';
378
            }
379 6
            foreach ($sibling_classes as $schema_type) {
380 6
                $i = $this->process_schema_type($this->_get_sibling_qb($schema_type, $parent), $i, $schema_type, $base_name, $extension);
381
            }
382
        } else {
383
            // No parent, we might be a root level class
384 2
            $is_root_class = false;
385 2
            $root_classes = midcom_helper_reflector_tree::get_root_classes();
386 2
            foreach ($root_classes as $schema_type) {
387 2
                if (midcom::get()->dbfactory->is_a($this->_object, $schema_type)) {
388 1
                    $is_root_class = true;
389 1
                    break;
390
                }
391
            }
392 2
            if (!$is_root_class) {
393
                // This should not happen, logging error and returning true (even though it's potentially dangerous)
394 1
                midcom::get()->auth->drop_sudo();
395 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);
396 1
                return [$i, $base_name];
397
            }
398 1
            foreach ($root_classes as $schema_type) {
399 1
                $i = $this->process_schema_type($this->_get_root_qb($schema_type), $i, $schema_type, $base_name, $extension);
400
            }
401
        }
402 7
        midcom::get()->auth->drop_sudo();
403
404 7
        return [$i, $base_name];
405
    }
406
407 7
    private function process_schema_type($qb, $i, string $schema_type, string $base_name, string $extension) : int
408
    {
409 7
        if (!$qb) {
410 7
            return $i;
411
        }
412 7
        $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
413
414 7
        $qb->add_constraint($child_name_property, 'LIKE', "{$base_name}-%" . $extension);
415 7
        $siblings = $qb->execute();
416 7
        if (!empty($siblings)) {
417
            $sibling = $siblings[0];
418
            $sibling_name = $sibling->{$child_name_property};
419
420
            $sibling_i = $this->_parse_filename($sibling_name, $extension)[0];
421
            if ($sibling_i >= $i) {
422
                $i = $sibling_i + 1;
423
            }
424
        }
425 7
        return $i;
426
    }
427
}
428