Skip to content

Commit 9ecb474

Browse files
committed
Zipdownload: fix escaping in Mbox downloads
1 parent a556c2c commit 9ecb474

1 file changed

Lines changed: 59 additions & 7 deletions

File tree

plugins/zipdownload/zipdownload.php

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,23 +394,75 @@ private function _filename_from_subject($str)
394394
}
395395
}
396396

397+
/**
398+
* Unfortunately a body line which could be mistaken for an Mbox header,
399+
* and which therefore must be escaped with '>', can be split between
400+
* buckets (and between calls to filter()), so if the bucket being filtered
401+
* ends in a way that *might* be a line needing escaping, postpone placing
402+
* it in the $out brigade until we've seen the next bucket and can decide
403+
* whether or not to escape. Don't bother checking if such a line might be
404+
* split among more than 2 buckets -- given even a little buffering, this
405+
* should never happen in practice.
406+
*/
397407
class zipdownload_mbox_filter extends \php_user_filter
398408
{
409+
private $prev_bucket;
410+
private $prev_match;
411+
private $first = true;
412+
399413
#[\Override]
400414
#[\ReturnTypeWillChange]
401415
public function filter($in, $out, &$consumed, $closing)
402416
{
417+
$out_empty = true;
418+
$replaced = 0;
419+
$m = [];
420+
403421
while ($bucket = stream_bucket_make_writeable($in)) {
404-
// messages are read line by line
405-
if (preg_match('/^>*From /', $bucket->data)) {
406-
$bucket->data = '>' . $bucket->data;
407-
$bucket->datalen++;
422+
// Escape lines which definitely need it
423+
$anchor = $this->first ? '^' : "\n"; // Only the file's first line may match w/o \n
424+
$this->first = false;
425+
$bucket->data = preg_replace("/({$anchor}>*)From /m", '\1>From ', $bucket->data, -1, $replaced);
426+
$consumed += $bucket->datalen;
427+
$bucket->datalen += $replaced;
428+
429+
// If the previous bucket was postponed...
430+
if ($this->prev_bucket) {
431+
// Escape its last line if necessary
432+
$joined = $this->prev_match . substr($bucket->data, 0, strspn($bucket->data, '>From '));
433+
if (preg_match('/^>*From /', $joined)) {
434+
if (strlen($this->prev_match)) { // prev_match == end of prev_bucket w/o \n, e.g. 'Fro' or '>>F' or ''
435+
$this->prev_bucket->data = substr_replace(
436+
$this->prev_bucket->data, '>' . $this->prev_match, -strlen($this->prev_match));
437+
} else {
438+
$this->prev_bucket->data .= '>';
439+
}
440+
$this->prev_bucket->datalen++;
441+
}
442+
443+
// And in either case send it out
444+
stream_bucket_append($out, $this->prev_bucket);
445+
$this->prev_bucket = null;
446+
$out_empty = false;
408447
}
409448

410-
$consumed += (int) $bucket->datalen;
411-
stream_bucket_append($out, $bucket);
449+
// Decide if the current bucket should be postponed or sent immediately
450+
if (str_contains("\n>From", substr($bucket->data, -1))
451+
&& preg_match('/\G\n>*(?:F(?:r(?:om?)?)?)?\z/', $bucket->data, $m, 0, strrpos($bucket->data, "\n"))
452+
) {
453+
$this->prev_bucket = $bucket;
454+
$this->prev_match = substr($m[0], 1); // What was matched without the initial \n (could be '')
455+
} else {
456+
stream_bucket_append($out, $bucket);
457+
$out_empty = false;
458+
}
412459
}
413460

414-
return \PSFS_PASS_ON;
461+
if ($closing && $this->prev_bucket) {
462+
stream_bucket_append($out, $this->prev_bucket);
463+
$this->prev_bucket = null;
464+
$out_empty = false;
465+
}
466+
return $out_empty ? \PSFS_FEED_ME : \PSFS_PASS_ON;
415467
}
416468
}

0 commit comments

Comments
 (0)