1c00051c052bb1f477b3eead6c19f9433e7ce445
1 <?php
3 class Semantics
4 {
5 var $registerExtensionFn_;
6 var $isExtensionRegisteredFn_;
8 var $command_;
9 var $comparator_;
10 var $matchType_;
11 var $s_;
12 var $unknown;
13 var $message;
14 var $nonTestCommands_ = '(require|if|elsif|else|reject|fileinto|redirect|stop|keep|discard|mark|unmark|setflag|addflag|removeflag)';
15 var $testsValidAfter_ = '(if|elsif|anyof|allof|not)';
16 var $testCommands_ = '(address|envelope|header|size|allof|anyof|exists|not|true|false)';
17 var $requireStrings_ = '(envelope|fileinto|reject|vacation|relational|subaddress|regex|imapflags|copy)';
19 function Semantics($command)
20 {
21 $this->command_ = $command;
22 $this->unknown = false;
23 switch ($command)
24 {
26 /********************
27 * control commands
28 */
29 case 'require':
30 /* require <capabilities: string-list> */
31 $this->s_ = array(
32 'valid_after' => '(script-start|require)',
33 'arguments' => array(
34 array('class' => 'string', 'list' => true, 'name' => 'require-string', 'occurrences' => '1', 'call' => 'setRequire_', 'values' => array(
35 array('occurrences' => '+', 'regex' => '"'. $this->requireStrings_ .'"'),
36 array('occurrences' => '+', 'regex' => '"comparator-i;(octet|ascii-casemap|ascii-numeric)"')
37 ))
38 )
39 );
40 break;
42 case 'if':
43 /* if <test> <block> */
44 $this->s_ = array(
45 'valid_after' => str_replace('(', '(script-start|', $this->nonTestCommands_),
46 'arguments' => array(
47 array('class' => 'identifier', 'occurrences' => '1', 'values' => array(
48 array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test')
49 )),
50 array('class' => 'block-start', 'occurrences' => '1', 'values' => array(
51 array('occurrences' => '1', 'regex' => '{', 'name' => 'block')
52 ))
53 )
54 );
55 break;
57 case 'elsif':
58 /* elsif <test> <block> */
59 $this->s_ = array(
60 'valid_after' => '(if|elsif)',
61 'arguments' => array(
62 array('class' => 'identifier', 'occurrences' => '1', 'values' => array(
63 array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test')
64 )),
65 array('class' => 'block-start', 'occurrences' => '1', 'values' => array(
66 array('occurrences' => '1', 'regex' => '{', 'name' => 'block')
67 ))
68 )
69 );
70 break;
72 case 'else':
73 /* else <block> */
74 $this->s_ = array(
75 'valid_after' => '(if|elsif)',
76 'arguments' => array(
77 array('class' => 'block-start', 'occurrences' => '1', 'values' => array(
78 array('occurrences' => '1', 'regex' => '{', 'name' => 'block')
79 ))
80 )
81 );
82 break;
85 /*******************
86 * action commands
87 */
88 case 'discard':
89 case 'keep':
90 case 'stop':
91 /* discard / keep / stop */
92 $this->s_ = array(
93 'valid_after' => str_replace('(', '(script-start|', $this->nonTestCommands_)
94 );
95 break;
97 case 'fileinto':
98 /* fileinto [":copy"] <folder: string> */
99 $this->s_ = array(
100 'requires' => 'fileinto',
101 'valid_after' => $this->nonTestCommands_,
102 'arguments' => array(
103 array('class' => 'tag', 'occurrences' => '?', 'values' => array(
104 array('occurrences' => '?', 'regex' => ':copy', 'requires' => 'copy', 'name' => 'copy')
105 )),
106 array('class' => 'string', 'occurrences' => '1', 'values' => array(
107 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'folder')
108 ))
109 )
110 );
111 break;
113 case 'mark':
114 case 'unmark':
115 /* mark / unmark */
116 $this->s_ = array(
117 'requires' => 'imapflags',
118 'valid_after' => $this->nonTestCommands_
119 );
120 break;
122 case 'redirect':
123 /* redirect [":copy"] <address: string> */
124 $this->s_ = array(
125 'valid_after' => str_replace('(', '(script-start|', $this->nonTestCommands_),
126 'arguments' => array(
127 array('class' => 'tag', 'occurrences' => '?', 'values' => array(
128 array('occurrences' => '?', 'regex' => ':copy', 'requires' => 'copy', 'name' => 'size-type')
129 )),
130 array('class' => 'string', 'occurrences' => '1', 'values' => array(
131 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'address')
132 ))
133 )
134 );
135 break;
137 case 'reject':
138 /* reject <reason: string> */
139 $this->s_ = array(
140 'requires' => 'reject',
141 'valid_after' => $this->nonTestCommands_,
142 'arguments' => array(
143 array('class' => 'string', 'occurrences' => '1', 'values' => array(
144 array('occurrences' => '1', 'regex' => '("|).*("|)', 'name' => 'reason')
145 ))
146 )
147 );
148 break;
150 case 'setflag':
151 case 'addflag':
152 case 'removeflag':
153 /* setflag <flag-list: string-list> */
154 /* addflag <flag-list: string-list> */
155 /* removeflag <flag-list: string-list> */
156 $this->s_ = array(
157 'requires' => 'imapflags',
158 'valid_after' =>$this->nonTestCommands_,
159 'arguments' => array(
160 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
161 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
162 ))
163 )
164 );
165 break;
167 case 'vacation':
168 /* vacation [":days" number] [":addresses" string-list] [":subject" string] [":mime"] <reason: string> */
169 $this->s_ = array(
170 'requires' => 'vacation',
171 'valid_after' => $this->nonTestCommands_,
172 'arguments' => array(
173 array('class' => 'tag', 'occurrences' => '*', 'values' => array(
174 array('occurrences' => '?', 'regex' => ':days', 'name' => 'days',
175 'add' => array(
176 array('class' => 'number', 'occurrences' => '1', 'values' => array(
177 array('occurrences' => '1', 'regex' => '.*', 'name' => 'period')
178 ))
179 )
180 ),
181 array('occurrences' => '?', 'regex' => ':addresses', 'name' => 'addresses',
182 'add' => array(
183 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
184 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'address')
185 ))
186 )
187 ),
188 array('occurrences' => '?', 'regex' => ':subject', 'name' => 'subject',
189 'add' => array(
190 array('class' => 'string', 'occurrences' => '1', 'values' => array(
191 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'subject')
192 ))
193 )
194 ),
195 array('occurrences' => '?', 'regex' => ':mime', 'name' => 'mime')
196 )),
197 array('class' => 'string', 'occurrences' => '1', 'values' => array(
198 array('occurrences' => '1', 'regex' => '.*', 'name' => 'reason')
199 ))
200 )
201 );
202 break;
205 /*****************
206 * test commands
207 */
208 case 'address':
209 /* address [address-part: tag] [comparator: tag] [match-type: tag] <header-list: string-list> <key-list: string-list> */
210 $this->s_ = array(
211 'valid_after' => $this->testsValidAfter_,
212 'arguments' => array(
213 array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array(
214 array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value|regex)', 'call' => 'setMatchType_', 'name' => 'match-type'),
215 array('occurrences' => '?', 'regex' => ':(all|localpart|domain|user|detail)', 'call' => 'checkAddrPart_', 'name' => 'address-part'),
216 array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator',
217 'add' => array(
218 array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array(
219 array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'),
220 array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string')
221 ))
222 )
223 )
224 )),
225 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
226 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header')
227 )),
228 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
229 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
230 ))
231 )
232 );
233 break;
235 case 'allof':
236 case 'anyof':
237 /* allof <tests: test-list>
238 anyof <tests: test-list> */
239 $this->s_ = array(
240 'valid_after' => $this->testsValidAfter_,
241 'arguments' => array(
242 array('class' => 'left-parant', 'occurrences' => '1', 'values' => array(
243 array('occurrences' => '1', 'regex' => '\(', 'name' => 'test-list')
244 )),
245 array('class' => 'identifier', 'occurrences' => '+', 'values' => array(
246 array('occurrences' => '+', 'regex' => $this->testCommands_, 'name' => 'test')
247 ))
248 )
249 );
250 break;
252 case 'envelope':
253 /* envelope [address-part: tag] [comparator: tag] [match-type: tag] <envelope-part: string-list> <key-list: string-list> */
254 $this->s_ = array(
255 'requires' => 'envelope',
256 'valid_after' => $this->testsValidAfter_,
257 'arguments' => array(
258 array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array(
259 array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value|regex)', 'call' => 'setMatchType_', 'name' => 'match-type'),
260 array('occurrences' => '?', 'regex' => ':(all|localpart|domain|user|detail)', 'call' => 'checkAddrPart_', 'name' => 'address-part'),
261 array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator',
262 'add' => array(
263 array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array(
264 array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'),
265 array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string')
266 ))
267 )
268 )
269 )),
270 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
271 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'envelope-part')
272 )),
273 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
274 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
275 ))
276 )
277 );
278 break;
280 case 'exists':
281 /* exists <header-names: string-list> */
282 $this->s_ = array(
283 'valid_after' => $this->testsValidAfter_,
284 'arguments' => array(
285 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
286 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header')
287 ))
288 )
289 );
290 break;
292 case 'header':
293 /* header [comparator: tag] [match-type: tag] <header-names: string-list> <key-list: string-list> */
294 $this->s_ = array(
295 'valid_after' => $this->testsValidAfter_,
296 'arguments' => array(
297 array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array(
298 array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value|regex)', 'call' => 'setMatchType_', 'name' => 'match-type'),
299 array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator',
300 'add' => array(
301 array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array(
302 array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'),
303 array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string')
304 ))
305 )
306 )
307 )),
308 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
309 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header')
310 )),
311 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
312 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
313 ))
314 )
315 );
316 break;
318 case 'not':
319 /* not <test> */
320 $this->s_ = array(
321 'valid_after' => $this->testsValidAfter_,
322 'arguments' => array(
323 array('class' => 'identifier', 'occurrences' => '1', 'values' => array(
324 array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test')
325 ))
326 )
327 );
328 break;
330 case 'size':
331 /* size <":over" / ":under"> <limit: number> */
332 $this->s_ = array(
333 'valid_after' => $this->testsValidAfter_,
334 'arguments' => array(
335 array('class' => 'tag', 'occurrences' => '1', 'values' => array(
336 array('occurrences' => '1', 'regex' => ':(over|under)', 'name' => 'size-type')
337 )),
338 array('class' => 'number', 'occurrences' => '1', 'values' => array(
339 array('occurrences' => '1', 'regex' => '.*', 'name' => 'limit')
340 ))
341 )
342 );
343 break;
345 case 'true':
346 case 'false':
347 /* true / false */
348 $this->s_ = array(
349 'valid_after' => $this->testsValidAfter_
350 );
351 break;
354 /********************
355 * unknown commands
356 */
357 default:
358 $this->unknown = true;
359 }
360 }
362 function setExtensionFuncs($setFn, $checkFn)
363 {
364 if (is_callable($setFn) && is_callable($checkFn))
365 {
366 $this->registerExtensionFn_ = $setFn;
367 $this->isExtensionRegisteredFn_ = $checkFn;
368 }
369 }
371 function setRequire_($extension)
372 {
373 call_user_func($this->registerExtensionFn_, $extension);
374 return true;
375 }
377 function wasRequired_($extension)
378 {
379 return call_user_func($this->isExtensionRegisteredFn_, $extension);
380 }
382 function setMatchType_($text)
383 {
384 // Do special processing for relational test extension
385 if ($text == ':count' || $text == ':value')
386 {
387 if (!$this->wasRequired_('relational'))
388 {
389 $this->message = 'missing require for match-type '. $text;
390 return false;
391 }
393 array_unshift($this->s_['arguments'],
394 array('class' => 'string', 'occurrences' => '1', 'values' => array(
395 array('occurrences' => '1', 'regex' => '"(lt|le|eq|ge|gt|ne)"', 'name' => 'relation-string'),
396 ))
397 );
398 }
399 // Do special processing for regex match-type extension
400 else if ($text == ':regex' && !$this->wasRequired_('regex'))
401 {
402 $this->message = 'missing require for match-type '. $text;
403 return false;
404 }
405 $this->matchType_ = $text;
406 return true;
407 }
409 function setComparator_($text)
410 {
411 $this->comparator_ = $text;
412 return true;
413 }
415 function checkAddrPart_($text)
416 {
417 if ($text == ':user' || $text == ':detail')
418 {
419 if (!$this->wasRequired_('subaddress'))
420 {
421 $this->message = 'missing require for tag '. $text;
422 return false;
423 }
424 }
425 return true;
426 }
428 function checkTags_()
429 {
430 if (isset($this->matchType_) &&
431 $this->matchType_ == ':count' &&
432 $this->comparator_ != '"i;ascii-numeric"')
433 {
434 $this->message = 'match-type :count needs comparator i;ascii-numeric';
435 return false;
436 }
437 return true;
438 }
440 function validCommand($prev, $line)
441 {
442 // Check if command is known
443 if ($this->unknown)
444 {
445 $this->message = 'line '. $line .': unknown command "'. $this->command_ .'"';
446 return false;
447 }
449 // Check if the command needs to be required
450 if (isset($this->s_['requires']) && !$this->wasRequired_($this->s_['requires']))
451 {
452 $this->message = 'line '. $line .': missing require for command "'. $this->command_ .'"';
453 return false;
454 }
456 // Check if command may appear here
457 if (!ereg($this->s_['valid_after'], $prev))
458 {
459 # $this->message = 'line '. $line .': "'. $this->command_ .'" may not appear after "'. $prev .'"';
460 # return false;
461 }
463 return true;
464 }
466 function validClass_($class, $id)
467 {
468 // Check if command expects any arguments
469 if (!isset($this->s_['arguments']))
470 {
471 $this->message = $id .' where semicolon expected';
472 return false;
473 }
475 foreach ($this->s_['arguments'] as $arg)
476 {
477 if ($class == $arg['class'])
478 {
479 return true;
480 }
482 // Is the argument required
483 if ($arg['occurrences'] != '?' && $arg['occurrences'] != '*')
484 {
485 $this->message = $id .' where '. $arg['class'] .' expected';
486 return false;
487 }
489 if (isset($arg['post-call']) &&
490 !call_user_func(array(&$this, $arg['post-call'])))
491 {
492 return false;
493 }
494 array_shift($this->s_['arguments']);
495 }
497 $this->message = 'unexpected '. $id;
498 return false;
499 }
501 function startStringList($line)
502 {
503 if (!$this->validClass_('string', 'string'))
504 {
505 $this->message = 'line '. $line .': '. $this->message;
506 return false;
507 }
508 else if (!isset($this->s_['arguments'][0]['list']))
509 {
510 $this->message = 'line '. $line .': '. 'left bracket where '. $this->s_['arguments'][0]['class'] .' expected';
511 return false;
512 }
514 $this->s_['arguments'][0]['occurrences'] = '+';
515 return true;
516 }
518 function endStringList()
519 {
520 array_shift($this->s_['arguments']);
521 }
523 function validToken($class, &$text, &$line)
524 {
525 $name = $class . ($class != $text ? " $text" : '');
527 // Make sure the argument has a valid class
528 if (!$this->validClass_($class, $name))
529 {
530 $this->message = 'line '. $line .': '. $this->message;
531 return false;
532 }
534 $arg = &$this->s_['arguments'][0];
535 foreach ($arg['values'] as $val)
536 {
537 if (preg_match('/^'. $val['regex'] .'$/m', $text))
538 {
539 // Check if the argument value needs a 'require'
540 if (isset($val['requires']) && !$this->wasRequired_($val['requires']))
541 {
542 $this->message = 'line '. $line .': missing require for '. $val['name'] .' '. $text;
543 return false;
544 }
546 // Check if a possible value of this argument may occur
547 if ($val['occurrences'] == '?' || $val['occurrences'] == '1')
548 {
549 $val['occurrences'] = '0';
550 }
551 else if ($val['occurrences'] == '+')
552 {
553 $val['occurrences'] = '*';
554 }
555 else if ($val['occurrences'] == '0')
556 {
557 $this->message = 'line '. $line .': too many '. $val['name'] .' '. $class .'s near '. $text;
558 return false;
559 }
561 // Call extra processing function if defined
562 if (isset($val['call']) && !call_user_func(array(&$this, $val['call']), $text) ||
563 isset($arg['call']) && !call_user_func(array(&$this, $arg['call']), $text))
564 {
565 $this->message = 'line '. $line .': '. $this->message;
566 return false;
567 }
569 // Set occurrences appropriately
570 if ($arg['occurrences'] == '?' || $arg['occurrences'] == '1')
571 {
572 array_shift($this->s_['arguments']);
573 }
574 else
575 {
576 $arg['occurrences'] = '*';
577 }
579 // Add argument(s) expected to follow right after this one
580 if (isset($val['add']))
581 {
582 while ($add_arg = array_pop($val['add']))
583 {
584 array_unshift($this->s_['arguments'], $add_arg);
585 }
586 }
588 return true;
589 }
590 }
592 $this->message = 'line '. $line .': unexpected '. $name;
593 return false;
594 }
596 function done($class, $text, $line)
597 {
598 if (isset($this->s_['arguments']))
599 {
600 foreach ($this->s_['arguments'] as $arg)
601 {
602 if ($arg['occurrences'] == '+' || $arg['occurrences'] == '1')
603 {
604 $this->message = 'line '. $line .': '. $class .' '. $text .' where '. $arg['class'] .' expected';
605 return false;
606 }
607 }
608 }
609 return true;
610 }
611 }
613 ?>