1 <?php
3 $requires_ = array();
5 class Semantics
6 {
7 var $command_;
8 var $comparator_;
9 var $matchType_;
10 var $s_;
11 var $unknown;
12 var $message;
13 var $testCommands_ = '(address|envelope|header|size|allof|anyof|exists|not|true|false)';
14 var $requireStrings_ = '(envelope|fileinto|reject|vacation|relational|subaddress)';
16 function Semantics($command)
17 {
18 $this->command_ = $command;
20 $this->unknown = false;
21 switch ($command)
22 {
24 /********************
25 * control commands
26 */
27 case 'require':
28 /* require <capabilities: string-list> */
29 $this->s_ = array(
30 'valid_after' => array('script-start', 'require'),
31 'arguments' => array(
32 array('class' => 'string', 'list' => true, 'name' => 'require-string', 'occurrences' => '1', 'call' => 'setRequire_', 'values' => array(
33 array('occurrences' => '+', 'regex' => '"'. $this->requireStrings_ .'"'),
34 array('occurrences' => '+', 'regex' => '"comparator-i;(octet|ascii-casemap|ascii-numeric)"')
35 ))
36 )
37 );
38 break;
40 case 'if':
41 /* if <test> <block> */
42 $this->s_ = array(
43 'valid_after' => array('script-start', 'require', 'if', 'elsif', 'else',
44 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'),
45 'arguments' => array(
46 array('class' => 'identifier', 'occurrences' => '1', 'values' => array(
47 array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test')
48 )),
49 array('class' => 'block-start', 'occurrences' => '1', 'values' => array(
50 array('occurrences' => '1', 'regex' => '{', 'name' => 'block')
51 ))
52 )
53 );
54 break;
56 case 'elsif':
57 /* elsif <test> <block> */
58 $this->s_ = array(
59 'valid_after' => array('if', 'elsif'),
60 'arguments' => array(
61 array('class' => 'identifier', 'occurrences' => '1', 'values' => array(
62 array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test')
63 )),
64 array('class' => 'block-start', 'occurrences' => '1', 'values' => array(
65 array('occurrences' => '1', 'regex' => '{', 'name' => 'block')
66 ))
67 )
68 );
69 break;
71 case 'else':
72 /* else <block> */
73 $this->s_ = array(
74 'valid_after' => array('if', 'elsif'),
75 'arguments' => array(
76 array('class' => 'block-start', 'occurrences' => '1', 'values' => array(
77 array('occurrences' => '1', 'regex' => '{', 'name' => 'block')
78 ))
79 )
80 );
81 break;
84 /*******************
85 * action commands
86 */
87 case 'keep':
88 case 'stop':
89 case 'discard':
90 /* keep / stop / discard */
91 $this->s_ = array(
92 'valid_after' => array('script-start', 'require', 'if', 'elsif', 'else',
93 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard')
94 );
95 break;
97 case 'fileinto':
98 /* fileinto <folder: string> */
99 $this->s_ = array(
100 'requires' => 'fileinto',
101 'valid_after' => array('require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'),
102 'arguments' => array(
103 array('class' => 'string', 'occurrences' => '1', 'values' => array(
104 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'folder')
105 ))
106 )
107 );
108 break;
110 case 'redirect':
111 /* redirect <address: string> */
112 $this->s_ = array(
113 'valid_after' => array('script-start', 'require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'),
114 'arguments' => array(
115 array('class' => 'string', 'occurrences' => '1', 'values' => array(
116 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'address')
117 ))
118 )
119 );
120 break;
122 case 'reject':
123 /* reject <reason: string> */
124 $this->s_ = array(
125 'requires' => 'reject',
126 'valid_after' => array('require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'),
127 'arguments' => array(
128 array('class' => 'string', 'occurrences' => '1', 'values' => array(
129 array('occurrences' => '1', 'regex' => '.*', 'name' => 'reason')
130 ))
131 )
132 );
133 break;
135 case 'vacation':
136 /* vacation [":days" number] [":addresses" string-list] [":subject" string] [":mime"] <reason: string> */
137 $this->s_ = array(
138 'requires' => 'vacation',
139 'valid_after' => array('require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'),
140 'arguments' => array(
141 array('class' => 'tag', 'occurrences' => '*', 'values' => array(
142 array('occurrences' => '?', 'regex' => ':days', 'name' => 'days',
143 'add' => array(
144 array('class' => 'number', 'occurrences' => '1', 'values' => array(
145 array('occurrences' => '1', 'regex' => '.*', 'name' => 'period')
146 ))
147 )
148 ),
149 array('occurrences' => '?', 'regex' => ':addresses', 'name' => 'addresses',
150 'add' => array(
151 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
152 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'address')
153 ))
154 )
155 ),
156 array('occurrences' => '?', 'regex' => ':subject', 'name' => 'subject',
157 'add' => array(
158 array('class' => 'string', 'occurrences' => '1', 'values' => array(
159 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'subject')
160 ))
161 )
162 ),
163 array('occurrences' => '?', 'regex' => ':mime', 'name' => 'mime')
164 )),
165 array('class' => 'string', 'occurrences' => '1', 'values' => array(
166 array('occurrences' => '1', 'regex' => '".*"', 'name' => 'reason')
167 ))
168 )
169 );
170 break;
173 /*****************
174 * test commands
175 */
176 case 'address':
177 /* address [address-part: tag] [comparator: tag] [match-type: tag] <header-list: string-list> <key-list: string-list> */
178 $this->s_ = array(
179 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
180 'arguments' => array(
181 array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array(
182 array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value)', 'call' => 'setMatchType_', 'name' => 'match-type'),
183 array('occurrences' => '?', 'regex' => ':(all|localpart|domain|user|detail)', 'call' => 'checkAddrPart_', 'name' => 'address-part'),
184 array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator',
185 'add' => array(
186 array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array(
187 array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'),
188 array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string')
189 ))
190 )
191 )
192 )),
193 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
194 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header')
195 )),
196 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
197 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
198 ))
199 )
200 );
201 break;
203 case 'allof':
204 case 'anyof':
205 /* allof <tests: test-list>
206 anyof <tests: test-list> */
207 $this->s_ = array(
208 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
209 'arguments' => array(
210 array('class' => 'left-parant', 'occurrences' => '1', 'values' => array(
211 array('occurrences' => '1', 'regex' => '\(', 'name' => 'test-list')
212 )),
213 array('class' => 'identifier', 'occurrences' => '+', 'values' => array(
214 array('occurrences' => '+', 'regex' => $this->testCommands_, 'name' => 'test')
215 ))
216 )
217 );
218 break;
220 case 'envelope':
221 /* envelope [address-part: tag] [comparator: tag] [match-type: tag] <envelope-part: string-list> <key-list: string-list> */
222 $this->s_ = array(
223 'requires' => 'envelope',
224 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
225 'arguments' => array(
226 array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array(
227 array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value)', 'call' => 'setMatchType_', 'name' => 'match-type'),
228 array('occurrences' => '?', 'regex' => ':(all|localpart|domain|user|detail)', 'call' => 'checkAddrPart_', 'name' => 'address-part'),
229 array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator',
230 'add' => array(
231 array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array(
232 array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'),
233 array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string')
234 ))
235 )
236 )
237 )),
238 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
239 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'envelope-part')
240 )),
241 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
242 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
243 ))
244 )
245 );
246 break;
248 case 'exists':
249 /* exists <header-names: string-list> */
250 $this->s_ = array(
251 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
252 'arguments' => array(
253 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
254 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header')
255 ))
256 )
257 );
258 break;
260 case 'header':
261 /* header [comparator: tag] [match-type: tag] <header-names: string-list> <key-list: string-list> */
262 $this->s_ = array(
263 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
264 'arguments' => array(
265 array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array(
266 array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value)', 'call' => 'setMatchType_', 'name' => 'match-type'),
267 array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator',
268 'add' => array(
269 array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array(
270 array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'),
271 array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string')
272 ))
273 )
274 )
275 )),
276 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
277 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header')
278 )),
279 array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array(
280 array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key')
281 ))
282 )
283 );
284 break;
286 case 'not':
287 /* not <test> */
288 $this->s_ = array(
289 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
290 'arguments' => array(
291 array('class' => 'identifier', 'occurrences' => '1', 'values' => array(
292 array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test')
293 ))
294 )
295 );
296 break;
298 case 'size':
299 /* size <":over" / ":under"> <limit: number> */
300 $this->s_ = array(
301 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'),
302 'arguments' => array(
303 array('class' => 'tag', 'occurrences' => '1', 'values' => array(
304 array('occurrences' => '1', 'regex' => ':(over|under)', 'name' => 'size-type')
305 )),
306 array('class' => 'number', 'occurrences' => '1', 'values' => array(
307 array('occurrences' => '1', 'regex' => '.*', 'name' => 'limit')
308 ))
309 )
310 );
311 break;
313 case 'true':
314 case 'false':
315 /* true / false */
316 $this->s_ = array(
317 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not')
318 );
319 break;
322 /********************
323 * unknown commands
324 */
325 default:
326 $this->unknown = true;
327 }
328 }
330 function setRequire_($text)
331 {
332 global $requires_;
334 if(!is_array($requires_)){
335 $requires_ = array();
336 }
338 array_push($requires_, $text);
339 return true;
340 }
342 function setMatchType_($text)
343 {
344 // Do special processing for relational test extension
345 if ($text == ':count' || $text == ':value')
346 {
347 global $requires_;
348 if (!in_array('"relational"', $requires_))
349 {
350 $this->message = 'missing require for match-type '. $text;
351 return false;
352 }
354 array_unshift($this->s_['arguments'],
355 array('class' => 'string', 'occurrences' => '1', 'values' => array(
356 array('occurrences' => '1', 'regex' => '"(lt|le|eq|ge|gt|ne)"', 'name' => 'relation-string'),
357 ))
358 );
359 }
360 $this->matchType_ = $text;
361 return true;
362 }
364 function setComparator_($text)
365 {
366 $this->comparator_ = $text;
367 return true;
368 }
370 function checkAddrPart_($text)
371 {
372 if ($text == ':user' || $text == ':detail')
373 {
374 global $requires_;
375 if (!in_array('"subaddress"', $requires_))
376 {
377 $this->message = 'missing require for tag '. $text;
378 return false;
379 }
380 }
381 return true;
382 }
384 function checkTags_()
385 {
386 if (isset($this->matchType_) &&
387 $this->matchType_ == ':count' &&
388 $this->comparator_ != '"i;ascii-numeric"')
389 {
390 $this->message = 'match-type :count needs comparator i;ascii-numeric';
391 return false;
392 }
393 return true;
394 }
396 function validAfter($prev)
397 {
398 return in_array($prev, $this->s_['valid_after']);
399 }
401 function validClass_($class, $id)
402 {
403 // Check if command expects any arguments
404 if (!isset($this->s_['arguments']))
405 {
406 $this->message = $id .' where semicolon expected';
407 return false;
408 }
410 foreach ($this->s_['arguments'] as $arg)
411 {
412 if ($class == $arg['class'])
413 {
414 return true;
415 }
417 // Is the argument required
418 if ($arg['occurrences'] != '?' && $arg['occurrences'] != '*')
419 {
420 $this->message = $id .' where '. $arg['class'] .' expected';
421 return false;
422 }
424 if (isset($arg['post-call']) &&
425 !call_user_func(array(&$this, $arg['post-call'])))
426 {
427 return false;
428 }
429 array_shift($this->s_['arguments']);
430 }
432 $this->message = 'unexpected '. $id;
433 return false;
434 }
436 function startStringList($line)
437 {
438 if (!$this->validClass_('string', 'string'))
439 {
440 $this->message = 'line '. $line .': '. $this->message;
441 return false;
442 }
443 else if (!isset($this->s_['arguments'][0]['list']))
444 {
445 $this->message = 'line '. $line .': '. 'left bracket where '. $this->s_['arguments'][0]['class'] .' expected';
446 return false;
447 }
449 $this->s_['arguments'][0]['occurrences'] = '+';
450 return true;
451 }
453 function endStringList()
454 {
455 array_shift($this->s_['arguments']);
456 }
458 function validToken($class, &$text, &$line)
459 {
460 $name = $class . ($class != $text ? " $text" : '');
462 // Check if the command needs to be required
463 // TODO: move this to somewhere more appropriate
464 global $requires_;
465 if (isset($this->s_['requires']) &&
466 !in_array('"'.$this->s_['requires'].'"', $requires_))
467 {
468 $this->message = 'line '. $line .': missing require for '. $this->command_;
469 return false;
470 }
472 // Make sure the argument has a valid class
473 if (!$this->validClass_($class, $name))
474 {
475 $this->message = 'line '. $line .': '. $this->message;
476 return false;
477 }
479 $arg = &$this->s_['arguments'][0];
480 foreach ($arg['values'] as $val)
481 {
482 if (preg_match('/^'. $val['regex'] .'$/m', $text))
483 {
484 // Check if the argument value needs a 'require'
485 if (isset($val['requires']) &&
486 !in_array('"'.$val['requires'].'"', $requires_))
487 {
488 $this->message = 'line '. $line .': missing require for '. $val['name'] .' '. $text;
489 return false;
490 }
492 // Check if a possible value of this argument may occur
493 if ($val['occurrences'] == '?' || $val['occurrences'] == '1')
494 {
495 $val['occurrences'] = '0';
496 }
497 else if ($val['occurrences'] == '+')
498 {
499 $val['occurrences'] = '*';
500 }
501 else if ($val['occurrences'] == '0')
502 {
503 $this->message = 'line '. $line .': too many '. $val['name'] .' '. $class .'s near '. $text;
504 return false;
505 }
507 // Call extra processing function if defined
508 if (isset($val['call']) && !call_user_func(array(&$this, $val['call']), $text) ||
509 isset($arg['call']) && !call_user_func(array(&$this, $arg['call']), $text))
510 {
511 $this->message = 'line '. $line .': '. $this->message;
512 return false;
513 }
515 // Set occurrences appropriately
516 if ($arg['occurrences'] == '?' || $arg['occurrences'] == '1')
517 {
518 array_shift($this->s_['arguments']);
519 }
520 else
521 {
522 $arg['occurrences'] = '*';
523 }
525 // Add argument(s) expected to follow right after this one
526 if (isset($val['add']))
527 {
528 while ($add_arg = array_pop($val['add']))
529 {
530 array_unshift($this->s_['arguments'], $add_arg);
531 }
532 }
534 return true;
535 }
536 }
538 $this->message = 'line '. $line .': unexpected '. $name;
539 return false;
540 }
542 function done($class, $text, $line)
543 {
544 if (isset($this->s_['arguments']))
545 {
546 foreach ($this->s_['arguments'] as $arg)
547 {
548 if ($arg['occurrences'] == '+' || $arg['occurrences'] == '1')
549 {
550 $this->message = 'line '. $line .': '. $class .' '. $text .' where '. $arg['class'] .' expected';
551 return false;
552 }
553 }
554 }
555 return true;
556 }
557 }
559 ?>