blob: e131d5b493a1c29b9604d025ea8c467da7ea8b75 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php namespace Sieve;
2
3require_once('SieveKeywordRegistry.php');
4require_once('SieveToken.php');
5require_once('SieveException.php');
6
7class SieveSemantics
8{
9 protected static $requiredExtensions_ = array();
10
11 protected $comparator_;
12 protected $matchType_;
13 protected $addressPart_;
14 protected $tags_ = array();
15 protected $arguments_;
16 protected $deps_ = array();
17 protected $followupToken_;
18
19 public function __construct($token, $prevToken)
20 {
21 $this->registry_ = SieveKeywordRegistry::get();
22 $command = strtolower($token->text);
23
24 // Check the registry for $command
25 if ($this->registry_->isCommand($command))
26 {
27 $xml = $this->registry_->command($command);
28 $this->arguments_ = $this->makeArguments_($xml);
29 $this->followupToken_ = SieveToken::Semicolon;
30 }
31 else if ($this->registry_->isTest($command))
32 {
33 $xml = $this->registry_->test($command);
34 $this->arguments_ = $this->makeArguments_($xml);
35 $this->followupToken_ = SieveToken::BlockStart;
36 }
37 else
38 {
39 throw new SieveException($token, 'unknown command '. $command);
40 }
41
42 // Check if command may appear at this position within the script
43 if ($this->registry_->isTest($command))
44 {
45 if (is_null($prevToken))
46 throw new SieveException($token, $command .' may not appear as first command');
47
48 if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text))
49 throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
50 }
51 else if (isset($prevToken))
52 {
53 switch ($command)
54 {
55 case 'require':
56 $valid_after = 'require';
57 break;
58 case 'elsif':
59 case 'else':
60 $valid_after = '(if|elsif)';
61 break;
62 default:
63 $valid_after = $this->commandsRegex_();
64 }
65
66 if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text))
67 throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
68 }
69
70 // Check for extension arguments to add to the command
71 foreach ($this->registry_->arguments($command) as $arg)
72 {
73 switch ((string) $arg['type'])
74 {
75 case 'tag':
76 array_unshift($this->arguments_, array(
77 'type' => SieveToken::Tag,
78 'occurrence' => $this->occurrence_($arg),
79 'regex' => $this->regex_($arg),
80 'call' => 'tagHook_',
81 'name' => $this->name_($arg),
82 'subArgs' => $this->makeArguments_($arg->children())
83 ));
84 break;
85 }
86 }
87 }
88
89 public function __destruct()
90 {
91 $this->registry_->put();
92 }
93
94 // TODO: the *Regex functions could possibly also be static properties
95 protected function requireStringsRegex_()
96 {
97 return '('. implode('|', $this->registry_->requireStrings()) .')';
98 }
99
100 protected function matchTypeRegex_()
101 {
102 return '('. implode('|', $this->registry_->matchTypes()) .')';
103 }
104
105 protected function addressPartRegex_()
106 {
107 return '('. implode('|', $this->registry_->addressParts()) .')';
108 }
109
110 protected function commandsRegex_()
111 {
112 return '('. implode('|', $this->registry_->commands()) .')';
113 }
114
115 protected function testsRegex_()
116 {
117 return '('. implode('|', $this->registry_->tests()) .')';
118 }
119
120 protected function comparatorRegex_()
121 {
122 return '('. implode('|', $this->registry_->comparators()) .')';
123 }
124
125 protected function occurrence_($arg)
126 {
127 if (isset($arg['occurrence']))
128 {
129 switch ((string) $arg['occurrence'])
130 {
131 case 'optional':
132 return '?';
133 case 'any':
134 return '*';
135 case 'some':
136 return '+';
137 }
138 }
139 return '1';
140 }
141
142 protected function name_($arg)
143 {
144 if (isset($arg['name']))
145 {
146 return (string) $arg['name'];
147 }
148 return (string) $arg['type'];
149 }
150
151 protected function regex_($arg)
152 {
153 if (isset($arg['regex']))
154 {
155 return (string) $arg['regex'];
156 }
157 return '.*';
158 }
159
160 protected function case_($arg)
161 {
162 if (isset($arg['case']))
163 {
164 return (string) $arg['case'];
165 }
166 return 'adhere';
167 }
168
169 protected function follows_($arg)
170 {
171 if (isset($arg['follows']))
172 {
173 return (string) $arg['follows'];
174 }
175 return '.*';
176 }
177
178 protected function makeValue_($arg)
179 {
180 if (isset($arg->value))
181 {
182 $res = $this->makeArguments_($arg->value);
183 return array_shift($res);
184 }
185 return null;
186 }
187
188 /**
189 * Convert an extension (test) commands parameters from XML to
190 * a PHP array the {@see Semantics} class understands.
191 * @param array(SimpleXMLElement) $parameters
192 * @return array
193 */
194 protected function makeArguments_($parameters)
195 {
196 $arguments = array();
197
198 foreach ($parameters as $arg)
199 {
200 // Ignore anything not a <parameter>
201 if ($arg->getName() != 'parameter')
202 continue;
203
204 switch ((string) $arg['type'])
205 {
206 case 'addresspart':
207 array_push($arguments, array(
208 'type' => SieveToken::Tag,
209 'occurrence' => $this->occurrence_($arg),
210 'regex' => $this->addressPartRegex_(),
211 'call' => 'addressPartHook_',
212 'name' => 'address part',
213 'subArgs' => $this->makeArguments_($arg)
214 ));
215 break;
216
217 case 'block':
218 array_push($arguments, array(
219 'type' => SieveToken::BlockStart,
220 'occurrence' => '1',
221 'regex' => '{',
222 'name' => 'block',
223 'subArgs' => $this->makeArguments_($arg)
224 ));
225 break;
226
227 case 'comparator':
228 array_push($arguments, array(
229 'type' => SieveToken::Tag,
230 'occurrence' => $this->occurrence_($arg),
231 'regex' => 'comparator',
232 'name' => 'comparator',
233 'subArgs' => array( array(
234 'type' => SieveToken::String,
235 'occurrence' => '1',
236 'call' => 'comparatorHook_',
237 'case' => 'adhere',
238 'regex' => $this->comparatorRegex_(),
239 'name' => 'comparator string',
240 'follows' => 'comparator'
241 ))
242 ));
243 break;
244
245 case 'matchtype':
246 array_push($arguments, array(
247 'type' => SieveToken::Tag,
248 'occurrence' => $this->occurrence_($arg),
249 'regex' => $this->matchTypeRegex_(),
250 'call' => 'matchTypeHook_',
251 'name' => 'match type',
252 'subArgs' => $this->makeArguments_($arg)
253 ));
254 break;
255
256 case 'number':
257 array_push($arguments, array(
258 'type' => SieveToken::Number,
259 'occurrence' => $this->occurrence_($arg),
260 'regex' => $this->regex_($arg),
261 'name' => $this->name_($arg),
262 'follows' => $this->follows_($arg)
263 ));
264 break;
265
266 case 'requirestrings':
267 array_push($arguments, array(
268 'type' => SieveToken::StringList,
269 'occurrence' => $this->occurrence_($arg),
270 'call' => 'setRequire_',
271 'case' => 'adhere',
272 'regex' => $this->requireStringsRegex_(),
273 'name' => $this->name_($arg)
274 ));
275 break;
276
277 case 'string':
278 array_push($arguments, array(
279 'type' => SieveToken::String,
280 'occurrence' => $this->occurrence_($arg),
281 'regex' => $this->regex_($arg),
282 'case' => $this->case_($arg),
283 'name' => $this->name_($arg),
284 'follows' => $this->follows_($arg)
285 ));
286 break;
287
288 case 'stringlist':
289 array_push($arguments, array(
290 'type' => SieveToken::StringList,
291 'occurrence' => $this->occurrence_($arg),
292 'regex' => $this->regex_($arg),
293 'case' => $this->case_($arg),
294 'name' => $this->name_($arg),
295 'follows' => $this->follows_($arg)
296 ));
297 break;
298
299 case 'tag':
300 array_push($arguments, array(
301 'type' => SieveToken::Tag,
302 'occurrence' => $this->occurrence_($arg),
303 'regex' => $this->regex_($arg),
304 'call' => 'tagHook_',
305 'name' => $this->name_($arg),
306 'subArgs' => $this->makeArguments_($arg->children()),
307 'follows' => $this->follows_($arg)
308 ));
309 break;
310
311 case 'test':
312 array_push($arguments, array(
313 'type' => SieveToken::Identifier,
314 'occurrence' => $this->occurrence_($arg),
315 'regex' => $this->testsRegex_(),
316 'name' => $this->name_($arg),
317 'subArgs' => $this->makeArguments_($arg->children())
318 ));
319 break;
320
321 case 'testlist':
322 array_push($arguments, array(
323 'type' => SieveToken::LeftParenthesis,
324 'occurrence' => '1',
325 'regex' => '\(',
326 'name' => $this->name_($arg),
327 'subArgs' => null
328 ));
329 array_push($arguments, array(
330 'type' => SieveToken::Identifier,
331 'occurrence' => '+',
332 'regex' => $this->testsRegex_(),
333 'name' => $this->name_($arg),
334 'subArgs' => $this->makeArguments_($arg->children())
335 ));
336 break;
337 }
338 }
339
340 return $arguments;
341 }
342
343 /**
344 * Add argument(s) expected / allowed to appear next.
345 * @param array $value
346 */
347 protected function addArguments_($identifier, $subArgs)
348 {
349 for ($i = count($subArgs); $i > 0; $i--)
350 {
351 $arg = $subArgs[$i-1];
352 if (preg_match('/^'. $arg['follows'] .'$/si', $identifier))
353 array_unshift($this->arguments_, $arg);
354 }
355 }
356
357 /**
358 * Add dependency that is expected to be fullfilled when parsing
359 * of the current command is {@see done}.
360 * @param array $dependency
361 */
362 protected function addDependency_($type, $name, $dependencies)
363 {
364 foreach ($dependencies as $d)
365 {
366 array_push($this->deps_, array(
367 'o_type' => $type,
368 'o_name' => $name,
369 'type' => $d['type'],
370 'name' => $d['name'],
371 'regex' => $d['regex']
372 ));
373 }
374 }
375
376 protected function invoke_($token, $func, $arg = array())
377 {
378 if (!is_array($arg))
379 $arg = array($arg);
380
381 $err = call_user_func_array(array(&$this, $func), $arg);
382
383 if ($err)
384 throw new SieveException($token, $err);
385 }
386
387 protected function setRequire_($extension)
388 {
389 array_push(self::$requiredExtensions_, $extension);
390 $this->registry_->activate($extension);
391 }
392
393 /**
394 * Hook function that is called after a address part match was found
395 * in a command. The kind of address part is remembered in case it's
396 * needed later {@see done}. For address parts from a extension
397 * dependency information and valid values are looked up as well.
398 * @param string $addresspart
399 */
400 protected function addressPartHook_($addresspart)
401 {
402 $this->addressPart_ = $addresspart;
403 $xml = $this->registry_->addresspart($this->addressPart_);
404
405 if (isset($xml))
406 {
407 // Add possible value and dependancy
408 $this->addArguments_($this->addressPart_, $this->makeArguments_($xml));
409 $this->addDependency_('address part', $this->addressPart_, $xml->requires);
410 }
411 }
412
413 /**
414 * Hook function that is called after a match type was found in a
415 * command. The kind of match type is remembered in case it's
416 * needed later {@see done}. For a match type from extensions
417 * dependency information and valid values are looked up as well.
418 * @param string $matchtype
419 */
420 protected function matchTypeHook_($matchtype)
421 {
422 $this->matchType_ = $matchtype;
423 $xml = $this->registry_->matchtype($this->matchType_);
424
425 if (isset($xml))
426 {
427 // Add possible value and dependancy
428 $this->addArguments_($this->matchType_, $this->makeArguments_($xml));
429 $this->addDependency_('match type', $this->matchType_, $xml->requires);
430 }
431 }
432
433 /**
434 * Hook function that is called after a comparator was found in
435 * a command. The comparator is remembered in case it's needed for
436 * comparsion later {@see done}. For a comparator from extensions
437 * dependency information is looked up as well.
438 * @param string $comparator
439 */
440 protected function comparatorHook_($comparator)
441 {
442 $this->comparator_ = $comparator;
443 $xml = $this->registry_->comparator($this->comparator_);
444
445 if (isset($xml))
446 {
447 // Add possible dependancy
448 $this->addDependency_('comparator', $this->comparator_, $xml->requires);
449 }
450 }
451
452 /**
453 * Hook function that is called after a tag was found in
454 * a command. The tag is remembered in case it's needed for
455 * comparsion later {@see done}. For a tags from extensions
456 * dependency information is looked up as well.
457 * @param string $tag
458 */
459 protected function tagHook_($tag)
460 {
461 array_push($this->tags_, $tag);
462 $xml = $this->registry_->argument($tag);
463
464 // Add possible dependancies
465 if (isset($xml))
466 $this->addDependency_('tag', $tag, $xml->requires);
467 }
468
469 protected function validType_($token)
470 {
471 foreach ($this->arguments_ as $arg)
472 {
473 if ($arg['occurrence'] == '0')
474 {
475 array_shift($this->arguments_);
476 continue;
477 }
478
479 if ($token->is($arg['type']))
480 return;
481
482 // Is the argument required
483 if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*')
484 throw new SieveException($token, $arg['type']);
485
486 array_shift($this->arguments_);
487 }
488
489 // Check if command expects any (more) arguments
490 if (empty($this->arguments_))
491 throw new SieveException($token, $this->followupToken_);
492
493 throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
494 }
495
496 public function startStringList($token)
497 {
498 $this->validType_($token);
499 $this->arguments_[0]['type'] = SieveToken::String;
500 $this->arguments_[0]['occurrence'] = '+';
501 }
502
503 public function continueStringList()
504 {
505 $this->arguments_[0]['occurrence'] = '+';
506 }
507
508 public function endStringList()
509 {
510 array_shift($this->arguments_);
511 }
512
513 public function validateToken($token)
514 {
515 // Make sure the argument has a valid type
516 $this->validType_($token);
517
518 foreach ($this->arguments_ as &$arg)
519 {
520 // Build regular expression according to argument type
521 switch ($arg['type'])
522 {
523 case SieveToken::String:
524 case SieveToken::StringList:
525 $regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/'
526 . ($arg['case'] == 'ignore' ? 'si' : 's');
527 break;
528 case SieveToken::Tag:
529 $regex = '/^:(?P<one>'. $arg['regex'] .')$/si';
530 break;
531 default:
532 $regex = '/^(?P<one>'. $arg['regex'] .')$/si';
533 }
534
535 if (preg_match($regex, $token->text, $match))
536 {
537 $text = ($match['one'] ? $match['one'] : $match['two']);
538
539 // Add argument(s) that may now appear after this one
540 if (isset($arg['subArgs']))
541 $this->addArguments_($text, $arg['subArgs']);
542
543 // Call extra processing function if defined
544 if (isset($arg['call']))
545 $this->invoke_($token, $arg['call'], $text);
546
547 // Check if a possible value of this argument may occur
548 if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1')
549 {
550 $arg['occurrence'] = '0';
551 }
552 else if ($arg['occurrence'] == '+')
553 {
554 $arg['occurrence'] = '*';
555 }
556
557 return;
558 }
559
560 if ($token->is($arg['type']) && $arg['occurrence'] == 1)
561 {
562 throw new SieveException($token,
563 SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected');
564 }
565 }
566
567 throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
568 }
569
570 public function done($token)
571 {
572 // Check if there are required arguments left
573 foreach ($this->arguments_ as $arg)
574 {
575 if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1')
576 throw new SieveException($token, $arg['type']);
577 }
578
579 // Check if the command depends on use of a certain tag
580 foreach ($this->deps_ as $d)
581 {
582 switch ($d['type'])
583 {
584 case 'addresspart':
585 $values = array($this->addressPart_);
586 break;
587
588 case 'matchtype':
589 $values = array($this->matchType_);
590 break;
591
592 case 'comparator':
593 $values = array($this->comparator_);
594 break;
595
596 case 'tag':
597 $values = $this->tags_;
598 break;
599 }
600
601 foreach ($values as $value)
602 {
603 if (preg_match('/^'. $d['regex'] .'$/mi', $value))
604 break 2;
605 }
606
607 throw new SieveException($token,
608 $d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']);
609 }
610 }
611}