Open Journal Systems  3.3.0
PKPSchemaService.inc.php
1 <?php
15 namespace PKP\Services;
16 
17 define('SCHEMA_ANNOUNCEMENT', 'announcement');
18 define('SCHEMA_AUTHOR', 'author');
19 define('SCHEMA_CONTEXT', 'context');
20 define('SCHEMA_EMAIL_TEMPLATE', 'emailTemplate');
21 define('SCHEMA_GALLEY', 'galley');
22 define('SCHEMA_ISSUE', 'issue');
23 define('SCHEMA_PUBLICATION', 'publication');
24 define('SCHEMA_REVIEW_ASSIGNMENT', 'reviewAssignment');
25 define('SCHEMA_REVIEW_ROUND', 'reviewRound');
26 define('SCHEMA_SECTION', 'section');
27 define('SCHEMA_SITE', 'site');
28 define('SCHEMA_SUBMISSION', 'submission');
29 define('SCHEMA_SUBMISSION_FILE', 'submissionFile');
30 define('SCHEMA_USER', 'user');
31 define('SCHEMA_USER_GROUP', 'userGroup');
32 
35  private $_schemas = [];
36 
49  public function get($schemaName, $forceReload = false) {
50 
51  if (!$forceReload && array_key_exists($schemaName, $this->_schemas)) {
52  return $this->_schemas[$schemaName];
53  }
54 
55  $schemaFile = sprintf('%s/lib/pkp/schemas/%s.json', BASE_SYS_DIR, $schemaName);
56  if (file_exists($schemaFile)) {
57  $schema = json_decode(file_get_contents($schemaFile));
58  if (!$schema) {
59  fatalError('Schema failed to decode. This usually means it is invalid JSON. Requested: ' . $schemaFile . '. Last JSON error: ' . json_last_error());
60  }
61  } else {
62  // allow plugins to create a custom schema and load it via hook
63  $schema = new \stdClass();
64  }
65 
66  // Merge an app-specific schema file if it exists
67  $appSchemaFile = sprintf('%s/schemas/%s.json', BASE_SYS_DIR, $schemaName);
68  if (file_exists($appSchemaFile)) {
69  $appSchema = json_decode(file_get_contents($appSchemaFile));
70  if (!$appSchema) {
71  fatalError('Schema failed to decode. This usually means it is invalid JSON. Requested: ' . $appSchemaFile . '. Last JSON error: ' . json_last_error());
72  }
73  $schema = $this->merge($schema, $appSchema);
74  }
75 
76  \HookRegistry::call('Schema::get::' . $schemaName, array(&$schema));
77 
78  $this->_schemas[$schemaName] = $schema;
79 
80  return $schema;
81  }
82 
97  public function merge($baseSchema, $additionalSchema) {
98  $newSchema = clone $baseSchema;
99  if (!empty($additionalSchema->title)) {
100  $newSchema->title = $additionalSchema->title;
101  }
102  if (!empty($additionalSchema->description)) {
103  $newSchema->description = $additionalSchema->description;
104  }
105  if (!empty($additionalSchema->properties)) {
106  if (empty($newSchema->properties)) {
107  $newSchema->properties = new \stdClass();
108  }
109  foreach ($additionalSchema->properties as $propName => $propSchema) {
110  $newSchema->properties->{$propName} = $propSchema;
111  }
112  }
113 
114  return $newSchema;
115  }
116 
126  public function getSummaryProps($schemaName) {
127  $schema = $this->get($schemaName);
128  $props = [];
129  foreach ($schema->properties as $propName => $propSchema) {
130  if (property_exists($propSchema, 'apiSummary') && $propSchema->apiSummary) {
131  $props[] = $propName;
132  }
133  }
134 
135  return $props;
136  }
137 
147  public function getFullProps($schemaName) {
148  $schema = $this->get($schemaName);
149 
150  $propNames = [];
151  foreach ($schema->properties as $propName => $propSchema) {
152  if (empty($propSchema->writeOnly)) {
153  $propNames[] = $propName;
154  }
155  }
156 
157  return $propNames;
158  }
159 
166  public function getRequiredProps($schemaName) {
167  $schema = $this->get($schemaName);
168 
169  if (!empty($schema->required)) {
170  return $schema->required;
171  }
172  return [];
173  }
174 
181  public function getMultilingualProps($schemaName) {
182  $schema = $this->get($schemaName);
183 
184  $multilingualProps = [];
185  foreach ($schema->properties as $propName => $propSchema) {
186  if (!empty($propSchema->multilingual)) {
187  $multilingualProps[] = $propName;
188  }
189  }
190 
191  return $multilingualProps;
192  }
193 
204  public function sanitize($schemaName, $props) {
205  $schema = $this->get($schemaName);
206  $cleanProps = [];
207 
208  foreach ($props as $propName => $propValue) {
209  if (empty($schema->properties->{$propName})
210  || empty($schema->properties->{$propName}->type)
211  || !empty($schema->properties->{$propName}->readOnly)) {
212  continue;
213  }
214  $propSchema = $schema->properties->{$propName};
215  if (!empty($propSchema->multilingual)) {
216  $values = [];
217  foreach ((array) $propValue as $localeKey => $localeValue) {
218  $values[$localeKey] = $this->coerce($localeValue, $propSchema->type, $propSchema);
219  }
220  if (!empty($values)) {
221  $cleanProps[$propName] = $values;
222  }
223  } else {
224  $cleanProps[$propName] = $this->coerce($propValue, $propSchema->type, $propSchema);
225  }
226  }
227 
228  return $cleanProps;
229  }
230 
241  public function coerce($value, $type, $schema) {
242  if (is_null($value)) {
243  return $value;
244  }
245  switch ($type) {
246  case 'boolean':
247  return (bool) $value;
248  case 'integer':
249  return (int) $value;
250  case 'number':
251  return (float) $value;
252  case 'string':
253  if (is_object($value) || is_array($value)) {
254  $value = serialize($value);
255  }
256  return (string) $value;
257  case 'array':
258  $newArray = [];
259  if (is_array($schema->items)) {
260  foreach ($schema->items as $i => $itemSchema) {
261  $newArray[$i] = $this->coerce($value[$i], $itemSchema->type, $itemSchema);
262  }
263  } elseif (is_array($value)) {
264  foreach ($value as $i => $v) {
265  $newArray[$i] = $this->coerce($v, $schema->items->type, $schema->items);
266  }
267  } else {
268  $newArray[] = serialize($value);
269  }
270  return $newArray;
271  case 'object':
272  $newObject = []; // we handle JSON objects as assoc arrays in PHP
273  foreach ($schema->properties as $propName => $propSchema) {
274  if (!isset($value[$propName]) || !empty($propSchema->readOnly)) {
275  continue;
276  }
277  $newObject[$propName] = $this->coerce($value[$propName], $propSchema->type, $propSchema);
278  }
279  return $newObject;
280  }
281  fatalError('Requested variable coercion for a type that was not recognized: ' . $type);
282  }
283 
294  public function getValidationRules($schemaName, $allowedLocales) {
295  $schema = $this->get($schemaName);
296 
297  $rules = [];
298  foreach ($schema->properties as $propName => $propSchema) {
299  if (!empty($propSchema->multilingual)) {
300  foreach ($allowedLocales as $localeKey) {
301  $rules = $this->addPropValidationRules($rules, $propName . '.' . $localeKey, $propSchema);
302  }
303  } else {
304  $rules = $this->addPropValidationRules($rules, $propName, $propSchema);
305  }
306  }
307 
308  return $rules;
309  }
310 
317  public function addPropValidationRules($rules, $ruleKey, $propSchema) {
318  if (!empty($propSchema->readOnly)) {
319  return $rules;
320  }
321  switch ($propSchema->type) {
322  case 'boolean':
323  case 'integer':
324  case 'numeric':
325  case 'string':
326  $rules[$ruleKey] = [$propSchema->type];
327  if (!empty($propSchema->validation)) {
328  $rules[$ruleKey] = array_merge($rules[$ruleKey], $propSchema->validation);
329  }
330  break;
331  case 'array':
332  if ($propSchema->items->type === 'object') {
333  $rules = $this->addPropValidationRules($rules, $ruleKey . '.*', $propSchema->items);
334  } else {
335  $rules[$ruleKey] = ['array'];
336  if (!empty($propSchema->validation)) {
337  $rules[$ruleKey] = array_merge($rules[$ruleKey], $propSchema->validation);
338  }
339  $rules[$ruleKey . '.*'] = [$propSchema->items->type];
340  if (!empty($propSchema->items->validation)) {
341  $rules[$ruleKey . '.*'] = array_merge($rules[$ruleKey . '.*'], $propSchema->items->validation);
342  }
343  }
344  break;
345  case 'object':
346  foreach ($propSchema->properties as $subPropName => $subPropSchema) {
347  $rules = $this->addPropValidationRules($rules, $ruleKey . '.' . $subPropName, $subPropSchema);
348  }
349  break;
350  }
351 
352  return $rules;
353  }
354 
382  public function formatValidationErrors($errorBag, $schema, $locales) {
383  $errors = $errorBag->getMessages();
384  $formatted = [];
385 
386  foreach ($errors as $ruleKey => $messages) {
387  $ruleKeyParts = explode('.', $ruleKey);
388  $propName = $ruleKeyParts[0];
389  if (!isset($formatted[$propName])) {
390  $formatted[$propName] = [];
391  }
392  if (!empty($schema->properties->{$propName}) && !empty($schema->properties->{$propName}->multilingual)) {
393  $localeKey = $ruleKeyParts[1];
394  if (!isset($formatted[$propName][$localeKey])) {
395  $formatted[$propName][$localeKey] = [];
396  }
397  $formatted[$propName][$localeKey] = array_merge($formatted[$propName][$localeKey], $messages);
398  } else {
399  $formatted[$propName] = array_merge($formatted[$propName], $messages);
400  }
401  }
402 
403  return $formatted;
404  }
405 
429  public function setDefaults($schemaName, $object, $supportedLocales, $primaryLocale, $localeParams = array()) {
430  $schema = $this->get($schemaName);
431 
432  // Ensure the locale files are loaded for all required locales
433  foreach ($supportedLocales as $localeKey) {
435  LOCALE_COMPONENT_PKP_DEFAULT, LOCALE_COMPONENT_APP_DEFAULT,
436  LOCALE_COMPONENT_PKP_COMMON, LOCALE_COMPONENT_APP_COMMON,
437  $localeKey
438  );
439  }
440 
441  foreach ($schema->properties as $propName => $propSchema) {
442  // Don't override existing values
443  if (!is_null($object->getData($propName))) {
444  continue;
445  }
446  if (!property_exists($propSchema, 'default') && !property_exists($propSchema, 'defaultLocaleKey')) {
447  continue;
448  }
449  if (!empty($propSchema->multilingual)) {
450  $value = [];
451  foreach ($supportedLocales as $localeKey) {
452  $value[$localeKey] = $this->getDefault($propSchema, $localeParams, $localeKey);
453  }
454  } else {
455  $value = $this->getDefault($propSchema, $localeParams, $primaryLocale);
456  }
457  $object->setData($propName, $value);
458  }
459 
460  return $object;
461  }
462 
471  public function getLocaleDefaults($schemaName, $locale, $localeParams) {
472  $schema = $this->get($schemaName);
473  \AppLocale::requireComponents(LOCALE_COMPONENT_PKP_DEFAULT, LOCALE_COMPONENT_APP_DEFAULT, $locale);
474 
475  $defaults = [];
476  foreach ($schema->properties as $propName => $propSchema) {
477  if (empty($propSchema->multilingual) || empty($propSchema->defaultLocaleKey)) {
478  continue;
479  }
480  $defaults[$propName] = $this->getDefault($propSchema, $localeParams, $locale);
481  }
482 
483  return $defaults;
484  }
485 
494  public function getDefault($propSchema, $localeParams = null, $localeKey = null) {
495  switch ($propSchema->type) {
496  case 'boolean':
497  case 'integer':
498  case 'number':
499  case 'string':
500  if (property_exists($propSchema, 'default')) {
501  return $propSchema->default;
502  } elseif (property_exists($propSchema, 'defaultLocaleKey')) {
503  return __($propSchema->defaultLocaleKey, $localeParams, $localeKey);
504  }
505  break;
506  case 'array':
507  $value = [];
508  foreach ($propSchema->default as $default) {
509  $itemSchema = $propSchema->items;
510  $itemSchema->default = $default;
511  $value[] = $this->getDefault($itemSchema, $localeParams, $localeKey);
512  }
513  return $value;
514  case 'object':
515  $value = [];
516  foreach ($propSchema->properties as $subPropName => $subPropSchema) {
517  if (!property_exists($propSchema->default, $subPropName)) {
518  continue;
519  }
520  $defaultSubProp = $propSchema->default->{$subPropName};
521  // If a prop is expected to be a string but the default value is an
522  // object with a `defaultLocaleKey` property, then we render that
523  // translation. Othewrise, we assign the values as-is and do not
524  // recursively check for nested objects/arrays inside of objects.
525  if ($subPropSchema->type === 'string' && is_object($defaultSubProp) && property_exists($defaultSubProp, 'defaultLocaleKey')) {
526  $value[$subPropName] = __($defaultSubProp->defaultLocaleKey, $localeParams, $localeKey);
527  } else {
528  $value[$subPropName] = $defaultSubProp;
529  }
530  }
531  return $value;
532  }
533  return null;
534  }
535 
565  public function addMissingMultilingualValues($schemaName, $values, $localeKeys) {
566  $schema = $this->get($schemaName);
567  $multilingualProps = $this->getMultilingualProps($schemaName);
568 
569  foreach ($values as $key => $value) {
570  if (!in_array($key, $multilingualProps)) {
571  continue;
572  }
573  foreach ($localeKeys as $localeKey) {
574  if (is_array($value) && array_key_exists($localeKey, $value)) {
575  continue;
576  }
577  switch ($schema->properties->{$key}->type) {
578  case 'string':
579  $values[$key][$localeKey] = '';
580  break;
581  case 'array':
582  $values[$key][$localeKey] = [];
583  break;
584  default:
585  $values[$key][$localeKey] = null;
586  }
587  }
588  }
589 
590  return $values;
591  }
592 }
PKP\Services
PKP\Services\PKPSchemaService\setDefaults
setDefaults($schemaName, $object, $supportedLocales, $primaryLocale, $localeParams=array())
Definition: PKPSchemaService.inc.php:432
AppLocale\requireComponents
static requireComponents()
Definition: env1/MockAppLocale.inc.php:56
PKP\Services\PKPSchemaService\coerce
coerce($value, $type, $schema)
Definition: PKPSchemaService.inc.php:244
BASE_SYS_DIR
const BASE_SYS_DIR(!defined('DIRECTORY_SEPARATOR'))
Definition: lib/pkp/includes/bootstrap.inc.php:32
PKP\Services\PKPSchemaService\merge
merge($baseSchema, $additionalSchema)
Definition: PKPSchemaService.inc.php:100
PKP\Services\PKPSchemaService\getDefault
getDefault($propSchema, $localeParams=null, $localeKey=null)
Definition: PKPSchemaService.inc.php:497
PKP\Services\PKPSchemaService
Definition: PKPSchemaService.inc.php:33
GuzzleHttp\json_decode
json_decode($json, $assoc=false, $depth=512, $options=0)
Definition: guzzlehttp/guzzle/src/functions.php:301
PKP\Services\PKPSchemaService\getValidationRules
getValidationRules($schemaName, $allowedLocales)
Definition: PKPSchemaService.inc.php:297
PKP\Services\PKPSchemaService\getFullProps
getFullProps($schemaName)
Definition: PKPSchemaService.inc.php:150
PKP\Services\PKPSchemaService\getSummaryProps
getSummaryProps($schemaName)
Definition: PKPSchemaService.inc.php:129
PKP\Services\PKPSchemaService\addMissingMultilingualValues
addMissingMultilingualValues($schemaName, $values, $localeKeys)
Definition: PKPSchemaService.inc.php:568
PKP\Services\PKPSchemaService\formatValidationErrors
formatValidationErrors($errorBag, $schema, $locales)
Definition: PKPSchemaService.inc.php:385
PKP\Services\PKPSchemaService\getMultilingualProps
getMultilingualProps($schemaName)
Definition: PKPSchemaService.inc.php:184
fatalError
if(!function_exists('import')) fatalError($reason)
Definition: functions.inc.php:32
PKP\Services\PKPSchemaService\addPropValidationRules
addPropValidationRules($rules, $ruleKey, $propSchema)
Definition: PKPSchemaService.inc.php:320
PKP\Services\PKPSchemaService\getLocaleDefaults
getLocaleDefaults($schemaName, $locale, $localeParams)
Definition: PKPSchemaService.inc.php:474
HookRegistry\call
static call($hookName, $args=null)
Definition: HookRegistry.inc.php:86
PKP\Services\PKPSchemaService\sanitize
sanitize($schemaName, $props)
Definition: PKPSchemaService.inc.php:207
PKP\Services\PKPSchemaService\getRequiredProps
getRequiredProps($schemaName)
Definition: PKPSchemaService.inc.php:169