Open Journal Systems  3.3.0
APIHandler.inc.php
1 <?php
2 
15 AppLocale::requireComponents(LOCALE_COMPONENT_PKP_API, LOCALE_COMPONENT_APP_API);
16 import('lib.pkp.classes.handler.PKPHandler');
17 
18 use \Slim\App;
19 import('lib.pkp.classes.core.APIResponse');
20 import('classes.core.Services');
21 
22 class APIHandler extends PKPHandler {
23  protected $_app;
24  protected $_request;
25  protected $_endpoints = array();
26  protected $_slimRequest = null;
27 
29  protected $_pathPattern;
30 
32  protected $_handlerPath = null;
33 
37  public function __construct() {
38  parent::__construct();
39  import('lib.pkp.classes.security.authorization.internal.ApiAuthorizationMiddleware');
40  import('lib.pkp.classes.security.authorization.internal.ApiTokenDecodingMiddleware');
41  import('lib.pkp.classes.security.authorization.internal.ApiCsrfMiddleware');
42  $this->_app = new \Slim\App(array(
43  // Load custom response handler
44  'response' => function($c) {
45  return new APIResponse();
46  },
47  'settings' => array(
48  // we need access to route within middleware
49  'determineRouteBeforeAppMiddleware' => true,
50  )
51  ));
52  $this->_app->add(new ApiAuthorizationMiddleware($this));
53  $this->_app->add(new ApiCsrfMiddleware($this));
54  $this->_app->add(new ApiTokenDecodingMiddleware($this));
55  // remove trailing slashes
56  $this->_app->add(function ($request, $response, $next) {
57  $uri = $request->getUri();
58  $path = $uri->getPath();
59  if ($path != '/' && substr($path, -1) == '/') {
60  // path with trailing slashes to non-trailing counterpart
61  $uri = $uri->withPath(substr($path, 0, -1));
62  if($request->getMethod() == 'GET') {
63  return $response->withRedirect((string)$uri, 301);
64  } else {
65  return $next($request->withUri($uri), $response);
66  }
67  }
68  return $next($request, $response);
69  });
70  // if pathinfo is disabled, rewrite URI to match Slim's expectation
71  $app = $this->getApp();
72  $handler = $this;
73  $this->_app->add(function($request, $response, $next) use($app, $handler) {
74  $uri = $request->getUri();
75  $endpoint = trim($request->getQueryParam('endpoint'));
76  $pathInfoEnabled = Config::getVar('general', 'disable_path_info') ? false : true;
77  $path = $uri->getPath();
78  if (!$pathInfoEnabled && !is_null($endpoint) && !isset($_SERVER['PATH_INFO']) && ($path == '/')) {
79  $basePath = $uri->getBasePath();
80  if($request->getMethod() == 'GET') {
81  $uri = $uri->withPath($basePath . $endpoint);
82  return $response->withRedirect((string)$uri, 301);
83  } else {
113  $uri = $uri->withPath($basePath . $endpoint);
114  $handler->_slimRequest = $request->withUri($uri);
115  return $app->process($handler->_slimRequest, $response);
116  }
117  } elseif ($pathInfoEnabled) {
118  // pkp/pkp-lib#4919: PKP software routes with PATH_INFO (unaffected by
119  // mod_rewrite) but Slim relies on REQUEST_URI. Inject PATH_INFO into
120  // Slim for consistent behavior in URL rewriting scenarios.
121  $newUri = $uri->withPath($_SERVER['PATH_INFO']);
122  if ($uri != $newUri) {
123  $handler->_slimRequest = $request->withUri($newUri);
124  return $app->process($handler->_slimRequest, $response);
125  }
126  }
127  return $next($request, $response);
128  });
129  // Allow remote requests to the API
130  $this->_app->add(function ($request, $response, $next) {
131  $response = $response->withHeader('Access-Control-Allow-Origin', '*');
132  return $next($request, $response);
133  });
134  $this->_request = Application::get()->getRequest();
135  $this->setupEndpoints();
136  }
137 
143  public function getRequest() {
144  return $this->_request;
145  }
146 
152  public function getSlimRequest() {
153  return $this->_slimRequest;
154  }
155 
160  public function setSlimRequest($slimRequest) {
161  return $this->_slimRequest = $slimRequest;
162  }
163 
168  public function getApp() {
169  return $this->_app;
170  }
171 
180  public function getEndpointPattern() {
181 
182  if (!isset($this->_pathPattern)) {
183  $this->_pathPattern = '/{contextPath}/api/{version}/' . $this->_handlerPath;
184  }
185 
187  }
188 
194  public function getEntityId($parameterName) {
195  assert(false);
196  return null;
197  }
198 
202  public function setupEndpoints() {
203  $app = $this->getApp();
204  $endpoints = $this->getEndpoints();
205  HookRegistry::call('APIHandler::endpoints', [&$endpoints, $this]);
206  foreach ($endpoints as $method => $definitions) {
207  foreach ($definitions as $parameters) {
208  $method = strtolower($method);
209  $pattern = $parameters['pattern'];
210  $handler = $parameters['handler'];
211  $roles = isset($parameters['roles']) ? $parameters['roles'] : null;
212  $app->$method($pattern, $handler)->setName($handler[1]);
213  if (!is_null($roles) && is_array($roles)) {
214  $this->addRoleAssignment($roles, $handler[1]);
215  }
216  }
217  }
218  }
219 
225  public function getEndpoints() {
226  return $this->_endpoints;
227  }
228 
237  public function getParameter($parameterName, $default = null) {
238  $slimRequest = $this->getSlimRequest();
239  if ($slimRequest == null) {
240  return $default;
241  }
242 
243  $route = $slimRequest->getAttribute('route');
244 
245  // we probably have an invalid url if route is null
246  if (!is_null($route)) {
247  $arguments = $route->getArguments();
248  if (isset($arguments[$parameterName])) {
249  return $arguments[$parameterName];
250  }
251 
252  $queryParams = $slimRequest->getQueryParams();
253  if (isset($queryParams[$parameterName])) {
254  return $queryParams[$parameterName];
255  }
256  }
257 
258  return $default;
259  }
260 
275  public function convertStringsToSchema($schema, $params) {
276  $schemaService = Services::get('schema');
277  $schema = $schemaService->get($schema);
278 
279  foreach ($params as $paramName => $paramValue) {
280  if (!property_exists($schema->properties, $paramName)) {
281  continue;
282  }
283  if (!empty($schema->properties->{$paramName}->multilingual)) {
284  foreach ($paramValue as $localeKey => $localeValue) {
285  $params[$paramName][$localeKey] = $this->_convertStringsToSchema(
286  $localeValue,
287  $schema->properties->{$paramName}->type,
288  $schema->properties->{$paramName}
289  );
290  }
291  } else {
292  $params[$paramName] = $this->_convertStringsToSchema(
293  $paramValue,
294  $schema->properties->{$paramName}->type,
295  $schema->properties->{$paramName}
296  );
297  }
298  }
299 
300  return $params;
301  }
302 
313  private function _convertStringsToSchema($value, $type, $schema) {
314  // Convert all empty strings to null except arrays (see note below)
315  if (is_string($value) && !strlen($value) && $type !== 'array') {
316  return null;
317  }
318  switch ($type) {
319  case 'boolean':
320  if (is_string($value)) {
321  if ($value === 'true' || $value === '1') {
322  return true;
323  } elseif ($value === 'false' || $value === '0') {
324  return false;
325  }
326  }
327  break;
328  case 'integer':
329  if (is_string($value) && ctype_digit($value)) {
330  return (int) $value;
331  }
332  break;
333  case 'number':
334  if (is_string($value) && is_numeric($value)) {
335  return floatval($value);
336  }
337  break;
338  case 'array':
339  if (is_array($value)) {
340  $newArray = [];
341  if (is_array($schema->items)) {
342  foreach ($schema->items as $i => $itemSchema) {
343  $newArray[$i] = $this->_convertStringsToSchema($value[$i], $itemSchema->type, $itemSchema);
344  }
345  } else {
346  foreach ($value as $i => $v) {
347  $newArray[$i] = $this->_convertStringsToSchema($v, $schema->items->type, $schema->items);
348  }
349  }
350  return $newArray;
351 
352  // An empty string is accepted as an empty array. This addresses the
353  // issue where browsers strip empty arrays from post data before sending.
354  // See: https://bugs.jquery.com/ticket/6481
355  } elseif (is_string($value) && !strlen($value)) {
356  return [];
357  }
358  break;
359  case 'object':
360  if (is_array($value)) {
361  $newObject = [];
362  foreach ($schema->properties as $propName => $propSchema) {
363  if (!isset($value[$propName])) {
364  continue;
365  }
366  $newObject[$propName] = $this->_convertStringsToSchema($value[$propName], $propSchema->type, $propSchema);
367  }
368  return $value;
369  }
370  break;
371  }
372  return $value;
373  }
374 
390  protected function _validateStatDates($params, $dateStartParam = 'dateStart', $dateEndParam = 'dateEnd') {
391  import('lib.pkp.classes.validation.ValidatorFactory');
392  $validator = \ValidatorFactory::make(
393  $params,
394  [
395  $dateStartParam => [
396  'date_format:Y-m-d',
397  'after_or_equal:' . STATISTICS_EARLIEST_DATE,
398  'before_or_equal:' . $dateEndParam,
399  ],
400  $dateEndParam => [
401  'date_format:Y-m-d',
402  'before_or_equal:yesterday',
403  'after_or_equal:' . $dateStartParam,
404  ],
405  ],
406  [
407  '*.date_format' => 'invalidFormat',
408  $dateStartParam . '.after_or_equal' => 'tooEarly',
409  $dateEndParam . '.before_or_equal' => 'tooLate',
410  $dateStartParam . '.before_or_equal' => 'invalidRange',
411  $dateEndParam . '.after_or_equal' => 'invalidRange',
412  ]
413  );
414 
415  if ($validator->fails()) {
416  $errors = $validator->errors()->getMessages();
417  if ((!empty($errors[$dateStartParam]) && in_array('invalidFormat', $errors[$dateStartParam]))
418  || (!empty($errors[$dateEndParam]) && in_array('invalidFormat', $errors[$dateEndParam]))) {
419  return 'api.stats.400.wrongDateFormat';
420  }
421  if (!empty($errors[$dateStartParam]) && in_array('tooEarly', $errors[$dateStartParam])) {
422  return 'api.stats.400.earlyDateRange';
423  }
424  if (!empty($errors[$dateEndParam]) && in_array('tooLate', $errors[$dateEndParam])) {
425  return 'api.stats.400.lateDateRange';
426  }
427  if ((!empty($errors[$dateStartParam]) && in_array('invalidRange', $errors[$dateStartParam]))
428  || (!empty($errors[$dateEndParam]) && in_array('invalidRange', $errors[$dateEndParam]))) {
429  return 'api.stats.400.wrongDateRange';
430  }
431  }
432 
433  return true;
434  }
435 }
PKPHandler\addRoleAssignment
addRoleAssignment($roleIds, $operations)
Definition: PKPHandler.inc.php:213
APIHandler\__construct
__construct()
Definition: APIHandler.inc.php:43
AppLocale\requireComponents
static requireComponents()
Definition: env1/MockAppLocale.inc.php:56
APIHandler\getEntityId
getEntityId($parameterName)
Definition: APIHandler.inc.php:200
APIHandler\getParameter
getParameter($parameterName, $default=null)
Definition: APIHandler.inc.php:243
APIHandler\setSlimRequest
setSlimRequest($slimRequest)
Definition: APIHandler.inc.php:166
APIHandler\_validateStatDates
_validateStatDates($params, $dateStartParam='dateStart', $dateEndParam='dateEnd')
Definition: APIHandler.inc.php:396
APIHandler\setupEndpoints
setupEndpoints()
Definition: APIHandler.inc.php:208
APIHandler\$_app
$_app
Definition: APIHandler.inc.php:23
APIHandler\$_pathPattern
$_pathPattern
Definition: APIHandler.inc.php:32
PKPHandler
Definition: PKPHandler.inc.php:17
APIHandler\getEndpoints
getEndpoints()
Definition: APIHandler.inc.php:231
APIHandler\getSlimRequest
getSlimRequest()
Definition: APIHandler.inc.php:158
Config\getVar
static getVar($section, $key, $default=null)
Definition: Config.inc.php:35
ValidatorFactory\make
static make($props, $rules, $messages=[])
Definition: ValidatorFactory.inc.php:38
APIHandler
Base request API handler.
Definition: APIHandler.inc.php:22
ApiCsrfMiddleware
Slim middleware which requires a CSRF token for POST, PUT and DELETE operations whenever an API Token...
Definition: ApiCsrfMiddleware.inc.php:17
APIHandler\$_request
$_request
Definition: APIHandler.inc.php:24
ApiAuthorizationMiddleware
Slim middleware which enforces authorization policies.
Definition: ApiAuthorizationMiddleware.inc.php:16
APIHandler\$_endpoints
$_endpoints
Definition: APIHandler.inc.php:25
APIHandler\convertStringsToSchema
convertStringsToSchema($schema, $params)
Definition: APIHandler.inc.php:281
PKPApplication\get
static get()
Definition: PKPApplication.inc.php:235
APIHandler\getRequest
getRequest()
Definition: APIHandler.inc.php:149
ApiTokenDecodingMiddleware
Slim middleware which decodes and validates JSON Web Tokens.
Definition: ApiTokenDecodingMiddleware.inc.php:18
APIHandler\$_slimRequest
$_slimRequest
Definition: APIHandler.inc.php:26
APIResponse
Extends the Response class in the Slim microframework.
Definition: APIResponse.inc.php:16
APIHandler\getEndpointPattern
getEndpointPattern()
Definition: APIHandler.inc.php:186
HookRegistry\call
static call($hookName, $args=null)
Definition: HookRegistry.inc.php:86
APIHandler\$_handlerPath
$_handlerPath
Definition: APIHandler.inc.php:38
APIHandler\getApp
getApp()
Definition: APIHandler.inc.php:174
PKPServices\get
static get($service)
Definition: PKPServices.inc.php:49