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
Bug
introduced
by
![]() |
|||||
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
![]() |
|||||
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 |