1<?php
2/**
3 * Coppermine Photo Gallery
4 *
5 * v1.0 originally written by Gregory Demar
6 *
7 * @copyright  Copyright (c) 2003-2020 Coppermine Dev Team
8 * @license	   GNU General Public License version 3 or later; see LICENSE
9 *
10 * include/archive.php
11 * @since  1.6.09
12 */
13
14/*--------------------------------------------------
15 | TAR/GZIP/BZIP2/ZIP ARCHIVE CLASSES 2.1
16 | By Devin Doucette
17 | Copyright (c) 2005 Devin Doucette
18 | Email: darksnoopy@shaw.ca
19 +--------------------------------------------------
20 | Email bugs/suggestions to darksnoopy@shaw.ca
21 +--------------------------------------------------
22 | This script has been created and released under
23 | the GNU GPL and is free to use and redistribute
24 | only if this copyright statement is not removed
25 +--------------------------------------------------*/
26
27class archive
28{
29	function __construct($name)
30	{
31		$this->options = array (
32			'basedir' => '.',
33			'name' => $name,
34			'prepend' => '',
35			'inmemory' => 0,
36			'overwrite' => 0,
37			'recurse' => 1,
38			'storepaths' => 1,
39			'followlinks' => 0,
40			'level' => 3,
41			'method' => 1,
42			'sfx' => '',
43			'type' => '',
44			'comment' => ''
45		);
46		$this->files = array ();
47		$this->exclude = array ();
48		$this->storeonly = array ();
49		$this->error = array ();
50	}
51
52	function set_options($options)
53	{
54		foreach ($options as $key => $value)
55			$this->options[$key] = $value;
56		if (!empty ($this->options['basedir']))
57		{
58			$this->options['basedir'] = str_replace('\\', '/', $this->options['basedir']);
59			$this->options['basedir'] = preg_replace('/\/+/', '/', $this->options['basedir']);
60			$this->options['basedir'] = preg_replace('/\/$/', '', $this->options['basedir']);
61		}
62		if (!empty ($this->options['name']))
63		{
64			$this->options['name'] = str_replace('\\', '/', $this->options['name']);
65			$this->options['name'] = preg_replace('/\/+/', '/', $this->options['name']);
66		}
67		if (!empty ($this->options['prepend']))
68		{
69			$this->options['prepend'] = str_replace('\\', '/', $this->options['prepend']);
70			$this->options['prepend'] = preg_replace('/^(\.*\/+)+/', '', $this->options['prepend']);
71			$this->options['prepend'] = preg_replace('/\/+/', '/', $this->options['prepend']);
72			$this->options['prepend'] = preg_replace('/\/$/', '', $this->options['prepend']) . '/';
73		}
74	}
75
76	function create_archive()
77	{
78		$this->make_list();
79
80		if ($this->options['inmemory'] == 0)
81		{
82			$pwd = getcwd();
83			chdir($this->options['basedir']);
84			if ($this->options['overwrite'] == 0 && file_exists($this->options['name'] . ($this->options['type'] == 'gzip' || $this->options['type'] == 'bzip' ? '.tmp' : '')))
85			{
86				$this->error[] = "File {$this->options['name']} already exists.";
87				chdir($pwd);
88				return 0;
89			}
90			else if ($this->archive = @fopen($this->options['name'] . ($this->options['type'] == 'gzip' || $this->options['type'] == 'bzip' ? '.tmp' : ''), 'wb+'))
91				chdir($pwd);
92			else
93			{
94				$this->error[] = "Could not open {$this->options['name']} for writing.";
95				chdir($pwd);
96				return 0;
97			}
98		}
99		else
100			$this->archive = '';
101
102		switch ($this->options['type'])
103		{
104			case 'zip':
105				if (!$this->create_zip())
106				{
107					$this->error[] = 'Could not create zip file.';
108					return 0;
109				}
110				break;
111			case 'bzip':
112				if (!$this->create_tar())
113				{
114					$this->error[] = 'Could not create tar file.';
115					return 0;
116				}
117				if (!$this->create_bzip())
118				{
119					$this->error[] = 'Could not create bzip2 file.';
120					return 0;
121				}
122				break;
123			case 'gzip':
124				if (!$this->create_tar())
125				{
126					$this->error[] = 'Could not create tar file.';
127					return 0;
128				}
129				if (!$this->create_gzip())
130				{
131					$this->error[] = 'Could not create gzip file.';
132					return 0;
133				}
134				break;
135			case 'tar':
136				if (!$this->create_tar())
137				{
138					$this->error[] = 'Could not create tar file.';
139					return 0;
140				}
141		}
142
143		if ($this->options['inmemory'] == 0)
144		{
145			fclose($this->archive);
146			if ($this->options['type'] == 'gzip' || $this->options['type'] == 'bzip')
147				unlink($this->options['basedir'] . '/' . $this->options['name'] . '.tmp');
148		}
149	}
150
151	function add_data($data)
152	{
153		if ($this->options['inmemory'] == 0)
154			fwrite($this->archive, $data);
155		else
156			$this->archive .= $data;
157	}
158
159	function make_list()
160	{
161		if (!empty ($this->exclude))
162			foreach ($this->files as $key => $value)
163				foreach ($this->exclude as $current)
164					if ($value['name'] == $current['name'])
165						unset ($this->files[$key]);
166		if (!empty ($this->storeonly))
167			foreach ($this->files as $key => $value)
168				foreach ($this->storeonly as $current)
169					if ($value['name'] == $current['name'])
170						$this->files[$key]['method'] = 0;
171		unset ($this->exclude, $this->storeonly);
172	}
173
174	function add_files($list)
175	{
176		$temp = $this->list_files($list);
177		foreach ($temp as $current)
178			$this->files[] = $current;
179	}
180
181	function exclude_files($list)
182	{
183		$temp = $this->list_files($list);
184		foreach ($temp as $current)
185			$this->exclude[] = $current;
186	}
187
188	function store_files($list)
189	{
190		$temp = $this->list_files($list);
191		foreach ($temp as $current)
192			$this->storeonly[] = $current;
193	}
194
195	function list_files($list)
196	{
197		if (!is_array ($list))
198		{
199			$temp = $list;
200			$list = array ($temp);
201			unset ($temp);
202		}
203
204		$files = array ();
205
206		$pwd = getcwd();
207		chdir($this->options['basedir']);
208
209		foreach ($list as $current)
210		{
211			$current = str_replace('\\', '/', $current);
212			$current = preg_replace('/\/+/', '/', $current);
213			$current = preg_replace('/\/$/', '', $current);
214			if (strstr($current, '*'))
215			{
216				$regex = preg_replace('/([\\\^\$\.\[\]\|\(\)\?\+\{\}\/])/', '\\\\\\1', $current);
217				$regex = str_replace('*', '.*', $regex);
218				$dir = strstr($current, '/') ? substr($current, 0, strrpos($current, '/')) : '.';
219				$temp = $this->parse_dir($dir);
220				foreach ($temp as $current2)
221					if (preg_match("/^{$regex}$/i", $current2['name']))
222						$files[] = $current2;
223				unset ($regex, $dir, $temp, $current);
224			}
225			else if (@is_dir($current))
226			{
227				$temp = $this->parse_dir($current);
228				foreach ($temp as $file)
229					$files[] = $file;
230				unset ($temp, $file);
231			}
232			else if (@file_exists($current))
233				$files[] = array ('name' => $current, 'name2' => $this->options['prepend'] .
234					preg_replace('/(\.+\/+)+/', '', ($this->options['storepaths'] == 0 && strstr($current, '/')) ?
235					substr($current, strrpos($current, '/') + 1) : $current),
236					'type' => @is_link($current) && $this->options['followlinks'] == 0 ? 2 : 0,
237					'ext' => substr($current, strrpos($current, '.')), 'stat' => stat($current));
238		}
239
240		chdir($pwd);
241
242		unset ($current, $pwd);
243
244		usort($files, array ($this, 'sort_files'));
245
246		return $files;
247	}
248
249	function parse_dir($dirname)
250	{
251		if ($this->options['storepaths'] == 1 && !preg_match('/^(\.+\/*)+$/', $dirname))
252			$files = array (array ('name' => $dirname, 'name2' => $this->options['prepend'] .
253			preg_replace('/(\.+\/+)+/', '', ($this->options['storepaths'] == 0 && strstr($dirname, '/')) ?
254			substr($dirname, strrpos($dirname, '/') + 1) : $dirname), 'type' => 5, 'stat' => stat($dirname)));
255		else
256			$files = array ();
257		$dir = @opendir($dirname);
258
259		while ($file = @readdir($dir))
260		{
261			$fullname = $dirname . '/' . $file;
262			if ($file == '.' || $file == '..')
263				continue;
264			else if (@is_dir($fullname))
265			{
266			if (empty ($this->options['recurse']))
267				continue;
268			$temp = $this->parse_dir($fullname);
269			foreach ($temp as $file2)
270				$files[] = $file2;
271			}
272			else if (@file_exists($fullname))
273				$files[] = array ('name' => $fullname, 'name2' => $this->options['prepend'] .
274					preg_replace('/(\.+\/+)+/', '', ($this->options['storepaths'] == 0 && strstr($fullname, '/')) ?
275					substr($fullname, strrpos($fullname, '/') + 1) : $fullname),
276					'type' => @is_link($fullname) && $this->options['followlinks'] == 0 ? 2 : 0,
277					'ext' => substr($file, strrpos($file, '.')), 'stat' => stat($fullname));
278		}
279
280		@closedir($dir);
281
282		return $files;
283	}
284
285	function sort_files($a, $b)
286	{
287		if ($a['type'] != $b['type'])
288			if ($a['type'] == 5 || $b['type'] == 2)
289				return -1;
290			else if ($a['type'] == 2 || $b['type'] == 5)
291				return 1;
292		else if ($a['type'] == 5)
293			return strcmp(strtolower($a['name']), strtolower($b['name']));
294		else if ($a['ext'] != $b['ext'])
295			return strcmp($a['ext'], $b['ext']);
296		else if ($a['stat'][7] != $b['stat'][7])
297			return $a['stat'][7] > $b['stat'][7] ? -1 : 1;
298		else
299			return strcmp(strtolower($a['name']), strtolower($b['name']));
300		return 0;
301	}
302
303	function download_file()
304	{
305		if ($this->options['inmemory'] == 0)
306		{
307			$this->error[] = 'Can only use download_file() if archive is in memory. Redirect to file otherwise, it is faster.';
308			return;
309		}
310		switch ($this->options['type'])
311		{
312			case 'zip':
313				header('Content-Type: application/zip');
314				break;
315			case 'bzip':
316				header('Content-Type: application/x-bzip2');
317				break;
318			case 'gzip':
319				header('Content-Type: application/x-gzip');
320				break;
321			case 'tar':
322				header('Content-Type: application/x-tar');
323		}
324		$header = 'Content-Disposition: attachment; filename="';
325		$header .= strstr($this->options['name'], '/') ? substr($this->options['name'], strrpos($this->options['name'], '/') + 1) : $this->options['name'];
326		$header .= '"';
327		header($header);
328		header('Content-Length: ' . strlen($this->archive));
329		header('Content-Transfer-Encoding: binary');
330		header('Cache-Control: no-cache, must-revalidate, max-age=60');
331		header('Expires: Sat, 01 Jan 2000 12:00:00 GMT');
332		print($this->archive);
333	}
334}
335
336class tar_file extends archive
337{
338	function __construct($name)
339	{
340		parent::__construct($name);
341		$this->options['type'] = 'tar';
342	}
343
344	function create_tar()
345	{
346		$pwd = getcwd();
347		chdir($this->options['basedir']);
348
349		foreach ($this->files as $current)
350		{
351			if ($current['name'] == $this->options['name'])
352				continue;
353			if (strlen($current['name2']) > 99)
354			{
355				$path = substr($current['name2'], 0, strpos($current['name2'], '/', strlen($current['name2']) - 100) + 1);
356				$current['name2'] = substr($current['name2'], strlen($path));
357				if (strlen($path) > 154 || strlen($current['name2']) > 99)
358				{
359					$this->error[] = "Could not add {$path}{$current['name2']} to archive because the filename is too long.";
360					continue;
361				}
362			}
363			$block = pack('a100a8a8a8a12a12a8a1a100a6a2a32a32a8a8a155a12', $current['name2'], sprintf('%07o',
364			$current['stat'][2]), sprintf('%07o', $current['stat'][4]), sprintf('%07o', $current['stat'][5]),
365			sprintf('%011o', $current['type'] == 2 ? 0 : $current['stat'][7]), sprintf('%011o', $current['stat'][9]),
366			'         ', $current['type'], $current['type'] == 2 ? @readlink($current['name']) : '', 'ustar ', ' ',
367			'Unknown', 'Unknown', '', '', !empty ($path) ? $path : '', '');
368
369			$checksum = 0;
370			for ($i = 0; $i < 512; $i++)
371			$checksum += ord(substr($block, $i, 1));
372			$checksum = pack('a8', sprintf('%07o', $checksum));
373			$block = substr_replace($block, $checksum, 148, 8);
374
375			if ($current['type'] == 2 || $current['stat'][7] == 0)
376				$this->add_data($block);
377			else if ($fp = @fopen($current['name'], 'rb'))
378			{
379				$this->add_data($block);
380				while ($temp = fread($fp, 1048576))
381					$this->add_data($temp);
382				if ($current['stat'][7] % 512 > 0)
383				{
384					$temp = '';
385					for ($i = 0; $i < 512 - $current['stat'][7] % 512; $i++)
386						$temp .= "\0";
387					$this->add_data($temp);
388				}
389				fclose($fp);
390			}
391			else
392				$this->error[] = "Could not open file {$current['name']} for reading. It was not added.";
393		}
394
395		$this->add_data(pack('a1024', ''));
396
397		chdir($pwd);
398
399		return 1;
400	}
401
402	function extract_files()
403	{
404		$pwd = getcwd();
405		chdir($this->options['basedir']);
406
407		if ($fp = $this->open_archive())
408		{
409			if ($this->options['inmemory'] == 1)
410				$this->files = array ();
411
412			while ($block = fread($fp, 512))
413			{
414				$temp = unpack('a100name/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1type/a100symlink/a6magic/a2temp/a32temp/a32temp/a8temp/a8temp/a155prefix/a12temp', $block);
415				$file = array (
416					'name' => $temp['prefix'] . $temp['name'],
417					'stat' => array (
418						2 => $temp['mode'],
419						4 => octdec($temp['uid']),
420						5 => octdec($temp['gid']),
421						7 => octdec($temp['size']),
422						9 => octdec($temp['mtime']),
423						),
424					'checksum' => octdec($temp['checksum']),
425					'type' => $temp['type'],
426					'magic' => $temp['magic'],
427				);
428				if ($file['checksum'] == 0x00000000)
429					break;
430				else if (substr($file['magic'], 0, 5) != 'ustar')
431				{
432					$this->error[] = 'This script does not support extracting this type of tar file.';
433					break;
434				}
435				$block = substr_replace($block, '         ', 148, 8);
436				$checksum = 0;
437				for ($i = 0; $i < 512; $i++)
438					$checksum += ord(substr($block, $i, 1));
439				if ($file['checksum'] != $checksum)
440					$this->error[] = "Could not extract from {$this->options['name']}, it is corrupt.";
441
442				if ($this->options['inmemory'] == 1)
443				{
444					$file['data'] = fread($fp, $file['stat'][7]);
445					fread($fp, (512 - $file['stat'][7] % 512) == 512 ? 0 : (512 - $file['stat'][7] % 512));
446					unset ($file['checksum'], $file['magic']);
447					$this->files[] = $file;
448				}
449				else if ($file['type'] == 5)
450				{
451					if (!is_dir($file['name']))
452					mkdir($file['name'], $file['stat'][2]);
453				}
454				else if ($this->options['overwrite'] == 0 && file_exists($file['name']))
455				{
456					$this->error[] = "{$file['name']} already exists.";
457					continue;
458				}
459				else if ($file['type'] == 2)
460				{
461					symlink($temp['symlink'], $file['name']);
462					chmod($file['name'], $file['stat'][2]);
463				}
464				else if ($new = @fopen($file['name'], 'wb'))
465				{
466					fwrite($new, fread($fp, $file['stat'][7]));
467					fread($fp, (512 - $file['stat'][7] % 512) == 512 ? 0 : (512 - $file['stat'][7] % 512));
468					fclose($new);
469					chmod($file['name'], $file['stat'][2]);
470				}
471				else
472				{
473					$this->error[] = "Could not open {$file['name']} for writing.";
474					continue;
475				}
476				chown($file['name'], $file['stat'][4]);
477				chgrp($file['name'], $file['stat'][5]);
478				touch($file['name'], $file['stat'][9]);
479				unset ($file);
480			}
481		}
482		else
483			$this->error[] = "Could not open file {$this->options['name']}";
484
485		chdir($pwd);
486	}
487
488	function open_archive()
489	{
490	return @fopen($this->options['name'], 'rb');
491	}
492}
493
494class gzip_file extends tar_file
495{
496	function __construct($name)
497	{
498		parent::__construct($name);
499		$this->options['type'] = 'gzip';
500	}
501
502	function create_gzip()
503	{
504		if ($this->options['inmemory'] == 0)
505		{
506			$pwd = getcwd();
507			chdir($this->options['basedir']);
508			if ($fp = gzopen($this->options['name'], "wb{$this->options['level']}"))
509			{
510				fseek($this->archive, 0);
511				while ($temp = fread($this->archive, 1048576))
512					gzwrite($fp, $temp);
513				gzclose($fp);
514				chdir($pwd);
515			}
516			else
517			{
518				$this->error[] = "Could not open {$this->options['name']} for writing.";
519				chdir($pwd);
520				return 0;
521			}
522		}
523		else
524			$this->archive = gzencode($this->archive, $this->options['level']);
525
526		return 1;
527	}
528
529	function open_archive()
530	{
531		return @gzopen($this->options['name'], 'rb');
532	}
533}
534
535class bzip_file extends tar_file
536{
537	function __construct($name)
538	{
539		parent::__construct($name);
540		$this->options['type'] = 'bzip';
541	}
542
543	function create_bzip()
544	{
545		if ($this->options['inmemory'] == 0)
546		{
547			$pwd = getcwd();
548			chdir($this->options['basedir']);
549			if ($fp = bzopen($this->options['name'], 'wb'))
550			{
551				fseek($this->archive, 0);
552				while ($temp = fread($this->archive, 1048576))
553					bzwrite($fp, $temp);
554				bzclose($fp);
555				chdir($pwd);
556			}
557			else
558			{
559				$this->error[] = "Could not open {$this->options['name']} for writing.";
560				chdir($pwd);
561				return 0;
562			}
563		}
564		else
565			$this->archive = bzcompress($this->archive, $this->options['level']);
566
567		return 1;
568	}
569
570	function open_archive()
571	{
572		return @bzopen($this->options['name'], 'rb');
573	}
574}
575
576class zip_file extends archive
577{
578	function __construct($name)
579	{
580		parent::__construct($name);
581		$this->options['type'] = 'zip';
582	}
583
584	function create_zip()
585	{
586		$files = 0;
587		$offset = 0;
588		$central = '';
589
590		if (!empty ($this->options['sfx']))
591			if ($fp = @fopen($this->options['sfx'], 'rb'))
592			{
593				$temp = fread($fp, filesize($this->options['sfx']));
594				fclose($fp);
595				$this->add_data($temp);
596				$offset += strlen($temp);
597				unset ($temp);
598			}
599			else
600				$this->error[] = "Could not open sfx module from {$this->options['sfx']}.";
601
602		$pwd = getcwd();
603		chdir($this->options['basedir']);
604
605		foreach ($this->files as $current)
606		{
607			if ($current['name'] == $this->options['name'])
608				continue;
609
610			$timedate = explode(' ', date('Y n j G i s', $current['stat'][9]));
611			$timedate = ($timedate[0] - 1980 << 25) | ($timedate[1] << 21) | ($timedate[2] << 16) |
612			($timedate[3] << 11) | ($timedate[4] << 5) | ($timedate[5]);
613
614			$block = pack('VvvvV', 0x04034b50, 0x000A, 0x0000, (isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate);
615
616			if ($current['stat'][7] == 0 && $current['type'] == 5)
617			{
618				$block .= pack('VVVvv', 0x00000000, 0x00000000, 0x00000000, strlen($current['name2']) + 1, 0x0000);
619				$block .= $current['name2'] . '/';
620				$this->add_data($block);
621				$central .= pack('VvvvvVVVVvvvvvVV', 0x02014b50, 0x0014, $this->options['method'] == 0 ? 0x0000 : 0x000A, 0x0000,
622					(isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate,
623					0x00000000, 0x00000000, 0x00000000, strlen($current['name2']) + 1, 0x0000, 0x0000, 0x0000, 0x0000, $current['type'] == 5 ? 0x00000010 : 0x00000000, $offset);
624				$central .= $current['name2'] . '/';
625				$files++;
626				$offset += (31 + strlen($current['name2']));
627			}
628			else if ($current['stat'][7] == 0)
629			{
630				$block .= pack('VVVvv', 0x00000000, 0x00000000, 0x00000000, strlen($current['name2']), 0x0000);
631				$block .= $current['name2'];
632				$this->add_data($block);
633				$central .= pack('VvvvvVVVVvvvvvVV', 0x02014b50, 0x0014, $this->options['method'] == 0 ? 0x0000 : 0x000A, 0x0000,
634					(isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate,
635					0x00000000, 0x00000000, 0x00000000, strlen($current['name2']), 0x0000, 0x0000, 0x0000, 0x0000, $current['type'] == 5 ? 0x00000010 : 0x00000000, $offset);
636				$central .= $current['name2'];
637				$files++;
638				$offset += (30 + strlen($current['name2']));
639			}
640			else if ($fp = @fopen($current['name'], 'rb'))
641			{
642				$temp = fread($fp, $current['stat'][7]);
643				fclose($fp);
644				$crc32 = crc32($temp);
645				if (!isset($current['method']) && $this->options['method'] == 1)
646				{
647					$temp = gzcompress($temp, $this->options['level']);
648					$size = strlen($temp) - 6;
649					$temp = substr($temp, 2, $size);
650				}
651				else
652					$size = strlen($temp);
653				$block .= pack('VVVvv', $crc32, $size, $current['stat'][7], strlen($current['name2']), 0x0000);
654				$block .= $current['name2'];
655				$this->add_data($block);
656				$this->add_data($temp);
657				unset ($temp);
658				$central .= pack('VvvvvVVVVvvvvvVV', 0x02014b50, 0x0014, $this->options['method'] == 0 ? 0x0000 : 0x000A, 0x0000,
659					(isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate,
660					$crc32, $size, $current['stat'][7], strlen($current['name2']), 0x0000, 0x0000, 0x0000, 0x0000, 0x00000000, $offset);
661				$central .= $current['name2'];
662				$files++;
663				$offset += (30 + strlen($current['name2']) + $size);
664			}
665			else
666				$this->error[] = "Could not open file {$current['name']} for reading. It was not added.";
667		}
668
669		$this->add_data($central);
670
671		$this->add_data(pack('VvvvvVVv', 0x06054b50, 0x0000, 0x0000, $files, $files, strlen($central), $offset,
672			!empty ($this->options['comment']) ? strlen($this->options['comment']) : 0x0000));
673
674		if (!empty ($this->options['comment']))
675			$this->add_data($this->options['comment']);
676
677		chdir($pwd);
678
679		return 1;
680	}
681}
682//EOF