├── .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 | [](https://travis-ci.org/firebase/php-jwt)
2 | [](https://packagist.org/packages/firebase/php-jwt)
3 | [](https://packagist.org/packages/firebase/php-jwt)
4 | [](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 |