PageRenderTime 178ms CodeModel.GetById 54ms app.highlight 33ms RepoModel.GetById 60ms app.codeStats 2ms

/vendor/Luracast/Restler/Restler.php

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