<?php
/*
* WebCompiler Compiler Handling
*
* Copyright (C) 2004-2005 David "BAILOPAN" Anderson and the AMX Mod X Development Team
* Copyright (C) 2005-2010 NiLuJe <niluje@ak-team.com>
*
* $Id: Compiler.class.php 6077 2010-04-20 22:26:49Z NiLuJe $
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*
*/

include 'compiler.config.inc.php';

init_db();

$cwd = getcwd();
define( 'COMPILERS_PATH',	$cwd.'/bin' );
define( 'TEMP_OUTPATH',		$cwd.'/tmp/bin' );
define( 'PLUG_SRCPATH',		$cwd.'/plug/src' );
define( 'TEMP_SRCPATH',		$cwd.'/tmp/src' );
define( 'PLUG_OUTPATH',		$cwd.'/plug/bin' );
define( 'SMA_TEMP',		0 );
define( 'SMA_PLUGIN',		1 );
/* Define le rep du compilo selon la version d'amx */
define( 'COMPILER_PATH',	COMPILERS_PATH.'/'.$BIN_DIR );
/* Use schedtool to renice/resched the compiler process (requires the schedtool util) */
define( 'USE_SCHEDTOOL',	1 );
//define( 'USE_SCHEDTOOL',	0 );
/* Configure schedtool arguments (default is '-D'  [SCHED_IDLE scheduling policy]) */
/* If you think SCHED_IDLE is a bit too aggressive (can only use free cpu time), you may want to switch to SCHED_BATCH + nice 19 (-B -n 19) */
define( 'SCHEDTOOL_ARGS',	'-D' );
/* OR Use nice to renice the compiler process */
define( 'USE_NICE',	0 );
//define( 'USE_NICE',	1 );
/* Nice priority level (default is '-n 19', lowest priority) */
define( 'NICE_ARGS',	'-n 19' );
/* Configure the timeout (in seconds), after which the compiler process should be killed if it's still running */
define( 'COMPILER_TIMEOUT',     20 );

///////////////////////////////
//// HELPER FUNCTIONS HERE ////
///////////////////////////////

function OutputAmxx($phys_file, $filename)
{
	$HTTP_USER_AGENT = '';
	if (!empty($_SERVER['HTTP_USER_AGENT']))
	{
		$HTTP_USER_AGENT = $_SERVER['HTTP_USER_AGENT'];
	}
	else if (!empty($HTTP_SERVER_VARS['HTTP_USER_AGENT']))
	{
		$HTTP_USER_AGENT = $HTTP_SERVER_VARS['HTTP_USER_AGENT'];
	}
	else if (!isset($HTTP_USER_AGENT))
	{
		$HTTP_USER_AGENT = '';
	}

	if (ereg('Opera(/| )([0-9].[0-9]{1,2})', $HTTP_USER_AGENT, $log_version))
	{
		$browser_version = $log_version[2];
		$browser_agent = 'opera';
	}
	else if (ereg('MSIE ([0-9].[0-9]{1,2})', $HTTP_USER_AGENT, $log_version))
	{
		$browser_version = $log_version[1];
		$browser_agent = 'ie';
	}
	else if (ereg('OmniWeb/([0-9].[0-9]{1,2})', $HTTP_USER_AGENT, $log_version))
	{
		$browser_version = $log_version[1];
		$browser_agent = 'omniweb';
	}
	else if (ereg('Netscape([0-9]{1})', $HTTP_USER_AGENT, $log_version))
	{
		$browser_version = $log_version[1];
		$browser_agent = 'netscape';
	}
	else if (ereg('Mozilla/([0-9].[0-9]{1,2})', $HTTP_USER_AGENT, $log_version))
	{
		$browser_version = $log_version[1];
		$browser_agent = 'mozilla';
	}
	else if (ereg('Konqueror/([0-9].[0-9]{1,2})', $HTTP_USER_AGENT, $log_version))
	{
		$browser_version = $log_version[1];
		$browser_agent = 'konqueror';
	}
	else
	{
		$browser_version = 0;
		$browser_agent = 'other';
	}

	// Correct the mime type - we force application/octetstream for all files, except images
	// Please do not change this, it is a security precaution
	$mimetype = ($browser_agent == 'ie' || $browser_agent == 'opera') ? 'application/octetstream' : 'application/octet-stream';

	// Now the tricky part... let's dance
	@ob_end_clean();
	@ini_set('zlib.output_compression', 'Off');
	header('Pragma: public');
	header('Content-Transfer-Encoding: none');

	// Send out the Headers
	header('Content-Type: ' . $mimetype . '; name="' . $filename . '"');
	header('Content-Disposition: inline; filename="' . $filename . '"');

	$size = @filesize($phys_file);
	if ($size)
	{
		header('Content-length: '.$size);
	}

	readfile($phys_file);
}

class Compiler
{
	var $source;
	var $bin;
	var $output;

	function Compiler($path, $bin, $name)
	{
		$this->name = $name;
		$this->bin = $bin;
		$this->source = $path;
	}

	function Compile($save_output)
	{
		global $BIN_NAME, $BIN_EXT, $AMX_VERSION, $DBG_LVL, $cwd;

		if (file_exists($this->bin))
			unlink($this->bin);


		/* Construction de la ligne de commande, avec bon compilo & bon niveau de debug, plus le support schedtool ou nice */
		if ( USE_SCHEDTOOL != 0 )
		{
			$cmd_prefix = 'schedtool '.SCHEDTOOL_ARGS.' -e ./';
		}
		elseif ( USE_NICE != 0 )
		{
			$cmd_prefix = 'nice '.NICE_ARGS.' ./';
		}
		else
		{
			$cmd_prefix = './';
		}
		/**
		 * Si AMX Mod X, on oublie le -d (Cf. http://forums.alliedmods.net/showthread.php?p=403223#post403223)
		 * Ne surtout pas forcer de redirections stdin/stderr, ça lancerait la commande à travers un wrapper shell,
		 * du coup proc_get_status choppe le pid du wrapper, pas du compilo, du coup le script reaper kill pas
		 * le bon process, et on l'a dans le derrière.
		 */

		if ( $BIN_NAME == "amxxpc" )
			$exec = $cmd_prefix.$BIN_NAME.' -i'.COMPILER_PATH.' '.$this->source.' -o'.$this->bin;
		else
			$exec = $cmd_prefix.$BIN_NAME.' -d'.$DBG_LVL.' -i'.COMPILER_PATH.' '.$this->source.' -o'.$this->bin;

		/* Check if compiler exists */
		if ( ! file_exists( COMPILER_PATH.'/'.$BIN_NAME ) )
		{
			echo '<p><span class="error">';
			printf( _('ERROR: Compiler for "%s" not found!'), $AMX_VERSION );
			echo '</span><br/><br/></p>'."\n";
			return false;
		}

		/* Launch the beast! */
		$descriptorspec = array(
			0 => array("pipe", "r"),  // stdin
			1 => array("pipe", "w"),  // stdout
			2 => array("pipe", "w")   // stderr
		);
		$process = proc_open( $exec, $descriptorspec, $pipes, COMPILER_PATH, NULL);	// PHP5! (Last two arguments, the first being cwd)
		if ( is_resource( $process ) ) {
			/* Get our process PID */
			$proc_info = proc_get_status( $process );
			$pid = $proc_info['pid'];

			/* Timeout setting & Sanity Checks */
			$webcomp_tout = COMPILER_TIMEOUT;
			if ( ! is_int( $webcomp_tout ) )
				$webcomp_tout = 20;
			if ( ! is_int( $pid ) )
				$pid = '$$';

			/* Lance un script en bg qui check le pid. Cf commentaires plus bas. */
			$reaper_bg = popen( "$cwd/webcomp_reaper.sh '$pid' '$webcomp_tout' &", "w" );
			pclose( $reaper_bg );

			/**
			 * fgets, stream_get_contents & cie freezent lorsque amxxpc part en boucle infinie, on ne peut donc pas compter sur eux
			 * pour détecter une boucle infinie ou bien gérer un système de timeout directement en PHP...
			 * D'ou le recours à mon pote the reaper, un script shell tout simple qui va vérifier toutes les secondes si le compilo
			 * tourne toujours, et prendre les mesures nécessaires si le timeout est atteint...
			 * On ne peut pas non plus ruser avec des redirections vers des fichiers, vu qu'ils ne sont écrits qu'au proc_close/pclose...
			 * Je n'ai donc trouvé aucun moyen d'éviter nativement en PHP la boucle infinie,
			 * d'où le recours au script bash lancé en background...
			 */
			$this->output = stream_get_contents($pipes[1]);
			fclose($pipes[1]);

			/* Get stderr content. Only useful with older compilers. After a newline for clarity. */
			if ( $BIN_NAME == "sc" )
			{
				$this->output .= "\n";
				$this->output .= stream_get_contents($pipes[2]);
				fclose($pipes[2]);
			}

			$ret_val = proc_close($process);
		}

		/* Catch Segfault */
		if ( $ret_val == 11 || $ret_val == 139 )	// 128 + 11 (SIGSEGV)
		{
			$this->output = _("Awww! The compiler crashed! (Segfault)\nSomething seems to be very wrong in your code...\nIf you're sure that your code is correct,\nyou may have discovered a compiler bug...")."\n".$this->output;

			/* Les vieux compilos Small ont du mal en debug >= 2, affiche un peu d'infos la dessus... */
			if ( $BIN_NAME == "sc" && $DBG_LVL >= 2 )
				$this->output .= "\n\n".sprintf( _("You were using a debug level superior or equal to 2 (%d),\nit has been proved to be quite unstable in combination with the compiler you used.\nYou may have more luck using a saner debug level, like 1 (compiler's default) or 0."), $DBG_LVL );

			$crashed = true;	// Mark as crashed, the compiler may have left a non-empty binary even if it crashed...
			$this->output = sanitize_output( $this->output, $this->source, $this->name );	// We want to show the output, so we need to clean it before.
		}
		/* Catch command not found (schedtool) */
		elseif ( $ret_val == 127 )
		{
			$this->output = _("ERROR: Command not found!\nPlease contact the administrator!\n(Hint: The define USE_SCHEDTOOL in Compiler.class.php is set to 1 but schedtool is not installed...)");
		}
		/* Catch permission problems */
		elseif ( $ret_val == 126 )
		{
			$this->output = _("ERROR: Permission issue!\nPlease contact the administrator!\n(Hint: See the README.txt for more info...)");
		}
		/* Catch timeout kill (by webcomp_reaper.sh) */
		elseif ( $ret_val == 9 || $ret_val == 137 )	// 128 + 9 (SIGKILL)
		{
			$this->output = sprintf( _("ERROR: The \"%s\" compiler process has been killed, because it has been running for more than %d seconds.\nEither your code is horribly broken, and made the compiler crash in an infinite loop,\nor it's too complex to compile in such a short time.\nEither way, this is done to spare CPU power on this webserver, so,\nif you're certain your code should compile, do it locally."), $AMX_VERSION, $webcomp_tout );
		}
		/* Catch compaction buffer overflow error, for compilers who doesn't segfault on this... */
		elseif ( $ret_val == 2 && $BIN_NAME == "sc" && $DBG_LVL >= 2 && strpos( $this->output, "fatal error 106: compaction buffer overflow" ) !== false )
		{
			$this->output .= "\n\n".sprintf( _("You were using a debug level superior or equal to 2 (%d),\nit has been proved to be quite unstable in combination with the compiler you used.\nYou may have more luck using a saner debug level, like 1 (compiler's default) or 0."), $DBG_LVL );
			$crashed = true;
			$this->output = sanitize_output( $this->output, $this->source, $this->name );
		}
		/* Catch missing includes, some compilers don't alter their return value in this case, and even output a semi-valid non-null compiled file... */
		elseif ( strpos( $this->output, "fatal error 100: cannot read from file" ) !== false )	// NOTE: We don't check ret_value, because while some compilers may correctly alter their return value, some others don't...
		{
			$this->output .= "\n\n"._("ERROR: The plugin you tried to compile needs a 3rd party include file.\nThis is not supported by this webcompiler.\nPlease use a local compiler.");
			$crashed = true;
			$this->output = sanitize_output( $this->output, $this->source, $this->name );
		}
		/* Catch funky failed compiles outputting > 0 octet broken compiled file with compilers not setting a return value... NOTE: Needs to be the Last resort, we don't want to break our shiny previous detailed errors catching :) */
		elseif ( strpos( $this->output, "Compilation aborted." ) !== false && file_exists($this->bin) && filesize($this->bin) > 0 )	// NOTE: We don't check ret_value, because while some compilers may correctly alter their return value (and set it to 2), some others don't...
		{
			$this->output .= "\n\n"._("ERROR: Your plugin failed to compile, see above.\nPlease note that the silly compiler still tried to output a compiled plugin, but since it would be broken,\nthe webcompiler caught it, and won't let you download it.");
			$crashed = true;
			$this->output = sanitize_output( $this->output, $this->source, $this->name );
		}
		/* And finally, catch most of the other possible failures based on the error messages, without relying on return status or existence of an output file... */
		elseif ( strpos( $this->output, "Compilation aborted." ) !== false || strpos( $this->output, "(compile failed)." ) !== false )
		{
			// NOTE: This may be a bit redundant with the following check to se if there's a valid (> 0 octet) output file
			$this->output .= "\n\n"._("ERROR: Your plugin failed to compile, see above.");
			$crashed = true;
			$this->output = sanitize_output( $this->output, $this->source, $this->name );
		}
		else
		{
			if ( $save_output )	// $save_output is always true in our case, in order to always be able to show compiler output in case of failure
			{
				/* On prépare notre output pour supprimer les infos sensibles, et afficher le `vrai` nom du plugin */
				$this->output = sanitize_output( $this->output, $this->source, $this->name );
			}
			else
				$this->output = $this->bin;
		}

		if (file_exists($this->bin) && filesize($this->bin) > 0 && !$crashed)
		{
			return true;
		} else {
			if (file_exists($this->bin))
				unlink($this->bin);
			/* Si amxx, del aussi tempfile en .amx */
			if ( $BIN_EXT == '.amxx' )
			{
				$tempfile = str_replace('.amxx', '.amx', $this->bin);
				if (file_exists($tempfile))
					unlink($tempfile);
			}
			return false;
		}
	}
}

function strip_filename($fname)
{
	if ( ! $fname )
	{
		return false;
	}
	else
	{
		preg_match('/([^\/]+)$/', $fname, $matches);
		return $matches[0];
	}
}

/* Préparation de l'output du compilo */
function sanitize_output( $string, $tempname, $real_name )
{
	global $BIN_EXT;

	$path_patterns = array( TEMP_OUTPATH.'/', TEMP_SRCPATH.'/', PLUG_OUTPATH.'/', PLUG_SRCPATH.'/', COMPILERS_PATH.'/' );
	$plug_patterns = array( TEMP_SRCPATH.'/', PLUG_SRCPATH.'/', '.sma', $BIN_EXT  );
	$ext_patterns = array( '.sma', $BIN_EXT  );

	$string = str_replace( $path_patterns, '', $string );		// On dégage les path de l'output.
	$tempname = str_replace( $plug_patterns, '', $tempname );	// On dégage les path & extension du plugin.
	$real_name = str_replace( $ext_patterns, '', $real_name );	// On dégage l'extension du nom réel, vu qu'elle est déjà dans l'output
	$string = str_replace ( $tempname, $real_name, $string );	// On remplace le nom bidon par le vrai nom.

	return $string;
}

//physical plugin filename to output path
function GetOutputPath($file, $mode)
{
	global $BIN_EXT;

	$obj  = str_replace('.sma', $BIN_EXT, strip_filename($file));

	$out = '';
	if ($mode==SMA_PLUGIN)
	{
		$out = PLUG_OUTPATH;
	} else {
		$out = TEMP_OUTPATH;
	}
	$out = $out.'/'.$obj;
	return $out;
}
?>
