PluginProbe
Postie / trunk
Postie vtrunk
1.9.80 1.9.79 1.9.78 1.9.77 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.10 1.7.11 1.7.12 1.7.13 1.7.14 1.7.15 1.7.16 1.7.17 1.7.18 1.7.2 1.7.20 All 239 releases
postie / lib / fException.php

fException.php in Postie trunk, at lib/fException.php

551 lines 22.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * An exception that allows for easy l10n, printing, tracing and hooking
5 *
6 * @copyright Copyright (c) 2007-2009 Will Bond
7 * @author Will Bond [wb] <will@flourishlib.com>
8 * @license http://flourishlib.com/license
9 *
10 * @package Flourish
11 * @link http://flourishlib.com/fException
12 *
13 * @version 1.0.0b8
14 * @changes 1.0.0b8 Added a missing line of backtrace to ::formatTrace() [wb, 2009-06-28]
15 * @changes 1.0.0b7 Updated ::__construct() to no longer require a message, like the Exception class, and allow for non-integer codes [wb, 2009-06-26]
16 * @changes 1.0.0b6 Fixed ::splitMessage() so that the original message is returned if no list items are found, added ::reorderMessage() [wb, 2009-06-02]
17 * @changes 1.0.0b5 Added ::splitMessage() to replace fCRUD::removeListItems() and fCRUD::reorderListItems() [wb, 2009-05-08]
18 * @changes 1.0.0b4 Added a check to ::__construct() to ensure that the `$code` parameter is numeric [wb, 2009-05-04]
19 * @changes 1.0.0b3 Fixed a bug with ::printMessage() messing up some HTML messages [wb, 2009-03-27]
20 * @changes 1.0.0b2 ::compose() more robustly handles `$components` passed as an array, ::__construct() now detects stray `%` characters [wb, 2009-02-05]
21 * @changes 1.0.0b The initial implementation [wb, 2007-06-14]
22 */
23 abstract class fException extends Exception {
24
25 /**
26 * Callbacks for when exceptions are created
27 *
28 * @var array
29 */
30 static private $callbacks = array();
31
32 /**
33 * Composes text using fText if loaded
34 *
35 * @param string $message The message to compose
36 * @param mixed $component A string or number to insert into the message
37 * @param mixed ...
38 * @return string The composed and possible translated message
39 */
40 static protected function compose($message) {
41 $components = array_slice(func_get_args(), 1);
42
43 // Handles components passed as an array
44 if (sizeof($components) == 1 && is_array($components[0])) {
45 $components = $components[0];
46 }
47
48 // If fText is loaded, use it
49 if (class_exists('fText', FALSE)) {
50 return call_user_func_array(
51 array('fText', 'compose'), array($message, $components)
52 );
53 } else {
54 return vsprintf($message, $components);
55 }
56 }
57
58 /**
59 * Creates a string representation of any variable using predefined strings for booleans, `NULL` and empty strings
60 *
61 * The string output format of this method is very similar to the output of
62 * [http://php.net/print_r print_r()] except that the following values
63 * are represented as special strings:
64 *
65 * - `TRUE`: `'{true}'`
66 * - `FALSE`: `'{false}'`
67 * - `NULL`: `'{null}'`
68 * - `''`: `'{empty_string}'`
69 *
70 * @param mixed $data The value to dump
71 * @return string The string representation of the value
72 */
73 static protected function dump($data) {
74 if (is_bool($data)) {
75 return ($data) ? '{true}' : '{false}';
76 } elseif (is_null($data)) {
77 return '{null}';
78 } elseif ($data === '') {
79 return '{empty_string}';
80 } elseif (is_array($data) || is_object($data)) {
81
82 ob_start();
83 var_dump($data);
84 $output = ob_get_contents();
85 ob_end_clean();
86
87 // Make the var dump more like a print_r
88 $output = preg_replace('#=>\n( )+(?=[a-zA-Z]|&)#m', ' => ', $output);
89 $output = str_replace('string(0) ""', '{empty_string}', $output);
90 $output = preg_replace('#=> (&)?NULL#', '=> \1{null}', $output);
91 $output = preg_replace('#=> (&)?bool\((false|true)\)#', '=> \1{\2}', $output);
92 $output = preg_replace('#string\(\d+\) "#', '', $output);
93 $output = preg_replace('#"(\n( )*)(?=\[|\})#', '\1', $output);
94 $output = preg_replace('#(?:float|int)\((-?\d+(?:.\d+)?)\)#', '\1', $output);
95 $output = preg_replace('#((?: )+)\["(.*?)"\]#', '\1[\2]', $output);
96 $output = preg_replace('#(?:&)?array\(\d+\) \{\n((?: )*)((?: )(?=\[)|(?=\}))#', "Array\n\\1(\n\\1\\2", $output);
97 $output = preg_replace('/object\((\w+)\)#\d+ \(\d+\) {\n((?: )*)((?: )(?=\[)|(?=\}))/', "\\1 Object\n\\2(\n\\2\\3", $output);
98 $output = preg_replace('#^((?: )+)}(?=\n|$)#m', "\\1)\n", $output);
99 $output = substr($output, 0, -2) . ')';
100
101 // Fix indenting issues with the var dump output
102 $output_lines = explode("\n", $output);
103 $new_output = array();
104 $stack = 0;
105 foreach ($output_lines as $line) {
106 if (preg_match('#^((?: )*)([^ ])#', $line, $match)) {
107 $spaces = strlen($match[1]);
108 if ($spaces && $match[2] == '(') {
109 $stack += 1;
110 }
111 $new_output[] = str_pad('', ($spaces) + (4 * $stack)) . $line;
112 if ($spaces && $match[2] == ')') {
113 $stack -= 1;
114 }
115 } else {
116 $new_output[] = str_pad('', ($spaces) + (4 * $stack)) . $line;
117 }
118 }
119
120 return join("\n", $new_output);
121 } else {
122 return (string) $data;
123 }
124 }
125
126 /**
127 * Adds a callback for when certain types of exceptions are created
128 *
129 * The callback will be called when any exception of this class, or any
130 * child class, specified is tossed. A single parameter will be passed
131 * to the callback, which will be the exception object.
132 *
133 * @param callback $callback The callback
134 * @param string $exception_type The type of exception to call the callback for
135 * @return void
136 */
137 static public function registerCallback($callback, $exception_type = NULL) {
138 if ($exception_type === NULL) {
139 $exception_type = 'fException';
140 }
141
142 if (!isset(self::$callbacks[$exception_type])) {
143 self::$callbacks[$exception_type] = array();
144 }
145
146 if (is_string($callback) && strpos($callback, '::') !== FALSE) {
147 $callback = explode('::', $callback);
148 }
149
150 self::$callbacks[$exception_type][] = $callback;
151 }
152
153 /**
154 * Compares the message matching strings by longest first so that the longest matches are made first
155 *
156 * @param string $a The first string to compare
157 * @param string $b The second string to compare
158 * @return integer `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
159 */
160 static private function sortMatchingArray($a, $b) {
161 return -1 * strnatcmp(strlen($a), strlen($b));
162 }
163
164 /**
165 * Sets the message for the exception, allowing for string interpolation and internationalization
166 *
167 * The `$message` can contain any number of formatting placeholders for
168 * string and number interpolation via [http://php.net/sprintf `sprintf()`].
169 * Any `%` signs that do not appear to be part of a valid formatting
170 * placeholder will be automatically escaped with a second `%`.
171 *
172 * The following aspects of valid `sprintf()` formatting codes are not
173 * accepted since they are redundant and restrict the non-formatting use of
174 * the `%` sign in exception messages:
175 * - `% 2d`: Using a literal space as a padding character - a space will be used if no padding character is specified
176 * - `%'.d`: Providing a padding character but no width - no padding will be applied without a width
177 *
178 * @param string $message The message for the exception. This accepts a subset of [http://php.net/sprintf `sprintf()`] strings - see method description for more details.
179 * @param mixed $component A string or number to insert into the message
180 * @param mixed ...
181 * @param mixed $code The exception code to set
182 * @return fException
183 */
184 public function __construct($message = '') {
185 $args = array_slice(func_get_args(), 1);
186 $required_args = preg_match_all(
187 '/
188 (?<!%) # Ensure this is not an escaped %
189 %( # The leading %
190 (?:\d+\$)? # Position
191 \+? # Sign specifier
192 (?:(?:0|\'.)?-?\d+|-?) # Padding, alignment and width or just alignment
193 (?:\.\d+)? # Precision
194 [bcdeufFosxX] # Type
195 )/x', $message, $matches
196 );
197
198 // Handle %s that weren't properly escaped
199 $formats = $matches[1];
200 $delimeters = ($formats) ? array_fill(0, sizeof($formats), '#') : array();
201 $lookahead = join(
202 '|', array_map(
203 'preg_quote', $formats, $delimeters
204 )
205 );
206 $lookahead = ($lookahead) ? '|' . $lookahead : '';
207 $message = preg_replace('#(?<!%)%(?!%' . $lookahead . ')#', '%%', $message);
208
209 // If we have an extra argument, it is the exception code
210 $code = NULL;
211 if ($required_args == sizeof($args) - 1) {
212 $code = array_pop($args);
213 }
214
215 if (sizeof($args) != $required_args) {
216 $message = self::compose(
217 '%1$d components were passed to the %2$s constructor, while %3$d were specified in the message', sizeof($args), get_class($this), $required_args
218 );
219 throw new Exception($message);
220 }
221
222 $args = array_map(array('fException', 'dump'), $args);
223
224 parent::__construct(self::compose($message, $args));
225 $this->code = $code;
226
227 foreach (self::$callbacks as $class => $callbacks) {
228 foreach ($callbacks as $callback) {
229 if ($this instanceof $class) {
230 call_user_func($callback, $this);
231 }
232 }
233 }
234 }
235
236 /**
237 * All requests that hit this method should be requests for callbacks
238 *
239 * @internal
240 *
241 * @param string $method The method to create a callback for
242 * @return callback The callback for the method requested
243 */
244 public function __get($method) {
245 return array($this, $method);
246 }
247
248 /**
249 * Gets the backtrace to currently called exception
250 *
251 * @return string A nicely formatted backtrace to this exception
252 */
253 public function formatTrace() {
254 $doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
255 $doc_root .= (substr($doc_root, -1) != DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
256
257 $backtrace = explode("\n", $this->getTraceAsString());
258 array_unshift($backtrace, $this->file . '(' . $this->line . ')');
259 $backtrace = preg_replace('/^#\d+\s+/', '', $backtrace);
260 $backtrace = str_replace($doc_root, '{doc_root}' . DIRECTORY_SEPARATOR, $backtrace);
261 $backtrace = array_diff($backtrace, array('{main}'));
262 $backtrace = array_reverse($backtrace);
263
264 return join("\n", $backtrace);
265 }
266
267 /**
268 * Returns the CSS class name for printing information about the exception
269 *
270 * @return void
271 */
272 protected function getCSSClass() {
273 $string = preg_replace('#^f#', '', get_class($this));
274
275 do {
276 $old_string = $string;
277 $string = preg_replace('/([a-zA-Z])([0-9])/', '\1_\2', $string);
278 $string = preg_replace('/([a-z0-9A-Z])([A-Z])/', '\1_\2', $string);
279 } while ($old_string != $string);
280
281 return strtolower($string);
282 }
283
284 /**
285 * Prepares content for output into HTML
286 *
287 * @return string The prepared content
288 */
289 protected function prepare($content) {
290 // See if the message has newline characters but not br tags, extracted from fHTML to reduce dependencies
291 static $inline_tags_minus_br = '<a><abbr><acronym><b><big><button><cite><code><del><dfn><em><font><i><img><input><ins><kbd><label><q><s><samp><select><small><span><strike><strong><sub><sup><textarea><tt><u><var>';
292 $content_with_newlines = (strip_tags($content, $inline_tags_minus_br)) ? $content : nl2br($content);
293
294 // Check to see if we have any block-level html, extracted from fHTML to reduce dependencies
295 $inline_tags = $inline_tags_minus_br . '<br>';
296 $no_block_html = strip_tags($content, $inline_tags) == $content;
297
298 // This code ensures the output is properly encoded for display in (X)HTML, extracted from fHTML to reduce dependencies
299 $reg_exp = "/<\s*\/?\s*[\w:]+(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*\/?\s*>|&(?:#\d+|\w+);|<\!--.*?-->/";
300 preg_match_all($reg_exp, $content, $html_matches, PREG_SET_ORDER);
301 $text_matches = preg_split($reg_exp, $content_with_newlines);
302
303 foreach ($text_matches as $key => $value) {
304 $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
305 }
306
307 for ($i = 0; $i < sizeof($html_matches); $i++) {
308 $text_matches[$i] .= $html_matches[$i][0];
309 }
310
311 $content_with_newlines = implode('', $text_matches);
312
313 $output = ($no_block_html) ? '<p>' : '';
314 $output .= $content_with_newlines;
315 $output .= ($no_block_html) ? '</p>' : '';
316
317 return $output;
318 }
319
320 /**
321 * Prints the message inside of a div with the class being 'exception %THIS_EXCEPTION_CLASS_NAME%'
322 *
323 * @return void
324 */
325 public function printMessage() {
326 echo '<div class="exception ' . $this->getCSSClass() . '">';
327 echo $this->prepare($this->message);
328 echo '</div>';
329 }
330
331 /**
332 * Prints the backtrace to currently called exception inside of a pre tag with the class being 'exception %THIS_EXCEPTION_CLASS_NAME% trace'
333 *
334 * @return void
335 */
336 public function printTrace() {
337 echo '<pre class="exception ' . $this->getCSSClass() . ' trace">';
338 echo $this->formatTrace();
339 echo '</pre>';
340 }
341
342 /**
343 * Reorders list items in the message based on simple string matching
344 *
345 * @param string $match This should be a string to match to one of the list items - whatever the order this is in the parameter list will be the order of the list item in the adjusted message
346 * @param string ...
347 * @return fException The exception object, to allow for method chaining
348 */
349 public function reorderMessage($match) {
350 // If we can't find a list, don't bother continuing
351 if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $message_parts)) {
352 return $this;
353 }
354
355 $matching_array = func_get_args();
356 // This ensures that we match on the longest string first
357 uasort($matching_array, array('self', 'sortMatchingArray'));
358
359 $beginning = $message_parts[1];
360 $list_contents = $message_parts[2];
361 $ending = $message_parts[3];
362
363 preg_match_all('#<li(.*?)</li>#i', $list_contents, $list_items, PREG_SET_ORDER);
364
365 $ordered_items = array_fill(0, sizeof($matching_array), array());
366 $other_items = array();
367
368 foreach ($list_items as $list_item) {
369 foreach ($matching_array as $num => $match_string) {
370 if (strpos($list_item[1], strval($match_string)) !== FALSE) {
371 $ordered_items[$num][] = $list_item[0];
372 continue 2;
373 }
374 }
375
376 $other_items[] = $list_item[0];
377 }
378
379 $final_list = array();
380 foreach ($ordered_items as $ordered_item) {
381 $final_list = array_merge($final_list, $ordered_item);
382 }
383 $final_list = array_merge($final_list, $other_items);
384
385 $this->message = $beginning . join("\n", $final_list) . $ending;
386
387 return $this;
388 }
389
390 /**
391 * Allows the message to be overwriten
392 *
393 * @param string $new_message The new message for the exception
394 * @return void
395 */
396 public function setMessage($new_message) {
397 $this->message = $new_message;
398 }
399
400 /**
401 * Splits an exception with an HTML list into multiple strings each containing part of the original message
402 *
403 * This method should be called with two or more parameters of arrays of
404 * string to match. If any of the provided strings are matching in a list
405 * item in the exception message, a new copy of the message will be created
406 * containing just the matching list items.
407 *
408 * Here is an exception message to be split:
409 *
410 * {{{
411 * #!html
412 * <p>The following problems were found:</p>
413 * <ul>
414 * <li>First Name: Please enter a value</li>
415 * <li>Last Name: Please enter a value</li>
416 * <li>Email: Please enter a value</li>
417 * <li>Address: Please enter a value</li>
418 * <li>City: Please enter a value</li>
419 * <li>State: Please enter a value</li>
420 * <li>Zip Code: Please enter a value</li>
421 * </ul>
422 * }}}
423 *
424 * The following PHP would split the exception into two messages:
425 *
426 * {{{
427 * #!php
428 * list ($name_exception, $address_exception) = $exception->splitMessage(
429 * array('First Name', 'Last Name', 'Email'),
430 * array('Address', 'City', 'State', 'Zip Code')
431 * );
432 * }}}
433 *
434 * The resulting messages would be:
435 *
436 * {{{
437 * #!html
438 * <p>The following problems were found:</p>
439 * <ul>
440 * <li>First Name: Please enter a value</li>
441 * <li>Last Name: Please enter a value</li>
442 * <li>Email: Please enter a value</li>
443 * </ul>
444 * }}}
445 *
446 * and
447 *
448 * {{{
449 * #!html
450 * <p>The following problems were found:</p>
451 * <ul>
452 * <li>Address: Please enter a value</li>
453 * <li>City: Please enter a value</li>
454 * <li>State: Please enter a value</li>
455 * <li>Zip Code: Please enter a value</li>
456 * </ul>
457 * }}}
458 *
459 * If no list items match the strings in a parameter, the result will be
460 * an empty string, allowing for simple display:
461 *
462 * {{{
463 * #!php
464 * fHTML::show($name_exception, 'error');
465 * }}}
466 *
467 * An empty string is returned when none of the list items matched the
468 * strings in the parameter. If no list items are found, the first value in
469 * the returned array will be the existing message and all other array
470 * values will be an empty string.
471 *
472 * @param array $list_item_matches An array of strings to filter the list items by, list items will be ordered in the same order as this array
473 * @param array ...
474 * @return array This will contain an array of strings corresponding to the parameters passed - see method description for details
475 */
476 public function splitMessage($list_item_matches) {
477 $class = get_class($this);
478
479 $matching_arrays = func_get_args();
480
481 if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $matches)) {
482 return array_merge(array($this->message), array_fill(0, sizeof($matching_arrays) - 1, ''));
483 }
484
485 $beginning_html = $matches[1];
486 $list_items_html = $matches[2];
487 $ending_html = $matches[3];
488
489 preg_match_all('#<li(.*?)</li>#i', $list_items_html, $list_items, PREG_SET_ORDER);
490
491 $output = array();
492
493 foreach ($matching_arrays as $matching_array) {
494
495 // This ensures that we match on the longest string first
496 uasort($matching_array, array('self', 'sortMatchingArray'));
497
498 // We may match more than one list item per matching string, so we need a multi-dimensional array to hold them
499 $matched_list_items = array_fill(0, sizeof($matching_array), array());
500 $found = FALSE;
501
502 foreach ($list_items as $list_item) {
503 foreach ($matching_array as $match_num => $matching_string) {
504 if (strpos($list_item[1], strval($matching_string)) !== FALSE) {
505 $matched_list_items[$match_num][] = $list_item[0];
506 $found = TRUE;
507 continue 2;
508 }
509 }
510 }
511
512 if (!$found) {
513 $output[] = '';
514 continue;
515 }
516
517 // This merges all of the multi-dimensional arrays back to one so we can do a simple join
518 $merged_list_items = array();
519 foreach ($matched_list_items as $match_num => $matched_items) {
520 $merged_list_items = array_merge($merged_list_items, $matched_items);
521 }
522
523 $output[] = $beginning_html . join("\n", $merged_list_items) . $ending_html;
524 }
525
526 return $output;
527 }
528
529 }
530
531 /**
532 * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
533 *
534 * Permission is hereby granted, free of charge, to any person obtaining a copy
535 * of this software and associated documentation files (the "Software"), to deal
536 * in the Software without restriction, including without limitation the rights
537 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
538 * copies of the Software, and to permit persons to whom the Software is
539 * furnished to do so, subject to the following conditions:
540 *
541 * The above copyright notice and this permission notice shall be included in
542 * all copies or substantial portions of the Software.
543 *
544 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
545 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
546 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
547 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
548 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
549 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
550 * THE SOFTWARE.
551 */