1 <?php
2
3 /*
4 * This file is part of the Brickrouge package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Brickrouge;
13
14 /**
15 * Inserts a value in a array before, or after, at given key.
16 *
17 * Numeric keys are not preserved.
18 *
19 * @param $array
20 * @param $relative
21 * @param $value
22 * @param $key
23 * @param $after
24 *
25 * @return A new array with the value inserted at the requested position.
26 */
27 function array_insert($array, $relative, $value, $key=null, $after=false)
28 {
29 $keys = array_keys($array);
30 $pos = array_search($relative, $keys, true);
31
32 if ($after)
33 {
34 $pos++;
35 }
36
37 $spliced = array_splice($array, $pos);
38
39 if ($key !== null)
40 {
41 $array = array_merge($array, array($key => $value));
42 }
43 else
44 {
45 array_unshift($spliced, $value);
46 }
47
48 return array_merge($array, $spliced);
49 }
50
51 function array_flatten(array $array)
52 {
53 $result = $array;
54
55 foreach ($array as $key => &$value)
56 {
57 _array_flatten_callback($result, '', $key, $value);
58 }
59
60 return $result;
61 }
62
63 function _array_flatten_callback(&$result, $pre, $key, &$value)
64 {
65 if (is_array($value))
66 {
67 foreach ($value as $vk => &$vv)
68 {
69 _array_flatten_callback($result, $pre ? ($pre . '[' . $key . ']') : $key, $vk, $vv);
70 }
71 }
72 else if (is_object($value))
73 {
74 // FIXME: throw new Exception('Don\'t know what to do with objects: \1', $value);
75 }
76 else
77 {
78 /* FIXME-20100520: this has been disabled because sometime empty values (e.g. '') are
79 * correct values. The function was first used with Brickrouge\Form which made sense at the time
80 * but now changing values would be a rather strange behaviour.
81 #
82 # a trick to create undefined values
83 #
84
85 if (!strlen($value))
86 {
87 $value = null;
88 }
89 */
90
91 if ($pre)
92 {
93 #
94 # only arrays are flattened
95 #
96
97 $pre .= '[' . $key . ']';
98
99 $result[$pre] = $value;
100 }
101 else
102 {
103 #
104 # simple values are kept intact
105 #
106
107 $result[$key] = $value;
108 }
109 }
110 }
111
112 /**
113 * Sorts an array using a stable sorting algorithm while preserving its keys.
114 *
115 * A stable sorting algorithm maintains the relative order of values with equal keys.
116 *
117 * The array is always sorted in ascending order but one can use the {@link array_reverse()}
118 * function to reverse the array. Also keys are preserved, even numeric ones, use the
119 * {@link array_values()} function to create an array with an ascending index.
120 *
121 * @param array $array
122 * @param callable $picker
123 */
124 function stable_sort(&$array, $picker=null)
125 {
126 static $transform, $restore;
127
128 $i = 0;
129
130 if (!$transform)
131 {
132 $transform = function(&$v, $k) use (&$i)
133 {
134 $v = array($v, ++$i, $k, $v);
135 };
136
137 $restore = function(&$v, $k)
138 {
139 $v = $v[3];
140 };
141 }
142
143 if ($picker)
144 {
145 array_walk
146 (
147 $array, function(&$v, $k) use (&$i, $picker)
148 {
149 $v = array($picker($v), ++$i, $k, $v);
150 }
151 );
152 }
153 else
154 {
155 array_walk($array, $transform);
156 }
157
158 asort($array);
159
160 array_walk($array, $restore);
161 }
162
163 /**
164 * Convert special characters to HTML entities.
165 *
166 * @param string $str The string being converted.
167 * @param string $charset Defines character set used in conversion. The default charset is
168 * {@link BrickRoute\CHARSET} (utf-8).
169 *
170 * @return string
171 */
172 function escape($str, $charset=CHARSET)
173 {
174 return htmlspecialchars($str, ENT_COMPAT, $charset);
175 }
176
177 function escape_all($str, $charset=CHARSET)
178 {
179 return htmlentities($str, ENT_COMPAT, $charset);
180 }
181
182 /**
183 * Shortens a string at a specified position.
184 *
185 * @param string $str The string to shorten.
186 * @param int $length The desired length of the string.
187 * @param float $position Position at which characters can be removed.
188 * @param bool $shortened `true` if the string was shortened, `false` otherwise.
189 *
190 * @return string
191 */
192 function shorten($str, $length=32, $position=.75, &$shortened=null)
193 {
194 $l = mb_strlen($str);
195
196 if ($l <= $length)
197 {
198 return $str;
199 }
200
201 $length--;
202 $position = (int) ($position * $length);
203
204 if ($position == 0)
205 {
206 $str = '…' . mb_substr($str, $l - $length);
207 }
208 else if ($position == $length)
209 {
210 $str = mb_substr($str, 0, $length) . '…';
211 }
212 else
213 {
214 $str = mb_substr($str, 0, $position) . '…' . mb_substr($str, $l - ($length - $position));
215 }
216
217 $shortened = true;
218
219 return $str;
220 }
221
222 function dump($value)
223 {
224 if (function_exists('xdebug_var_dump'))
225 {
226 ob_start();
227
228 xdebug_var_dump($value);
229
230 $value = ob_get_clean();
231 }
232 else
233 {
234 $value = '<pre>' . escape(print_r($value, true)) . '</pre>';
235 }
236
237 return $value;
238 }
239
240 /**
241 * Returns a web accessible path to a web inaccessible file.
242 *
243 * If the accessible file does not exists it is created.
244 *
245 * The directory where the files are copied is defined by the {@link ACCESSIBLE_ASSETS} constant.
246 *
247 * Note: Calls to this function are forwarded to {@link Helpers::get_accessible_file()}.
248 *
249 * @param string $path Absolute path to the web inaccessible file.
250 * @param string $suffix Optional suffix for the web accessible filename.
251 *
252 * @return string The pathname of the replacement.
253 *
254 * @throws \Exception if the replacement file could not be created.
255 */
256 function get_accessible_file($path, $suffix=null)
257 {
258 return Helpers::get_accessible_file($path, $suffix);
259 }
260
261 /**
262 * Formats the given string by replacing placeholders with the given values.
263 *
264 * Note: Calls to this function are forwarded to {@link Helpers::format()}.
265 *
266 * @param string $str The string to format.
267 * @param array $args An array of replacement for the placeholders. Occurrences in `$str` of any
268 * key in `$args` are replaced with the corresponding sanitized value. The sanitization function
269 * depends on the first character of the key:
270 *
271 * - `:key`: Replace as is. Use this for text that has already been sanitized.
272 * - `!key`: Sanitize using the {@link escape()} function.
273 * - `%key`: Sanitize using the {@link escape()} function and wrap inside an `EM` markup.
274 *
275 * Numeric indexes can also be used e.g `\2` or `{2}` are replaced by the value of the index
276 * 2.
277 *
278 * @return string
279 */
280 function format($str, array $args=array())
281 {
282 return Helpers::format($str, $args);
283 }
284
285 /**
286 * Formats a number into a size with unit (o, Ko, Mo, Go).
287 *
288 * Before the string is formatted it is localised with the {@link t()} function.
289 *
290 * Note: Calls to this function are forwarded to {@link Helpers::format_size()}.
291 *
292 * @param int $size
293 *
294 * @return string
295 */
296 function format_size($size)
297 {
298 return Helpers::format_size($size);
299 }
300
301 /**
302 * Normalizes the input provided and returns the normalized string.
303 *
304 * Note: Calls to this function are forwarded to {@link Helpers::normalize()}.
305 *
306 * @param string $str The string to normalize.
307 * @param string $separator Whitespaces replacement.
308 * @param string $charset The charset of the input string, defaults to {@link Brickrouge\CHARSET}
309 * (utf-8).
310 *
311 * @return string
312 */
313 function normalize($str, $separator='-', $charset=CHARSET)
314 {
315 return Helpers::normalize($str, $separator, $charset);
316 }
317
318 /**
319 * Translates a string to the current language or to a given language.
320 *
321 * The native string language is supposed to be english (en).
322 *
323 * Note: Calls to this function are forwarded to {@link Helpers::t}.
324 *
325 * @param string $str The native string to translate.
326 * @param array $args An array of replacements to make after the translation. The replacement is
327 * handled by the {@link format()} function.
328 * @param array $options An array of additional options, with the following elements:
329 * - 'default': The default string to use if the translation failed.
330 * - 'scope': The scope of the translation.
331 *
332 * @return mixed
333 *
334 * @see ICanBoogie\I18n\Translator
335 */
336 function t($str, array $args=array(), array $options=array())
337 {
338 return Helpers::t($str, $args, $options);
339 }
340
341 /**
342 * Returns the global document object.
343 *
344 * This document is used by classes when they need to add assets. Once assets are collected one can
345 * simple echo the assets while building the response HTML.
346 *
347 * Example:
348 *
349 * <?php
350 *
351 * namespace Brickrouge;
352 *
353 * $document = get_document();
354 * $document->css->add(Brickrouge\ASSETS . 'brickrouge.css');
355 * $document->js->add(Brickrouge\ASSETS . 'brickrouge.js');
356 *
357 * ?><!DOCTYPE html>
358 * <html>
359 * <head>
360 * <?php echo $document->css ?>
361 * </head>
362 * <body>
363 * <?php echo $document->js ?>
364 * </body>
365 * </html>
366 *
367 * Note: Calls to this function are forwarded to {@link Helpers::get_document()}.
368 *
369 * @return Document
370 */
371 function get_document()
372 {
373 return Helpers::get_document();
374 }
375
376 /**
377 * Checks if the session is started, and start it otherwise.
378 *
379 * The session is used by the {@link Form} class to store validation errors and store
380 * its forms for later validation. Take a look at the {@link Form::validate()} and
381 * {@link Form::save()} methods.
382 *
383 * Note: Calls to this function are forwarded to {@link Helpers::check_session()}.
384 */
385 function check_session()
386 {
387 Helpers::check_session();
388 }
389
390 /**
391 * Stores of form for later validation.
392 *
393 * Note: Calls to this function are forwarded to {@link Helpers::store_form()}.
394 *
395 * @param Form $form The form to store.
396 *
397 * @return string A key that must be used to retrieve the form.
398 */
399 function store_form(Form $form)
400 {
401 return Helpers::store_form($form);
402 }
403
404 /**
405 * Retrieve a stored form.
406 *
407 * Note: Calls to this function are forwarded to {@link Helpers::retrieve_form()}.
408 *
409 * @param string $key Key of the form to retrieve.
410 *
411 * @return Form|null The retrieved form or null if none where found for the specified key.
412 */
413 function retrieve_form($key)
414 {
415 return Helpers::retrieve_form($key);
416 }
417
418 /**
419 * Stores the validation errors of a form.
420 *
421 * Note: Calls to this function are forwarded to {@link Helpers::store_form_errors()}.
422 *
423 * @param string $name The name of the form.
424 * @param array $errors The validation errors of the form.
425 */
426 function store_form_errors($name, $errors)
427 {
428 Helpers::store_form_errors($name, $errors);
429 }
430
431 /**
432 * Retrieves the validation errors of a form.
433 *
434 * Note: Calls to this function are forwarded to {@link Helpers::retrieve_form_errors()}.
435 *
436 * @param string $name The name if the form.
437 *
438 * @return array
439 */
440 function retrieve_form_errors($name)
441 {
442 return Helpers::retrieve_form_errors($name);
443 }
444
445 /**
446 * Renders an exception into a string.
447 *
448 * Note: Calls to this function are forwarded to {@link Helpers::render_exception()}.
449 *
450 * @param \Exception $exception
451 *
452 * @return string
453 */
454 function render_exception(\Exception $exception)
455 {
456 return Helpers::render_exception($exception);
457 }
458
459 /**
460 * Brickrouge helpers.
461 *
462 * The following helpers are patchable:
463 *
464 * - {@link check_session()}
465 * - {@link format()}
466 * - {@link format_size()}
467 * - {@link get_accessible_file()}
468 * - {@link get_document()}
469 * - {@link normalize()}
470 * - {@link render_exception()}
471 * - {@link retrieve_form()}
472 * - {@link retrieve_form_errors()}
473 * - {@link store_form()}
474 * - {@link store_form_errors()}
475 * - {@link t()}
476 *
477 * @method void check_session() check_session()
478 * @method string format() format(string $str, array $args=array())
479 * @method string format_size() format_size(number $size)
480 * @method string get_accessible_file() get_accessible_file(string $path, $suffix=null)
481 * @method Document get_document() get_document()
482 * @method string normalize() normalize(string $str)
483 * @method string render_exception() render_exception(\Exception $exception)
484 * @method Form retrieve_form() retrieve_form(string $name)
485 * @method \ICanboogie\Errors retrieve_form_errors() retrieve_form_errors(string $name)
486 * @method string store_form() store_form(Form $form)
487 * @method void store_form_errors() store_form_errors(string $name, \ICanBoogie\Errors $errors)
488 * @method string t() t(string $str, array $args=array(), array $options=array())
489 */
490 class Helpers
491 {
492 static private $jumptable = array
493 (
494 'check_session' => array(__CLASS__, 'check_session'),
495 'format' => array(__CLASS__, 'format'),
496 'format_size' => array(__CLASS__, 'format_size'),
497 'get_accessible_file' => array(__CLASS__, 'get_accessible_file'),
498 'get_document' => array(__CLASS__, 'get_document'),
499 'normalize' => array(__CLASS__, 'normalize'),
500 'render_exception' => array(__CLASS__, 'render_exception'),
501 'retrieve_form' => array(__CLASS__, 'retrieve_form'),
502 'retrieve_form_errors' => array(__CLASS__, 'retrieve_form_errors'),
503 'store_form' => array(__CLASS__, 'store_form'),
504 'store_form_errors' => array(__CLASS__, 'store_form_errors'),
505 't' => array(__CLASS__, 't')
506 );
507
508 /**
509 * Calls the callback of a patchable function.
510 *
511 * @param string $name Name of the function.
512 * @param array $arguments Arguments.
513 *
514 * @return mixed
515 */
516 static public function __callstatic($name, array $arguments)
517 {
518 return call_user_func_array(self::$jumptable[$name], $arguments);
519 }
520
521 /**
522 * Patches a patchable function.
523 *
524 * @param string $name Name of the function.
525 * @param collable $callback Callback.
526 *
527 * @throws \RuntimeException is attempt to patch an undefined function.
528 */
529 static public function patch($name, $callback)
530 {
531 if (empty(self::$jumptable[$name]))
532 {
533 throw new \RuntimeException("Undefined patchable: $name.");
534 }
535
536 self::$jumptable[$name] = $callback;
537 }
538
539 /*
540 * fallbacks
541 */
542
543 /**
544 * This method is the fallback for the {@link format()} function.
545 */
546 static private function format($str, array $args=array())
547 {
548 if (!$args)
549 {
550 return $str;
551 }
552
553 $holders = array();
554 $i = 0;
555
556 foreach ($args as $key => $value)
557 {
558 ++$i;
559
560 if (is_array($value) || is_object($value))
561 {
562 $value = dump($value);
563 }
564 else if (is_bool($value))
565 {
566 $value = $value ? '<em>true</em>' : '<em>false</em>';
567 }
568 else if (is_null($value))
569 {
570 $value = '<em>null</em>';
571 }
572
573 if (is_string($key))
574 {
575 switch ($key{0})
576 {
577 case ':': break;
578 case '!': $value = escape($value); break;
579 case '%': $value = '<q>' . escape($value) . '</q>'; break;
580
581 default:
582 {
583 $escaped_value = escape($value);
584
585 $holders['!' . $key] = $escaped_value;
586 $holders['%' . $key] = '<q>' . $escaped_value . '</q>';
587
588 $key = ':' . $key;
589 }
590 }
591 }
592 else if (is_numeric($key))
593 {
594 $key = '\\' . $i;
595 $holders['{' . $i . '}'] = $value;
596 }
597
598 $holders[$key] = $value;
599 }
600
601 return strtr($str, $holders);
602 }
603
604 /**
605 * This method is the fallback for the {@link format_size()} function.
606 */
607 static private function format_size($size)
608 {
609 if ($size < 1024)
610 {
611 $str = ":size\xC2\xA0b";
612 }
613 else if ($size < 1024 * 1024)
614 {
615 $str = ":size\xC2\xA0Kb";
616 $size = $size / 1024;
617 }
618 else if ($size < 1024 * 1024 * 1024)
619 {
620 $str = ":size\xC2\xA0Mb";
621 $size = $size / (1024 * 1024);
622 }
623 else
624 {
625 $str = ":size\xC2\xA0Gb";
626 $size = $size / (1024 * 1024 * 1024);
627 }
628
629 return t($str, array(':size' => round($size)));
630 }
631
632 /**
633 * This method is the fallback for the {@link normalize()} function.
634 */
635 static private function normalize($str, $separator='-', $charset=CHARSET)
636 {
637 $str = str_replace('\'', '', $str);
638
639 $str = htmlentities($str, ENT_NOQUOTES, $charset);
640 $str = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $str);
641 $str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str); // pour les ligatures e.g. 'œ'
642 $str = preg_replace('#&[^;]+;#', '', $str); // supprime les autres caractères
643
644 $str = strtolower($str);
645 $str = preg_replace('#[^a-z0-9]+#', $separator, $str);
646 $str = trim($str, $separator);
647
648 return $str;
649 }
650
651 /**
652 * This method is the fallback for the {@link t()} function.
653 *
654 * We usually rely on the ICanBoogie framework I18n features to translate our string, if it is
655 * not available we simply format the string using the {@link Brickrouge\format()} function.
656 */
657 static private function t($str, array $args=array(), array $options=array())
658 {
659 return format($str, $args);
660 }
661
662 /**
663 * This method is the fallback for the {@link get_document()} function.
664 */
665 static private function get_document()
666 {
667 if (self::$document === null)
668 {
669 self::$document = new Document();
670 }
671
672 return self::$document;
673 }
674
675 private static $document;
676
677 /**
678 * This method is the fallback for the {@link check_session()} function.
679 */
680 static private function check_session()
681 {
682 if (session_id())
683 {
684 return;
685 }
686
687 session_start();
688 }
689
690 const STORE_KEY = 'brickrouge.stored_forms';
691 const STORE_MAX = 10;
692
693 /**
694 * Fallback for the {@link store_form()} function.
695 *
696 * The form is saved in the session in the STORE_KEY array.
697 *
698 * @param Form $form
699 *
700 * @return string The key to use to retrieve the form.
701 */
702 static private function store_form(Form $form)
703 {
704 check_session();
705
706 #
707 # before we store anything, we do some cleanup. in order to avoid sessions filled with
708 # used forms. We only maintain a few. The limit is set using the STORE_MAX constant.
709 #
710
711 if (isset($_SESSION[self::STORE_KEY]))
712 {
713 $n = count($_SESSION[self::STORE_KEY]);
714
715 if ($n > self::STORE_MAX)
716 {
717 $_SESSION[self::STORE_KEY] = array_slice($_SESSION[self::STORE_KEY], $n - self::STORE_MAX);
718 }
719 }
720
721 $key = md5(uniqid(mt_rand(), true));
722
723 $_SESSION[self::STORE_KEY][$key] = serialize($form);
724
725 return $key;
726 }
727
728 /**
729 * Fallback for the {@link retrieve_form()} function.
730 *
731 * @param string $key
732 *
733 * @return void|Form The retrieved form or null if the key matched none.
734 */
735 static private function retrieve_form($key)
736 {
737 check_session();
738
739 if (empty($_SESSION[self::STORE_KEY][$key]))
740 {
741 return;
742 }
743
744 $form = unserialize($_SESSION[self::STORE_KEY][$key]);
745
746 unset($_SESSION[self::STORE_KEY][$key]);
747
748 return $form;
749 }
750
751 static private $errors;
752
753 /**
754 * This method is the fallback for the {@link store_form_errors()} function.
755 */
756 static private function store_form_errors($name, $errors)
757 {
758 self::$errors[$name] = $errors;
759 }
760
761 /**
762 * This method is the fallback for the {@link retrieve_form_errors()} function.
763 */
764 static private function retrieve_form_errors($name)
765 {
766 return isset(self::$errors[$name]) ? self::$errors[$name] : array();
767 }
768
769 /**
770 * This method is the fallback for the {@link render_exception()} function.
771 *
772 * @param \Exception $exception
773 *
774 * @return string
775 */
776 static private function render_exception(\Exception $exception)
777 {
778 return (string) $exception;
779 }
780
781 /**
782 * This method is the fallback for the {@link get_accessible_file()} function.
783 *
784 * @param string $path Absolute path to the web inaccessible file.
785 * @param string $suffix Optional suffix for the web accessible filename.
786 *
787 * @return string The pathname of the replacement.
788 *
789 * @throws \Exception if the replacement file could not be created.
790 */
791 static private function get_accessible_file($path, $suffix=null)
792 {
793 $key = sprintf('%s-%04x%s.%s', md5($path), strlen($path), ($suffix ? '-' . $suffix : ''), pathinfo($path, PATHINFO_EXTENSION));
794 $replacement_path = ACCESSIBLE_ASSETS;
795 $replacement = $replacement_path . $key;
796
797 if (!is_writable($replacement_path))
798 {
799 throw new \Exception(format('Unable to make the file %path web accessible, the destination directory %replacement_path is not writtable.', array('path' => $path, 'replacement_path' => $replacement_path)));
800 }
801
802 if (!file_exists($replacement) || filemtime($path) > filemtime($replacement))
803 {
804 file_put_contents($replacement, file_get_contents($path));
805 }
806
807 return $replacement;
808 }
809 }