Mercurial > hg > rc1
comparison plugins/managesieve/lib/Roundcube/rcube_sieve_script.php @ 0:1e000243b222
vanilla 1.3.3 distro, I hope
| author | Charlie Root |
|---|---|
| date | Thu, 04 Jan 2018 15:50:29 -0500 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:1e000243b222 |
|---|---|
| 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 } |
