<?php
/******************************************************************************
* Subs-Package.php                                                            *
*******************************************************************************
* SMF: Simple Machines Forum                                                  *
* Open-Source Project Inspired by Zef Hemel (zef@zefhemel.com)                *
* =========================================================================== *
* Software Version:           SMF 1.0.3                                       *
* Software by:                Simple Machines (http://www.simplemachines.org) *
* Copyright 2001-2005 by:     Lewis Media (http://www.lewismedia.com)         *
* Support, News, Updates at:  http://www.simplemachines.org                   *
*******************************************************************************
* This program is free software; you may redistribute it and/or modify it     *
* under the terms of the provided license as published by Lewis Media.        *
*                                                                             *
* This program is distributed in the hope that it is and will be useful,      *
* but WITHOUT ANY WARRANTIES; without even any implied warranty of            *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                        *
*                                                                             *
* See the "license.txt" file for details of the Simple Machines license.      *
* The latest version can always be found at http://www.simplemachines.org.    *
******************************************************************************/
if (!defined('SMF'))
	die('Hacking attempt...');

/*	This file's central purpose of existence is that of making the package
	manager work nicely.  It contains functions for handling tar.gz and zip
	files, as well as a simple xml parser to handle the xml package stuff.
	Not to mention a few functions to make file handling easier.

	array read_tgz_file(string filename, string destination,
			bool single_file = false, bool overwrite = false)
		- reads a .tar.gz file, filename, in and extracts file(s) from it.
		- essentially just a shortcut for read_tgz_data().

	array read_tgz_data(string data, string destination,
			bool single_file = false, bool overwrite = false)
		- extracts a file or files from the .tar.gz contained in data.
		- detects if the file is really a .zip file, and if so returns the
		  result of read_zip_data
		- if destination is null, returns a list of files in the archive.
		- if single_file is true, returns the contents of the file specified
		  by destination, if it exists, or false.
		- if single_file is true, destination can start with * and / to
		  signify that the file may come from any directory.
		- destination should not begin with a / if single_file is true.
		- overwrites existing files with newer modification times if and
		  only if overwrite is true.
		- creates the destination directory if it doesn't exist, and is
		  is specified.
		- requires zlib support be built into PHP.
		- returns an array of the files extracted.

	array read_zip_data(string data, string destination,
			bool single_file = false, bool overwrite = false)
		- extracts a file or files from the .zip contained in data.
		- if destination is null, returns a list of files in the archive.
		- if single_file is true, returns the contents of the file specified
		  by destination, if it exists, or false.
		- if single_file is true, destination can start with * and / to
		  signify that the file may come from any directory.
		- destination should not begin with a / if single_file is true.
		- overwrites existing files with newer modification times if and
		  only if overwrite is true.
		- creates the destination directory if it doesn't exist, and is
		  is specified.
		- requires zlib support be built into PHP.
		- returns an array of the files extracted.

	bool url_exists(string url)
		- checks to see if url is valid, and returns a 200 status code.
		- will return false if the file is "moved permanently" or similar.
		- returns true if the remote url exists.

	array loadInstalledPackages()
		- loads and returns an array of installed packages.
		- gets this information from Packages/installed.list.
		- returns the array of data.

	void saveInstalledPackages(array installed_mods)
		- saves an array (as generated by loadInstalledPackages) as the
		  list of installed packages.
		- writes this information to Packages/installed.list.
		- requires the above file to be writable.

	array getPackageInfo(string filename)
		- loads a package's information and returns a representative array.
		- expects the file to be a package in Packages/.
		- returns false if the package-info is invalid.
		- returns a basic array of id, version, filename, and similar
		  information.
		- in the array returned, an xmlArray is available in 'xml'.

	array parsePackageInfo(xmlArray &package, bool testing_only = true,
			string method = 'install', string previous_version = '')
		- parses the actions in package-info.xml files from packages.
		- package should be an xmlArray with package-info as its base.
		- testing_only should be true if the package should not actually be
		  applied.
		- method is upgrade, install, or uninstall.  Its default is install.
		- previous_version should be set to the previous installed version
		  of this package, if any.
		- does not handle failure terribly well; testing first is always
		  better.
		- returns an array of those changes made.

	bool matchPackageVersion(string version, string versions)
		- checks if version matches any of the versions in versions.
		- supports comma separated version numbers, with or without
		  whitespace.
		- supports lower and upper bounds. (1.0-1.2)
		- returns true if the version matched.

	string parse_path(string path)
		- parses special identifiers out of the specified path.
		- returns the parsed path.

	void deltree(string path, bool delete_directory = false)
		- deletes a directory, and all the files and direcories inside it.
		- requires access to delete these files.

	bool mktree(string path, int mode)
		- creates the specified tree structure with the mode specified.
		- creates every directory in path until it finds one that already
		  exists.
		- returns true if successful, false otherwise.

	void copytree(string source, string destination)
		- copies one directory structure over to another.
		- requires the destination to be writable.

	array parseModification(string file, bool testing = true, bool undo = false)
		- parses a xml-style modification file (file).
		- testing tells it the modifications shouldn't actually be saved.
		- undo specifies that the modifications the file requests should be
		  undone; this doesn't work with everything (regular expressions.)
		- returns an array of those changes made.

	array parseBoardMod(string file, bool testing = true, bool undo = false)
		- parses a boardmod-style modification file (file).
		- testing tells it the modifications shouldn't actually be saved.
		- undo specifies that the modifications the file requests should be
		  undone.
		- returns an array of those changes made.

	int file_put_contents(string filename, string data)
		- only defined if it doesn't already exist. (PHP 5)
		- writes data to a file, almost exactly like the built in function.
		- uses text mode for text mode file extensions.
		- returns the number of bytes written.

	---------------------------------------------------------------------------
	The following functions are all within the xmlArray class, which is the xml
	parser.  There are more functions, but these are the ones that should be
	used from outside the class:

	class xmlArray(string data, bool auto_trim = false,
			int error_level = error_reporting(), bool is_clone = false)
		- creates a new xmlArray, which is an simple xml dom parser.
		- data should be the xml data or an array of, unless is_clone is true.
		- auto_trim can be used to automatically trim textual data.
		- error_level specifies whether notices should be generated for
		  missing elements and attributes.
		- if is_clone is true, the xmlArray is cloned from another - used
		  internally only.

	string xmlArray::name()
		- retrieves the name of the current element, usually ''.

	string xmlArray::fetch(string path, bool get_elements = false)
		- retrieves the textual value of the specified path.
		- children are parsed for text, but only textual data is returned
		  unless get_elements is true.

	xmlArray xmlArray::path(string path, bool return_set = false)
		- finds any elements that match the path specified.
		- will always return a set if there is more than one of the element
		  or return_set is true.
		- returns in the form of a new xmlArray.

	bool xmlArray::exists(string path)
		- returns whether the specified path matches at least one element.

	int xmlArray::count(string path)
		- returns the number of elements the path matches.

	array xmlArray::set(string path)
		- returns an array of xmlArray's matching the specified path.
		- this differs from ->path(path, true) in that instead of an xmlArray
		  of elements, an array of xmlArray's is returned for use with foreach.

	string xmlArray::create_xml(string path = '.')
		- returns the specified path as an xml file.
*/

// Get the data from the file and extract it.
function read_tgz_file($gzfilename, $destination, $single_file = false, $overwrite = false)
{
	if (function_exists('file_get_contents'))
	{
		$data = @file_get_contents($gzfilename);

		if ($data === false)
			return false;
	}
	else
	{
		$fp = @fopen($gzfilename, 'rb');
		if ($fp === false)
			return false;

		$data = '';
		while (!feof($fp))
			$data .= fread($fp, 4096);
		fclose($fp);
	}

	return read_tgz_data($data, $destination, $single_file, $overwrite);
}

// Extract tar.gz data.  If destination is null, return a listing.
function read_tgz_data($data, $destination, $single_file = false, $overwrite = false)
{
	umask(0);
	if ($destination !== null && !file_exists($destination) && !$single_file)
		mkdir($destination, 0777);

	// No signature?
	if (strlen($data) < 2)
		return false;

	$id = unpack('H2a/H2b', substr($data, 0, 2));
	if (strtolower($id['a'] . $id['b']) != '1f8b')
	{
		// Okay, this ain't no tar.gz, but maybe it's a zip file.
		if (substr($data, 0, 2) == 'PK')
			return read_zip_data($data, $destination, $single_file, $overwrite);
		else
			return false;
	}

	$flags = unpack('Ct/Cf', substr($data, 2, 2));

	// Not deflate!
	if ($flags['t'] != 8)
		return false;
	$flags = $flags['f'];

	$offset = 10;
	$octdec = array('mode', 'uid', 'gid', 'size', 'mtime', 'checksum', 'type');

	// "Read" the filename and comment.
	if ($flags & 12)
	{
		while ($flags & 8 && $data{$offset++} != "\0")
			$offset;
		while ($flags & 4 && $data{$offset++} != "\0")
			$offset;
	}

	$crc = unpack('Vcrc32/Visize', substr($data, strlen($data) - 8, 8));
	$data = @gzinflate(substr($data, $offset, strlen($data) - 8 - $offset));

	if ($crc['crc32'] != crc32($data))
		return false;

	$blocks = strlen($data) / 512 - 1;
	$offset = 0;

	$return = array();

	while ($offset < $blocks)
	{
		$header = substr($data, $offset << 9, 512);
		$current = unpack('a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1type/a100linkname/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155path', $header);

		foreach ($current as $k => $v)
		{
			if (in_array($k, $octdec))
				$current[$k] = octdec(trim($v));
			else
				$current[$k] = trim($v);
		}

		$checksum = 256;
		for ($i = 0; $i < 148; $i++)
			$checksum += ord($header{$i});
		for ($i = 156; $i < 512; $i++)
			$checksum += ord($header{$i});

		if ($current['checksum'] != $checksum)
			return $return;

		$size = ceil($current['size'] / 512);
		$current['data'] = substr($data, ++$offset << 9, $current['size']);
		$offset += $size;

		// Not a directory and doesn't exist already...
		if (substr($current['filename'], -1, 1) != '/' && !file_exists($destination . '/' . $current['filename']))
			$write_this = true;
		// File exists... check if it is newer.
		elseif (substr($current['filename'], -1, 1) != '/')
			$write_this = $overwrite || filemtime($destination . '/' . $current['filename']) < $current['mtime'];
		// Folder... create.
		elseif ($destination !== null && !$single_file)
		{
			// Protect from accidental parent directory writing...
			$current['filename'] = strtr($current['filename'], array('../' => '', '/..' => ''));

			if (!file_exists($destination . '/' . $current['filename']))
				mkdir($destination . '/' . $current['filename'], 0777);
			$write_this = false;
		}
		else
			$write_this = false;

		if ($write_this && $destination !== null)
		{
			if (strpos($current['filename'], '/') !== false && !$single_file)
			{
				$dirs = explode('/', $current['filename']);
				array_pop($dirs);

				$dirpath = $destination . '/';
				foreach ($dirs as $dir)
				{
					if (!file_exists($dirpath . $dir))
						mkdir($dirpath . $dir, 0777);
					$dirpath .= $dir . '/';
				}
			}

			// Is this the file we're looking for?
			if ($single_file && ($destination == $current['filename'] || $destination == '*/' . basename($current['filename'])))
				return $current['data'];
			// If we're looking for another file, keep going.
			elseif ($single_file)
				continue;

			file_put_contents($destination . '/' . $current['filename'], $current['data']);
		}

		if (substr($current['filename'], -1, 1) != '/')
			$return[] = array(
				'filename' => $current['filename'],
				'size' => $current['size'],
				'skipped' => false
			);
	}

	if ($single_file)
		return false;
	else
		return $return;
}

// Extract zip data.  If destination is null, return a listing.
function read_zip_data($data, $destination, $single_file = false, $overwrite = false)
{
	umask(0);
	if ($destination !== null && !file_exists($destination) && !$single_file)
		mkdir($destination, 0777);

	// Look for the PK header...
	if (substr($data, 0, 2) != 'PK')
		return false;

	// Find the central whosamawhatsit at the end; if there's a comment it's a pain.
	if (substr($data, -22, 4) == 'PK' . chr(5) . chr(6))
		$p = -22;
	else
	{
		// Have to find where the comment begins, ugh.
		for ($p = -22; $p > -strlen($data); $p--)
		{
			if (substr($data, $p, 4) == 'PK' . chr(5) . chr(6))
				break;
		}
	}

	$return = array();

	// Get the basic zip file info.
	$zip_info = unpack('vfiles/Vsize/Voffset', substr($data, $p + 10, 10));

	$p = $zip_info['offset'];
	for ($i = 0; $i < $zip_info['files']; $i++)
	{
		// Make sure this is a file entry...
		if (substr($data, $p, 4) != 'PK' . chr(1) . chr(2))
			return false;

		// Get all the important file information.
		$file_info = unpack('Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', substr($data, $p + 16, 30));
		$file_info['filename'] = substr($data, $p + 46, $file_info['filename_len']);

		// Skip all the information we don't care about anyway.
		$p += 46 + $file_info['filename_len'] + $file_info['extra_len'] + $file_info['comment_len'];

		// If this is a file, and it doesn't exist.... happy days!
		if (substr($file_info['filename'], -1, 1) != '/' && !file_exists($destination . '/' . $file_info['filename']))
			$write_this = true;
		// If the file exists, we may not want to overwrite it.
		elseif (substr($file_info['filename'], -1, 1) != '/')
			$write_this = $overwrite;
		// This is a directory, so we're gonna want to create it. (probably...)
		elseif ($destination !== null && !$single_file)
		{
			// Just a little accident prevention, don't mind me.
			$file_info['filename'] = strtr($file_info['filename'], array('../' => '', '/..' => ''));

			if (!file_exists($destination . '/' . $file_info['filename']))
				mkdir($destination . '/' . $file_info['filename'], 0777);
			$write_this = false;
		}
		else
			$write_this = false;

		// Okay!  We can write this file, looks good from here...
		if ($write_this && $destination !== null)
		{
			if (strpos($file_info['filename'], '/') !== false && !$single_file)
			{
				// Make any parents this file may need to have for things to work out.
				$dirs = explode('/', $file_info['filename']);
				array_pop($dirs);

				$dirpath = $destination . '/';
				foreach ($dirs as $dir)
				{
					if (!file_exists($dirpath . $dir))
						mkdir($dirpath . $dir, 0777);
					$dirpath .= $dir . '/';
				}
			}

			// Check that the data is there and does exist.
			if (substr($data, $file_info['offset'], 4) != 'PK' . chr(3) . chr(4))
				return false;

			// Get the actual compressed data.
			$file_info['data'] = substr($data, $file_info['offset'] + 30 + $file_info['filename_len'] + $file_info['extra_len'], $file_info['compressed_size']);

			// Only inflate it if we need to ;).
			if ($file_info['compressed_size'] != $file_info['size'])
				$file_info['data'] = @gzinflate($file_info['data']);

			// If we're looking for a specific file, and this is it... ka-bam, baby.
			if ($single_file && ($destination == $file_info['filename'] || $destination == '*/' . basename($file_info['filename'])))
				return $file_info['data'];
			// Oh?  Another file.  Fine.  You don't like this file, do you?  I know how it is.  Yeah... just go away.  No, don't apologize.  I know this file's just not *good enough* for you.
			elseif ($single_file)
				continue;

			file_put_contents($destination . '/' . $file_info['filename'], $file_info['data']);
		}

		if (substr($file_info['filename'], -1, 1) != '/')
			$return[] = array(
				'filename' => $file_info['filename'],
				'size' => $file_info['size'],
				'skipped' => false
			);
	}

	if ($single_file)
		return false;
	else
		return $return;
}

// Checks the existence of a remote file since file_exists() does not do remote.
function url_exists($url)
{
	$a_url = parse_url($url);

	if (!isset($a_url['scheme']))
		return false;

	// Attempt to connect...
	$temp = '';
	$fid = fsockopen($a_url['host'], !isset($a_url['port']) ? 80 : $a_url['port'], $temp, $temp, 8);
	if (!$fid)
		return false;

	fputs($fid, 'HEAD ' . $a_url['path'] . " HTTP/1.0\r\nHost: " . $a_url['host'] . "\r\n\r\n");
	$head = fread($fid, 1024);
	fclose($fid);

	return preg_match('~^HTTP/.+\s+200~i', $head) == 1;
}

// Load the installed packages.
function loadInstalledPackages()
{
	global $boarddir;

	$installed_mods = file($boarddir . '/Packages/installed.list');

	$installed = array();
	for ($i = 0, $n = count($installed_mods); $i < $n; $i++)
	{
		// Skip any empty lines.
		if (trim($installed_mods[$i]) == '')
			continue;

		// Ignore errors with borked installed.list's.
		list ($name, $file, $id, $version) = array_pad(explode('|^|', $installed_mods[$i]), 4, '');

		// Pretty simple, eh?
		$installed[] = array(
			'name' => stripslashes($name),
			'filename' => stripslashes($file),
			'id' => $id,
			'version' => trim($version)
		);
	}

	return $installed;
}

function saveInstalledPackages($instmods)
{
	global $boarddir;

	// Attempt to make the installed.list file writable if it isn't yet.
	if (!is_writable($boarddir . '/Packages/installed.list'))
		@chmod($boarddir . '/Packages/installed.list', 0777);

	$data = '';
	foreach ($instmods as $packageInfo)
	{
		if (empty($packageInfo))
			continue;

		$data .= trim($packageInfo['name']) . '|^|' . trim($packageInfo['filename']) . '|^|' . trim($packageInfo['id']) . '|^|' . trim($packageInfo['version']) . "\n";
	}
	file_put_contents($boarddir . '/Packages/installed.list', $data);
}

function getPackageInfo($gzfilename)
{
	global $boarddir;

	// Extract package-info.xml from downloaded file. (*/ is used because it could be in any directory.)
	if (strpos($gzfilename, 'http://') !== false)
		$packageInfo = read_tgz_file($gzfilename, '*/package-info.xml', true);
	else
	{
		if (!file_exists($boarddir . '/Packages/' . $gzfilename))
			return false;

		$packageInfo = read_tgz_file($boarddir . '/Packages/' . $gzfilename, '*/package-info.xml', true);
	}

	// Parse package-info.xml into an xmlArray.
	$packageInfo = new xmlArray($packageInfo);

	if (!$packageInfo->exists('package-info[0]'))
		return false;

	$packageInfo = $packageInfo->path('package-info[0]');

	$package = $packageInfo->to_array();
	$package['xml'] = $packageInfo;
	$package['filename'] = $gzfilename;

	if (!isset($package['type']))
		$package['type'] = 'modification';

	return $package;
}

// Parses a package-info.xml file - method can be 'install', 'upgrade', or 'uninstall'.
function parsePackageInfo(&$packageXML, $testing_only = true, $method = 'install', $previous_version = '')
{
	global $boarddir, $forum_version;

	// Mayday!  That action doesn't exist!!
	if (!$packageXML->exists($method))
		return array();

	// We haven't found the package script yet...
	$script = false;
	$the_version = strtr($forum_version, array('SMF ' => ''));

	// Get all the versions of this method and find the right one.
	$these_methods = $packageXML->set($method);
	foreach ($these_methods as $this_method)
	{
		// They specified certain versions this part is for.
		if ($this_method->exists('@for'))
		{
			// Don't keep going if this won't work for this version of SMF.
			if (!matchPackageVersion($the_version, $this_method->fetch('@for')))
				continue;
		}

		// Upgrades may go from a certain old version of the mod.
		if ($method == 'upgrade' && $this_method->exists('@from'))
		{
			// Well, this is for the wrong old version...
			if (!matchPackageVersion($previous_version, $this_method->fetch('@from')))
				continue;
		}

		// We've found it!
		$script = $this_method;
		break;
	}

	// Bad news, a matching script wasn't found!
	if ($script === false)
		return array();

	// Find all the actions in this method - in theory, these should only be allowed actions. (* means all.)
	$actions = $script->set('*');
	$return = array();

	$temp_auto = 0;

	// This is the testing phase... nothing shall be done yet.
	foreach ($actions as $action)
	{
		$actionType = $action->name();

		if ($actionType == 'readme' || $actionType == 'code' || $actionType == 'modification')
		{
			if ($action->exists('@type') && $action->fetch('@type') == 'inline')
			{
				$filename = $boarddir . '/Packages/temp/$auto_' . $temp_auto++ . ($actionType == 'readme' ? '.txt' : ($actionType == 'code' ? '.php' : '.mod'));
				file_put_contents($filename, $action->fetch('.'));
				$filename = strtr($filename, array($boarddir . '/Packages/temp/' => ''));
			}
			else
				$filename = $action->fetch('.');

			$return[] = array(
				'type' => $actionType,
				'filename' => $filename,
				'description' => '',
				'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true',
				'boardmod' => $action->exists('@format') && $action->fetch('@format') == 'boardmod',
			);

			continue;
		}

		$this_action = &$return[];
		$this_action = array(
			'type' => $actionType,
			'filename' => $action->fetch('@name'),
			'description' => $action->fetch('.')
		);

		// If there is a destination, make sure it makes sense.
		if (substr($actionType, 0, 6) != 'remove')
			$this_action['destination'] = parse_path($action->fetch('@destination')) . '/' . basename($this_action['filename']);
		else
			$this_action['filename'] = parse_path($this_action['filename']);

		// If we're moving or requiring (copying) a file
		if (substr($actionType, 0, 4) == 'move' || substr($actionType, 0, 7) == 'require')
		{
			if ($action->exists('@from'))
				$this_action['source'] = parse_path($action->fetch('@from'));
			else
				$this_action['source'] = $boarddir . '/Packages/temp/' . $this_action['filename'];
		}

		// Check if these things can be done. (chmod's etc.)
		if ($actionType == 'create-dir')
		{
			if (!mktree($this_action['destination'], false))
			{
				$temp = $this_action['destination'];
				while (!file_exists($temp) && strlen($temp) > 1)
					$temp = dirname($temp);

				$return[] = array(
					'type' => 'chmod',
					'filename' => $temp
				);
			}
		}
		elseif ($actionType == 'create-file')
		{
			if (!mktree(dirname($this_action['destination']), false))
			{
				$temp = dirname($this_action['destination']);
				while (!file_exists($temp) && strlen($temp) > 1)
					$temp = dirname($temp);

				$return[] = array(
					'type' => 'chmod',
					'filename' => $temp
				);
			}

			if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
				$return[] = array(
					'type' => 'chmod',
					'filename' => $this_action['destination']
				);
		}
		elseif ($actionType == 'require-dir')
		{
			if (!mktree($this_action['destination'], false))
			{
				$temp = $this_action['destination'];
				while (!file_exists($temp) && strlen($temp) > 1)
					$temp = dirname($temp);

				$return[] = array(
					'type' => 'chmod',
					'filename' => $temp
				);
			}
		}
		elseif ($actionType == 'require-file')
		{
			if (!mktree(dirname($this_action['destination']), false))
			{
				$temp = dirname($this_action['destination']);
				while (!file_exists($temp) && strlen($temp) > 1)
					$temp = dirname($temp);

				$return[] = array(
					'type' => 'chmod',
					'filename' => $temp
				);
			}

			if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
				$return[] = array(
					'type' => 'chmod',
					'filename' => $this_action['destination']
				);
		}
		elseif ($actionType == 'move-dir' || $actionType == 'move-file')
		{
			if (!mktree(dirname($this_action['destination']), false))
			{
				$temp = dirname($this_action['destination']);
				while (!file_exists($temp) && strlen($temp) > 1)
					$temp = dirname($temp);

				$return[] = array(
					'type' => 'chmod',
					'filename' => $temp
				);
			}

			if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
				$return[] = array(
					'type' => 'chmod',
					'filename' => $this_action['destination']
				);
		}
		elseif ($actionType == 'remove-dir')
		{
			if (!is_writable($this_action['filename']) && file_exists($this_action['destination']))
				$return[] = array(
					'type' => 'chmod',
					'filename' => $this_action['filename']
				);
		}
		elseif ($actionType == 'remove-file')
		{
			if (!is_writable($this_action['filename']) && file_exists($this_action['filename']))
				$return[] = array(
					'type' => 'chmod',
					'filename' => $this_action['filename']
				);
		}
	}

	// Only testing - just return a list of things to be done.
	if ($testing_only)
		return $return;

	umask(0);

	$failure = false;
	$not_done = array(array('type' => '!'));
	foreach ($return as $action)
	{
		if ($action['type'] == 'modification' || $action['type'] == 'code')
			$not_done[] = $action;

		if ($action['type'] == 'create-dir')
			$failure |= !mktree($action['destination'], 0777);
		elseif ($action['type'] == 'create-file')
		{
			$failure |= !mktree(dirname($action['destination']), 0777);

			// Create an empty file.
			$f = fopen($action['destination'], 'w');

			if ($f == false)
				$failure = true;
			else
				fclose($f);
		}
		elseif ($action['type'] == 'require-dir')
			copytree($action['source'], $action['destination']);
		elseif ($action['type'] == 'require-file')
		{
			$failure |= !mktree(dirname($action['destination']), 0777);

			if (!is_writable($action['destination']))
				@chmod($action['destination'], 0777);

			$failure |= !copy($action['source'], $action['destination']);
		}
		elseif ($action['type'] == 'move-dir' || $action['type'] == 'move-file')
		{
			$failure |= !mktree(dirname($action['destination']), 0777);

			$failure |= !rename($action['source'], $action['destination']);
		}
		elseif ($action['type'] == 'remove-dir')
			deltree($action['filename']);
		elseif ($action['type'] == 'remove-file')
		{
			if (!is_writable($action['filename']))
				@chmod($action['filename'], 0777);

			$failure |= !unlink($action['filename']);
		}
	}

	return $not_done;
}

// This is such a pain I created a function for it :P.
function matchPackageVersion($version, $versions)
{
	$for = explode(',', $versions);

	// Trim them all!
	for ($i = 0, $n = count($for); $i < $n; $i++)
		$for[$i] = trim($for[$i]);

	// The version is explicitly defined... too easy.
	if (in_array($version, $for))
		return true;

	foreach ($for as $list)
	{
		// Look for a version specification like "1.0-1.2".
		if (strpos($list, '-') === false)
			continue;

		list ($lower, $upper) = explode('-', $list);
		if (trim($lower) < $version && trim($upper) > $version)
			return true;
	}

	// Well, I guess it doesn't match...
	return false;
}

function parse_path($path)
{
	global $modSettings, $boarddir, $sourcedir, $settings;

	$search = array(
		'\\',
		'$boarddir',
		'$sourcedir',
		'$avatardir',
		'$themedir',
		'$languagedir',
		'$smileysdir',
	);
	$replace = array(
		'/',
		$boarddir,
		$sourcedir,
		$modSettings['avatar_directory'],
		$settings['default_theme_dir'],
		$settings['default_theme_dir'] . '/languages',
		$modSettings['smileys_dir'],
	);

	return str_replace($search, $replace, $path);
}

function deltree($dir, $delete_dir = true)
{
	if (!file_exists($dir))
		return;

	$current_dir = opendir($dir);
	if ($current_dir == false)
		return;

	while ($entryname = readdir($current_dir))
	{
		if (in_array($entryname, array('.', '..')))
			continue;

		if (is_dir($dir . '/' . $entryname))
			deltree($dir . '/' . $entryname);
		else
		{
			if (!is_writable($dir . '/' . $entryname))
				@chmod($dir . '/' . $entryname, 0777);
			unlink($dir . '/' . $entryname);
		}
	}

	closedir($current_dir);

	if ($delete_dir)
	{
		if (!is_writable($dir))
			@chmod($dir, 0777);
		rmdir($dir);
	}
}

function mktree($strPath, $mode)
{
	if (is_dir($strPath))
	{
		if (!is_writable($strPath) && $mode !== false)
			@chmod($strPath, $mode);
		return is_writable($strPath);
	}

	if (!mktree(dirname($strPath), $mode))
		return false;

	if (!is_writable(dirname($strPath)) && $mode !== false)
		@chmod(dirname($strPath), $mode);

	return $mode === false ? is_writable(dirname($strPath)) || file_exists($strPath) : mkdir($strPath, $mode);
}

function copytree($source, $destination)
{
	if (!file_exists($destination))
		mktree($destination, 0777);
	elseif (!is_writable($destination))
		@chmod($destination, 0777);

	$current_dir = opendir($source);
	if ($current_dir == false)
		return;

	while ($entryname = readdir($current_dir))
	{
		if (in_array($entryname, array('.', '..')))
			continue;

		if (is_dir($source . '/' . $entryname))
			copytree($source . '/' . $entryname, $destination . '/' . $entryname);
		else
			copy($source . '/' . $entryname, $destination . '/' . $entryname);
	}

	closedir($current_dir);
}

// Parse an xml based modification file.
function parseModification($file, $testing = true, $undo = false)
{
	global $boarddir, $sourcedir, $settings, $txt, $modSettings;

	@set_time_limit(600);
	$xml = new xmlArray(strtr($file, array("\r" => '')));
	$actions = array();
	$everything_found = true;

	if (!$xml->exists('modification') || !$xml->exists('modification/file'))
	{
		$actions[] = array(
			'type' => 'error',
			'filename' => '-',
			'debug' => $txt['package_modification_malformed']
		);
		return $actions;
	}

	$files = $xml->set('modification/file');
	foreach ($files as $file)
	{
		$working_file = parse_path(trim($file->fetch('@name')));

		// Doesn't exist - give an error or what?
		if (!file_exists($working_file) && (!$file->exists('@error') || (trim($file->fetch('@error')) != 'ignore' && trim($file->fetch('@error')) != 'skip')))
		{
			$actions[] = array(
				'type' => 'missing',
				'filename' => $working_file,
				'debug' => $txt['package_modification_missing']
			);

			$everything_found = false;
			continue;
		}
		// Skip the file if it doesn't exist.
		elseif (!file_exists($working_file) && $file->exists('@error') && trim($file->fetch('@error')) == 'skip')
			continue;
		// Okay, we're creating this file then...?
		elseif (!file_exists($working_file))
			$working_data = '';
		// Phew, it exists!  Load 'er up!
		else
			$working_data = str_replace("\r", '', implode('', file($working_file)));

		$actions[] = array(
			'type' => 'opened',
			'filename' => $working_file
		);

		$operations = $file->exists('operation') ? $file->set('operation') : array();
		foreach ($operations as $operation)
		{
			// Special case: find the end of the file!
			if ($operation->exists('search/@position') && trim($operation->fetch('search/@position')) == 'end')
			{
				$replace_with = $operation->fetch('add');

				if (substr(rtrim($working_data), -3) == "\n?" . '>')
					$working_data = substr(rtrim($working_data), 0, -3) . $replace_with . "\n?" . '>';
				else
					$working_data = $working_data . $replace_with;

				$actions[] = array(
					'type' => 'append',
					'filename' => $working_file,
					'add' => $replace_with
				);
			}
			// Otherwise we will need to look for the search data.
			else
			{
				// Start with what we have....
				$working_search = '';
				$replace_with = $operation->exists('add') ? $operation->fetch('add') : '';

				// Are we doing a fancy-shmancy regexp search?
				$regexp = false;

				$temp_searches = $operation->set('search');
				if (count($temp_searches) > 1)
				{
					// Resort the list so replace comes first - it must.
					$searches = array();
					foreach ($temp_searches as $i => $v)
					{
						// A quick check on whether it's a regular expression search...
						if (!$undo && $temp_searches[$i]->exists('@regexp') && trim($temp_searches[$i]->fetch('@regexp')) == 'true')
							$regexp = true;

						if (!$v->exists('@position') || ($v->fetch('@position') != 'before' && $v->fetch('@position') != 'after'))
						{
							$searches[] = $v;
							unset($temp_searches[$i]);
						}
					}

					// Add on the rest, in the order they were in.
					$searches = array_merge($searches, $temp_searches);
				}
				else
				{
					$searches = $temp_searches;
					$regexp = !$undo && $searches[0]->exists('@regexp') && trim($searches[0]->fetch('@regexp')) == 'true';
				}

				// If we're not using regular expression search, replace out any $'s and \'s!
				if (!$regexp)
				{
					// Shuzzup.  This is done so we can safely use a regular expression. ($0 is bad!!)
					if (!$undo)
						$replace_with = strtr($replace_with, array('$' => '[$PACK' . 'AGE1$]', '\\' => '[$PACK' . 'AGE2$]'));
					else
						$replace_with = preg_quote($replace_with, '~');
				}

				// Go through all the search conditions.
				foreach ($searches as $search)
				{
					$this_clean_search = $search->fetch('.');

					// Are we not using regular expressions explicitly?
					if (!$regexp)
					{
						$this_search = preg_quote($this_clean_search, '~');

						// Remember, can't have whitespace correction and regexp on at the same time.
						if ($search->exists('@whitespace') && trim($search->fetch('@whitespace')) != 'loose')
							$this_search = preg_replace('~[ \t]+~', '[ \t]+', $this_search);

						// Shuzzup again.  Read the comment above a few lines where this is done to $replace_with...
						if ($undo)
							$working_search = strtr($working_search, array('$' => '[$PACK' . 'AGE1$]', '\\' => '[$PACK' . 'AGE2$]'));
					}
					else
						$this_search = $this_clean_search;

					// Get the position to replace on.
					$position = $search->exists('@position') ? trim($search->fetch('@position')) : 'replace';

					if ($position == 'before')
					{
						if (!$regexp)
						{
							if (!$undo)
							{
								$working_search = '(' . $this_search . ')' . $working_search;
								$replace_with = '$1' . $replace_with;
							}
							else
							{
								$working_search = '$1' . $working_search;
								$replace_with = '(' . $this_search . ')' . $replace_with;
							}
						}
						else
						{
							$working_search = $this_search . $working_search;
							$replace_with = $this_clean_search . $replace_with;
						}
					}
					elseif ($position == 'after')
					{
						if (!$regexp)
						{
							if (!$undo)
							{
								$working_search = $working_search . '(' . $this_search . ')';
								$replace_with = $replace_with . '$1';
							}
							else
							{
								$working_search = $working_search . '$1';
								$replace_with = $replace_with . '(' . $this_search . ')';
							}
						}
						else
						{
							$working_search = $working_search . $this_search;
							$replace_with = $replace_with . $this_clean_search;
						}
					}
					else
					{
						if (!$undo)
							$working_search = $this_search;
						else
							$working_search = $this_clean_search;
					}
				}

				if ($undo)
				{
					$temp = $replace_with;
					$replace_with = $working_search;
					$working_search = $temp;

					// We can't undo this!
					if (trim($working_search) == '' || trim($replace_with) == '')
						continue;
				}
				elseif ($working_search == '')
					continue;

				$failed = preg_match('~' . $working_search . '~s', $working_data) == 0;
				if ($failed && (!$operation->exists('@error') || $operation->fetch('@error') == 'fatal'))
				{
					$actions[] = array(
						'type' => 'failure',
						'filename' => $working_file,
						'search' => $working_search
					);

					$everything_found = false;
					continue;
				}
				elseif (!$failed && $operation->exists('@error') && $operation->fetch('@error') == 'required')
				{
					$actions[] = array(
						'type' => 'failure',
						'filename' => $working_file,
						'search' => $working_search
					);

					$everything_found = false;
					continue;
				}

				if ($replace_with == '')
					continue;

				$working_data = preg_replace('~' . $working_search . '~s', $replace_with, $working_data);

				$actions[] = array(
					'type' => 'replace',
					'filename' => $working_file,
					'search' => $working_search,
					'replace' => $replace_with
				);
			}
		}

		// Fix any little helper symbols ;).
		$working_data = strtr($working_data, array('[$PACK' . 'AGE1$]' => '$', '[$PACK' . 'AGE2$]' => '\\'));

		if (file_exists($working_file) && !is_writable($working_file))
			@chmod($working_file, 0755);
		if (file_exists($working_file) && !is_writable($working_file))
			@chmod($working_file, 0777);

		if (!file_exists($working_file) && !is_writable(dirname($working_file)))
			@chmod(dirname($working_file), 0755);
		if (!file_exists($working_file) && !is_writable(dirname($working_file)))
			@chmod(dirname($working_file), 0777);

		if ((file_exists($working_file) && !is_writable($working_file)) || !is_writable(dirname($working_file)))
			$actions[] = array(
				'type' => 'chmod',
				'filename' => $working_file
			);

		if (basename($working_file) == 'Settings_bak.php')
			continue;

		if (!$testing)
		{
			if (!empty($modSettings['package_make_backups']) && file_exists($working_file))
			{
				// No, no, not Settings.php!
				if (basename($working_file) == 'Settings.php')
					copy($working_file, dirname($working_file) . '/Settings_bak.php');
				else
					copy($working_file, $working_file . '~');
			}

			file_put_contents($working_file, $working_data);
		}

		$actions[] = array(
			'type' => 'saved',
			'filename' => $working_file
		);
	}

	$actions[] = array(
		'type' => 'result',
		'status' => $everything_found
	);

	return $actions;
}

// Parses a BoardMod format mod file...
function parseBoardMod($file, $testing = true, $undo = false)
{
	global $boarddir, $sourcedir, $settings, $txt, $modSettings;

	@set_time_limit(600);
	$file = strtr($file, array("\r" => ''));

	$working_file = null;
	$working_search = null;
	$working_data = '';
	$replace_with = null;

	$actions = array();
	$everything_found = true;

	while (preg_match('~<(edit file|file|search|search for|add|add after|replace|add before|before)>\n(.*?)\n</\\1>~is', $file, $code_match) != 0)
	{
		// Edit a specific file.
		if ($code_match[1] == 'file' || $code_match[1] == 'edit file')
		{
			// Backup the old file.
			if ($working_file !== null)
			{
				if (file_exists($working_file) && !is_writable($working_file))
					@chmod($working_file, 0777);
				elseif (!is_writable(dirname($working_file)))
					@chmod(dirname($working_file), 0777);

				// Don't even dare.
				if (basename($working_file) == 'Settings_bak.php')
					continue;

				if (!is_writable($working_file))
					$actions[] = array(
						'type' => 'chmod',
						'filename' => $working_file
					);

				if (!$testing)
				{
					if (!empty($modSettings['package_make_backups']) && file_exists($working_file))
					{
						if (basename($working_file) == 'Settings.php')
							copy($working_file, dirname($working_file) . '/Settings_bak.php');
						else
							copy($working_file, $working_file . '~');
					}

					file_put_contents($working_file, $working_data);
				}
			}

			if ($working_file !== null)
				$actions[] = array(
					'type' => 'saved',
					'filename' => $working_file
				);

			// Make sure the file exists!
			$code_match[2] = parse_path($code_match[2]);
			$working_file = $code_match[2];
			if (!file_exists($code_match[2]))
			{
				$places_to_check = array($boarddir, $sourcedir, $settings['default_theme_dir'], $settings['default_theme_dir'] . '/languages');

				foreach ($places_to_check as $place)
					if (file_exists($place . '/' . $code_match[2]))
					{
						$code_match[2] = $place . '/' . $code_match[2];
						break;
					}
			}

			if (file_exists($code_match[2]))
			{
				// Load the new file.
				$working_data = str_replace("\r", '', implode('', file($code_match[2])));

				$actions[] = array(
					'type' => 'opened',
					'filename' => $working_file
				);
			}
			else
			{
				$actions[] = array(
					'type' => 'missing',
					'filename' => $working_file
				);

				$working_file = null;
				$everything_found = false;
			}

			// Can't be searching for something...
			$working_search = null;
		}
		// Search for a specific string.
		elseif (($code_match[1] == 'search' || $code_match[1] == 'search for') && $working_file !== null)
		{
			if ($working_search !== null)
			{
				$actions[] = array(
					'type' => 'error',
					'filename' => $working_file
				);

				$everything_found = false;
			}

			$working_search = $code_match[2];
		}
		// Must've already loaded a search string.
		elseif ($working_search !== null)
		{
			// This is the base string....
			$replace_with = $code_match[2];

			// Add this afterward...
			if ($code_match[1] == 'add' || $code_match[1] == 'add after')
				$replace_with = $working_search . "\n" . $replace_with;
			// Add this beforehand.
			elseif ($code_match[1] == 'before' || $code_match[1] == 'add before')
				$replace_with .= "\n" . $working_search;
			// Otherwise.. replace with $replace_with ;).
		}

		// If we have a search string, replace string, and open file..
		if ($working_search !== null && $replace_with !== null && $working_file !== null)
		{
			// Make sure it's somewhere in the string.
			if ($undo)
			{
				$temp = $replace_with;
				$replace_with = $working_search;
				$working_search = $temp;
			}

			if (strpos($working_data, $working_search) !== false)
			{
				$working_data = str_replace($working_search, $replace_with, $working_data);

				$actions[] = array(
					'type' => 'replace',
					'filename' => $working_file,
					'search' => $working_search,
					'replace' => $replace_with
				);
			}
			// It wasn't found!
			else
			{
				$actions[] = array(
					'type' => 'failure',
					'filename' => $working_file,
					'search' => $working_search
				);

				$everything_found = false;
			}

			// These don't hold any meaning now.
			$working_search = null;
			$replace_with = null;
		}

		// Get rid of the old tag.
		$file = substr_replace($file, '', strpos($file, $code_match[0]), strlen($code_match[0]));
	}

	// Backup the old file.
	if ($working_file !== null)
	{
		if (file_exists($working_file) && !is_writable($working_file))
			@chmod($working_file, 0777);
		elseif (!is_writable(dirname($working_file)))
			@chmod(dirname($working_file), 0777);

		if (!is_writable($working_file))
			$actions[] = array(
				'type' => 'chmod',
				'filename' => $working_file
			);

		if (!$testing)
		{
			if (!empty($modSettings['package_make_backups']) && file_exists($working_file))
				copy($working_file, $working_file . '~');

			file_put_contents($working_file, $working_data);
		}
	}

	if ($working_file !== null)
		$actions[] = array(
			'type' => 'saved',
			'filename' => $working_file
		);

	$actions[] = array(
		'type' => 'result',
		'status' => $everything_found
	);

	return $actions;
}

// Create a file - this is defined in PHP 5, just use the same function name.
if (!function_exists('file_put_contents'))
{
	function file_put_contents($filename, $data)
	{
		$text_filetypes = array('php', 'txt', '.js', 'css', 'vbs', 'tml', 'htm');

		$fp = fopen($filename, in_array(substr($filename, -3), $text_filetypes) ? 'w' : 'wb');

		if (!$fp)
			return 0;

		fwrite($fp, $data);
		fclose($fp);

		return strlen($data);
	}
}

// An xml array.  Reads in xml, allows you to access it simply.  Version 1.1.
class xmlArray
{
	// The array and debugging output level.
	var $array, $debug_level, $trim;

	// Create an xml array.
	//	the xml data, trim elements?, debugging output level, reserved.
	//ie. $xml = new xmlArray(file('data.xml'));
	function xmlArray($data, $auto_trim = false, $level = null, $is_clone = false)
	{
		// Set the debug level.
		$this->debug_level = $level !== null ? $level : error_reporting();
		$this->trim = $auto_trim;

		// Is the data already parsed?
		if ($is_clone)
		{
			$this->array = $data;
			return;
		}

		// Is the input an array? (ie. passed from file()?)
		if (is_array($data))
			$data = implode('', $data);

		// Remove any xml declaration or doctype, and parse out comments and CDATA.
		$data = $this->_to_cdata(preg_replace(array('/^<\?xml.+?\?>/is', '/<!DOCTYPE[^>]+?>/s', '/<!--.*?-->/s'), '', $data));

		// Now parse the xml!
		$this->array = $this->_parse($data);
	}

	// Get the root element's name.
	//ie. echo $element->name();
	function name()
	{
		return isset($this->array['name']) ? $this->array['name'] : '';
	}

	// Get a specified element's value or attribute by path.
	//	the path to the element to fetch, whether to include elements?
	//ie. $data = $xml->fetch('html/head/title');
	function fetch($path, $get_elements = false)
	{
		// Get the element, in array form.
		$array = $this->path($path);

		if ($array === false)
			return false;

		// Getting elements into this is a bit complicated...
		if ($get_elements && !is_string($array))
		{
			$temp = '';

			// Use the _xml() function to get the xml data.
			foreach ($array->array as $val)
			{
				// Skip the name and any attributes.
				if (is_array($val))
					$temp .= $this->_xml($val, null);
			}

			// Just get the XML data and then take out the CDATAs.
			return $this->_to_cdata($temp);
		}

		// Return the value - taking care to pick out all the text values.
		return is_string($array) ? $array : $this->_fetch($array->array);
	}

	// Get an element, returns a new xmlArray.
	//	the path to the element to get, always return full result set? (ie. don't contract a single item.)
	//ie. $element = $xml->path('html/body');
	function path($path, $return_full = false)
	{
		// Split up the path.
		$path = explode('/', $path);

		// Start with a base array.
		$array = $this->array;

		// For each element in the path.
		foreach ($path as $el)
		{
			// Deal with sets....
			if (strpos($el, '[') !== false)
			{
				$lvl = (int) substr($el, strpos($el, '[') + 1);
				$el = substr($el, 0, strpos($el, '['));
			}
			// Find an attribute.
			elseif (substr($el, 0, 1) == '@')
			{
				// It simplifies things if the attribute is already there ;).
				if (isset($array[$el]))
					return $array[$el];
				else
				{
					// Cause an error.
					if ($this->debug_level & E_NOTICE)
						trigger_error('Undefined XML attribute: ' . substr($el, 1), E_USER_NOTICE);
					return false;
				}
			}
			else
				$lvl = null;

			// Find this element.
			$array = $this->_path($array, $el, $lvl);
		}

		// Clean up after $lvl, for $return_full.
		if ($return_full && (!isset($array['name']) || substr($array['name'], -1) != ']'))
			$array = array('name' => $el . '[]', $array);

		// Create the right type of class...
		$newClass = get_class($this);

		// Return a new xmlArray for the result.
		return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true);
	}

	// Check if an element exists.
	//	the path to the element to get.
	//ie. echo $xml->exists('html/body') ? 'y' : 'n';
	function exists($path)
	{
		// Split up the path.
		$path = explode('/', $path);

		// Start with a base array.
		$array = $this->array;

		// For each element in the path.
		foreach ($path as $el)
		{
			// Deal with sets....
			if (strpos($el, '[') !== false)
			{
				$lvl = (int) substr($el, strpos($el, '[') + 1);
				$el = substr($el, 0, strpos($el, '['));
			}
			// Find an attribute.
			elseif (substr($el, 0, 1) == '@')
				return isset($array[$el]);
			else
				$lvl = null;

			// Find this element.
			$array = $this->_path($array, $el, $lvl, true);
		}

		return $array !== false;
	}

	// Count the number of occurances of a path.
	//	the path to search for.
	//ie. echo $xml->count('html/head/meta');
	function count($path)
	{
		// Get the element, always returning a full set.
		$temp = $this->path($path, true);

		// Start at zero, then count up all the numeric keys.
		$i = 0;
		foreach ($temp->array as $item)
		{
			if (is_array($item))
				$i++;
		}

		return $i;
	}

	// Get an array of xmlArray's for use with foreach.
	//	the path to search for.
	//ie. foreach ($xml->set('html/body/p') as $p)
	function set($path)
	{
		// None as yet, just get the path.
		$array = array();
		$xml = $this->path($path, true);

		foreach ($xml->array as $val)
		{
			// Skip these, they aren't elements.
			if (!is_array($val) || $val['name'] == '!')
				continue;

			// Create the right type of class...
			$newClass = get_class($this);

			// Create a new xmlArray and stick it in the array.
			$array[] = new $newClass($val, $this->trim, $this->debug_level, true);
		}

		return $array;
	}

	// Create an xml file from an xml array.
	//	the path to the element. (optional)
	//ie. echo $this->create_xml()
	function create_xml($path = null)
	{
		// Was a path specified?  If so, use that array.
		if ($path !== null)
		{
			$path = $this->path($path);

			// The path was not found!!!
			if ($path === false)
				return false;

			$path = $path->array;
		}
		// Just use the current array.
		else
			$path = $this->array;

		// Add the xml declaration to the front.
		return '<?xml version="1.0"?' . '>' . $this->_xml($path, 0);
	}

	// Output the xml in an array form.
	//	the path to output.
	//ie. print_r($xml->to_array());
	function to_array($path = null)
	{
		// Are we doing a specific path?
		if ($path !== null)
		{
			$path = $this->path($path);

			// The path was not found!!!
			if ($path === false)
				return false;

			$path = $path->array;
		}
		// No, so just use the current array.
		else
			$path = $this->array;

		return $this->_array($path);
	}

	// Parse data into an array. (privately used...)
	function _parse($data)
	{
		// Start with an 'empty' array with no data.
		$current = array(
		);

		// Loop until we're out of data.
		while ($data != '')
		{
			// Find and remove the next tag.
			preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)(\s\/)?>/', $data, $match);
			if (isset($match[0]))
				$data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1);

			// Didn't find a tag?  Keep looping....
			if (!isset($match[1]) || $match[1] == '')
			{
				// If there's no <, the rest is data.
				if (strpos($data, '<') === false)
				{
					$text_value = $this->_from_cdata($data);
					$data = '';

					if ($text_value != '')
						$current[] = array(
							'name' => '!',
							'value' => $text_value
						);
				}
				// If the < isn't immediately next to the current position... more data.
				elseif (strpos($data, '<') > 0)
				{
					$text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<')));
					$data = substr($data, strpos($data, '<'));

					if ($text_value != '')
						$current[] = array(
							'name' => '!',
							'value' => $text_value
						);
				}
				// If we're looking at a </something> with no start, kill it.
				elseif (strpos($data, '<') !== false && strpos($data, '<') == 0)
				{
					if (strpos($data, '<', 1) !== false)
					{
						$text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<', 1)));
						$data = substr($data, strpos($data, '<', 1));

						if ($text_value != '')
							$current[] = array(
								'name' => '!',
								'value' => $text_value
							);
					}
					else
					{
						$text_value = $this->_from_cdata($data);
						$data = '';

						if ($text_value != '')
							$current[] = array(
								'name' => '!',
								'value' => $text_value
							);
					}
				}

				// Wait for an actual occurance of an element.
				continue;
			}

			// Create a new element in the array.
			$el = &$current[];
			$el['name'] = $match[1];

			// If this ISN'T empty, remove the close tag and parse the inner data.
			if ((!isset($match[3]) || $match[3] != ' /') && (!isset($match[2]) || $match[2] != ' /'))
			{
				$tag = preg_quote($match[1], '/');
				$reg = '/\A(.*?(<' . $tag . ' .*?>.*?<\/' . $tag . '>.*?)*?)<\/' . $tag . '>/s';

				// Remove the element and fetch the inner data.
				preg_match($reg, $data, $inner_match);
				$data = preg_replace($reg, '', $data, 1);

				if (!isset($inner_match[1]))
					continue;

				// Parse the inner data.
				if (strpos($inner_match[1], '<') !== false)
					$el += $this->_parse($inner_match[1]);
				elseif (trim($inner_match[1]) != '')
				{
					$text_value = $this->_from_cdata($inner_match[1]);
					if ($text_value != '')
						$el[] = array(
							'name' => '!',
							'value' => $text_value
						);
				}
			}

			// If we're dealing with attributes as well, parse them out.
			if (isset($match[2]) && $match[2] != '')
			{
				// Find all the attribute pairs in the string.
				preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER);

				// Set them as @attribute-name.
				foreach ($attr as $match_attr)
					$el['@' . $match_attr[1]] = $match_attr[2];
			}
		}

		// Return the parsed array.
		return $current;
	}

	// Get a specific element's xml. (priavtely used...)
	function _xml($array, $indent)
	{
		$indentation = $indent !== null ? '
' . str_repeat('	', $indent) : '';

		// This is a set of elements, with no name...
		if (is_array($array) && !isset($array['name']))
		{
			$temp = '';
			foreach ($array as $val)
				$temp .= $this->_xml($val, $indent);
			return $temp;
		}

		// This is just text!
		if ($array['name'] == '!')
			return $indentation . '<![CDATA[' . $array['value'] . ']]>';
		elseif (substr($array['name'], -2) == '[]')
			$array['name'] = substr($array['name'], 0, -2);

		// Start the element.
		$output = $indentation . '<' . $array['name'];

		$inside_elements = false;
		$output_el = '';

		// Run through and recurively output all the elements or attrbutes inside this.
		foreach ($array as $k => $v)
		{
			if (substr($k, 0, 1) == '@')
				$output .= ' ' . substr($k, 1) . '="' . $v . '"';
			elseif (is_array($v))
			{
				$output_el .= $this->_xml($v, $indent === null ? null : $indent + 1);
				$inside_elements = true;
			}
		}

		// Indent, if necessary.... then close the tag.
		if ($inside_elements)
			$output .= '>' . $output_el . $indentation . '</' . $array['name'] . '>';
		else
			$output .= ' />';

		return $output;
	}

	// Return an element as an array...
	function _array($array)
	{
		$return = array();
		$text = '';
		foreach ($array as $value)
		{
			if (!is_array($value) || !isset($value['name']))
				continue;

			if ($value['name'] == '!')
				$text .= $value['value'];
			else
				$return[$value['name']] = $this->_array($value);
		}

		if (empty($return))
			return $text;
		else
			return $return;
	}

	// Parse out CDATA tags. (htmlspecialchars them...)
	function _to_cdata($data)
	{
		// Match all of the CDATA tags.
		preg_match_all('/<!\[CDATA\[(.*?)\]\]>/s', $data, $match, PREG_SET_ORDER);

		// Replace them with htmlentities'd versions.
		foreach ($match as $m)
			$data = str_replace($m[0], htmlentities($m[1], ENT_QUOTES), $data);

		return $data;
	}

	// Turn the CDATAs back to normal text.
	function _from_cdata($data)
	{
		// Get the HTML translation table and reverse it.
		$trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES));

		// Translate all the entities out.
		$data = strtr(preg_replace('~&#(\d{1,4});~e', "chr('\$1')", $data), $trans_tbl);

		return $this->trim ? trim($data) : $data;
	}

	// Given an array, return the text from that array. (recursive and privately used.)
	function _fetch($array)
	{
		// Don't return anything if this is just a string.
		if (is_string($array))
			return '';

		$temp = '';
		foreach ($array as $text)
		{
			// This means it's most likely an attribute or the name itself.
			if (!isset($text['name']))
				continue;

			// This is text!
			if ($text['name'] == '!')
				$temp .= $text['value'];
			// Another element - dive in ;).
			else
				$temp .= $this->_fetch($text);
		}

		// Return all the bits and pieces we've put together.
		return $temp;
	}

	// Get a specific array by path, one level down. (privately used...)
	function _path($array, $path, $level, $no_error = false)
	{
		// Is $array even an array?  It might be false!
		if (!is_array($array))
			return false;

		// Asking for *no* path?
		if ($path == '' || $path == '.')
			return $array;
		$paths = explode('|', $path);

		// A * means all elements of any name.
		$show_all = in_array('*', $paths);

		$results = array();

		// Check each element.
		foreach ($array as $value)
		{
			if (!is_array($value) || $value['name'] === '!')
				continue;

			if ($show_all || in_array($value['name'], $paths))
			{
				// Skip elements before "the one".
				if ($level !== null && $level > 0)
					$level--;
				else
					$results[] = $value;
			}
		}

		// No results found...
		if (empty($results))
		{
			// Cause an error.
			if ($this->debug_level & E_NOTICE && !$no_error)
				trigger_error('Undefined XML element: ' . $path, E_USER_NOTICE);
			return false;
		}
		// Only one result.
		elseif (count($results) == 1 || $level !== null)
			return $results[0];
		// Return the result set.
		else
			return $results + array('name' => $path . '[]');
	}
}

// http://www.faqs.org/rfcs/rfc959.html
class ftp_connection
{
	var $connection = 'no_connection', $error = false;

	// Create a new FTP connection...
	function ftp_connection($ftp_server, $ftp_port, $ftp_user, $ftp_pass)
	{
		// Connect to the FTP server.
		$this->connection = @fsockopen($ftp_server, $ftp_port, $err, $err, 5);
		if (!$this->connection)
		{
			$this->error = 'bad_server';
			return;
		}

		// Get the welcome message...
		if (!$this->check_response(220))
		{
			echo $this->error = 'bad_response';
			return;
		}

		// Send the username, it should ask for a password.
		fwrite($this->connection, 'USER ' . $ftp_user . "\r\n");
		if (!$this->check_response(331))
		{
			$this->error = 'bad_username';
			return;
		}

		// Now send the password... and hope it goes okay.
		fwrite($this->connection, 'PASS ' . $ftp_pass . "\r\n");
		if (!$this->check_response(230))
		{
			$this->error = 'bad_password';
			return;
		}
	}

	function chdir($ftp_path)
	{
		if (!is_resource($this->connection))
			return false;

		// No slash on the end, please...
		if (substr($ftp_path, -1) == '/')
			$ftp_path = substr($ftp_path, 0, -1);

		fwrite($this->connection, 'CWD ' . $ftp_path . "\r\n");
		if (!$this->check_response(250))
		{
			$this->error = 'bad_path';
			return false;
		}

		return true;
	}

	function chmod($ftp_file, $chmod)
	{
		if (!is_resource($this->connection))
			return false;

		// Convert the chmod value from octal (0777) to text ("777").
		fwrite($this->connection, 'SITE CHMOD ' . decoct($chmod) . ' ' . $ftp_file . "\r\n");
		if (!$this->check_response(200))
		{
			$this->error = 'bad_file';
			return false;
		}

		return true;
	}

	function unlink($ftp_file)
	{
		// We are actually connected, right?
		if (!is_resource($this->connection))
			return false;

		// Delete file X.
		fwrite($this->connection, 'DELE ' . $ftp_file . "\r\n");
		if (!$this->check_response(250))
		{
			$this->error = 'bad_file';
			return false;
		}

		return true;
	}

	function check_response($desired)
	{
		// Wait for a response that isn't continued with -, but don't wait too long.
		$time = time();
		do
			$response = fgets($this->connection, 1024);
		while (substr($response, 3, 1) != ' ' && time() - $time < 5);

		// Was the desired response returned?
		return substr($response, 0, 3) == $desired;
	}

	function close()
	{
		// Goodbye!
		fwrite($this->connection, "QUIT\r\n");
		fclose($this->connection);

		return true;
	}
}

?>
