<?php
/*
	patches.inc
	Copyright (C) 2012 Jim Pingle
	All rights reserved.

	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions are met:

	1. Redistributions of source code must retain the above copyright notice,
	   this list of conditions and the following disclaimer.

	2. Redistributions in binary form must reproduce the above copyright
	   notice, this list of conditions and the following disclaimer in the
	   documentation and/or other materials provided with the distribution.

	THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
	INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
	AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
	OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
	POSSIBILITY OF SUCH DAMAGE.
*/

require_once("globals.inc");
require_once("util.inc");

$git_root_url = "http://github.com/pfsense/pfsense/commit/";
$patch_suffix = ".patch";
$patch_dir = "/var/patches";
$patch_cmd = "/usr/bin/patch";

function patch_package_install() {
	patch_add_shellcmd();
}

function patch_package_deinstall() {
	patch_remove_shellcmd();
}

function patch_commit($patch, $action, $test=false, $fulldetail=false) {
	global $patch_dir, $patch_cmd, $patch_suffix;
	$directory = empty($patch['basedir']) ? "/" : $patch['basedir'];
	$filename = '-i ' . $patch_dir . '/' . $patch['uniqid'] . $patch_suffix;
	$check = ($test) ? "--check" : "";
	$force = ($action == "revert") ? "-f" : "-t";
	$direction = ($action == "revert") ? "--reverse" : "--forward";
	$whitespace = isset($patch['ignorewhitespace']) ? "--ignore-whitespace" : "";
	$pathstrip = '-p' . $patch['pathstrip'];
	$full_patch_command = "{$patch_cmd} --directory={$directory} {$force} {$pathstrip} {$filename} {$check} {$direction} {$whitespace}";
	patch_write($patch);
	if (!$fulldetail)
		$output = (mwexec($full_patch_command, true) == 0);
	else
		$output = $full_patch_command . "\n\n" . shell_exec($full_patch_command . ' 2>&1');
	patch_erase($patch);
	return $output;
}

/* Attempt to apply a patch */
function patch_apply($patch) {
	return patch_commit($patch, "apply", false);
}

/* Attempt to revert a patch */
function patch_revert($patch) {
	return patch_commit($patch, "revert", false);
}

/* Test if a patch would apply cleanly */
function patch_test_apply($patch, $fulldetail=false) {
	return patch_commit($patch, "apply", true, $fulldetail);
}

/* Test if a patch would revert cleanly */
function patch_test_revert($patch, $fulldetail=false) {
	return patch_commit($patch, "revert", true, $fulldetail);
}

/* Fetch a patch from a URL or github */
function patch_fetch(& $patch) {
	$url = patch_fixup_url($patch['location']);
	$text = @file_get_contents($url);
	if (empty($text)) {
		return false;
	} else {
		$patch['patch'] = base64_encode($text);
		write_config("Fetched patch {$patch['descr']}");
		return true;
	}
}

/* Write a patch file out to $patch_dir */
function patch_write($patch) {
	global $patch_dir, $patch_suffix;
	if (!file_exists($patch_dir)) {
		safe_mkdir($patch_dir);
	}
	if (empty($patch['patch'])) {
		return false;
	} else {
		$text = base64_decode($patch['patch']);
		$filename = $patch_dir . '/' . $patch['uniqid'] . $patch_suffix;
		return (file_put_contents($filename, $text) > 0);
	}
}

function patch_erase($patch) {
	global $patch_dir, $patch_suffix;
	if (!file_exists($patch_dir)) {
		return true;
	}
	$filename = $patch_dir . '/' . $patch['uniqid'] . $patch_suffix;
	return @unlink($filename);
}

/* Detect a github URL or commit ID and fix it up */
function patch_fixup_url($url) {
	global $git_root_url, $patch_suffix;
	// If it's a commit id then prepend git url, and add .patch
	if (is_commit_id($url)) {
		$url = $git_root_url . $url . $patch_suffix;
	} elseif (is_URL($url)) {
		$urlbits = explode("/", $url);
		if (substr($urlbits[2], -10) == "github.com") {
			// If it's a github url and does not already end in .patch, add it
			if (substr($url, -strlen($patch_suffix)) != $patch_suffix) {
				// Make sure it's really a URL to a commit id before adding .patch
				if (is_commit_id(array_pop($urlbits))) {
					$url .= $patch_suffix;
				}
			}
		}
	}
	return $url;
}

function is_commit_id($str) {
	return preg_match("/^[0-9a-f]{5,40}$/", $str);
}

function is_github_url($url) {
	$urlbits = explode("/", $url);
	return (substr($urlbits[2], -10) == "github.com");
}

function bootup_apply_patches() {
	global $config;

	$a_patches = &$config['installedpackages']['patches']['item'];

	foreach ($a_patches as $patch) {
		/* Skip the patch if it should not be automatically applied. */
		if (!isset($patch['autoapply']))
			continue;
		/* If the patch can be reverted it is already applied, so skip it. */
		if (!patch_test_revert($patch)) {
			/* Only attempt to apply if it can be applied. */
			if (patch_test_apply($patch)) {
				patch_apply($patch);
			}
		}
	}
}

function patch_add_shellcmd() {
	global $config;
	$a_earlyshellcmd = &$config['system']['earlyshellcmd'];
	if (!is_array($a_earlyshellcmd))
		$a_earlyshellcmd = array();
	$found = false;
	foreach ($a_earlyshellcmd as $idx => $cmd)
		if (stristr($cmd, "apply_patches.php"))
			$found = true;
	if (!$found) {
		$a_earlyshellcmd[] = "/usr/local/bin/php -f /usr/local/bin/apply_patches.php";
		write_config("System Patches package added a shellcmd");
	}
}

function patch_remove_shellcmd() {
	global $config;
	$a_earlyshellcmd = &$config['system']['earlyshellcmd'];
	if (!is_array($a_earlyshellcmd))
		$a_earlyshellcmd = array();
	$removed = false;
	foreach ($a_earlyshellcmd as $idx => $cmd) {
		if (stristr($cmd, "apply_patches.php")) {
			unset($a_earlyshellcmd[$idx]);
			$removed = true;
		}
	}
	if ($removed)
		write_config("System Patches package removed a shellcmd");
}

?>