├── .gitignore ├── README.md ├── composer.json ├── config └── config.yml.dist ├── icon.png ├── src ├── Controller │ ├── AuthenticateController.php │ └── RestController.php ├── DataFormatter.php ├── Directives │ ├── CountDirective.php │ ├── FilterDirective.php │ ├── PaginationDirective.php │ ├── RelatedDirective.php │ └── UnrelatedDirective.php ├── RestExtension.php └── Services │ ├── CookieAuthenticationService.php │ ├── IdentifyService.php │ ├── JwtAuthenticationService.php │ ├── RestResponseService.php │ └── Vendors │ └── JsonApi.php └── vendor └── firebase └── php-jwt ├── LICENSE ├── README.md ├── composer.json ├── package.xml └── src ├── BeforeValidException.php ├── ExpiredException.php ├── JWT.php └── SignatureInvalidException.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_* 2 | config.yml 3 | composer.lock 4 | tests/tmp/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rest Api for Bolt 2 | ====================== 3 | #### The backend for your applications. 4 | 5 | - Use Rest with JWT (json web token) 6 | - Create, update, index and retrieve content in json 7 | - Follow the "json api" specification 8 | - Extensible (soon documentation!) 9 | 10 | ___ 11 | 12 | Use 13 | ====================== 14 | 15 | #### Login with JWT. 16 | 17 | curl -X POST -H "https://example.com/auth/login?username=myuser&password=mypass" 18 | 19 | #### Get the TOKEN. 20 | the token is returned in the login response, in the X-Access-Token Header 21 | 22 | X-Access-Token →Bearer eyJ0eXAiOiJKV165QiLCJh6G75d7iJIUzI1NiJ9.eyJpYXQiOjE0N57jQ1NMDgsImV4cCI6MTQ2NDU1ODE0NCwiZGF0YSI6eyJpZCI6InhuZXQifX0.dm7XqR91-Wl6zC9jupVVcu4khQz_LOq0cYf56BXHTIw 23 | 24 | ___ 25 | 26 | #### Get list a contents : USE GET REQUEST 27 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages" 28 | 29 | ###### "filter" param 30 | refine your result, use "||" ">" or "<" 31 | 32 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages?&filter[brand]=foo&filter[model]=bar&filter[status]=draft" 33 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages?&filter[brand]=car&filter[brand]=bmw || fiat" 34 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages?&filter[brand]=car&filter[id]=>100" 35 | 36 | ###### "deep" filter 37 | when deep is enabled, the relationships be treated as one more field of content, useful if for example I want to search for content by the username, working with "filter" param. 38 | 39 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages?filter[contain]=john&filter[deep]=true" 40 | 41 | ###### "related" filter 42 | refine your result according the related content 43 | 44 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages&related=clients:5,10" 45 | 46 | ###### "unrelated" filter 47 | exclude from the results content that is related to certain content type 48 | 49 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/review?filter[unrelated]=report:1" 50 | 51 | ###### "fields" param 52 | limit the format of the result to the fields in the parameter 53 | 54 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/review?fields=title,details" 55 | 56 | ###### "page" param 57 | paginate the results according this param, 58 | or return specific page 59 | 60 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/review?page[size]=10&page[num]=2" 61 | 62 | ###### "order" param 63 | order the result by field or metedata, use "-" prefix with invert the natural order 64 | 65 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/review?order=status" 66 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/review?order=title" 67 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/review?order=-title" 68 | 69 | ###### Use the response headers as pagination helpers 70 | 'X-Total-Count' // total 71 | 'X-Pagination-Page' // actual page 72 | 'X-Pagination-Limit' // limit by page 73 | 74 | ___ 75 | #### Retrieve one content: USE GET REQUEST 76 | curl -X GET -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages/1" 77 | 78 | ___ 79 | #### Create content: USE POST REQUEST and send the data in the body 80 | curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages/1" 81 | ___ 82 | #### Update content: USE PATCH REQUEST and send the data in the body 83 | curl -X PATCH -H "Accept: application/json" -H "Content-Type: application/merge-patch+json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages/1" 84 | ___ 85 | #### Delete content: USE DELETE REQUEST for delete a content 86 | If all goes well, the response should be a "204, not content" 87 | curl -X DELETE -H "Accept: application/json" -H "Authorization: Bearer here.myauth.token" -H "https://example.com/api/pages/1" 88 | ___ 89 | 90 | #### About REST and JWT 91 | #### [Read about Rest](https://en.wikipedia.org/wiki/Representational_state_transfer) 92 | #### [Read about JWT](https://jwt.io/) 93 | ___ 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serweb/rest", 3 | "description": "Rest Api Implementation with JWT", 4 | "type": "bolt-extension", 5 | "keywords": [ 6 | "rest", 7 | "restful", 8 | "json", 9 | "authentication", 10 | "authorization", 11 | "jwt", 12 | "json web token" 13 | ], 14 | "require": { 15 | "bolt/bolt": "^3.0" 16 | }, 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Luciano Rodríguez", 21 | "email": "info@serweb.com.ar" 22 | } 23 | ], 24 | "minimum-stability": "dev", 25 | "prefer-stable": true, 26 | "autoload": { 27 | "psr-4": { 28 | "Bolt\\Extension\\SerWeb\\Rest\\": "src", 29 | "Firebase\\JWT\\": "vendor/firebase/php-jwt/src" 30 | } 31 | }, 32 | "extra": { 33 | "bolt-assets": "web", 34 | "bolt-class": "Bolt\\Extension\\SerWeb\\Rest\\RestExtension", 35 | "bolt-icon": "icon.png" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/config.yml.dist: -------------------------------------------------------------------------------- 1 | # paths 2 | endpoints: 3 | rest: '/api' 4 | authenticate: '/auth' 5 | 6 | security: 7 | providers: [jwt] 8 | # json web tokens configurations 9 | jwt: 10 | secret: IamASecretKeyChangeMePlease 11 | prefix: Bearer 12 | lifetime: 36000 13 | user_param: username 14 | pass_param: password 15 | request_header_name: Authorization 16 | response_header_name: X-Access-Token 17 | algoritm: HS256 # HS256 or HS512 or HS384 or RS256 18 | 19 | # cross-origin resourse sharing 20 | cors: 21 | enabled: true 22 | allow-origin: '*' 23 | 24 | delete: 25 | soft: true 26 | status: 'held' 27 | 28 | only_published: true 29 | 30 | # media types in request and responses 31 | media: 32 | accept: '*' 33 | content-type: '*' 34 | 35 | # enable / disable where, order, filter 36 | params: true 37 | 38 | 39 | default-options: 40 | sort: "-datechanged" 41 | 42 | filter-fields-available: ['status', 'type', 'issue', 'datepublish', 'contain'] -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serweb-labs/bolt-rest-api/391385984219da772499aac63f3b12c732491054/icon.png -------------------------------------------------------------------------------- /src/Controller/AuthenticateController.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class AuthenticateController implements ControllerProviderInterface 22 | { 23 | /** @var array The extension's configuration parameters */ 24 | private $config; 25 | private $app; 26 | 27 | /** 28 | * Initiate the controller with Bolt Application instance and extension config. 29 | * 30 | * @param array $config 31 | */ 32 | 33 | public function __construct(array $config, Application $app) 34 | { 35 | $this->config = $config; 36 | $this->app = $app; 37 | } 38 | 39 | /** 40 | * Specify which method handles which route. 41 | * 42 | * @param Application $app An Application instance 43 | * 44 | * @return ControllerCollection A ControllerCollection instance 45 | */ 46 | 47 | public function connect(Application $app) 48 | { 49 | 50 | /** @var $ctr \Silex\ControllerCollection */ 51 | $ctr = $this->app['controllers_factory']; 52 | 53 | $ctr->post('/login', array($this, 'restLogin')) 54 | ->bind('restLogin'); 55 | 56 | $ctr->options('/login', array($this, 'corsRestLogin')) 57 | ->bind('corsRestLogin'); 58 | 59 | return $ctr; 60 | 61 | } 62 | 63 | /** 64 | * Handle a login attempt. 65 | * 66 | * @param \Silex\Application $app The application/container 67 | * @param Request $request The Symfony Request 68 | * 69 | * @return \Symfony\Component\HttpFoundation\Response 70 | */ 71 | public function restLogin(Request $request) 72 | { 73 | $c = $this->config["security"]; 74 | $username = trim($request->get($c['jwt']['user_param'])); 75 | $password = trim($request->get($c['jwt']['pass_param'])); 76 | $key = $c["jwt"]["secret"]; 77 | $event = new AccessControlEvent($request); 78 | 79 | try { 80 | 81 | if (empty($username) || empty($password)) { 82 | throw new \Exception('Username does not exist.'); 83 | } 84 | 85 | if (!$this->app['access_control.login']->login($username, $password, $event)) { 86 | throw new \Exception('Login Fail.'); 87 | } else { 88 | $time = time(); 89 | 90 | $data = array( 91 | 'iat' => $time, 92 | 'exp' => $time + ($c["jwt"]["lifetime"]), 93 | 'data' => [ 94 | 'id' => $username, 95 | ] 96 | ); 97 | 98 | $jwt = JWT::encode($data, $key, $c["jwt"]['algoritm']); 99 | $token = $jwt; 100 | 101 | $response = new Response(); 102 | $response->headers->set('X-Access-Token', $token); 103 | 104 | $response->headers->set( 105 | 'Access-Control-Allow-Origin', 106 | $this->config["cors"]["allow-origin"] 107 | ); 108 | 109 | $response->headers->set('Access-Control-Allow-Credentials', 'true'); 110 | 111 | $response->headers->set( 112 | 'Access-Control-Expose-Headers', 113 | $c["jwt"]["response_header_name"] 114 | ); 115 | return $response; 116 | } 117 | } catch (\Exception $e) { 118 | return new Response(Trans::__("fail"), 401); 119 | } 120 | } 121 | 122 | /** 123 | * Handle a login attempt. 124 | * 125 | * @param Request $request The Symfony Request 126 | * 127 | * @return \Symfony\Component\HttpFoundation\Response 128 | */ 129 | 130 | public function corsRestLogin() 131 | { 132 | $response = new Response(); 133 | $c = $this->config["security"]; 134 | 135 | if ($this->config["cors"]["enabled"]) { 136 | $response->headers->set('Access-Control-Allow-Methods', 'POST'); 137 | 138 | $response->headers->set( 139 | 'Access-Control-Allow-Origin', 140 | $this->config["cors"]["allow-origin"] 141 | ); 142 | 143 | $response->headers->set( 144 | 'Access-Control-Allow-Headers', 145 | $c["jwt"]["request_header_name"] . ", content-type" 146 | ); 147 | 148 | $response->headers->set('Access-Control-Allow-Credentials', 'true'); 149 | } 150 | $response->headers->set('Allow', 'POST'); 151 | 152 | return $response; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Controller/RestController.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class RestController implements ControllerProviderInterface 22 | { 23 | /** @var array The extension's configuration parameters */ 24 | private $config; 25 | private $app; 26 | private $user; 27 | private $vendor; 28 | private $params; 29 | 30 | /** 31 | * Initiate the controller with Bolt Application instance and extension config. 32 | * 33 | * @param array $config 34 | */ 35 | 36 | public function __construct(array $config, Application $app) 37 | { 38 | $this->app = $app; 39 | $this->config = $this->makeConfig($config); 40 | } 41 | 42 | /** 43 | * Specify which method handles which route. 44 | * @TODO: need support related content (JSONAPI SPEC) 45 | * 46 | * @param Application $app An Application instance 47 | * 48 | * @return ControllerCollection A ControllerCollection instance 49 | */ 50 | 51 | public function connect(Application $app) 52 | { 53 | 54 | /** @var $ctr \Silex\ControllerCollection */ 55 | $ctr = $this->app['controllers_factory']; 56 | 57 | $ctr->get('/{contenttype}', array($this, 'listContentAction')) 58 | ->value('action', 'view') 59 | ->bind('rest.listContent') 60 | ->before(array($this, 'before')); 61 | 62 | $ctr->get('/{contenttype}/{slug}', array($this, 'getContentAction')) 63 | ->value('action', 'view') 64 | ->bind('rest.getContent') 65 | ->before(array($this, 'before')); 66 | 67 | $ctr->get('/{contenttype}/{slug}/{relatedct}', array($this, 'relatedContentAction')) 68 | ->value('action', 'view') 69 | ->bind('rest.relatedcontent') 70 | ->before(array($this, 'before')); 71 | 72 | $ctr->post('/{contenttype}', array($this, 'createContentAction')) 73 | ->value('action', 'create') 74 | ->bind('rest.createcontent') 75 | ->before(array($this, 'before')); 76 | 77 | $ctr->patch('/{contenttype}/{slug}', array($this, 'updateContentAction')) 78 | ->value('action', 'edit') 79 | ->bind('rest.updatecontent') 80 | ->before(array($this, 'before')); 81 | 82 | $ctr->delete('/{contenttype}/{slug}', array($this, 'deleteContentAction')) 83 | ->value('action', 'delete') 84 | ->bind('rest.deletecontent') 85 | ->before(array($this, 'before')); 86 | 87 | $ctr->options('/{contenttype}', array($this, 'corsResponse')) 88 | ->bind('rest.listContent.options'); 89 | 90 | $ctr->options('/{contenttype}/{slug}', array($this, 'corsResponse')) 91 | ->bind('rest.getContent.options'); 92 | 93 | $ctr->options('/{contenttype}/{slug}/{ct}', array($this, 'corsResponse')) 94 | ->bind('rest.relatedContent.options'); 95 | return $ctr; 96 | } 97 | 98 | private function getDefaultSort() 99 | { 100 | return "datepublish"; 101 | } 102 | 103 | // @TODO: contenttype specif 104 | private function getDefaultLimit() 105 | { 106 | return ($this->app['config']->get('general/listing_records')); 107 | } 108 | 109 | // @TODO: in the future maybe slug only change 110 | // on create content 111 | private function makeConfig($config) 112 | { 113 | $default = array( 114 | "filter-fields-available" => ["status"], 115 | "read-only-fields" => array( 116 | 'datechanged', 117 | 'datedepublish', 118 | 'datepublish', 119 | 'datecreated', 120 | 'ownerid', 121 | 'id', 122 | ), 123 | "soft-delete" => array( 124 | "enabled" => false, 125 | ), 126 | "default-query" => array( 127 | "status" => "published", 128 | "contain" => false, 129 | ), 130 | "default-postfilter" => array( 131 | "related" => false, 132 | "unrelated" => false, 133 | "deep" => false 134 | ), 135 | "default-options" => array( 136 | "count" => true, 137 | "limit" => $this->getDefaultLimit(), 138 | "page" => 1, 139 | "sort" => $this->getDefaultSort(), 140 | ), 141 | "thumbnail" => array( 142 | "width" => 500, 143 | "height" => 500, 144 | ) 145 | ); 146 | 147 | 148 | return ($config) ? array_replace_recursive((array) $default, (array) $config) : $default; 149 | } 150 | 151 | 152 | private function checkPermission($request) 153 | { 154 | $contenttype = $request->get("contenttype"); 155 | $slug = $request->get("slug") ? ":" . $request->get("slug") : ""; 156 | $action = $request->get("action"); 157 | $what = "contenttype:{$contenttype}:{$action}{$slug}"; 158 | return $this->app['permissions']->isAllowed($what, $this->user); 159 | } 160 | 161 | 162 | /** 163 | * Before functions in the Rest API controller 164 | * 165 | * @param Request $request The Symfony Request 166 | * 167 | * @return abort|null 168 | */ 169 | 170 | public function before(Request $request) 171 | { 172 | // Get and set User 173 | $user = $this->app['users']->getUser($this->app['rest']->getUsername()); 174 | $this->user = $user; 175 | 176 | // Check permissions 177 | $allow = $this->checkPermission($request); 178 | if (!$allow) { 179 | $error = Trans::__("You don't have the correct permissions"); 180 | return $this->abort($error, Response::HTTP_FORBIDDEN); 181 | } 182 | 183 | // @TODO: temporal 184 | 185 | // Get expected content type 186 | $mime = $request->headers->get('Content-Type'); 187 | 188 | $this->vendor = "jsonApi"; //$this->app['rest.vendors']->getVendor($mime); 189 | 190 | // Parsing request in json 191 | if (0 === strpos($mime, 'application/json') || 0 === strpos($mime, 'application/merge-patch+json')) { 192 | $data = json_decode($request->getContent(), true); 193 | $request->request->replace(is_array($data) ? $data : array()); 194 | } 195 | 196 | return null; 197 | } 198 | 199 | /** 200 | * Abort: response wrapper 201 | * 202 | * @param array $data 203 | * @param int $code 204 | * 205 | * @return response 206 | */ 207 | 208 | public function abort($data, $code) 209 | { 210 | return $this->app['rest.response']->response(array("message" => $data), $code); 211 | } 212 | 213 | /** 214 | * Get multiple content action in the Rest API controller 215 | * 216 | * @param string $contenttypeslug 217 | * 218 | * @return abort|response 219 | */ 220 | 221 | public function listContentAction(Request $request) 222 | { 223 | return $this->app["rest.{$this->vendor}"]->listContent($this->config); 224 | } 225 | 226 | 227 | public function getContentAction($contenttype, $slug) 228 | { 229 | return $this->app["rest.{$this->vendor}"]->readContent($contenttype, $slug, $this->config); 230 | } 231 | 232 | public function updateContentAction($contenttype, $slug) 233 | { 234 | return $this->app["rest.{$this->vendor}"]->updateContent($contenttype, $slug, $this->config); 235 | } 236 | 237 | public function createContentAction($contenttype) 238 | { 239 | return $this->app["rest.{$this->vendor}"]->createContent($contenttype, $this->config); 240 | } 241 | 242 | public function deleteContentAction($contenttype, $slug) 243 | { 244 | return $this->app["rest.{$this->vendor}"]->deleteContent($contenttype, $slug, $this->config); 245 | } 246 | 247 | public function relatedContentAction($contenttype, $slug, $relatedct, Request $request) 248 | { 249 | return $this->app["rest.{$this->vendor}"]->relatedContent($contenttype, $slug, $relatedct, $this->config); 250 | } 251 | 252 | 253 | /** 254 | * CORS: cross origin resourse sharing handler 255 | * 256 | * @return response 257 | */ 258 | public function corsResponse() 259 | { 260 | if ($this->config["cors"]['enabled']) { 261 | $response = new Response(); 262 | $response->headers->set( 263 | 'Access-Control-Allow-Origin', 264 | $this->config["cors"]["allow-origin"] 265 | ); 266 | $a = $this->config["security"]["jwt"]["request_header_name"] . ", X-Pagination-Limit, X-Pagination-Page, X-Total-Count, Content-Type"; 267 | 268 | $methods = "GET, POST, PATCH, PUT, DELETE"; 269 | 270 | $response->headers->set( 271 | 'Access-Control-Allow-Headers', 272 | $a 273 | ); 274 | 275 | $response->headers->set( 276 | 'Access-Control-Allow-Methods', 277 | $methods 278 | ); 279 | 280 | return $response; 281 | } else { 282 | return ""; 283 | } 284 | } 285 | 286 | /** 287 | * Pagination helper 288 | * 289 | * @return response 290 | */ 291 | public function paginate($arr, $limit, $page) 292 | { 293 | $to = ($limit) * $page; 294 | $from = $to - ($limit); 295 | return array_slice($arr, $from, $to); 296 | } 297 | 298 | private function toArray($el) 299 | { 300 | if (!is_array($el)) { 301 | return [$el]; 302 | } 303 | 304 | return $el; 305 | } 306 | 307 | private function intersect($array1, $array2) 308 | { 309 | $result = array_intersect($array1, $array2); 310 | $q = count($result); 311 | 312 | if ($q > 0) { 313 | return true; 314 | } 315 | 316 | return false; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/DataFormatter.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | */ 10 | 11 | use Symfony\Component\Debug\Exception\ContextErrorException; 12 | use Carbon\Carbon; 13 | 14 | class DataFormatter 15 | { 16 | protected $app; 17 | private $config; 18 | private $params; 19 | private $fields; 20 | private $status; 21 | private $include; 22 | private $includesStack; 23 | private $queryCache; 24 | private $cache; 25 | public $count; 26 | 27 | public function __construct($app, $config, $params = array()) 28 | { 29 | $this->app = $app; 30 | $this->config = $config; 31 | $this->params = $params; 32 | $this->fields = empty($params['fields']) ? false : $params['fields']; 33 | $this->status = empty($params['query']['status']) ? "published" : $params['query']['status']; 34 | $this->include = empty($params['include']) ? [] : $params['include']; 35 | $this->includesStack = []; 36 | $this->queryCache = []; 37 | $this->cache = []; 38 | } 39 | 40 | public function listing($contenttype, $items, $basic = false) 41 | { 42 | if (!$this->isIterable($items)) { 43 | throw new \Exception("empty content"); 44 | } 45 | 46 | if (empty($items)) { 47 | $items = array(); 48 | } 49 | $all = []; 50 | foreach ($items as $item) { 51 | $all[] = $this->item($item); 52 | } 53 | 54 | return array( 55 | 'links' => $basic ? array() : $this->getLinksList(), 56 | 'meta' => $basic ? array() : array( 57 | 'count' => (Int) $this->count, 58 | 'page' => (Int) $this->params['pagination']->page, 59 | 'limit' => (Int) $this->params['pagination']->limit 60 | 61 | ), 62 | 'data' => $all, 63 | 'included' => $this->includesToArray($this->includesStack) 64 | ); 65 | } 66 | 67 | public function one($item) 68 | { 69 | 70 | $content = $this->item($item); 71 | 72 | return array( 73 | 'links' => $this->getLinksOne($item->id), 74 | 'data' => $content, 75 | 'included' => $this->includesToArray($this->includesStack) 76 | ); 77 | } 78 | 79 | public function item($item, $deep = true, $child = false) 80 | { 81 | $contenttype = $item->contenttype['slug']; 82 | 83 | // allowed fields 84 | if (isset($this->fields[$contenttype])) { 85 | $allowedFields = explode(",", $this->fields[$contenttype]); 86 | } else { 87 | $allowedFields = false; 88 | } 89 | 90 | $fields = array_keys($item->contenttype['fields']); 91 | 92 | // Always include the ID in the set of fields 93 | array_unshift($fields, 'id'); 94 | 95 | $fields = $item->contenttype['fields']; 96 | $values = array(); 97 | 98 | foreach ($fields as $field => $value) { 99 | if ($allowedFields && !in_array($field, $allowedFields)) { 100 | continue; 101 | } 102 | $values[$field] = $item->{$field}; 103 | } 104 | 105 | // metadata values 106 | if (!$allowedFields || in_array('ownerid', $allowedFields)) { 107 | $values['ownerid'] = $item->ownerid; 108 | } 109 | if (!$allowedFields || in_array('datepublish', $allowedFields)) { 110 | $values['datepublish'] = $item->datepublish->toIso8601String(); 111 | } 112 | if (!$allowedFields || in_array('datechanged', $allowedFields)) { 113 | $values['datechanged'] = $item->datechanged->toIso8601String(); 114 | } 115 | if (!$allowedFields || in_array('datecreated', $allowedFields)) { 116 | $values['datecreated'] = $item->datecreated->toIso8601String(); 117 | } 118 | 119 | // @TODO: custom field formatters by static class in extension config file 120 | // Check if we have image or file fields present. If so, see if we need to 121 | // use the full URL's for these. 122 | foreach ($item->contenttype['fields'] as $key => $field) { 123 | 124 | if (($field['type'] == 'image' || $field['type'] == 'file') && isset($values[$key])) { 125 | if (isset($values[$key]['file'])) { 126 | $values[$key]['url'] = sprintf( 127 | '%s%s%s', 128 | $this->app['paths']['canonical'], 129 | $this->app['paths']['files'], 130 | $values[$key]['file'] 131 | ); 132 | } 133 | } 134 | 135 | if ($field['type'] == 'image' && isset($values[$key]) && is_array($this->config['thumbnail'])) { 136 | 137 | if (isset($values[$key]['file'])) { 138 | $values[$key]['thumbnail'] = sprintf( 139 | '%s/thumbs/%sx%s/%s', 140 | $this->app['paths']['canonical'], 141 | $this->config['thumbnail']['width'], 142 | $this->config['thumbnail']['height'], 143 | $values[$key]['file'] 144 | ); 145 | } 146 | 147 | } 148 | else if ($field['type'] == 'date') { 149 | if (isset($values[$key]) && $values[$key] instanceof Carbon) { 150 | $values[$key] = $values[$key]->toIso8601String(); 151 | } 152 | } 153 | 154 | } 155 | 156 | $relationship = []; 157 | 158 | // get explicit relations 159 | $relations = empty($item->contenttype['relations']) ? [] : $item->contenttype['relations']; 160 | $cts = array_keys($relations); 161 | 162 | foreach ($item->relation->toArray() as $rel) { 163 | if ($rel['from_contenttype'] == $contenttype) { 164 | // from relation 165 | $rct = $rel['to_contenttype']; 166 | $rid = $rel['to_id']; 167 | } else { 168 | // to relation 169 | $rct = $rel['from_contenttype']; 170 | $rid = $rel['from_id']; 171 | } 172 | 173 | // only return relation in contenttype.yml 174 | // @TODO: create and check configuration 175 | // like "return all relations ever" 176 | if (!in_array($rct, $cts)) { 177 | continue; 178 | } 179 | 180 | // add cache repeated 181 | $this->addToCache($rct, $rid); 182 | 183 | 184 | // test deleted status 185 | // @TODO, need RFC, too slow 186 | if ($this->config['delete']['soft']) { 187 | $f = $this->getFromCache($rct, $rid); 188 | if ($f) { 189 | $deleted = ($f->status == $this->config['delete']['status']); 190 | if ($deleted) { 191 | continue; 192 | } 193 | } 194 | else { 195 | continue; 196 | } 197 | } 198 | 199 | // @TODO: RFC 200 | // this return deleted and not published values :/ 201 | if (!array_key_exists($rct, $relationship)) { 202 | $relationship[$rct] = array( 203 | "data" => array(), 204 | "links" => array() 205 | ); 206 | } 207 | 208 | $relationship[$rct]['data'][] = array('type' => $rct, 'id' => $rid); 209 | $relationship[$rct]['links'] = $this->getLinksRelated($item, $rct); 210 | 211 | // @TODO: support "dot sintax" in include 212 | // follow JSON API specification 213 | if (in_array($rct, $this->include) && $deep) { 214 | if (!array_key_exists($rct, $this->includesStack)) { 215 | $this->includesStack[$rct] = []; 216 | } 217 | 218 | if (!array_key_exists($rid, $this->includesStack[$rct])) { 219 | $this->includesStack[$rct][$rid] = $this->item($this->getFromCache($rct, $rid), false, true); 220 | } 221 | } 222 | } 223 | 224 | 225 | 226 | $content = array( 227 | "type" => $contenttype, 228 | "id" => $item->id, 229 | "attributes" => $values, 230 | "relationships" => $relationship, 231 | "links" => $this->getLinksItem($item, $child) 232 | ); 233 | 234 | return $content; 235 | } 236 | 237 | 238 | private function includesToArray($includes) 239 | { 240 | $array = []; 241 | foreach ($includes as $ct => $items) { 242 | foreach ($items as $key => $value) { 243 | $array[] = $value; 244 | } 245 | } 246 | return $array; 247 | } 248 | 249 | private function getLinksItem($item) 250 | { 251 | return array( 252 | "self" => sprintf( 253 | '%s%s/%s/%s', 254 | $this->app['paths']['canonical'], 255 | $this->config['endpoints']['rest'], 256 | $item->contenttype['slug'], 257 | $item->id 258 | ) 259 | ); 260 | } 261 | 262 | private function getLinksList() { 263 | return array( 264 | 'self' => sprintf( 265 | '%s%s/%s%s', 266 | $this->app['paths']['canonical'], 267 | $this->config['endpoints']['rest'], 268 | $this->params['contenttype']['slug'], 269 | $this->paramsToUri() 270 | ), 271 | 'first' => sprintf( 272 | '%s%s/%s%s', 273 | $this->app['paths']['canonical'], 274 | $this->config['endpoints']['rest'], 275 | $this->params['contenttype']['slug'], 276 | '' 277 | ), 278 | 'next' => sprintf( 279 | '%s%s/%s%s', 280 | $this->app['paths']['canonical'], 281 | $this->config['endpoints']['rest'], 282 | $this->params['contenttype']['slug'], 283 | $this->paramsToUri('next') 284 | ), 285 | 'last' => sprintf( 286 | '%s%s/%s%s', 287 | $this->app['paths']['canonical'], 288 | $this->config['endpoints']['rest'], 289 | $this->params['contenttype']['slug'], 290 | $this->paramsToUri('last') 291 | ), 292 | ); 293 | } 294 | 295 | private function getLinksOne($id) { 296 | return array( 297 | 'self' => sprintf( 298 | '%s%s/%s/%s%s', 299 | $this->app['paths']['canonical'], 300 | $this->config['endpoints']['rest'], 301 | $this->params['contenttype']['slug'], 302 | $id, 303 | $this->paramsToUri() 304 | ), 305 | ); 306 | } 307 | 308 | private function getLinksRelated($item, $rct) 309 | { 310 | return array( 311 | "self" => sprintf( 312 | '%s%s/%s/%s/relationships/%s', 313 | $this->app['paths']['canonical'], 314 | $this->config['endpoints']['rest'], 315 | $item->contenttype['slug'], 316 | $item->id, 317 | $rct 318 | ), 319 | "related" => sprintf( 320 | '%s%s/%s/%s/%s', 321 | $this->app['paths']['canonical'], 322 | $this->config['endpoints']['rest'], 323 | $item->contenttype['slug'], 324 | $item->id, 325 | $rct 326 | ) 327 | ); 328 | } 329 | 330 | private function paramsToUri($modifier = false) { 331 | return ""; 332 | } 333 | 334 | private function isIterable($var) 335 | { 336 | return (is_array($var) || $var instanceof \Traversable); 337 | } 338 | 339 | private function coalesce() { 340 | return array_shift(array_filter(func_get_args())); 341 | } 342 | private function addToCache($ct, $id) 343 | { 344 | if (!array_key_exists($ct, $this->queryCache)) { 345 | $this->queryCache[$ct] = []; 346 | } 347 | 348 | if (!array_key_exists($ct, $this->cache)) { 349 | $this->cache[$ct] = []; 350 | } 351 | 352 | if (!array_key_exists($id, $this->queryCache[$ct])) { 353 | $q = "{$ct}/{$id}"; 354 | $this->queryCache[$ct][$id] = function () use ($q) { 355 | return $this->app['query']->getContent($q, []); 356 | }; 357 | } 358 | } 359 | 360 | private function getFromCache($ct, $id) 361 | { 362 | if (array_key_exists($ct, $this->cache)) { 363 | if (array_key_exists($id, $this->cache[$ct])) { 364 | return $this->cache[$ct][$id]; 365 | } 366 | } 367 | 368 | if (array_key_exists($ct, $this->queryCache)) { 369 | if (array_key_exists($id, $this->queryCache[$ct])) { 370 | // fetch query 371 | $this->cache[$ct][$id] = $this->queryCache[$ct][$id](); 372 | return $this->cache[$ct][$id]; 373 | } 374 | } 375 | 376 | return false; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/Directives/CountDirective.php: -------------------------------------------------------------------------------- 1 | get = function () use ($query) { 19 | $queryCount = clone $query->getQueryBuilder(); 20 | $queryCount 21 | ->resetQueryParts(['maxResults', 'firstResult', 'orderBy']) 22 | ->setFirstResult(null) 23 | ->setMaxResults(null) 24 | ->select('COUNT(*) as total'); 25 | return $queryCount->execute()->rowCount(); 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Directives/FilterDirective.php: -------------------------------------------------------------------------------- 1 | getContentType(); 20 | $fields = call_user_func($search->getFields, $ct); 21 | $alias = "_" . $ct; 22 | 23 | $qb = $query->getQueryBuilder(); 24 | $orX = $qb->expr()->orX(); 25 | $term = "'%" . $search->term . "%'"; 26 | foreach ($fields as $field) { 27 | $col = $alias.".".$field; 28 | $orX->add($qb->expr()->like($col, $term)); 29 | } 30 | 31 | $qb->andWhere($orX); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Directives/PaginationDirective.php: -------------------------------------------------------------------------------- 1 | page - 1) * $pagination->limit; 21 | $query->getQueryBuilder()->setFirstResult($offset); 22 | $query->getQueryBuilder()->setMaxResults($pagination->limit); 23 | 24 | // counter 25 | $pagination->count = function () use ($query) { 26 | $queryCount = clone $query->getQueryBuilder(); 27 | $queryCount 28 | //->resetQueryParts(['maxResults', 'firstResult', 'orderBy']) 29 | ->setFirstResult(null) 30 | ->setMaxResults(null) 31 | ->select('COUNT(*) as total'); 32 | 33 | return $queryCount->execute()->rowCount(); 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Directives/RelatedDirective.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 22 | 23 | if (count($to_ct) > 0 && empty(!$to_ids)) { 24 | $qb->andWhere($to_ct.".to_id IN(" . $to_ids . ")"); 25 | } elseif (count($to_ct) > 0) { 26 | $qb->having("COUNT(" . $to_ct . ".id) > 0"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Directives/UnrelatedDirective.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 22 | if (count($to_ct) > 0 && count($to_ids) > 0) { 23 | $qb->andWhere($to_ct.".to_id NOT IN(" . $to_ids . ")"); 24 | } elseif (count($to_ct) > 0) { 25 | $qb->having("COUNT(" . $to_ct . ".id)=0"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RestExtension.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class RestExtension extends SimpleExtension 28 | { 29 | 30 | /** 31 | * {@inheritdoc} 32 | * 33 | * Mount the RestController class 34 | * 35 | * To see specific bindings between route and controller method see 'connect()' 36 | * function in the ExampleController class. 37 | */ 38 | protected function registerFrontendControllers() 39 | { 40 | $app = $this->getContainer(); 41 | $config = $this->getConfig(); 42 | 43 | return [ 44 | $config['endpoints']['rest'] => new RestController($config, $app), 45 | $config['endpoints']['authenticate'] => new AuthenticateController($config, $app), 46 | ]; 47 | } 48 | 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | protected function registerServices(Application $app) 54 | { 55 | $config = $this->getConfig(); 56 | 57 | $app['query.parser'] = $app->extend('query.parser', function (ContentQueryParser $parser) { 58 | $parser->addDirectiveHandler('pagination', new PaginationDirective()); 59 | $parser->addDirectiveHandler('related', new RelatedDirective()); 60 | $parser->addDirectiveHandler('unrelated', new UnrelatedDirective()); 61 | $parser->addDirectiveHandler('filter', new FilterDirective()); 62 | $parser->addDirectiveHandler('count', new CountDirective()); 63 | return $parser; 64 | }); 65 | 66 | 67 | foreach ($config['security']['providers'] as $provider) { 68 | $name = ucfirst($provider) . "AuthenticationService"; 69 | $cl = "Bolt\Extension\SerWeb\Rest\Services\\" . $name; 70 | 71 | $app[$name] = $app->share( 72 | function ($app) use ($config, $cl) { 73 | return new $cl($config, $app); 74 | } 75 | ); 76 | } 77 | 78 | $app['rest'] = $app->share( 79 | function ($app) use ($config) { 80 | $id = 'Bolt\Extension\SerWeb\Rest\Services\IdentifyService'; 81 | return new $id($app, $config); 82 | } 83 | ); 84 | 85 | $app['rest.response'] = $app->share( 86 | function ($app) use ($config) { 87 | $service = 'Bolt\Extension\SerWeb\Rest\Services\RestResponseService'; 88 | return new $service($app, $config); 89 | } 90 | ); 91 | 92 | /* vendors */ 93 | $app['rest.jsonApi'] = $app->share( 94 | function ($app) use ($config) { 95 | return new JsonApi($app, $config); 96 | } 97 | ); 98 | 99 | $app['rest.cors'] = $app->share( 100 | function () use ($config) { 101 | if ($config["cors"]['enabled']) { 102 | $response = new Response(); 103 | $response->headers->set( 104 | 'Access-Control-Allow-Origin', 105 | $config["cors"]["allow-origin"] 106 | ); 107 | $a = $config["security"]["jwt"]["request_header_name"] . ", X-Pagination-Limit, X-Pagination-Page, X-Total-Count, Content-Type"; 108 | 109 | $methods = "GET, POST, PATCH, PUT, DELETE"; 110 | 111 | $response->headers->set( 112 | 'Access-Control-Allow-Headers', 113 | $a 114 | ); 115 | 116 | $response->headers->set( 117 | 'Access-Control-Allow-Methods', 118 | $methods 119 | ); 120 | 121 | return $response; 122 | } else { 123 | return ""; 124 | } 125 | } 126 | ); 127 | 128 | $app->register(new SerializerServiceProvider()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Services/CookieAuthenticationService.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CookieAuthenticationService 15 | { 16 | private $app; 17 | private $config; 18 | 19 | public function __construct(array $config, Application $app) 20 | { 21 | $this->app = $app; 22 | $this->config = $config; 23 | } 24 | 25 | 26 | public function autenticate() 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Services/IdentifyService.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class IdentifyService 15 | { 16 | private $app; 17 | private $config; 18 | public $endpoint; 19 | 20 | function __construct (Application $app, $config) { 21 | $this->app = $app; 22 | $this->config = $config; 23 | $this->endpoint = $config['endpoints']['rest']; 24 | } 25 | 26 | public function getConfig() { 27 | return $this->config; 28 | } 29 | 30 | public function getUsername() 31 | { 32 | foreach ($this->config['security']['providers'] as $provider) { 33 | $name = ucfirst($provider) . "AuthenticationService"; 34 | $result = $this->app[$name]->autenticate(); 35 | if ($result['result'] === true) { 36 | return $result['data']; 37 | } 38 | } 39 | return 'Anonymous'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/JwtAuthenticationService.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class JwtAuthenticationService 16 | { 17 | private $app; 18 | private $config; 19 | 20 | public function __construct(array $config, Application $app) 21 | { 22 | $this->app = $app; 23 | $this->config = $config; 24 | } 25 | 26 | public function autenticate() 27 | { 28 | $cfg = $this->config['security']['jwt']; 29 | 30 | $time = time(); 31 | 32 | // GET Token 33 | $jwt = $this->app['request']->headers->get($cfg['request_header_name']); 34 | if (strpos($jwt, $cfg['prefix'] . " ") !== false) { 35 | $final = str_replace($cfg['prefix'] . " ", "", $jwt); 36 | } 37 | 38 | // Validate Token 39 | try { 40 | $data = JWT::decode($final, $cfg['secret'], array($cfg['algoritm'])); 41 | $result = array('result' => true, 'data' => $data->data->id); 42 | } catch (\Exception $e) { 43 | return $result = array('result' => false); 44 | } 45 | 46 | return $result; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/RestResponseService.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class RestResponseService 17 | { 18 | /** @var array The extension's configuration parameters */ 19 | private $config; 20 | private $app; 21 | 22 | /** 23 | * Initiate the controller with Bolt Application instance and extension config. 24 | * 25 | * @param array $config 26 | */ 27 | public function __construct(Application $app, array $config) 28 | { 29 | $this->config = $config; 30 | $this->app = $app; 31 | } 32 | 33 | 34 | 35 | /** 36 | * Detect "Accept" head and proccess 37 | * 38 | * @param array $data 39 | * @param int $code 40 | * @param array $headers 41 | * 42 | * @return $this->$method|Symfony\Component\HttpFoundation\Response; 43 | */ 44 | public function response($data, $code, $headers = array(), $envelope = false) 45 | { 46 | $default = 'application/json'; 47 | $media = $this->app['request']->headers->get('Accept', $default); 48 | 49 | $utilFragment = explode(";", $media); 50 | $acceptList = explode(",", $utilFragment[0]); 51 | 52 | foreach ($acceptList as $media) { 53 | $media = Slugify::create()->slugify($media, ' '); 54 | $media = ucwords($media); 55 | $media = str_replace(" ", "", $media); 56 | $media[0] = strtolower($media[0]); 57 | $method = $media . "Response"; 58 | $exist = method_exists($this, $method); 59 | 60 | if ($exist) { 61 | return $this->$method($data, $code, $headers, $envelope); 62 | } 63 | } 64 | 65 | return new Response("Unsupported Media Type", 415); 66 | } 67 | 68 | 69 | /** 70 | * Process Json Response in Rest API controller 71 | * 72 | * @param array $data 73 | * @param int $code 74 | * @param array $headers 75 | * 76 | * @return Symfony\Component\HttpFoundation\Response; 77 | */ 78 | public function applicationJsonResponse($data, $code, $headers = array(), $envelope = false) 79 | { 80 | if ($this->config["cors"]['enabled']) { 81 | $headers['Access-Control-Allow-Origin'] = $this->config["cors"]["allow-origin"]; 82 | $headers['Access-Control-Expose-Headers'] = 'X-Pagination-Limit, X-Pagination-Page, X-Total-Count'; 83 | }; 84 | 85 | $headers['Content-Type'] = 'application/json; charset=UTF-8'; 86 | 87 | $response = new Response("{}", $code, $headers); 88 | 89 | if ($envelope) { 90 | $array = array('headers' => $headers, 'response' => $data); 91 | } else { 92 | $array = $data; 93 | } 94 | 95 | $json = json_encode( 96 | $array, 97 | JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT 98 | ); 99 | 100 | $response->setContent($json); 101 | 102 | return $response; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Services/Vendors/JsonApi.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class JsonApi 24 | { 25 | /** @var array The extension's configuration parameters */ 26 | private $config; 27 | private $app; 28 | private $user; 29 | private $vendor; 30 | private $params; 31 | 32 | /** 33 | * Initiate the controller with Bolt Application instance and extension config. 34 | * 35 | * @param array $config 36 | */ 37 | 38 | public function __construct(Application $app) 39 | { 40 | $this->app = $app; 41 | } 42 | 43 | /** 44 | * Abort: response wrapper 45 | * 46 | * @param array $data 47 | * @param int $code 48 | * 49 | * @return response 50 | */ 51 | 52 | public function abort($data, $code) 53 | { 54 | return $this->app['rest.response']->response(array("message" => $data), $code); 55 | } 56 | 57 | /** 58 | * Parse parameters by request and configurationn 59 | * 60 | * @param Request $request 61 | * @param string $ct 62 | * 63 | * @return void 64 | */ 65 | private function getParams(Request $request, $ct = false) 66 | { 67 | $ct = $ct ? : $request->get("contenttype"); 68 | $contenttype = $this->app['storage']->getContentType($ct); 69 | 70 | return array( 71 | "query" => $this->digestQuery($request->get('filter', [])), 72 | "postfilter" => $this->digestPostfilter($request->get('filter', [])), 73 | "fields" => $request->get('fields', []), 74 | "include" => $this->digestInclude($request->get('include', "")), 75 | "pagination" => $this->digestPagination($request->get('page', null)), 76 | "sort" => $request->get('sort', $this->config['default-options']['sort']), 77 | "contenttype" => $contenttype, 78 | "ct" => $contenttype['slug'] 79 | ); 80 | } 81 | 82 | /** 83 | * parse query 84 | * 85 | * @param array $filter 86 | * 87 | * @return array 88 | */ 89 | private function digestQuery($filter) 90 | { 91 | $query = array( 92 | "status" => empty($filter['status']) ? $this->config['default-query']['status'] : $filter['status'], 93 | "contain" => empty($filter['contain']) ? $this->config['default-query']['contain'] : $filter['contain'], 94 | ); 95 | 96 | foreach ($filter as $key => $value) { 97 | if (in_array($key, $this->config['filter-fields-available'])) { 98 | if (!in_array($key, $query)) { 99 | $query[$key] = $value; 100 | } 101 | } 102 | } 103 | return $query; 104 | } 105 | 106 | /** 107 | * parse post-filters 108 | * 109 | * @param array $filter 110 | * 111 | * @return array 112 | */ 113 | private function digestPostfilter($filter) 114 | { 115 | $query = array( 116 | "related" => empty($filter['related']) ? $this->config['default-postfilter']['related'] : $filter['related'], 117 | "unrelated" => empty($filter['unrelated']) ? $this->config['default-postfilter']['unrelated'] : $filter['unrelated'], 118 | "deep" => empty($filter['deep']) ? $this->config['default-postfilter']['deep'] : $filter['deep'], 119 | ); 120 | return $query; 121 | } 122 | 123 | /** 124 | * Make pagination object 125 | * 126 | * @param array $pager 127 | * 128 | * @return stdClass 129 | */ 130 | private function digestPagination($pager) 131 | { 132 | $pagination = new \stdClass(); 133 | $pagination->page = $pager['number'] ? : $this->config['default-options']['page']; 134 | $pagination->limit = $pager['size'] ? : $this->config['default-options']['limit']; 135 | return $pagination; 136 | } 137 | 138 | 139 | /** 140 | * Parse string of include param 141 | * 142 | * @param string $rawInclude 143 | * 144 | * @return array 145 | */ 146 | private function digestInclude($rawInclude) 147 | { 148 | $include = preg_split('/,/', $rawInclude, null, PREG_SPLIT_NO_EMPTY); 149 | return $include; 150 | } 151 | 152 | 153 | /** 154 | * Get multiple content action in the Rest API controller 155 | * 156 | * @param string $contenttypeslug 157 | * 158 | * @return abort|response 159 | */ 160 | 161 | public function listContent($config) 162 | { 163 | $this->config = $config; 164 | $request = $this->app['request']; 165 | $params = $this->getParams($request); 166 | 167 | // Rest best practices: allow only plural version of resource 168 | if ($params['ct'] !== $request->get('contenttype')) { 169 | return $this->abort("Page not found.", Response::HTTP_NOT_FOUND); 170 | } 171 | 172 | // @TODO: maybe in controller 173 | // If the contenttype is 'viewless', don't show the record page. 174 | if (isset($params['contenttype']['viewless']) && 175 | $params['contenttype']['viewless'] === true) { 176 | return $this->abort("Page not found.", Response::HTTP_NOT_FOUND); 177 | } 178 | 179 | // get repo 180 | $repo = $this->app['storage']->getRepository($params['ct']); 181 | $deep = ($params['postfilter']['deep']) ? true : false; 182 | $related = ($params['postfilter']['related']) ? true : false; 183 | $unrelated = ($params['postfilter']['unrelated']) ? true : false; 184 | 185 | $content = $this->fetchContent($params); 186 | 187 | // postfilter 188 | if ($deep || $related || $unrelated) { 189 | $content = $this->postFilter($content['content'], $params); 190 | } 191 | 192 | // pagination 193 | $headers = array( 194 | 'X-Total-Count' => $content['count'], 195 | 'X-Pagination-Page' => $params['pagination']->page, 196 | 'X-Pagination-Limit' => $params['pagination']->limit, 197 | ); 198 | 199 | $formatter = new DataFormatter($this->app, $config, $params); 200 | $formatter->count = $content['count']; 201 | $data = $formatter->listing($params['contenttype'], $content['content']); 202 | 203 | return $this->app['rest.response']->response($data, 200, $headers); 204 | } 205 | 206 | /** 207 | * fetchContent 208 | * 209 | * @param array $params 210 | * @return array 211 | */ 212 | private function fetchContent($params) 213 | { 214 | $options = []; 215 | $q = $params['query']; 216 | 217 | 218 | if ($q['contain']) { 219 | $filter = new \stdClass(); 220 | $filter->term = $q['contain']; 221 | $app = $this->app; 222 | $filter->getFields = function ($ct) use ($app) { 223 | return array_keys($app['storage']->getContentType($ct)['fields']); 224 | }; 225 | // todo: "_" It is not consistant in all Bolt versions: normalize 226 | $options["filter"] = $filter; 227 | } 228 | 229 | if ($params['sort']) { 230 | $options["order"] = $params['sort']; 231 | } 232 | 233 | if ($params['postfilter']['deep'] || $params['ct'] == 'search') { 234 | $contenttypes = implode(",", array_keys($this->app['config']->get('contenttypes'))); 235 | } 236 | else { 237 | if ($params['pagination']) { 238 | $options["pagination"] = $params['pagination']; 239 | } 240 | $contenttypes = $params['ct']; 241 | } 242 | 243 | unset( 244 | $q['contain'] 245 | ); 246 | 247 | // aditional queries 248 | $options = (count($q > 0)) ? array_merge((array) $options, (array) $q) : $options; 249 | 250 | $results = $this->app['query']->getContent( 251 | $contenttypes, 252 | $options 253 | ); 254 | 255 | // pagination 256 | if (isset($options["pagination"])) { 257 | $count = call_user_func($options['pagination']->count); 258 | } 259 | else { 260 | $count = null; 261 | } 262 | return array("content" => $results, "count" => $count); 263 | } 264 | 265 | /** 266 | * Takes a collection of content and 267 | * filters them according to the criteria 268 | * 269 | * @param store $content 270 | * @param array $params 271 | * 272 | * @return array 273 | */ 274 | private function postFilter($content, $params) 275 | { 276 | $deep = $params['postfilter']['deep']; 277 | $related = $params['postfilter']['related'] ? : null; 278 | $unrelated = $params['postfilter']['unrelated'] ? : null; 279 | $ct = $params['ct']; 280 | $ids = []; 281 | $load = []; 282 | $matched = []; 283 | 284 | // fetch all 285 | if ($deep) { 286 | foreach ($content as $key => $item) { 287 | if ($item->contenttype['slug'] == $ct) { 288 | 289 | // if is some contenttype 290 | if (in_array($item->id, $ids)) { 291 | continue; 292 | } 293 | 294 | $matched[] = $item; 295 | $ids[] = $item->id; 296 | } 297 | } 298 | 299 | foreach ($content as $key => $item) { 300 | if ($item->contenttype['slug'] != $ct) { 301 | $load = array_merge($load, $this->getBidirectionalRelations($item, $ct)); 302 | } 303 | } 304 | $load = array_diff($load, $ids); 305 | if (!empty($load)) { 306 | $all = $this->app['query']->getContent($ct, array('id' => implode(" || ", $load))); 307 | $all->add($matched); 308 | } 309 | else { 310 | $all = $matched; 311 | } 312 | 313 | } 314 | else { 315 | $all = $this->toArray($content); 316 | } 317 | 318 | // positive related filter 319 | if (isset($related)) { 320 | $all = $this->positiveRelatedFilter($all, $related); 321 | } 322 | 323 | // negative related filte 324 | if (isset($unrelated)) { 325 | $all = $this->negativeRelatedFilter($all, $unrelated); 326 | } 327 | 328 | //pagination 329 | $partial = $this->paginate($all, $params['pagination']->limit, $params['pagination']->page); 330 | $count = count($all); 331 | 332 | return array("content" => $partial, "count" => $count); 333 | } 334 | 335 | /** 336 | * Get ids of all related content in two ways 337 | * 338 | * @param Content $item 339 | * @param string $related 340 | * @param int $id 341 | * 342 | * @return arrayy 343 | */ 344 | public function getBidirectionalRelations($item, $related, $id = null) { 345 | $rels = $item->relation->getField($related, true, $item->contenttype['slug'], $id); 346 | $ids = []; 347 | foreach ($rels as $rel) { 348 | if ($rel['from_contenttype'] == $related) { 349 | $ids[] = $rel['from_id']; 350 | } else { 351 | // to relation 352 | $ids[] = $rel['to_id']; 353 | } 354 | } 355 | return $ids; 356 | } 357 | 358 | /** 359 | * View Content Action in the Rest API controller 360 | * 361 | * @param string $contenttypeslug 362 | * @param string|integer $slug integer|string 363 | * 364 | * @return abort|response 365 | */ 366 | 367 | public function readContent($contenttypeslug, $slug, $config) 368 | { 369 | $this->config = $config; 370 | $contenttype = $this->app['storage']->getContentType($contenttypeslug); 371 | $isSoft = $this->config['delete']['soft']; 372 | $softStatus = $this->config['delete']['status']; 373 | $request = $this->app['request']; 374 | $params = $this->getParams($request); 375 | 376 | // Rest best practices: allow only plural version of resource 377 | if ($contenttype['slug'] !== $contenttypeslug) { 378 | return $this->abort("Page $contenttypeslug not found.", Response::HTTP_NOT_FOUND); 379 | } 380 | 381 | // If the contenttype is 'viewless', don't show the record page. 382 | if (isset($contenttype['viewless']) && $contenttype['viewless'] === true) { 383 | return $this->abort( 384 | "Page $contenttypeslug/$slug not found.", 385 | Response::HTTP_NOT_FOUND 386 | ); 387 | } 388 | 389 | $slug = $this->app['slugify']->slugify($slug); 390 | 391 | // First, try to get it by slug. 392 | $content = $this->app['storage']->getContent($contenttype['slug'], array('slug' => $slug, 'returnsingle' => true, 'log_not_found' => !is_numeric($slug))); 393 | 394 | if (!$content && is_numeric($slug)) { 395 | // And otherwise try getting it by ID 396 | $content = $this->app['query']->getContent($contenttype['slug'], array('id' => $slug, 'returnsingle' => true)); 397 | } 398 | 399 | // No content, no page! 400 | if (!$content) { 401 | return $this->abort( 402 | "Page $contenttypeslug/$slug not found.", 403 | Response::HTTP_NOT_FOUND 404 | ); 405 | } 406 | 407 | // format 408 | $formatter = new DataFormatter($this->app, $config, $params); 409 | $map = $formatter->one($content); 410 | 411 | // todo: move to DataFormatter 412 | $data = $map; 413 | unset($data['data']['links']); 414 | unset($data['data']['metadata']); 415 | 416 | return $this->app['rest.response']->response($data, 200); 417 | } 418 | 419 | /** 420 | * View Content Action in the Rest API controller 421 | * 422 | * @param string $contenttypeslug 423 | * @param string|integer $slug integer|string 424 | * 425 | * @return abort|response 426 | */ 427 | 428 | public function relatedContent($contenttypeslug, $slug, $relatedct, $config) 429 | { 430 | $this->config = $config; 431 | $contenttype = $this->app['storage']->getContentType($contenttypeslug); 432 | $isSoft = $this->config['delete']['soft']; 433 | $softStatus = $this->config['delete']['status']; 434 | $request = $this->app['request']; 435 | $params = $this->getParams($request, $relatedct); 436 | 437 | // Rest best practices: allow only plural version of resource 438 | if ($contenttype['slug'] !== $contenttypeslug) { 439 | return $this->abort("Page $contenttypeslug not found.", Response::HTTP_NOT_FOUND); 440 | } 441 | 442 | // If the contenttype is 'viewless', don't show the record page. 443 | if (isset($contenttype['viewless']) && $contenttype['viewless'] === true) { 444 | return $this->abort( 445 | "Page $contenttypeslug/$slug not found.", 446 | Response::HTTP_NOT_FOUND 447 | ); 448 | } 449 | 450 | $slug = $this->app['slugify']->slugify($slug); 451 | $repo = $this->app['storage']->getRepository($contenttype['slug']); 452 | $content = $repo->find($slug); 453 | 454 | // No content, no page! 455 | if (!$content) { 456 | return $this->abort( 457 | "Page $contenttypeslug/$slug not found.", 458 | Response::HTTP_NOT_FOUND 459 | ); 460 | } 461 | 462 | // get all relations 463 | $ids = $this->getBidirectionalRelations($content, $relatedct, $slug); 464 | 465 | // offset 466 | $ids = $this->paginate($ids, $params['pagination']->limit, $params['pagination']->page); 467 | 468 | if (count($ids) > 0) { 469 | $content = $this->app['query']->getContent($relatedct, array( 'status' => $params['query']['status'], 'id' => implode(" || ", $ids))); 470 | $formatter = new DataFormatter($this->app, $config, $params); 471 | $formatter->count = $content->count(); 472 | $data = $formatter->listing($relatedct, $content); 473 | } 474 | else { 475 | $data = array( 476 | "data" => [], 477 | "meta" => [ 478 | "count" => 0, 479 | "page" => 1, 480 | "limit" => $params['pagination']->limit 481 | ], 482 | 483 | ); 484 | } 485 | return $this->app['rest.response']->response($data, 200); 486 | } 487 | 488 | public function loadRelatedContent($content, $relatedct, $slug, $status) { 489 | $ids = $this->getBidirectionalRelations($content, $relatedct, $slug); 490 | $fetch = $this->app['query']->getContent($relatedct, array('status' => $status, 'id' => implode(" || ", $ids))); 491 | return $this->toArray($fetch); 492 | } 493 | 494 | 495 | /** 496 | * Insert Action: proccess create or update 497 | * 498 | * @param content $content 499 | * @param string $contenttypeslug 500 | * @param string $oldStatus 501 | * @param repository $repo 502 | * 503 | * @return response 504 | */ 505 | 506 | public function insertAction($content, $contenttypeslug, $oldStatus, $repo) 507 | { 508 | $request = $this->app['request']; 509 | $contenttype = $this->app['storage']->getContentType($contenttypeslug); 510 | 511 | // Add non successfull control values to request values 512 | // http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2 513 | // Also do some corrections 514 | $requestAll = $request->request->all()['data']; 515 | 516 | foreach ($contenttype['fields'] as $key => $values) { 517 | if (isset($requestAll[$key])) { 518 | switch ($values['type']) { 519 | case 'float': 520 | // We allow ',' and '.' as decimal point and need '.' internally 521 | $requestAll[$key] = str_replace(',', '.', $requestAll[$key]); 522 | break; 523 | } 524 | } else { 525 | switch ($values['type']) { 526 | case 'select': 527 | if (isset($values['multiple']) && $values['multiple'] === true) { 528 | $requestAll[$key] = array(); 529 | } 530 | break; 531 | case 'checkbox': 532 | $requestAll[$key] = 0; 533 | break; 534 | // default values prevent 535 | // sql errors 536 | case 'float': 537 | $requestAll[$key] = 0; 538 | break; 539 | case 'integer': 540 | $requestAll[$key] = 0; 541 | break; 542 | } 543 | } 544 | } 545 | 546 | 547 | // assign values 548 | foreach ($contenttype['fields'] as $key => $values) { 549 | if (array_key_exists($key, $requestAll)) { 550 | $content[$key] = $requestAll[$key]; 551 | } 552 | } 553 | 554 | // status 555 | $defaultStatus = $contenttype['default_status'] == "publish" ? 'published' : $contenttype['default_status']; 556 | $fallbackStatus = $contenttype['default_status'] ? $defaultStatus: 'published'; 557 | $beforeStatus = $oldStatus ?: $fallbackStatus; 558 | 559 | if (array_key_exists('status', $requestAll)) { 560 | $newStatus = $requestAll['status']; 561 | } else { 562 | $newStatus = $beforeStatus; 563 | } 564 | 565 | $status = $this->app['users']->isContentStatusTransitionAllowed( 566 | $beforeStatus, 567 | $newStatus, 568 | $contenttype['slug'], 569 | $content['id'] 570 | ); 571 | 572 | if (!$status) { 573 | $error["message"] = Trans::__("Error processing the request"); 574 | $this->abort($error, 500); 575 | } 576 | 577 | $content->status = $newStatus; 578 | 579 | // datepublish 580 | if (array_key_exists('datepublish', $requestAll)) { 581 | $datepublishStr = $requestAll['datepublish']; 582 | $time = Carbon::parse($datepublishStr); 583 | $content->datepublish = new \DateTime($time); 584 | } 585 | 586 | // set owner id 587 | $content['ownerid'] = $this->user['id']; 588 | 589 | // slug: When storing, we should never have an empty slug/URI. 590 | if (!$content['slug'] || empty($content['slug'])) { 591 | $content['slug'] = 'slug-' . md5(mt_rand()); 592 | } 593 | 594 | $content->setDatechanged('now'); 595 | 596 | if (isset($requestAll['relation'])) { 597 | $relation = $requestAll['relation']; 598 | } else { 599 | $relation = []; 600 | } 601 | 602 | $values = array('relation' => $relation); 603 | $arr = []; 604 | 605 | if ($values['relation']) { 606 | foreach ($values['relation'] as $key => $value) { 607 | if (!is_array($value)) { 608 | $values['relation'][$key] = array((string)trim($value)); 609 | } 610 | else { 611 | foreach ($value as $item) { 612 | $arr[$key][] = ((string)trim($item)); 613 | } 614 | $values['relation'] = $arr; 615 | } 616 | } 617 | } 618 | 619 | /** @var Collection\Relations $related */ 620 | $related = $this->app['storage']->createCollection('Bolt\Storage\Entity\Relations'); 621 | $related->setFromPost($values, $content); 622 | $content->setRelation($related); 623 | 624 | 625 | // add note if exist 626 | $note = $request->request->get('note'); 627 | 628 | if ($note && array_key_exists('notes', $contenttype['fields'])) { 629 | $notes = json_decode($content['notes'], true); 630 | if (!array_key_exists('data', $notes)) { 631 | $notes['data'] = array(); 632 | } 633 | $date = new \DateTime('now'); 634 | $date = $date->format('Y-m-d'); 635 | $notes['data'][] = array('data' => $note, 'date' => $date); 636 | $content['notes'] = json_encode($notes); 637 | } 638 | 639 | 640 | // save 641 | $result = $repo->save($content); 642 | 643 | // get ID 644 | $slug = $content->getId(); 645 | 646 | // $result; 647 | if (!$result) { 648 | $error["message"] = Trans::__("Error processing the request"); 649 | $this->abort($error, 500); 650 | } 651 | 652 | $responseData = array('id' => $slug); 653 | 654 | $location = sprintf( 655 | '%s%s/%s%s', 656 | $this->app['paths']['canonical'], 657 | $this->config['endpoints']['rest'], 658 | $contenttypeslug, 659 | $slug 660 | ); 661 | 662 | // Defalt headers 663 | $headers = array(); 664 | 665 | // Detecting whether the answer is " created " 666 | if ($oldStatus == "") { 667 | $headers = array('Location' => $location); 668 | $code = 201; 669 | } else { 670 | $code = 200; 671 | } 672 | 673 | return $this->app['rest.response']->response($responseData, $code, $headers); 674 | } 675 | 676 | 677 | /** 678 | * Add Content Action in the Rest API controller 679 | * 680 | * @param string $contenttypeslug The content type slug 681 | * 682 | * @return insertAction 683 | */ 684 | 685 | public function createContent($contenttypeslug, $config) 686 | { 687 | $this->config = $config; 688 | $content = $this->app['storage']->getContentObject($contenttypeslug); 689 | $repo = $this->app['storage']->getRepository($contenttypeslug); 690 | 691 | //set defaults 692 | $content = $repo->create( 693 | array( 694 | 'contenttype' => $contenttypeslug, 695 | 'datepublish' => date('Y-m-d'), 696 | 'datecreated' => date('Y-m-d'), 697 | 'status' => 'published' 698 | ) 699 | ); 700 | 701 | return $this->insertAction($content, $contenttypeslug, "", $repo); 702 | } 703 | 704 | /** 705 | * Update Content Action in the Rest API controller 706 | * 707 | * @param string $contenttypeslug The content type slug 708 | * @param integer|string $slug The slug of content 709 | * 710 | * @return insertAction 711 | */ 712 | 713 | public function updateContent($contenttypeslug, $slug, $config) 714 | { 715 | $this->config = $config; 716 | 717 | $repo = $this->app['storage']->getRepository($contenttypeslug); 718 | $content = $repo->find($slug); 719 | $oldStatus = $content->status; 720 | return $this->insertAction($content, $contenttypeslug, $oldStatus, $repo); 721 | } 722 | 723 | /** 724 | * Delete Content Action in the Rest API controller 725 | * 726 | * @param string $contenttypeslug The content type slug 727 | * @param integer|string $slug The slug of content 728 | * 729 | * @return response 730 | */ 731 | 732 | public function deleteContent($contenttypeslug, $slug, $config) 733 | { 734 | $this->config = $config; 735 | $isSoft = $this->config['delete']['soft']; 736 | $status = $this->config['delete']['status']; 737 | $contenttype = $this->app['storage']->getContentType($contenttypeslug); 738 | $repo = $this->app['storage']->getRepository($contenttype['slug']); 739 | $row = $repo->find($slug); 740 | 741 | if ($isSoft) { 742 | $row->status = $status; 743 | $repo->save($row); 744 | $result = true; 745 | } else { 746 | $result = $repo->delete($row); 747 | } 748 | 749 | $content = array('action' => $result); 750 | return $this->app['rest.response']->response($content, 204); 751 | } 752 | 753 | 754 | // filter by related (ex. where {related: "book:1,2,3"}) 755 | public function positiveRelatedFilter($partial, $related) 756 | { 757 | $partial = $this->toArray($partial); 758 | $rel = explode(":", $related); 759 | $relations = []; 760 | 761 | if (count($rel) > 1) { 762 | $relations = explode(",", $rel[1]); 763 | } 764 | 765 | foreach ($partial as $key => $item) { 766 | $detect = false; 767 | $ids = $this->getBidirectionalRelations($item, $rel[0]); 768 | 769 | if (!empty($ids)) { 770 | if (count($relations) == 0) { 771 | $detect = true; 772 | } else if (count(array_intersect($ids, $relations)) > 0) { 773 | $detect = true; 774 | } 775 | } 776 | 777 | if (!$detect) { 778 | unset($partial[$key]); 779 | } 780 | } 781 | return $partial; 782 | } 783 | 784 | 785 | /** 786 | * Exclude those that are related to a certain type of content 787 | * 788 | * @param array | collection $partial 789 | * @param string $unrelated 790 | * 791 | * @return array 792 | */ 793 | private function negativeRelatedFilter($partial, $unrelated) 794 | { 795 | $rel = explode(":", $unrelated); 796 | $relations = []; 797 | $except = []; 798 | $list = []; 799 | 800 | if (count($rel) > 1) { 801 | $relations = explode(",", $rel[1]); 802 | } 803 | 804 | foreach ($relations as $value) { 805 | if (strpos($value, "!") !== false) { 806 | $except[] = str_replace("!", "", $value); 807 | continue; 808 | } 809 | $list[] = $value; 810 | } 811 | 812 | foreach ($partial as $key => $item) { 813 | $detect = false; 814 | $ids = $this->getBidirectionalRelations($item, $rel[0]); 815 | if (!empty($ids)) { 816 | if (count($list) == 0) { 817 | $detect = true; 818 | if (count(array_intersect($ids, $except)) > 0) { 819 | $detect = false; 820 | } 821 | } else if (count(array_intersect($ids, $list)) > 0) { 822 | $detect = true; 823 | } 824 | } 825 | 826 | if ($detect) { 827 | unset($partial[$key]); 828 | } 829 | } 830 | 831 | return $partial; 832 | } 833 | 834 | 835 | 836 | /** 837 | * Pagination helper 838 | * 839 | * @param array $arr 840 | * @param int $limit 841 | * @param int $page 842 | 843 | * @return array 844 | */ 845 | private function paginate($arr, $limit, $page) 846 | { 847 | if (!is_array($arr)) { 848 | $arr = $this->toArray($arr); 849 | } 850 | $to = ($limit) * $page; 851 | $from = $to - ($limit); 852 | return array_slice($arr, $from, $to); 853 | } 854 | 855 | /** 856 | * Iterable to aray 857 | * 858 | * @param collection | object | array $el 859 | * 860 | * @return array 861 | */ 862 | private function toArray($el) 863 | { 864 | if (is_array($el)) { 865 | return $el; 866 | } 867 | $arr = []; 868 | foreach ($el as $key => $value) { 869 | $arr[] = $value; 870 | } 871 | 872 | return $arr; 873 | } 874 | 875 | /** 876 | * Check interception 877 | * 878 | * @param array $array1 879 | * @param array $array2 880 | * 881 | * @return bool 882 | */ 883 | private function intersect($array1, $array2) 884 | { 885 | $result = array_intersect($array1, $array2); 886 | $q = count($result); 887 | 888 | if ($q > 0) { 889 | return true; 890 | } 891 | 892 | return false; 893 | } 894 | 895 | } 896 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Neuman Vong 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Neuman Vong nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) 2 | [![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) 3 | [![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) 4 | [![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) 5 | 6 | PHP-JWT 7 | ======= 8 | A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). 9 | 10 | Installation 11 | ------------ 12 | 13 | Use composer to manage your dependencies and download PHP-JWT: 14 | 15 | ```bash 16 | composer require firebase/php-jwt 17 | ``` 18 | 19 | Example 20 | ------- 21 | ```php 22 | "http://example.org", 28 | "aud" => "http://example.com", 29 | "iat" => 1356999524, 30 | "nbf" => 1357000000 31 | ); 32 | 33 | /** 34 | * IMPORTANT: 35 | * You must specify supported algorithms for your application. See 36 | * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 37 | * for a list of spec-compliant algorithms. 38 | */ 39 | $jwt = JWT::encode($token, $key); 40 | $decoded = JWT::decode($jwt, $key, array('HS256')); 41 | 42 | print_r($decoded); 43 | 44 | /* 45 | NOTE: This will now be an object instead of an associative array. To get 46 | an associative array, you will need to cast it as such: 47 | */ 48 | 49 | $decoded_array = (array) $decoded; 50 | 51 | /** 52 | * You can add a leeway to account for when there is a clock skew times between 53 | * the signing and verifying servers. It is recommended that this leeway should 54 | * not be bigger than a few minutes. 55 | * 56 | * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef 57 | */ 58 | JWT::$leeway = 60; // $leeway in seconds 59 | $decoded = JWT::decode($jwt, $key, array('HS256')); 60 | 61 | ?> 62 | ``` 63 | 64 | Changelog 65 | --------- 66 | 67 | #### 4.0.0 / 2016-07-17 68 | - Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! 69 | - Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! 70 | - Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! 71 | - Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! 72 | 73 | #### 3.0.0 / 2015-07-22 74 | - Minimum PHP version updated from `5.2.0` to `5.3.0`. 75 | - Add `\Firebase\JWT` namespace. See 76 | [#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to 77 | [@Dashron](https://github.com/Dashron)! 78 | - Require a non-empty key to decode and verify a JWT. See 79 | [#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to 80 | [@sjones608](https://github.com/sjones608)! 81 | - Cleaner documentation blocks in the code. See 82 | [#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to 83 | [@johanderuijter](https://github.com/johanderuijter)! 84 | 85 | #### 2.2.0 / 2015-06-22 86 | - Add support for adding custom, optional JWT headers to `JWT::encode()`. See 87 | [#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to 88 | [@mcocaro](https://github.com/mcocaro)! 89 | 90 | #### 2.1.0 / 2015-05-20 91 | - Add support for adding a leeway to `JWT:decode()` that accounts for clock skew 92 | between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! 93 | - Add support for passing an object implementing the `ArrayAccess` interface for 94 | `$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! 95 | 96 | #### 2.0.0 / 2015-04-01 97 | - **Note**: It is strongly recommended that you update to > v2.0.0 to address 98 | known security vulnerabilities in prior versions when both symmetric and 99 | asymmetric keys are used together. 100 | - Update signature for `JWT::decode(...)` to require an array of supported 101 | algorithms to use when verifying token signatures. 102 | 103 | 104 | Tests 105 | ----- 106 | Run the tests using phpunit: 107 | 108 | ```bash 109 | $ pear install PHPUnit 110 | $ phpunit --configuration phpunit.xml.dist 111 | PHPUnit 3.7.10 by Sebastian Bergmann. 112 | ..... 113 | Time: 0 seconds, Memory: 2.50Mb 114 | OK (5 tests, 5 assertions) 115 | ``` 116 | 117 | License 118 | ------- 119 | [3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). 120 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase/php-jwt", 3 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 4 | "homepage": "https://github.com/firebase/php-jwt", 5 | "authors": [ 6 | { 7 | "name": "Neuman Vong", 8 | "email": "neuman+pear@twilio.com", 9 | "role": "Developer" 10 | }, 11 | { 12 | "name": "Anant Narayanan", 13 | "email": "anant@php.net", 14 | "role": "Developer" 15 | } 16 | ], 17 | "license": "BSD-3-Clause", 18 | "require": { 19 | "php": ">=5.3.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Firebase\\JWT\\": "src" 24 | } 25 | }, 26 | "minimum-stability": "dev" 27 | } 28 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | JWT 7 | pear.php.net 8 | A JWT encoder/decoder. 9 | A JWT encoder/decoder library for PHP. 10 | 11 | Neuman Vong 12 | lcfrs 13 | neuman+pear@twilio.com 14 | yes 15 | 16 | 17 | Firebase Operations 18 | firebase 19 | operations@firebase.com 20 | yes 21 | 22 | 2015-07-22 23 | 24 | 3.0.0 25 | 3.0.0 26 | 27 | 28 | beta 29 | beta 30 | 31 | BSD 3-Clause License 32 | 33 | Initial release with basic support for JWT encoding, decoding and signature verification. 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 5.1 47 | 48 | 49 | 1.7.0 50 | 51 | 52 | json 53 | 54 | 55 | hash 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0.1.0 64 | 0.1.0 65 | 66 | 67 | beta 68 | beta 69 | 70 | 2015-04-01 71 | BSD 3-Clause License 72 | 73 | Initial release with basic support for JWT encoding, decoding and signature verification. 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/src/BeforeValidException.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Anant Narayanan 19 | * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD 20 | * @link https://github.com/firebase/php-jwt 21 | */ 22 | class JWT 23 | { 24 | 25 | /** 26 | * When checking nbf, iat or expiration times, 27 | * we want to provide some extra leeway time to 28 | * account for clock skew. 29 | */ 30 | public static $leeway = 0; 31 | 32 | /** 33 | * Allow the current timestamp to be specified. 34 | * Useful for fixing a value within unit testing. 35 | * 36 | * Will default to PHP time() value if null. 37 | */ 38 | public static $timestamp = null; 39 | 40 | public static $supported_algs = array( 41 | 'HS256' => array('hash_hmac', 'SHA256'), 42 | 'HS512' => array('hash_hmac', 'SHA512'), 43 | 'HS384' => array('hash_hmac', 'SHA384'), 44 | 'RS256' => array('openssl', 'SHA256'), 45 | ); 46 | 47 | /** 48 | * Decodes a JWT string into a PHP object. 49 | * 50 | * @param string $jwt The JWT 51 | * @param string|array $key The key, or map of keys. 52 | * If the algorithm used is asymmetric, this is the public key 53 | * @param array $allowed_algs List of supported verification algorithms 54 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 55 | * 56 | * @return object The JWT's payload as a PHP object 57 | * 58 | * @throws UnexpectedValueException Provided JWT was invalid 59 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed 60 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' 61 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' 62 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim 63 | * 64 | * @uses jsonDecode 65 | * @uses urlsafeB64Decode 66 | */ 67 | public static function decode($jwt, $key, $allowed_algs = array()) 68 | { 69 | $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; 70 | 71 | if (empty($key)) { 72 | throw new InvalidArgumentException('Key may not be empty'); 73 | } 74 | if (!is_array($allowed_algs)) { 75 | throw new InvalidArgumentException('Algorithm not allowed'); 76 | } 77 | $tks = explode('.', $jwt); 78 | if (count($tks) != 3) { 79 | throw new UnexpectedValueException('Wrong number of segments'); 80 | } 81 | list($headb64, $bodyb64, $cryptob64) = $tks; 82 | if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { 83 | throw new UnexpectedValueException('Invalid header encoding'); 84 | } 85 | if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { 86 | throw new UnexpectedValueException('Invalid claims encoding'); 87 | } 88 | $sig = static::urlsafeB64Decode($cryptob64); 89 | 90 | if (empty($header->alg)) { 91 | throw new UnexpectedValueException('Empty algorithm'); 92 | } 93 | if (empty(static::$supported_algs[$header->alg])) { 94 | throw new UnexpectedValueException('Algorithm not supported'); 95 | } 96 | if (!in_array($header->alg, $allowed_algs)) { 97 | throw new UnexpectedValueException('Algorithm not allowed'); 98 | } 99 | if (is_array($key) || $key instanceof \ArrayAccess) { 100 | if (isset($header->kid)) { 101 | $key = $key[$header->kid]; 102 | } else { 103 | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); 104 | } 105 | } 106 | 107 | // Check the signature 108 | if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { 109 | throw new SignatureInvalidException('Signature verification failed'); 110 | } 111 | 112 | // Check if the nbf if it is defined. This is the time that the 113 | // token can actually be used. If it's not yet that time, abort. 114 | if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { 115 | throw new BeforeValidException( 116 | 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) 117 | ); 118 | } 119 | 120 | // Check that this token has been created before 'now'. This prevents 121 | // using tokens that have been created for later use (and haven't 122 | // correctly used the nbf claim). 123 | if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { 124 | throw new BeforeValidException( 125 | 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) 126 | ); 127 | } 128 | 129 | // Check if this token has expired. 130 | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { 131 | throw new ExpiredException('Expired token'); 132 | } 133 | 134 | return $payload; 135 | } 136 | 137 | /** 138 | * Converts and signs a PHP object or array into a JWT string. 139 | * 140 | * @param object|array $payload PHP object or array 141 | * @param string $key The secret key. 142 | * If the algorithm used is asymmetric, this is the private key 143 | * @param string $alg The signing algorithm. 144 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 145 | * @param mixed $keyId 146 | * @param array $head An array with header elements to attach 147 | * 148 | * @return string A signed JWT 149 | * 150 | * @uses jsonEncode 151 | * @uses urlsafeB64Encode 152 | */ 153 | public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) 154 | { 155 | $header = array('typ' => 'JWT', 'alg' => $alg); 156 | if ($keyId !== null) { 157 | $header['kid'] = $keyId; 158 | } 159 | if ( isset($head) && is_array($head) ) { 160 | $header = array_merge($head, $header); 161 | } 162 | $segments = array(); 163 | $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); 164 | $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); 165 | $signing_input = implode('.', $segments); 166 | 167 | $signature = static::sign($signing_input, $key, $alg); 168 | $segments[] = static::urlsafeB64Encode($signature); 169 | 170 | return implode('.', $segments); 171 | } 172 | 173 | /** 174 | * Sign a string with a given key and algorithm. 175 | * 176 | * @param string $msg The message to sign 177 | * @param string|resource $key The secret key 178 | * @param string $alg The signing algorithm. 179 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 180 | * 181 | * @return string An encrypted message 182 | * 183 | * @throws DomainException Unsupported algorithm was specified 184 | */ 185 | public static function sign($msg, $key, $alg = 'HS256') 186 | { 187 | if (empty(static::$supported_algs[$alg])) { 188 | throw new DomainException('Algorithm not supported'); 189 | } 190 | list($function, $algorithm) = static::$supported_algs[$alg]; 191 | switch($function) { 192 | case 'hash_hmac': 193 | return hash_hmac($algorithm, $msg, $key, true); 194 | case 'openssl': 195 | $signature = ''; 196 | $success = openssl_sign($msg, $signature, $key, $algorithm); 197 | if (!$success) { 198 | throw new DomainException("OpenSSL unable to sign data"); 199 | } else { 200 | return $signature; 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * Verify a signature with the message, key and method. Not all methods 207 | * are symmetric, so we must have a separate verify and sign method. 208 | * 209 | * @param string $msg The original message (header and body) 210 | * @param string $signature The original signature 211 | * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key 212 | * @param string $alg The algorithm 213 | * 214 | * @return bool 215 | * 216 | * @throws DomainException Invalid Algorithm or OpenSSL failure 217 | */ 218 | private static function verify($msg, $signature, $key, $alg) 219 | { 220 | if (empty(static::$supported_algs[$alg])) { 221 | throw new DomainException('Algorithm not supported'); 222 | } 223 | 224 | list($function, $algorithm) = static::$supported_algs[$alg]; 225 | switch($function) { 226 | case 'openssl': 227 | $success = openssl_verify($msg, $signature, $key, $algorithm); 228 | if (!$success) { 229 | throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string()); 230 | } else { 231 | return $signature; 232 | } 233 | case 'hash_hmac': 234 | default: 235 | $hash = hash_hmac($algorithm, $msg, $key, true); 236 | if (function_exists('hash_equals')) { 237 | return hash_equals($signature, $hash); 238 | } 239 | $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); 240 | 241 | $status = 0; 242 | for ($i = 0; $i < $len; $i++) { 243 | $status |= (ord($signature[$i]) ^ ord($hash[$i])); 244 | } 245 | $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); 246 | 247 | return ($status === 0); 248 | } 249 | } 250 | 251 | /** 252 | * Decode a JSON string into a PHP object. 253 | * 254 | * @param string $input JSON string 255 | * 256 | * @return object Object representation of JSON string 257 | * 258 | * @throws DomainException Provided string was invalid JSON 259 | */ 260 | public static function jsonDecode($input) 261 | { 262 | if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { 263 | /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you 264 | * to specify that large ints (like Steam Transaction IDs) should be treated as 265 | * strings, rather than the PHP default behaviour of converting them to floats. 266 | */ 267 | $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); 268 | } else { 269 | /** Not all servers will support that, however, so for older versions we must 270 | * manually detect large ints in the JSON string and quote them (thus converting 271 | *them to strings) before decoding, hence the preg_replace() call. 272 | */ 273 | $max_int_length = strlen((string) PHP_INT_MAX) - 1; 274 | $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); 275 | $obj = json_decode($json_without_bigints); 276 | } 277 | 278 | if (function_exists('json_last_error') && $errno = json_last_error()) { 279 | static::handleJsonError($errno); 280 | } elseif ($obj === null && $input !== 'null') { 281 | throw new DomainException('Null result with non-null input'); 282 | } 283 | return $obj; 284 | } 285 | 286 | /** 287 | * Encode a PHP object into a JSON string. 288 | * 289 | * @param object|array $input A PHP object or array 290 | * 291 | * @return string JSON representation of the PHP object or array 292 | * 293 | * @throws DomainException Provided object could not be encoded to valid JSON 294 | */ 295 | public static function jsonEncode($input) 296 | { 297 | $json = json_encode($input); 298 | if (function_exists('json_last_error') && $errno = json_last_error()) { 299 | static::handleJsonError($errno); 300 | } elseif ($json === 'null' && $input !== null) { 301 | throw new DomainException('Null result with non-null input'); 302 | } 303 | return $json; 304 | } 305 | 306 | /** 307 | * Decode a string with URL-safe Base64. 308 | * 309 | * @param string $input A Base64 encoded string 310 | * 311 | * @return string A decoded string 312 | */ 313 | public static function urlsafeB64Decode($input) 314 | { 315 | $remainder = strlen($input) % 4; 316 | if ($remainder) { 317 | $padlen = 4 - $remainder; 318 | $input .= str_repeat('=', $padlen); 319 | } 320 | return base64_decode(strtr($input, '-_', '+/')); 321 | } 322 | 323 | /** 324 | * Encode a string with URL-safe Base64. 325 | * 326 | * @param string $input The string you want encoded 327 | * 328 | * @return string The base64 encode of what you passed in 329 | */ 330 | public static function urlsafeB64Encode($input) 331 | { 332 | return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); 333 | } 334 | 335 | /** 336 | * Helper method to create a JSON error. 337 | * 338 | * @param int $errno An error number from json_last_error() 339 | * 340 | * @return void 341 | */ 342 | private static function handleJsonError($errno) 343 | { 344 | $messages = array( 345 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 346 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 347 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON' 348 | ); 349 | throw new DomainException( 350 | isset($messages[$errno]) 351 | ? $messages[$errno] 352 | : 'Unknown JSON error: ' . $errno 353 | ); 354 | } 355 | 356 | /** 357 | * Get the number of bytes in cryptographic strings. 358 | * 359 | * @param string 360 | * 361 | * @return int 362 | */ 363 | private static function safeStrlen($str) 364 | { 365 | if (function_exists('mb_strlen')) { 366 | return mb_strlen($str, '8bit'); 367 | } 368 | return strlen($str); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/src/SignatureInvalidException.php: -------------------------------------------------------------------------------- 1 |