Completed
Push — developer ( 42f535...e7ef61 )
by Błażej
348:28 queued 312:32
created
libraries/restler/restler.php 1 patch
Indentation   +1293 added lines, -1293 removed lines patch added patch discarded remove patch
@@ -16,877 +16,877 @@  discard block
 block discarded – undo
16 16
  */
17 17
 class Restler
18 18
 {
19
-    // ==================================================================
20
-    //
21
-    // Public variables
22
-    //
23
-    // ------------------------------------------------------------------
24
-
25
-    const VERSION = '2.2.1';
26
-
27
-    /**
28
-     * URL of the currently mapped service
29
-     * @var string
30
-     */
31
-    public $url;
32
-
33
-    /**
34
-     * Http request method of the current request.
35
-     * Any value between [GET, PUT, POST, DELETE]
36
-     * @var string
37
-     */
38
-    public $request_method;
39
-
40
-    /**
41
-     * Requested data format. Instance of the current format class
42
-     * which implements the iFormat interface
43
-     * @var iFormat
44
-     * @example jsonFormat, xmlFormat, yamlFormat etc
45
-     */
46
-    public $request_format;
47
-
48
-    /**
49
-     * Data sent to the service
50
-     * @var array
51
-     */
52
-    public $request_data = array();
53
-
54
-    /**
55
-     * Used in production mode to store the URL Map to disk
56
-     * @var string
57
-     */
58
-    public $cache_dir;
59
-
60
-    /**
61
-     * base directory to locate format and auth files
62
-     * @var string
63
-     */
64
-    public $base_dir;
65
-
66
-    /**
67
-     * Name of an iRespond implementation class
68
-     * @var string
69
-     */
70
-    public $response = 'DefaultResponse';
71
-
72
-    /**
73
-     * Response data format. Instance of the current format class
74
-     * which implements the iFormat interface
75
-     * @var iFormat
76
-     * @example jsonFormat, xmlFormat, yamlFormat etc
77
-     */
78
-    public $response_format;
79
-
80
-    // ==================================================================
81
-    //
82
-    // Private & Protected variables
83
-    //
84
-    // ------------------------------------------------------------------
85
-
86
-    /**
87
-     * When set to false, it will run in debug mode and parse the
88
-     * class files every time to map it to the URL
89
-     * @var boolean
90
-     */
91
-    protected $production_mode;
92
-
93
-    /**
94
-     * Associated array that maps urls to their respective class and method
95
-     * @var array
96
-     */
97
-    protected $routes = array();
98
-
99
-    /**
100
-     * Associated array that maps formats to their respective format class name
101
-     * @var array
102
-     */
103
-    protected $format_map = array();
104
-
105
-    /**
106
-     * Instance of the current api service class
107
-     * @var object
108
-     */
109
-    protected $service_class_instance;
110
-
111
-    /**
112
-     * Name of the api method being called
113
-     * @var string
114
-     */
115
-    protected $service_method;
116
-
117
-    /**
118
-     * list of authentication classes
119
-     * @var array
120
-     */
121
-    protected $auth_classes = array();
122
-
123
-    /**
124
-     * list of error handling classes
125
-     * @var array
126
-     */
127
-    protected $error_classes = array();
128
-
129
-    /**
130
-     * HTTP status codes
131
-     * @var array
132
-     */
133
-    private $codes = array(
134
-        100 => 'Continue',
135
-        101 => 'Switching Protocols',
136
-        200 => 'OK',
137
-        201 => 'Created',
138
-        202 => 'Accepted',
139
-        203 => 'Non-Authoritative Information',
140
-        204 => 'No Content',
141
-        205 => 'Reset Content',
142
-        206 => 'Partial Content',
143
-        300 => 'Multiple Choices',
144
-        301 => 'Moved Permanently',
145
-        302 => 'Found',
146
-        303 => 'See Other',
147
-        304 => 'Not Modified',
148
-        305 => 'Use Proxy',
149
-        306 => '(Unused)',
150
-        307 => 'Temporary Redirect',
151
-        400 => 'Bad Request',
152
-        401 => 'Unauthorized',
153
-        402 => 'Payment Required',
154
-        403 => 'Forbidden',
155
-        404 => 'Not Found',
156
-        405 => 'Method Not Allowed',
157
-        406 => 'Not Acceptable',
158
-        407 => 'Proxy Authentication Required',
159
-        408 => 'Request Timeout',
160
-        409 => 'Conflict',
161
-        410 => 'Gone',
162
-        411 => 'Length Required',
163
-        412 => 'Precondition Failed',
164
-        413 => 'Request Entity Too Large',
165
-        414 => 'Request-URI Too Long',
166
-        415 => 'Unsupported Media Type',
167
-        416 => 'Requested Range Not Satisfiable',
168
-        417 => 'Expectation Failed',
169
-        500 => 'Internal Server Error',
170
-        501 => 'Not Implemented',
171
-        502 => 'Bad Gateway',
172
-        503 => 'Service Unavailable',
173
-        504 => 'Gateway Timeout',
174
-        505 => 'HTTP Version Not Supported'
175
-    );
176
-
177
-    /**
178
-     * Caching of url map is enabled or not
179
-     * @var boolean
180
-     */
181
-    protected $cached;
182
-
183
-    // ==================================================================
184
-    //
185
-    // Public functions
186
-    //
187
-    // ------------------------------------------------------------------
188
-
189
-
190
-    /**
191
-     * Constructor
192
-     * @param boolean $production_mode When set to false, it will run in
193
-     * debug mode and parse the class files every time to map it to the URL
194
-     */
195
-    public function __construct($production_mode = false)
196
-    {
197
-        $this->production_mode = $production_mode;
198
-        $this->cache_dir = getcwd();
199
-        $this->base_dir = RESTLER_PATH;
200
-    }
201
-
202
-
203
-    /**
204
-     * Store the url map cache if needed
205
-     */
206
-    public function __destruct()
207
-    {
208
-        if ($this->production_mode && !($this->cached)) {
209
-            $this->saveCache();
210
-        }
211
-    }
212
-
213
-
214
-    /**
215
-     * Use it in production mode to refresh the url map cache
216
-     */
217
-    public function refreshCache()
218
-    {
219
-        $this->routes = array();
220
-        $this->cached = false;
221
-    }
222
-
223
-
224
-    /**
225
-     * Call this method and pass all the formats that should be
226
-     * supported by the API. Accepts multiple parameters
227
-     * @param string class name of the format class that implements iFormat
228
-     * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
229
-     */
230
-    public function setSupportedFormats()
231
-    {
232
-        $args = func_get_args();
233
-        $extensions = array();
234
-        foreach ($args as $class_name) {
235
-            if (!is_string($class_name) || !class_exists($class_name)) {
236
-                throw new Exception("$class_name is not a vaild Format Class.");
237
-            }
238
-            $obj = new $class_name;
239
-            if (!($obj instanceof iFormat)) {
240
-                throw new Exception('Invalid format class; must implement '
241
-                    . 'iFormat interface');
242
-            }
243
-            foreach ($obj->getMIMEMap() as $extension => $mime) {
244
-                if (!isset($this->format_map[$extension])) {
245
-                    $this->format_map[$extension] = $class_name;
246
-                }
247
-                $mime = explode(',', $mime);
248
-                if (!is_array($mime)) {
249
-                    $mime = array($mime);
250
-                }
251
-                foreach ($mime as $value) {
252
-                    if (!isset($this->format_map[$value])) {
253
-                        $this->format_map[$value] = $class_name;
254
-                    }
255
-                }
256
-                $extensions[".$extension"] = true;
257
-            }
258
-        }
259
-        $this->format_map['default'] = $args[0];
260
-        $this->format_map['extensions'] = array_keys($extensions);
261
-    }
262
-
263
-
264
-    /**
265
-     * Add api classes throgh this method. All the public methods
266
-     * that do not start with _ (underscore) will be  will be exposed
267
-     * as the public api by default.
268
-     *
269
-     * All the protected methods that do not start with _ (underscore)
270
-     * will exposed as protected api which will require authentication
271
-     * @param string $class name of the service class
272
-     * @param string $basePath optional url prefix for mapping, uses
273
-     * lowercase version of the class name when not specified
274
-     * @throws Exception when supplied with invalid class name
275
-     */
276
-    public function addAPIClass($class_name, $base_path = null)
277
-    {
278
-        if (!class_exists($class_name)) {
279
-            throw new Exception("API class $class_name is missing.");
280
-        }
281
-        $this->loadCache();
282
-        if (!$this->cached) {
283
-            if (is_null($base_path)) {
284
-                $base_path = strtolower($class_name);
285
-                $index = strrpos($class_name, '\\');
286
-                if ($index !== false) {
287
-                    $base_path = substr($base_path, $index + 1);
288
-                }
289
-            } else {
290
-                $base_path = trim($base_path, '/');
291
-            }
292
-            if (strlen($base_path) > 0) {
293
-                $base_path .= '/';
294
-            }
295
-            $this->generateMap($class_name, $base_path);
296
-        }
297
-    }
298
-
299
-
300
-    /**
301
-     * protected methods will need atleast one authentication class to be set
302
-     * in order to allow that method to be executed
303
-     * @param string $class_name of the authentication class
304
-     * @param string $base_path optional url prefix for mapping
305
-     */
306
-    public function addAuthenticationClass($class_name, $base_path = null)
307
-    {
308
-        $this->auth_classes[] = $class_name;
309
-        $this->addAPIClass($class_name, $base_path);
310
-    }
311
-
312
-
313
-    /**
314
-     * Add class for custom error handling
315
-     * @param string $class_name of the error handling class
316
-     */
317
-    public function addErrorClass($class_name)
318
-    {
319
-        $this->error_classes[] = $class_name;
320
-    }
321
-
322
-
323
-    /**
324
-     * Convenience method to respond with an error message
325
-     * @param int $statusCode http error code
326
-     * @param string $errorMessage optional custom error message
327
-     */
328
-    public function handleError($status_code, $error_message = null)
329
-    {
330
-        $method = "handle$status_code";
331
-        $handled = false;
332
-        foreach ($this->error_classes as $class_name) {
333
-            if (method_exists($class_name, $method)) {
334
-                $obj = new $class_name();
335
-                $obj->restler = $this;
336
-                $obj->$method();
337
-                $handled = true;
338
-            }
339
-        }
340
-        if ($handled) {
341
-            return;
342
-        }
343
-        $message = $this->codes[$status_code]
344
-            . (!$error_message ? '' : ': ' . $error_message);
345
-        $this->setStatus($status_code);
346
-        $responder = new $this->response();
347
-        $responder->restler = $this;
348
-        $this->sendData($responder->__formatError($status_code, $message));
349
-    }
350
-
351
-
352
-    /**
353
-     * An initialize function to allow use of the restler error generation 
354
-     * functions for pre-processing and pre-routing of requests.
355
-     */
356
-    public function init()
357
-    {
358
-        if (empty($this->format_map)) {
359
-            $this->setSupportedFormats('JsonFormat');
360
-        }
361
-        $this->url = $this->getPath();
362
-        $this->request_method = $this->getRequestMethod();
363
-        $this->response_format = $this->getResponseFormat();
364
-        $this->request_format = $this->getRequestFormat();
365
-        if (is_null($this->request_format)) {
366
-            $this->request_format = $this->response_format;
367
-        }
368
-        if ($this->request_method == 'PUT' || $this->request_method == 'POST') {
369
-            $this->request_data = $this->getRequestData();
370
-        }
371
-    }
372
-
373
-
374
-    /**
375
-     * Main function for processing the api request
376
-     * and return the response
377
-     * @throws Exception when the api service class is missing
378
-     * @throws RestException to send error response
379
-     */
380
-    public function handle()
381
-    {
382
-        $this->init();
383
-        $o = $this->mapUrlToMethod();
384
-
385
-        if (!isset($o->class_name)) {
386
-            $this->handleError(404);
387
-        } else {
388
-            try {
389
-                if ($o->method_flag) {
390
-                    $auth_method = '__isAuthenticated';
391
-                    if (!count($this->auth_classes)) {
392
-                        throw new RestException(401);
393
-                    }
394
-                    foreach ($this->auth_classes as $auth_class) {
395
-                        $auth_obj = new $auth_class();
396
-                        $auth_obj->restler = $this;
397
-                        $this->applyClassMetadata($auth_class, $auth_obj, $o);
398
-                        if (!method_exists($auth_obj, $auth_method)) {
399
-                            throw new RestException(401, 'Authentication Class '
400
-                                . 'should implement iAuthenticate');
401
-                        } else if (!$auth_obj->$auth_method()) {
402
-                            throw new RestException(401);
403
-                        }
404
-                    }
405
-                }
406
-                $this->applyClassMetadata(get_class($this->request_format),
407
-                    $this->request_format, $o);
408
-                $pre_process = '_' . $this->request_format->getExtension() . '_'
409
-                    . $o->method_name;
410
-                $this->service_method = $o->method_name;
411
-                if ($o->method_flag == 2) {
412
-                    $o = unprotect($o);
413
-                }
414
-                $object = $this->service_class_instance = new $o->class_name();
415
-                $object->restler = $this;
416
-                if (method_exists($o->class_name, $pre_process)) {
417
-                    call_user_func_array(
418
-                        array($object, $pre_process), $o->arguments
419
-                    );
420
-                }
421
-                switch ($o->method_flag) {
422
-                    case 3:
423
-                        $reflection_method = new ReflectionMethod($object,
424
-                                $o->method_name);
425
-                        $reflection_method->setAccessible(true);
426
-                        $result = $reflection_method->invokeArgs($object,
427
-                            $o->arguments);
428
-                        break;
429
-                    case 2:
430
-                    case 1:
431
-                    default:
432
-                        $result = call_user_func_array(array(
433
-                            $object,
434
-                            $o->method_name), $o->arguments
435
-                        );
436
-                        break;
437
-                }
438
-            } catch (RestException $e) {
439
-                $this->handleError($e->getCode(), $e->getMessage());
440
-            }
441
-        }
442
-        $responder = new $this->response();
443
-        $responder->restler = $this;
444
-        $this->applyClassMetadata($this->response, $responder, $o);
445
-        if (isset($result) && $result !== null) {
446
-            $result = $responder->__formatResponse($result);
447
-            $this->sendData($result);
448
-        }
449
-    }
450
-
451
-
452
-    /**
453
-     * Encodes the response in the prefered format
454
-     * and sends back
455
-     * @param $data array php data
456
-     */
457
-    public function sendData($data)
458
-    {
459
-        $data = $this->response_format->encode($data, 
460
-            !($this->production_mode)
461
-        );
462
-        $post_process = '_' . $this->service_method . '_'
463
-            . $this->response_format->getExtension();
464
-        if (isset($this->service_class_instance)
465
-            && method_exists($this->service_class_instance, $post_process)
466
-        ) {
467
-            $data = call_user_func(array($this->service_class_instance,
468
-                $post_process), $data);
469
-        }
470
-        header("Cache-Control: no-cache, must-revalidate");
471
-        header("Expires: 0");
472
-        header('Content-Type: ' . $this->response_format->getMIME());
473
-        //.'; charset=utf-8');
474
-        header("X-Powered-By: Luracast Restler v" . Restler::VERSION);
19
+	// ==================================================================
20
+	//
21
+	// Public variables
22
+	//
23
+	// ------------------------------------------------------------------
24
+
25
+	const VERSION = '2.2.1';
26
+
27
+	/**
28
+	 * URL of the currently mapped service
29
+	 * @var string
30
+	 */
31
+	public $url;
32
+
33
+	/**
34
+	 * Http request method of the current request.
35
+	 * Any value between [GET, PUT, POST, DELETE]
36
+	 * @var string
37
+	 */
38
+	public $request_method;
39
+
40
+	/**
41
+	 * Requested data format. Instance of the current format class
42
+	 * which implements the iFormat interface
43
+	 * @var iFormat
44
+	 * @example jsonFormat, xmlFormat, yamlFormat etc
45
+	 */
46
+	public $request_format;
47
+
48
+	/**
49
+	 * Data sent to the service
50
+	 * @var array
51
+	 */
52
+	public $request_data = array();
53
+
54
+	/**
55
+	 * Used in production mode to store the URL Map to disk
56
+	 * @var string
57
+	 */
58
+	public $cache_dir;
59
+
60
+	/**
61
+	 * base directory to locate format and auth files
62
+	 * @var string
63
+	 */
64
+	public $base_dir;
65
+
66
+	/**
67
+	 * Name of an iRespond implementation class
68
+	 * @var string
69
+	 */
70
+	public $response = 'DefaultResponse';
71
+
72
+	/**
73
+	 * Response data format. Instance of the current format class
74
+	 * which implements the iFormat interface
75
+	 * @var iFormat
76
+	 * @example jsonFormat, xmlFormat, yamlFormat etc
77
+	 */
78
+	public $response_format;
79
+
80
+	// ==================================================================
81
+	//
82
+	// Private & Protected variables
83
+	//
84
+	// ------------------------------------------------------------------
85
+
86
+	/**
87
+	 * When set to false, it will run in debug mode and parse the
88
+	 * class files every time to map it to the URL
89
+	 * @var boolean
90
+	 */
91
+	protected $production_mode;
92
+
93
+	/**
94
+	 * Associated array that maps urls to their respective class and method
95
+	 * @var array
96
+	 */
97
+	protected $routes = array();
98
+
99
+	/**
100
+	 * Associated array that maps formats to their respective format class name
101
+	 * @var array
102
+	 */
103
+	protected $format_map = array();
104
+
105
+	/**
106
+	 * Instance of the current api service class
107
+	 * @var object
108
+	 */
109
+	protected $service_class_instance;
110
+
111
+	/**
112
+	 * Name of the api method being called
113
+	 * @var string
114
+	 */
115
+	protected $service_method;
116
+
117
+	/**
118
+	 * list of authentication classes
119
+	 * @var array
120
+	 */
121
+	protected $auth_classes = array();
122
+
123
+	/**
124
+	 * list of error handling classes
125
+	 * @var array
126
+	 */
127
+	protected $error_classes = array();
128
+
129
+	/**
130
+	 * HTTP status codes
131
+	 * @var array
132
+	 */
133
+	private $codes = array(
134
+		100 => 'Continue',
135
+		101 => 'Switching Protocols',
136
+		200 => 'OK',
137
+		201 => 'Created',
138
+		202 => 'Accepted',
139
+		203 => 'Non-Authoritative Information',
140
+		204 => 'No Content',
141
+		205 => 'Reset Content',
142
+		206 => 'Partial Content',
143
+		300 => 'Multiple Choices',
144
+		301 => 'Moved Permanently',
145
+		302 => 'Found',
146
+		303 => 'See Other',
147
+		304 => 'Not Modified',
148
+		305 => 'Use Proxy',
149
+		306 => '(Unused)',
150
+		307 => 'Temporary Redirect',
151
+		400 => 'Bad Request',
152
+		401 => 'Unauthorized',
153
+		402 => 'Payment Required',
154
+		403 => 'Forbidden',
155
+		404 => 'Not Found',
156
+		405 => 'Method Not Allowed',
157
+		406 => 'Not Acceptable',
158
+		407 => 'Proxy Authentication Required',
159
+		408 => 'Request Timeout',
160
+		409 => 'Conflict',
161
+		410 => 'Gone',
162
+		411 => 'Length Required',
163
+		412 => 'Precondition Failed',
164
+		413 => 'Request Entity Too Large',
165
+		414 => 'Request-URI Too Long',
166
+		415 => 'Unsupported Media Type',
167
+		416 => 'Requested Range Not Satisfiable',
168
+		417 => 'Expectation Failed',
169
+		500 => 'Internal Server Error',
170
+		501 => 'Not Implemented',
171
+		502 => 'Bad Gateway',
172
+		503 => 'Service Unavailable',
173
+		504 => 'Gateway Timeout',
174
+		505 => 'HTTP Version Not Supported'
175
+	);
176
+
177
+	/**
178
+	 * Caching of url map is enabled or not
179
+	 * @var boolean
180
+	 */
181
+	protected $cached;
182
+
183
+	// ==================================================================
184
+	//
185
+	// Public functions
186
+	//
187
+	// ------------------------------------------------------------------
188
+
189
+
190
+	/**
191
+	 * Constructor
192
+	 * @param boolean $production_mode When set to false, it will run in
193
+	 * debug mode and parse the class files every time to map it to the URL
194
+	 */
195
+	public function __construct($production_mode = false)
196
+	{
197
+		$this->production_mode = $production_mode;
198
+		$this->cache_dir = getcwd();
199
+		$this->base_dir = RESTLER_PATH;
200
+	}
201
+
202
+
203
+	/**
204
+	 * Store the url map cache if needed
205
+	 */
206
+	public function __destruct()
207
+	{
208
+		if ($this->production_mode && !($this->cached)) {
209
+			$this->saveCache();
210
+		}
211
+	}
212
+
213
+
214
+	/**
215
+	 * Use it in production mode to refresh the url map cache
216
+	 */
217
+	public function refreshCache()
218
+	{
219
+		$this->routes = array();
220
+		$this->cached = false;
221
+	}
222
+
223
+
224
+	/**
225
+	 * Call this method and pass all the formats that should be
226
+	 * supported by the API. Accepts multiple parameters
227
+	 * @param string class name of the format class that implements iFormat
228
+	 * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
229
+	 */
230
+	public function setSupportedFormats()
231
+	{
232
+		$args = func_get_args();
233
+		$extensions = array();
234
+		foreach ($args as $class_name) {
235
+			if (!is_string($class_name) || !class_exists($class_name)) {
236
+				throw new Exception("$class_name is not a vaild Format Class.");
237
+			}
238
+			$obj = new $class_name;
239
+			if (!($obj instanceof iFormat)) {
240
+				throw new Exception('Invalid format class; must implement '
241
+					. 'iFormat interface');
242
+			}
243
+			foreach ($obj->getMIMEMap() as $extension => $mime) {
244
+				if (!isset($this->format_map[$extension])) {
245
+					$this->format_map[$extension] = $class_name;
246
+				}
247
+				$mime = explode(',', $mime);
248
+				if (!is_array($mime)) {
249
+					$mime = array($mime);
250
+				}
251
+				foreach ($mime as $value) {
252
+					if (!isset($this->format_map[$value])) {
253
+						$this->format_map[$value] = $class_name;
254
+					}
255
+				}
256
+				$extensions[".$extension"] = true;
257
+			}
258
+		}
259
+		$this->format_map['default'] = $args[0];
260
+		$this->format_map['extensions'] = array_keys($extensions);
261
+	}
262
+
263
+
264
+	/**
265
+	 * Add api classes throgh this method. All the public methods
266
+	 * that do not start with _ (underscore) will be  will be exposed
267
+	 * as the public api by default.
268
+	 *
269
+	 * All the protected methods that do not start with _ (underscore)
270
+	 * will exposed as protected api which will require authentication
271
+	 * @param string $class name of the service class
272
+	 * @param string $basePath optional url prefix for mapping, uses
273
+	 * lowercase version of the class name when not specified
274
+	 * @throws Exception when supplied with invalid class name
275
+	 */
276
+	public function addAPIClass($class_name, $base_path = null)
277
+	{
278
+		if (!class_exists($class_name)) {
279
+			throw new Exception("API class $class_name is missing.");
280
+		}
281
+		$this->loadCache();
282
+		if (!$this->cached) {
283
+			if (is_null($base_path)) {
284
+				$base_path = strtolower($class_name);
285
+				$index = strrpos($class_name, '\\');
286
+				if ($index !== false) {
287
+					$base_path = substr($base_path, $index + 1);
288
+				}
289
+			} else {
290
+				$base_path = trim($base_path, '/');
291
+			}
292
+			if (strlen($base_path) > 0) {
293
+				$base_path .= '/';
294
+			}
295
+			$this->generateMap($class_name, $base_path);
296
+		}
297
+	}
298
+
299
+
300
+	/**
301
+	 * protected methods will need atleast one authentication class to be set
302
+	 * in order to allow that method to be executed
303
+	 * @param string $class_name of the authentication class
304
+	 * @param string $base_path optional url prefix for mapping
305
+	 */
306
+	public function addAuthenticationClass($class_name, $base_path = null)
307
+	{
308
+		$this->auth_classes[] = $class_name;
309
+		$this->addAPIClass($class_name, $base_path);
310
+	}
311
+
312
+
313
+	/**
314
+	 * Add class for custom error handling
315
+	 * @param string $class_name of the error handling class
316
+	 */
317
+	public function addErrorClass($class_name)
318
+	{
319
+		$this->error_classes[] = $class_name;
320
+	}
321
+
322
+
323
+	/**
324
+	 * Convenience method to respond with an error message
325
+	 * @param int $statusCode http error code
326
+	 * @param string $errorMessage optional custom error message
327
+	 */
328
+	public function handleError($status_code, $error_message = null)
329
+	{
330
+		$method = "handle$status_code";
331
+		$handled = false;
332
+		foreach ($this->error_classes as $class_name) {
333
+			if (method_exists($class_name, $method)) {
334
+				$obj = new $class_name();
335
+				$obj->restler = $this;
336
+				$obj->$method();
337
+				$handled = true;
338
+			}
339
+		}
340
+		if ($handled) {
341
+			return;
342
+		}
343
+		$message = $this->codes[$status_code]
344
+			. (!$error_message ? '' : ': ' . $error_message);
345
+		$this->setStatus($status_code);
346
+		$responder = new $this->response();
347
+		$responder->restler = $this;
348
+		$this->sendData($responder->__formatError($status_code, $message));
349
+	}
350
+
351
+
352
+	/**
353
+	 * An initialize function to allow use of the restler error generation 
354
+	 * functions for pre-processing and pre-routing of requests.
355
+	 */
356
+	public function init()
357
+	{
358
+		if (empty($this->format_map)) {
359
+			$this->setSupportedFormats('JsonFormat');
360
+		}
361
+		$this->url = $this->getPath();
362
+		$this->request_method = $this->getRequestMethod();
363
+		$this->response_format = $this->getResponseFormat();
364
+		$this->request_format = $this->getRequestFormat();
365
+		if (is_null($this->request_format)) {
366
+			$this->request_format = $this->response_format;
367
+		}
368
+		if ($this->request_method == 'PUT' || $this->request_method == 'POST') {
369
+			$this->request_data = $this->getRequestData();
370
+		}
371
+	}
372
+
373
+
374
+	/**
375
+	 * Main function for processing the api request
376
+	 * and return the response
377
+	 * @throws Exception when the api service class is missing
378
+	 * @throws RestException to send error response
379
+	 */
380
+	public function handle()
381
+	{
382
+		$this->init();
383
+		$o = $this->mapUrlToMethod();
384
+
385
+		if (!isset($o->class_name)) {
386
+			$this->handleError(404);
387
+		} else {
388
+			try {
389
+				if ($o->method_flag) {
390
+					$auth_method = '__isAuthenticated';
391
+					if (!count($this->auth_classes)) {
392
+						throw new RestException(401);
393
+					}
394
+					foreach ($this->auth_classes as $auth_class) {
395
+						$auth_obj = new $auth_class();
396
+						$auth_obj->restler = $this;
397
+						$this->applyClassMetadata($auth_class, $auth_obj, $o);
398
+						if (!method_exists($auth_obj, $auth_method)) {
399
+							throw new RestException(401, 'Authentication Class '
400
+								. 'should implement iAuthenticate');
401
+						} else if (!$auth_obj->$auth_method()) {
402
+							throw new RestException(401);
403
+						}
404
+					}
405
+				}
406
+				$this->applyClassMetadata(get_class($this->request_format),
407
+					$this->request_format, $o);
408
+				$pre_process = '_' . $this->request_format->getExtension() . '_'
409
+					. $o->method_name;
410
+				$this->service_method = $o->method_name;
411
+				if ($o->method_flag == 2) {
412
+					$o = unprotect($o);
413
+				}
414
+				$object = $this->service_class_instance = new $o->class_name();
415
+				$object->restler = $this;
416
+				if (method_exists($o->class_name, $pre_process)) {
417
+					call_user_func_array(
418
+						array($object, $pre_process), $o->arguments
419
+					);
420
+				}
421
+				switch ($o->method_flag) {
422
+					case 3:
423
+						$reflection_method = new ReflectionMethod($object,
424
+								$o->method_name);
425
+						$reflection_method->setAccessible(true);
426
+						$result = $reflection_method->invokeArgs($object,
427
+							$o->arguments);
428
+						break;
429
+					case 2:
430
+					case 1:
431
+					default:
432
+						$result = call_user_func_array(array(
433
+							$object,
434
+							$o->method_name), $o->arguments
435
+						);
436
+						break;
437
+				}
438
+			} catch (RestException $e) {
439
+				$this->handleError($e->getCode(), $e->getMessage());
440
+			}
441
+		}
442
+		$responder = new $this->response();
443
+		$responder->restler = $this;
444
+		$this->applyClassMetadata($this->response, $responder, $o);
445
+		if (isset($result) && $result !== null) {
446
+			$result = $responder->__formatResponse($result);
447
+			$this->sendData($result);
448
+		}
449
+	}
450
+
451
+
452
+	/**
453
+	 * Encodes the response in the prefered format
454
+	 * and sends back
455
+	 * @param $data array php data
456
+	 */
457
+	public function sendData($data)
458
+	{
459
+		$data = $this->response_format->encode($data, 
460
+			!($this->production_mode)
461
+		);
462
+		$post_process = '_' . $this->service_method . '_'
463
+			. $this->response_format->getExtension();
464
+		if (isset($this->service_class_instance)
465
+			&& method_exists($this->service_class_instance, $post_process)
466
+		) {
467
+			$data = call_user_func(array($this->service_class_instance,
468
+				$post_process), $data);
469
+		}
470
+		header("Cache-Control: no-cache, must-revalidate");
471
+		header("Expires: 0");
472
+		header('Content-Type: ' . $this->response_format->getMIME());
473
+		//.'; charset=utf-8');
474
+		header("X-Powered-By: Luracast Restler v" . Restler::VERSION);
475 475
 		if($this->production_mode){
476 476
 			die($data);
477 477
 		}else{
478 478
 			echo $data;
479 479
 		}
480
-    }
481
-
482
-
483
-    /**
484
-     * Sets the HTTP response status
485
-     * @param int $code response code
486
-     */
487
-    public function setStatus($code)
488
-    {
489
-        header("{$_SERVER['SERVER_PROTOCOL']} $code " . 
490
-            $this->codes[strval($code)]);
491
-    }
492
-
493
-
494
-    /**
495
-     * Compare two strings and remove the common
496
-     * sub string from the first string and return it
497
-     * @param string $first
498
-     * @param string $second
499
-     * @param string $char optional, set it as
500
-     * blank string for char by char comparison
501
-     * @return string
502
-     */
503
-    public function removeCommonPath($first, $second, $char = '/')
504
-    {
505
-        $first = explode($char, $first);
506
-        $second = explode($char, $second);
507
-        while (count($second)) {
508
-            if ($first[0] == $second[0]) {
509
-                array_shift($first);
510
-            } else {
511
-                break;
512
-            }
513
-            array_shift($second);
514
-        }
515
-        return implode($char, $first);
516
-    }
517
-
518
-
519
-    /**
520
-     * Save cache to file
521
-     */
522
-    public function saveCache()
523
-    {
524
-        $file = $this->cache_dir . '/routes.php';
525
-        $s = '$o=array();' . PHP_EOL;
526
-        foreach ($this->routes as $key => $value) {
527
-            $s .= PHP_EOL . PHP_EOL . PHP_EOL . 
528
-                "############### $key ###############" . PHP_EOL . PHP_EOL;
529
-            $s .= '$o[\'' . $key . '\']=array();';
530
-            foreach ($value as $ke => $va) {
531
-                $s .= PHP_EOL . PHP_EOL . "#==== $key $ke" . PHP_EOL . PHP_EOL;
532
-                $s .= '$o[\'' . $key . '\'][\'' . $ke . '\']=' . str_replace(
533
-                        PHP_EOL, PHP_EOL . "\t", var_export($va, true)
534
-                    ) . ';';
535
-            }
536
-        }
537
-        $s .= PHP_EOL . 'return $o;';
538
-        $r = @file_put_contents($file, "<?php $s");
539
-        @chmod($file, 0777);
540
-        if ($r === false) {
541
-            throw new Exception(
542
-                "The cache directory located at '$this->cache_dir' needs to "
543
-                . "have the permissions set to read/write/execute for everyone"
544
-                . " in order to save cache and improve performance.");
545
-        }
546
-    }
547
-
548
-    // ==================================================================
549
-    //
550
-    // Protected functions
551
-    //
552
-    // ------------------------------------------------------------------
553
-
554
-
555
-    /**
556
-     * Parses the requst url and get the api path
557
-     * @return string api path
558
-     */
559
-    protected function getPath()
560
-    {
561
-        $path = urldecode($this->removeCommonPath($_SERVER['REQUEST_URI'],
562
-                $_SERVER['SCRIPT_NAME']));
563
-        $path = preg_replace('/(\/*\?.*$)|(\/$)/', '', $path);
564
-        $path = str_replace($this->format_map['extensions'], '', $path);
565
-        return $path;
566
-    }
567
-
568
-
569
-    /**
570
-     * Parses the request to figure out the http request type
571
-     * @return string which will be one of the following
572
-     * [GET, POST, PUT, DELETE]
573
-     * @example GET
574
-     */
575
-    protected function getRequestMethod()
576
-    {
577
-        $method = $_SERVER['REQUEST_METHOD'];
578
-        if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
579
-            $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
580
-        }
581
-        //support for HEAD request
582
-        if ($method == 'HEAD') {
583
-            $method = 'GET';
584
-        }
585
-        return $method;
586
-    }
587
-
588
-
589
-    /**
590
-     * Parses the request to figure out format of the request data
591
-     * @return iFormat any class that implements iFormat
592
-     * @example JsonFormat
593
-     */
594
-    protected function getRequestFormat()
595
-    {
596
-        $format = null;
597
-        //check if client has sent any information on request format
598
-        if (isset($_SERVER['CONTENT_TYPE'])) {
599
-            $mime = explode(';', $_SERVER['CONTENT_TYPE']);
600
-            $mime = $mime[0];
601
-            if ($mime == UrlEncodedFormat::MIME) {
602
-                $format = new UrlEncodedFormat();
603
-            } else {
604
-                if (isset($this->format_map[$mime])) {
605
-                    $format = $this->format_map[$mime];
606
-                    $format = is_string($format) ? new $format : $format;
607
-                    $format->setMIME($mime);
608
-                }
609
-            }
610
-        }
611
-        return $format;
612
-    }
613
-
614
-
615
-    /**
616
-     * Parses the request to figure out the best format for response
617
-     * @return iFormat any class that implements iFormat
618
-     * @example JsonFormat
619
-     */
620
-    protected function getResponseFormat()
621
-    {
622
-        //check if client has specified an extension
623
-        /**
624
-         * @var iFormat
625
-         */
626
-        $format = null;
627
-        $extensions = explode('.',
628
-            parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
629
-        while ($extensions) {
630
-            $extension = array_pop($extensions);
631
-            $extension = explode('/', $extension);
632
-            $extension = array_shift($extension);
633
-            if ($extension && isset($this->format_map[$extension])) {
634
-                $format = $this->format_map[$extension];
635
-                $format = is_string($format) ? new $format : $format;
636
-                $format->setExtension($extension);
637
-                return $format;
638
-            }
639
-        }
640
-        //check if client has sent list of accepted data formats
641
-        if (isset($_SERVER['HTTP_ACCEPT'])) {
642
-            $acceptList = array();
643
-            $accepts = explode(',', strtolower($_SERVER['HTTP_ACCEPT']));
644
-            if (!is_array($accepts)) {
645
-                $accepts = array($accepts);
646
-            }
647
-            foreach ($accepts as $pos => $accept) {
648
-                $parts = explode(';q=', trim($accept));
649
-                $type = array_shift($parts);
650
-                $quality = count($parts) ? 
651
-                    floatval(array_shift($parts)) : 
652
-                    (1000 - $pos) / 1000;
653
-                $acceptList[$type] = $quality;
654
-            }
655
-            arsort($acceptList);
656
-            foreach ($acceptList as $accept => $quality) {
657
-                if (isset($this->format_map[$accept])) {
658
-                    $format = $this->format_map[$accept];
659
-                    $format = is_string($format) ? new $format : $format;
660
-                    $format->setMIME($accept);
661
-                    // Tell cache content is based on Accept header
662
-                    header("Vary: Accept"); 
663
-                    return $format;
664
-                }
665
-            }
666
-        } else {
667
-            // RFC 2616: If no Accept header field is
668
-            // present, then it is assumed that the
669
-            // client accepts all media types.
670
-            $_SERVER['HTTP_ACCEPT'] = '*/*';
671
-        }
672
-        if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) {
673
-            if (strpos($_SERVER['HTTP_ACCEPT'], 'application/*') !== false) {
674
-                $format = new JsonFormat;
675
-            } else if (strpos($_SERVER['HTTP_ACCEPT'], 'text/*') !== false) {
676
-                $format = new XmlFormat;
677
-            } else if (strpos($_SERVER['HTTP_ACCEPT'], '*/*') !== false) {
678
-                $format = new $this->format_map['default'];
679
-            }
680
-        }
681
-        if (empty($format)) {
682
-            // RFC 2616: If an Accept header field is present, and if the 
683
-            // server cannot send a response which is acceptable according to 
684
-            // the combined Accept field value, then the server SHOULD send 
685
-            // a 406 (not acceptable) response.
686
-            header('HTTP/1.1 406 Not Acceptable');
687
-            die('406 Not Acceptable: The server was unable to ' . 
688
-                    'negotiate content for this request.');
689
-        } else {
690
-            // Tell cache content is based ot Accept header
691
-            header("Vary: Accept"); 
692
-            return $format;
693
-        }
694
-    }
695
-
696
-
697
-    /**
698
-     * Parses the request data and returns it
699
-     * @return array php data
700
-     */
701
-    protected function getRequestData()
702
-    {
703
-        try {
704
-            $r = file_get_contents('php://input');
705
-            if (is_null($r)) {
706
-                return $_GET;
707
-            }
708
-            $r = $this->request_format->decode($r);
709
-            return is_null($r) ? array() : $r;
710
-        } catch (RestException $e) {
711
-            $this->handleError($e->getCode(), $e->getMessage());
712
-        }
713
-    }
714
-
715
-
716
-    protected function mapUrlToMethod()
717
-    {
718
-        if (!isset($this->routes[$this->request_method])) {
719
-            return array();
720
-        }
721
-        $urls = $this->routes[$this->request_method];
722
-        if (!$urls) {
723
-            return array();
724
-        }
725
-
726
-        $found = false;
727
-        $this->request_data += $_GET;
728
-        $params = array('request_data' => $this->request_data);
729
-        $params += $this->request_data;
730
-        $lc = strtolower($this->url);
731
-        foreach ($urls as $url => $call) {
732
-            $call = (object) $call;
733
-            if (strstr($url, ':')) {
734
-                $regex = preg_replace('/\\\:([^\/]+)/', '(?P<$1>[^/]+)',
735
-                    preg_quote($url));
736
-                if (preg_match(":^$regex$:i", $this->url, $matches)) {
737
-                    foreach ($matches as $arg => $match) {
738
-                        if (isset($call->arguments[$arg])) {
739
-                            //flog("$arg => $match $args[$arg]");
740
-                            $params[$arg] = $match;
741
-                        }
742
-                    }
743
-                    $found = true;
744
-                    break;
745
-                }
746
-            } else if ($url == $lc) {
747
-                $found = true;
748
-                break;
749
-            }
750
-        }
751
-        if ($found) {
752
-            $p = $call->defaults;
753
-            foreach ($call->arguments as $key => $value) {
754
-                if (isset($params[$key])) {
755
-                    $p[$value] = $params[$key];
756
-                }
757
-            }
758
-            $call->arguments = $p;
759
-            return $call;
760
-        }
761
-    }
762
-
763
-
764
-    /**
765
-     * Apply static and non-static properties defined in
766
-     * the method information anotation
767
-     * @param String $class_name
768
-     * @param Object $instance instance of that class
769
-     * @param Object $method_info method information and metadata
770
-     */
771
-    protected function applyClassMetadata($class_name, $instance, $method_info)
772
-    {
773
-        if (isset($method_info->metadata[$class_name])
774
-            && is_array($method_info->metadata[$class_name])
775
-        ) {
776
-            foreach ($method_info->metadata[$class_name] as
777
-                    $property => $value) {
778
-                if (property_exists($class_name, $property)) {
779
-                    $reflection_property = 
780
-                        new ReflectionProperty($class_name, $property);
781
-                    $reflection_property->setValue($instance, $value);
782
-                }
783
-            }
784
-        }
785
-    }
786
-
787
-
788
-    protected function loadCache()
789
-    {
790
-        if ($this->cached !== null) {
791
-            return;
792
-        }
793
-        $file = $this->cache_dir . '/routes.php';
794
-        $this->cached = false;
795
-
796
-        if ($this->production_mode) {
797
-            if (file_exists($file)) {
798
-                $routes = include($file);
799
-            }
800
-            if (isset($routes) && is_array($routes)) {
801
-                $this->routes = $routes;
802
-                $this->cached = true;
803
-            }
804
-        } else {
805
-            //@unlink($this->cache_dir . "/$name.php");
806
-        }
807
-    }
808
-
809
-
810
-    /**
811
-     * Generates cachable url to method mapping
812
-     * @param string $class_name
813
-     * @param string $base_path
814
-     */
815
-    protected function generateMap($class_name, $base_path = '')
816
-    {
817
-        $reflection = new ReflectionClass($class_name);
818
-        $class_metadata = parse_doc($reflection->getDocComment());
819
-        $methods = $reflection->getMethods(
820
-            ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_PROTECTED
821
-        );
822
-        foreach ($methods as $method) {
823
-            $doc = $method->getDocComment();
824
-            $arguments = array();
825
-            $defaults = array();
826
-            $metadata = $class_metadata + parse_doc($doc);
827
-            $params = $method->getParameters();
828
-            $position = 0;
829
-            foreach ($params as $param) {
830
-                $arguments[$param->getName()] = $position;
831
-                $defaults[$position] = $param->isDefaultValueAvailable() ? 
832
-                    $param->getDefaultValue() : null;
833
-                $position++;
834
-            }
835
-            $method_flag = $method->isProtected() ? 
836
-                (isRestlerCompatibilityModeEnabled() ? 2 : 3) : 
837
-                (isset($metadata['protected']) ? 1 : 0);
838
-
839
-            //take note of the order
840
-            $call = array(
841
-                'class_name' => $class_name,
842
-                'method_name' => $method->getName(),
843
-                'arguments' => $arguments,
844
-                'defaults' => $defaults,
845
-                'metadata' => $metadata,
846
-                'method_flag' => $method_flag
847
-            );
848
-            $method_url = strtolower($method->getName());
849
-            if (preg_match_all(
850
-                '/@url\s+(GET|POST|PUT|DELETE|HEAD|OPTIONS)[ \t]*\/?(\S*)/s',
851
-                    $doc, $matches, PREG_SET_ORDER)
852
-            ) {
853
-                foreach ($matches as $match) {
854
-                    $http_method = $match[1];
855
-                    $url = rtrim($base_path . $match[2], '/');
856
-                    $this->routes[$http_method][$url] = $call;
857
-                }
858
-            } elseif ($method_url[0] != '_') { 
859
-                //not prefixed with underscore
860
-                // no configuration found so use convention
861
-                if (preg_match_all('/^(GET|POST|PUT|DELETE|HEAD|OPTIONS)/i',
862
-                        $method_url, $matches)
863
-                ) {
864
-                    $http_method = strtoupper($matches[0][0]);
865
-                    $method_url = substr($method_url, strlen($http_method));
866
-                } else {
867
-                    $http_method = 'GET';
868
-                }
869
-                $url = $base_path
870
-                    . ($method_url == 'index' || $method_url == 'default' ? '' :
871
-                        $method_url);
872
-                $url = rtrim($url, '/');
873
-                $this->routes[$http_method][$url] = $call;
874
-                foreach ($params as $param) {
875
-                    if ($param->getName() == 'request_data') {
876
-                        break;
877
-                    }
878
-                    $url .= $url == '' ? ':' : '/:';
879
-                    $url .= $param->getName();
880
-                    $this->routes[$http_method][$url] = $call;
881
-                }
882
-            }
883
-        }
884
-    }
480
+	}
481
+
482
+
483
+	/**
484
+	 * Sets the HTTP response status
485
+	 * @param int $code response code
486
+	 */
487
+	public function setStatus($code)
488
+	{
489
+		header("{$_SERVER['SERVER_PROTOCOL']} $code " . 
490
+			$this->codes[strval($code)]);
491
+	}
492
+
493
+
494
+	/**
495
+	 * Compare two strings and remove the common
496
+	 * sub string from the first string and return it
497
+	 * @param string $first
498
+	 * @param string $second
499
+	 * @param string $char optional, set it as
500
+	 * blank string for char by char comparison
501
+	 * @return string
502
+	 */
503
+	public function removeCommonPath($first, $second, $char = '/')
504
+	{
505
+		$first = explode($char, $first);
506
+		$second = explode($char, $second);
507
+		while (count($second)) {
508
+			if ($first[0] == $second[0]) {
509
+				array_shift($first);
510
+			} else {
511
+				break;
512
+			}
513
+			array_shift($second);
514
+		}
515
+		return implode($char, $first);
516
+	}
517
+
518
+
519
+	/**
520
+	 * Save cache to file
521
+	 */
522
+	public function saveCache()
523
+	{
524
+		$file = $this->cache_dir . '/routes.php';
525
+		$s = '$o=array();' . PHP_EOL;
526
+		foreach ($this->routes as $key => $value) {
527
+			$s .= PHP_EOL . PHP_EOL . PHP_EOL . 
528
+				"############### $key ###############" . PHP_EOL . PHP_EOL;
529
+			$s .= '$o[\'' . $key . '\']=array();';
530
+			foreach ($value as $ke => $va) {
531
+				$s .= PHP_EOL . PHP_EOL . "#==== $key $ke" . PHP_EOL . PHP_EOL;
532
+				$s .= '$o[\'' . $key . '\'][\'' . $ke . '\']=' . str_replace(
533
+						PHP_EOL, PHP_EOL . "\t", var_export($va, true)
534
+					) . ';';
535
+			}
536
+		}
537
+		$s .= PHP_EOL . 'return $o;';
538
+		$r = @file_put_contents($file, "<?php $s");
539
+		@chmod($file, 0777);
540
+		if ($r === false) {
541
+			throw new Exception(
542
+				"The cache directory located at '$this->cache_dir' needs to "
543
+				. "have the permissions set to read/write/execute for everyone"
544
+				. " in order to save cache and improve performance.");
545
+		}
546
+	}
547
+
548
+	// ==================================================================
549
+	//
550
+	// Protected functions
551
+	//
552
+	// ------------------------------------------------------------------
553
+
554
+
555
+	/**
556
+	 * Parses the requst url and get the api path
557
+	 * @return string api path
558
+	 */
559
+	protected function getPath()
560
+	{
561
+		$path = urldecode($this->removeCommonPath($_SERVER['REQUEST_URI'],
562
+				$_SERVER['SCRIPT_NAME']));
563
+		$path = preg_replace('/(\/*\?.*$)|(\/$)/', '', $path);
564
+		$path = str_replace($this->format_map['extensions'], '', $path);
565
+		return $path;
566
+	}
567
+
568
+
569
+	/**
570
+	 * Parses the request to figure out the http request type
571
+	 * @return string which will be one of the following
572
+	 * [GET, POST, PUT, DELETE]
573
+	 * @example GET
574
+	 */
575
+	protected function getRequestMethod()
576
+	{
577
+		$method = $_SERVER['REQUEST_METHOD'];
578
+		if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
579
+			$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
580
+		}
581
+		//support for HEAD request
582
+		if ($method == 'HEAD') {
583
+			$method = 'GET';
584
+		}
585
+		return $method;
586
+	}
587
+
588
+
589
+	/**
590
+	 * Parses the request to figure out format of the request data
591
+	 * @return iFormat any class that implements iFormat
592
+	 * @example JsonFormat
593
+	 */
594
+	protected function getRequestFormat()
595
+	{
596
+		$format = null;
597
+		//check if client has sent any information on request format
598
+		if (isset($_SERVER['CONTENT_TYPE'])) {
599
+			$mime = explode(';', $_SERVER['CONTENT_TYPE']);
600
+			$mime = $mime[0];
601
+			if ($mime == UrlEncodedFormat::MIME) {
602
+				$format = new UrlEncodedFormat();
603
+			} else {
604
+				if (isset($this->format_map[$mime])) {
605
+					$format = $this->format_map[$mime];
606
+					$format = is_string($format) ? new $format : $format;
607
+					$format->setMIME($mime);
608
+				}
609
+			}
610
+		}
611
+		return $format;
612
+	}
613
+
614
+
615
+	/**
616
+	 * Parses the request to figure out the best format for response
617
+	 * @return iFormat any class that implements iFormat
618
+	 * @example JsonFormat
619
+	 */
620
+	protected function getResponseFormat()
621
+	{
622
+		//check if client has specified an extension
623
+		/**
624
+		 * @var iFormat
625
+		 */
626
+		$format = null;
627
+		$extensions = explode('.',
628
+			parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
629
+		while ($extensions) {
630
+			$extension = array_pop($extensions);
631
+			$extension = explode('/', $extension);
632
+			$extension = array_shift($extension);
633
+			if ($extension && isset($this->format_map[$extension])) {
634
+				$format = $this->format_map[$extension];
635
+				$format = is_string($format) ? new $format : $format;
636
+				$format->setExtension($extension);
637
+				return $format;
638
+			}
639
+		}
640
+		//check if client has sent list of accepted data formats
641
+		if (isset($_SERVER['HTTP_ACCEPT'])) {
642
+			$acceptList = array();
643
+			$accepts = explode(',', strtolower($_SERVER['HTTP_ACCEPT']));
644
+			if (!is_array($accepts)) {
645
+				$accepts = array($accepts);
646
+			}
647
+			foreach ($accepts as $pos => $accept) {
648
+				$parts = explode(';q=', trim($accept));
649
+				$type = array_shift($parts);
650
+				$quality = count($parts) ? 
651
+					floatval(array_shift($parts)) : 
652
+					(1000 - $pos) / 1000;
653
+				$acceptList[$type] = $quality;
654
+			}
655
+			arsort($acceptList);
656
+			foreach ($acceptList as $accept => $quality) {
657
+				if (isset($this->format_map[$accept])) {
658
+					$format = $this->format_map[$accept];
659
+					$format = is_string($format) ? new $format : $format;
660
+					$format->setMIME($accept);
661
+					// Tell cache content is based on Accept header
662
+					header("Vary: Accept"); 
663
+					return $format;
664
+				}
665
+			}
666
+		} else {
667
+			// RFC 2616: If no Accept header field is
668
+			// present, then it is assumed that the
669
+			// client accepts all media types.
670
+			$_SERVER['HTTP_ACCEPT'] = '*/*';
671
+		}
672
+		if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) {
673
+			if (strpos($_SERVER['HTTP_ACCEPT'], 'application/*') !== false) {
674
+				$format = new JsonFormat;
675
+			} else if (strpos($_SERVER['HTTP_ACCEPT'], 'text/*') !== false) {
676
+				$format = new XmlFormat;
677
+			} else if (strpos($_SERVER['HTTP_ACCEPT'], '*/*') !== false) {
678
+				$format = new $this->format_map['default'];
679
+			}
680
+		}
681
+		if (empty($format)) {
682
+			// RFC 2616: If an Accept header field is present, and if the 
683
+			// server cannot send a response which is acceptable according to 
684
+			// the combined Accept field value, then the server SHOULD send 
685
+			// a 406 (not acceptable) response.
686
+			header('HTTP/1.1 406 Not Acceptable');
687
+			die('406 Not Acceptable: The server was unable to ' . 
688
+					'negotiate content for this request.');
689
+		} else {
690
+			// Tell cache content is based ot Accept header
691
+			header("Vary: Accept"); 
692
+			return $format;
693
+		}
694
+	}
695
+
696
+
697
+	/**
698
+	 * Parses the request data and returns it
699
+	 * @return array php data
700
+	 */
701
+	protected function getRequestData()
702
+	{
703
+		try {
704
+			$r = file_get_contents('php://input');
705
+			if (is_null($r)) {
706
+				return $_GET;
707
+			}
708
+			$r = $this->request_format->decode($r);
709
+			return is_null($r) ? array() : $r;
710
+		} catch (RestException $e) {
711
+			$this->handleError($e->getCode(), $e->getMessage());
712
+		}
713
+	}
714
+
715
+
716
+	protected function mapUrlToMethod()
717
+	{
718
+		if (!isset($this->routes[$this->request_method])) {
719
+			return array();
720
+		}
721
+		$urls = $this->routes[$this->request_method];
722
+		if (!$urls) {
723
+			return array();
724
+		}
725
+
726
+		$found = false;
727
+		$this->request_data += $_GET;
728
+		$params = array('request_data' => $this->request_data);
729
+		$params += $this->request_data;
730
+		$lc = strtolower($this->url);
731
+		foreach ($urls as $url => $call) {
732
+			$call = (object) $call;
733
+			if (strstr($url, ':')) {
734
+				$regex = preg_replace('/\\\:([^\/]+)/', '(?P<$1>[^/]+)',
735
+					preg_quote($url));
736
+				if (preg_match(":^$regex$:i", $this->url, $matches)) {
737
+					foreach ($matches as $arg => $match) {
738
+						if (isset($call->arguments[$arg])) {
739
+							//flog("$arg => $match $args[$arg]");
740
+							$params[$arg] = $match;
741
+						}
742
+					}
743
+					$found = true;
744
+					break;
745
+				}
746
+			} else if ($url == $lc) {
747
+				$found = true;
748
+				break;
749
+			}
750
+		}
751
+		if ($found) {
752
+			$p = $call->defaults;
753
+			foreach ($call->arguments as $key => $value) {
754
+				if (isset($params[$key])) {
755
+					$p[$value] = $params[$key];
756
+				}
757
+			}
758
+			$call->arguments = $p;
759
+			return $call;
760
+		}
761
+	}
762
+
763
+
764
+	/**
765
+	 * Apply static and non-static properties defined in
766
+	 * the method information anotation
767
+	 * @param String $class_name
768
+	 * @param Object $instance instance of that class
769
+	 * @param Object $method_info method information and metadata
770
+	 */
771
+	protected function applyClassMetadata($class_name, $instance, $method_info)
772
+	{
773
+		if (isset($method_info->metadata[$class_name])
774
+			&& is_array($method_info->metadata[$class_name])
775
+		) {
776
+			foreach ($method_info->metadata[$class_name] as
777
+					$property => $value) {
778
+				if (property_exists($class_name, $property)) {
779
+					$reflection_property = 
780
+						new ReflectionProperty($class_name, $property);
781
+					$reflection_property->setValue($instance, $value);
782
+				}
783
+			}
784
+		}
785
+	}
786
+
787
+
788
+	protected function loadCache()
789
+	{
790
+		if ($this->cached !== null) {
791
+			return;
792
+		}
793
+		$file = $this->cache_dir . '/routes.php';
794
+		$this->cached = false;
795
+
796
+		if ($this->production_mode) {
797
+			if (file_exists($file)) {
798
+				$routes = include($file);
799
+			}
800
+			if (isset($routes) && is_array($routes)) {
801
+				$this->routes = $routes;
802
+				$this->cached = true;
803
+			}
804
+		} else {
805
+			//@unlink($this->cache_dir . "/$name.php");
806
+		}
807
+	}
808
+
809
+
810
+	/**
811
+	 * Generates cachable url to method mapping
812
+	 * @param string $class_name
813
+	 * @param string $base_path
814
+	 */
815
+	protected function generateMap($class_name, $base_path = '')
816
+	{
817
+		$reflection = new ReflectionClass($class_name);
818
+		$class_metadata = parse_doc($reflection->getDocComment());
819
+		$methods = $reflection->getMethods(
820
+			ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_PROTECTED
821
+		);
822
+		foreach ($methods as $method) {
823
+			$doc = $method->getDocComment();
824
+			$arguments = array();
825
+			$defaults = array();
826
+			$metadata = $class_metadata + parse_doc($doc);
827
+			$params = $method->getParameters();
828
+			$position = 0;
829
+			foreach ($params as $param) {
830
+				$arguments[$param->getName()] = $position;
831
+				$defaults[$position] = $param->isDefaultValueAvailable() ? 
832
+					$param->getDefaultValue() : null;
833
+				$position++;
834
+			}
835
+			$method_flag = $method->isProtected() ? 
836
+				(isRestlerCompatibilityModeEnabled() ? 2 : 3) : 
837
+				(isset($metadata['protected']) ? 1 : 0);
838
+
839
+			//take note of the order
840
+			$call = array(
841
+				'class_name' => $class_name,
842
+				'method_name' => $method->getName(),
843
+				'arguments' => $arguments,
844
+				'defaults' => $defaults,
845
+				'metadata' => $metadata,
846
+				'method_flag' => $method_flag
847
+			);
848
+			$method_url = strtolower($method->getName());
849
+			if (preg_match_all(
850
+				'/@url\s+(GET|POST|PUT|DELETE|HEAD|OPTIONS)[ \t]*\/?(\S*)/s',
851
+					$doc, $matches, PREG_SET_ORDER)
852
+			) {
853
+				foreach ($matches as $match) {
854
+					$http_method = $match[1];
855
+					$url = rtrim($base_path . $match[2], '/');
856
+					$this->routes[$http_method][$url] = $call;
857
+				}
858
+			} elseif ($method_url[0] != '_') { 
859
+				//not prefixed with underscore
860
+				// no configuration found so use convention
861
+				if (preg_match_all('/^(GET|POST|PUT|DELETE|HEAD|OPTIONS)/i',
862
+						$method_url, $matches)
863
+				) {
864
+					$http_method = strtoupper($matches[0][0]);
865
+					$method_url = substr($method_url, strlen($http_method));
866
+				} else {
867
+					$http_method = 'GET';
868
+				}
869
+				$url = $base_path
870
+					. ($method_url == 'index' || $method_url == 'default' ? '' :
871
+						$method_url);
872
+				$url = rtrim($url, '/');
873
+				$this->routes[$http_method][$url] = $call;
874
+				foreach ($params as $param) {
875
+					if ($param->getName() == 'request_data') {
876
+						break;
877
+					}
878
+					$url .= $url == '' ? ':' : '/:';
879
+					$url .= $param->getName();
880
+					$this->routes[$http_method][$url] = $call;
881
+				}
882
+			}
883
+		}
884
+	}
885 885
 
886 886
 }
887 887
 
888 888
 if (version_compare(PHP_VERSION, '5.3.0') < 0) {
889
-    require_once 'compat.php';
889
+	require_once 'compat.php';
890 890
 }
891 891
 
892 892
 // ==================================================================
@@ -910,10 +910,10 @@  discard block
 block discarded – undo
910 910
 {
911 911
 
912 912
 
913
-    public function __construct($http_status_code, $error_message = null)
914
-    {
915
-        parent::__construct($error_message, $http_status_code);
916
-    }
913
+	public function __construct($http_status_code, $error_message = null)
914
+	{
915
+		parent::__construct($error_message, $http_status_code);
916
+	}
917 917
 
918 918
 }
919 919
 
@@ -931,21 +931,21 @@  discard block
 block discarded – undo
931 931
 {
932 932
 
933 933
 
934
-    /**
935
-     * Result of an api call is passed to this method
936
-     * to create a standard structure for the data
937
-     * @param unknown_type $result can be a primitive or array or object
938
-     */
939
-    public function __formatResponse($result);
934
+	/**
935
+	 * Result of an api call is passed to this method
936
+	 * to create a standard structure for the data
937
+	 * @param unknown_type $result can be a primitive or array or object
938
+	 */
939
+	public function __formatResponse($result);
940 940
 
941 941
 
942
-    /**
943
-     * When the api call results in RestException this method
944
-     * will be called to return the error message
945
-     * @param int $status_code
946
-     * @param String $message
947
-     */
948
-    public function __formatError($status_code, $message);
942
+	/**
943
+	 * When the api call results in RestException this method
944
+	 * will be called to return the error message
945
+	 * @param int $status_code
946
+	 * @param String $message
947
+	 */
948
+	public function __formatError($status_code, $message);
949 949
 }
950 950
 
951 951
 /**
@@ -962,21 +962,21 @@  discard block
 block discarded – undo
962 962
 {
963 963
 
964 964
 
965
-    public function __formatResponse($result)
966
-    {
967
-        return $result;
968
-    }
965
+	public function __formatResponse($result)
966
+	{
967
+		return $result;
968
+	}
969 969
 
970 970
 
971
-    public function __formatError($statusCode, $message)
972
-    {
973
-        return array(
974
-            'error' => array(
975
-                'code' => $statusCode,
976
-                'message' => $message
977
-            )
978
-        );
979
-    }
971
+	public function __formatError($statusCode, $message)
972
+	{
973
+		return array(
974
+			'error' => array(
975
+				'code' => $statusCode,
976
+				'message' => $message
977
+			)
978
+		);
979
+	}
980 980
 
981 981
 }
982 982
 
@@ -994,11 +994,11 @@  discard block
 block discarded – undo
994 994
 {
995 995
 
996 996
 
997
-    /**
998
-     * Auth function that is called when a protected method is requested
999
-     * @return boolean true or false
1000
-     */
1001
-    public function __isAuthenticated();
997
+	/**
998
+	 * Auth function that is called when a protected method is requested
999
+	 * @return boolean true or false
1000
+	 */
1001
+	public function __isAuthenticated();
1002 1002
 }
1003 1003
 
1004 1004
 /**
@@ -1016,60 +1016,60 @@  discard block
 block discarded – undo
1016 1016
 {
1017 1017
 
1018 1018
 
1019
-    /**
1020
-     * Get Extension => MIME type mappings as an associative array
1021
-     * @return array list of mime strings for the format
1022
-     * @example array('json'=>'application/json');
1023
-     */
1024
-    public function getMIMEMap();
1019
+	/**
1020
+	 * Get Extension => MIME type mappings as an associative array
1021
+	 * @return array list of mime strings for the format
1022
+	 * @example array('json'=>'application/json');
1023
+	 */
1024
+	public function getMIMEMap();
1025 1025
 
1026 1026
 
1027
-    /**
1028
-     * Set the selected MIME type
1029
-     * @param string $mime MIME type
1030
-     */
1031
-    public function setMIME($mime);
1027
+	/**
1028
+	 * Set the selected MIME type
1029
+	 * @param string $mime MIME type
1030
+	 */
1031
+	public function setMIME($mime);
1032 1032
 
1033 1033
 
1034
-    /**
1035
-     * Get selected MIME type
1036
-     */
1037
-    public function getMIME();
1034
+	/**
1035
+	 * Get selected MIME type
1036
+	 */
1037
+	public function getMIME();
1038 1038
 
1039 1039
 
1040
-    /**
1041
-     * Set the selected file extension
1042
-     * @param string $extension file extension
1043
-     */
1044
-    public function setExtension($extension);
1040
+	/**
1041
+	 * Set the selected file extension
1042
+	 * @param string $extension file extension
1043
+	 */
1044
+	public function setExtension($extension);
1045 1045
 
1046 1046
 
1047
-    /**
1048
-     * Get the selected file extension
1049
-     * @return string file extension
1050
-     */
1051
-    public function getExtension();
1047
+	/**
1048
+	 * Get the selected file extension
1049
+	 * @return string file extension
1050
+	 */
1051
+	public function getExtension();
1052 1052
 
1053 1053
 
1054
-    /**
1055
-     * Encode the given data in the format
1056
-     * @param array $data resulting data that needs to
1057
-     * be encoded in the given format
1058
-     * @param boolean $human_readable set to true when restler
1059
-     * is not running in production mode. Formatter has to
1060
-     * make the encoded output more human readable
1061
-     * @return string encoded string
1062
-     */
1063
-    public function encode($data, $human_readable = false);
1054
+	/**
1055
+	 * Encode the given data in the format
1056
+	 * @param array $data resulting data that needs to
1057
+	 * be encoded in the given format
1058
+	 * @param boolean $human_readable set to true when restler
1059
+	 * is not running in production mode. Formatter has to
1060
+	 * make the encoded output more human readable
1061
+	 * @return string encoded string
1062
+	 */
1063
+	public function encode($data, $human_readable = false);
1064 1064
 
1065 1065
 
1066
-    /**
1067
-     * Decode the given data from the format
1068
-     * @param string $data data sent from client to
1069
-     * the api in the given format.
1070
-     * @return array associative array of the parsed data
1071
-     */
1072
-    public function decode($data);
1066
+	/**
1067
+	 * Decode the given data from the format
1068
+	 * @param string $data data sent from client to
1069
+	 * the api in the given format.
1070
+	 * @return array associative array of the parsed data
1071
+	 */
1072
+	public function decode($data);
1073 1073
 }
1074 1074
 
1075 1075
 /**
@@ -1085,57 +1085,57 @@  discard block
 block discarded – undo
1085 1085
 class UrlEncodedFormat implements iFormat
1086 1086
 {
1087 1087
 
1088
-    const MIME = 'application/x-www-form-urlencoded';
1089
-    const EXTENSION = 'post';
1088
+	const MIME = 'application/x-www-form-urlencoded';
1089
+	const EXTENSION = 'post';
1090 1090
 
1091 1091
 
1092
-    public function getMIMEMap()
1093
-    {
1094
-        return array(self::EXTENSION => self::MIME);
1095
-    }
1092
+	public function getMIMEMap()
1093
+	{
1094
+		return array(self::EXTENSION => self::MIME);
1095
+	}
1096 1096
 
1097 1097
 
1098
-    public function getMIME()
1099
-    {
1100
-        return self::MIME;
1101
-    }
1098
+	public function getMIME()
1099
+	{
1100
+		return self::MIME;
1101
+	}
1102 1102
 
1103 1103
 
1104
-    public function getExtension()
1105
-    {
1106
-        return self::EXTENSION;
1107
-    }
1104
+	public function getExtension()
1105
+	{
1106
+		return self::EXTENSION;
1107
+	}
1108 1108
 
1109 1109
 
1110
-    public function setMIME($mime)
1111
-    {
1112
-        //do nothing
1113
-    }
1110
+	public function setMIME($mime)
1111
+	{
1112
+		//do nothing
1113
+	}
1114 1114
 
1115 1115
 
1116
-    public function setExtension($extension)
1117
-    {
1118
-        //do nothing
1119
-    }
1116
+	public function setExtension($extension)
1117
+	{
1118
+		//do nothing
1119
+	}
1120 1120
 
1121 1121
 
1122
-    public function encode($data, $human_readable = false)
1123
-    {
1124
-        return http_build_query($data);
1125
-    }
1122
+	public function encode($data, $human_readable = false)
1123
+	{
1124
+		return http_build_query($data);
1125
+	}
1126 1126
 
1127 1127
 
1128
-    public function decode($data)
1129
-    {
1130
-        parse_str($data, $r);
1131
-        return $r;
1132
-    }
1128
+	public function decode($data)
1129
+	{
1130
+		parse_str($data, $r);
1131
+		return $r;
1132
+	}
1133 1133
 
1134 1134
 
1135
-    public function __toString()
1136
-    {
1137
-        return $this->getExtension();
1138
-    }
1135
+	public function __toString()
1136
+	{
1137
+		return $this->getExtension();
1138
+	}
1139 1139
 
1140 1140
 }
1141 1141
 
@@ -1152,158 +1152,158 @@  discard block
 block discarded – undo
1152 1152
 class JsonFormat implements iFormat
1153 1153
 {
1154 1154
 
1155
-    const MIME = 'application/json,application/javascript';
1156
-
1157
-    static $mime = 'application/json';
1158
-
1159
-    const EXTENSION = 'json';
1160
-
1161
-
1162
-    public function getMIMEMap()
1163
-    {
1164
-        return array(self::EXTENSION => self::MIME);
1165
-    }
1166
-
1167
-
1168
-    public function getMIME()
1169
-    {
1170
-        return self::$mime;
1171
-    }
1172
-
1173
-
1174
-    public function getExtension()
1175
-    {
1176
-        return self::EXTENSION;
1177
-    }
1178
-
1179
-
1180
-    public function setMIME($mime)
1181
-    {
1182
-        self::$mime = $mime;
1183
-    }
1184
-
1185
-
1186
-    public function setExtension($extension)
1187
-    {
1188
-        //do nothing
1189
-    }
1190
-
1191
-
1192
-    public function encode($data, $human_readable = false)
1193
-    {
1194
-        return $human_readable ? 
1195
-            $this->json_format(json_encode(object_to_array($data))) : 
1196
-            json_encode(object_to_array($data));
1197
-    }
1198
-
1199
-
1200
-    public function decode($data)
1201
-    {
1202
-        $decoded = json_decode($data);
1203
-        if (function_exists('json_last_error')) {
1204
-            $message = '';
1205
-            switch (json_last_error()) {
1206
-                case JSON_ERROR_NONE:
1207
-                    return object_to_array($decoded);
1208
-                    break;
1209
-                case JSON_ERROR_DEPTH:
1210
-                    $message = 'maximum stack depth exceeded';
1211
-                    break;
1212
-                case JSON_ERROR_STATE_MISMATCH:
1213
-                    $message = 'underflow or the modes mismatch';
1214
-                    break;
1215
-                case JSON_ERROR_CTRL_CHAR:
1216
-                    $message = 'unexpected control character found';
1217
-                    break;
1218
-                case JSON_ERROR_SYNTAX:
1219
-                    $message = 'malformed JSON';
1220
-                    break;
1221
-                case JSON_ERROR_UTF8:
1222
-                    $message = 'malformed UTF-8 characters, '.
1223
-                        'possibly incorrectly encoded';
1224
-                    break;
1225
-                default:
1226
-                    $message = 'unknown error';
1227
-                    break;
1228
-            }
1229
-            throw new RestException(400, 'Error parsing JSON, ' . $message);
1230
-        } else if (strlen($data) && $decoded === null || $decoded === $data) {
1231
-            throw new RestException(400, 'Error parsing JSON');
1232
-        }
1233
-        return object_to_array($decoded);
1234
-    }
1235
-
1236
-
1237
-    /**
1238
-     * Pretty print JSON string
1239
-     * @param string $json
1240
-     * @return string formated json
1241
-     */
1242
-    private function json_format($json)
1243
-    {
1244
-        $tab = "  ";
1245
-        $new_json = "";
1246
-        $indent_level = 0;
1247
-        $in_string = false;
1248
-        $len = strlen($json);
1249
-
1250
-        for ($c = 0; $c < $len; $c++) {
1251
-            $char = $json[$c];
1252
-            switch ($char) {
1253
-                case '{':
1254
-                case '[':
1255
-                    if (!$in_string) {
1256
-                        $new_json .= $char . "\n" .
1257
-                            str_repeat($tab, $indent_level + 1);
1258
-                        $indent_level++;
1259
-                    } else {
1260
-                        $new_json .= $char;
1261
-                    }
1262
-                    break;
1263
-                case '}':
1264
-                case ']':
1265
-                    if (!$in_string) {
1266
-                        $indent_level--;
1267
-                        $new_json .= "\n" . str_repeat($tab, $indent_level) 
1268
-                            . $char;
1269
-                    } else {
1270
-                        $new_json .= $char;
1271
-                    }
1272
-                    break;
1273
-                case ',':
1274
-                    if (!$in_string) {
1275
-                        $new_json .= ",\n" . str_repeat($tab, $indent_level);
1276
-                    } else {
1277
-                        $new_json .= $char;
1278
-                    }
1279
-                    break;
1280
-                case ':':
1281
-                    if (!$in_string) {
1282
-                        $new_json .= ": ";
1283
-                    } else {
1284
-                        $new_json .= $char;
1285
-                    }
1286
-                    break;
1287
-                case '"':
1288
-                    if ($c == 0) {
1289
-                        $in_string = true;
1290
-                    } else if ($c > 0 && $json[$c - 1] != '\\') {
1291
-                        $in_string = !$in_string;
1292
-                    }
1293
-                default:
1294
-                    $new_json .= $char;
1295
-                    break;
1296
-            }
1297
-        }
1298
-
1299
-        return $new_json;
1300
-    }
1301
-
1302
-
1303
-    public function __toString()
1304
-    {
1305
-        return $this->getExtension();
1306
-    }
1155
+	const MIME = 'application/json,application/javascript';
1156
+
1157
+	static $mime = 'application/json';
1158
+
1159
+	const EXTENSION = 'json';
1160
+
1161
+
1162
+	public function getMIMEMap()
1163
+	{
1164
+		return array(self::EXTENSION => self::MIME);
1165
+	}
1166
+
1167
+
1168
+	public function getMIME()
1169
+	{
1170
+		return self::$mime;
1171
+	}
1172
+
1173
+
1174
+	public function getExtension()
1175
+	{
1176
+		return self::EXTENSION;
1177
+	}
1178
+
1179
+
1180
+	public function setMIME($mime)
1181
+	{
1182
+		self::$mime = $mime;
1183
+	}
1184
+
1185
+
1186
+	public function setExtension($extension)
1187
+	{
1188
+		//do nothing
1189
+	}
1190
+
1191
+
1192
+	public function encode($data, $human_readable = false)
1193
+	{
1194
+		return $human_readable ? 
1195
+			$this->json_format(json_encode(object_to_array($data))) : 
1196
+			json_encode(object_to_array($data));
1197
+	}
1198
+
1199
+
1200
+	public function decode($data)
1201
+	{
1202
+		$decoded = json_decode($data);
1203
+		if (function_exists('json_last_error')) {
1204
+			$message = '';
1205
+			switch (json_last_error()) {
1206
+				case JSON_ERROR_NONE:
1207
+					return object_to_array($decoded);
1208
+					break;
1209
+				case JSON_ERROR_DEPTH:
1210
+					$message = 'maximum stack depth exceeded';
1211
+					break;
1212
+				case JSON_ERROR_STATE_MISMATCH:
1213
+					$message = 'underflow or the modes mismatch';
1214
+					break;
1215
+				case JSON_ERROR_CTRL_CHAR:
1216
+					$message = 'unexpected control character found';
1217
+					break;
1218
+				case JSON_ERROR_SYNTAX:
1219
+					$message = 'malformed JSON';
1220
+					break;
1221
+				case JSON_ERROR_UTF8:
1222
+					$message = 'malformed UTF-8 characters, '.
1223
+						'possibly incorrectly encoded';
1224
+					break;
1225
+				default:
1226
+					$message = 'unknown error';
1227
+					break;
1228
+			}
1229
+			throw new RestException(400, 'Error parsing JSON, ' . $message);
1230
+		} else if (strlen($data) && $decoded === null || $decoded === $data) {
1231
+			throw new RestException(400, 'Error parsing JSON');
1232
+		}
1233
+		return object_to_array($decoded);
1234
+	}
1235
+
1236
+
1237
+	/**
1238
+	 * Pretty print JSON string
1239
+	 * @param string $json
1240
+	 * @return string formated json
1241
+	 */
1242
+	private function json_format($json)
1243
+	{
1244
+		$tab = "  ";
1245
+		$new_json = "";
1246
+		$indent_level = 0;
1247
+		$in_string = false;
1248
+		$len = strlen($json);
1249
+
1250
+		for ($c = 0; $c < $len; $c++) {
1251
+			$char = $json[$c];
1252
+			switch ($char) {
1253
+				case '{':
1254
+				case '[':
1255
+					if (!$in_string) {
1256
+						$new_json .= $char . "\n" .
1257
+							str_repeat($tab, $indent_level + 1);
1258
+						$indent_level++;
1259
+					} else {
1260
+						$new_json .= $char;
1261
+					}
1262
+					break;
1263
+				case '}':
1264
+				case ']':
1265
+					if (!$in_string) {
1266
+						$indent_level--;
1267
+						$new_json .= "\n" . str_repeat($tab, $indent_level) 
1268
+							. $char;
1269
+					} else {
1270
+						$new_json .= $char;
1271
+					}
1272
+					break;
1273
+				case ',':
1274
+					if (!$in_string) {
1275
+						$new_json .= ",\n" . str_repeat($tab, $indent_level);
1276
+					} else {
1277
+						$new_json .= $char;
1278
+					}
1279
+					break;
1280
+				case ':':
1281
+					if (!$in_string) {
1282
+						$new_json .= ": ";
1283
+					} else {
1284
+						$new_json .= $char;
1285
+					}
1286
+					break;
1287
+				case '"':
1288
+					if ($c == 0) {
1289
+						$in_string = true;
1290
+					} else if ($c > 0 && $json[$c - 1] != '\\') {
1291
+						$in_string = !$in_string;
1292
+					}
1293
+				default:
1294
+					$new_json .= $char;
1295
+					break;
1296
+			}
1297
+		}
1298
+
1299
+		return $new_json;
1300
+	}
1301
+
1302
+
1303
+	public function __toString()
1304
+	{
1305
+		return $this->getExtension();
1306
+	}
1307 1307
 
1308 1308
 }
1309 1309
 
@@ -1321,123 +1321,123 @@  discard block
 block discarded – undo
1321 1321
 class DocParser
1322 1322
 {
1323 1323
 
1324
-    private $params = array();
1325
-
1326
-
1327
-    public function parse($doc = '')
1328
-    {
1329
-        if ($doc == '') {
1330
-            return $this->params;
1331
-        }
1332
-        //Get the comment
1333
-        if (preg_match('#^/\*\*(.*)\*/#s', $doc, $comment) === false) {
1334
-            return $this->params;
1335
-        }
1336
-        $comment = trim($comment[1]);
1337
-        //Get all the lines and strip the * from the first character
1338
-        if (preg_match_all('#^\s*\*(.*)#m', $comment, $lines) === false) {
1339
-            return $this->params;
1340
-        }
1341
-        $this->parseLines($lines[1]);
1342
-        return $this->params;
1343
-    }
1344
-
1345
-
1346
-    private function parseLines($lines)
1347
-    {
1348
-        foreach ($lines as $line) {
1349
-            $parsedLine = $this->parseLine($line); //Parse the line
1350
-
1351
-            if ($parsedLine === false && !isset($this->params['description'])) {
1352
-                if (isset($desc)) {
1353
-                    //Store the first line in the short description
1354
-                    $this->params['description'] = implode(PHP_EOL, $desc);
1355
-                }
1356
-                $desc = array();
1357
-            } else if ($parsedLine !== false) {
1358
-                $desc[] = $parsedLine; //Store the line in the long description
1359
-            }
1360
-        }
1361
-        $desc = implode(' ', $desc);
1362
-        if (!empty($desc)) {
1363
-            $this->params['long_description'] = $desc;
1364
-        }
1365
-    }
1366
-
1367
-
1368
-    private function parseLine($line)
1369
-    {
1370
-        //trim the whitespace from the line
1371
-        $line = trim($line);
1372
-
1373
-        if (empty($line)) {
1374
-            return false; //Empty line
1375
-        }
1376
-
1377
-        if (strpos($line, '@') === 0) {
1378
-            if (strpos($line, ' ') > 0) {
1379
-                //Get the parameter name
1380
-                $param = substr($line, 1, strpos($line, ' ') - 1);
1381
-                $value = substr($line, strlen($param) + 2); //Get the value
1382
-            } else {
1383
-                $param = substr($line, 1);
1384
-                $value = '';
1385
-            }
1386
-            //Parse the line and return false if the parameter is valid
1387
-            if ($this->setParam($param, $value)) {
1388
-                return false;
1389
-            }
1390
-        }
1391
-        return $line;
1392
-    }
1393
-
1394
-
1395
-    private function setParam($param, $value)
1396
-    {
1397
-        if ($param == 'param' || $param == 'return') {
1398
-            $value = $this->formatParamOrReturn($value);
1399
-        }
1400
-        if ($param == 'class') {
1401
-            list($param, $value) = $this->formatClass($value);
1402
-        }
1403
-
1404
-        if (empty($this->params[$param])) {
1405
-            $this->params[$param] = $value;
1406
-        } else if ($param == 'param') {
1407
-            $arr = array($this->params[$param], $value);
1408
-            $this->params[$param] = $arr;
1409
-        } else {
1410
-            $this->params[$param] = $value + $this->params[$param];
1411
-        }
1412
-        return true;
1413
-    }
1414
-
1415
-
1416
-    private function formatClass($value)
1417
-    {
1418
-        $r = preg_split("[\(|\)]", $value);
1419
-        if (count($r) > 1) {
1420
-            $param = $r[0];
1421
-            parse_str($r[1], $value);
1422
-            foreach ($value as $key => $val) {
1423
-                $val = explode(',', $val);
1424
-                if (count($val) > 1) {
1425
-                    $value[$key] = $val;
1426
-                }
1427
-            }
1428
-        } else {
1429
-            $param = 'Unknown';
1430
-        }
1431
-        return array($param, $value);
1432
-    }
1433
-
1434
-
1435
-    private function formatParamOrReturn($string)
1436
-    {
1437
-        $pos = strpos($string, ' ');
1438
-        $type = substr($string, 0, $pos);
1439
-        return '(' . $type . ')' . substr($string, $pos + 1);
1440
-    }
1324
+	private $params = array();
1325
+
1326
+
1327
+	public function parse($doc = '')
1328
+	{
1329
+		if ($doc == '') {
1330
+			return $this->params;
1331
+		}
1332
+		//Get the comment
1333
+		if (preg_match('#^/\*\*(.*)\*/#s', $doc, $comment) === false) {
1334
+			return $this->params;
1335
+		}
1336
+		$comment = trim($comment[1]);
1337
+		//Get all the lines and strip the * from the first character
1338
+		if (preg_match_all('#^\s*\*(.*)#m', $comment, $lines) === false) {
1339
+			return $this->params;
1340
+		}
1341
+		$this->parseLines($lines[1]);
1342
+		return $this->params;
1343
+	}
1344
+
1345
+
1346
+	private function parseLines($lines)
1347
+	{
1348
+		foreach ($lines as $line) {
1349
+			$parsedLine = $this->parseLine($line); //Parse the line
1350
+
1351
+			if ($parsedLine === false && !isset($this->params['description'])) {
1352
+				if (isset($desc)) {
1353
+					//Store the first line in the short description
1354
+					$this->params['description'] = implode(PHP_EOL, $desc);
1355
+				}
1356
+				$desc = array();
1357
+			} else if ($parsedLine !== false) {
1358
+				$desc[] = $parsedLine; //Store the line in the long description
1359
+			}
1360
+		}
1361
+		$desc = implode(' ', $desc);
1362
+		if (!empty($desc)) {
1363
+			$this->params['long_description'] = $desc;
1364
+		}
1365
+	}
1366
+
1367
+
1368
+	private function parseLine($line)
1369
+	{
1370
+		//trim the whitespace from the line
1371
+		$line = trim($line);
1372
+
1373
+		if (empty($line)) {
1374
+			return false; //Empty line
1375
+		}
1376
+
1377
+		if (strpos($line, '@') === 0) {
1378
+			if (strpos($line, ' ') > 0) {
1379
+				//Get the parameter name
1380
+				$param = substr($line, 1, strpos($line, ' ') - 1);
1381
+				$value = substr($line, strlen($param) + 2); //Get the value
1382
+			} else {
1383
+				$param = substr($line, 1);
1384
+				$value = '';
1385
+			}
1386
+			//Parse the line and return false if the parameter is valid
1387
+			if ($this->setParam($param, $value)) {
1388
+				return false;
1389
+			}
1390
+		}
1391
+		return $line;
1392
+	}
1393
+
1394
+
1395
+	private function setParam($param, $value)
1396
+	{
1397
+		if ($param == 'param' || $param == 'return') {
1398
+			$value = $this->formatParamOrReturn($value);
1399
+		}
1400
+		if ($param == 'class') {
1401
+			list($param, $value) = $this->formatClass($value);
1402
+		}
1403
+
1404
+		if (empty($this->params[$param])) {
1405
+			$this->params[$param] = $value;
1406
+		} else if ($param == 'param') {
1407
+			$arr = array($this->params[$param], $value);
1408
+			$this->params[$param] = $arr;
1409
+		} else {
1410
+			$this->params[$param] = $value + $this->params[$param];
1411
+		}
1412
+		return true;
1413
+	}
1414
+
1415
+
1416
+	private function formatClass($value)
1417
+	{
1418
+		$r = preg_split("[\(|\)]", $value);
1419
+		if (count($r) > 1) {
1420
+			$param = $r[0];
1421
+			parse_str($r[1], $value);
1422
+			foreach ($value as $key => $val) {
1423
+				$val = explode(',', $val);
1424
+				if (count($val) > 1) {
1425
+					$value[$key] = $val;
1426
+				}
1427
+			}
1428
+		} else {
1429
+			$param = 'Unknown';
1430
+		}
1431
+		return array($param, $value);
1432
+	}
1433
+
1434
+
1435
+	private function formatParamOrReturn($string)
1436
+	{
1437
+		$pos = strpos($string, ' ');
1438
+		$type = substr($string, 0, $pos);
1439
+		return '(' . $type . ')' . substr($string, $pos + 1);
1440
+	}
1441 1441
 
1442 1442
 }
1443 1443
 
@@ -1450,30 +1450,30 @@  discard block
 block discarded – undo
1450 1450
 
1451 1451
 function parse_doc($php_doc_comment)
1452 1452
 {
1453
-    $p = new DocParser();
1454
-    return $p->parse($php_doc_comment);
1453
+	$p = new DocParser();
1454
+	return $p->parse($php_doc_comment);
1455 1455
 
1456
-    $p = new Parser($php_doc_comment);
1457
-    return $p;
1456
+	$p = new Parser($php_doc_comment);
1457
+	return $p;
1458 1458
 
1459
-    $php_doc_comment = preg_replace(
1460
-        "/(^[\\s]*\\/\\*\\*)
1459
+	$php_doc_comment = preg_replace(
1460
+		"/(^[\\s]*\\/\\*\\*)
1461 1461
         |(^[\\s]\\*\\/)
1462 1462
         |(^[\\s]*\\*?\\s)
1463 1463
         |(^[\\s]*)
1464 1464
         |(^[\\t]*)/ixm",
1465
-        "", $php_doc_comment);
1466
-    $php_doc_comment = str_replace("\r", "", $php_doc_comment);
1467
-    $php_doc_comment = preg_replace("/([\\t])+/", "\t", $php_doc_comment);
1468
-    return explode("\n", $php_doc_comment);
1469
-
1470
-    $php_doc_comment = trim(preg_replace('/\r?\n *\* */', ' ', 
1471
-            $php_doc_comment));
1472
-    return $php_doc_comment;
1473
-
1474
-    preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $php_doc_comment,
1475
-        $matches);
1476
-    return array_combine($matches[1], $matches[2]);
1465
+		"", $php_doc_comment);
1466
+	$php_doc_comment = str_replace("\r", "", $php_doc_comment);
1467
+	$php_doc_comment = preg_replace("/([\\t])+/", "\t", $php_doc_comment);
1468
+	return explode("\n", $php_doc_comment);
1469
+
1470
+	$php_doc_comment = trim(preg_replace('/\r?\n *\* */', ' ', 
1471
+			$php_doc_comment));
1472
+	return $php_doc_comment;
1473
+
1474
+	preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $php_doc_comment,
1475
+		$matches);
1476
+	return array_combine($matches[1], $matches[2]);
1477 1477
 }
1478 1478
 
1479 1479
 
@@ -1492,21 +1492,21 @@  discard block
 block discarded – undo
1492 1492
  */
1493 1493
 function object_to_array($object, $utf_encode = false)
1494 1494
 {
1495
-    if (is_array($object)
1496
-        || (is_object($object)
1497
-        && !($object instanceof JsonSerializable))
1498
-    ) {
1499
-        $array = array();
1500
-        foreach ($object as $key => $value) {
1501
-            $value = object_to_array($value, $utf_encode);
1502
-            if ($utf_encode && is_string($value)) {
1503
-                $value = utf8_encode($value);
1504
-            }
1505
-            $array[$key] = $value;
1506
-        }
1507
-        return $array;
1508
-    }
1509
-    return $object;
1495
+	if (is_array($object)
1496
+		|| (is_object($object)
1497
+		&& !($object instanceof JsonSerializable))
1498
+	) {
1499
+		$array = array();
1500
+		foreach ($object as $key => $value) {
1501
+			$value = object_to_array($value, $utf_encode);
1502
+			if ($utf_encode && is_string($value)) {
1503
+				$value = utf8_encode($value);
1504
+			}
1505
+			$array[$key] = $value;
1506
+		}
1507
+		return $array;
1508
+	}
1509
+	return $object;
1510 1510
 }
1511 1511
 
1512 1512
 
@@ -1516,21 +1516,21 @@  discard block
 block discarded – undo
1516 1516
  */
1517 1517
 function autoload_formats($class_name)
1518 1518
 {
1519
-    $class_name = strtolower($class_name);
1519
+	$class_name = strtolower($class_name);
1520 1520
 	
1521
-    $file = RESTLER_PATH . "../../../api/mobile_services/$class_name.php";
1522
-    if (file_exists($file)) {
1523
-        require_once ($file);
1524
-    } else {
1521
+	$file = RESTLER_PATH . "../../../api/mobile_services/$class_name.php";
1522
+	if (file_exists($file)) {
1523
+		require_once ($file);
1524
+	} else {
1525 1525
 		$file = RESTLER_PATH . "/../../api/mobile_services/$class_name.php";
1526
-        if (file_exists($file)) {
1527
-            require_once ($file);
1528
-        } elseif (file_exists(RESTLER_PATH . "/../api/mobile_services/$class_name.php")) {
1529
-            require_once ("/../api/mobile_services/$class_name.php");
1530
-        } elseif (file_exists("$class_name.php")) {
1531
-            require_once ("$class_name.php");
1532
-        }
1533
-    }
1526
+		if (file_exists($file)) {
1527
+			require_once ($file);
1528
+		} elseif (file_exists(RESTLER_PATH . "/../api/mobile_services/$class_name.php")) {
1529
+			require_once ("/../api/mobile_services/$class_name.php");
1530
+		} elseif (file_exists("$class_name.php")) {
1531
+			require_once ("$class_name.php");
1532
+		}
1533
+	}
1534 1534
 }
1535 1535
 
1536 1536
 // ==================================================================
@@ -1547,10 +1547,10 @@  discard block
 block discarded – undo
1547 1547
 if (!function_exists('isRestlerCompatibilityModeEnabled')) {
1548 1548
 
1549 1549
 
1550
-    function isRestlerCompatibilityModeEnabled()
1551
-    {
1552
-        return false;
1553
-    }
1550
+	function isRestlerCompatibilityModeEnabled()
1551
+	{
1552
+		return false;
1553
+	}
1554 1554
 
1555 1555
 }
1556 1556
 define('RESTLER_PATH', dirname(__FILE__));
Please login to merge, or discard this patch.
libraries/SabreDAV/VObject/Parser/MimeDir.php 1 patch
Indentation   +645 added lines, -645 removed lines patch added patch discarded remove patch
@@ -24,328 +24,328 @@  discard block
 block discarded – undo
24 24
  */
25 25
 class MimeDir extends Parser {
26 26
 
27
-    /**
28
-     * The input stream.
29
-     *
30
-     * @var resource
31
-     */
32
-    protected $input;
33
-
34
-    /**
35
-     * Root component.
36
-     *
37
-     * @var Component
38
-     */
39
-    protected $root;
40
-
41
-    /**
42
-     * By default all input will be assumed to be UTF-8.
43
-     *
44
-     * However, both iCalendar and vCard might be encoded using different
45
-     * character sets. The character set is usually set in the mime-type.
46
-     *
47
-     * If this is the case, use setEncoding to specify that a different
48
-     * encoding will be used. If this is set, the parser will automatically
49
-     * convert all incoming data to UTF-8.
50
-     *
51
-     * @var string
52
-     */
53
-    protected $charset = 'UTF-8';
54
-
55
-    /**
56
-     * The list of character sets we support when decoding.
57
-     *
58
-     * This would be a const expression but for now we need to support PHP 5.5
59
-     */
60
-    protected static $SUPPORTED_CHARSETS = [
61
-        'UTF-8',
62
-        'ISO-8859-1',
63
-        'Windows-1252',
64
-    ];
65
-
66
-    /**
67
-     * Parses an iCalendar or vCard file.
68
-     *
69
-     * Pass a stream or a string. If null is parsed, the existing buffer is
70
-     * used.
71
-     *
72
-     * @param string|resource|null $input
73
-     * @param int $options
74
-     *
75
-     * @return Sabre\VObject\Document
76
-     */
77
-    public function parse($input = null, $options = 0) {
78
-
79
-        $this->root = null;
80
-
81
-        if (!is_null($input)) {
82
-            $this->setInput($input);
83
-        }
84
-
85
-        if (0 !== $options) {
86
-            $this->options = $options;
87
-        }
88
-
89
-        $this->parseDocument();
90
-
91
-        return $this->root;
92
-
93
-    }
94
-
95
-    /**
96
-     * By default all input will be assumed to be UTF-8.
97
-     *
98
-     * However, both iCalendar and vCard might be encoded using different
99
-     * character sets. The character set is usually set in the mime-type.
100
-     *
101
-     * If this is the case, use setEncoding to specify that a different
102
-     * encoding will be used. If this is set, the parser will automatically
103
-     * convert all incoming data to UTF-8.
104
-     *
105
-     * @param string $charset
106
-     */
107
-    public function setCharset($charset) {
108
-
109
-        if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
110
-            throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: ' . implode(', ', self::$SUPPORTED_CHARSETS) . ')');
111
-        }
112
-        $this->charset = $charset;
113
-
114
-    }
115
-
116
-    /**
117
-     * Sets the input buffer. Must be a string or stream.
118
-     *
119
-     * @param resource|string $input
120
-     *
121
-     * @return void
122
-     */
123
-    public function setInput($input) {
124
-
125
-        // Resetting the parser
126
-        $this->lineIndex = 0;
127
-        $this->startLine = 0;
128
-
129
-        if (is_string($input)) {
130
-            // Convering to a stream.
131
-            $stream = fopen('php://temp', 'r+');
132
-            fwrite($stream, $input);
133
-            rewind($stream);
134
-            $this->input = $stream;
135
-        } elseif (is_resource($input)) {
136
-            $this->input = $input;
137
-        } else {
138
-            throw new \InvalidArgumentException('This parser can only read from strings or streams.');
139
-        }
140
-
141
-    }
142
-
143
-    /**
144
-     * Parses an entire document.
145
-     *
146
-     * @return void
147
-     */
148
-    protected function parseDocument() {
149
-
150
-        $line = $this->readLine();
151
-
152
-        // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
153
-        // It's 0xEF 0xBB 0xBF in UTF-8 hex.
154
-        if (3 <= strlen($line)
155
-            && ord($line[0]) === 0xef
156
-            && ord($line[1]) === 0xbb
157
-            && ord($line[2]) === 0xbf) {
158
-            $line = substr($line, 3);
159
-        }
160
-
161
-        switch (strtoupper($line)) {
162
-            case 'BEGIN:VCALENDAR' :
163
-                $class = VCalendar::$componentMap['VCALENDAR'];
164
-                break;
165
-            case 'BEGIN:VCARD' :
166
-                $class = VCard::$componentMap['VCARD'];
167
-                break;
168
-            default :
169
-                throw new ParseException('This parser only supports VCARD and VCALENDAR files');
170
-        }
171
-
172
-        $this->root = new $class([], false);
173
-
174
-        while (true) {
175
-
176
-            // Reading until we hit END:
177
-            $line = $this->readLine();
178
-            if (strtoupper(substr($line, 0, 4)) === 'END:') {
179
-                break;
180
-            }
181
-            $result = $this->parseLine($line);
182
-            if ($result) {
183
-                $this->root->add($result);
184
-            }
185
-
186
-        }
187
-
188
-        $name = strtoupper(substr($line, 4));
189
-        if ($name !== $this->root->name) {
190
-            throw new ParseException('Invalid MimeDir file. expected: "END:' . $this->root->name . '" got: "END:' . $name . '"');
191
-        }
192
-
193
-    }
194
-
195
-    /**
196
-     * Parses a line, and if it hits a component, it will also attempt to parse
197
-     * the entire component.
198
-     *
199
-     * @param string $line Unfolded line
200
-     *
201
-     * @return Node
202
-     */
203
-    protected function parseLine($line) {
204
-
205
-        // Start of a new component
206
-        if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') {
207
-
208
-            $component = $this->root->createComponent(substr($line, 6), [], false);
209
-
210
-            while (true) {
211
-
212
-                // Reading until we hit END:
213
-                $line = $this->readLine();
214
-                if (strtoupper(substr($line, 0, 4)) === 'END:') {
215
-                    break;
216
-                }
217
-                $result = $this->parseLine($line);
218
-                if ($result) {
219
-                    $component->add($result);
220
-                }
221
-
222
-            }
223
-
224
-            $name = strtoupper(substr($line, 4));
225
-            if ($name !== $component->name) {
226
-                throw new ParseException('Invalid MimeDir file. expected: "END:' . $component->name . '" got: "END:' . $name . '"');
227
-            }
228
-
229
-            return $component;
230
-
231
-        } else {
232
-
233
-            // Property reader
234
-            $property = $this->readProperty($line);
235
-            if (!$property) {
236
-                // Ignored line
237
-                return false;
238
-            }
239
-            return $property;
240
-
241
-        }
242
-
243
-    }
244
-
245
-    /**
246
-     * We need to look ahead 1 line every time to see if we need to 'unfold'
247
-     * the next line.
248
-     *
249
-     * If that was not the case, we store it here.
250
-     *
251
-     * @var null|string
252
-     */
253
-    protected $lineBuffer;
254
-
255
-    /**
256
-     * The real current line number.
257
-     */
258
-    protected $lineIndex = 0;
259
-
260
-    /**
261
-     * In the case of unfolded lines, this property holds the line number for
262
-     * the start of the line.
263
-     *
264
-     * @var int
265
-     */
266
-    protected $startLine = 0;
267
-
268
-    /**
269
-     * Contains a 'raw' representation of the current line.
270
-     *
271
-     * @var string
272
-     */
273
-    protected $rawLine;
274
-
275
-    /**
276
-     * Reads a single line from the buffer.
277
-     *
278
-     * This method strips any newlines and also takes care of unfolding.
279
-     *
280
-     * @throws \Sabre\VObject\EofException
281
-     *
282
-     * @return string
283
-     */
284
-    protected function readLine() {
285
-
286
-        if (!is_null($this->lineBuffer)) {
287
-            $rawLine = $this->lineBuffer;
288
-            $this->lineBuffer = null;
289
-        } else {
290
-            do {
291
-                $eof = feof($this->input);
292
-
293
-                $rawLine = fgets($this->input);
294
-
295
-                if ($eof || (feof($this->input) && $rawLine === false)) {
296
-                    throw new EofException('End of document reached prematurely');
297
-                }
298
-                if ($rawLine === false) {
299
-                    throw new ParseException('Error reading from input stream');
300
-                }
301
-                $rawLine = rtrim($rawLine, "\r\n");
302
-            } while ($rawLine === ''); // Skipping empty lines
303
-            $this->lineIndex++;
304
-        }
305
-        $line = $rawLine;
306
-
307
-        $this->startLine = $this->lineIndex;
308
-
309
-        // Looking ahead for folded lines.
310
-        while (true) {
311
-
312
-            $nextLine = rtrim(fgets($this->input), "\r\n");
313
-            $this->lineIndex++;
314
-            if (!$nextLine) {
315
-                break;
316
-            }
317
-            if ($nextLine[0] === "\t" || $nextLine[0] === " ") {
318
-                $line .= substr($nextLine, 1);
319
-                $rawLine .= "\n " . substr($nextLine, 1);
320
-            } else {
321
-                $this->lineBuffer = $nextLine;
322
-                break;
323
-            }
324
-
325
-        }
326
-        $this->rawLine = $rawLine;
327
-        return $line;
328
-
329
-    }
330
-
331
-    /**
332
-     * Reads a property or component from a line.
333
-     *
334
-     * @return void
335
-     */
336
-    protected function readProperty($line) {
337
-
338
-        if ($this->options & self::OPTION_FORGIVING) {
339
-            $propNameToken = 'A-Z0-9\-\._\\/';
340
-        } else {
341
-            $propNameToken = 'A-Z0-9\-\.';
342
-        }
343
-
344
-        $paramNameToken = 'A-Z0-9\-';
345
-        $safeChar = '^";:,';
346
-        $qSafeChar = '^"';
347
-
348
-        $regex = "/
27
+	/**
28
+	 * The input stream.
29
+	 *
30
+	 * @var resource
31
+	 */
32
+	protected $input;
33
+
34
+	/**
35
+	 * Root component.
36
+	 *
37
+	 * @var Component
38
+	 */
39
+	protected $root;
40
+
41
+	/**
42
+	 * By default all input will be assumed to be UTF-8.
43
+	 *
44
+	 * However, both iCalendar and vCard might be encoded using different
45
+	 * character sets. The character set is usually set in the mime-type.
46
+	 *
47
+	 * If this is the case, use setEncoding to specify that a different
48
+	 * encoding will be used. If this is set, the parser will automatically
49
+	 * convert all incoming data to UTF-8.
50
+	 *
51
+	 * @var string
52
+	 */
53
+	protected $charset = 'UTF-8';
54
+
55
+	/**
56
+	 * The list of character sets we support when decoding.
57
+	 *
58
+	 * This would be a const expression but for now we need to support PHP 5.5
59
+	 */
60
+	protected static $SUPPORTED_CHARSETS = [
61
+		'UTF-8',
62
+		'ISO-8859-1',
63
+		'Windows-1252',
64
+	];
65
+
66
+	/**
67
+	 * Parses an iCalendar or vCard file.
68
+	 *
69
+	 * Pass a stream or a string. If null is parsed, the existing buffer is
70
+	 * used.
71
+	 *
72
+	 * @param string|resource|null $input
73
+	 * @param int $options
74
+	 *
75
+	 * @return Sabre\VObject\Document
76
+	 */
77
+	public function parse($input = null, $options = 0) {
78
+
79
+		$this->root = null;
80
+
81
+		if (!is_null($input)) {
82
+			$this->setInput($input);
83
+		}
84
+
85
+		if (0 !== $options) {
86
+			$this->options = $options;
87
+		}
88
+
89
+		$this->parseDocument();
90
+
91
+		return $this->root;
92
+
93
+	}
94
+
95
+	/**
96
+	 * By default all input will be assumed to be UTF-8.
97
+	 *
98
+	 * However, both iCalendar and vCard might be encoded using different
99
+	 * character sets. The character set is usually set in the mime-type.
100
+	 *
101
+	 * If this is the case, use setEncoding to specify that a different
102
+	 * encoding will be used. If this is set, the parser will automatically
103
+	 * convert all incoming data to UTF-8.
104
+	 *
105
+	 * @param string $charset
106
+	 */
107
+	public function setCharset($charset) {
108
+
109
+		if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
110
+			throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: ' . implode(', ', self::$SUPPORTED_CHARSETS) . ')');
111
+		}
112
+		$this->charset = $charset;
113
+
114
+	}
115
+
116
+	/**
117
+	 * Sets the input buffer. Must be a string or stream.
118
+	 *
119
+	 * @param resource|string $input
120
+	 *
121
+	 * @return void
122
+	 */
123
+	public function setInput($input) {
124
+
125
+		// Resetting the parser
126
+		$this->lineIndex = 0;
127
+		$this->startLine = 0;
128
+
129
+		if (is_string($input)) {
130
+			// Convering to a stream.
131
+			$stream = fopen('php://temp', 'r+');
132
+			fwrite($stream, $input);
133
+			rewind($stream);
134
+			$this->input = $stream;
135
+		} elseif (is_resource($input)) {
136
+			$this->input = $input;
137
+		} else {
138
+			throw new \InvalidArgumentException('This parser can only read from strings or streams.');
139
+		}
140
+
141
+	}
142
+
143
+	/**
144
+	 * Parses an entire document.
145
+	 *
146
+	 * @return void
147
+	 */
148
+	protected function parseDocument() {
149
+
150
+		$line = $this->readLine();
151
+
152
+		// BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
153
+		// It's 0xEF 0xBB 0xBF in UTF-8 hex.
154
+		if (3 <= strlen($line)
155
+			&& ord($line[0]) === 0xef
156
+			&& ord($line[1]) === 0xbb
157
+			&& ord($line[2]) === 0xbf) {
158
+			$line = substr($line, 3);
159
+		}
160
+
161
+		switch (strtoupper($line)) {
162
+			case 'BEGIN:VCALENDAR' :
163
+				$class = VCalendar::$componentMap['VCALENDAR'];
164
+				break;
165
+			case 'BEGIN:VCARD' :
166
+				$class = VCard::$componentMap['VCARD'];
167
+				break;
168
+			default :
169
+				throw new ParseException('This parser only supports VCARD and VCALENDAR files');
170
+		}
171
+
172
+		$this->root = new $class([], false);
173
+
174
+		while (true) {
175
+
176
+			// Reading until we hit END:
177
+			$line = $this->readLine();
178
+			if (strtoupper(substr($line, 0, 4)) === 'END:') {
179
+				break;
180
+			}
181
+			$result = $this->parseLine($line);
182
+			if ($result) {
183
+				$this->root->add($result);
184
+			}
185
+
186
+		}
187
+
188
+		$name = strtoupper(substr($line, 4));
189
+		if ($name !== $this->root->name) {
190
+			throw new ParseException('Invalid MimeDir file. expected: "END:' . $this->root->name . '" got: "END:' . $name . '"');
191
+		}
192
+
193
+	}
194
+
195
+	/**
196
+	 * Parses a line, and if it hits a component, it will also attempt to parse
197
+	 * the entire component.
198
+	 *
199
+	 * @param string $line Unfolded line
200
+	 *
201
+	 * @return Node
202
+	 */
203
+	protected function parseLine($line) {
204
+
205
+		// Start of a new component
206
+		if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') {
207
+
208
+			$component = $this->root->createComponent(substr($line, 6), [], false);
209
+
210
+			while (true) {
211
+
212
+				// Reading until we hit END:
213
+				$line = $this->readLine();
214
+				if (strtoupper(substr($line, 0, 4)) === 'END:') {
215
+					break;
216
+				}
217
+				$result = $this->parseLine($line);
218
+				if ($result) {
219
+					$component->add($result);
220
+				}
221
+
222
+			}
223
+
224
+			$name = strtoupper(substr($line, 4));
225
+			if ($name !== $component->name) {
226
+				throw new ParseException('Invalid MimeDir file. expected: "END:' . $component->name . '" got: "END:' . $name . '"');
227
+			}
228
+
229
+			return $component;
230
+
231
+		} else {
232
+
233
+			// Property reader
234
+			$property = $this->readProperty($line);
235
+			if (!$property) {
236
+				// Ignored line
237
+				return false;
238
+			}
239
+			return $property;
240
+
241
+		}
242
+
243
+	}
244
+
245
+	/**
246
+	 * We need to look ahead 1 line every time to see if we need to 'unfold'
247
+	 * the next line.
248
+	 *
249
+	 * If that was not the case, we store it here.
250
+	 *
251
+	 * @var null|string
252
+	 */
253
+	protected $lineBuffer;
254
+
255
+	/**
256
+	 * The real current line number.
257
+	 */
258
+	protected $lineIndex = 0;
259
+
260
+	/**
261
+	 * In the case of unfolded lines, this property holds the line number for
262
+	 * the start of the line.
263
+	 *
264
+	 * @var int
265
+	 */
266
+	protected $startLine = 0;
267
+
268
+	/**
269
+	 * Contains a 'raw' representation of the current line.
270
+	 *
271
+	 * @var string
272
+	 */
273
+	protected $rawLine;
274
+
275
+	/**
276
+	 * Reads a single line from the buffer.
277
+	 *
278
+	 * This method strips any newlines and also takes care of unfolding.
279
+	 *
280
+	 * @throws \Sabre\VObject\EofException
281
+	 *
282
+	 * @return string
283
+	 */
284
+	protected function readLine() {
285
+
286
+		if (!is_null($this->lineBuffer)) {
287
+			$rawLine = $this->lineBuffer;
288
+			$this->lineBuffer = null;
289
+		} else {
290
+			do {
291
+				$eof = feof($this->input);
292
+
293
+				$rawLine = fgets($this->input);
294
+
295
+				if ($eof || (feof($this->input) && $rawLine === false)) {
296
+					throw new EofException('End of document reached prematurely');
297
+				}
298
+				if ($rawLine === false) {
299
+					throw new ParseException('Error reading from input stream');
300
+				}
301
+				$rawLine = rtrim($rawLine, "\r\n");
302
+			} while ($rawLine === ''); // Skipping empty lines
303
+			$this->lineIndex++;
304
+		}
305
+		$line = $rawLine;
306
+
307
+		$this->startLine = $this->lineIndex;
308
+
309
+		// Looking ahead for folded lines.
310
+		while (true) {
311
+
312
+			$nextLine = rtrim(fgets($this->input), "\r\n");
313
+			$this->lineIndex++;
314
+			if (!$nextLine) {
315
+				break;
316
+			}
317
+			if ($nextLine[0] === "\t" || $nextLine[0] === " ") {
318
+				$line .= substr($nextLine, 1);
319
+				$rawLine .= "\n " . substr($nextLine, 1);
320
+			} else {
321
+				$this->lineBuffer = $nextLine;
322
+				break;
323
+			}
324
+
325
+		}
326
+		$this->rawLine = $rawLine;
327
+		return $line;
328
+
329
+	}
330
+
331
+	/**
332
+	 * Reads a property or component from a line.
333
+	 *
334
+	 * @return void
335
+	 */
336
+	protected function readProperty($line) {
337
+
338
+		if ($this->options & self::OPTION_FORGIVING) {
339
+			$propNameToken = 'A-Z0-9\-\._\\/';
340
+		} else {
341
+			$propNameToken = 'A-Z0-9\-\.';
342
+		}
343
+
344
+		$paramNameToken = 'A-Z0-9\-';
345
+		$safeChar = '^";:,';
346
+		$qSafeChar = '^"';
347
+
348
+		$regex = "/
349 349
             ^(?P<name> [$propNameToken]+ ) (?=[;:])        # property name
350 350
             |
351 351
             (?<=:)(?P<propValue> .+)$                      # property value
@@ -358,338 +358,338 @@  discard block
 block discarded – undo
358 358
             ) (?=[;:,])
359 359
             /xi";
360 360
 
361
-        preg_match_all($regex, $line, $matches,  PREG_SET_ORDER);
362
-
363
-        $property = [
364
-            'name'       => null,
365
-            'parameters' => [],
366
-            'value'      => null
367
-        ];
368
-
369
-        $lastParam = null;
370
-
371
-        /**
372
-         * Looping through all the tokens.
373
-         *
374
-         * Note that we are looping through them in reverse order, because if a
375
-         * sub-pattern matched, the subsequent named patterns will not show up
376
-         * in the result.
377
-         */
378
-        foreach ($matches as $match) {
379
-
380
-            if (isset($match['paramValue'])) {
381
-                if ($match['paramValue'] && $match['paramValue'][0] === '"') {
382
-                    $value = substr($match['paramValue'], 1, -1);
383
-                } else {
384
-                    $value = $match['paramValue'];
385
-                }
386
-
387
-                $value = $this->unescapeParam($value);
388
-
389
-                if (is_null($lastParam)) {
390
-                    throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
391
-                }
392
-                if (is_null($property['parameters'][$lastParam])) {
393
-                    $property['parameters'][$lastParam] = $value;
394
-                } elseif (is_array($property['parameters'][$lastParam])) {
395
-                    $property['parameters'][$lastParam][] = $value;
396
-                } else {
397
-                    $property['parameters'][$lastParam] = [
398
-                        $property['parameters'][$lastParam],
399
-                        $value
400
-                    ];
401
-                }
402
-                continue;
403
-            }
404
-            if (isset($match['paramName'])) {
405
-                $lastParam = strtoupper($match['paramName']);
406
-                if (!isset($property['parameters'][$lastParam])) {
407
-                    $property['parameters'][$lastParam] = null;
408
-                }
409
-                continue;
410
-            }
411
-            if (isset($match['propValue'])) {
412
-                $property['value'] = $match['propValue'];
413
-                continue;
414
-            }
415
-            if (isset($match['name']) && $match['name']) {
416
-                $property['name'] = strtoupper($match['name']);
417
-                continue;
418
-            }
419
-
420
-            // @codeCoverageIgnoreStart
421
-            throw new \LogicException('This code should not be reachable');
422
-            // @codeCoverageIgnoreEnd
423
-
424
-        }
425
-
426
-        if (is_null($property['value'])) {
427
-            $property['value'] = '';
428
-        }
429
-        if (!$property['name']) {
430
-            if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
431
-                return false;
432
-            }
433
-            throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
434
-        }
435
-
436
-        // vCard 2.1 states that parameters may appear without a name, and only
437
-        // a value. We can deduce the value based on it's name.
438
-        //
439
-        // Our parser will get those as parameters without a value instead, so
440
-        // we're filtering these parameters out first.
441
-        $namedParameters = [];
442
-        $namelessParameters = [];
443
-
444
-        foreach ($property['parameters'] as $name => $value) {
445
-            if (!is_null($value)) {
446
-                $namedParameters[$name] = $value;
447
-            } else {
448
-                $namelessParameters[] = $name;
449
-            }
450
-        }
451
-
452
-        $propObj = $this->root->createProperty($property['name'], null, $namedParameters);
453
-
454
-        foreach ($namelessParameters as $namelessParameter) {
455
-            $propObj->add(null, $namelessParameter);
456
-        }
457
-
458
-        if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') {
459
-            $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
460
-        } else {
461
-            $charset = $this->charset;
462
-            if ($this->root->getDocumentType() === Document::VCARD21 && isset($propObj['CHARSET'])) {
463
-                // vCard 2.1 allows the character set to be specified per property.
464
-                $charset = (string)$propObj['CHARSET'];
465
-            }
466
-            switch ($charset) {
467
-                case 'UTF-8' :
468
-                    break;
469
-                case 'ISO-8859-1' :
470
-                    $property['value'] = utf8_encode($property['value']);
471
-                    break;
472
-                case 'Windows-1252' :
473
-                    $property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset);
474
-                    break;
475
-                default :
476
-                    throw new ParseException('Unsupported CHARSET: ' . $propObj['CHARSET']);
477
-            }
478
-            $propObj->setRawMimeDirValue($property['value']);
479
-        }
480
-
481
-        return $propObj;
482
-
483
-    }
484
-
485
-    /**
486
-     * Unescapes a property value.
487
-     *
488
-     * vCard 2.1 says:
489
-     *   * Semi-colons must be escaped in some property values, specifically
490
-     *     ADR, ORG and N.
491
-     *   * Semi-colons must be escaped in parameter values, because semi-colons
492
-     *     are also use to separate values.
493
-     *   * No mention of escaping backslashes with another backslash.
494
-     *   * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
495
-     *     span values over more than 1 line.
496
-     *
497
-     * vCard 3.0 says:
498
-     *   * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
499
-     *     escaped, all time time.
500
-     *   * Comma's are used for delimeters in multiple values
501
-     *   * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
502
-     *     as in some properties semi-colon is used for separators.
503
-     *   * Properties using semi-colons: N, ADR, GEO, ORG
504
-     *   * Both ADR and N's individual parts may be broken up further with a
505
-     *     comma.
506
-     *   * Properties using commas: NICKNAME, CATEGORIES
507
-     *
508
-     * vCard 4.0 (rfc6350) says:
509
-     *   * Commas must be escaped.
510
-     *   * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
511
-     *     delimiter, depending on the property.
512
-     *   * Backslashes must be escaped
513
-     *   * Newlines must be escaped as either \N or \n.
514
-     *   * Some compound properties may contain multiple parts themselves, so a
515
-     *     comma within a semi-colon delimited property may also be unescaped
516
-     *     to denote multiple parts _within_ the compound property.
517
-     *   * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
518
-     *   * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
519
-     *
520
-     * Even though the spec says that commas must always be escaped, the
521
-     * example for GEO in Section 6.5.2 seems to violate this.
522
-     *
523
-     * iCalendar 2.0 (rfc5545) says:
524
-     *   * Commas or semi-colons may be used as delimiters, depending on the
525
-     *     property.
526
-     *   * Commas, semi-colons, backslashes, newline (\N or \n) are always
527
-     *     escaped, unless they are delimiters.
528
-     *   * Colons shall not be escaped.
529
-     *   * Commas can be considered the 'default delimiter' and is described as
530
-     *     the delimiter in cases where the order of the multiple values is
531
-     *     insignificant.
532
-     *   * Semi-colons are described as the delimiter for 'structured values'.
533
-     *     They are specifically used in Semi-colons are used as a delimiter in
534
-     *     REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
535
-     *
536
-     * Now for the parameters
537
-     *
538
-     * If delimiter is not set (null) this method will just return a string.
539
-     * If it's a comma or a semi-colon the string will be split on those
540
-     * characters, and always return an array.
541
-     *
542
-     * @param string $input
543
-     * @param string $delimiter
544
-     *
545
-     * @return string|string[]
546
-     */
547
-    static function unescapeValue($input, $delimiter = ';') {
548
-
549
-        $regex = '#  (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
550
-        if ($delimiter) {
551
-            $regex .= ' | (' . $delimiter . ')';
552
-        }
553
-        $regex .= ') #x';
554
-
555
-        $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
556
-
557
-        $resultArray = [];
558
-        $result = '';
559
-
560
-        foreach ($matches as $match) {
561
-
562
-            switch ($match) {
563
-                case '\\\\' :
564
-                    $result .= '\\';
565
-                    break;
566
-                case '\N' :
567
-                case '\n' :
568
-                    $result .= "\n";
569
-                    break;
570
-                case '\;' :
571
-                    $result .= ';';
572
-                    break;
573
-                case '\,' :
574
-                    $result .= ',';
575
-                    break;
576
-                case $delimiter :
577
-                    $resultArray[] = $result;
578
-                    $result = '';
579
-                    break;
580
-                default :
581
-                    $result .= $match;
582
-                    break;
583
-
584
-            }
585
-
586
-        }
587
-
588
-        $resultArray[] = $result;
589
-        return $delimiter ? $resultArray : $result;
590
-
591
-    }
592
-
593
-    /**
594
-     * Unescapes a parameter value.
595
-     *
596
-     * vCard 2.1:
597
-     *   * Does not mention a mechanism for this. In addition, double quotes
598
-     *     are never used to wrap values.
599
-     *   * This means that parameters can simply not contain colons or
600
-     *     semi-colons.
601
-     *
602
-     * vCard 3.0 (rfc2425, rfc2426):
603
-     *   * Parameters _may_ be surrounded by double quotes.
604
-     *   * If this is not the case, semi-colon, colon and comma may simply not
605
-     *     occur (the comma used for multiple parameter values though).
606
-     *   * If it is surrounded by double-quotes, it may simply not contain
607
-     *     double-quotes.
608
-     *   * This means that a parameter can in no case encode double-quotes, or
609
-     *     newlines.
610
-     *
611
-     * vCard 4.0 (rfc6350)
612
-     *   * Behavior seems to be identical to vCard 3.0
613
-     *
614
-     * iCalendar 2.0 (rfc5545)
615
-     *   * Behavior seems to be identical to vCard 3.0
616
-     *
617
-     * Parameter escaping mechanism (rfc6868) :
618
-     *   * This rfc describes a new way to escape parameter values.
619
-     *   * New-line is encoded as ^n
620
-     *   * ^ is encoded as ^^.
621
-     *   * " is encoded as ^'
622
-     *
623
-     * @param string $input
624
-     *
625
-     * @return void
626
-     */
627
-    private function unescapeParam($input) {
628
-
629
-        return
630
-            preg_replace_callback(
631
-                '#(\^(\^|n|\'))#',
632
-                function($matches) {
633
-                    switch ($matches[2]) {
634
-                        case 'n' :
635
-                            return "\n";
636
-                        case '^' :
637
-                            return '^';
638
-                        case '\'' :
639
-                            return '"';
640
-
641
-                    // @codeCoverageIgnoreStart
642
-                    }
643
-                    // @codeCoverageIgnoreEnd
644
-                },
645
-                $input
646
-            );
647
-    }
648
-
649
-    /**
650
-     * Gets the full quoted printable value.
651
-     *
652
-     * We need a special method for this, because newlines have both a meaning
653
-     * in vCards, and in QuotedPrintable.
654
-     *
655
-     * This method does not do any decoding.
656
-     *
657
-     * @return string
658
-     */
659
-    private function extractQuotedPrintableValue() {
660
-
661
-        // We need to parse the raw line again to get the start of the value.
662
-        //
663
-        // We are basically looking for the first colon (:), but we need to
664
-        // skip over the parameters first, as they may contain one.
665
-        $regex = '/^
361
+		preg_match_all($regex, $line, $matches,  PREG_SET_ORDER);
362
+
363
+		$property = [
364
+			'name'       => null,
365
+			'parameters' => [],
366
+			'value'      => null
367
+		];
368
+
369
+		$lastParam = null;
370
+
371
+		/**
372
+		 * Looping through all the tokens.
373
+		 *
374
+		 * Note that we are looping through them in reverse order, because if a
375
+		 * sub-pattern matched, the subsequent named patterns will not show up
376
+		 * in the result.
377
+		 */
378
+		foreach ($matches as $match) {
379
+
380
+			if (isset($match['paramValue'])) {
381
+				if ($match['paramValue'] && $match['paramValue'][0] === '"') {
382
+					$value = substr($match['paramValue'], 1, -1);
383
+				} else {
384
+					$value = $match['paramValue'];
385
+				}
386
+
387
+				$value = $this->unescapeParam($value);
388
+
389
+				if (is_null($lastParam)) {
390
+					throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
391
+				}
392
+				if (is_null($property['parameters'][$lastParam])) {
393
+					$property['parameters'][$lastParam] = $value;
394
+				} elseif (is_array($property['parameters'][$lastParam])) {
395
+					$property['parameters'][$lastParam][] = $value;
396
+				} else {
397
+					$property['parameters'][$lastParam] = [
398
+						$property['parameters'][$lastParam],
399
+						$value
400
+					];
401
+				}
402
+				continue;
403
+			}
404
+			if (isset($match['paramName'])) {
405
+				$lastParam = strtoupper($match['paramName']);
406
+				if (!isset($property['parameters'][$lastParam])) {
407
+					$property['parameters'][$lastParam] = null;
408
+				}
409
+				continue;
410
+			}
411
+			if (isset($match['propValue'])) {
412
+				$property['value'] = $match['propValue'];
413
+				continue;
414
+			}
415
+			if (isset($match['name']) && $match['name']) {
416
+				$property['name'] = strtoupper($match['name']);
417
+				continue;
418
+			}
419
+
420
+			// @codeCoverageIgnoreStart
421
+			throw new \LogicException('This code should not be reachable');
422
+			// @codeCoverageIgnoreEnd
423
+
424
+		}
425
+
426
+		if (is_null($property['value'])) {
427
+			$property['value'] = '';
428
+		}
429
+		if (!$property['name']) {
430
+			if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
431
+				return false;
432
+			}
433
+			throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
434
+		}
435
+
436
+		// vCard 2.1 states that parameters may appear without a name, and only
437
+		// a value. We can deduce the value based on it's name.
438
+		//
439
+		// Our parser will get those as parameters without a value instead, so
440
+		// we're filtering these parameters out first.
441
+		$namedParameters = [];
442
+		$namelessParameters = [];
443
+
444
+		foreach ($property['parameters'] as $name => $value) {
445
+			if (!is_null($value)) {
446
+				$namedParameters[$name] = $value;
447
+			} else {
448
+				$namelessParameters[] = $name;
449
+			}
450
+		}
451
+
452
+		$propObj = $this->root->createProperty($property['name'], null, $namedParameters);
453
+
454
+		foreach ($namelessParameters as $namelessParameter) {
455
+			$propObj->add(null, $namelessParameter);
456
+		}
457
+
458
+		if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') {
459
+			$propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
460
+		} else {
461
+			$charset = $this->charset;
462
+			if ($this->root->getDocumentType() === Document::VCARD21 && isset($propObj['CHARSET'])) {
463
+				// vCard 2.1 allows the character set to be specified per property.
464
+				$charset = (string)$propObj['CHARSET'];
465
+			}
466
+			switch ($charset) {
467
+				case 'UTF-8' :
468
+					break;
469
+				case 'ISO-8859-1' :
470
+					$property['value'] = utf8_encode($property['value']);
471
+					break;
472
+				case 'Windows-1252' :
473
+					$property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset);
474
+					break;
475
+				default :
476
+					throw new ParseException('Unsupported CHARSET: ' . $propObj['CHARSET']);
477
+			}
478
+			$propObj->setRawMimeDirValue($property['value']);
479
+		}
480
+
481
+		return $propObj;
482
+
483
+	}
484
+
485
+	/**
486
+	 * Unescapes a property value.
487
+	 *
488
+	 * vCard 2.1 says:
489
+	 *   * Semi-colons must be escaped in some property values, specifically
490
+	 *     ADR, ORG and N.
491
+	 *   * Semi-colons must be escaped in parameter values, because semi-colons
492
+	 *     are also use to separate values.
493
+	 *   * No mention of escaping backslashes with another backslash.
494
+	 *   * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
495
+	 *     span values over more than 1 line.
496
+	 *
497
+	 * vCard 3.0 says:
498
+	 *   * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
499
+	 *     escaped, all time time.
500
+	 *   * Comma's are used for delimeters in multiple values
501
+	 *   * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
502
+	 *     as in some properties semi-colon is used for separators.
503
+	 *   * Properties using semi-colons: N, ADR, GEO, ORG
504
+	 *   * Both ADR and N's individual parts may be broken up further with a
505
+	 *     comma.
506
+	 *   * Properties using commas: NICKNAME, CATEGORIES
507
+	 *
508
+	 * vCard 4.0 (rfc6350) says:
509
+	 *   * Commas must be escaped.
510
+	 *   * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
511
+	 *     delimiter, depending on the property.
512
+	 *   * Backslashes must be escaped
513
+	 *   * Newlines must be escaped as either \N or \n.
514
+	 *   * Some compound properties may contain multiple parts themselves, so a
515
+	 *     comma within a semi-colon delimited property may also be unescaped
516
+	 *     to denote multiple parts _within_ the compound property.
517
+	 *   * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
518
+	 *   * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
519
+	 *
520
+	 * Even though the spec says that commas must always be escaped, the
521
+	 * example for GEO in Section 6.5.2 seems to violate this.
522
+	 *
523
+	 * iCalendar 2.0 (rfc5545) says:
524
+	 *   * Commas or semi-colons may be used as delimiters, depending on the
525
+	 *     property.
526
+	 *   * Commas, semi-colons, backslashes, newline (\N or \n) are always
527
+	 *     escaped, unless they are delimiters.
528
+	 *   * Colons shall not be escaped.
529
+	 *   * Commas can be considered the 'default delimiter' and is described as
530
+	 *     the delimiter in cases where the order of the multiple values is
531
+	 *     insignificant.
532
+	 *   * Semi-colons are described as the delimiter for 'structured values'.
533
+	 *     They are specifically used in Semi-colons are used as a delimiter in
534
+	 *     REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
535
+	 *
536
+	 * Now for the parameters
537
+	 *
538
+	 * If delimiter is not set (null) this method will just return a string.
539
+	 * If it's a comma or a semi-colon the string will be split on those
540
+	 * characters, and always return an array.
541
+	 *
542
+	 * @param string $input
543
+	 * @param string $delimiter
544
+	 *
545
+	 * @return string|string[]
546
+	 */
547
+	static function unescapeValue($input, $delimiter = ';') {
548
+
549
+		$regex = '#  (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
550
+		if ($delimiter) {
551
+			$regex .= ' | (' . $delimiter . ')';
552
+		}
553
+		$regex .= ') #x';
554
+
555
+		$matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
556
+
557
+		$resultArray = [];
558
+		$result = '';
559
+
560
+		foreach ($matches as $match) {
561
+
562
+			switch ($match) {
563
+				case '\\\\' :
564
+					$result .= '\\';
565
+					break;
566
+				case '\N' :
567
+				case '\n' :
568
+					$result .= "\n";
569
+					break;
570
+				case '\;' :
571
+					$result .= ';';
572
+					break;
573
+				case '\,' :
574
+					$result .= ',';
575
+					break;
576
+				case $delimiter :
577
+					$resultArray[] = $result;
578
+					$result = '';
579
+					break;
580
+				default :
581
+					$result .= $match;
582
+					break;
583
+
584
+			}
585
+
586
+		}
587
+
588
+		$resultArray[] = $result;
589
+		return $delimiter ? $resultArray : $result;
590
+
591
+	}
592
+
593
+	/**
594
+	 * Unescapes a parameter value.
595
+	 *
596
+	 * vCard 2.1:
597
+	 *   * Does not mention a mechanism for this. In addition, double quotes
598
+	 *     are never used to wrap values.
599
+	 *   * This means that parameters can simply not contain colons or
600
+	 *     semi-colons.
601
+	 *
602
+	 * vCard 3.0 (rfc2425, rfc2426):
603
+	 *   * Parameters _may_ be surrounded by double quotes.
604
+	 *   * If this is not the case, semi-colon, colon and comma may simply not
605
+	 *     occur (the comma used for multiple parameter values though).
606
+	 *   * If it is surrounded by double-quotes, it may simply not contain
607
+	 *     double-quotes.
608
+	 *   * This means that a parameter can in no case encode double-quotes, or
609
+	 *     newlines.
610
+	 *
611
+	 * vCard 4.0 (rfc6350)
612
+	 *   * Behavior seems to be identical to vCard 3.0
613
+	 *
614
+	 * iCalendar 2.0 (rfc5545)
615
+	 *   * Behavior seems to be identical to vCard 3.0
616
+	 *
617
+	 * Parameter escaping mechanism (rfc6868) :
618
+	 *   * This rfc describes a new way to escape parameter values.
619
+	 *   * New-line is encoded as ^n
620
+	 *   * ^ is encoded as ^^.
621
+	 *   * " is encoded as ^'
622
+	 *
623
+	 * @param string $input
624
+	 *
625
+	 * @return void
626
+	 */
627
+	private function unescapeParam($input) {
628
+
629
+		return
630
+			preg_replace_callback(
631
+				'#(\^(\^|n|\'))#',
632
+				function($matches) {
633
+					switch ($matches[2]) {
634
+						case 'n' :
635
+							return "\n";
636
+						case '^' :
637
+							return '^';
638
+						case '\'' :
639
+							return '"';
640
+
641
+					// @codeCoverageIgnoreStart
642
+					}
643
+					// @codeCoverageIgnoreEnd
644
+				},
645
+				$input
646
+			);
647
+	}
648
+
649
+	/**
650
+	 * Gets the full quoted printable value.
651
+	 *
652
+	 * We need a special method for this, because newlines have both a meaning
653
+	 * in vCards, and in QuotedPrintable.
654
+	 *
655
+	 * This method does not do any decoding.
656
+	 *
657
+	 * @return string
658
+	 */
659
+	private function extractQuotedPrintableValue() {
660
+
661
+		// We need to parse the raw line again to get the start of the value.
662
+		//
663
+		// We are basically looking for the first colon (:), but we need to
664
+		// skip over the parameters first, as they may contain one.
665
+		$regex = '/^
666 666
             (?: [^:])+ # Anything but a colon
667 667
             (?: "[^"]")* # A parameter in double quotes
668 668
             : # start of the value we really care about
669 669
             (.*)$
670 670
         /xs';
671 671
 
672
-        preg_match($regex, $this->rawLine, $matches);
672
+		preg_match($regex, $this->rawLine, $matches);
673 673
 
674
-        $value = $matches[1];
675
-        // Removing the first whitespace character from every line. Kind of
676
-        // like unfolding, but we keep the newline.
677
-        $value = str_replace("\n ", "\n", $value);
674
+		$value = $matches[1];
675
+		// Removing the first whitespace character from every line. Kind of
676
+		// like unfolding, but we keep the newline.
677
+		$value = str_replace("\n ", "\n", $value);
678 678
 
679
-        // Microsoft products don't always correctly fold lines, they may be
680
-        // missing a whitespace. So if 'forgiving' is turned on, we will take
681
-        // those as well.
682
-        if ($this->options & self::OPTION_FORGIVING) {
683
-            while (substr($value, -1) === '=') {
684
-                // Reading the line
685
-                $this->readLine();
686
-                // Grabbing the raw form
687
-                $value .= "\n" . $this->rawLine;
688
-            }
689
-        }
679
+		// Microsoft products don't always correctly fold lines, they may be
680
+		// missing a whitespace. So if 'forgiving' is turned on, we will take
681
+		// those as well.
682
+		if ($this->options & self::OPTION_FORGIVING) {
683
+			while (substr($value, -1) === '=') {
684
+				// Reading the line
685
+				$this->readLine();
686
+				// Grabbing the raw form
687
+				$value .= "\n" . $this->rawLine;
688
+			}
689
+		}
690 690
 
691
-        return $value;
691
+		return $value;
692 692
 
693
-    }
693
+	}
694 694
 
695 695
 }
Please login to merge, or discard this patch.
libraries/htmlpurifier/library/HTMLPurifier/UnitConverter.php 1 patch
Indentation   +295 added lines, -295 removed lines patch added patch discarded remove patch
@@ -7,301 +7,301 @@
 block discarded – undo
7 7
 class HTMLPurifier_UnitConverter
8 8
 {
9 9
 
10
-    const ENGLISH = 1;
11
-    const METRIC = 2;
12
-    const DIGITAL = 3;
13
-
14
-    /**
15
-     * Units information array. Units are grouped into measuring systems
16
-     * (English, Metric), and are assigned an integer representing
17
-     * the conversion factor between that unit and the smallest unit in
18
-     * the system. Numeric indexes are actually magical constants that
19
-     * encode conversion data from one system to the next, with a O(n^2)
20
-     * constraint on memory (this is generally not a problem, since
21
-     * the number of measuring systems is small.)
22
-     */
23
-    protected static $units = array(
24
-        self::ENGLISH => array(
25
-            'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
26
-            'pt' => 4,
27
-            'pc' => 48,
28
-            'in' => 288,
29
-            self::METRIC => array('pt', '0.352777778', 'mm'),
30
-        ),
31
-        self::METRIC => array(
32
-            'mm' => 1,
33
-            'cm' => 10,
34
-            self::ENGLISH => array('mm', '2.83464567', 'pt'),
35
-        ),
36
-    );
37
-
38
-    /**
39
-     * Minimum bcmath precision for output.
40
-     * @type int
41
-     */
42
-    protected $outputPrecision;
43
-
44
-    /**
45
-     * Bcmath precision for internal calculations.
46
-     * @type int
47
-     */
48
-    protected $internalPrecision;
49
-
50
-    /**
51
-     * Whether or not BCMath is available.
52
-     * @type bool
53
-     */
54
-    private $bcmath;
55
-
56
-    public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false)
57
-    {
58
-        $this->outputPrecision = $output_precision;
59
-        $this->internalPrecision = $internal_precision;
60
-        $this->bcmath = !$force_no_bcmath && function_exists('bcmul');
61
-    }
62
-
63
-    /**
64
-     * Converts a length object of one unit into another unit.
65
-     * @param HTMLPurifier_Length $length
66
-     *      Instance of HTMLPurifier_Length to convert. You must validate()
67
-     *      it before passing it here!
68
-     * @param string $to_unit
69
-     *      Unit to convert to.
70
-     * @return HTMLPurifier_Length|bool
71
-     * @note
72
-     *      About precision: This conversion function pays very special
73
-     *      attention to the incoming precision of values and attempts
74
-     *      to maintain a number of significant figure. Results are
75
-     *      fairly accurate up to nine digits. Some caveats:
76
-     *          - If a number is zero-padded as a result of this significant
77
-     *            figure tracking, the zeroes will be eliminated.
78
-     *          - If a number contains less than four sigfigs ($outputPrecision)
79
-     *            and this causes some decimals to be excluded, those
80
-     *            decimals will be added on.
81
-     */
82
-    public function convert($length, $to_unit)
83
-    {
84
-        if (!$length->isValid()) {
85
-            return false;
86
-        }
87
-
88
-        $n = $length->getN();
89
-        $unit = $length->getUnit();
90
-
91
-        if ($n === '0' || $unit === false) {
92
-            return new HTMLPurifier_Length('0', false);
93
-        }
94
-
95
-        $state = $dest_state = false;
96
-        foreach (self::$units as $k => $x) {
97
-            if (isset($x[$unit])) {
98
-                $state = $k;
99
-            }
100
-            if (isset($x[$to_unit])) {
101
-                $dest_state = $k;
102
-            }
103
-        }
104
-        if (!$state || !$dest_state) {
105
-            return false;
106
-        }
107
-
108
-        // Some calculations about the initial precision of the number;
109
-        // this will be useful when we need to do final rounding.
110
-        $sigfigs = $this->getSigFigs($n);
111
-        if ($sigfigs < $this->outputPrecision) {
112
-            $sigfigs = $this->outputPrecision;
113
-        }
114
-
115
-        // BCMath's internal precision deals only with decimals. Use
116
-        // our default if the initial number has no decimals, or increase
117
-        // it by how ever many decimals, thus, the number of guard digits
118
-        // will always be greater than or equal to internalPrecision.
119
-        $log = (int)floor(log(abs($n), 10));
120
-        $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
121
-
122
-        for ($i = 0; $i < 2; $i++) {
123
-
124
-            // Determine what unit IN THIS SYSTEM we need to convert to
125
-            if ($dest_state === $state) {
126
-                // Simple conversion
127
-                $dest_unit = $to_unit;
128
-            } else {
129
-                // Convert to the smallest unit, pending a system shift
130
-                $dest_unit = self::$units[$state][$dest_state][0];
131
-            }
132
-
133
-            // Do the conversion if necessary
134
-            if ($dest_unit !== $unit) {
135
-                $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
136
-                $n = $this->mul($n, $factor, $cp);
137
-                $unit = $dest_unit;
138
-            }
139
-
140
-            // Output was zero, so bail out early. Shouldn't ever happen.
141
-            if ($n === '') {
142
-                $n = '0';
143
-                $unit = $to_unit;
144
-                break;
145
-            }
146
-
147
-            // It was a simple conversion, so bail out
148
-            if ($dest_state === $state) {
149
-                break;
150
-            }
151
-
152
-            if ($i !== 0) {
153
-                // Conversion failed! Apparently, the system we forwarded
154
-                // to didn't have this unit. This should never happen!
155
-                return false;
156
-            }
157
-
158
-            // Pre-condition: $i == 0
159
-
160
-            // Perform conversion to next system of units
161
-            $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
162
-            $unit = self::$units[$state][$dest_state][2];
163
-            $state = $dest_state;
164
-
165
-            // One more loop around to convert the unit in the new system.
166
-
167
-        }
168
-
169
-        // Post-condition: $unit == $to_unit
170
-        if ($unit !== $to_unit) {
171
-            return false;
172
-        }
173
-
174
-        // Useful for debugging:
175
-        // "<pre>n";
176
-        // "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
177
-
178
-        $n = $this->round($n, $sigfigs);
179
-        if (strpos($n, '.') !== false) {
180
-            $n = rtrim($n, '0');
181
-        }
182
-        $n = rtrim($n, '.');
183
-
184
-        return new HTMLPurifier_Length($n, $unit);
185
-    }
186
-
187
-    /**
188
-     * Returns the number of significant figures in a string number.
189
-     * @param string $n Decimal number
190
-     * @return int number of sigfigs
191
-     */
192
-    public function getSigFigs($n)
193
-    {
194
-        $n = ltrim($n, '0+-');
195
-        $dp = strpos($n, '.'); // decimal position
196
-        if ($dp === false) {
197
-            $sigfigs = strlen(rtrim($n, '0'));
198
-        } else {
199
-            $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
200
-            if ($dp !== 0) {
201
-                $sigfigs--;
202
-            }
203
-        }
204
-        return $sigfigs;
205
-    }
206
-
207
-    /**
208
-     * Adds two numbers, using arbitrary precision when available.
209
-     * @param string $s1
210
-     * @param string $s2
211
-     * @param int $scale
212
-     * @return string
213
-     */
214
-    private function add($s1, $s2, $scale)
215
-    {
216
-        if ($this->bcmath) {
217
-            return bcadd($s1, $s2, $scale);
218
-        } else {
219
-            return $this->scale((float)$s1 + (float)$s2, $scale);
220
-        }
221
-    }
222
-
223
-    /**
224
-     * Multiples two numbers, using arbitrary precision when available.
225
-     * @param string $s1
226
-     * @param string $s2
227
-     * @param int $scale
228
-     * @return string
229
-     */
230
-    private function mul($s1, $s2, $scale)
231
-    {
232
-        if ($this->bcmath) {
233
-            return bcmul($s1, $s2, $scale);
234
-        } else {
235
-            return $this->scale((float)$s1 * (float)$s2, $scale);
236
-        }
237
-    }
238
-
239
-    /**
240
-     * Divides two numbers, using arbitrary precision when available.
241
-     * @param string $s1
242
-     * @param string $s2
243
-     * @param int $scale
244
-     * @return string
245
-     */
246
-    private function div($s1, $s2, $scale)
247
-    {
248
-        if ($this->bcmath) {
249
-            return bcdiv($s1, $s2, $scale);
250
-        } else {
251
-            return $this->scale((float)$s1 / (float)$s2, $scale);
252
-        }
253
-    }
254
-
255
-    /**
256
-     * Rounds a number according to the number of sigfigs it should have,
257
-     * using arbitrary precision when available.
258
-     * @param float $n
259
-     * @param int $sigfigs
260
-     * @return string
261
-     */
262
-    private function round($n, $sigfigs)
263
-    {
264
-        $new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1
265
-        $rp = $sigfigs - $new_log - 1; // Number of decimal places needed
266
-        $neg = $n < 0 ? '-' : ''; // Negative sign
267
-        if ($this->bcmath) {
268
-            if ($rp >= 0) {
269
-                $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1);
270
-                $n = bcdiv($n, '1', $rp);
271
-            } else {
272
-                // This algorithm partially depends on the standardized
273
-                // form of numbers that comes out of bcmath.
274
-                $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
275
-                $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
276
-            }
277
-            return $n;
278
-        } else {
279
-            return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
280
-        }
281
-    }
282
-
283
-    /**
284
-     * Scales a float to $scale digits right of decimal point, like BCMath.
285
-     * @param float $r
286
-     * @param int $scale
287
-     * @return string
288
-     */
289
-    private function scale($r, $scale)
290
-    {
291
-        if ($scale < 0) {
292
-            // The f sprintf type doesn't support negative numbers, so we
293
-            // need to cludge things manually. First get the string.
294
-            $r = sprintf('%.0f', (float)$r);
295
-            // Due to floating point precision loss, $r will more than likely
296
-            // look something like 4652999999999.9234. We grab one more digit
297
-            // than we need to precise from $r and then use that to round
298
-            // appropriately.
299
-            $precise = (string)round(substr($r, 0, strlen($r) + $scale), -1);
300
-            // Now we return it, truncating the zero that was rounded off.
301
-            return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
302
-        }
303
-        return sprintf('%.' . $scale . 'f', (float)$r);
304
-    }
10
+	const ENGLISH = 1;
11
+	const METRIC = 2;
12
+	const DIGITAL = 3;
13
+
14
+	/**
15
+	 * Units information array. Units are grouped into measuring systems
16
+	 * (English, Metric), and are assigned an integer representing
17
+	 * the conversion factor between that unit and the smallest unit in
18
+	 * the system. Numeric indexes are actually magical constants that
19
+	 * encode conversion data from one system to the next, with a O(n^2)
20
+	 * constraint on memory (this is generally not a problem, since
21
+	 * the number of measuring systems is small.)
22
+	 */
23
+	protected static $units = array(
24
+		self::ENGLISH => array(
25
+			'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
26
+			'pt' => 4,
27
+			'pc' => 48,
28
+			'in' => 288,
29
+			self::METRIC => array('pt', '0.352777778', 'mm'),
30
+		),
31
+		self::METRIC => array(
32
+			'mm' => 1,
33
+			'cm' => 10,
34
+			self::ENGLISH => array('mm', '2.83464567', 'pt'),
35
+		),
36
+	);
37
+
38
+	/**
39
+	 * Minimum bcmath precision for output.
40
+	 * @type int
41
+	 */
42
+	protected $outputPrecision;
43
+
44
+	/**
45
+	 * Bcmath precision for internal calculations.
46
+	 * @type int
47
+	 */
48
+	protected $internalPrecision;
49
+
50
+	/**
51
+	 * Whether or not BCMath is available.
52
+	 * @type bool
53
+	 */
54
+	private $bcmath;
55
+
56
+	public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false)
57
+	{
58
+		$this->outputPrecision = $output_precision;
59
+		$this->internalPrecision = $internal_precision;
60
+		$this->bcmath = !$force_no_bcmath && function_exists('bcmul');
61
+	}
62
+
63
+	/**
64
+	 * Converts a length object of one unit into another unit.
65
+	 * @param HTMLPurifier_Length $length
66
+	 *      Instance of HTMLPurifier_Length to convert. You must validate()
67
+	 *      it before passing it here!
68
+	 * @param string $to_unit
69
+	 *      Unit to convert to.
70
+	 * @return HTMLPurifier_Length|bool
71
+	 * @note
72
+	 *      About precision: This conversion function pays very special
73
+	 *      attention to the incoming precision of values and attempts
74
+	 *      to maintain a number of significant figure. Results are
75
+	 *      fairly accurate up to nine digits. Some caveats:
76
+	 *          - If a number is zero-padded as a result of this significant
77
+	 *            figure tracking, the zeroes will be eliminated.
78
+	 *          - If a number contains less than four sigfigs ($outputPrecision)
79
+	 *            and this causes some decimals to be excluded, those
80
+	 *            decimals will be added on.
81
+	 */
82
+	public function convert($length, $to_unit)
83
+	{
84
+		if (!$length->isValid()) {
85
+			return false;
86
+		}
87
+
88
+		$n = $length->getN();
89
+		$unit = $length->getUnit();
90
+
91
+		if ($n === '0' || $unit === false) {
92
+			return new HTMLPurifier_Length('0', false);
93
+		}
94
+
95
+		$state = $dest_state = false;
96
+		foreach (self::$units as $k => $x) {
97
+			if (isset($x[$unit])) {
98
+				$state = $k;
99
+			}
100
+			if (isset($x[$to_unit])) {
101
+				$dest_state = $k;
102
+			}
103
+		}
104
+		if (!$state || !$dest_state) {
105
+			return false;
106
+		}
107
+
108
+		// Some calculations about the initial precision of the number;
109
+		// this will be useful when we need to do final rounding.
110
+		$sigfigs = $this->getSigFigs($n);
111
+		if ($sigfigs < $this->outputPrecision) {
112
+			$sigfigs = $this->outputPrecision;
113
+		}
114
+
115
+		// BCMath's internal precision deals only with decimals. Use
116
+		// our default if the initial number has no decimals, or increase
117
+		// it by how ever many decimals, thus, the number of guard digits
118
+		// will always be greater than or equal to internalPrecision.
119
+		$log = (int)floor(log(abs($n), 10));
120
+		$cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
121
+
122
+		for ($i = 0; $i < 2; $i++) {
123
+
124
+			// Determine what unit IN THIS SYSTEM we need to convert to
125
+			if ($dest_state === $state) {
126
+				// Simple conversion
127
+				$dest_unit = $to_unit;
128
+			} else {
129
+				// Convert to the smallest unit, pending a system shift
130
+				$dest_unit = self::$units[$state][$dest_state][0];
131
+			}
132
+
133
+			// Do the conversion if necessary
134
+			if ($dest_unit !== $unit) {
135
+				$factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
136
+				$n = $this->mul($n, $factor, $cp);
137
+				$unit = $dest_unit;
138
+			}
139
+
140
+			// Output was zero, so bail out early. Shouldn't ever happen.
141
+			if ($n === '') {
142
+				$n = '0';
143
+				$unit = $to_unit;
144
+				break;
145
+			}
146
+
147
+			// It was a simple conversion, so bail out
148
+			if ($dest_state === $state) {
149
+				break;
150
+			}
151
+
152
+			if ($i !== 0) {
153
+				// Conversion failed! Apparently, the system we forwarded
154
+				// to didn't have this unit. This should never happen!
155
+				return false;
156
+			}
157
+
158
+			// Pre-condition: $i == 0
159
+
160
+			// Perform conversion to next system of units
161
+			$n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
162
+			$unit = self::$units[$state][$dest_state][2];
163
+			$state = $dest_state;
164
+
165
+			// One more loop around to convert the unit in the new system.
166
+
167
+		}
168
+
169
+		// Post-condition: $unit == $to_unit
170
+		if ($unit !== $to_unit) {
171
+			return false;
172
+		}
173
+
174
+		// Useful for debugging:
175
+		// "<pre>n";
176
+		// "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
177
+
178
+		$n = $this->round($n, $sigfigs);
179
+		if (strpos($n, '.') !== false) {
180
+			$n = rtrim($n, '0');
181
+		}
182
+		$n = rtrim($n, '.');
183
+
184
+		return new HTMLPurifier_Length($n, $unit);
185
+	}
186
+
187
+	/**
188
+	 * Returns the number of significant figures in a string number.
189
+	 * @param string $n Decimal number
190
+	 * @return int number of sigfigs
191
+	 */
192
+	public function getSigFigs($n)
193
+	{
194
+		$n = ltrim($n, '0+-');
195
+		$dp = strpos($n, '.'); // decimal position
196
+		if ($dp === false) {
197
+			$sigfigs = strlen(rtrim($n, '0'));
198
+		} else {
199
+			$sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
200
+			if ($dp !== 0) {
201
+				$sigfigs--;
202
+			}
203
+		}
204
+		return $sigfigs;
205
+	}
206
+
207
+	/**
208
+	 * Adds two numbers, using arbitrary precision when available.
209
+	 * @param string $s1
210
+	 * @param string $s2
211
+	 * @param int $scale
212
+	 * @return string
213
+	 */
214
+	private function add($s1, $s2, $scale)
215
+	{
216
+		if ($this->bcmath) {
217
+			return bcadd($s1, $s2, $scale);
218
+		} else {
219
+			return $this->scale((float)$s1 + (float)$s2, $scale);
220
+		}
221
+	}
222
+
223
+	/**
224
+	 * Multiples two numbers, using arbitrary precision when available.
225
+	 * @param string $s1
226
+	 * @param string $s2
227
+	 * @param int $scale
228
+	 * @return string
229
+	 */
230
+	private function mul($s1, $s2, $scale)
231
+	{
232
+		if ($this->bcmath) {
233
+			return bcmul($s1, $s2, $scale);
234
+		} else {
235
+			return $this->scale((float)$s1 * (float)$s2, $scale);
236
+		}
237
+	}
238
+
239
+	/**
240
+	 * Divides two numbers, using arbitrary precision when available.
241
+	 * @param string $s1
242
+	 * @param string $s2
243
+	 * @param int $scale
244
+	 * @return string
245
+	 */
246
+	private function div($s1, $s2, $scale)
247
+	{
248
+		if ($this->bcmath) {
249
+			return bcdiv($s1, $s2, $scale);
250
+		} else {
251
+			return $this->scale((float)$s1 / (float)$s2, $scale);
252
+		}
253
+	}
254
+
255
+	/**
256
+	 * Rounds a number according to the number of sigfigs it should have,
257
+	 * using arbitrary precision when available.
258
+	 * @param float $n
259
+	 * @param int $sigfigs
260
+	 * @return string
261
+	 */
262
+	private function round($n, $sigfigs)
263
+	{
264
+		$new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1
265
+		$rp = $sigfigs - $new_log - 1; // Number of decimal places needed
266
+		$neg = $n < 0 ? '-' : ''; // Negative sign
267
+		if ($this->bcmath) {
268
+			if ($rp >= 0) {
269
+				$n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1);
270
+				$n = bcdiv($n, '1', $rp);
271
+			} else {
272
+				// This algorithm partially depends on the standardized
273
+				// form of numbers that comes out of bcmath.
274
+				$n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
275
+				$n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
276
+			}
277
+			return $n;
278
+		} else {
279
+			return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
280
+		}
281
+	}
282
+
283
+	/**
284
+	 * Scales a float to $scale digits right of decimal point, like BCMath.
285
+	 * @param float $r
286
+	 * @param int $scale
287
+	 * @return string
288
+	 */
289
+	private function scale($r, $scale)
290
+	{
291
+		if ($scale < 0) {
292
+			// The f sprintf type doesn't support negative numbers, so we
293
+			// need to cludge things manually. First get the string.
294
+			$r = sprintf('%.0f', (float)$r);
295
+			// Due to floating point precision loss, $r will more than likely
296
+			// look something like 4652999999999.9234. We grab one more digit
297
+			// than we need to precise from $r and then use that to round
298
+			// appropriately.
299
+			$precise = (string)round(substr($r, 0, strlen($r) + $scale), -1);
300
+			// Now we return it, truncating the zero that was rounded off.
301
+			return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
302
+		}
303
+		return sprintf('%.' . $scale . 'f', (float)$r);
304
+	}
305 305
 }
306 306
 
307 307
 // vim: et sw=4 sts=4
Please login to merge, or discard this patch.