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