Open Monograph Press  3.3.0
ValidatorFactory.inc.php
1 <?php
14 use Illuminate\Container\Container;
15 use Illuminate\Filesystem\Filesystem;
16 use Illuminate\Translation\FileLoader;
17 use Illuminate\Translation\Translator;
18 use Illuminate\Validation\DatabasePresenceVerifier;
19 use Illuminate\Validation\Factory;
20 
21 // Import VALIDATE_ACTION_... constants
22 import('lib.pkp.classes.services.interfaces.EntityWriteInterface');
23 
25 
38  static public function make($props, $rules, $messages = []) {
39 
40  // This configures a non-existent translation file, but it is necessary to
41  // instantiate Laravel's validator. We override the messages with our own
42  // translated strings before returning the validator.
43  $loader = new FileLoader(new Filesystem, 'lang');
44  $translator = new Translator($loader, 'en');
45  $validation = new Factory($translator, new Container);
46 
47  // Add custom validation rule which extends Laravel's email rule to accept
48  // @localhost addresses. @localhost addresses are only loosely validated
49  // for allowed characters.
50  $validation->extend('email_or_localhost', function($attribute, $value, $parameters, $validator) use ($validation) {
51  $emailValidator = $validation->make(
52  ['value' => $value],
53  ['value' => 'email']
54  );
55  if ($emailValidator->passes()) {
56  return true;
57  }
58  $regexValidator = $validation->make(
59  ['value' => $value],
60  ['value' => ['regex:/^[-a-zA-Z0-9!#\$%&\'\*\+\.\/=\?\^_\`\{\|\}~]*(@localhost)$/']]
61  );
62  if ($regexValidator->passes()) {
63  return true;
64  }
65 
66  return false;
67  });
68 
69  // Add custom validation rule for ISSNs
70  $validation->extend('issn', function($attribute, $value, $parameters, $validator) use ($validation) {
71  $regexValidator = $validation->make(['value' => $value], ['value' => 'regex:/^(\d{4})-(\d{3}[\dX])$/']);
72  if ($regexValidator->fails()) {
73  return false;
74  }
75  // ISSN check digit: http://www.loc.gov/issn/basics/basics-checkdigit.html
76  $numbers = str_replace('-', '', $value);
77  $check = 0;
78  for ($i = 0; $i < 7; $i++) {
79  $check += $numbers[$i] * (8 - $i);
80  }
81  $check = $check % 11;
82  switch ($check) {
83  case 0:
84  $check = '0';
85  break;
86  case 1:
87  $check = 'X';
88  break;
89  default:
90  $check = (string) (11 - $check);
91  }
92 
93  return ($numbers[7] === $check);
94  });
95 
96  // Add custom validation rule for orcids
97  $validation->extend('orcid', function($attribute, $value, $parameters, $validator) use ($validation) {
98  $orcidRegexValidator = $validation->make(
99  ['value' => $value],
100  ['value' => 'regex:/^http[s]?:\/\/orcid.org\/(\d{4})-(\d{4})-(\d{4})-(\d{3}[0-9X])$/']
101  );
102  if ($orcidRegexValidator->fails()) {
103  return false;
104  }
105  // ISNI check digit: http://www.isni.org/content/faq#FAQ16
106  $digits = preg_replace("/[^0-9X]/", "", $value);
107 
108  $total = 0;
109  for ($i = 0; $i < 15; $i++) {
110  $total = ($total + $digits[$i]) * 2;
111  }
112 
113  $remainder = $total % 11;
114  $result = (12 - $remainder) % 11;
115 
116  return ($digits[15] == ($result == 10 ? 'X' : $result));
117  });
118 
119  // Add custom validation rule for currency
120  $validation->extend('currency', function($attribute, $value, $parameters, $validator) {
121  $isoCodes = new \Sokil\IsoCodes\IsoCodesFactory();
122  $currency = $isoCodes->getCurrencies()->getByLetterCode($value);
123  return isset($currency);
124  });
125 
126  $validator = $validation->make($props, $rules, ValidatorFactory::getMessages($messages));
127 
128  return $validator;
129  }
130 
138  static public function getMessages($messages = []) {
139 
140  static $defaultMessages = [];
141 
142  if (empty($defaultMessages)) {
143  $defaultMessages = [
144  'accepted' => __('validator.accepted'),
145  'active_url' => __('validator.active_url'),
146  'after' => __('validator.after'),
147  'alpha' => __('validator.alpha'),
148  'alpha_dash' => __('validator.alpha_dash'),
149  'alpha_num' => __('validator.alpha_num'),
150  'array' => __('validator.array'),
151  'before' => __('validator.before'),
152  'between' => [
153  'numeric' => __('validator.between.numeric'),
154  'file' => __('validator.between.file'),
155  'string' => __('validator.between.string'),
156  'array' => __('validator.between.array'),
157  ],
158  'boolean' => __('validator.boolean'),
159  'confirmed' => __('validator.confirmed'),
160  'currency' => __('validator.currency'),
161  'date' => __('validator.date'),
162  'date_format' => __('validator.date_format'),
163  'different' => __('validator.different'),
164  'digits' => __('validator.digits'),
165  'digits_between' => __('validator.digits_between'),
166  'email' => __('validator.email'),
167  'email_or_localhost' => __('validator.email'),
168  'exists' => __('validator.exists'),
169  'filled' => __('validator.filled'),
170  'image' => __('validator.image'),
171  'in' => __('validator.in'),
172  'integer' => __('validator.integer'),
173  'ip' => __('validator.ip'),
174  'issn' => __('validator.issn'),
175  'json' => __('validator.json'),
176  'max' => [
177  'numeric' => __('validator.max.numeric'),
178  'file' => __('validator.max.file'),
179  'string' => __('validator.max.string'),
180  'array' => __('validator.max.array'),
181  ],
182  'mimes' => __('validator.mimes'),
183  'min' => [
184  'numeric' => __('validator.min.numeric'),
185  'file' => __('validator.min.file'),
186  'string' => __('validator.min.string'),
187  'array' => __('validator.min.array'),
188  ],
189  'not_in' => __('validator.not_in'),
190  'numeric' => __('validator.numeric'),
191  'present' => __('validator.present'),
192  'regex' => __('validator.regex'),
193  'required' => __('validator.required'),
194  'required_if' => __('validator.required_if'),
195  'required_unless' => __('validator.required_unless'),
196  'required_with' => __('validator.required_with'),
197  'required_with_all' => __('validator.required_with_all'),
198  'required_without' => __('validator.required_without'),
199  'required_without_all' => __('validator.required_without_all'),
200  'same' => __('validator.same'),
201  'size' => [
202  'numeric' => __('validator.size.numeric'),
203  'file' => __('validator.size.file'),
204  'string' => __('validator.size.string'),
205  'array' => __('validator.size.array'),
206  ],
207  'string' => __('validator.string'),
208  'timezone' => __('validator.timezone'),
209  'unique' => __('validator.unique'),
210  'url' => __('validator.url'),
211  ];
212  }
213 
214  $messages = array_merge($defaultMessages, $messages);
215 
216  // Convert variables in translated strings from {$variable} syntax to
217  // Laravel's :variable syntax.
218  foreach ($messages as $rule => $message) {
219  if (is_string($message)) {
220  $messages[$rule] = self::convertMessageSyntax($message);
221  } else {
222  foreach ($message as $subRule => $subMessage) {
223  $messages[$rule][$subRule] = self::convertMessageSyntax($subMessage);
224  }
225  }
226  }
227 
228  return $messages;
229  }
230 
238  static public function convertMessageSyntax($message) {
239  return preg_replace('/\{\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/', ':\1', $message);
240  }
241 
261  static public function required($validator, $action, $requiredProps, $multilingualProps, $allowedLocales, $primaryLocale) {
262  $validator->after(function($validator) use ($action, $requiredProps, $multilingualProps, $allowedLocales, $primaryLocale) {
263 
264  $allLocales = AppLocale::getAllLocales();
265  $primaryLocaleName = $primaryLocale;
266  foreach ($allLocales as $locale => $name) {
267  if ($locale === $primaryLocale) {
268  $primaryLocaleName = $name;
269  }
270  }
271 
272  $props = $validator->getData();
273 
274  foreach ($requiredProps as $requiredProp) {
275 
276  // Required multilingual props should only be
277  // required in the primary locale
278  if (in_array($requiredProp, $multilingualProps)) {
279  if ($action === VALIDATE_ACTION_ADD) {
280  if (self::isEmpty($props[$requiredProp]) || self::isEmpty($props[$requiredProp][$primaryLocale])) {
281  $validator->errors()->add($requiredProp . '.' . $primaryLocale, __('validator.required'));
282  }
283  } else {
284  if (isset($props[$requiredProp]) && array_key_exists($primaryLocale, $props[$requiredProp]) && self::isEmpty($props[$requiredProp][$primaryLocale])) {
285  if (count($allowedLocales) === 1) {
286  $validator->errors()->add($requiredProp, __('validator.required'));
287  } else {
288  $validator->errors()->add($requiredProp . '.' . $primaryLocale, __('form.requirePrimaryLocale', array('language' => $primaryLocaleName)));
289  }
290  }
291  }
292 
293  } else {
294  if (($action === VALIDATE_ACTION_ADD && self::isEmpty($props[$requiredProp])) ||
295  ($action === VALIDATE_ACTION_EDIT && array_key_exists($requiredProp, $props) && self::isEmpty($props[$requiredProp]))) {
296  $validator->errors()->add($requiredProp, __('validator.required'));
297  }
298  }
299  }
300  });
301  }
302 
308  static private function isEmpty($value) {
309  return $value == '';
310  }
311 
320  static public function allowedLocales($validator, $multilingualProps, $allowedLocales) {
321  $validator->after(function($validator) use ($multilingualProps, $allowedLocales) {
322  $props = $validator->getData();
323  foreach ($props as $propName => $propValue) {
324  if (!in_array($propName, $multilingualProps)) {
325  continue;
326  }
327  if (!is_array($propValue)) {
328  $validator->errors()->add($propName . '.' . current($allowedLocales), __('validator.localeExpected'));
329  break;
330  }
331  foreach ($propValue as $localeKey => $localeValue) {
332  if (!in_array($localeKey, $allowedLocales)) {
333  $validator->errors()->add($propName . '.' . $localeKey, __('validator.locale'));
334  break;
335  }
336  }
337  }
338  });
339  }
340 
354  static public function temporaryFilesExist($validator, $uploadProps, $multilingualUploadProps, $props, $allowedLocales, $userId) {
355  $validator->after(function($validator) use ($uploadProps, $multilingualUploadProps, $props, $allowedLocales, $userId) {
356  import('lib.pkp.classes.file.TemporaryFileManager');
357  $temporaryFileManager = new TemporaryFileManager();
358  foreach ($uploadProps as $uploadProp) {
359  if (!isset($props[$uploadProp])) {
360  continue;
361  }
362  if (in_array($uploadProp, $multilingualUploadProps)) {
363  foreach ($allowedLocales as $localeKey) {
364  if (!isset($props[$uploadProp][$localeKey])
365  || !isset($props[$uploadProp][$localeKey]['temporaryFileId'])
366  || $validator->errors()->get($uploadProp . '.' . $localeKey)) {
367  continue;
368  }
369  if (!$temporaryFileManager->getFile($props[$uploadProp][$localeKey]['temporaryFileId'], $userId)) {
370  $validator->errors()->add($uploadProp . '.' . $localeKey, __('common.noTemporaryFile'));
371  }
372  }
373  } else {
374  if (!isset($props[$uploadProp]['temporaryFileId'])) {
375  continue;
376  }
377  if (!$temporaryFileManager->getFile($props[$uploadProp]['temporaryFileId'], $userId)) {
378  $validator->errors()->add($uploadProp, __('common.noTemporaryFile'));
379  }
380  }
381  }
382  });
383  }
384 }
TemporaryFileManager
Definition: TemporaryFileManager.inc.php:19
PKPLocale\getAllLocales
static & getAllLocales()
Definition: PKPLocale.inc.php:537
ValidatorFactory\temporaryFilesExist
static temporaryFilesExist($validator, $uploadProps, $multilingualUploadProps, $props, $allowedLocales, $userId)
Definition: ValidatorFactory.inc.php:354
ValidatorFactory\allowedLocales
static allowedLocales($validator, $multilingualProps, $allowedLocales)
Definition: ValidatorFactory.inc.php:320
ValidatorFactory
A factory class for creating a Validator from the Laravel framework.
Definition: ValidatorFactory.inc.php:24
ValidatorFactory\convertMessageSyntax
static convertMessageSyntax($message)
Definition: ValidatorFactory.inc.php:238
ValidatorFactory\required
static required($validator, $action, $requiredProps, $multilingualProps, $allowedLocales, $primaryLocale)
Definition: ValidatorFactory.inc.php:261
ValidatorFactory\make
static make($props, $rules, $messages=[])
Definition: ValidatorFactory.inc.php:38
ValidatorFactory\getMessages
static getMessages($messages=[])
Definition: ValidatorFactory.inc.php:138