commit | author | age
|
48e9c1
|
1 |
<?php |
T |
2 |
|
|
3 |
/** |
|
4 |
* Class for operations on Sieve scripts |
|
5 |
* |
|
6 |
* Copyright (C) 2008-2011, The Roundcube Dev Team |
|
7 |
* Copyright (C) 2011, Kolab Systems AG |
|
8 |
* |
07c6c6
|
9 |
* This program is free software: you can redistribute it and/or modify |
TB |
10 |
* it under the terms of the GNU General Public License as published by |
|
11 |
* the Free Software Foundation, either version 3 of the License, or |
|
12 |
* (at your option) any later version. |
48e9c1
|
13 |
* |
T |
14 |
* This program is distributed in the hope that it will be useful, |
|
15 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
16 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
17 |
* GNU General Public License for more details. |
|
18 |
* |
07c6c6
|
19 |
* You should have received a copy of the GNU General Public License |
TB |
20 |
* along with this program. If not, see http://www.gnu.org/licenses/. |
48e9c1
|
21 |
*/ |
T |
22 |
|
|
23 |
class rcube_sieve_script |
|
24 |
{ |
|
25 |
public $content = array(); // script rules array |
|
26 |
|
e499a1
|
27 |
private $vars = array(); // "global" variables |
AM |
28 |
private $prefix = ''; // script header (comments) |
|
29 |
private $supported = array( // supported Sieve extensions: |
ed837a
|
30 |
'body', // RFC5173 |
48e9c1
|
31 |
'copy', // RFC3894 |
ed837a
|
32 |
'date', // RFC5260 |
e499a1
|
33 |
'duplicate', // RFC7352 |
ed837a
|
34 |
'enotify', // RFC5435 |
AM |
35 |
'envelope', // RFC5228 |
|
36 |
'ereject', // RFC5429 |
|
37 |
'fileinto', // RFC5228 |
48e9c1
|
38 |
'imapflags', // draft-melnikov-sieve-imapflags-06 |
T |
39 |
'imap4flags', // RFC5232 |
e499a1
|
40 |
'include', // RFC6609 |
ed837a
|
41 |
'index', // RFC5260 |
e499a1
|
42 |
'notify', // RFC5435 |
ed837a
|
43 |
'regex', // draft-ietf-sieve-regex-01 |
AM |
44 |
'reject', // RFC5429 |
|
45 |
'relational', // RFC3431 |
|
46 |
'subaddress', // RFC5233 |
|
47 |
'vacation', // RFC5230 |
|
48 |
'vacation-seconds', // RFC6131 |
|
49 |
'variables', // RFC5229 |
|
50 |
// @TODO: spamtest+virustest, mailbox |
48e9c1
|
51 |
); |
T |
52 |
|
|
53 |
/** |
|
54 |
* Object constructor |
|
55 |
* |
|
56 |
* @param string Script's text content |
|
57 |
* @param array List of capabilities supported by server |
|
58 |
*/ |
|
59 |
public function __construct($script, $capabilities=array()) |
|
60 |
{ |
|
61 |
$capabilities = array_map('strtolower', (array) $capabilities); |
|
62 |
|
|
63 |
// disable features by server capabilities |
|
64 |
if (!empty($capabilities)) { |
|
65 |
foreach ($this->supported as $idx => $ext) { |
|
66 |
if (!in_array($ext, $capabilities)) { |
|
67 |
unset($this->supported[$idx]); |
|
68 |
} |
|
69 |
} |
|
70 |
} |
|
71 |
|
|
72 |
// Parse text content of the script |
|
73 |
$this->_parse_text($script); |
|
74 |
} |
|
75 |
|
|
76 |
/** |
|
77 |
* Adds rule to the script (at the end) |
|
78 |
* |
|
79 |
* @param string Rule name |
|
80 |
* @param array Rule content (as array) |
|
81 |
* |
|
82 |
* @return int The index of the new rule |
|
83 |
*/ |
|
84 |
public function add_rule($content) |
|
85 |
{ |
|
86 |
// TODO: check this->supported |
|
87 |
array_push($this->content, $content); |
|
88 |
return sizeof($this->content)-1; |
|
89 |
} |
|
90 |
|
|
91 |
public function delete_rule($index) |
|
92 |
{ |
|
93 |
if(isset($this->content[$index])) { |
|
94 |
unset($this->content[$index]); |
|
95 |
return true; |
|
96 |
} |
|
97 |
return false; |
|
98 |
} |
|
99 |
|
|
100 |
public function size() |
|
101 |
{ |
|
102 |
return sizeof($this->content); |
|
103 |
} |
|
104 |
|
|
105 |
public function update_rule($index, $content) |
|
106 |
{ |
|
107 |
// TODO: check this->supported |
|
108 |
if ($this->content[$index]) { |
|
109 |
$this->content[$index] = $content; |
|
110 |
return $index; |
|
111 |
} |
|
112 |
return false; |
|
113 |
} |
|
114 |
|
|
115 |
/** |
|
116 |
* Sets "global" variable |
|
117 |
* |
|
118 |
* @param string $name Variable name |
|
119 |
* @param string $value Variable value |
|
120 |
* @param array $mods Variable modifiers |
|
121 |
*/ |
|
122 |
public function set_var($name, $value, $mods = array()) |
|
123 |
{ |
|
124 |
// Check if variable exists |
|
125 |
for ($i=0, $len=count($this->vars); $i<$len; $i++) { |
|
126 |
if ($this->vars[$i]['name'] == $name) { |
|
127 |
break; |
|
128 |
} |
|
129 |
} |
|
130 |
|
|
131 |
$var = array_merge($mods, array('name' => $name, 'value' => $value)); |
|
132 |
$this->vars[$i] = $var; |
|
133 |
} |
|
134 |
|
|
135 |
/** |
|
136 |
* Unsets "global" variable |
|
137 |
* |
|
138 |
* @param string $name Variable name |
|
139 |
*/ |
|
140 |
public function unset_var($name) |
|
141 |
{ |
|
142 |
// Check if variable exists |
|
143 |
foreach ($this->vars as $idx => $var) { |
|
144 |
if ($var['name'] == $name) { |
|
145 |
unset($this->vars[$idx]); |
|
146 |
break; |
|
147 |
} |
|
148 |
} |
|
149 |
} |
|
150 |
|
|
151 |
/** |
|
152 |
* Gets the value of "global" variable |
|
153 |
* |
|
154 |
* @param string $name Variable name |
|
155 |
* |
|
156 |
* @return string Variable value |
|
157 |
*/ |
|
158 |
public function get_var($name) |
|
159 |
{ |
|
160 |
// Check if variable exists |
|
161 |
for ($i=0, $len=count($this->vars); $i<$len; $i++) { |
|
162 |
if ($this->vars[$i]['name'] == $name) { |
|
163 |
return $this->vars[$i]['name']; |
|
164 |
} |
|
165 |
} |
|
166 |
} |
|
167 |
|
|
168 |
/** |
|
169 |
* Sets script header content |
|
170 |
* |
|
171 |
* @param string $text Header content |
|
172 |
*/ |
|
173 |
public function set_prefix($text) |
|
174 |
{ |
|
175 |
$this->prefix = $text; |
|
176 |
} |
|
177 |
|
|
178 |
/** |
|
179 |
* Returns script as text |
|
180 |
*/ |
|
181 |
public function as_text() |
|
182 |
{ |
|
183 |
$output = ''; |
|
184 |
$exts = array(); |
|
185 |
$idx = 0; |
|
186 |
|
|
187 |
if (!empty($this->vars)) { |
|
188 |
if (in_array('variables', (array)$this->supported)) { |
|
189 |
$has_vars = true; |
|
190 |
array_push($exts, 'variables'); |
|
191 |
} |
|
192 |
foreach ($this->vars as $var) { |
|
193 |
if (empty($has_vars)) { |
|
194 |
// 'variables' extension not supported, put vars in comments |
|
195 |
$output .= sprintf("# %s %s\n", $var['name'], $var['value']); |
|
196 |
} |
|
197 |
else { |
|
198 |
$output .= 'set '; |
|
199 |
foreach (array_diff(array_keys($var), array('name', 'value')) as $opt) { |
|
200 |
$output .= ":$opt "; |
|
201 |
} |
|
202 |
$output .= self::escape_string($var['name']) . ' ' . self::escape_string($var['value']) . ";\n"; |
|
203 |
} |
|
204 |
} |
|
205 |
} |
eb1ee0
|
206 |
|
AM |
207 |
$imapflags = in_array('imap4flags', $this->supported) ? 'imap4flags' : 'imapflags'; |
|
208 |
$notify = in_array('enotify', $this->supported) ? 'enotify' : 'notify'; |
48e9c1
|
209 |
|
T |
210 |
// rules |
|
211 |
foreach ($this->content as $rule) { |
|
212 |
$script = ''; |
|
213 |
$tests = array(); |
|
214 |
$i = 0; |
|
215 |
|
|
216 |
// header |
|
217 |
if (!empty($rule['name']) && strlen($rule['name'])) { |
|
218 |
$script .= '# rule:[' . $rule['name'] . "]\n"; |
|
219 |
} |
|
220 |
|
|
221 |
// constraints expressions |
|
222 |
if (!empty($rule['tests'])) { |
|
223 |
foreach ($rule['tests'] as $test) { |
|
224 |
$tests[$i] = ''; |
|
225 |
switch ($test['test']) { |
|
226 |
case 'size': |
|
227 |
$tests[$i] .= ($test['not'] ? 'not ' : ''); |
|
228 |
$tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg']; |
|
229 |
break; |
|
230 |
|
|
231 |
case 'true': |
|
232 |
$tests[$i] .= ($test['not'] ? 'false' : 'true'); |
|
233 |
break; |
|
234 |
|
|
235 |
case 'exists': |
|
236 |
$tests[$i] .= ($test['not'] ? 'not ' : ''); |
|
237 |
$tests[$i] .= 'exists ' . self::escape_string($test['arg']); |
|
238 |
break; |
|
239 |
|
|
240 |
case 'header': |
|
241 |
$tests[$i] .= ($test['not'] ? 'not ' : ''); |
|
242 |
$tests[$i] .= 'header'; |
|
243 |
|
ed837a
|
244 |
$this->add_index($test, $tests[$i], $exts); |
AM |
245 |
$this->add_operator($test, $tests[$i], $exts); |
48e9c1
|
246 |
|
T |
247 |
$tests[$i] .= ' ' . self::escape_string($test['arg1']); |
|
248 |
$tests[$i] .= ' ' . self::escape_string($test['arg2']); |
|
249 |
break; |
|
250 |
|
|
251 |
case 'address': |
|
252 |
case 'envelope': |
|
253 |
if ($test['test'] == 'envelope') { |
|
254 |
array_push($exts, 'envelope'); |
|
255 |
} |
|
256 |
|
|
257 |
$tests[$i] .= ($test['not'] ? 'not ' : ''); |
|
258 |
$tests[$i] .= $test['test']; |
|
259 |
|
ed837a
|
260 |
if ($test['test'] != 'envelope') { |
AM |
261 |
$this->add_index($test, $tests[$i], $exts); |
|
262 |
} |
|
263 |
|
b2c586
|
264 |
// :all address-part is optional, skip it |
AM |
265 |
if (!empty($test['part']) && $test['part'] != 'all') { |
48e9c1
|
266 |
$tests[$i] .= ' :' . $test['part']; |
T |
267 |
if ($test['part'] == 'user' || $test['part'] == 'detail') { |
|
268 |
array_push($exts, 'subaddress'); |
|
269 |
} |
|
270 |
} |
|
271 |
|
ed837a
|
272 |
$this->add_operator($test, $tests[$i], $exts); |
48e9c1
|
273 |
|
T |
274 |
$tests[$i] .= ' ' . self::escape_string($test['arg1']); |
|
275 |
$tests[$i] .= ' ' . self::escape_string($test['arg2']); |
|
276 |
break; |
|
277 |
|
|
278 |
case 'body': |
|
279 |
array_push($exts, 'body'); |
|
280 |
|
|
281 |
$tests[$i] .= ($test['not'] ? 'not ' : '') . 'body'; |
|
282 |
|
|
283 |
if (!empty($test['part'])) { |
|
284 |
$tests[$i] .= ' :' . $test['part']; |
|
285 |
|
|
286 |
if (!empty($test['content']) && $test['part'] == 'content') { |
|
287 |
$tests[$i] .= ' ' . self::escape_string($test['content']); |
|
288 |
} |
|
289 |
} |
|
290 |
|
ed837a
|
291 |
$this->add_operator($test, $tests[$i], $exts); |
48e9c1
|
292 |
|
T |
293 |
$tests[$i] .= ' ' . self::escape_string($test['arg']); |
ed837a
|
294 |
break; |
AM |
295 |
|
|
296 |
case 'date': |
|
297 |
case 'currentdate': |
|
298 |
array_push($exts, 'date'); |
|
299 |
|
|
300 |
$tests[$i] .= ($test['not'] ? 'not ' : '') . $test['test']; |
|
301 |
|
|
302 |
$this->add_index($test, $tests[$i], $exts); |
|
303 |
|
|
304 |
if (!empty($test['originalzone']) && $test['test'] == 'date') { |
|
305 |
$tests[$i] .= ' :originalzone'; |
|
306 |
} |
|
307 |
else if (!empty($test['zone'])) { |
|
308 |
$tests[$i] .= ' :zone ' . self::escape_string($test['zone']); |
|
309 |
} |
|
310 |
|
|
311 |
$this->add_operator($test, $tests[$i], $exts); |
|
312 |
|
|
313 |
if ($test['test'] == 'date') { |
|
314 |
$tests[$i] .= ' ' . self::escape_string($test['header']); |
|
315 |
} |
|
316 |
|
|
317 |
$tests[$i] .= ' ' . self::escape_string($test['part']); |
|
318 |
$tests[$i] .= ' ' . self::escape_string($test['arg']); |
|
319 |
|
48e9c1
|
320 |
break; |
e499a1
|
321 |
|
AM |
322 |
case 'duplicate': |
|
323 |
array_push($exts, 'duplicate'); |
|
324 |
|
|
325 |
$tests[$i] .= ($test['not'] ? 'not ' : '') . $test['test']; |
|
326 |
|
|
327 |
$tokens = array('handle', 'uniqueid', 'header'); |
|
328 |
foreach ($tokens as $token) |
|
329 |
if ($test[$token] !== null && $test[$token] !== '') { |
|
330 |
$tests[$i] .= " :$token " . self::escape_string($test[$token]); |
|
331 |
} |
|
332 |
|
|
333 |
if (!empty($test['seconds'])) { |
|
334 |
$tests[$i] .= ' :seconds ' . intval($test['seconds']); |
|
335 |
} |
|
336 |
|
|
337 |
if (!empty($test['last'])) { |
|
338 |
$tests[$i] .= ' :last'; |
|
339 |
} |
|
340 |
|
|
341 |
break; |
48e9c1
|
342 |
} |
e499a1
|
343 |
|
48e9c1
|
344 |
$i++; |
T |
345 |
} |
|
346 |
} |
|
347 |
|
|
348 |
// disabled rule: if false #.... |
|
349 |
if (!empty($tests)) { |
|
350 |
$script .= 'if ' . ($rule['disabled'] ? 'false # ' : ''); |
|
351 |
|
|
352 |
if (count($tests) > 1) { |
|
353 |
$tests_str = implode(', ', $tests); |
|
354 |
} |
|
355 |
else { |
|
356 |
$tests_str = $tests[0]; |
|
357 |
} |
|
358 |
|
|
359 |
if ($rule['join'] || count($tests) > 1) { |
|
360 |
$script .= sprintf('%s (%s)', $rule['join'] ? 'allof' : 'anyof', $tests_str); |
|
361 |
} |
|
362 |
else { |
|
363 |
$script .= $tests_str; |
|
364 |
} |
|
365 |
$script .= "\n{\n"; |
|
366 |
} |
|
367 |
|
|
368 |
// action(s) |
|
369 |
if (!empty($rule['actions'])) { |
|
370 |
foreach ($rule['actions'] as $action) { |
|
371 |
$action_script = ''; |
|
372 |
|
|
373 |
switch ($action['type']) { |
|
374 |
|
|
375 |
case 'fileinto': |
|
376 |
array_push($exts, 'fileinto'); |
|
377 |
$action_script .= 'fileinto '; |
|
378 |
if ($action['copy']) { |
|
379 |
$action_script .= ':copy '; |
|
380 |
array_push($exts, 'copy'); |
|
381 |
} |
|
382 |
$action_script .= self::escape_string($action['target']); |
|
383 |
break; |
|
384 |
|
|
385 |
case 'redirect': |
|
386 |
$action_script .= 'redirect '; |
|
387 |
if ($action['copy']) { |
|
388 |
$action_script .= ':copy '; |
|
389 |
array_push($exts, 'copy'); |
|
390 |
} |
|
391 |
$action_script .= self::escape_string($action['target']); |
|
392 |
break; |
|
393 |
|
|
394 |
case 'reject': |
|
395 |
case 'ereject': |
|
396 |
array_push($exts, $action['type']); |
|
397 |
$action_script .= $action['type'].' ' |
|
398 |
. self::escape_string($action['target']); |
|
399 |
break; |
|
400 |
|
|
401 |
case 'addflag': |
|
402 |
case 'setflag': |
|
403 |
case 'removeflag': |
eb1ee0
|
404 |
array_push($exts, $imapflags); |
48e9c1
|
405 |
$action_script .= $action['type'].' ' |
T |
406 |
. self::escape_string($action['target']); |
|
407 |
break; |
|
408 |
|
|
409 |
case 'keep': |
|
410 |
case 'discard': |
|
411 |
case 'stop': |
|
412 |
$action_script .= $action['type']; |
|
413 |
break; |
|
414 |
|
|
415 |
case 'include': |
|
416 |
array_push($exts, 'include'); |
|
417 |
$action_script .= 'include '; |
|
418 |
foreach (array_diff(array_keys($action), array('target', 'type')) as $opt) { |
|
419 |
$action_script .= ":$opt "; |
|
420 |
} |
|
421 |
$action_script .= self::escape_string($action['target']); |
|
422 |
break; |
|
423 |
|
|
424 |
case 'set': |
|
425 |
array_push($exts, 'variables'); |
|
426 |
$action_script .= 'set '; |
|
427 |
foreach (array_diff(array_keys($action), array('name', 'value', 'type')) as $opt) { |
|
428 |
$action_script .= ":$opt "; |
|
429 |
} |
|
430 |
$action_script .= self::escape_string($action['name']) . ' ' . self::escape_string($action['value']); |
|
431 |
break; |
|
432 |
|
2ac00a
|
433 |
case 'notify': |
eb1ee0
|
434 |
array_push($exts, $notify); |
2ac00a
|
435 |
$action_script .= 'notify'; |
eb1ee0
|
436 |
|
f22de5
|
437 |
$method = $action['method']; |
AM |
438 |
unset($action['method']); |
|
439 |
$action['options'] = (array) $action['options']; |
|
440 |
|
|
441 |
// Here we support draft-martin-sieve-notify-01 used by Cyrus |
cd97f0
|
442 |
if ($notify == 'notify') { |
AM |
443 |
switch ($action['importance']) { |
|
444 |
case 1: $action_script .= " :high"; break; |
f22de5
|
445 |
//case 2: $action_script .= " :normal"; break; |
cd97f0
|
446 |
case 3: $action_script .= " :low"; break; |
AM |
447 |
} |
f22de5
|
448 |
|
AM |
449 |
// Old-draft way: :method "mailto" :options "email@address" |
|
450 |
if (!empty($method)) { |
|
451 |
$parts = explode(':', $method, 2); |
|
452 |
$action['method'] = $parts[0]; |
|
453 |
array_unshift($action['options'], $parts[1]); |
|
454 |
} |
|
455 |
|
cd97f0
|
456 |
unset($action['importance']); |
f22de5
|
457 |
unset($action['from']); |
AM |
458 |
unset($method); |
cd97f0
|
459 |
} |
AM |
460 |
|
f22de5
|
461 |
foreach (array('id', 'importance', 'method', 'options', 'from', 'message') as $n_tag) { |
2ac00a
|
462 |
if (!empty($action[$n_tag])) { |
PS |
463 |
$action_script .= " :$n_tag " . self::escape_string($action[$n_tag]); |
|
464 |
} |
|
465 |
} |
cd97f0
|
466 |
|
eb1ee0
|
467 |
if (!empty($method)) { |
f22de5
|
468 |
$action_script .= ' ' . self::escape_string($method); |
eb1ee0
|
469 |
} |
AM |
470 |
|
2ac00a
|
471 |
break; |
PS |
472 |
|
48e9c1
|
473 |
case 'vacation': |
T |
474 |
array_push($exts, 'vacation'); |
|
475 |
$action_script .= 'vacation'; |
b6fa7d
|
476 |
if (isset($action['seconds'])) { |
AM |
477 |
array_push($exts, 'vacation-seconds'); |
|
478 |
$action_script .= " :seconds " . intval($action['seconds']); |
|
479 |
} |
|
480 |
else if (!empty($action['days'])) { |
|
481 |
$action_script .= " :days " . intval($action['days']); |
|
482 |
} |
48e9c1
|
483 |
if (!empty($action['addresses'])) |
T |
484 |
$action_script .= " :addresses " . self::escape_string($action['addresses']); |
|
485 |
if (!empty($action['subject'])) |
|
486 |
$action_script .= " :subject " . self::escape_string($action['subject']); |
|
487 |
if (!empty($action['handle'])) |
|
488 |
$action_script .= " :handle " . self::escape_string($action['handle']); |
|
489 |
if (!empty($action['from'])) |
|
490 |
$action_script .= " :from " . self::escape_string($action['from']); |
|
491 |
if (!empty($action['mime'])) |
|
492 |
$action_script .= " :mime"; |
|
493 |
$action_script .= " " . self::escape_string($action['reason']); |
|
494 |
break; |
|
495 |
} |
|
496 |
|
|
497 |
if ($action_script) { |
|
498 |
$script .= !empty($tests) ? "\t" : ''; |
|
499 |
$script .= $action_script . ";\n"; |
|
500 |
} |
|
501 |
} |
|
502 |
} |
|
503 |
|
|
504 |
if ($script) { |
|
505 |
$output .= $script . (!empty($tests) ? "}\n" : ''); |
|
506 |
$idx++; |
|
507 |
} |
|
508 |
} |
|
509 |
|
|
510 |
// requires |
b6fa7d
|
511 |
if (!empty($exts)) { |
AM |
512 |
$exts = array_unique($exts); |
|
513 |
|
|
514 |
if (in_array('vacation-seconds', $exts) && ($key = array_search('vacation', $exts)) !== false) { |
|
515 |
unset($exts[$key]); |
|
516 |
} |
|
517 |
|
ed837a
|
518 |
sort($exts); // for convenience use always the same order |
AM |
519 |
|
b6fa7d
|
520 |
$output = 'require ["' . implode('","', $exts) . "\"];\n" . $output; |
AM |
521 |
} |
48e9c1
|
522 |
|
T |
523 |
if (!empty($this->prefix)) { |
|
524 |
$output = $this->prefix . "\n\n" . $output; |
|
525 |
} |
|
526 |
|
|
527 |
return $output; |
|
528 |
} |
|
529 |
|
|
530 |
/** |
|
531 |
* Returns script object |
|
532 |
* |
|
533 |
*/ |
|
534 |
public function as_array() |
|
535 |
{ |
|
536 |
return $this->content; |
|
537 |
} |
|
538 |
|
|
539 |
/** |
|
540 |
* Returns array of supported extensions |
|
541 |
* |
|
542 |
*/ |
|
543 |
public function get_extensions() |
|
544 |
{ |
|
545 |
return array_values($this->supported); |
|
546 |
} |
|
547 |
|
|
548 |
/** |
|
549 |
* Converts text script to rules array |
|
550 |
* |
|
551 |
* @param string Text script |
|
552 |
*/ |
|
553 |
private function _parse_text($script) |
|
554 |
{ |
80cb36
|
555 |
$prefix = ''; |
AM |
556 |
$options = array(); |
|
557 |
$position = 0; |
|
558 |
$length = strlen($script); |
48e9c1
|
559 |
|
80cb36
|
560 |
while ($position < $length) { |
AM |
561 |
// skip whitespace chars |
|
562 |
$position = self::ltrim_position($script, $position); |
|
563 |
$rulename = ''; |
48e9c1
|
564 |
|
T |
565 |
// Comments |
80cb36
|
566 |
while ($script[$position] === '#') { |
AM |
567 |
$endl = strpos($script, "\n", $position) ?: $length; |
|
568 |
$line = substr($script, $position, $endl - $position); |
48e9c1
|
569 |
|
T |
570 |
// Roundcube format |
|
571 |
if (preg_match('/^# rule:\[(.*)\]/', $line, $matches)) { |
|
572 |
$rulename = $matches[1]; |
|
573 |
} |
|
574 |
// KEP:14 variables |
|
575 |
else if (preg_match('/^# (EDITOR|EDITOR_VERSION) (.+)$/', $line, $matches)) { |
|
576 |
$this->set_var($matches[1], $matches[2]); |
|
577 |
} |
|
578 |
// Horde-Ingo format |
|
579 |
else if (!empty($options['format']) && $options['format'] == 'INGO' |
|
580 |
&& preg_match('/^# (.*)/', $line, $matches) |
|
581 |
) { |
|
582 |
$rulename = $matches[1]; |
|
583 |
} |
|
584 |
else if (empty($options['prefix'])) { |
|
585 |
$prefix .= $line . "\n"; |
|
586 |
} |
|
587 |
|
80cb36
|
588 |
$position = $endl + 1; |
48e9c1
|
589 |
} |
T |
590 |
|
|
591 |
// handle script header |
|
592 |
if (empty($options['prefix'])) { |
|
593 |
$options['prefix'] = true; |
|
594 |
if ($prefix && strpos($prefix, 'horde.org/ingo')) { |
|
595 |
$options['format'] = 'INGO'; |
|
596 |
} |
|
597 |
} |
|
598 |
|
|
599 |
// Control structures/blocks |
80cb36
|
600 |
if (preg_match('/^(if|else|elsif)/i', substr($script, $position, 5))) { |
AM |
601 |
$rule = $this->_tokenize_rule($script, $position); |
48e9c1
|
602 |
if (strlen($rulename) && !empty($rule)) { |
T |
603 |
$rule['name'] = $rulename; |
|
604 |
} |
|
605 |
} |
|
606 |
// Simple commands |
|
607 |
else { |
80cb36
|
608 |
$rule = $this->_parse_actions($script, $position, ';'); |
48e9c1
|
609 |
if (!empty($rule[0]) && is_array($rule)) { |
T |
610 |
// set "global" variables |
|
611 |
if ($rule[0]['type'] == 'set') { |
|
612 |
unset($rule[0]['type']); |
|
613 |
$this->vars[] = $rule[0]; |
022f51
|
614 |
unset($rule); |
48e9c1
|
615 |
} |
T |
616 |
else { |
|
617 |
$rule = array('actions' => $rule); |
|
618 |
} |
|
619 |
} |
|
620 |
} |
|
621 |
|
|
622 |
if (!empty($rule)) { |
|
623 |
$this->content[] = $rule; |
|
624 |
} |
|
625 |
} |
|
626 |
|
|
627 |
if (!empty($prefix)) { |
|
628 |
$this->prefix = trim($prefix); |
|
629 |
} |
|
630 |
} |
|
631 |
|
|
632 |
/** |
|
633 |
* Convert text script fragment to rule object |
|
634 |
* |
80cb36
|
635 |
* @param string $content The whole script content |
AM |
636 |
* @param int &$position Start position in the script |
48e9c1
|
637 |
* |
T |
638 |
* @return array Rule data |
|
639 |
*/ |
80cb36
|
640 |
private function _tokenize_rule($content, &$position) |
48e9c1
|
641 |
{ |
80cb36
|
642 |
$cond = strtolower(self::tokenize($content, 1, $position)); |
48e9c1
|
643 |
if ($cond != 'if' && $cond != 'elsif' && $cond != 'else') { |
T |
644 |
return null; |
|
645 |
} |
|
646 |
|
|
647 |
$disabled = false; |
|
648 |
$join = false; |
6af79f
|
649 |
$join_not = false; |
80cb36
|
650 |
$length = strlen($content); |
48e9c1
|
651 |
|
T |
652 |
// disabled rule (false + comment): if false # ..... |
80cb36
|
653 |
if (preg_match('/^\s*false\s+#\s*/i', substr($content, $position, 20), $m)) { |
AM |
654 |
$position += strlen($m[0]); |
48e9c1
|
655 |
$disabled = true; |
T |
656 |
} |
|
657 |
|
80cb36
|
658 |
while ($position < $length) { |
AM |
659 |
$tokens = self::tokenize($content, true, $position); |
48e9c1
|
660 |
$separator = array_pop($tokens); |
T |
661 |
|
|
662 |
if (!empty($tokens)) { |
|
663 |
$token = array_shift($tokens); |
|
664 |
} |
|
665 |
else { |
|
666 |
$token = $separator; |
|
667 |
} |
|
668 |
|
|
669 |
$token = strtolower($token); |
|
670 |
|
|
671 |
if ($token == 'not') { |
|
672 |
$not = true; |
|
673 |
$token = strtolower(array_shift($tokens)); |
|
674 |
} |
|
675 |
else { |
|
676 |
$not = false; |
|
677 |
} |
|
678 |
|
6af79f
|
679 |
// we support "not allof" as a negation of allof sub-tests |
AM |
680 |
if ($join_not) { |
|
681 |
$not = !$not; |
|
682 |
} |
|
683 |
|
48e9c1
|
684 |
switch ($token) { |
T |
685 |
case 'allof': |
6af79f
|
686 |
$join = true; |
AM |
687 |
$join_not = $not; |
48e9c1
|
688 |
break; |
6af79f
|
689 |
|
48e9c1
|
690 |
case 'anyof': |
T |
691 |
break; |
|
692 |
|
|
693 |
case 'size': |
6af79f
|
694 |
$test = array('test' => 'size', 'not' => $not); |
ed837a
|
695 |
|
AM |
696 |
$test['arg'] = array_pop($tokens); |
|
697 |
|
48e9c1
|
698 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
T |
699 |
if (!is_array($tokens[$i]) |
|
700 |
&& preg_match('/^:(under|over)$/i', $tokens[$i]) |
|
701 |
) { |
ed837a
|
702 |
$test['type'] = strtolower(substr($tokens[$i], 1)); |
48e9c1
|
703 |
} |
T |
704 |
} |
|
705 |
|
ed837a
|
706 |
$tests[] = $test; |
48e9c1
|
707 |
break; |
T |
708 |
|
|
709 |
case 'header': |
|
710 |
case 'address': |
|
711 |
case 'envelope': |
ed837a
|
712 |
$test = array('test' => $token, 'not' => $not); |
AM |
713 |
|
|
714 |
$test['arg2'] = array_pop($tokens); |
|
715 |
$test['arg1'] = array_pop($tokens); |
|
716 |
|
|
717 |
$test += $this->test_tokens($tokens); |
|
718 |
|
|
719 |
if ($token != 'header' && !empty($tokens)) { |
|
720 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
|
721 |
if (!is_array($tokens[$i]) && preg_match('/^:(localpart|domain|all|user|detail)$/i', $tokens[$i])) { |
|
722 |
$test['part'] = strtolower(substr($tokens[$i], 1)); |
|
723 |
} |
48e9c1
|
724 |
} |
T |
725 |
} |
|
726 |
|
ed837a
|
727 |
$tests[] = $test; |
48e9c1
|
728 |
break; |
T |
729 |
|
|
730 |
case 'body': |
ed837a
|
731 |
$test = array('test' => 'body', 'not' => $not); |
48e9c1
|
732 |
|
ed837a
|
733 |
$test['arg'] = array_pop($tokens); |
AM |
734 |
|
|
735 |
$test += $this->test_tokens($tokens); |
|
736 |
|
|
737 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
|
738 |
if (!is_array($tokens[$i]) && preg_match('/^:(raw|content|text)$/i', $tokens[$i])) { |
|
739 |
$test['part'] = strtolower(substr($tokens[$i], 1)); |
|
740 |
|
|
741 |
if ($test['part'] == 'content') { |
|
742 |
$test['content'] = $tokens[++$i]; |
48e9c1
|
743 |
} |
T |
744 |
} |
|
745 |
} |
|
746 |
|
ed837a
|
747 |
$tests[] = $test; |
AM |
748 |
break; |
|
749 |
|
|
750 |
case 'date': |
|
751 |
case 'currentdate': |
|
752 |
$test = array('test' => $token, 'not' => $not); |
|
753 |
|
|
754 |
$test['arg'] = array_pop($tokens); |
|
755 |
$test['part'] = array_pop($tokens); |
|
756 |
|
|
757 |
if ($token == 'date') { |
|
758 |
$test['header'] = array_pop($tokens); |
|
759 |
} |
|
760 |
|
|
761 |
$test += $this->test_tokens($tokens); |
|
762 |
|
|
763 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
|
764 |
if (!is_array($tokens[$i]) && preg_match('/^:zone$/i', $tokens[$i])) { |
|
765 |
$test['zone'] = $tokens[++$i]; |
|
766 |
} |
|
767 |
else if (!is_array($tokens[$i]) && preg_match('/^:originalzone$/i', $tokens[$i])) { |
|
768 |
$test['originalzone'] = true; |
|
769 |
} |
|
770 |
} |
|
771 |
|
|
772 |
$tests[] = $test; |
48e9c1
|
773 |
break; |
T |
774 |
|
e499a1
|
775 |
case 'duplicate': |
AM |
776 |
$test = array('test' => $token, 'not' => $not); |
|
777 |
|
|
778 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
|
779 |
if (!is_array($tokens[$i])) { |
|
780 |
if (preg_match('/^:(handle|header|uniqueid|seconds)$/i', $tokens[$i], $m)) { |
|
781 |
$test[strtolower($m[1])] = $tokens[++$i]; |
|
782 |
} |
|
783 |
else if (preg_match('/^:last$/i', $tokens[$i])) { |
|
784 |
$test['last'] = true; |
|
785 |
} |
|
786 |
} |
|
787 |
} |
|
788 |
|
|
789 |
$tests[] = $test; |
|
790 |
break; |
|
791 |
|
48e9c1
|
792 |
case 'exists': |
6af79f
|
793 |
$tests[] = array('test' => 'exists', 'not' => $not, |
48e9c1
|
794 |
'arg' => array_pop($tokens)); |
T |
795 |
break; |
|
796 |
|
|
797 |
case 'true': |
6af79f
|
798 |
$tests[] = array('test' => 'true', 'not' => $not); |
48e9c1
|
799 |
break; |
T |
800 |
|
|
801 |
case 'false': |
6af79f
|
802 |
$tests[] = array('test' => 'true', 'not' => !$not); |
48e9c1
|
803 |
break; |
T |
804 |
} |
|
805 |
|
|
806 |
// goto actions... |
|
807 |
if ($separator == '{') { |
|
808 |
break; |
|
809 |
} |
|
810 |
} |
|
811 |
|
|
812 |
// ...and actions block |
80cb36
|
813 |
$actions = $this->_parse_actions($content, $position); |
48e9c1
|
814 |
|
T |
815 |
if ($tests && $actions) { |
|
816 |
$result = array( |
|
817 |
'type' => $cond, |
|
818 |
'tests' => $tests, |
|
819 |
'actions' => $actions, |
|
820 |
'join' => $join, |
|
821 |
'disabled' => $disabled, |
|
822 |
); |
|
823 |
} |
|
824 |
|
|
825 |
return $result; |
|
826 |
} |
|
827 |
|
|
828 |
/** |
|
829 |
* Parse body of actions section |
|
830 |
* |
80cb36
|
831 |
* @param string $content The whole script content |
AM |
832 |
* @param int &$position Start position in the script |
|
833 |
* @param string $end End of text separator |
48e9c1
|
834 |
* |
T |
835 |
* @return array Array of parsed action type/target pairs |
|
836 |
*/ |
80cb36
|
837 |
private function _parse_actions($content, &$position, $end = '}') |
48e9c1
|
838 |
{ |
T |
839 |
$result = null; |
80cb36
|
840 |
$length = strlen($content); |
48e9c1
|
841 |
|
80cb36
|
842 |
while ($position < $length) { |
AM |
843 |
$tokens = self::tokenize($content, true, $position); |
48e9c1
|
844 |
$separator = array_pop($tokens); |
ed837a
|
845 |
$token = !empty($tokens) ? array_shift($tokens) : $separator; |
48e9c1
|
846 |
|
T |
847 |
switch ($token) { |
|
848 |
case 'discard': |
|
849 |
case 'keep': |
|
850 |
case 'stop': |
|
851 |
$result[] = array('type' => $token); |
|
852 |
break; |
|
853 |
|
|
854 |
case 'fileinto': |
|
855 |
case 'redirect': |
ed837a
|
856 |
$action = array('type' => $token, 'target' => array_pop($tokens)); |
AM |
857 |
$args = array('copy'); |
|
858 |
$action += $this->action_arguments($tokens, $args); |
48e9c1
|
859 |
|
ed837a
|
860 |
$result[] = $action; |
AM |
861 |
break; |
48e9c1
|
862 |
|
ed837a
|
863 |
case 'vacation': |
AM |
864 |
$action = array('type' => 'vacation', 'reason' => array_pop($tokens)); |
|
865 |
$args = array('mime'); |
|
866 |
$vargs = array('seconds', 'days', 'addresses', 'subject', 'handle', 'from'); |
|
867 |
$action += $this->action_arguments($tokens, $args, $vargs); |
|
868 |
|
|
869 |
$result[] = $action; |
48e9c1
|
870 |
break; |
T |
871 |
|
|
872 |
case 'reject': |
|
873 |
case 'ereject': |
|
874 |
case 'setflag': |
|
875 |
case 'addflag': |
|
876 |
case 'removeflag': |
ed837a
|
877 |
$result[] = array('type' => $token, 'target' => array_pop($tokens)); |
48e9c1
|
878 |
break; |
T |
879 |
|
|
880 |
case 'include': |
ed837a
|
881 |
$action = array('type' => 'include', 'target' => array_pop($tokens)); |
AM |
882 |
$args = array('once', 'optional', 'global', 'personal'); |
|
883 |
$action += $this->action_arguments($tokens, $args); |
48e9c1
|
884 |
|
ed837a
|
885 |
$result[] = $action; |
48e9c1
|
886 |
break; |
T |
887 |
|
|
888 |
case 'set': |
ed837a
|
889 |
$action = array('type' => 'set', 'value' => array_pop($tokens), 'name' => array_pop($tokens)); |
AM |
890 |
$args = array('lower', 'upper', 'lowerfirst', 'upperfirst', 'quotewildcard', 'length'); |
|
891 |
$action += $this->action_arguments($tokens, $args); |
48e9c1
|
892 |
|
ed837a
|
893 |
$result[] = $action; |
48e9c1
|
894 |
break; |
T |
895 |
|
|
896 |
case 'require': |
|
897 |
// skip, will be build according to used commands |
ed837a
|
898 |
// $result[] = array('type' => 'require', 'target' => array_pop($tokens)); |
48e9c1
|
899 |
break; |
T |
900 |
|
2ac00a
|
901 |
case 'notify': |
ed837a
|
902 |
$action = array('type' => 'notify'); |
AM |
903 |
$priorities = array('high' => 1, 'normal' => 2, 'low' => 3); |
f22de5
|
904 |
$vargs = array('from', 'id', 'importance', 'options', 'message', 'method'); |
ed837a
|
905 |
$args = array_keys($priorities); |
AM |
906 |
$action += $this->action_arguments($tokens, $args, $vargs); |
2ac00a
|
907 |
|
f22de5
|
908 |
// Here we'll convert draft-martin-sieve-notify-01 into RFC 5435 |
ed837a
|
909 |
if (!isset($action['importance'])) { |
AM |
910 |
foreach ($priorities as $key => $val) { |
|
911 |
if (isset($action[$key])) { |
|
912 |
$action['importance'] = $val; |
|
913 |
unset($action[$key]); |
cd97f0
|
914 |
} |
2ac00a
|
915 |
} |
PS |
916 |
} |
eb1ee0
|
917 |
|
f22de5
|
918 |
$action['options'] = (array) $action['options']; |
ed837a
|
919 |
|
f22de5
|
920 |
// Old-draft way: :method "mailto" :options "email@address" |
AM |
921 |
if (!empty($action['method']) && !empty($action['options'])) { |
|
922 |
$action['method'] .= ':' . array_shift($action['options']); |
|
923 |
} |
|
924 |
// unnamed parameter is a :method in enotify extension |
|
925 |
else if (!isset($action['method'])) { |
|
926 |
$action['method'] = array_pop($tokens); |
2ac00a
|
927 |
} |
PS |
928 |
|
ed837a
|
929 |
$result[] = $action; |
2ac00a
|
930 |
break; |
48e9c1
|
931 |
} |
T |
932 |
|
|
933 |
if ($separator == $end) |
|
934 |
break; |
|
935 |
} |
|
936 |
|
|
937 |
return $result; |
|
938 |
} |
|
939 |
|
|
940 |
/** |
ed837a
|
941 |
* Add comparator to the test |
48e9c1
|
942 |
*/ |
T |
943 |
private function add_comparator($test, &$out, &$exts) |
|
944 |
{ |
|
945 |
if (empty($test['comparator'])) { |
|
946 |
return; |
|
947 |
} |
|
948 |
|
|
949 |
if ($test['comparator'] == 'i;ascii-numeric') { |
|
950 |
array_push($exts, 'relational'); |
|
951 |
array_push($exts, 'comparator-i;ascii-numeric'); |
|
952 |
} |
|
953 |
else if (!in_array($test['comparator'], array('i;octet', 'i;ascii-casemap'))) { |
|
954 |
array_push($exts, 'comparator-' . $test['comparator']); |
|
955 |
} |
|
956 |
|
|
957 |
// skip default comparator |
|
958 |
if ($test['comparator'] != 'i;ascii-casemap') { |
|
959 |
$out .= ' :comparator ' . self::escape_string($test['comparator']); |
|
960 |
} |
|
961 |
} |
|
962 |
|
|
963 |
/** |
ed837a
|
964 |
* Add index argument to the test |
AM |
965 |
*/ |
|
966 |
private function add_index($test, &$out, &$exts) |
|
967 |
{ |
|
968 |
if (!empty($test['index'])) { |
|
969 |
array_push($exts, 'index'); |
|
970 |
$out .= ' :index ' . intval($test['index']) . ($test['last'] ? ' :last' : ''); |
|
971 |
} |
|
972 |
} |
|
973 |
|
|
974 |
/** |
|
975 |
* Add operators to the test |
|
976 |
*/ |
|
977 |
private function add_operator($test, &$out, &$exts) |
|
978 |
{ |
|
979 |
if (empty($test['type'])) { |
|
980 |
return; |
|
981 |
} |
|
982 |
|
889c76
|
983 |
// relational operator |
ed837a
|
984 |
if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) { |
AM |
985 |
array_push($exts, 'relational'); |
|
986 |
|
439fd7
|
987 |
$out .= ' :' . $m[1] . ' "' . $m[2] . '"'; |
ed837a
|
988 |
} |
AM |
989 |
else { |
|
990 |
if ($test['type'] == 'regex') { |
|
991 |
array_push($exts, 'regex'); |
|
992 |
} |
|
993 |
|
|
994 |
$out .= ' :' . $test['type']; |
|
995 |
} |
889c76
|
996 |
|
AM |
997 |
$this->add_comparator($test, $out, $exts); |
ed837a
|
998 |
} |
AM |
999 |
|
|
1000 |
/** |
|
1001 |
* Extract test tokens |
|
1002 |
*/ |
|
1003 |
private function test_tokens(&$tokens) |
|
1004 |
{ |
|
1005 |
$test = array(); |
|
1006 |
$result = array(); |
|
1007 |
|
|
1008 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
e499a1
|
1009 |
if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) { |
AM |
1010 |
$test['comparator'] = $tokens[++$i]; |
302aff
|
1011 |
} |
e499a1
|
1012 |
else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) { |
AM |
1013 |
$test['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i]; |
|
1014 |
} |
|
1015 |
else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) { |
|
1016 |
$test['type'] = strtolower(substr($tokens[$i], 1)); |
|
1017 |
} |
|
1018 |
else if (!is_array($tokens[$i]) && preg_match('/^:index$/i', $tokens[$i])) { |
|
1019 |
$test['index'] = intval($tokens[++$i]); |
|
1020 |
if ($tokens[$i+1] && preg_match('/^:last$/i', $tokens[$i+1])) { |
|
1021 |
$test['last'] = true; |
|
1022 |
$i++; |
|
1023 |
} |
|
1024 |
} |
|
1025 |
else { |
|
1026 |
$result[] = $tokens[$i]; |
|
1027 |
} |
ed837a
|
1028 |
} |
AM |
1029 |
|
|
1030 |
$tokens = $result; |
|
1031 |
|
|
1032 |
return $test; |
|
1033 |
} |
|
1034 |
|
|
1035 |
/** |
|
1036 |
* Extract action arguments |
|
1037 |
*/ |
|
1038 |
private function action_arguments(&$tokens, $bool_args, $val_args = array()) |
|
1039 |
{ |
|
1040 |
$action = array(); |
|
1041 |
$result = array(); |
|
1042 |
|
|
1043 |
for ($i=0, $len=count($tokens); $i<$len; $i++) { |
|
1044 |
$tok = $tokens[$i]; |
|
1045 |
if (!is_array($tok) && $tok[0] == ':') { |
|
1046 |
$tok = strtolower(substr($tok, 1)); |
|
1047 |
if (in_array($tok, $bool_args)) { |
|
1048 |
$action[$tok] = true; |
|
1049 |
} |
|
1050 |
else if (in_array($tok, $val_args)) { |
|
1051 |
$action[$tok] = $tokens[++$i]; |
|
1052 |
} |
|
1053 |
else { |
|
1054 |
$result[] = $tok; |
|
1055 |
} |
|
1056 |
} |
|
1057 |
else { |
|
1058 |
$result[] = $tok; |
|
1059 |
} |
|
1060 |
} |
|
1061 |
|
|
1062 |
$tokens = $result; |
|
1063 |
|
|
1064 |
return $action; |
|
1065 |
} |
|
1066 |
|
|
1067 |
/** |
48e9c1
|
1068 |
* Escape special chars into quoted string value or multi-line string |
T |
1069 |
* or list of strings |
|
1070 |
* |
|
1071 |
* @param string $str Text or array (list) of strings |
|
1072 |
* |
|
1073 |
* @return string Result text |
|
1074 |
*/ |
|
1075 |
static function escape_string($str) |
|
1076 |
{ |
|
1077 |
if (is_array($str) && count($str) > 1) { |
|
1078 |
foreach($str as $idx => $val) |
|
1079 |
$str[$idx] = self::escape_string($val); |
|
1080 |
|
|
1081 |
return '[' . implode(',', $str) . ']'; |
|
1082 |
} |
|
1083 |
else if (is_array($str)) { |
|
1084 |
$str = array_pop($str); |
|
1085 |
} |
|
1086 |
|
|
1087 |
// multi-line string |
|
1088 |
if (preg_match('/[\r\n\0]/', $str) || strlen($str) > 1024) { |
|
1089 |
return sprintf("text:\n%s\n.\n", self::escape_multiline_string($str)); |
|
1090 |
} |
|
1091 |
// quoted-string |
|
1092 |
else { |
|
1093 |
return '"' . addcslashes($str, '\\"') . '"'; |
|
1094 |
} |
|
1095 |
} |
|
1096 |
|
|
1097 |
/** |
|
1098 |
* Escape special chars in multi-line string value |
|
1099 |
* |
|
1100 |
* @param string $str Text |
|
1101 |
* |
|
1102 |
* @return string Text |
|
1103 |
*/ |
|
1104 |
static function escape_multiline_string($str) |
|
1105 |
{ |
|
1106 |
$str = preg_split('/(\r?\n)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE); |
|
1107 |
|
|
1108 |
foreach ($str as $idx => $line) { |
|
1109 |
// dot-stuffing |
|
1110 |
if (isset($line[0]) && $line[0] == '.') { |
|
1111 |
$str[$idx] = '.' . $line; |
|
1112 |
} |
|
1113 |
} |
|
1114 |
|
|
1115 |
return implode($str); |
|
1116 |
} |
|
1117 |
|
|
1118 |
/** |
|
1119 |
* Splits script into string tokens |
|
1120 |
* |
80cb36
|
1121 |
* @param string $str The script |
AM |
1122 |
* @param mixed $num Number of tokens to return, 0 for all |
|
1123 |
* or True for all tokens until separator is found. |
|
1124 |
* Separator will be returned as last token. |
|
1125 |
* @param int &$position Parsing start position |
48e9c1
|
1126 |
* |
T |
1127 |
* @return mixed Tokens array or string if $num=1 |
|
1128 |
*/ |
80cb36
|
1129 |
static function tokenize($str, $num = 0, &$position = 0) |
48e9c1
|
1130 |
{ |
T |
1131 |
$result = array(); |
80cb36
|
1132 |
$length = strlen($str); |
48e9c1
|
1133 |
|
T |
1134 |
// remove spaces from the beginning of the string |
80cb36
|
1135 |
while ($position < $length && (!$num || $num === true || count($result) < $num)) { |
AM |
1136 |
// skip whitespace chars |
|
1137 |
$position = self::ltrim_position($str, $position); |
|
1138 |
|
|
1139 |
switch ($str[$position]) { |
48e9c1
|
1140 |
|
T |
1141 |
// Quoted string |
|
1142 |
case '"': |
80cb36
|
1143 |
for ($pos = $position + 1; $pos < $length; $pos++) { |
48e9c1
|
1144 |
if ($str[$pos] == '"') { |
T |
1145 |
break; |
|
1146 |
} |
|
1147 |
if ($str[$pos] == "\\") { |
|
1148 |
if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") { |
|
1149 |
$pos++; |
|
1150 |
} |
|
1151 |
} |
|
1152 |
} |
|
1153 |
if ($str[$pos] != '"') { |
|
1154 |
// error |
|
1155 |
} |
80cb36
|
1156 |
|
48e9c1
|
1157 |
// we need to strip slashes for a quoted string |
80cb36
|
1158 |
$result[] = stripslashes(substr($str, $position + 1, $pos - $position - 1)); |
AM |
1159 |
$position = $pos + 1; |
48e9c1
|
1160 |
break; |
T |
1161 |
|
|
1162 |
// Parenthesized list |
|
1163 |
case '[': |
80cb36
|
1164 |
$position++; |
AM |
1165 |
$result[] = self::tokenize($str, 0, $position); |
48e9c1
|
1166 |
break; |
T |
1167 |
case ']': |
80cb36
|
1168 |
$position++; |
48e9c1
|
1169 |
return $result; |
T |
1170 |
break; |
|
1171 |
|
|
1172 |
// list/test separator |
|
1173 |
case ',': |
|
1174 |
// command separator |
|
1175 |
case ';': |
|
1176 |
// block/tests-list |
|
1177 |
case '(': |
|
1178 |
case ')': |
|
1179 |
case '{': |
|
1180 |
case '}': |
80cb36
|
1181 |
$sep = $str[$position]; |
AM |
1182 |
$position++; |
48e9c1
|
1183 |
if ($num === true) { |
T |
1184 |
$result[] = $sep; |
|
1185 |
break 2; |
|
1186 |
} |
|
1187 |
break; |
|
1188 |
|
|
1189 |
// bracket-comment |
|
1190 |
case '/': |
80cb36
|
1191 |
if ($str[$position + 1] == '*') { |
AM |
1192 |
if ($end_pos = strpos($str, '*/', $position + 2)) { |
|
1193 |
$position = $end_pos + 2; |
48e9c1
|
1194 |
} |
T |
1195 |
else { |
|
1196 |
// error |
80cb36
|
1197 |
$position = $length; |
48e9c1
|
1198 |
} |
T |
1199 |
} |
|
1200 |
break; |
|
1201 |
|
|
1202 |
// hash-comment |
|
1203 |
case '#': |
80cb36
|
1204 |
if ($lf_pos = strpos($str, "\n", $position)) { |
AM |
1205 |
$position = $lf_pos + 1; |
48e9c1
|
1206 |
break; |
T |
1207 |
} |
|
1208 |
else { |
80cb36
|
1209 |
$position = $length; |
48e9c1
|
1210 |
} |
T |
1211 |
|
|
1212 |
// String atom |
|
1213 |
default: |
|
1214 |
// empty or one character |
80cb36
|
1215 |
if ($position == $length) { |
48e9c1
|
1216 |
break 2; |
T |
1217 |
} |
e499a1
|
1218 |
if ($length - $position < 2) { |
AM |
1219 |
$result[] = substr($str, $position); |
|
1220 |
$position = $length; |
|
1221 |
break; |
|
1222 |
} |
48e9c1
|
1223 |
|
T |
1224 |
// tag/identifier/number |
e499a1
|
1225 |
if (preg_match('/[a-zA-Z0-9:_]+/', $str, $m, PREG_OFFSET_CAPTURE, $position) |
AM |
1226 |
&& $m[0][1] == $position |
|
1227 |
) { |
|
1228 |
$atom = $m[0][0]; |
|
1229 |
$position += strlen($atom); |
48e9c1
|
1230 |
|
80cb36
|
1231 |
if ($atom != 'text:') { |
AM |
1232 |
$result[] = $atom; |
48e9c1
|
1233 |
} |
T |
1234 |
// multiline string |
|
1235 |
else { |
80cb36
|
1236 |
// skip whitespace chars (except \r\n) |
AM |
1237 |
$position = self::ltrim_position($str, $position, false); |
|
1238 |
|
48e9c1
|
1239 |
// possible hash-comment after "text:" |
80cb36
|
1240 |
if ($str[$position] === '#') { |
AM |
1241 |
$endl = strpos($str, "\n", $position); |
|
1242 |
$position = $endl ?: $length; |
48e9c1
|
1243 |
} |
T |
1244 |
|
80cb36
|
1245 |
// skip \n or \r\n |
AM |
1246 |
if ($str[$position] == "\n") { |
|
1247 |
$position++; |
|
1248 |
} |
|
1249 |
else if ($str[$position] == "\r" && $str[$position] == "\n") { |
|
1250 |
$position += 2; |
|
1251 |
} |
|
1252 |
|
|
1253 |
$text = ''; |
|
1254 |
|
|
1255 |
// get text until alone dot in a line |
|
1256 |
while ($position < $length) { |
|
1257 |
$pos = strpos($str, "\n.", $position); |
|
1258 |
if ($pos === false) { |
|
1259 |
break; |
|
1260 |
} |
|
1261 |
|
|
1262 |
$text .= substr($str, $position, $pos - $position); |
|
1263 |
$position = $pos + 2; |
|
1264 |
|
|
1265 |
if ($str[$pos] == "\n" |
|
1266 |
|| ($str[$pos] == "\r" && $str[$pos + 1] == "\n") |
|
1267 |
) { |
|
1268 |
break; |
|
1269 |
} |
|
1270 |
} |
|
1271 |
|
|
1272 |
// remove dot-stuffing |
|
1273 |
$text = str_replace("\n..", "\n.", $text); |
|
1274 |
|
48e9c1
|
1275 |
$result[] = $text; |
80cb36
|
1276 |
$position++; |
48e9c1
|
1277 |
} |
T |
1278 |
} |
|
1279 |
// fallback, skip one character as infinite loop prevention |
|
1280 |
else { |
80cb36
|
1281 |
$position++; |
48e9c1
|
1282 |
} |
T |
1283 |
|
|
1284 |
break; |
|
1285 |
} |
|
1286 |
} |
|
1287 |
|
|
1288 |
return $num === 1 ? (isset($result[0]) ? $result[0] : null) : $result; |
|
1289 |
} |
|
1290 |
|
80cb36
|
1291 |
/** |
AM |
1292 |
* Skip whitespace characters in a string from specified position. |
|
1293 |
*/ |
|
1294 |
static function ltrim_position($content, $position, $br = true) |
|
1295 |
{ |
|
1296 |
$blanks = array("\t", "\0", "\x0B", " "); |
|
1297 |
|
|
1298 |
if ($br) { |
|
1299 |
$blanks[] = "\r"; |
|
1300 |
$blanks[] = "\n"; |
|
1301 |
} |
|
1302 |
|
|
1303 |
while (isset($content[$position]) && isset($content[$position + 1]) |
|
1304 |
&& in_array($content[$position], $blanks, true) |
|
1305 |
) { |
|
1306 |
$position++; |
|
1307 |
} |
|
1308 |
|
|
1309 |
return $position; |
|
1310 |
} |
48e9c1
|
1311 |
} |