1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8//this script may only be included - so its better to die if called directly.
9if (strpos($_SERVER['SCRIPT_NAME'], basename(__FILE__)) !== false) {
10	header('location: index.php');
11	exit;
12}
13
14/**
15 * GZIP stuff.
16 *
17 * Note that we use gzopen()/gzwrite() instead of gzcompress() even if
18 * gzcompress() is available. Gzcompress() puts out data with
19 * different headers --- in particular it includes an "adler-32"
20 * checksum rather than a "CRC32" checksum. Since we need the CRC-32
21 * checksum, and since not all PHP's have gzcompress(), we'll just
22 * stick with gzopen().
23 */
24function gzip_cleanup()
25{
26	global $gzip_tmpfile;
27
28	if ($gzip_tmpfile) {
29		@unlink($gzip_tmpfile);
30	}
31}
32
33function gzip_tempnam()
34{
35	global $gzip_tmpfile;
36
37	if (! $gzip_tmpfile) {
38		//FIXME: does this work on non-unix machines?
39		$gzip_tmpfile = tempnam('/tmp', 'wkzip');
40
41		register_shutdown_function('gzip_cleanup');
42	}
43
44	return $gzip_tmpfile;
45}
46
47function gzip_compress($data)
48{
49	$filename = gzip_tempnam();
50
51	if (! ($fp = gzopen($filename, 'wb'))) {
52		trigger_error(sprintf('%s failed', 'gzopen'), E_USER_ERROR);
53	}
54
55	gzwrite($fp, $data, strlen($data));
56
57	if (! gzclose($fp)) {
58		trigger_error(sprintf('%s failed', 'gzclose'), E_USER_ERROR);
59	}
60
61	$size = filesize($filename);
62
63	if (! ($fp = fopen($filename, 'rb'))) {
64		trigger_error(sprintf('%s failed', 'fopen'), E_USER_ERROR);
65	}
66
67	if (! ($z = fread($fp, $size)) || strlen($z) != $size) {
68		trigger_error(sprintf('%s failed', 'fread'), E_USER_ERROR);
69	}
70
71	if (! fclose($fp)) {
72		trigger_error(sprintf('%s failed', 'fclose'), E_USER_ERROR);
73	}
74
75	unlink($filename);
76	return $z;
77}
78
79function gzip_uncompress($data)
80{
81	$filename = gzip_tempnam();
82
83	if (! ($fp = fopen($filename, 'wb'))) {
84		trigger_error(sprintf('%s failed', 'fopen'), E_USER_ERROR);
85	}
86
87	fwrite($fp, $data, strlen($data));
88
89	if (! fclose($fp)) {
90		trigger_error(sprintf('%s failed', 'fclose'), E_USER_ERROR);
91	}
92
93	if (! ($fp = gzopen($filename, 'rb'))) {
94		trigger_error(sprintf('%s failed', 'gzopen'), E_USER_ERROR);
95	}
96
97	$unz = '';
98
99	while ($buf = gzread($fp, 4096)) {
100		$unz .= $buf;
101	}
102
103	if (! gzclose($fp)) {
104		trigger_error(sprintf("%s failed", 'gzclose'), E_USER_ERROR);
105	}
106
107	unlink($filename);
108	return $unz;
109}
110
111/**
112 * CRC32 computation. Hacked from Info-zip's zip-2.3 source code.
113 */
114function zip_crc32($str, $crc = 0)
115{
116	static $zip_crc_table;
117
118	if (empty($zip_crc_table)) {
119		/* NOTE: The range of PHP ints seems to be -0x80000000 to 0x7fffffff.
120		 * So, had to munge these constants.
121		 */
122		$zip_crc_table = [
123				0x00000000,
124				0x77073096,
125				-0x11f19ed4,
126				-0x66f6ae46,
127				0x076dc419,
128				0x706af48f,
129				-0x169c5acb,
130				-0x619b6a5d,
131				0x0edb8832,
132				0x79dcb8a4,
133				-0x1f2a16e2,
134				-0x682d2678,
135				0x09b64c2b,
136				0x7eb17cbd,
137				-0x1847d2f9,
138				-0x6f40e26f,
139				0x1db71064,
140				0x6ab020f2,
141				-0x0c468eb8,
142				-0x7b41be22,
143				0x1adad47d,
144				0x6ddde4eb,
145				-0x0b2b4aaf,
146				-0x7c2c7a39,
147				0x136c9856,
148				0x646ba8c0,
149				-0x029d0686,
150				-0x759a3614,
151				0x14015c4f,
152				0x63066cd9,
153				-0x05f0c29d,
154				-0x72f7f20b,
155				0x3b6e20c8,
156				0x4c69105e,
157				-0x2a9fbe1c,
158				-0x5d988e8e,
159				0x3c03e4d1,
160				0x4b04d447,
161				-0x2df27a03,
162				-0x5af54a95,
163				0x35b5a8fa,
164				0x42b2986c,
165				-0x2444362a,
166				-0x534306c0,
167				0x32d86ce3,
168				0x45df5c75,
169				-0x2329f231,
170				-0x542ec2a7,
171				0x26d930ac,
172				0x51de003a,
173				-0x3728ae80,
174				-0x402f9eea,
175				0x21b4f4b5,
176				0x56b3c423,
177				-0x30456a67,
178				-0x47425af1,
179				0x2802b89e,
180				0x5f058808,
181				-0x39f3264e,
182				-0x4ef416dc,
183				0x2f6f7c87,
184				0x58684c11,
185				-0x3e9ee255,
186				-0x4999d2c3,
187				0x76dc4190,
188				0x01db7106,
189				-0x672ddf44,
190				-0x102aefd6,
191				0x71b18589,
192				0x06b6b51f,
193				-0x60401b5b,
194				-0x17472bcd,
195				0x7807c9a2,
196				0x0f00f934,
197				-0x69f65772,
198				-0x1ef167e8,
199				0x7f6a0dbb,
200				0x086d3d2d,
201				-0x6e9b9369,
202				-0x199ca3ff,
203				0x6b6b51f4,
204				0x1c6c6162,
205				-0x7a9acf28,
206				-0x0d9dffb2,
207				0x6c0695ed,
208				0x1b01a57b,
209				-0x7df70b3f,
210				-0x0af03ba9,
211				0x65b0d9c6,
212				0x12b7e950,
213				-0x74414716,
214				-0x03467784,
215				0x62dd1ddf,
216				0x15da2d49,
217				-0x732c830d,
218				-0x042bb39b,
219				0x4db26158,
220				0x3ab551ce,
221				-0x5c43ff8c,
222				-0x2b44cf1e,
223				0x4adfa541,
224				0x3dd895d7,
225				-0x5b2e3b93,
226				-0x2c290b05,
227				0x4369e96a,
228				0x346ed9fc,
229				-0x529877ba,
230				-0x259f4730,
231				0x44042d73,
232				0x33031de5,
233				-0x55f5b3a1,
234				-0x22f28337,
235				0x5005713c,
236				0x270241aa,
237				-0x41f4eff0,
238				-0x36f3df7a,
239				0x5768b525,
240				0x206f85b3,
241				-0x46992bf7,
242				-0x319e1b61,
243				0x5edef90e,
244				0x29d9c998,
245				-0x4f2f67de,
246				-0x3828574c,
247				0x59b33d17,
248				0x2eb40d81,
249				-0x4842a3c5,
250				-0x3f459353,
251				-0x12477ce0,
252				-0x65404c4a,
253				0x03b6e20c,
254				0x74b1d29a,
255				-0x152ab8c7,
256				-0x622d8851,
257				0x04db2615,
258				0x73dc1683,
259				-0x1c9cf4ee,
260				-0x6b9bc47c,
261				0x0d6d6a3e,
262				0x7a6a5aa8,
263				-0x1bf130f5,
264				-0x6cf60063,
265				0x0a00ae27,
266				0x7d079eb1,
267				-0x0ff06cbc,
268				-0x78f75c2e,
269				0x1e01f268,
270				0x6906c2fe,
271				-0x089da8a3,
272				-0x7f9a9835,
273				0x196c3671,
274				0x6e6b06e7,
275				-0x012be48a,
276				-0x762cd420,
277				0x10da7a5a,
278				0x67dd4acc,
279				-0x06462091,
280				-0x71411007,
281				0x17b7be43,
282				0x60b08ed5,
283				-0x29295c18,
284				-0x5e2e6c82,
285				0x38d8c2c4,
286				0x4fdff252,
287				-0x2e44980f,
288				-0x5943a899,
289				0x3fb506dd,
290				0x48b2364b,
291				-0x27f2d426,
292				-0x50f5e4b4,
293				0x36034af6,
294				0x41047a60,
295				-0x209f103d,
296				-0x579820ab,
297				0x316e8eef,
298				0x4669be79,
299				-0x349e4c74,
300				-0x43997ce6,
301				0x256fd2a0,
302				0x5268e236,
303				-0x33f3886b,
304				-0x44f4b8fd,
305				0x220216b9,
306				0x5505262f,
307				-0x3a45c442,
308				-0x4d42f4d8,
309				0x2bb45a92,
310				0x5cb36a04,
311				-0x3d280059,
312				-0x4a2f30cf,
313				0x2cd99e8b,
314				0x5bdeae1d,
315				-0x649b3d50,
316				-0x139c0dda,
317				0x756aa39c,
318				0x026d930a,
319				-0x63f6f957,
320				-0x14f1c9c1,
321				0x72076785,
322				0x05005713,
323				-0x6a40b57e,
324				-0x1d4785ec,
325				0x7bb12bae,
326				0x0cb61b38,
327				-0x6d2d7165,
328				-0x1a2a41f3,
329				0x7cdcefb7,
330				0x0bdbdf21,
331				-0x792c2d2c,
332				-0x0e2b1dbe,
333				0x68ddb3f8,
334				0x1fda836e,
335				-0x7e41e933,
336				-0x0946d9a5,
337				0x6fb077e1,
338				0x18b74777,
339				-0x77f7a51a,
340				-0x00f09590,
341				0x66063bca,
342				0x11010b5c,
343				-0x709a6101,
344				-0x079d5197,
345				0x616bffd3,
346				0x166ccf45,
347				-0x5ff51d88,
348				-0x28f22d12,
349				0x4e048354,
350				0x3903b3c2,
351				-0x5898d99f,
352				-0x2f9fe909,
353				0x4969474d,
354				0x3e6e77db,
355				-0x512e95b6,
356				-0x2629a524,
357				0x40df0b66,
358				0x37d83bf0,
359				-0x564351ad,
360				-0x2144613b,
361				0x47b2cf7f,
362				0x30b5ffe9,
363				-0x42420de4,
364				-0x35453d76,
365				0x53b39330,
366				0x24b4a3a6,
367				-0x452fc9fb,
368				-0x3228f96d,
369				0x54de5729,
370				0x23d967bf,
371				-0x4c9985d2,
372				-0x3b9eb548,
373				0x5d681b02,
374				0x2a6f2b94,
375				-0x4bf441c9,
376				-0x3cf3715f,
377				0x5a05df1b,
378				0x2d02ef8d
379					];
380	}
381
382	$crc = ~$crc;
383
384	for ($i = 0, $istrlen_str = strlen($str); $i < $istrlen_str; $i++) {
385		$crc = ($zip_crc_table[($crc ^ ord($str[$i])) & 0xff] ^ (($crc >> 8) & 0xffffff));
386	}
387
388	return ~$crc;
389}
390
391define('GZIP_MAGIC', "\037\213");
392define('GZIP_DEFLATE', 010);
393
394function zip_deflate($content)
395{
396	// Compress content, and suck information from gzip header.
397	$z = gzip_compress($content);
398
399	// Suck OS type byte from gzip header. FIXME: this smells bad.
400	extract(unpack("a2magic/Ccomp_type/Cflags/@9/Cos_type", $z));
401
402	if ($magic != GZIP_MAGIC) {
403		trigger_error(sprintf('Bad %s', 'gzip magic'), E_USER_ERROR);
404	}
405
406	if ($comp_type != GZIP_DEFLATE) {
407		trigger_error(sprintf('Bad %s', 'gzip comp type'), E_USER_ERROR);
408	}
409
410	if (($flags & 0x3e) != 0) {
411		trigger_error(sprintf('Bad %s', sprintf('flags (0x%02x)', $flags)), E_USER_ERROR);
412	}
413
414	$gz_header_len = 10;
415	$gz_data_len = strlen($z) - $gz_header_len - 8;
416
417	if ($gz_data_len < 0) {
418		trigger_error('not enough gzip output?', E_USER_ERROR);
419	}
420
421	extract(unpack('Vcrc32', substr($z, $gz_header_len + $gz_data_len)));
422
423	return [
424			substr($z, $gz_header_len, $gz_data_len), // gzipped data
425			$crc32,                                   // crc
426			$os_type                                  // OS type
427	];
428}
429
430function zip_inflate($data, $crc32, $uncomp_size)
431{
432	if (! function_exists('gzopen')) {
433		global $request;
434
435		$request->finish(_("Can't inflate data: zlib support not enabled in this PHP"));
436	}
437
438	// Reconstruct gzip header and ungzip the data.
439	$mtime = time(); //(Bogus mtime)
440
441	return gzip_uncompress(pack("a2CxV@10", GZIP_MAGIC, GZIP_DEFLATE, $mtime) . $data . pack('VV', $crc32, $uncomp_size));
442}
443
444function unixtime2dostime($unix_time)
445{
446	if ($unix_time % 1) {
447		$unix_time++; // Round up to even seconds.
448	}
449
450	list($year, $month, $mday, $hour, $min, $sec) = explode(' ', TikiLib::date_format('%Y %m %e %H %M %S', $unix_time));
451
452	if ($year < 1980) {
453		list($year, $month, $mday, $hour, $min, $sec) = [
454				1980,
455				1,
456				1,
457				0,
458				0,
459				0
460				];
461	}
462
463	$dosdate = (($year - 1980) << 9) | ($month << 5) | $mday;
464	$dostime = ($hour << 11) | ($min << 5) | ($sec >> 1);
465
466	return [
467			$dosdate,
468			$dostime
469			];
470}
471
472function dostime2unixtime($dosdate, $dostime)
473{
474	$mday = $dosdate & 0x1f;
475
476	$month = ($dosdate >> 5) & 0x0f;
477	$year = 1980 + (($dosdate >> 9) & 0x7f);
478
479	$sec = ($dostime & 0x1f) * 2;
480	$min = ($dostime >> 5) & 0x3f;
481	$hour = ($dostime >> 11) & 0x1f;
482
483	return TikiLib::make_time($hour, $min, $sec, $month, $mday, $year);
484}
485
486/**
487 * Class for zipfile creation.
488 */
489define('ZIP_DEFLATE', GZIP_DEFLATE);
490define('ZIP_STORE', 0);
491define('ZIP_CENTHEAD_MAGIC', "PK\001\002");
492define('ZIP_LOCHEAD_MAGIC', "PK\003\004");
493define('ZIP_ENDDIR_MAGIC', "PK\005\006");
494
495class ZipWriter
496{
497	function __construct($comment = '', $zipname = 'archive.zip')
498	{
499		$this->comment = $comment;
500
501		$this->nfiles = 0;
502		$this->dir = ''; // "Central directory block"
503		$this->offset = 0; // Current file position.
504
505		$zipname = addslashes($zipname);
506		header("Content-Type: application/zip; name=\"$zipname\"");
507		header("Content-Disposition: attachment; filename=\"$zipname\"");
508	}
509
510	function addRegularFile($filename, $content, $attrib = false)
511	{
512		if (! $attrib) {
513			$attrib = [];
514		}
515
516		$size = strlen($content);
517
518		if (function_exists('gzopen')) {
519			list($data, $crc32, $os_type) = zip_deflate($content);
520
521			if (strlen($data) < $size) {
522				$content = $data;	// Use compressed data.
523
524				$comp_type = ZIP_DEFLATE;
525			} else {
526				unset($crc32);	// force plain store.
527			}
528		}
529
530		if (! isset($crc32)) {
531			$comp_type = ZIP_STORE;
532
533			$crc32 = zip_crc32($content);
534		}
535
536		if (! empty($attrib['write_protected'])) {
537			$atx = (0100444 << 16) | 1; // S_IFREG + read permissions to
538		} // everybody.
539		else {
540			$atx = (0100644 << 16); // Add owner write perms.
541		}
542
543		$ati = $attrib['is_ascii'] ? 1 : 0;
544
545		if (empty($attrib['mtime'])) {
546			$attrib['mtime'] = time();
547		}
548
549		list($mod_date, $mod_time) = unixtime2dostime($attrib['mtime']);
550
551		// Construct parts common to "Local file header" and "Central
552		// directory file header."
553		if (! isset($attrib['extra_field'])) {
554			$attrib['extra_field'] = '';
555		}
556
557		if (! isset($attrib['file_comment'])) {
558			$attrib['file_comment'] = '';
559		}
560
561		$head = pack(
562			"vvvvvVVVvv",
563			20, // Version needed to extract (FIXME: is this right?)
564			0, // Gen purp bit flag
565			$comp_type,
566			$mod_time,
567			$mod_date,
568			$crc32,
569			strlen($content),
570			$size,
571			strlen($filename),
572			strlen($attrib['extra_field'])
573		);
574
575		// Construct the "Local file header"
576		$lheader = ZIP_LOCHEAD_MAGIC . $head . $filename . $attrib['extra_field'];
577
578		// Construct the "central directory file header"
579		$this->dir .= pack("a4CC", ZIP_CENTHEAD_MAGIC, 23, $os_type);
580		$this->dir .= $head;
581		$this->dir .= pack(
582			"vvvVV",
583			strlen($attrib['file_comment']),
584			0, // Disk number start
585			$ati, // Internal file attributes
586			$atx, // External file attributes
587			$this->offset // Relative offset of local header
588		);
589		$this->dir .= $filename . $attrib['extra_field'] . $attrib['file_comment'];
590
591		// Output the "Local file header" and file contents.
592		echo $lheader;
593		echo $content;
594
595		$this->offset += strlen($lheader) + strlen($content);
596		$this->nfiles++;
597	}
598
599	function finish()
600	{
601		// Output the central directory
602		echo $this->dir;
603
604		// Construct the "End of central directory record"
605		echo ZIP_ENDDIR_MAGIC;
606		echo pack(
607			"vvvvVVv",
608			0, // Number of this disk.
609			0, // Number of disk with start of c dir
610			$this->nfiles, // Number entries on this disk
611			$this->nfiles, // Number entries
612			strlen($this->dir), // Size of central directory
613			$this->offset, // Offset of central directory
614			strlen($this->comment)
615		);
616		echo $this->comment;
617	}
618}
619
620/**
621 * Class for reading zip files.
622 *
623 * BUGS:
624 *
625 * Many of the ExitWiki()'s should probably be warn()'s (eg. CRC mismatch).
626 *
627 * Only a subset of zip formats is recognized. (I think that
628 * unsupported formats will be recognized as such rather than silently
629 * munged.)
630 *
631 * We don't read the central directory. This means we don't see the
632 * file attributes (text? read-only?), or file comments.
633 *
634 * Right now we ignore the file mod date and time, since we don't need it.
635 */
636class ZipReader
637{
638	function __construct($zipfile)
639	{
640		if (! is_string($zipfile)) {
641			$this->fp = $zipfile; // File already open
642		} elseif (! ($this->fp = fopen($zipfile, "rb"))) {
643			trigger_error(sprintf(_("Can't open zip file '%s' for reading"), $zipfile), E_USER_ERROR);
644		}
645	}
646
647	function _read($nbytes)
648	{
649		$chunk = fread($this->fp, $nbytes);
650
651		if (strlen($chunk) != $nbytes) {
652			trigger_error(_("Unexpected EOF in zip file"), E_USER_ERROR);
653		}
654
655		return $chunk;
656	}
657
658	function done()
659	{
660		fclose($this->fp);
661
662		return false;
663	}
664
665	function readFile()
666	{
667		$head = $this->_read(30);
668
669		extract(
670			unpack(
671				'a4magic/vreq_version/vflags/vcomp_type'
672				. '/vmod_time/vmod_date'
673				. '/Vcrc32/Vcomp_size/Vuncomp_size'
674				. '/vfilename_len/vextrafld_len',
675				$head
676			)
677		);
678
679		//FIXME: we should probably check $req_version.
680		$attrib['mtime'] = dostime2unixtime($mod_date, $mod_time);
681
682		if ($magic != ZIP_LOCHEAD_MAGIC) {
683			if ($magic != ZIP_CENTHEAD_MAGIC) {
684				// FIXME: better message?
685				ExitWiki(sprintf('Bad header type: %s', $magic));
686			}
687
688			return $this->done();
689		}
690
691		if (($flags & 0x21) != 0) {
692			ExitWiki('Encryption and/or zip patches not supported.');
693		}
694
695		if (($flags & 0x08) != 0) {
696			// FIXME: better message?
697			ExitWiki('Postponed CRC not yet supported.');
698		}
699
700		$filename = $this->_read($filename_len);
701
702		if ($extrafld_len != 0) {
703			$attrib['extra_field'] = $this->_read($extrafld_len);
704		}
705
706		$data = $this->_read($comp_size);
707
708		if ($comp_type == ZIP_DEFLATE) {
709			$data = zip_inflate($data, $crc32, $uncomp_size);
710		} elseif ($comp_type == ZIP_STORE) {
711			$crc = zip_crc32($data);
712
713			if ($crc32 != $crc) {
714				ExitWiki(sprintf('CRC mismatch %x != %x', $crc, $crc32));
715			}
716		} else {
717			ExitWiki(sprintf('Compression method %s unsupported', $comp_method));
718		}
719
720		if (strlen($data) != $uncomp_size) {
721			ExitWiki(sprintf('Uncompressed size mismatch %d != %d', strlen($data), $uncomp_size));
722		}
723
724		return [
725				$filename,
726				$data,
727				$attrib
728				];
729	}
730}
731
732/**
733 * Routines for Mime mailification of pages.
734 */
735//FIXME: these should go elsewhere (libmime?).
736
737/**
738 * Routines for quoted-printable en/decoding.
739 */
740function QuotedPrintableEncode($string)
741{
742	// Quote special characters in line.
743	$quoted = '';
744
745	while ($string) {
746		// The complicated regexp is to force quoting of trailing spaces.
747		preg_match('/^([ !-<>-~]*)(?:([!-<>-~]$)|(.))/s', $string, $match);
748
749		$quoted .= $match[1] . $match[2];
750
751		if (! empty($match[3])) {
752			$quoted .= sprintf('=%02X', ord($match[3]));
753		}
754
755		$string = substr($string, strlen($match[0]));
756	}
757
758	// Split line.
759	// This splits the line (preferably after white-space) into lines
760	// which are no longer than 76 chars (after adding trailing '=' for
761	// soft line break, but before adding \r\n.)
762	return preg_replace('/(?=.{77})(.{10,74}[ \t]|.{71,73}[^=][^=])/s', "\\1=\r\n", $quoted);
763}
764
765function QuotedPrintableDecode($string)
766{
767	// Eliminate soft line-breaks.
768	$string = preg_replace('/=[ \t\r]*\n/', '', $string);
769
770	return quoted_printable_decode($string);
771}
772
773define('MIME_TOKEN_REGEXP', "[-!#-'*+.0-9A-Z^-~]+");
774
775function MimeContentTypeHeader($type, $subtype, $params)
776{
777	$header = "Content-Type: $type/$subtype";
778
779	reset($params);
780
781	foreach ($params as $key => $val) {
782		//FIXME: what about non-ascii printables in $val?
783		if (! preg_match('/^' . MIME_TOKEN_REGEXP . '$/', $val)) {
784			$val = '"' . addslashes($val) . '"';
785		}
786
787		$header .= ";\r\n  $key=$val";
788	}
789
790	return "$header\r\n";
791}
792
793function MimeMultipart($parts)
794{
795	global $mime_multipart_count;
796
797	// The string "=_" can not occur in quoted-printable encoded data.
798	$boundary = "=_multipart_boundary_" . ++$mime_multipart_count;
799
800	$head = MimeContentTypeHeader('multipart', 'mixed', ['boundary' => $boundary]);
801
802	$sep = "\r\n--$boundary\r\n";
803
804	return $head . $sep . implode($sep, $parts) . "\r\n--${boundary}--\r\n";
805}
806
807/**
808 * For reference see:
809 * http://www.nacs.uci.edu/indiv/ehood/MIME/2045/rfc2045.html
810 * http://www.faqs.org/rfcs/rfc2045.html
811 * (RFC 1521 has been superceeded by RFC 2045 & others).
812 *
813 * Also see http://www.faqs.org/rfcs/rfc2822.html
814 *
815 *
816 * Notes on content-transfer-encoding.
817 *
818 * "7bit" means short lines of US-ASCII.
819 * "8bit" means short lines of octets with (possibly) the high-order bit set.
820 * "binary" means lines are not necessarily short enough for SMTP
821 * transport, and non-ASCII characters may be present.
822 *
823 * Only "7bit", "quoted-printable", and "base64" are universally safe
824 * for transport via e-mail. (Though many MTAs can/will be configured to
825 * automatically convert encodings to a safe type if they receive
826 * mail encoded in '8bit' and/or 'binary' encodings.
827 */
828function MimeifyPageRevision($page)
829{
830	//$page = $revision->getPage();
831	// FIXME: add 'hits' to $params
832	$params = [
833			'pagename' => $page['pageName'],
834			'flags' => '',
835			'author' => $page['user'],
836			'version' => $page['version'],
837			'lastmodified' => $page['lastModif']
838			];
839
840	$params['author_id'] = $page['ip'];
841	$params['summary'] = $page['comment'];
842
843	if (isset($page['hits'])) {
844		$params['hits'] = $page['hits'];
845	}
846
847	$params['description'] = $page['description'];
848
849	$params['charset'] = 'utf-8';
850
851	// Non-US-ASCII is not allowed in Mime headers (at least not without
852	// special handling) --- so we urlencode all parameter values.
853	foreach ($params as $key => $val) {
854		$params[$key] = rawurlencode($val);
855	}
856
857	$out = MimeContentTypeHeader('application', 'x-tikiwiki', $params);
858	$out .= sprintf("Content-Transfer-Encoding: %s\r\n", 'binary');
859	$out .= "\r\n";
860	$lines = explode("\n", $page["data"]);
861
862	foreach ($lines as $line) {
863		// This is a dirty hack to allow saving binary text files. See above.
864		$line = rtrim($line);
865
866		$out .= "$line\r\n";
867	}
868
869	return $out;
870}
871
872/**
873 * Routines for parsing Mime-ified phpwiki pages.
874 */
875function ParseRFC822Headers(&$string)
876{
877	if (preg_match("/^From (.*)\r?\n/", $string, $match)) {
878		$headers['from '] = preg_replace('/^\s+|\s+$/', '', $match[1]);
879
880		$string = substr($string, strlen($match[0]));
881	}
882
883	while (preg_match('/^([!-9;-~]+) [ \t]* : [ \t]* ' . '( .* \r?\n (?: [ \t] .* \r?\n)* )/x', $string, $match)) {
884		$headers[strtolower($match[1])] = preg_replace('/^\s+|\s+$/', '', $match[2]);
885
886		$string = substr($string, strlen($match[0]));
887	}
888
889	if (empty($headers)) {
890		return false;
891	}
892
893	if (! preg_match("/^\r?\n/", $string, $match)) {
894		// No blank line after headers.
895		return false;
896	}
897
898	$string = substr($string, strlen($match[0]));
899
900	return $headers;
901}
902
903function ParseMimeContentType($string)
904{
905	// FIXME: Remove (RFC822 style comments).
906
907	// Get type/subtype
908	if (! preg_match(':^\s*(' . MIME_TOKEN_REGEXP . ')\s*' . '/' . '\s*(' . MIME_TOKEN_REGEXP . ')\s*:x', $string, $match)) {
909		ExitWiki(sprintf("Bad %s", 'MIME content-type'));
910	}
911
912	$type = strtolower($match[1]);
913	$subtype = strtolower($match[2]);
914	$string = substr($string, strlen($match[0]));
915
916	$param = [];
917
918	while (preg_match('/^;\s*(' . MIME_TOKEN_REGEXP . ')\s*=\s*' . '(?:(' . MIME_TOKEN_REGEXP . ')|"((?:[^"\\\\]|\\.)*)") \s*/sx', $string, $match)) {
919		//" <--kludge for brain-dead syntax coloring
920		if (strlen($match[2])) {
921			$val = $match[2];
922		} else {
923			$val = preg_replace('/[\\\\](.)/s', '\\1', $match[3]);
924		}
925
926		$param[strtolower($match[1])] = $val;
927
928		$string = substr($string, strlen($match[0]));
929	}
930
931	return [$type, $subtype,$param];
932}
933
934function ParseMimeMultipart($data, $boundary)
935{
936	if (! $boundary) {
937		ExitWiki('No boundary?');
938	}
939
940	$boundary = preg_quote($boundary);
941
942	while (preg_match("/^(|.*?\n)--$boundary((?:--)?)[^\n]*\n/s", $data, $match)) {
943		$data = substr($data, strlen($match[0]));
944
945		if (! isset($parts)) {
946			$parts = []; // First time through: discard leading chaff
947		} else {
948			if ($content = ParseMimeifiedPages($match[1])) {
949				foreach ($content as $p) {
950					$parts[] = $p;
951				}
952			}
953		}
954
955		if ($match[2]) {
956			return $parts; // End boundary found.
957		}
958	}
959
960	ExitWiki('No end boundary?');
961}
962
963function GenerateFootnotesFromRefs($params)
964{
965	$footnotes = [];
966
967	reset($params);
968
969	foreach ($params as $p => $reference) {
970		if (preg_match('/^ref([1-9][0-9]*)$/', $p, $m)) {
971			$footnotes[$m[1]] = sprintf(_("[%d] See [%s]"), $m[1], rawurldecode($reference));
972		}
973	}
974
975	if (count($footnotes) > 0) {
976		ksort($footnotes);
977
978		return "-----\n" . "!" . _("References") . "\n" . join("\n%%%\n", $footnotes) . "\n";
979	} else {
980		return '';
981	}
982}
983
984// Convert references in meta-data to footnotes.
985// Only zip archives generated by phpwiki 1.2.x or earlier should have
986// references.
987function ParseMimeifiedPages($data)
988{
989	if (! ($headers = ParseRFC822Headers($data)) || empty($headers['content-type'])) {
990		//trigger_error( sprintf(_("Can't find %s"),'content-type header'),
991		//  E_USER_WARNING );
992		return false;
993	}
994
995	$typeheader = $headers['content-type'];
996
997	if (! (list($type, $subtype, $params) = ParseMimeContentType($typeheader))) {
998		trigger_error(sprintf("Can't parse %s: (%s)", 'content-type', $typeheader), E_USER_WARNING);
999
1000		return false;
1001	}
1002
1003	if ("$type/$subtype" == 'multipart/mixed') {
1004		return ParseMimeMultipart($data, $params['boundary']);
1005	} elseif ("$type/$subtype" != 'application/x-phpwiki') {
1006		trigger_error(sprintf("Bad %s", "content-type: $type/$subtype"), E_USER_WARNING);
1007
1008		return false;
1009	}
1010
1011	// FIXME: more sanity checking?
1012	$page = [];
1013	$pagedata = [];
1014	$versiondata = [];
1015
1016	foreach ($params as $key => $value) {
1017		if (empty($value)) {
1018			continue;
1019		}
1020
1021		$value = rawurldecode($value);
1022
1023		switch ($key) {
1024			case 'pagename':
1025			case 'version':
1026				$page[$key] = $value;
1027				break;
1028
1029			case 'flags':
1030				if (preg_match('/PAGE_LOCKED/', $value)) {
1031					$pagedata['locked'] = 'yes';
1032				}
1033				break;
1034
1035			case 'created':
1036			case 'hits':
1037				$pagedata[$key] = $value;
1038				break;
1039
1040			case 'lastmodified':
1041				$versiondata['mtime'] = $value;
1042				break;
1043
1044			case 'author':
1045			case 'author_id':
1046			case 'summary':
1047			case 'markup':
1048				$versiondata[$key] = $value;
1049				break;
1050		}
1051	}
1052
1053	// FIXME: do we need to try harder to find a pagename if we
1054	// haven't got one yet?
1055	if (! isset($versiondata['author'])) {
1056		global $request;
1057
1058		$user = $request->getUser();
1059		$versiondata['author'] = $user->getId(); //FIXME:?
1060	}
1061
1062	$encoding = strtolower($headers['content-transfer-encoding']);
1063
1064	if ($encoding == 'quoted-printable') {
1065		$data = QuotedPrintableDecode($data);
1066	} elseif ($encoding && $encoding != 'binary') {
1067		ExitWiki(sprintf("Unknown %s", 'encoding type: $encoding'));
1068	}
1069
1070	$data .= GenerateFootnotesFromRefs($params);
1071
1072	$page['content'] = preg_replace('/[ \t\r]*\n/', "\n", chop($data));
1073	$page['pagedata'] = $pagedata;
1074	$page['versiondata'] = $versiondata;
1075
1076	return [$page];
1077}
1078// Local Variables:
1079// mode: php
1080// tab-width: 8
1081// c-basic-offset: 4
1082// c-hanging-comment-ender-p: nil
1083// indent-tabs-mode: nil
1084// End:
1085