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