Test Failed
Push — main ( c8394f...8477f1 )
by Rafael
66:21
created

Restler::mapAPIClasses()   F

Complexity

Conditions 21
Paths 1311

Size

Total Lines 93
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 68
nc 1311
nop 1
dl 0
loc 93
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Luracast\Restler;
4
5
use Exception;
6
use InvalidArgumentException;
7
use Luracast\Restler\Data\ApiMethodInfo;
8
use Luracast\Restler\Data\ValidationInfo;
9
use Luracast\Restler\Data\Validator;
10
use Luracast\Restler\Format\iFormat;
11
use Luracast\Restler\Format\iDecodeStream;
12
use Luracast\Restler\Format\UrlEncodedFormat;
13
14
/**
15
 * REST API Server. It is the server part of the Restler framework.
16
 * inspired by the RestServer code from
17
 * <http://jacwright.com/blog/resources/RestServer.txt>
18
 *
19
 *
20
 * @category   Framework
21
 * @package    Restler
22
 * @author     R.Arul Kumaran <[email protected]>
23
 * @copyright  2010 Luracast
24
 * @license    http://www.opensource.org/licenses/lgpl-license.php LGPL
25
 * @link       http://luracast.com/products/restler/
26
 *
27
 *
28
 * @method static void onGet() onGet(Callable $function) fired before reading the request details
29
 * @method static void onRoute() onRoute(Callable $function) fired before finding the api method
30
 * @method static void onNegotiate() onNegotiate(Callable $function) fired before content negotiation
31
 * @method static void onPreAuthFilter() onPreAuthFilter(Callable $function) fired before pre auth filtering
32
 * @method static void onAuthenticate() onAuthenticate(Callable $function) fired before auth
33
 * @method static void onPostAuthFilter() onPostAuthFilter(Callable $function) fired before post auth filtering
34
 * @method static void onValidate() onValidate(Callable $function) fired before validation
35
 * @method static void onCall() onCall(Callable $function) fired before api method call
36
 * @method static void onCompose() onCompose(Callable $function) fired before composing response
37
 * @method static void onRespond() onRespond(Callable $function) fired before sending response
38
 * @method static void onComplete() onComplete(Callable $function) fired after sending response
39
 * @method static void onMessage() onMessage(Callable $function) fired before composing error response
40
 *
41
 * @method void onGet() onGet(Callable $function) fired before reading the request details
42
 * @method void onRoute() onRoute(Callable $function) fired before finding the api method
43
 * @method void onNegotiate() onNegotiate(Callable $function) fired before content negotiation
44
 * @method void onPreAuthFilter() onPreAuthFilter(Callable $function) fired before pre auth filtering
45
 * @method void onAuthenticate() onAuthenticate(Callable $function) fired before auth
46
 * @method void onPostAuthFilter() onPostAuthFilter(Callable $function) fired before post auth filtering
47
 * @method void onValidate() onValidate(Callable $function) fired before validation
48
 * @method void onCall() onCall(Callable $function) fired before api method call
49
 * @method void onCompose() onCompose(Callable $function) fired before composing response
50
 * @method void onRespond() onRespond(Callable $function) fired before sending response
51
 * @method void onComplete() onComplete(Callable $function) fired after sending response
52
 * @method void onMessage() onMessage(Callable $function) fired before composing error response
53
 *
54
 * @property bool|null  _authenticated
55
 * @property bool _authVerified
56
 */
57
class Restler extends EventDispatcher
58
{
59
    const VERSION = '3.1.0';
60
61
    // ==================================================================
62
    //
63
    // Public variables
64
    //
65
    // ------------------------------------------------------------------
66
    /**
67
     * Reference to the last exception thrown
68
     * @var RestException
69
     */
70
    public $exception = null;
71
    /**
72
     * Used in production mode to store the routes and more
73
     *
74
     * @var iCache
75
     */
76
    public $cache;
77
    /**
78
     * URL of the currently mapped service
79
     *
80
     * @var string
81
     */
82
    public $url;
83
    /**
84
     * Http request method of the current request.
85
     * Any value between [GET, PUT, POST, DELETE]
86
     *
87
     * @var string
88
     */
89
    public $requestMethod;
90
    /**
91
     * Requested data format.
92
     * Instance of the current format class
93
     * which implements the iFormat interface
94
     *
95
     * @var iFormat
96
     * @example jsonFormat, xmlFormat, yamlFormat etc
97
     */
98
    public $requestFormat;
99
    /**
100
     * Response data format.
101
     *
102
     * Instance of the current format class
103
     * which implements the iFormat interface
104
     *
105
     * @var iFormat
106
     * @example jsonFormat, xmlFormat, yamlFormat etc
107
     */
108
    public $responseFormat;
109
    /**
110
     * Http status code
111
     *
112
     * @var int|null when specified it will override @status comment
113
     */
114
    public $responseCode = null;
115
    /**
116
     * @var string base url of the api service
117
     */
118
    protected $baseUrl;
119
    /**
120
     * @var bool Used for waiting till verifying @format
121
     *           before throwing content negotiation failed
122
     */
123
    protected $requestFormatDiffered = false;
124
    /**
125
     * method information including metadata
126
     *
127
     * @var ApiMethodInfo
128
     */
129
    public $apiMethodInfo;
130
    /**
131
     * @var int for calculating execution time
132
     */
133
    protected $startTime;
134
    /**
135
     * When set to false, it will run in debug mode and parse the
136
     * class files every time to map it to the URL
137
     *
138
     * @var boolean
139
     */
140
    protected $productionMode = false;
141
    public $refreshCache = false;
142
    /**
143
     * Caching of url map is enabled or not
144
     *
145
     * @var boolean
146
     */
147
    protected $cached;
148
    /**
149
     * @var int
150
     */
151
    protected $apiVersion = 1;
152
    /**
153
     * @var int
154
     */
155
    protected $requestedApiVersion = 1;
156
    /**
157
     * @var int
158
     */
159
    protected $apiMinimumVersion = 1;
160
    /**
161
     * @var array
162
     */
163
    protected $apiVersionMap = array();
164
    /**
165
     * Associated array that maps formats to their respective format class name
166
     *
167
     * @var array
168
     */
169
    protected $formatMap = array();
170
    /**
171
     * List of the Mime Types that can be produced as a response by this API
172
     *
173
     * @var array
174
     */
175
    protected $writableMimeTypes = array();
176
    /**
177
     * List of the Mime Types that are supported for incoming requests by this API
178
     *
179
     * @var array
180
     */
181
    protected $readableMimeTypes = array();
182
    /**
183
     * Associated array that maps formats to their respective format class name
184
     *
185
     * @var array
186
     */
187
    protected $formatOverridesMap = array('extensions' => array());
188
    /**
189
     * list of filter classes
190
     *
191
     * @var array
192
     */
193
    protected $filterClasses = array();
194
    /**
195
     * instances of filter classes that are executed after authentication
196
     *
197
     * @var array
198
     */
199
    protected $postAuthFilterClasses = array();
200
201
202
    // ==================================================================
203
    //
204
    // Protected variables
205
    //
206
    // ------------------------------------------------------------------
207
208
    /**
209
     * Data sent to the service
210
     *
211
     * @var array
212
     */
213
    protected $requestData = array();
214
    /**
215
     * list of authentication classes
216
     *
217
     * @var array
218
     */
219
    protected $authClasses = array();
220
    /**
221
     * list of error handling classes
222
     *
223
     * @var array
224
     */
225
    protected $errorClasses = array();
226
    protected $authenticated = false;
227
    protected $authVerified = false;
228
    /**
229
     * @var mixed
230
     */
231
    protected $responseData;
232
233
    /**
234
     * Constructor
235
     *
236
     * @param boolean $productionMode    When set to false, it will run in
237
     *                                   debug mode and parse the class files
238
     *                                   every time to map it to the URL
239
     *
240
     * @param bool    $refreshCache      will update the cache when set to true
241
     */
242
    public function __construct($productionMode = false, $refreshCache = false)
243
    {
244
        parent::__construct();
245
        $this->startTime = time();
246
        Util::$restler = $this;
247
        Scope::set('Restler', $this);
248
        $this->productionMode = $productionMode;
249
        if (is_null(Defaults::$cacheDirectory)) {
250
            Defaults::$cacheDirectory = dirname($_SERVER['SCRIPT_FILENAME']) .
251
                DIRECTORY_SEPARATOR . 'cache';
252
        }
253
        $this->cache = new Defaults::$cacheClass();
254
        $this->refreshCache = $refreshCache;
255
        // use this to rebuild cache every time in production mode
256
        if ($productionMode && $refreshCache) {
257
            $this->cached = false;
258
        }
259
    }
260
261
    /**
262
     * Main function for processing the api request
263
     * and return the response
264
     *
265
     * @throws Exception     when the api service class is missing
266
     * @throws RestException to send error response
267
     */
268
    public function handle()
269
    {
270
        try {
271
            try {
272
                try {
273
                    $this->get();
274
                } catch (Exception $e) {
275
                    $this->requestData
276
                        = array(Defaults::$fullRequestDataName => array());
277
                    if (!$e instanceof RestException) {
278
                        $e = new RestException(
279
                            500,
280
                            $this->productionMode ? null : $e->getMessage(),
281
                            array(),
282
                            $e
283
                        );
284
                    }
285
                    $this->route();
286
                    throw $e;
287
                }
288
                if (Defaults::$useVendorMIMEVersioning)
289
                    $this->responseFormat = $this->negotiateResponseFormat();
290
                $this->route();
291
            } catch (Exception $e) {
292
                $this->negotiate();
293
                if (!$e instanceof RestException) {
294
                    $e = new RestException(
295
                        500,
296
                        $this->productionMode ? null : $e->getMessage(),
297
                        array(),
298
                        $e
299
                    );
300
                }
301
                throw $e;
302
            }
303
            $this->negotiate();
304
            $this->preAuthFilter();
305
            $this->authenticate();
306
            $this->postAuthFilter();
307
            $this->validate();
308
            $this->preCall();
309
            $this->call();
310
            $this->compose();
311
            $this->postCall();
312
            if (Defaults::$returnResponse) {
313
                return $this->respond();
314
            }
315
            $this->respond();
316
        } catch (Exception $e) {
317
            try{
318
                if (Defaults::$returnResponse) {
319
                    return $this->message($e);
320
                }
321
                $this->message($e);
322
            } catch (Exception $e2) {
323
                if (Defaults::$returnResponse) {
324
                    return $this->message($e2);
325
                }
326
                $this->message($e2);
327
            }
328
        }
329
    }
330
331
    /**
332
     * read the request details
333
     *
334
     * Find out the following
335
     *  - baseUrl
336
     *  - url requested
337
     *  - version requested (if url based versioning)
338
     *  - http verb/method
339
     *  - negotiate content type
340
     *  - request data
341
     *  - set defaults
342
     */
343
    protected function get()
344
    {
345
        $this->dispatch('get');
346
        if (empty($this->formatMap)) {
347
            $this->setSupportedFormats('JsonFormat');
348
        }
349
        $this->url = $this->getPath();
350
        $this->requestMethod = Util::getRequestMethod();
351
        $this->requestFormat = $this->getRequestFormat();
352
        $this->requestData = $this->getRequestData(false);
353
354
        //parse defaults
355
        foreach ($_GET as $key => $value) {
356
            if (isset(Defaults::$aliases[$key])) {
357
                $_GET[Defaults::$aliases[$key]] = $value;
358
                unset($_GET[$key]);
359
                $key = Defaults::$aliases[$key];
360
            }
361
            if (in_array($key, Defaults::$overridables)) {
362
                Defaults::setProperty($key, $value);
363
            }
364
        }
365
    }
366
367
    /**
368
     * Returns a list of the mime types (e.g.  ["application/json","application/xml"]) that the API can respond with
369
     * @return array
370
     */
371
    public function getWritableMimeTypes()
372
    {
373
        return $this->writableMimeTypes;
374
    }
375
376
    /**
377
     * Returns the list of Mime Types for the request that the API can understand
378
     * @return array
379
     */
380
    public function getReadableMimeTypes()
381
    {
382
        return $this->readableMimeTypes;
383
    }
384
385
    /**
386
     * Call this method and pass all the formats that should be  supported by
387
     * the API Server. Accepts multiple parameters
388
     *
389
     * @param string ,... $formatName   class name of the format class that
390
     *                                  implements iFormat
391
     *
392
     * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
393
     * @throws Exception
394
     */
395
    public function setSupportedFormats($format = null /*[, $format2...$farmatN]*/)
396
    {
397
        $args = func_get_args();
398
        $extensions = array();
399
        $throwException = $this->requestFormatDiffered;
400
        $this->writableMimeTypes = $this->readableMimeTypes = array();
401
        foreach ($args as $className) {
402
            $obj = Scope::get($className);
403
404
            if (!$obj instanceof iFormat)
405
                throw new Exception('Invalid format class; must implement ' .
406
                    'iFormat interface');
407
            if ($throwException && get_class($obj) == get_class($this->requestFormat)) {
408
                $throwException = false;
409
            }
410
411
            foreach ($obj->getMIMEMap() as $mime => $extension) {
412
                if($obj->isWritable()){
413
                    $this->writableMimeTypes[] = $mime;
414
                    $extensions[".$extension"] = true;
415
                }
416
                if($obj->isReadable())
417
                    $this->readableMimeTypes[] = $mime;
418
                if (!isset($this->formatMap[$extension]))
419
                    $this->formatMap[$extension] = $className;
420
                if (!isset($this->formatMap[$mime]))
421
                    $this->formatMap[$mime] = $className;
422
            }
423
        }
424
        if ($throwException) {
425
            throw new RestException(
426
                403,
427
                'Content type `' . $this->requestFormat->getMIME() . '` is not supported.'
428
            );
429
        }
430
        $this->formatMap['default'] = $args[0];
431
        $this->formatMap['extensions'] = array_keys($extensions);
432
    }
433
434
    /**
435
     * Call this method and pass all the formats that can be used to override
436
     * the supported formats using `@format` comment. Accepts multiple parameters
437
     *
438
     * @param string ,... $formatName   class name of the format class that
439
     *                                  implements iFormat
440
     *
441
     * @example $restler->setOverridingFormats('JsonFormat', 'XmlFormat'...);
442
     * @throws Exception
443
     */
444
    public function setOverridingFormats($format = null /*[, $format2...$farmatN]*/)
445
    {
446
        $args = func_get_args();
447
        $extensions = array();
448
        foreach ($args as $className) {
449
            $obj = Scope::get($className);
450
451
            if (!$obj instanceof iFormat)
452
                throw new Exception('Invalid format class; must implement ' .
453
                    'iFormat interface');
454
455
            foreach ($obj->getMIMEMap() as $mime => $extension) {
456
                if (!isset($this->formatOverridesMap[$extension]))
457
                    $this->formatOverridesMap[$extension] = $className;
458
                if (!isset($this->formatOverridesMap[$mime]))
459
                    $this->formatOverridesMap[$mime] = $className;
460
                if($obj->isWritable())
461
                    $extensions[".$extension"] = true;
462
            }
463
        }
464
        $this->formatOverridesMap['extensions'] = array_keys($extensions);
465
    }
466
467
    /**
468
     * Set one or more string to be considered as the base url
469
     *
470
     * When more than one base url is provided, restler will make
471
     * use of $_SERVER['HTTP_HOST'] to find the right one
472
     *
473
     * @param string ,... $url
474
     */
475
    public function setBaseUrls($url /*[, $url2...$urlN]*/)
476
    {
477
        if (func_num_args() > 1) {
478
            $urls = func_get_args();
479
            usort($urls, function ($a, $b) {
480
                return strlen($a) - strlen($b);
481
            });
482
            foreach ($urls as $u) {
483
                if (0 === strpos($_SERVER['HTTP_HOST'], parse_url($u, PHP_URL_HOST))) {
484
                    $this->baseUrl = $u;
485
                    return;
486
                }
487
            }
488
        }
489
        $this->baseUrl = $url;
490
    }
491
492
    /**
493
     * Parses the request url and get the api path
494
     *
495
     * @return string api path
496
     */
497
    protected function getPath()
498
    {
499
        // fix SCRIPT_NAME for PHP 5.4 built-in web server
500
        if (false === strpos($_SERVER['SCRIPT_NAME'], '.php'))
501
            $_SERVER['SCRIPT_NAME']
502
                = '/' . substr($_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT']) + 1);
503
504
        list($base, $path) = Util::splitCommonPath(
505
            strtok(urldecode($_SERVER['REQUEST_URI']), '?'), //remove query string
506
            $_SERVER['SCRIPT_NAME']
507
        );
508
509
        if (!$this->baseUrl) {
510
            // Fix port number retrieval if port is specified in HOST header.
511
            $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
512
            $portPos = strpos($host, ":");
513
            if ($portPos){
514
               $port = substr($host, $portPos + 1);
515
            } else {
516
               $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80';
517
               $port = isset($_SERVER['HTTP_X_FORWARDED_PORT']) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : $port; // Amazon ELB
518
            }
519
            $https = $port == '443' ||
520
                (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || // Amazon ELB
521
                (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on');
522
            $baseUrl = ($https ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'];
523
            if (!$https && $port != '80' || $https && $port != '443')
524
                $baseUrl .= ':' . $port;
525
            $this->baseUrl = $baseUrl . $base;
526
        } elseif (!empty($base) && false === strpos($this->baseUrl, $base)) {
527
            $this->baseUrl .= $base;
528
        }
529
530
        $path = str_replace(
531
            array_merge(
532
                $this->formatMap['extensions'],
533
                $this->formatOverridesMap['extensions']
534
            ),
535
            '',
536
            rtrim($path, '/') //remove trailing slash if found
537
        );
538
539
        if (Defaults::$useUrlBasedVersioning && strlen($path) && $path[0] == 'v') {
540
            $version = intval(substr($path, 1));
541
            if ($version && $version <= $this->apiVersion) {
542
                $this->requestedApiVersion = $version;
543
                $path = explode('/', $path, 2);
544
                $path = count($path) == 2 ? $path[1] : '';
545
            }
546
        } else {
547
            $this->requestedApiVersion = $this->apiMinimumVersion;
548
        }
549
        return $path;
550
    }
551
552
    /**
553
     * Parses the request to figure out format of the request data
554
     *
555
     * @throws RestException
556
     * @return iFormat any class that implements iFormat
557
     * @example JsonFormat
558
     */
559
    protected function getRequestFormat()
560
    {
561
        $format = null ;
562
        // check if client has sent any information on request format
563
        if (
564
            !empty($_SERVER['CONTENT_TYPE']) ||
565
            (
566
                !empty($_SERVER['HTTP_CONTENT_TYPE']) &&
567
                $_SERVER['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']
568
            )
569
        ) {
570
            $mime = $_SERVER['CONTENT_TYPE'];
571
            if (false !== $pos = strpos($mime, ';')) {
572
                $mime = substr($mime, 0, $pos);
573
            }
574
            if ($mime == UrlEncodedFormat::MIME)
575
                $format = Scope::get('UrlEncodedFormat');
576
            elseif (isset($this->formatMap[$mime])) {
577
                $format = Scope::get($this->formatMap[$mime]);
578
                $format->setMIME($mime);
579
            } elseif (!$this->requestFormatDiffered && isset($this->formatOverridesMap[$mime])) {
580
                //if our api method is not using an @format comment
581
                //to point to this $mime, we need to throw 403 as in below
582
                //but since we don't know that yet, we need to defer that here
583
                $format = Scope::get($this->formatOverridesMap[$mime]);
584
                $format->setMIME($mime);
585
                $this->requestFormatDiffered = true;
586
            } else {
587
                throw new RestException(
588
                    403,
589
                    "Content type `$mime` is not supported."
590
                );
591
            }
592
        }
593
        if(!$format){
594
            $format = Scope::get($this->formatMap['default']);
595
        }
596
        return $format;
597
    }
598
599
    public function getRequestStream()
600
    {
601
        static $tempStream = false;
602
        if (!$tempStream) {
603
            $tempStream = fopen('php://temp', 'r+');
604
            $rawInput = fopen('php://input', 'r');
605
            stream_copy_to_stream($rawInput, $tempStream);
606
        }
607
        rewind($tempStream);
608
        return $tempStream;
609
    }
610
611
    /**
612
     * Parses the request data and returns it
613
     *
614
     * @param bool $includeQueryParameters
615
     *
616
     * @return array php data
617
     */
618
    public function getRequestData($includeQueryParameters = true)
619
    {
620
        $get = UrlEncodedFormat::decoderTypeFix($_GET);
621
        if (
622
            $this->requestMethod == 'PUT'
623
            || $this->requestMethod == 'PATCH'
624
            || $this->requestMethod == 'POST'
625
        ) {
626
            if (!empty($this->requestData)) {
627
                return $includeQueryParameters
628
                    ? $this->requestData + $get
629
                    : $this->requestData;
630
            }
631
632
            $stream = $this->getRequestStream();
633
            if($stream === FALSE)
634
                return array();
635
            $r = $this->requestFormat instanceof iDecodeStream
636
                ? $this->requestFormat->decodeStream($stream)
637
                : $this->requestFormat->decode(stream_get_contents($stream));
638
639
            $r = is_array($r)
640
                ? array_merge($r, array(Defaults::$fullRequestDataName => $r))
641
                : array(Defaults::$fullRequestDataName => $r);
642
            return $includeQueryParameters
643
                ? $r + $get
644
                : $r;
645
        }
646
        return $includeQueryParameters ? $get : array(); //no body
647
    }
648
649
    /**
650
     * Find the api method to execute for the requested Url
651
     */
652
    protected function route()
653
    {
654
        $this->dispatch('route');
655
656
        $params = $this->getRequestData();
657
658
        //backward compatibility for restler 2 and below
659
        if (!Defaults::$smartParameterParsing) {
660
            $params = $params + array(Defaults::$fullRequestDataName => $params);
661
        }
662
663
        $this->apiMethodInfo = $o = Routes::find(
664
            $this->url, $this->requestMethod,
665
            $this->requestedApiVersion, $params
666
        );
667
        //set defaults based on api method comments
668
        if (isset($o->metadata)) {
669
            foreach (Defaults::$fromComments as $key => $defaultsKey) {
670
                if (array_key_exists($key, $o->metadata)) {
671
                    $value = $o->metadata[$key];
672
                    Defaults::setProperty($defaultsKey, $value);
673
                }
674
            }
675
        }
676
        if (!isset($o->className))
677
            throw new RestException(404);
678
679
        if(isset($this->apiVersionMap[$o->className])){
680
            Scope::$classAliases[Util::getShortName($o->className)]
681
                = $this->apiVersionMap[$o->className][$this->requestedApiVersion];
682
        }
683
684
        foreach ($this->authClasses as $auth) {
685
            if (isset($this->apiVersionMap[$auth])) {
686
                Scope::$classAliases[$auth] = $this->apiVersionMap[$auth][$this->requestedApiVersion];
687
            } elseif (isset($this->apiVersionMap[Scope::$classAliases[$auth]])) {
688
                Scope::$classAliases[$auth]
689
                    = $this->apiVersionMap[Scope::$classAliases[$auth]][$this->requestedApiVersion];
690
            }
691
        }
692
    }
693
694
    /**
695
     * Negotiate the response details such as
696
     *  - cross origin resource sharing
697
     *  - media type
698
     *  - charset
699
     *  - language
700
     *
701
     * @throws RestException
702
     */
703
    protected function negotiate()
704
    {
705
        $this->dispatch('negotiate');
706
        $this->negotiateCORS();
707
        $this->responseFormat = $this->negotiateResponseFormat();
708
        $this->negotiateCharset();
709
        $this->negotiateLanguage();
710
    }
711
712
    protected function negotiateCORS()
713
    {
714
        if (
715
            $this->requestMethod == 'OPTIONS'
716
            && Defaults::$crossOriginResourceSharing
717
        ) {
718
            if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
719
                header('Access-Control-Allow-Methods: '
720
                    . Defaults::$accessControlAllowMethods);
721
722
            if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
723
                header('Access-Control-Allow-Headers: '
724
                    . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
725
726
            header('Access-Control-Allow-Origin: ' .
727
                ((Defaults::$accessControlAllowOrigin == '*' && isset($_SERVER['HTTP_ORIGIN']))
728
                    ? $_SERVER['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin));
729
            header('Access-Control-Allow-Credentials: true');
730
731
            exit(0);
732
        }
733
    }
734
735
    // ==================================================================
736
    //
737
    // Protected functions
738
    //
739
    // ------------------------------------------------------------------
740
741
    /**
742
     * Parses the request to figure out the best format for response.
743
     * Extension, if present, overrides the Accept header
744
     *
745
     * @throws RestException
746
     * @return iFormat
747
     * @example JsonFormat
748
     */
749
    protected function negotiateResponseFormat()
750
    {
751
        $metadata = Util::nestedValue($this, 'apiMethodInfo', 'metadata');
752
        //check if the api method insists on response format using @format comment
753
754
        if ($metadata && isset($metadata['format'])) {
755
            $formats = explode(',', (string)$metadata['format']);
756
            foreach ($formats as $i => $f) {
757
                $f = trim($f);
758
                if (!in_array($f, $this->formatOverridesMap))
759
                    throw new RestException(
760
                        500,
761
                        "Given @format is not present in overriding formats. Please call `\$r->setOverridingFormats('$f');` first."
762
                    );
763
                $formats[$i] = $f;
764
            }
765
            call_user_func_array(array($this, 'setSupportedFormats'), $formats);
766
        }
767
768
        // check if client has specified an extension
769
        /** @var $format iFormat*/
770
        $format = null;
771
        $extensions = explode(
772
            '.',
773
            parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
774
        );
775
        while ($extensions) {
776
            $extension = array_pop($extensions);
777
            $extension = explode('/', $extension);
778
            $extension = array_shift($extension);
779
            if ($extension && isset($this->formatMap[$extension])) {
780
                $format = Scope::get($this->formatMap[$extension]);
781
                $format->setExtension($extension);
782
                // echo "Extension $extension";
783
                return $format;
784
            }
785
        }
786
        // check if client has sent list of accepted data formats
787
        if (isset($_SERVER['HTTP_ACCEPT'])) {
788
            $acceptList = Util::sortByPriority($_SERVER['HTTP_ACCEPT']);
789
            foreach ($acceptList as $accept => $quality) {
790
                if (isset($this->formatMap[$accept])) {
791
                    $format = Scope::get($this->formatMap[$accept]);
792
                    $format->setMIME($accept);
793
                    //echo "MIME $accept";
794
                    // Tell cache content is based on Accept header
795
                    @header('Vary: Accept');
796
797
                    return $format;
798
                } elseif (false !== ($index = strrpos($accept, '+'))) {
799
                    $mime = substr($accept, 0, $index);
800
                    if (
801
                        is_string(Defaults::$apiVendor)
802
                        && 0 === stripos(
803
                            $mime,
804
                            'application/vnd.'
805
                            . Defaults::$apiVendor . '-v')
806
                    ) {
807
                        $extension = substr($accept, $index + 1);
808
                        if (isset($this->formatMap[$extension])) {
809
                            //check the MIME and extract version
810
                            $version = intval(substr(
811
                                $mime,
812
                                18 + strlen(Defaults::$apiVendor)));
813
                            if ($version > 0 && $version <= $this->apiVersion) {
814
                                $this->requestedApiVersion = $version;
815
                                $format = Scope::get($this->formatMap[$extension]);
816
                                $format->setExtension($extension);
817
                                // echo "Extension $extension";
818
                                Defaults::$useVendorMIMEVersioning = true;
819
                                @header('Vary: Accept');
820
821
                                return $format;
822
                            }
823
                        }
824
                    }
825
                }
826
            }
827
        } else {
828
            // RFC 2616: If no Accept header field is
829
            // present, then it is assumed that the
830
            // client accepts all media types.
831
            $_SERVER['HTTP_ACCEPT'] = '*/*';
832
        }
833
        if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) {
834
            if (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/*')) {
835
                $format = Scope::get('JsonFormat');
836
            } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], 'text/*')) {
837
                $format = Scope::get('XmlFormat');
838
            } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], '*/*')) {
839
                $format = Scope::get($this->formatMap['default']);
840
            }
841
        }
842
        if (empty($format)) {
843
            // RFC 2616: If an Accept header field is present, and if the
844
            // server cannot send a response which is acceptable according to
845
            // the combined Accept field value, then the server SHOULD send
846
            // a 406 (not acceptable) response.
847
            $format = Scope::get($this->formatMap['default']);
848
            $this->responseFormat = $format;
849
            throw new RestException(
850
                406,
851
                'Content negotiation failed. ' .
852
                'Try `' . $format->getMIME() . '` instead.'
853
            );
854
        } else {
855
            // Tell cache content is based at Accept header
856
            @header("Vary: Accept");
857
            return $format;
858
        }
859
    }
860
861
    protected function negotiateCharset()
862
    {
863
        if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
864
            $found = false;
865
            $charList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_CHARSET']);
866
            foreach ($charList as $charset => $quality) {
867
                if (in_array($charset, Defaults::$supportedCharsets)) {
868
                    $found = true;
869
                    Defaults::$charset = $charset;
870
                    break;
871
                }
872
            }
873
            if (!$found) {
874
                if (strpos($_SERVER['HTTP_ACCEPT_CHARSET'], '*') !== false) {
875
                    //use default charset
876
                } else {
877
                    throw new RestException(
878
                        406,
879
                        'Content negotiation failed. ' .
880
                        'Requested charset is not supported'
881
                    );
882
                }
883
            }
884
        }
885
    }
886
887
    protected function negotiateLanguage()
888
    {
889
        if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
890
            $found = false;
891
            $langList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_LANGUAGE']);
892
            foreach ($langList as $lang => $quality) {
893
                foreach (Defaults::$supportedLanguages as $supported) {
894
                    if (strcasecmp($supported, $lang) == 0) {
895
                        $found = true;
896
                        Defaults::$language = $supported;
897
                        break 2;
898
                    }
899
                }
900
            }
901
            if (!$found) {
902
                if (strpos($_SERVER['HTTP_ACCEPT_LANGUAGE'], '*') !== false) {
903
                    //use default language
904
                } else {
905
                    //ignore
906
                }
907
            }
908
        }
909
    }
910
911
    /**
912
     * Filer api calls before authentication
913
     */
914
    protected function preAuthFilter()
915
    {
916
        if (empty($this->filterClasses)) {
917
            return;
918
        }
919
        $this->dispatch('preAuthFilter');
920
        foreach ($this->filterClasses as $filterClass) {
921
            /**
922
             * @var iFilter
923
             */
924
            $filterObj = Scope::get($filterClass);
925
926
            if (!$filterObj instanceof iFilter) {
927
                throw new RestException(
928
                    500, 'Filter Class ' .
929
                    'should implement iFilter');
930
            } else if (!($ok = $filterObj->__isAllowed())) {
931
                if (
932
                    is_null($ok)
933
                    && $filterObj instanceof iUseAuthentication
934
                ) {
935
                    //handle at authentication stage
936
                    $this->postAuthFilterClasses[] = $filterClass;
937
                    continue;
938
                }
939
                throw new RestException(403); //Forbidden
940
            }
941
        }
942
    }
943
944
    protected function authenticate()
945
    {
946
        $o = &$this->apiMethodInfo;
947
        $accessLevel = max(Defaults::$apiAccessLevel, $o->accessLevel);
948
        if ($accessLevel || count($this->postAuthFilterClasses)) {
949
            $this->dispatch('authenticate');
950
            if (!count($this->authClasses) && $accessLevel > 1) {
951
                throw new RestException(
952
                    403,
953
                    'at least one Authentication Class is required'
954
                );
955
            }
956
            $unauthorized = false;
957
            foreach ($this->authClasses as $authClass) {
958
                try {
959
                    $authObj = Scope::get($authClass);
960
                    if (!method_exists($authObj, Defaults::$authenticationMethod)) {
961
                        throw new RestException(
962
                            500, 'Authentication Class ' .
963
                            'should implement iAuthenticate');
964
                    } elseif (
965
                        !$authObj->{Defaults::$authenticationMethod}()
966
                    ) {
967
                        throw new RestException(401);
968
                    }
969
                    $unauthorized = false;
970
                    break;
971
                } catch (InvalidAuthCredentials $e) {
972
                    $this->authenticated = false;
973
                    throw $e;
974
                } catch (RestException $e) {
975
                    if (!$unauthorized) {
976
                        $unauthorized = $e;
977
                    }
978
                }
979
            }
980
            $this->authVerified = true;
981
            if ($unauthorized) {
982
                if ($accessLevel > 1) { //when it is not a hybrid api
983
                    throw $unauthorized;
984
                } else {
985
                    $this->authenticated = false;
986
                }
987
            } else {
988
                $this->authenticated = true;
989
            }
990
        }
991
    }
992
993
    /**
994
     * Filer api calls after authentication
995
     */
996
    protected function postAuthFilter()
997
    {
998
        if(empty($this->postAuthFilterClasses)) {
999
            return;
1000
        }
1001
        $this->dispatch('postAuthFilter');
1002
        foreach ($this->postAuthFilterClasses as $filterClass) {
1003
            Scope::get($filterClass);
1004
        }
1005
    }
1006
1007
    protected function validate()
1008
    {
1009
        if (!Defaults::$autoValidationEnabled) {
1010
            return;
1011
        }
1012
        $this->dispatch('validate');
1013
1014
        $o = & $this->apiMethodInfo;
1015
        foreach ($o->metadata['param'] as $index => $param) {
1016
            $info = & $param [CommentParser::$embeddedDataName];
1017
            if (
1018
                !isset($info['validate'])
1019
                || $info['validate'] != false
1020
            ) {
1021
                if (isset($info['method'])) {
1022
                    $info ['apiClassInstance'] = Scope::get($o->className);
1023
                }
1024
                //convert to instance of ValidationInfo
1025
                $info = new ValidationInfo($param);
1026
                //initialize validator
1027
                Scope::get(Defaults::$validatorClass);
1028
                $validator = Defaults::$validatorClass;
1029
                //if(!is_subclass_of($validator, 'Luracast\\Restler\\Data\\iValidate')) {
1030
                //changed the above test to below for addressing this php bug
1031
                //https://bugs.php.net/bug.php?id=53727
1032
                if (function_exists("$validator::validate")) {
1033
                    throw new \UnexpectedValueException(
1034
                        '`Defaults::$validatorClass` must implement `iValidate` interface'
1035
                    );
1036
                }
1037
                $valid = $o->parameters[$index];
1038
                $o->parameters[$index] = null;
1039
                if (empty(Validator::$exceptions))
1040
                    $o->metadata['param'][$index]['autofocus'] = true;
1041
                $valid = $validator::validate(
1042
                    $valid, $info
1043
                );
1044
                $o->parameters[$index] = $valid;
1045
                unset($o->metadata['param'][$index]['autofocus']);
1046
            }
1047
        }
1048
    }
1049
1050
    protected function call()
1051
    {
1052
        $this->dispatch('call');
1053
        $o = & $this->apiMethodInfo;
1054
        $accessLevel = max(
1055
            Defaults::$apiAccessLevel,
1056
            $o->accessLevel);
1057
        if (function_exists('newrelic_name_transaction'))
1058
            newrelic_name_transaction("{$o->className}/{$o->methodName}");
1059
        $object =  Scope::get($o->className);
1060
        switch ($accessLevel) {
1061
            case 3: //protected method
1062
                $reflectionMethod = new \ReflectionMethod(
1063
                    $object,
1064
                    $o->methodName
1065
                );
1066
                $reflectionMethod->setAccessible(true);
1067
                $result = $reflectionMethod->invokeArgs(
1068
                    $object,
1069
                    $o->parameters
1070
                );
1071
                break;
1072
            default:
1073
                $result = call_user_func_array(array(
1074
                    $object,
1075
                    $o->methodName
1076
                ), $o->parameters);
1077
        }
1078
        $this->responseData = $result;
1079
    }
1080
1081
    protected function compose()
1082
    {
1083
        $this->dispatch('compose');
1084
        $this->composeHeaders();
1085
        /**
1086
         * @var iCompose Default Composer
1087
         */
1088
        $compose = Scope::get(Defaults::$composeClass);
1089
        $this->responseData = is_null($this->responseData) &&
1090
        Defaults::$emptyBodyForNullResponse
1091
            ? ''
1092
            : $this->responseFormat->encode(
1093
                $compose->response($this->responseData),
1094
                !$this->productionMode
1095
            );
1096
    }
1097
1098
    public function composeHeaders(RestException $e = null)
1099
    {
1100
        //only GET method should be cached if allowed by API developer
1101
        $expires = $this->requestMethod == 'GET' ? Defaults::$headerExpires : 0;
1102
        if(!is_array(Defaults::$headerCacheControl))
1103
            Defaults::$headerCacheControl = array(Defaults::$headerCacheControl);
1104
        $cacheControl = Defaults::$headerCacheControl[0];
1105
        if ($expires > 0) {
1106
            $cacheControl = $this->apiMethodInfo->accessLevel
1107
                ? 'private, ' : 'public, ';
1108
            $cacheControl .= end(Defaults::$headerCacheControl);
1109
            $cacheControl = str_replace('{expires}', $expires, $cacheControl);
1110
            $expires = gmdate('D, d M Y H:i:s \G\M\T', time() + $expires);
1111
        }
1112
        @header('Cache-Control: ' . $cacheControl);
1113
        @header('Expires: ' . $expires);
1114
        @header('X-Powered-By: Luracast Restler v' . Restler::VERSION);
1115
1116
        if (
1117
            Defaults::$crossOriginResourceSharing
1118
            && isset($_SERVER['HTTP_ORIGIN'])
1119
        ) {
1120
            header('Access-Control-Allow-Origin: ' .
1121
                (Defaults::$accessControlAllowOrigin == '*'
1122
                    ? $_SERVER['HTTP_ORIGIN']
1123
                    : Defaults::$accessControlAllowOrigin));
1124
            header('Access-Control-Allow-Credentials: true');
1125
            header('Access-Control-Max-Age: 86400');
1126
        }
1127
1128
        $this->responseFormat->setCharset(Defaults::$charset);
1129
        $charset = $this->responseFormat->getCharset()
1130
            ? : Defaults::$charset;
1131
1132
        @header('Content-Type: ' . (
1133
            Defaults::$useVendorMIMEVersioning
1134
                ? 'application/vnd.'
1135
                . Defaults::$apiVendor
1136
                . "-v{$this->requestedApiVersion}"
1137
                . '+' . $this->responseFormat->getExtension()
1138
                : $this->responseFormat->getMIME())
1139
            . '; charset=' . $charset);
1140
1141
        @header('Content-Language: ' . Defaults::$language);
1142
1143
        if (isset($this->apiMethodInfo->metadata['header'])) {
1144
            foreach ($this->apiMethodInfo->metadata['header'] as $header)
1145
                @header($header, true);
1146
        }
1147
        $code = 200;
1148
        if (!Defaults::$suppressResponseCode) {
1149
            if ($e) {
1150
                $code = $e->getCode();
1151
            } elseif ($this->responseCode) {
1152
                $code = $this->responseCode;
1153
            } elseif (isset($this->apiMethodInfo->metadata['status'])) {
1154
                $code = $this->apiMethodInfo->metadata['status'];
1155
            }
1156
        }
1157
        $this->responseCode = $code;
1158
        @header(
1159
            "{$_SERVER['SERVER_PROTOCOL']} $code " .
1160
            (isset(RestException::$codes[$code]) ? RestException::$codes[$code] : '')
1161
        );
1162
    }
1163
1164
    protected function respond()
1165
    {
1166
        $this->dispatch('respond');
1167
        //handle throttling
1168
        if (Defaults::$throttle) {
1169
            $elapsed = time() - $this->startTime;
1170
            if (Defaults::$throttle / 1e3 > $elapsed) {
1171
                usleep(1e6 * (Defaults::$throttle / 1e3 - $elapsed));
1172
            }
1173
        }
1174
        if ($this->responseCode == 401) {
1175
            $authString = count($this->authClasses)
1176
                ? Scope::get($this->authClasses[0])->__getWWWAuthenticateString()
1177
                : 'Unknown';
1178
            @header('WWW-Authenticate: ' . $authString, false);
1179
        }
1180
        $this->dispatch('complete');
1181
        if (Defaults::$returnResponse) {
1182
            return $this->responseData;
1183
        } else {
1184
            echo $this->responseData;
1185
            exit;
1186
        }
1187
    }
1188
1189
    protected function message(Exception $exception)
1190
    {
1191
        $this->dispatch('message');
1192
1193
        if (!$exception instanceof RestException) {
1194
            $exception = new RestException(
1195
                500,
1196
                $this->productionMode ? null : $exception->getMessage(),
1197
                array(),
1198
                $exception
1199
            );
1200
        }
1201
1202
        $this->exception = $exception;
1203
1204
        $method = 'handle' . $exception->getCode();
1205
        $handled = false;
1206
        foreach ($this->errorClasses as $className) {
1207
            if (method_exists($className, $method)) {
1208
                $obj = Scope::get($className);
1209
                if ($obj->$method($exception))
1210
                    $handled = true;
1211
            }
1212
        }
1213
        if ($handled) {
1214
            return;
1215
        }
1216
        if (!isset($this->responseFormat)) {
1217
            $this->responseFormat = Scope::get('JsonFormat');
1218
        }
1219
        $this->composeHeaders($exception);
1220
        /**
1221
         * @var iCompose Default Composer
1222
         */
1223
        $compose = Scope::get(Defaults::$composeClass);
1224
        $this->responseData = $this->responseFormat->encode(
1225
            $compose->message($exception),
1226
            !$this->productionMode
1227
        );
1228
        if (Defaults::$returnResponse) {
1229
            return $this->respond();
1230
        }
1231
        $this->respond();
1232
    }
1233
1234
    /**
1235
     * Provides backward compatibility with older versions of Restler
1236
     *
1237
     * @param int $version restler version
1238
     *
1239
     * @throws \OutOfRangeException
1240
     */
1241
    public function setCompatibilityMode($version = 2)
1242
    {
1243
        if ($version <= intval(self::VERSION) && $version > 0) {
1244
            require __DIR__ . "/compatibility/restler{$version}.php";
1245
            return;
1246
        }
1247
        throw new \OutOfRangeException();
1248
    }
1249
1250
    /**
1251
     * @param int $version                 maximum version number supported
1252
     *                                     by  the api
1253
     * @param int $minimum                 minimum version number supported
1254
     * (optional)
1255
     *
1256
     * @throws InvalidArgumentException
1257
     * @return void
1258
     */
1259
    public function setAPIVersion($version = 1, $minimum = 1)
1260
    {
1261
        if (!is_int($version) && $version < 1) {
1262
            throw new InvalidArgumentException('version should be an integer greater than 0');
1263
        }
1264
        $this->apiVersion = $version;
1265
        if (is_int($minimum)) {
1266
            $this->apiMinimumVersion = $minimum;
1267
        }
1268
    }
1269
1270
    /**
1271
     * Classes implementing iFilter interface can be added for filtering out
1272
     * the api consumers.
1273
     *
1274
     * It can be used for rate limiting based on usage from a specific ip
1275
     * address or filter by country, device etc.
1276
     *
1277
     * @param $className
1278
     */
1279
    public function addFilterClass($className)
1280
    {
1281
        $this->filterClasses[] = $className;
1282
    }
1283
1284
    /**
1285
     * protected methods will need at least one authentication class to be set
1286
     * in order to allow that method to be executed
1287
     *
1288
     * @param string $className     of the authentication class
1289
     * @param string $resourcePath  optional url prefix for mapping
1290
     */
1291
    public function addAuthenticationClass($className, $resourcePath = null)
1292
    {
1293
        $this->authClasses[] = $className;
1294
        $this->addAPIClass($className, $resourcePath);
1295
    }
1296
1297
    /**
1298
     * Add api classes through this method.
1299
     *
1300
     * All the public methods that do not start with _ (underscore)
1301
     * will be will be exposed as the public api by default.
1302
     *
1303
     * All the protected methods that do not start with _ (underscore)
1304
     * will exposed as protected api which will require authentication
1305
     *
1306
     * @param string $className       name of the service class
1307
     * @param string $resourcePath    optional url prefix for mapping, uses
1308
     *                                lowercase version of the class name when
1309
     *                                not specified
1310
     *
1311
     * @return null
1312
     *
1313
     * @throws Exception when supplied with invalid class name
1314
     */
1315
    public function addAPIClass($className, $resourcePath = null)
1316
    {
1317
        try{
1318
            if ($this->productionMode && is_null($this->cached)) {
1319
                $routes = $this->cache->get('routes');
1320
                if (isset($routes) && is_array($routes)) {
1321
                    $this->apiVersionMap = $routes['apiVersionMap'];
1322
                    unset($routes['apiVersionMap']);
1323
                    Routes::fromArray($routes);
1324
                    $this->cached = true;
1325
                } else {
1326
                    $this->cached = false;
1327
                }
1328
            }
1329
            if (isset(Scope::$classAliases[$className])) {
1330
                $className = Scope::$classAliases[$className];
1331
            }
1332
            if (!$this->cached) {
1333
                $maxVersionMethod = '__getMaximumSupportedVersion';
1334
                if (class_exists($className)) {
1335
                    if (method_exists($className, $maxVersionMethod)) {
1336
                        $max = $className::$maxVersionMethod();
1337
                        for ($i = 1; $i <= $max; $i++) {
1338
                            $this->apiVersionMap[$className][$i] = $className;
1339
                        }
1340
                    } else {
1341
                        $this->apiVersionMap[$className][1] = $className;
1342
                    }
1343
                }
1344
                //versioned api
1345
                if (false !== ($index = strrpos($className, '\\'))) {
1346
                    $name = substr($className, 0, $index)
1347
                        . '\\v{$version}' . substr($className, $index);
1348
                } else if (false !== ($index = strrpos($className, '_'))) {
1349
                    $name = substr($className, 0, $index)
1350
                        . '_v{$version}' . substr($className, $index);
1351
                } else {
1352
                    $name = 'v{$version}\\' . $className;
1353
                }
1354
1355
                for (
1356
                    $version = $this->apiMinimumVersion;
1357
                     $version <= $this->apiVersion;
1358
                     $version++
1359
                ) {
1360
                    $versionedClassName = str_replace(
1361
                        '{$version}', $version,
1362
                        $name);
1363
                    if (class_exists($versionedClassName)) {
1364
                        Routes::addAPIClass(
1365
                            $versionedClassName,
1366
                            Util::getResourcePath(
1367
                                $className,
1368
                                $resourcePath
1369
                            ),
1370
                            $version
1371
                        );
1372
                        if (method_exists($versionedClassName, $maxVersionMethod)) {
1373
                            $max = $versionedClassName::$maxVersionMethod();
1374
                            for ($i = $version; $i <= $max; $i++) {
1375
                                $this->apiVersionMap[$className][$i] = $versionedClassName;
1376
                            }
1377
                        } else {
1378
                            $this->apiVersionMap[$className][$version] = $versionedClassName;
1379
                        }
1380
                    } elseif (isset($this->apiVersionMap[$className][$version])) {
1381
                        Routes::addAPIClass(
1382
                            $this->apiVersionMap[$className][$version],
1383
                            Util::getResourcePath(
1384
                                $className,
1385
                                $resourcePath
1386
                            ),
1387
                            $version
1388
                        );
1389
                    }
1390
                }
1391
            }
1392
        } catch (Exception $e) {
1393
            $e = new Exception(
1394
                "addAPIClass('$className') failed. " . $e->getMessage(),
1395
                $e->getCode(),
1396
                $e
1397
            );
1398
            $this->setSupportedFormats('JsonFormat');
1399
            $this->message($e);
1400
        }
1401
    }
1402
1403
    /**
1404
     * Add class for custom error handling
1405
     *
1406
     * @param string $className   of the error handling class
1407
     */
1408
    public function addErrorClass($className)
1409
    {
1410
        $this->errorClasses[] = $className;
1411
    }
1412
1413
    /**
1414
     * protected methods will need at least one authentication class to be set
1415
     * in order to allow that method to be executed.  When multiple authentication
1416
     * classes are in use, this function provides better performance by setting
1417
     * all auth classes through a single function call.
1418
     *
1419
     * @param array $classNames     array of associative arrays containing
1420
     *                              the authentication class name & optional
1421
     *                              url prefix for mapping.
1422
     */
1423
    public function setAuthClasses(array $classNames)
1424
    {
1425
        $this->authClasses = array_merge($this->authClasses, array_values($classNames));
1426
    }
1427
1428
    /**
1429
     * Add multiple api classes through this method.
1430
     *
1431
     * This method provides better performance when large number
1432
     * of API classes are in use as it processes them all at once,
1433
     * as opposed to hundreds (or more) addAPIClass calls.
1434
     *
1435
     *
1436
     * All the public methods that do not start with _ (underscore)
1437
     * will be will be exposed as the public api by default.
1438
     *
1439
     * All the protected methods that do not start with _ (underscore)
1440
     * will exposed as protected api which will require authentication
1441
     *
1442
     * @param array $map        array of associative arrays containing
1443
     *                          the class name & optional url prefix
1444
     *                          for mapping.
1445
     *
1446
     * @return null
1447
     *
1448
     * @throws Exception when supplied with invalid class name
1449
     */
1450
    public function mapAPIClasses(array $map)
1451
    {
1452
        try {
1453
            if ($this->productionMode && is_null($this->cached)) {
1454
                $routes = $this->cache->get('routes');
1455
                if (isset($routes) && is_array($routes)) {
1456
                    $this->apiVersionMap = $routes['apiVersionMap'];
1457
                    unset($routes['apiVersionMap']);
1458
                    Routes::fromArray($routes);
1459
                    $this->cached = true;
1460
                } else {
1461
                    $this->cached = false;
1462
                }
1463
            }
1464
            $maxVersionMethod = '__getMaximumSupportedVersion';
1465
            if (!$this->productionMode || !$this->cached) {
1466
                foreach ($map as $className => $resourcePath) {
1467
                    if (is_numeric($className)) {
1468
                        $className = $resourcePath;
1469
                        $resourcePath = null;
1470
                    }
1471
                    if (isset(Scope::$classAliases[$className])) {
1472
                        $className = Scope::$classAliases[$className];
1473
                    }
1474
                    if (class_exists($className)) {
1475
                        if (method_exists($className, $maxVersionMethod)) {
1476
                            $max = $className::$maxVersionMethod();
1477
                            for ($i = 1; $i <= $max; $i++) {
1478
                                $this->apiVersionMap[$className][$i] = $className;
1479
                            }
1480
                        } else {
1481
                            $this->apiVersionMap[$className][1] = $className;
1482
                        }
1483
                    }
1484
                    //versioned api
1485
                    if (false !== ($index = strrpos($className, '\\'))) {
1486
                        $name = substr($className, 0, $index)
1487
                            . '\\v{$version}' . substr($className, $index);
1488
                    } else {
1489
                        if (false !== ($index = strrpos($className, '_'))) {
1490
                            $name = substr($className, 0, $index)
1491
                                . '_v{$version}' . substr($className, $index);
1492
                        } else {
1493
                            $name = 'v{$version}\\' . $className;
1494
                        }
1495
                    }
1496
1497
                    for (
1498
                        $version = $this->apiMinimumVersion;
1499
                         $version <= $this->apiVersion;
1500
                         $version++
1501
                    ) {
1502
                        $versionedClassName = str_replace(
1503
                            '{$version}', $version,
1504
                            $name);
1505
                        if (class_exists($versionedClassName)) {
1506
                            Routes::addAPIClass(
1507
                                $versionedClassName,
1508
                                Util::getResourcePath(
1509
                                    $className,
1510
                                    $resourcePath
1511
                                ),
1512
                                $version
1513
                            );
1514
                            if (method_exists($versionedClassName, $maxVersionMethod)) {
1515
                                $max = $versionedClassName::$maxVersionMethod();
1516
                                for ($i = $version; $i <= $max; $i++) {
1517
                                    $this->apiVersionMap[$className][$i] = $versionedClassName;
1518
                                }
1519
                            } else {
1520
                                $this->apiVersionMap[$className][$version] = $versionedClassName;
1521
                            }
1522
                        } elseif (isset($this->apiVersionMap[$className][$version])) {
1523
                            Routes::addAPIClass(
1524
                                $this->apiVersionMap[$className][$version],
1525
                                Util::getResourcePath(
1526
                                    $className,
1527
                                    $resourcePath
1528
                                ),
1529
                                $version
1530
                            );
1531
                        }
1532
                    }
1533
                }
1534
            }
1535
        } catch (Exception $e) {
1536
            $e = new Exception(
1537
                "mapAPIClasses failed. " . $e->getMessage(),
1538
                $e->getCode(),
1539
                $e
1540
            );
1541
            $this->setSupportedFormats('JsonFormat');
1542
            $this->message($e);
1543
        }
1544
    }
1545
1546
    /**
1547
     * Associated array that maps formats to their respective format class name
1548
     *
1549
     * @return array
1550
     */
1551
    public function getFormatMap()
1552
    {
1553
        return $this->formatMap;
1554
    }
1555
1556
    /**
1557
     * API version requested by the client
1558
     * @return int
1559
     */
1560
    public function getRequestedApiVersion()
1561
    {
1562
        return $this->requestedApiVersion;
1563
    }
1564
1565
    /**
1566
     * When false, restler will run in debug mode and parse the class files
1567
     * every time to map it to the URL
1568
     *
1569
     * @return bool
1570
     */
1571
    public function getProductionMode()
1572
    {
1573
        return $this->productionMode;
1574
    }
1575
1576
    /**
1577
     * Chosen API version
1578
     *
1579
     * @return int
1580
     */
1581
    public function getApiVersion()
1582
    {
1583
        return $this->apiVersion;
1584
    }
1585
1586
    /**
1587
     * Base Url of the API Service
1588
     *
1589
     * @return string
1590
     *
1591
     * @example http://localhost/restler3
1592
     * @example http://restler3.com
1593
     */
1594
    public function getBaseUrl()
1595
    {
1596
        return $this->baseUrl;
1597
    }
1598
1599
    /**
1600
     * List of events that fired already
1601
     *
1602
     * @return array
1603
     */
1604
    public function getEvents()
1605
    {
1606
        return $this->events;
1607
    }
1608
1609
    /**
1610
     * Magic method to expose some protected variables
1611
     *
1612
     * @param string $name name of the hidden property
1613
     *
1614
     * @return null|mixed
1615
     */
1616
    public function __get($name)
1617
    {
1618
        if ($name[0] == '_') {
1619
            $hiddenProperty = substr($name, 1);
1620
            if (isset($this->$hiddenProperty)) {
1621
                return $this->$hiddenProperty;
1622
            }
1623
        }
1624
        return null;
1625
    }
1626
1627
    /**
1628
     * Store the url map cache if needed
1629
     */
1630
    public function __destruct()
1631
    {
1632
        if ($this->productionMode && !$this->cached) {
1633
            if (empty($this->url) && empty($this->requestMethod)) {
1634
                // url and requestMethod is NOT set:
1635
                // This can only happen, when an exception was thrown outside of restler, so that the method Restler::handle was NOT called.
1636
                // In this case, the routes can now be corrupt/incomplete, because we don't know, if all API-classes could be registered
1637
                // before the exception was thrown. So, don't cache the routes, because the routes can now be corrupt/incomplete!
1638
                return;
1639
            }
1640
            if ($this->exception instanceof RestException && $this->exception->getStage() === 'setup') {
1641
                // An exception has occured during configuration of restler. Maybe we could not add all API-classes correctly!
1642
                // So, don't cache the routes, because the routes can now be corrupt/incomplete!
1643
                return;
1644
            }
1645
1646
            $this->cache->set(
1647
                'routes',
1648
                Routes::toArray() +
1649
                array('apiVersionMap' => $this->apiVersionMap)
1650
            );
1651
        }
1652
    }
1653
1654
    /**
1655
     * pre call
1656
     *
1657
     * call _pre_{methodName)_{extension} if exists with the same parameters as
1658
     * the api method
1659
     *
1660
     * @example _pre_get_json
1661
     *
1662
     */
1663
    protected function preCall()
1664
    {
1665
        $o = & $this->apiMethodInfo;
1666
        $preCall = '_pre_' . $o->methodName . '_'
1667
            . $this->requestFormat->getExtension();
1668
1669
        if (method_exists($o->className, $preCall)) {
1670
            $this->dispatch('preCall');
1671
            call_user_func_array(array(
1672
                Scope::get($o->className),
1673
                $preCall
1674
            ), $o->parameters);
1675
        }
1676
    }
1677
1678
    /**
1679
     * post call
1680
     *
1681
     * call _post_{methodName}_{extension} if exists with the composed and
1682
     * serialized (applying the repose format) response data
1683
     *
1684
     * @example _post_get_json
1685
     */
1686
    protected function postCall()
1687
    {
1688
        $o = & $this->apiMethodInfo;
1689
        $postCall = '_post_' . $o->methodName . '_' .
1690
            $this->responseFormat->getExtension();
1691
        if (method_exists($o->className, $postCall)) {
1692
            $this->dispatch('postCall');
1693
            $this->responseData = call_user_func(array(
1694
                Scope::get($o->className),
1695
                $postCall
1696
            ), $this->responseData);
1697
        }
1698
    }
1699
}
1700