Code: Select all
<?php
class Extension {
var $filename;
var $rules;
var $directive;
var $_compiled;
function Extension($filename, $rules, $directive = null) {
$this->filename = $filename;
$this->rules = $rules;
$this->directive = $directive;
}
function getFilename() {return $this->filename;}
function getRules() {return $this->rules;}
function getDirective() {return $this->directive;}
function newFromFile($file) {
$string = file_get_contents($file);
return Extension::newFromString($string);
}
function newFromString($string) {
$lines = explode("\n", $string);
$extensions = array();
foreach ($lines as $line) {
if (!($line = trim($line))) continue;
if (($comment_start = strpos($line, ';')) !== false) {
//strip out comment
$line = substr($line, 0, $comment_start);
}
if (!($line = trim($line))) continue;
$bits = explode('/', $line);
$filename = trim($bits[0]);
$rules = preg_split('/\s+/', trim($bits[1]));
$directive = isset($bits[2]) ? trim($bits[2]) : null;
$extensions[] =& new Extension($filename, $rules, $directive);
}
return $extensions;
}
function canLoad($version) {
if (empty($_compiled)) $this->_compiled = $this->_compileRules($this->rules);
$compiled_version = $this->_compileVersion($version);
//first, determine if the value is excluded, if it is, immediately false
$excluded = $this->_matchesCompiledRules($compiled_version,
$this->_compiled['exclude']);
if ($excluded) return false;
//now, check if it's in the forward
if (!empty($this->_compiled['forward'])) {
$result = version_compare($version, $this->_compiled['forward'],
'>=');
if ($result) return true;
}
//now, check if it matches any of the inclusion ones
$included = $this->_matchesCompiledRules($compiled_version,
$this->_compiled['include']);
return $included;
}
function _compileRules($rules) {
$return = array('include' => array(), 'exclude' => array(),
'forward' => null);
foreach ($rules as $rule) {
//is exclusion?
if ($rule[0] == '!') {
$return['exclude'][] = $this->_compileVersion(substr($rule, 1));
continue;
}
//is forward?
if ($rule[strlen($rule)-1] == '+') {
$return['forward'] = substr($rule, 0, strlen($rule)-1);
continue;
}
//ah, it's inclusion
$return['include'][] = $this->_compileVersion($rule);
}
return $return;
}
function _compileVersion($version) {
$version = preg_replace('/([^0-9\.]+)/', '.$1.', $version);
$version = str_replace(array('-','_','+','..'), '.', $version);
return explode('.', $version);
}
function _matchesCompiledRules($compiled_version, $compiled_rules) {
foreach($compiled_rules as $rule) {
$matches = true;
foreach($rule as $key => $number) {
if (!isset($compiled_version[$key])) break;
if ($compiled_version[$key] != $number) {
$matches = false;
break;
}
}
if ($matches) {
return true;
}
}
return false;
}
function toIniString() {
return ($this->directive ? $this->directive : 'extension') . ' = ' . $this->filename;
}
}
?>Code: Select all
<?php
class ExtensionTestCase extends UnitTestCase
{
function test_newFromString() {
//regular case
$input[0] = 'php_iconv.dll / 4';
$expect[0] = array(new Extension('php_iconv.dll', array('4')));
$input[1] = 'php_apc.dll / 4.3.11 4.4.0 5.1+';
$expect[1] = array(new Extension('php_apc.dll', array('4.3.11', '4.4.0', '5.1+')));
$input[2] = 'php_zip.dll / 4+ !6.0.0-dev';
$expect[2] = array(new Extension('php_zip.dll', array('4+', '!6.0.0-dev')));
//multiple lines
$input[] = $input[0] . "\n" . $input[1] . "\n" . $input[2];
$expect[] = array_merge($expect[0], $expect[1], $expect[2]);
//empty lines
$input[] = $input[0] . "\n\n\n" . $input[1];
$expect[] = array_merge($expect[0], $expect[1]);
//comments
$input[] = $input[0] . ' ; this is arbitrary text';
$expect[] = $expect[0];
$input[] = '; just comments';
$expect[] = array();
//have extension use something other than extension. Theoretically,
//we can extend this to every possible configuration variable... >:)
$input[] = 'php_xdebug.dll / 4.3.11 5+ / zend_extension_ts';
$expect[] = array(new Extension('php_xdebug.dll', array('4.3.11','5+'), 'zend_extension_ts'));
$input[] = 'php_xdebug.dll / 4.3.11 4.4.0 4.4.1 5.0.5 5.1+ / zend_extension_ts';
$expect[] = array(new Extension('php_xdebug.dll', array('4.3.11', '4.4.0', '4.4.1', '5.0.5', '5.1+'), 'zend_extension_ts'));
//need to add rules for "bad" lines
$size = count($input);
for($i = 0; $i < $size; $i++) {
$result = Extension::newFromString($input[$i]);
$this->assertEqual($expect[$i], $result);
paintIf($result, $expect[$i] != $result);
paintIf($expect[$i], $expect[$i] != $result);
}
}
function test_isLoadable() {
$dll = 'php_foobar.dll';
//single version, matches perfectly
$extension[] = new Extension($dll, array('4.3.11'));
$version[] = '4.3.11';
$expect[] = true;
//multiple versions, matches one perfectly
$extension[] = new Extension($dll, array('4.3.11','4.4.1','5.1.0'));
$version[] = '4.4.1';
$expect[] = true;
//multiple versions, matches none
$extension[] = new Extension($dll, array('4.3.11','5.1.2'));
$version[] = '4.3.2';
$expect[] = false;
//coarse grained version matches subversion
$extension[] = new Extension($dll, array('4.3','4.4.2'));
$version[] = '4.3.11';
$expect[] = true;
//blanket version declaration
$extension[] = new Extension($dll, array('5'));
$version[] = '5.1.3';
$expect[] = true;
//special suffixes
$extension[] = new Extension($dll, array('6'));
$version[] = '6.0.0-dev';
$expect[] = true;
//exclusion from blanket
$extension[] = new Extension($dll, array('5', '!5.0.3'));
$version[] = '5.0.3';
$expect[] = false;
//plus operator to extend forever
$extension[] = new Extension($dll, array('4+'));
$version[] = array('3.4.11', '4.0.0', '98.1.123-dev');
$expect[] = array(false, true, true);
//standard case
$extension[] = new Extension($dll, array('4.3.11', '4.4.1', '5.0.5', '5.1+'));
$version[] = array('5.1.2', '9.9.9', '4.3.10');
$expect[] = array(true, true, false);
//buggy case?
$extension[] = new Extension('php_xdebug.dll', array('4.3.11', '4.4.0', '4.4.1', '5.0.5', '5.1+'), 'zend_extension_ts');
$version[] = array('4.3.11');
$expect[] = array(true);
for ($size = count($extension), $i = 0; $i < $size; $i++) {
if (!is_array($version[$i])) $version[$i] = array($version[$i]);
if (!is_array($expect[$i])) $expect[$i] = array($expect[$i]);
for ($sub_size=count($version[$i]), $k=0; $k<$sub_size; $k++) {
$result = $extension[$i]->canLoad($version[$i][$k]);
$this->assertEqual($expect[$i][$k], $result,
'Unexpected ' . ($result ? 'true' : 'false') . ' on ' . $i .
' regarding version ' . $version[$i][$k] . ' matching rules: (' .
implode(', ', $extension[$i]->rules) . ')');
}
}
}
function test_toIniString() {
$extension = new Extension('php_test.dll',array());
$this->assertEqual('extension = php_test.dll', $extension->toIniString());
$extension = new Extension('php_test.dll',array(),'zend_extension_ts');
$this->assertEqual('zend_extension_ts = php_test.dll', $extension->toIniString());
}
}
?>Code: Select all
#!/usr/bin/php
<?php
set_time_limit(0);
echo "== PHP Switch Utility ==\n\n";
require_once('/php/ini_generator/Extension.php');
function prompt($length = '255') {
if (!isset ($GLOBALS['StdinPointer'])) $GLOBALS['StdinPointer'] = fopen("php://stdin","r");
$line = fgets($GLOBALS['StdinPointer'],$length);
return trim($line);
}
function write_apache2_php_conf($file, $version, $type = 'cgi') {
$major_version = (int) substr($version, 0, strpos($version, '.'));
$new_config = '';
if ($type == 'sapi') {
$new_config .= "LoadModule \"C:/php/$version/php{$major_version}apache2.dll\"\n";
$new_config .= 'PHPIniDir "C:/php"'."\n";
} else { // $type == 'cgi'
$new_config .= "ScriptAlias /php/ \"C:/php/$version/\"\n";
if ($major_version == 4) {
$new_config .= 'Action application/x-httpd-php "/php/php.exe"'."\n";
} else {
$new_config .= 'Action application/x-httpd-php "/php/php-cgi.exe"'."\n";
}
}
$fh = fopen($file, 'w');
$status = fwrite($fh, $new_config);
fclose($fh);
return $status;
}
function write_php_ini($file, $version, $base_file, $extension_file) {
$extensions = Extension::newFromFile($extension_file);
$ini_extensions = '';
foreach ($extensions as $extension) {
if (!$extension->canLoad($version)) continue;
$ini_extensions .= $extension->toIniString() . "\r\n";
}
$ini_base = file_get_contents($base_file);
$new_config = $ini_base .
"\r\n; This portion is automatically generated for PHP $version\r\n" .
$ini_extensions;
$new_config = str_replace('{EXT_DIR}', "C:\\php\\$version\\ext", $new_config);
$fh = fopen($file, 'w');
$status = fwrite($fh, $new_config);
fclose($fh);
return $status;
}
$config_file = 'C:\Program Files\Apache Group\Apache2\conf\php.conf';
//determine the current setup
$cur_config = file_get_contents($config_file);
preg_match('#ScriptAlias /php/ (?:")?(?:C|c):/php/([A-Za-z0-9.]+)/(?:")?#', $cur_config, $matches);
$cur_version = $matches[1];
//determine installed PHP stuffs
$versions = array();
$dir = '/php/';
if (!is_dir($dir)) exit('PHP directory doesn\'t exist!');
if (!($dh = opendir($dir))) exit('PHP directory is not readable');
while (($filename = readdir($dh)) !== false) {
if (!is_numeric($filename[0])) continue;
//$versions[] = explode('.', $filename); //better sorting
$versions[] = $filename;
}
closedir($dh);
//output info
echo "Current version is $cur_version.\n";
echo "Available versions:\n";
foreach ($versions as $version) {
echo " - $version\n";
}
echo "\n";
$repeats = 0;
while( true ) {
if ($repeats || empty($argv[1])) {
echo "Switch to: ";
$new_version = prompt(); //allow passing via arguments
} else {
$new_version = $argv[1];
}
if (empty($new_version)) exit('PHP switch aborted.');
$repeats++;
if ($repeats > 10) exit('Max repeats reached, aborting.');
if (!in_array($new_version, $versions)) {
echo "Invalid version. Press ENTER to abort.\n";
continue;
}
break;
}
echo "Writing new Apache configuration... ";
if (!write_apache2_php_conf($config_file, $new_version, 'cgi')) {
exit('failed.');
}
echo "done\n";
echo "Writing new PHP configuration... ";
if (!write_php_ini('C:\php\ini_dir\webserver\php.ini', $new_version,
'C:\php\ini_dir\php.ini', 'C:\php\ini_dir\extensions.txt')) {
exit('failed.');
}
echo "done\n";
echo "Restarting Apache... ";
shell_exec('"C:\Program Files\Apache Group\Apache2\bin\Apache.exe" -k restart');
echo "done\n";
?>Code: Select all
php_tidy.dll / 4.3.11 4.4.1 5.0.5 5.1+
php_apd.dll / 5.0.5 5.1+
php_iconv.dll / 4
{EXT_DIR}\php_xdebug.dll / 4.3.11 4.4.0 4.4.1 5.0.5 5.1+ / zend_extension_tsCode: Select all
============================
= Extensions: How it works =
============================
== Deficiencies ==
After looking at this in depth, it seems to me that we could actually extend
the syntax to something like this:
magic_quotes_gpc(4.3.11 5.0.1 6+ !6.0.0-dev) = off ; complicated!
Which means that all our stuff is not limited to just extensions. Feature creep
however... for now, we should replace the / seperator with something a bit
nicer, because zend_extension (and friends) need the full path to the extension.
Possible alternatives are [*?|<>] (they do not seem to be used by the
filesystem, but they could possibly be valid ini files. Alternatively, a newline
or escapable character approach may be favored. Double character is also
possible. Also, using a constant like {SEP} which, before the file gets
written out, translates into the ending is a possible way of bypassing.
I like *.
== What PHP Sees ==
Because of the way PHP is set up, the ini file has to be called php.ini, so
we need seperate folders for each of the files. Note that while the webserver
file is dynamically generated, in interest of making sure utility scripts
don't go broke, the cli ini has been and always will be for one version only
and not dynamically generated. Currently it's for 4.3.11.
ini_dir/
webserver/php.ini
cli/php.ini
== What we see ==
Currently the only customization we are doing is extension loading. If that is
the case, that we will always only be worried about extension loading in respect
to version, then it would be smarter to optimize the files for extensions.
We also have some macros defined, such as {EXT_DIR} which get substituted when
the ini file is generated (especially for extension_dir).
Here's how the magical ini_dir/extensions.txt file looks sorta like:
php_tidy.dll / 4.3.11 4.4.1 5.0.5 5.1+
php_xdebug.dll / 4.3.11 4.4.0 4.4.1 5.0.5 5.1+
php_apd.dll / 5.0.5 5.1+
php_apc.dll / 4.3.11 4.4.0 5.1+
php_iconv.dll / 4
php_w32api.dll / 4
Say, we can't get php_zip for 6.0.0-dev, then it becomes:
php_zip.dll / 4+ !6.0.0-dev
== Syntax ==
A valid line is:
/\${$dll_name}\s+\/\s+{$version_descriptions}{$comment}^/
or
/\$\s+{$comment}^/
{$dll_name} is a valid filename with no spaces in it.
{$version_descriptions} is:
/((!)?[0-9]+(\.[0-9]+)*(-[A-Za-z]+)?(\+)?(\s)+)+/
1 \-2------------/ \-3--------/ 4
1 - NOT, which excludes the extension the optional blanket + operator
2 - Version number that can have 2 or 1 digits (then it matches all descendants)
3 - Special prefix, like "alpha" or "dev." Use sparingly!
4 - AND EVERYTHING LATER, which indicates extension is available for all later
versions and that one. Since we don't support PHP 3, 4+ means all versions
{$comment} is: /\$;.+^/
== API ==
API is like:
class Extension {
var $filename;
var $rules;
var $directive;
function Extension($filename, $rules, $directive) {}
function newFromFile($file) {return Extensions;}
function newFromString($string) {return Extensions;}
function isValid($version) {return bool;}
function toIniLine() {return string;}
}
You grab a whole bunch of Extension objects from Extension::newFromFile(), then
loop through them all calling isValid, if it is valid, call toIniLine and append
to the configuration file. isValid would have all the version parsing black
magic, and new would be the "private" function that does the config file
parsing.