<?php
/**
 * Skeleton Test class file
 *
 * @package PhpSecInfo
 * @author Ed Finkler <coj@funkatron.com>
 */

/**
 * require the main PhpSecInfo class
 */
require_once(PHPSECINFO_BASE_DIR.'/PhpSecInfo.php');



define ('PHPSECINFO_TEST_RESULT_OK', -1);

define ('PHPSECINFO_TEST_RESULT_NOTICE', -2);

define ('PHPSECINFO_TEST_RESULT_WARN', -4);

define ('PHPSECINFO_TEST_RESULT_ERROR', -1024);

define ('PHPSECINFO_TEST_RESULT_NOTRUN', -2048);

define ('PHPSECINFO_TEST_COMMON_TMPDIR', '/tmp');

define ('PHPSECINFO_TEST_MOREINFO_BASEURL', 'http://phpsec.org/projects/phpsecinfo/tests/');

/**
 * This is a skeleton class for PhpSecInfo tests  You should extend this to make a "group" skeleton
 * to categorize tests under, then make a subdir with your group name that contains test classes
 * extending your group skeleton class.
 * @package PhpSecInfo
 */
class PhpSecInfo_Test
{

	/**
	 * This value is used to group test results together.
	 *
	 * For example, all tests related to the mysql lib should be grouped under "mysql."
	 *
	 * @var string
	 */
	var $test_group = 'misc';


	/**
	 * This should be a <b>unique</b>, human-readable identifier for this test
	 *
	 * @var string
	 */
	var $test_name  = 'misc_test';


	/**
	 * This is the recommended value the test will be looking for
	 *
	 * @var mixed
	 */
	var $recommended_value = "bar";


	/**
	 * The result returned from the test
	 *
	 * @var integer
	 */
	var $_result = PHPSECINFO_TEST_RESULT_NOTRUN;


	/**
	 * The message corresponding to the result of the test
	 *
	 * @var string
	 */
	var $_message;


	/**
	 * the language code.  Should be a pointer to the setting in the PhpSecInfo object
	 *
	 * @var string
	 */
	var $_language = PHPSECINFO_LANG_DEFAULT;

	/**
	 * Enter description here...
	 *
	 * @var mixed
	 */
	var $current_value;

	/**
	 * This is a hash of messages that correspond to various test result levels.
	 *
	 * There are five messages, each corresponding to one of the result constants
	 * (PHPSECINFO_TEST_RESULT_OK, PHPSECINFO_TEST_RESULT_NOTICE, PHPSECINFO_TEST_RESULT_WARN,
	 * PHPSECINFO_TEST_RESULT_ERROR, PHPSECINFO_TEST_RESULT_NOTRUN)
	 *
	 *
	 * @var array array
	 */
	var $_messages = array();




	/**
	 * Constructor for Test skeleton class
	 *
	 * @return PhpSecInfo_Test
	 */
	function PhpSecInfo_Test() {
		//$this->_setTestValues();

		$this->_retrieveCurrentValue();
		//$this->setRecommendedValue();

		$this->_setMessages();
	}


	/**
	 * Determines whether or not it's appropriate to run this test (for example, if
	 * this test is for a particular library, it shouldn't be run if the lib isn't
	 * loaded).
	 *
	 * This is a terrible name, but I couldn't think of a better one atm.
	 *
	 * @return boolean
	 */
	function isTestable() {

		return true;
	}


	/**
	 * The "meat" of the test.  This is where the real test code goes.  You should override this when extending
	 *
	 * @return integer
	 */
	function _execTest() {

		return PHPSECINFO_TEST_RESULT_NOTRUN;
	}


	/**
	 * This function loads up result messages into the $this->_messages array.
	 *
	 * Using this method rather than setting $this->_messages directly allows result
	 * messages to be inherited.  This is broken out into a separate function rather
	 * than the constructor for ease of extension purposes (php4 is whack, man).
	 *
	 */
	function _setMessages() {
		$this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK,		'en', 'This setting should be safe');
		$this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE,	'en', 'This could potentially be a security issue');
		$this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN,	'en', 'This setting may be a serious security problem');
		$this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR,	'en', 'There was an error running this test');
		$this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN,	'en', 'This test cannot be run');
	}


	/**
	 * Placeholder - extend for tests
	 *
	 */
	function _retrieveCurrentValue() {
		$this->current_value = "foo";
	}



	/**
	 * This is the wrapper that executes the test and sets the result code and message
	 */
	function test() {
		$result = $this->_execTest();
		$this->_setResult($result);

	}



	/**
	 * Retrieves the result
	 *
	 * @return integer
	 */
	function getResult() {
		return $this->_result;
	}




	/**
	 * Retrieves the message for the current result
	 *
	 * @return string
	 */
	function getMessage() {
		if (!isset($this->_message) || empty($this->_message)) {
			$this->_setMessage($this->_result, $this->_language);
		}

		return $this->_message;
	}



	/**
	 * Sets the message for a given result code and language
	 *
	 * <code>
	 * $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN,	'en', 'This test cannot be run');
	 * </code>
	 *
	 * @param integer $result_code
	 * @param string $language_code
	 * @param string $message
	 *
	 */
	function setMessageForResult($result_code, $language_code, $message) {

		if ( !isset($this->_messages[$result_code]) ) {
			$this->_messages[$result_code] = array();
		}

		if ( !is_array($this->_messages[$result_code]) ) {
			$this->_messages[$result_code] = array();
		}

		$this->_messages[$result_code][$language_code] = $message;

	}




	/**
	 * returns the current value.  This function should be used to access the
	 * value for display.  All values are cast as strings
	 *
	 * @return string
	 */
	function getCurrentTestValue() {
		return $this->getStringValue($this->current_value);
	}

	/**
	 * returns the recommended value.  This function should be used to access the
	 * value for display.  All values are cast as strings
	 *
	 * @return string
	 */
	function getRecommendedTestValue() {
		return $this->getStringValue($this->recommended_value);
	}


	/**
	 * Sets the result code
	 *
	 * @param integer $result_code
	 */
	function _setResult($result_code) {
		$this->_result = $result_code;
	}


	/**
	 * Sets the $this->_message variable based on the passed result and language codes
	 *
	 * @param integer $result_code
	 * @param string $language_code
	 */
	function _setMessage($result_code, $language_code) {
		$messages = $this->_messages[$result_code];
		$message  = $messages[$language_code];
		$this->_message = $message;
	}


	/**
	 * Returns a link to a page with detailed information about the test
	 *
	 * URL is formatted as PHPSECINFO_TEST_MOREINFO_BASEURL + testName
	 *
	 * @see PHPSECINFO_TEST_MOREINFO_BASEURL
	 *
	 * @return string|boolean
	 */
	function getMoreInfoURL() {
		if ($tn = $this->getTestName()) {
			return PHPSECINFO_TEST_MOREINFO_BASEURL.strtolower("{$tn}.html");
		} else {
			return false;
		}
	}




	/**
	 * This retrieves the name of this test.
	 *
	 * If a name has not been set, this returns a formatted version of the class name.
	 *
	 * @return string
	 */
	function getTestName() {
		if (isset($this->test_name) && !empty($this->test_name)) {
			return $this->test_name;
		} else {
			return ucwords(
			str_replace('_', ' ',
			get_class($this)
			)
			);
		}

	}


	/**
	 * sets the test name
	 *
	 * @param string $test_name
	 */
	function setTestName($test_name) {
		$this->test_name = $test_name;
	}


	/**
	 * Returns the test group this test belongs to
	 *
	 * @return string
	 */
	function getTestGroup() {
		return $this->test_group;
	}


	/**
	 * sets the test group
	 *
	 * @param string $test_group
	 */
	function setTestGroup($test_group) {
		$this->test_group = $test_group;
	}


	/**
	 * This function takes the shorthand notation used in memory limit settings for PHP
	 * and returns the byte value.  Totally stolen from http://us3.php.net/manual/en/function.ini-get.php
	 *
	 * <code>
	 * echo 'post_max_size in bytes = ' . $this->return_bytes(ini_get('post_max_size'));
	 * </code>
	 *
	 * @link http://php.net/manual/en/function.ini-get.php
	 * @param string $val
	 * @return integer
	 */
	function returnBytes($val) {
		$val = trim($val);

		if ( (int)$val === 0 ) {
			return 0;
		}

		$last = strtolower($val{strlen($val)-1});
		switch($last) {
			// The 'G' modifier is available since PHP 5.1.0
			case 'g':
				$val *= 1024;
			case 'm':
				$val *= 1024;
			case 'k':
				$val *= 1024;
		}

		return $val;
	}


	/**
	 * This just does the usual PHP string casting, except for
	 * the boolean FALSE value, where the string "0" is returned
	 * instead of an empty string
	 *
	 * @param mixed $val
	 * @return string
	 */
	function getStringValue($val) {
		if ($val === FALSE) {
			return "0";
		} else {
			return (string)$val;
		}
	}


	/**
	 * This method converts the several possible return values from
	 * allegedly "boolean" ini settings to proper booleans
	 *
	 * Properly converted input values are: 'off', 'on', 'false', 'true', '', '0', '1'
	 * (the last two might not be neccessary, but I'd rather be safe)
	 *
	 * If the ini_value doesn't match any of those, the value is returned as-is.
	 *
	 * @param string $ini_key   the ini_key you need the value of
	 * @return boolean|mixed
	 */
	function getBooleanIniValue($ini_key) {

		$ini_val = ini_get($ini_key);

		switch ( strtolower($ini_val) ) {

			case 'off':
				return false;
				break;
			case 'on':
				return true;
				break;
			case 'false':
				return false;
				break;
			case 'true':
				return true;
				break;
			case '0':
				return false;
				break;
			case '1':
				return true;
				break;
			case '':
				return false;
				break;
			default:
				return $ini_val;

		}

	}

	/**
	 * sys_get_temp_dir provides some temp dir detection capability
	 * that is lacking in versions of PHP that do not have the
	 * sys_get_temp_dir() function
	 *
	 * @return string|NULL
	 */
	function sys_get_temp_dir() {
		// Try to get from environment variable
		if ( !empty($_ENV['TMP']) ) {
			return realpath( $_ENV['TMP'] );
		} else if ( !empty($_ENV['TMPDIR']) ) {
			return realpath( $_ENV['TMPDIR'] );
		} else if ( !empty($_ENV['TEMP']) ) {
			return realpath( $_ENV['TEMP'] );
		} else {
			return NULL;
		}
	}


	/**
	 * A quick function to determine whether we're running on Windows.
	 * Uses the PHP_OS constant.
	 *
	 * @return boolean
	 */
	function osIsWindows() {
		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
			return true;
		} else {
			return false;
		}
	}


	/**
	 * Returns an array of data returned from the UNIX 'id' command
	 *
	 * includes uid, username, gid, groupname, and groups (if "exec"
	 * is enabled). Groups is an array of all the groups the user
	 * belongs to.  Keys are the group ids, values are the group names.
	 *
	 * returns FALSE if no suitable function is available to retrieve
	 * the data
	 *
	 * @return array|boolean
	 */
	function getUnixId() {

		if ($this->osIsWindows()) {
			return false;
		}

		$success = false;


		if (function_exists("exec") && !PhpSecInfo_Test::getBooleanIniValue('safe_mode')) {
			$id_raw = exec('id');
			// uid=1000(coj) gid=1000(coj) groups=1000(coj),1001(admin)
			preg_match( "|uid=(\d+)\((\S+)\)\s+gid=(\d+)\((\S+)\)\s+groups=(.+)|i",
						$id_raw,
						$matches);

			if (!$matches) {
				/**
				 * for some reason the output from 'id' wasn't as we expected.
				 * return false so the test doesn't run.
				 */
				$success = false;
			} else {
				$id_data = array(	'uid'=>$matches[1],
									'username'=>$matches[2],
									'gid'=>$matches[3],
									'group'=>$matches[4] );

				if ($matches[5]) {
					$gs = $matches[5];
					$gs = explode(',', $gs);
					foreach ($gs as $groupstr) {
						preg_match("/(\d+)\(([^\)]+)\)/", $groupstr, $subs);
						$groups[$subs[1]] = $subs[2];
					}
					ksort($groups);
					$id_data['groups'] = $groups;
				}
				$success = true;
			}

		}

		if (!$success && function_exists("posix_getpwuid") && function_exists("posix_geteuid")
			&& function_exists('posix_getgrgid') && function_exists('posix_getgroups') ) {
			$data = posix_getpwuid( posix_getuid() );
			$id_data['uid'] = $data['uid'];
			$id_data['username'] = $data['name'];
			$id_data['gid'] = $data['gid'];
			//$group_data = posix_getgrgid( posix_getegid() );
			//$id_data['group'] = $group_data['name'];
			$groups = posix_getgroups();
			foreach ( $groups as $gid ) {
				//$group_data = posix_getgrgid(posix_getgid());
				$id_data['groups'][$gid] = '<unknown>';
			}
			$success = true;
		}

		if ($success) {
			return $id_data;
		} else {
			return false;
		}
	}

}
