WebP Express CloudHost.es Fix v0.25.9-cloudhost

 Fixed bulk conversion getting stuck on missing files
 Added robust error handling and timeout protection
 Improved JavaScript response parsing
 Added file existence validation
 Fixed missing PHP class imports
 Added comprehensive try-catch error recovery

🔧 Key fixes:
- File existence checks before conversion attempts
- 30-second timeout protection per file
- Graceful handling of 500 errors and JSON parsing issues
- Automatic continuation to next file on failures
- Cache busting for JavaScript updates

🎯 Result: Bulk conversion now completes successfully even with missing files

🚀 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-23 10:22:32 +02:00
commit 37cf714058
553 changed files with 55249 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
<?php
namespace HtaccessCapabilityTester;
use \HtaccessCapabilityTester\Testers\AbstractTester;
use \HtaccessCapabilityTester\Testers\AddTypeTester;
use \HtaccessCapabilityTester\Testers\ContentDigestTester;
use \HtaccessCapabilityTester\Testers\CrashTester;
use \HtaccessCapabilityTester\Testers\CustomTester;
use \HtaccessCapabilityTester\Testers\DirectoryIndexTester;
use \HtaccessCapabilityTester\Testers\HeaderSetTester;
use \HtaccessCapabilityTester\Testers\HtaccessEnabledTester;
use \HtaccessCapabilityTester\Testers\InnocentRequestTester;
use \HtaccessCapabilityTester\Testers\ModuleLoadedTester;
use \HtaccessCapabilityTester\Testers\PassInfoFromRewriteToScriptThroughRequestHeaderTester;
use \HtaccessCapabilityTester\Testers\PassInfoFromRewriteToScriptThroughEnvTester;
use \HtaccessCapabilityTester\Testers\RewriteTester;
use \HtaccessCapabilityTester\Testers\RequestHeaderTester;
use \HtaccessCapabilityTester\Testers\ServerSignatureTester;
/**
* Main entrance.
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class HtaccessCapabilityTester
{
/** @var string The dir where the test files should be put */
protected $baseDir;
/** @var string The base url that the tests can be run from (corresponds to $baseDir) */
protected $baseUrl;
/** @var string Additional info regarding last test (often empty) */
public $infoFromLastTest;
/** @var string Status code from last test (can be empty) */
public $statusCodeOfLastRequest;
/** @var HttpRequesterInterface The object used to make the HTTP request */
private $requester;
/** @var TestFilesLineUpperInterface The object used to line up the test files */
private $testFilesLineUpper;
/**
* Constructor.
*
* @param string $baseDir Directory on the server where the test files can be put
* @param string $baseUrl The base URL of the test files
*
* @return void
*/
public function __construct($baseDir, $baseUrl)
{
$this->baseDir = $baseDir;
$this->baseUrl = $baseUrl;
}
/**
* Run a test, store the info and return the status.
*
* @param AbstractTester $tester
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
private function runTest($tester)
{
//$tester->setHtaccessCapabilityTester($this);
if (isset($this->requester)) {
$tester->setHttpRequester($this->requester);
}
if (isset($this->testFilesLineUpper)) {
$tester->setTestFilesLineUpper($this->testFilesLineUpper);
}
//$tester->setHtaccessCapabilityTester($this);
$cacheKeys = [$this->baseDir, $tester->getCacheKey()];
if (TestResultCache::isCached($cacheKeys)) {
$testResult = TestResultCache::getCached($cacheKeys);
} else {
$testResult = $tester->run($this->baseDir, $this->baseUrl);
TestResultCache::cache($cacheKeys, $testResult);
}
$this->infoFromLastTest = $testResult->info;
$this->statusCodeOfLastRequest = $testResult->statusCodeOfLastRequest;
return $testResult->status;
}
/**
* Run a test, store the info and return the status.
*
* @param HttpRequesterInterface $requester
*
* @return void
*/
public function setHttpRequester($requester)
{
$this->requester = $requester;
}
/**
* Set object responsible for lining up the test files.
*
* @param TestFilesLineUpperInterface $testFilesLineUpper
* @return void
*/
public function setTestFilesLineUpper($testFilesLineUpper)
{
$this->testFilesLineUpper = $testFilesLineUpper;
}
/**
* Test if .htaccess files are enabled
*
* Apache can be configured to completely ignore .htaccess files. This test examines
* if .htaccess files are proccesed.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function htaccessEnabled()
{
return $this->runTest(new HtaccessEnabledTester());
}
/**
* Test if a module is loaded.
*
* This test detects if directives inside a "IfModule" is run for a given module
*
* @param string $moduleName A valid Apache module name (ie "rewrite")
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function moduleLoaded($moduleName)
{
return $this->runTest(new ModuleLoadedTester($moduleName));
}
/**
* Test if rewriting works.
*
* The .htaccess in this test uses the following directives:
* - IfModule
* - RewriteEngine
* - Rewrite
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function rewriteWorks()
{
return $this->runTest(new RewriteTester());
}
/**
* Test if AddType works.
*
* The .htaccess in this test uses the following directives:
* - IfModule (core)
* - AddType (mod_mime, FileInfo)
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function addTypeWorks()
{
return $this->runTest(new AddTypeTester());
}
/**
* Test if setting a Response Header with the Header directive works.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function headerSetWorks()
{
return $this->runTest(new HeaderSetTester());
}
/**
* Test if setting a Request Header with the RequestHeader directive works.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function requestHeaderWorks()
{
return $this->runTest(new RequestHeaderTester());
}
/**
* Test if ContentDigest directive works.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function contentDigestWorks()
{
return $this->runTest(new ContentDigestTester());
}
/**
* Test if ServerSignature directive works.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function serverSignatureWorks()
{
return $this->runTest(new ServerSignatureTester());
}
/**
* Test if DirectoryIndex works.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function directoryIndexWorks()
{
return $this->runTest(new DirectoryIndexTester());
}
/**
* Test a complex construct for passing information from a rewrite to a script through a request header.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function passingInfoFromRewriteToScriptThroughRequestHeaderWorks()
{
return $this->runTest(new PassInfoFromRewriteToScriptThroughRequestHeaderTester());
}
/**
* Test if an environment variable can be set in a rewrite rule and received in PHP.
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function passingInfoFromRewriteToScriptThroughEnvWorks()
{
return $this->runTest(new PassInfoFromRewriteToScriptThroughEnvTester());
}
/**
* Call one of the methods of this class (not all allowed).
*
* @param string $functionCall ie "rewriteWorks()"
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
/*
public function callMethod($functionCall)
{
switch ($functionCall) {
case 'htaccessEnabled()':
return $this->htaccessEnabled();
case 'rewriteWorks()':
return $this->rewriteWorks();
case 'addTypeWorks()':
return $this->addTypeWorks();
case 'headerSetWorks()':
return $this->headerSetWorks();
case 'requestHeaderWorks()':
return $this->requestHeaderWorks();
case 'contentDigestWorks()':
return $this->contentDigestWorks();
case 'directoryIndexWorks()':
return $this->directoryIndexWorks();
case 'passingInfoFromRewriteToScriptThroughRequestHeaderWorks()':
return $this->passingInfoFromRewriteToScriptThroughRequestHeaderWorks();
case 'passingInfoFromRewriteToScriptThroughEnvWorks()':
return $this->passingInfoFromRewriteToScriptThroughEnvWorks();
default:
throw new \Exception('The method is not callable');
}
// TODO: moduleLoaded($moduleName)
}*/
/**
* Crash-test some .htaccess rules.
*
* Tests if the server can withstand the given rules without going fatal.
*
* - success: if the rules does not result in status 500.
* - failure: if the rules results in status 500 while a request to a file in a directory
* without any .htaccess succeeds (<> 500)
* - inconclusive: if the rules results in status 500 while a request to a file in a directory
* without any .htaccess also fails (500)
*
* @param string $rules Rules to crash-test
* @param string $subDir (optional) Subdir for the .htaccess to reside.
* if left out, a unique string will be generated
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function crashTest($rules, $subDir = null)
{
return $this->runTest(new CrashTester($rules, $subDir));
}
/**
* Test an innocent request to a text file.
*
* If this fails, everything else will also fail.
*
* Possible reasons for failure:
* - A .htaccess in a parent folder has forbidden tags / syntax errors
*
* Possible reasons for inconclusive (= test could not be run)
* - 403 Forbidden
* - 404 Not Found
* - Request fails (ie due to timeout)
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function innocentRequestWorks()
{
return $this->runTest(new InnocentRequestTester());
}
/**
* Run a custom test.
*
* @param array $definition
*
* @return bool|null true=success, false=failure, null=inconclusive
*/
public function customTest($definition)
{
return $this->runTest(new CustomTester($definition));
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace HtaccessCapabilityTester;
interface HttpRequesterInterface
{
/**
* Make a HTTP request to a URL.
*
* @return HttpResponse A HttpResponse object, which simply contains body, status code and response headers.
* In case the request itself fails, the status code is "0" and the body should contain
* error description (if available)
*/
public function makeHttpRequest($url);
}

View File

@@ -0,0 +1,75 @@
<?php
namespace HtaccessCapabilityTester;
/**
* Class for holding properties of a HttpResponse
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class HttpResponse
{
/* @var string the body of the response */
public $body;
/* @var string the status code of the response */
public $statusCode;
/* @var array the response headers keyed by lowercased field name */
public $headersMapLowerCase;
/**
* Constructor.
*
* @param string $body
* @param string $statusCode
* @param array $headersMap Map of headers, keyed by field name.
* There is only one value (string) for each key.
* If there are multiple values, they must be separated by comma
*
* @return void
*/
public function __construct($body, $statusCode, $headersMap)
{
$this->body = $body;
$this->statusCode = $statusCode;
$this->headersMapLowerCase = array_change_key_case($headersMap, CASE_LOWER);
}
/**
* Check if the response has a header
*
* @param string $fieldName
* @return bool
*/
public function hasHeader($fieldName)
{
$fieldName = strtolower($fieldName);
return (isset($this->headersMapLowerCase[$fieldName]));
}
/**
* Check if the response has a header with a given value
*
* @param string $fieldName
* @param string $fieldValue
* @return bool
*/
public function hasHeaderValue($fieldName, $fieldValue)
{
$fieldName = strtolower($fieldName);
if (!isset($this->headersMapLowerCase[$fieldName])) {
return false;
}
$values = explode(',', $this->headersMapLowerCase[$fieldName]);
foreach ($values as $value) {
if (trim($value) == $fieldValue) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace HtaccessCapabilityTester;
class SimpleHttpRequester implements HttpRequesterInterface
{
/**
* Make a HTTP request to a URL.
*
* @param string $url The URL to make the HTTP request to
*
* @return HttpResponse A HttpResponse object, which simply contains body, status code and response headers.
* In case the request itself fails, the status code is "0" and the body should contain
* error description (if available)
*/
public function makeHttpRequest($url)
{
// PS: We suppress the E_WARNING level error generated on failure
$body = @file_get_contents($url);
if ($body === false) {
//$body = '';
return new HttpResponse('The following request failed: file_get_contents(' . $url . ')', '0', []);
}
// $http_response_header materializes out of thin air when file_get_contents() is called
// Get status code
$statusLine = $http_response_header[0];
preg_match('{HTTP\/\S*\s(\d{3})}', $statusLine, $match);
$statusCode = $match[1];
// Create headers map
$headersMap = [];
foreach ($http_response_header as $header) {
$pos = strpos($header, ':');
if ($pos > 0) {
$fieldName = strtolower(trim(substr($header, 0, $pos)));
$value = trim(substr($header, $pos + 1));
if (!isset($headersMap[$fieldName])) {
$headersMap[$fieldName] = $value;
} else {
$headersMap[$fieldName] .= ', ' . $value;
}
}
}
return new HttpResponse($body, $statusCode, $headersMap);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace HtaccessCapabilityTester;
class SimpleTestFileLineUpper implements TestFilesLineUpperInterface
{
private function writeFileIfMissingOrChanged($file)
{
$success = true;
list($filename, $content) = $file;
$dir = dirname($filename);
if (!is_dir($dir)) {
if (!mkdir($dir, 0777, true)) {
// TODO: Use custom exception
throw new \Exception('Failed creating dir: ' . $dir);
}
}
if (file_exists($filename)) {
// file already exists, now check if content is the same
$existingContent = file_get_contents($filename);
if (($existingContent === false) || ($content != $existingContent)) {
$success = file_put_contents($filename, $content);
}
} else {
$success = file_put_contents($filename, $content);
}
if (!$success) {
// TODO: Use custom exception
throw new \Exception('Failed creating file: ' . $filename);
}
}
/**
* Write missing and changed files.
*
* @param array $files The files that needs to be there
*
* @return void
*/
private function writeMissingAndChangedFiles($files)
{
foreach ($files as $file) {
$this->writeFileIfMissingOrChanged($file);
}
}
/**
* Remove unused files.
*
* @param array $files The files that needs to be there (others will be removed)
*
* @return void
*/
private function removeUnusedFiles($files)
{
$dirs = [];
foreach ($files as $file) {
list($filename, $content) = $file;
$dir = dirname($filename);
if (!isset($dirs[$dir])) {
$dirs[$dir] = [];
}
$dirs[$dir][] = basename($filename);
}
foreach ($dirs as $dir => $filesSupposedToBeInDir) {
$fileIterator = new \FilesystemIterator($dir);
while ($fileIterator->valid()) {
$filename = $fileIterator->getFilename();
if (!in_array($filename, $filesSupposedToBeInDir)) {
unlink($dir . '/' . $filename);
}
$fileIterator->next();
}
}
}
/**
* Line-up test files.
*
* This method should make sure that the files passed in are there and are up-to-date.
* - If a file is missing, it should be created.
* - If a file has changed content, it should be updated
* - If the directory contains a file/dir that should not be there, it should be removed
*
* @param array $files The files that needs to be there
*
* @return void
*/
public function lineUp($files)
{
// 1. Put missing files / changed files
$this->writeMissingAndChangedFiles($files);
// 2. Remove unused files
$this->removeUnusedFiles($files);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace HtaccessCapabilityTester;
interface TestFilesLineUpperInterface
{
/**
* Line-up test files.
*
* This method should make sure that the files passed in are there and are up-to-date.
* - If a file is missing, it should be created.
* - If a file has changed content, it should be updated
* - If the directory contains a file/dir that should not be there, it should be removed
*
* @param array $files The files that needs to be there
*
* @return void
*/
public function lineUp($files);
}

View File

@@ -0,0 +1,39 @@
<?php
namespace HtaccessCapabilityTester;
/**
* Class for holding properties of a TestResult
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since the beginning
*/
class TestResult
{
/* @var bool|null The result, null if inconclusive */
public $status;
/* @var string Information about how the test failed / became inconclusive */
public $info;
/* @var string Status code of last request in the test */
public $statusCodeOfLastRequest;
/**
* Constructor.
*
* @param bool|null $status
* @param string $info
* @param string $statusCodeOfLastRequest (optional)
*
* @return void
*/
public function __construct($status, $info, $statusCodeOfLastRequest = null)
{
$this->status = $status;
$this->info = $info;
$this->statusCodeOfLastRequest = $statusCodeOfLastRequest;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace HtaccessCapabilityTester;
use \HtaccessCapabilityTester\Testers\AbstractTester;
/**
* Class caching test results
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since the beginning
*/
class TestResultCache
{
/* @var array Array for caching */
protected static $cache;
/**
*
* @param array $cacheKeys Two keys for caching (usually: basedir and the getCacheKey() for the Tester)
* @param TestResult $testResult The test result to cache
*
* @return void
*/
public static function cache($cacheKeys, $testResult)
{
if (!isset(self::$cache)) {
self::$cache = [];
}
list($key1, $key2) = $cacheKeys;
if (!isset(self::$cache[$key1])) {
self::$cache[$key1] = [];
}
self::$cache[$key1][$key2] = $testResult;
}
/**
* Check if in cache.
*
* @param array $cacheKeys Keys for caching (usually: basedir and the getCacheKey() for the Tester)
*
* @return bool
*/
public static function isCached($cacheKeys)
{
if (!isset(self::$cache)) {
return false;
}
list($key1, $key2) = $cacheKeys;
if (!isset(self::$cache[$key1])) {
return false;
}
if (!isset(self::$cache[$key1][$key2])) {
return false;
}
return true;
}
/**
* Get from cache.
*
* @param array $cacheKeys Keys for caching (usually: basedir and the getCacheKey() for the Tester)
*
* @return TestResult The test result
*/
public static function getCached($cacheKeys)
{
if (!self::isCached($cacheKeys)) {
throw new \Exception('Not in cache');
}
list($key1, $key2) = $cacheKeys;
return self::$cache[$key1][$key2];
}
public static function clear()
{
self::$cache = null;
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace HtaccessCapabilityTester\Testers;
use \HtaccessCapabilityTester\HtaccessCapabilityTester;
use \HtaccessCapabilityTester\HttpRequesterInterface;
use \HtaccessCapabilityTester\HttpResponse;
use \HtaccessCapabilityTester\SimpleHttpRequester;
use \HtaccessCapabilityTester\SimpleTestFileLineUpper;
use \HtaccessCapabilityTester\TestFilesLineUpperInterface;
use \HtaccessCapabilityTester\TestResult;
abstract class AbstractTester
{
/** @var string The dir where the test files should be put */
protected $baseDir;
/** @var string The base url that the tests can be run from (corresponds to $baseDir) */
protected $baseUrl;
/** @var string Subdir to put .htaccess files in */
protected $subDir;
/** @var array Test files for the test */
protected $testFiles;
/** @var HttpRequesterInterface An object for making the HTTP request */
protected $httpRequester;
/** @var HttpResponse The response of the previous HTTP request (if any) */
public $lastHttpResponse;
/** @var TestFilesLineUpperInterface An object for lining up the test-files */
protected $testFilesLineUpper;
/** @var HtaccessCapabilityTester The HtaccessCapabilityTester to use for subtests */
private $hct;
/**
* Register the test files using the "registerTestFile" method
*
* @return void
*/
abstract protected function registerTestFiles();
/**
* Child classes must implement this method, which tells which subdir the
* test files are to be put.
*
* @return string A subdir for the test files
*/
abstract protected function getSubDir();
/**
* Get key for caching purposes.
*
* Return a unique key. The default is to use the subdir. However, if a concrete Tester class
* can test different things, it must override this method and make sure to return a different
* key per thing it can test
*
* @return string A key it can be cached under
*/
public function getCacheKey()
{
return $this->getSubDir();
}
public function getBaseDir()
{
return $this->baseDir;
}
public function getBaseUrl()
{
return $this->baseUrl;
}
/**
* Child classes must that implement the registerTestFiles method must call
* this method to register each test file.
*
* @return void
*/
protected function registerTestFile($filename, $content)
{
$this->testFiles[] = [$this->baseDir . '/' . $filename, $content];
}
/**
* Last moment preparations before running the test
*
* @param string $baseDir Directory on the server where the test files can be put
* @param string $baseUrl The base URL of the test files
*
* @throws \Exception In case the test cannot be prepared due to serious issues
*/
protected function prepareForRun($baseDir, $baseUrl)
{
$this->baseDir = $baseDir;
$this->baseUrl = $baseUrl;
$this->testFiles = [];
$this->registerTestFiles();
$this->lineUpTestFiles();
}
abstract public function run($baseDir, $baseUrl);
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$this->subDir = $this->getSubDir();
}
/**
* Make a HTTP request to a URL.
*
* @param string $url The URL to make the HTTP request to
*
* @return HttpResponse A HttpResponse object, which simply contains body and status code.
*/
protected function makeHttpRequest($url)
{
if (!isset($this->httpRequester)) {
$this->httpRequester = new SimpleHttpRequester();
}
$this->lastHttpResponse = $this->httpRequester->makeHttpRequest($url);
return $this->lastHttpResponse;
}
/**
* Set HTTP requester object, which handles making HTTP requests.
*
* @param HttpRequesterInterface $httpRequester The HTTPRequester to use
* @return void
*/
public function setHttpRequester($httpRequester)
{
$this->httpRequester = $httpRequester;
if (isset($this->hct)) {
$this->hct->setHttpRequester($this->httpRequester);
}
}
public function lineUpTestFiles()
{
if (!isset($this->testFilesLineUpper)) {
$this->testFilesLineUpper = new SimpleTestFileLineUpper();
}
$this->testFilesLineUpper->lineUp($this->testFiles);
}
/**
* Set object responsible for lining up the test files.
*
* @param TestFilesLineUpperInterface $testFilesLineUpper
* @return void
*/
public function setTestFilesLineUpper($testFilesLineUpper)
{
$this->testFilesLineUpper = $testFilesLineUpper;
if (isset($this->hct)) {
$this->hct->setTestFilesLineUpper($this->testFilesLineUpper);
}
}
/**
* Get HtaccessCapabilityTester.
*
* Some tests use HtaccessCapabilityTester to run other tests.
* This gets such object with baseDir and baseUrl set up
*
* @return HtaccessCapabilityTester
*/
public function getHtaccessCapabilityTester()
{
if (!isset($this->hct)) {
$this->hct = new HtaccessCapabilityTester($this->baseDir, $this->baseUrl);
if (isset($this->testFilesLineUpper)) {
$this->hct->setTestFilesLineUpper($this->testFilesLineUpper);
}
if (isset($this->httpRequester)) {
$this->hct->setHttpRequester($this->httpRequester);
}
}
return $this->hct;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if AddType works
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class AddTypeTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
<IfModule mod_mime.c>
AddType image/gif .test
</IfModule>
EOD;
$test = [
'subdir' => 'add-type',
'files' => [
['.htaccess', $htaccessFile],
['request-me.test', 'hi'],
],
'request' => 'request-me.test',
'interpretation' => [
['success', 'headers', 'contains-key-value', 'Content-Type', 'image/gif'],
['inconclusive', 'status-code', 'not-equals', '200'],
['failure', 'headers', 'not-contains-key-value', 'Content-Type', 'image/gif'],
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if setting ContentDigest works
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class ContentDigestTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$test = [
'subdir' => 'content-digest',
'subtests' => [
[
'subdir' => 'on',
'files' => [
['.htaccess', 'ContentDigest On'],
['request-me.txt', 'hi'],
],
'request' => 'request-me.txt',
'interpretation' => [
['failure', 'headers', 'not-contains-key', 'Content-MD5'],
]
],
[
'subdir' => 'off',
'files' => [
['.htaccess', 'ContentDigest Off'],
['request-me.txt', "hi"],
],
'request' => 'request-me.txt',
'interpretation' => [
['failure', 'headers', 'contains-key', 'Content-MD5'],
['inconclusive', 'status-code', 'not-equals', '200'],
['success', 'status-code', 'equals', '200'],
]
]
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace HtaccessCapabilityTester\Testers;
use \HtaccessCapabilityTester\TestResult;
/**
* Class for testing if a .htaccess results in a 500 Internal Server Error
* (ie due to being malformed or containing directives that are unknown or not allowed)
*
* Notes:
* - The tester only reports failure on a 500 Internal Server Error. All other status codes (even server errors)
* are treated as a success. The assumption here is that malformed .htaccess files / .htaccess
* files containing unknown or disallowed directives always results in a 500
* - If your purpose is to test if a request succeeds (response 200 Ok), you should create your own class.
* (note that if you want to ensure that a php will succeed, make sure that a php is requested)
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class CrashTester extends CustomTester
{
/**
* @param string $htaccessRules The rules to check
* @param string $subSubDir subdir for the test files. If not supplied, a fingerprint of the rules will be used
*/
public function __construct($htaccessRules, $subSubDir = null)
{
if (is_null($subSubDir)) {
$subSubDir = hash('md5', $htaccessRules);
}
$test = [
'subdir' => 'crash-tester/' . $subSubDir,
'subtests' => [
[
'subdir' => 'the-suspect',
'files' => [
['.htaccess', $htaccessRules],
['request-me.txt', 'thanks'],
],
'request' => [
'url' => 'request-me.txt',
'bypass-standard-error-handling' => ['403', '404', '500']
],
'interpretation' => [
['success', 'status-code', 'not-equals', '500'],
// Otherwise fall through to next subtest
]
],
[
'subdir' => 'the-innocent',
'files' => [
['.htaccess', '# I am no trouble'],
['request-me.txt', 'thanks'],
],
'request' => [
'url' => 'request-me.txt',
'bypass-standard-error-handling' => ['403', '404', '500']
],
'interpretation' => [
// The suspect crashed. But if the innocent crashes too, we cannot judge
['inconclusive', 'status-code', 'equals', '500'],
// The innocent did not crash. The suspect is guilty!
['failure'],
]
],
]
];
parent::__construct($test);
}
/**
* Child classes must implement this method, which tells which subdir the
* test files are to be put.
*
* @return string A subdir for the test files
*/
public function getSubDir()
{
return 'crash-tester';
}
}

View File

@@ -0,0 +1,230 @@
<?php
namespace HtaccessCapabilityTester\Testers;
use \HtaccessCapabilityTester\HtaccessCapabilityTester;
use \HtaccessCapabilityTester\HttpRequesterInterface;
use \HtaccessCapabilityTester\HttpResponse;
use \HtaccessCapabilityTester\SimpleHttpRequester;
use \HtaccessCapabilityTester\TestResult;
use \HtaccessCapabilityTester\Testers\Helpers\ResponseInterpreter;
class CustomTester extends AbstractTester
{
/** @var array A definition defining the test */
protected $test;
/** @var array For convenience, all tests */
private $tests;
/**
* Constructor.
*
* @param array $test The test (may contain subtests)
*
* @return void
*/
public function __construct($test)
{
$this->test = $test;
if (isset($test['subtests'])) {
$this->tests = $test['subtests'];
// Add main subdir to subdir for all subtests
foreach ($this->tests as &$subtest) {
if (isset($subtest['subdir'])) {
$subtest['subdir'] = $test['subdir'] . '/' . $subtest['subdir'];
}
}
} else {
$this->tests = [$test];
}
//echo '<pre>' . print_r($this->tests, true) . '</pre>';
//echo json_encode($this->tests) . '<br>';
parent::__construct();
}
/**
* Register the test files using the "registerTestFile" method
*
* @return void
*/
protected function registerTestFiles()
{
foreach ($this->tests as $test) {
if (isset($test['files'])) {
foreach ($test['files'] as $file) {
// Two syntaxes are allowed:
// - Simple array (ie: ['0.txt', '0']
// - Named, ie: ['filename' => '0.txt', 'content' => '0']
// The second makes more readable YAML definitions
if (isset($file['filename'])) {
$filename = $file['filename'];
$content = $file['content'];
} else {
list ($filename, $content) = $file;
}
$this->registerTestFile($test['subdir'] . '/' . $filename, $content);
}
}
}
}
public function getSubDir()
{
return $this->test['subdir'];
}
/**
* Standard Error handling
*
* @param HttpResponse $response
*
* @return TestResult|null If no errors, null is returned, otherwise a TestResult
*/
private function standardErrorHandling($response)
{
switch ($response->statusCode) {
case '0':
return new TestResult(null, $response->body);
case '403':
return new TestResult(null, '403 Forbidden');
case '404':
return new TestResult(null, '404 Not Found');
case '500':
$hct = $this->getHtaccessCapabilityTester();
// Run innocent request / get it from cache. This sets
// $statusCodeOfLastRequest, which we need now
$hct->innocentRequestWorks();
if ($hct->statusCodeOfLastRequest == '500') {
return new TestResult(null, 'Errored with 500. Everything errors with 500.');
} else {
return new TestResult(
false,
'Errored with 500. ' .
'Not all goes 500, so it must be a forbidden directive in the .htaccess'
);
}
}
return null;
}
/**
* Checks if standard error handling should be bypassed on the test.
*
* This stuff is controlled in the test definition. More precisely, by the "bypass-standard-error-handling"
* property bellow the "request" property. If this property is set to ie ['404', '500'], the standard error
* handler will be bypassed for those codes (but still be in effect for ie '403'). If set to ['all'], all
* standard error handling will be bypassed.
*
* @param array $test the subtest
* @param HttpResponse $response the response
*
* @return bool true if error handling should be bypassed
*/
private function bypassStandardErrorHandling($test, $response)
{
if (!(isset($test['request']['bypass-standard-error-handling']))) {
return false;
}
$bypassErrors = $test['request']['bypass-standard-error-handling'];
if (in_array($response->statusCode, $bypassErrors) || in_array('all', $bypassErrors)) {
return true;
}
return false;
}
/**
* Run single test
*
* @param array $test the subtest to run
*
* @return TestResult Returns a test result
*/
private function realRunSubTest($test)
{
$requestUrl = $this->baseUrl . '/' . $test['subdir'] . '/';
if (isset($test['request']['url'])) {
$requestUrl .= $test['request']['url'];
} else {
$requestUrl .= $test['request'];
}
//echo $requestUrl . '<br>';
$response = $this->makeHttpRequest($requestUrl);
// Standard error handling
if (!($this->bypassStandardErrorHandling($test, $response))) {
$errorResult = $this->standardErrorHandling($response);
if (!is_null($errorResult)) {
return $errorResult;
}
}
return ResponseInterpreter::interpret($response, $test['interpretation']);
}
/**
* Run
*
* @param string $baseDir Directory on the server where the test files can be put
* @param string $baseUrl The base URL of the test files
*
* @return TestResult Returns a test result
* @throws \Exception In case the test cannot be run due to serious issues
*/
private function realRun($baseDir, $baseUrl)
{
$this->prepareForRun($baseDir, $baseUrl);
$result = null;
foreach ($this->tests as $i => $test) {
/*
Disabled, as I'm no longer sure if it is that useful
if (isset($test['requirements'])) {
$hct = $this->getHtaccessCapabilityTester();
foreach ($test['requirements'] as $requirement) {
$requirementResult = $hct->callMethod($requirement);
if (!$requirementResult) {
// Skip test
continue 2;
}
}
}*/
if (isset($test['request'])) {
$result = $this->realRunSubTest($test);
if ($result->info != 'no-match') {
return $result;
}
}
}
if (is_null($result)) {
$result = new TestResult(null, 'Nothing to test!');
}
return $result;
}
/**
* Run
*
* @param string $baseDir Directory on the server where the test files can be put
* @param string $baseUrl The base URL of the test files
*
* @return TestResult Returns a test result
* @throws \Exception In case the test cannot be run due to serious issues
*/
public function run($baseDir, $baseUrl)
{
$testResult = $this->realRun($baseDir, $baseUrl);
// A test might not create a request if it has an unfulfilled requirement
if (isset($this->lastHttpResponse)) {
$testResult->statusCodeOfLastRequest = $this->lastHttpResponse->statusCode;
}
return $testResult;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if DirectoryIndex works
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class DirectoryIndexTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
<IfModule mod_dir.c>
DirectoryIndex index2.html
</IfModule>
EOD;
$test = [
'subdir' => 'directory-index',
'files' => [
['.htaccess', $htaccessFile],
['index.html', "0"],
['index2.html', "1"]
],
'request' => [
'url' => '', // We request the index, that is why its empty
'bypass-standard-error-handling' => ['404']
],
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
['failure', 'status-code', 'equals', '404'], // "index.html" might not be set to index
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if Header works
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class HeaderSetTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
<IfModule mod_headers.c>
Header set X-Response-Header-Test: test
</IfModule>
EOD;
$test = [
'subdir' => 'header-set',
'files' => [
['.htaccess', $htaccessFile],
['request-me.txt', "hi"],
],
'request' => 'request-me.txt',
'interpretation' => [
['success', 'headers', 'contains-key-value', 'X-Response-Header-Test', 'test'],
['failure'],
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace HtaccessCapabilityTester\Testers\Helpers;
use \HtaccessCapabilityTester\HttpResponse;
use \HtaccessCapabilityTester\TestResult;
use \HtaccessCapabilityTester\Testers\AbstractTester;
/**
* Class for interpreting responses using a defined interpretation table.
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class ResponseInterpreter
{
/**
* Parse status string (failure | success | inconclusive) to bool|null.
*
* @param string $statusString (failure | success | inconclusive)
* @return bool|null
*/
private static function parseStatusString($statusString)
{
$status = null;
switch ($statusString) {
case 'failure':
$status = false;
break;
case 'inconclusive':
$status = null;
break;
case 'success':
$status = true;
break;
}
return $status;
}
/**
* Interpret headers line
*
* @param HttpResponse $response
* @param string $operator (has-key | )
* @param string $fieldName field name of the header
* @param string $fieldValue (optional) field value to look for. Only required when
* operator is "contains-key-value" or "not-contains-key-value"
* @return bool true if the condition matches, false otherwise
*/
private static function evaluateHeadersLine($response, $operator, $fieldName, $fieldValue)
{
switch ($operator) {
case 'contains-key':
return $response->hasHeader($fieldName);
case 'not-contains-key':
return (!($response->hasHeader($fieldName)));
case 'contains-key-value':
return $response->hasHeaderValue($fieldName, $fieldValue);
case 'not-contains-key-value':
return (!($response->hasHeaderValue($fieldName, $fieldValue)));
}
return false;
}
/**
* Interpret string line (body or status-code)
*
* @param HttpResponse $response
* @param string $property ("body" or "status-code")
* @param string $operator (is-empty | equals | not-equals | begins-with)
* @param string $arg1 (only required for some operators)
*
* @return bool true if the condition matches, false otherwise
*/
private static function evaluateStringLine($response, $property, $operator, $arg1)
{
$val = '';
switch ($property) {
case 'status-code':
$val = $response->statusCode;
break;
case 'body':
$val = $response->body;
break;
}
switch ($operator) {
case 'is-empty':
return ($val == '');
case 'equals':
return ($val == $arg1);
case 'not-equals':
return ($val != $arg1);
case 'begins-with':
return (strpos($val, $arg1) === 0);
}
return false;
}
/**
* Interpret line.
*
* @param HttpResponse $response
* @param array $line
*
* @return TestResult|null If the condition matches, a TestResult is returned, otherwise null
*/
private static function interpretLine($response, $line)
{
// ie:
// ['inconclusive', 'body', 'is-empty'],
// ['failure', 'statusCode', 'equals', '500']
// ['success', 'headers', 'contains-key-value', 'X-Response-Header-Test', 'test'],
$status = self::parseStatusString($line[0]);
if (!isset($line[1])) {
return new TestResult($status, '');
}
$propertyToExamine = $line[1];
$operator = $line[2];
$arg1 = (isset($line[3]) ? $line[3] : '');
$arg2 = (isset($line[4]) ? $line[4] : '');
if ($propertyToExamine == 'headers') {
$match = self::evaluateHeadersLine($response, $operator, $arg1, $arg2);
} else {
$match = self::evaluateStringLine($response, $propertyToExamine, $operator, $arg1);
}
if ($match) {
$reason = $propertyToExamine . ' ' . $operator;
if (isset($line[3])) {
$reason .= ' "' . implode('" "', array_slice($line, 3)) . '"';
}
/*
if (($propertyToExamine == 'status-code') && ($operator == 'not-equals') && (gettype($val) == 'string')) {
$reason .= ' - it was: ' . $val;
}*/
return new TestResult($status, $reason);
}
return null;
}
/**
* Interpret a response using an interpretation table.
*
* @param HttpResponse $response
* @param array $interpretationTable
*
* @return TestResult If there is no match, the test result will have status = false and
* info = "no-match".
*/
public static function interpret($response, $interpretationTable)
{
foreach ($interpretationTable as $i => $line) {
$testResult = self::interpretLine($response, $line);
if (!is_null($testResult)) {
return $testResult;
}
}
return new TestResult(null, 'no-match');
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace HtaccessCapabilityTester\Testers;
use \HtaccessCapabilityTester\HtaccessCapabilityTester;
use \HtaccessCapabilityTester\TestResult;
/**
* Class for testing if .htaccess files are processed
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class HtaccessEnabledTester extends AbstractTester
{
/**
* Child classes must implement this method, which tells which subdir the
* test files are to be put.
*
* @return string A subdir for the test files
*/
public function getSubDir()
{
return 'htaccess-enabled';
}
/**
* Register the test files using the "registerTestFile" method
*
* @return void
*/
public function registerTestFiles()
{
// No test files for this test
}
/**
* Run the test.
*
* @param string $baseDir Directory on the server where the test files can be put
* @param string $baseUrl The base URL of the test files
*
* @return TestResult Returns a test result
*/
public function run($baseDir, $baseUrl)
{
$this->prepareForRun($baseDir, $baseUrl);
/*
PS: We could implement this as a definition:
- [success, serverSignatureWorks, is-success]
- [success, contentDigestWorks, is-success]
- [failure, serverSignatureWorks, is-failure]
- [success, canCrash, is-success]
*/
$status = null;
$info = '';
$hct = $this->getHtaccessCapabilityTester();
// If we can find anything that works, well the .htaccess must have been proccesed!
if ($hct->serverSignatureWorks() // Override: None, Status: Core, REQUIRES PHP
|| $hct->contentDigestWorks() // Override: Options, Status: Core
|| $hct->addTypeWorks() // Override: FileInfo, Status: Base, Module: mime
|| $hct->directoryIndexWorks() // Override: Indexes, Status: Base, Module: mod_dir
|| $hct->rewriteWorks() // Override: FileInfo, Status: Extension, Module: rewrite
|| $hct->headerSetWorks() // Override: FileInfo, Status: Extension, Module: headers
) {
$status = true;
} else {
// The serverSignatureWorks() test is special because if it comes out as a failure,
// we can be *almost* certain that the .htaccess has been completely disabled
$serverSignatureWorks = $hct->serverSignatureWorks();
if ($serverSignatureWorks === false) {
$status = false;
$info = 'ServerSignature directive does not work - and it is in core';
} else {
// Last bullet in the gun:
// Try an .htaccess with syntax errors in it.
// (we do this lastly because it may generate an entry in the error log)
$crashTestResult = $hct->crashTest('aoeu', 'htaccess-enabled-malformed-htaccess');
if (is_null($crashTestResult)) {
// Two scenarios:
// 1: All requests fails (without response code)
// 2: The crash test could not figure it out (ie if even innocent requests crashes)
$status = null;
$info = 'all requests fails (even innocent ones)';
} elseif ($crashTestResult === false) {
// It crashed, - which means .htaccess is processed!
$status = true;
$info = 'syntax error in an .htaccess causes crash';
} else {
// It did not crash. So the .htaccess is not processed, as syntax errors
// makes servers crash
$status = false;
$info = 'syntax error in an .htaccess does not cause crash';
}
}
}
return new TestResult($status, $info);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace HtaccessCapabilityTester\Testers;
use \HtaccessCapabilityTester\TestResult;
/**
* Class for testing if an innocent request for a txt file succeeds
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class InnocentRequestTester extends CustomTester
{
public function __construct()
{
$test = [
'subdir' => 'innocent-request',
'files' => [
['request-me.txt', 'thank you my dear'],
],
'request' => [
'url' => 'request-me.txt',
'bypass-standard-error-handling' => ['all']
],
'interpretation' => [
['success', 'status-code', 'equals', '200'],
['inconclusive', 'status-code', 'equals', '0'],
['inconclusive', 'status-code', 'equals', '403'],
['inconclusive', 'status-code', 'equals', '404'],
['failure'],
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,369 @@
<?php
namespace HtaccessCapabilityTester\Testers;
use \HtaccessCapabilityTester\TestResult;
/**
* Class for testing if a module is loaded.
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class ModuleLoadedTester extends AbstractTester
{
/* @var string A valid Apache module name (ie "rewrite") */
protected $moduleName;
/**
* Constructor.
*
* @return void
*/
public function __construct($moduleName)
{
$this->moduleName = $moduleName;
}
/**
* Child classes must implement this method, which tells which subdir the
* test files are to be put.
*
* @return string A subdir for the test files
*/
public function getSubDir()
{
return 'module-loaded/' . $this->moduleName;
}
/**
* Register the test files using the "registerTestFile" method
*
* @return void
*/
public function registerTestFiles()
{
// No test files for this test
}
private function getServerSignatureBasedTest()
{
// Test files, method : Using ServerSignature
// --------------------------------------------------
// Requires (in order not to be inconclusive):
// - Override: All
// - Status: Core
// - Directives: ServerSignature, IfModule
// - PHP?: Yes
$php = <<<'EOD'
<?php
if (isset($_SERVER['SERVER_SIGNATURE']) && ($_SERVER['SERVER_SIGNATURE'] != '')) {
echo 1;
} else {
echo 0;
}
EOD;
$htaccess = <<<'EOD'
# The beauty of this trick is that ServerSignature is available in core.
# (it requires no modules and cannot easily be made forbidden)
# However, it requires PHP to check for the effect
ServerSignature Off
<IfModule mod_xxx.c>
ServerSignature On
</IfModule>
EOD;
$htaccess = str_replace('mod_xxx', 'mod_' . $this->moduleName, $htaccess);
return [
'subdir' => $this->getSubDir() . '/server-signature',
'files' => [
['.htaccess', $htaccess],
['test.php', $php],
],
'request' => 'test.php',
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
// This time we do not fail for 500 because it is very unlikely that any of the
// directives used are forbidden
]
];
}
/**
* @return array
*/
private function getRewriteBasedTest()
{
// Test files, method: Using Rewrite
// --------------------------------------------------
// Requires (in order not to be inconclusive)
// - Module: mod_rewrite
// - Override: FileInfo
// - Directives: RewriteEngine, RewriteRule and IfModule
// - PHP?: No
$htaccess = <<<'EOD'
RewriteEngine On
<IfModule mod_xxx.c>
RewriteRule ^request-me\.txt$ 1.txt [L]
</IfModule>
<IfModule !mod_xxx.c>
RewriteRule ^request-me\.txt$ 0.txt [L]
</IfModule>
EOD;
$htaccess = str_replace('mod_xxx', 'mod_' . $this->moduleName, $htaccess);
return [
'subdir' => $this->getSubDir() . '/rewrite',
'files' => [
['.htaccess', $htaccess],
['0.txt', '0'],
['1.txt', '1'],
['request-me.txt', 'Redirect failed even though rewriting has been proven to work. Strange!'],
],
'request' => 'request-me.txt',
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
//['inconclusive', 'status-code', 'not-equals', '200'],
]
];
}
/**
* @return array
*/
private function getHeaderSetBasedTest()
{
// Test files, method: Using Response Header
// --------------------------------------------------
// Requires (in order not to be inconclusive)
// - Module: mod_headers
// - Override: FileInfo
// - Directives: Header and IfModule
// - PHP?: No
$htaccess = <<<'EOD'
<IfModule mod_xxx.c>
Header set X-Response-Header-Test: 1
</IfModule>
<IfModule !mod_xxx.c>
Header set X-Response-Header-Test: 0
</IfModule>
EOD;
$htaccess = str_replace('mod_xxx', 'mod_' . $this->moduleName, $htaccess);
return [
'subdir' => $this->getSubDir() . '/header-set',
'files' => [
['.htaccess', $htaccess],
['request-me.txt', 'thanks'],
],
'request' => 'request-me.txt',
'interpretation' => [
['success', 'headers', 'contains-key-value', 'X-Response-Header-Test', '1'],
['failure', 'headers', 'contains-key-value', 'X-Response-Header-Test', '0'],
]
];
}
/**
* @return array
*/
private function getContentDigestBasedTest()
{
// Test files, method: Using ContentDigest
// --------------------------------------------------
//
// Requires (in order not to be inconclusive)
// - Module: None - its in core
// - Override: Options
// - Directives: ContentDigest
// - PHP?: No
$htaccess = <<<'EOD'
<IfModule mod_xxx.c>
ContentDigest On
</IfModule>
<IfModule !mod_xxx.c>
ContentDigest Off
</IfModule>
EOD;
$htaccess = str_replace('mod_xxx', 'mod_' . $this->moduleName, $htaccess);
return [
'subdir' => $this->getSubDir() . '/content-digest',
'files' => [
['.htaccess', $htaccess],
['request-me.txt', 'thanks'],
],
'request' => 'request-me.txt',
'interpretation' => [
['success', 'headers', 'contains-key', 'Content-MD5'],
['failure', 'headers', 'not-contains-key', 'Content-MD5'],
]
];
}
/**
* @return array
*/
private function getDirectoryIndexBasedTest()
{
// Test files, method: Using DirectoryIndex
// --------------------------------------------------
//
// Requires (in order not to be inconclusive)
// - Module: mod_dir (Status: Base)
// - Override: Indexes
// - Directives: DirectoryIndex
// - PHP?: No
$htaccess = <<<'EOD'
<IfModule mod_xxx.c>
DirectoryIndex 1.html
</IfModule>
<IfModule !mod_xxx.c>
DirectoryIndex 0.html
</IfModule>
EOD;
$htaccess = str_replace('mod_xxx', 'mod_' . $this->moduleName, $htaccess);
return [
'subdir' => $this->getSubDir() . '/directory-index',
'files' => [
['.htaccess', $htaccess],
['0.html', '0'],
['1.html', '1'],
],
'request' => '', // empty - in order to request the index
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
]
];
}
/**
* @return array
*/
private function getAddTypeBasedTest()
{
// Test files, method: Using AddType
// --------------------------------------------------
//
// Requires (in order not to be inconclusive)
// - Module: mod_mime
// - Override: FileInfo
// - Directives: AddType and IfModule
// - PHP?: No
$htaccess = <<<'EOD'
<IfModule mod_xxx.c>
AddType image/gif .test
</IfModule>
<IfModule !mod_xxx.c>
AddType image/jpeg .test
</IfModule>
EOD;
$htaccess = str_replace('mod_xxx', 'mod_' . $this->moduleName, $htaccess);
return [
'subdir' => $this->getSubDir() . '/add-type',
'files' => [
['.htaccess', $htaccess],
['request-me.test', 'hi'],
],
'request' => 'request-me.test',
'interpretation' => [
['success', 'headers', 'contains-key-value', 'Content-Type', 'image/gif'],
['failure', 'headers', 'contains-key-value', 'Content-Type', 'image/jpeg'],
]
];
}
/**
* @return bool|null
*/
private function run2()
{
$hct = $this->getHtaccessCapabilityTester();
$testResult = $hct->customTest($this->getServerSignatureBasedTest());
if (!is_null($testResult)) {
// PHP
return $testResult;
}
if ($hct->contentDigestWorks()) {
// Override: Options
return $hct->customTest($this->getContentDigestBasedTest());
}
if ($hct->addTypeWorks()) {
// Override: FileInfo, Status: Base (mod_mime)
return $hct->customTest($this->getAddTypeBasedTest());
}
if ($hct->directoryIndexWorks()) {
// Override: Indexes, Status: Base (mod_dir)
return $hct->customTest($this->getDirectoryIndexBasedTest());
}
if ($hct->rewriteWorks()) {
// Override: FileInfo, Module: mod_rewrite
return $hct->customTest($this->getRewriteBasedTest());
}
if ($hct->headerSetWorks()) {
//Override: FileInfo, Module: mod_headers
return $hct->customTest($this->getHeaderSetBasedTest());
}
return null;
}
/**
* Run the test.
*
* @param string $baseDir Directory on the server where the test files can be put
* @param string $baseUrl The base URL of the test files
*
* @return TestResult Returns a test result
*/
public function run($baseDir, $baseUrl)
{
$this->prepareForRun($baseDir, $baseUrl);
$hct = $this->getHtaccessCapabilityTester();
$htaccessEnabledTest = $hct->htaccessEnabled();
if ($htaccessEnabledTest === false) {
return new TestResult(false, '.htaccess files are ignored');
} elseif (is_null($htaccessEnabledTest)) {
// We happen to know that if that test cannot establish anything,
// then none of the usual weapons works - we can surrender right away
return new TestResult(null, 'no methods available - we surrender early');
}
$status = $this->run2();
if (is_null($status)) {
return new TestResult(null, 'no methods worked');
} else {
return new TestResult($status, '');
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if an environment variable can be set in a rewrite rule and received in PHP.
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class PassInfoFromRewriteToScriptThroughEnvTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
<IfModule mod_rewrite.c>
# Testing if we can pass environment variable from .htaccess to script in a RewriteRule
# We pass document root, because that can easily be checked by the script
RewriteEngine On
RewriteRule ^test\.php$ - [E=PASSTHROUGHENV:%{DOCUMENT_ROOT},L]
</IfModule>
EOD;
$phpFile = <<<'EOD'
<?php
/**
* Get environment variable set with mod_rewrite module
* Return false if the environment variable isn't found
*/
function getEnvPassedInRewriteRule($envName) {
// Environment variables passed through the REWRITE module have "REWRITE_" as a prefix
// (in Apache, not Litespeed, if I recall correctly).
// Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
// We simply look for an environment variable that ends with what we are looking for.
// (so make sure to make it unique)
$len = strlen($envName);
foreach ($_SERVER as $key => $item) {
if (substr($key, -$len) == $envName) {
return $item;
}
}
return false;
}
$result = getEnvPassedInRewriteRule('PASSTHROUGHENV');
if ($result === false) {
echo '0';
exit;
}
echo ($result == $_SERVER['DOCUMENT_ROOT'] ? '1' : '0');
EOD;
$test = [
'subdir' => 'pass-info-from-rewrite-to-script-through-env',
'files' => [
['.htaccess', $htaccessFile],
['test.php', $phpFile],
],
'request' => 'test.php',
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
['inconclusive', 'body', 'begins-with', '<' . '?php'],
['inconclusive']
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Say you have a rewrite rule that points to a PHP script and you would like to pass some information
* along to the PHP. Usually, you will just pass it in the query string. But this won't do if the information
* is sensitive. In that case, there are some tricks available. The trick being tested here sets tells the
* RewriteRule directive to set an environment variable which a RequestHeader directive picks up on and passes
* on to the script in a request header.
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class PassInfoFromRewriteToScriptThroughRequestHeaderTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
<IfModule mod_rewrite.c>
RewriteEngine On
# We pass document root, because that can easily be checked by the script
RewriteRule ^test\.php$ - [E=PASSTHROUGHHEADER:%{DOCUMENT_ROOT},L]
<IfModule mod_headers.c>
RequestHeader set PASSTHROUGHHEADER "%{PASSTHROUGHHEADER}e" env=PASSTHROUGHHEADER
</IfModule>
</IfModule>
EOD;
$phpFile = <<<'EOD'
<?php
if (isset($_SERVER['HTTP_PASSTHROUGHHEADER'])) {
echo ($_SERVER['HTTP_PASSTHROUGHHEADER'] == $_SERVER['DOCUMENT_ROOT'] ? 1 : 0);
exit;
}
echo '0';
EOD;
$test = [
'subdir' => 'pass-info-from-rewrite-to-script-through-request-header',
'files' => [
['.htaccess', $htaccessFile],
['test.php', $phpFile],
],
'request' => 'test.php',
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
['inconclusive', 'body', 'begins-with', '<' . '?php'],
['inconclusive']
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if RequestHeader works
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class RequestHeaderTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
<IfModule mod_headers.c>
# Certain hosts seem to strip non-standard request headers,
# so we use a standard one to avoid a false negative
RequestHeader set User-Agent "request-header-test"
</IfModule>
EOD;
$phpFile = <<<'EOD'
<?php
if (isset($_SERVER['HTTP_USER_AGENT'])) {
echo (($_SERVER['HTTP_USER_AGENT'] == 'request-header-test') ? "1" : "0");
} else {
echo "0";
}
EOD;
// PS:
// There is a little edge case: When .htaccess is disabled AND phps are either not processed
// or access is denied. This ought to return *failure*, but it currently returns *inconclusive*.
$test = [
'subdir' => 'request-header',
'files' => [
['.htaccess', $htaccessFile],
['test.php', $phpFile],
],
'request' => 'test.php',
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
['inconclusive', 'body', 'begins-with', '<' . '?php'],
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if rewriting works at the tested location.
*
* The tester reports success when:
* - a rewrite is proven to be working
*
* The tester reports failure when:
* - Server does not have mod_rewrite installed
* - Server is set up to ignore .htaccess files in the directory
* - Server disallows any the following directives in the directory: RewriteEngine, Rewrite, IfModule
* (if disallowed, the result is either a 500 Internal Server Error or that the directive is
* ignored, depending on whether Nonfatal is set)
* - The request results in a 500 Internal Server Error due to another problem than a disallowed
* directive (this is, there is a risk for a false negative)
*
* The test works by creating an .htaccess which redirects requests to "0.txt"
* to "1.txt" and then requesting "0.txt".
*
* Notes:
* - The test might result in the following being written to the error log:
* "RewriteEngine not allowed here"
* - We are not redirecting to a php, because that would additionally require phps
* to be run in that directory
* - We are wrapping the .htaccess directives in a "<IfModule mod_rewrite.c>" and therefore this test
* also relies on the IfModule directive being allowed. It probably usually is, as it is harmless.
* Also, it is good practice to use it, so in most cases it is good that this is checked
* too. Actually, the <IfModule> wrap isn't neccessary for our test to work, as the test
* identifies a 500 Internal Error as test failure. However, not having the wrap would
* cause the test to generate an entry in the error log when mod_rewrite isn't installed
* (regardless if overrides are configured to Nonfatal or not):
* "Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included
* in the server configuration"
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class RewriteTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$htaccessFile = <<<'EOD'
# Testing for mod_rewrite
# -----------------------
# If mod_rewrite is enabled, redirect to 1.txt, which returns "1".
# If mod_rewrite is disabled, the rewriting fails, and we end at 0.txt, which returns "0".
#
# Notes:
# - We are not redirecting to a php, because that would additionally require phps
# to be run in that directory
# - We are wrapping it in a "<IfModule mod_rewrite.c>" and therefore this test also relies
# on the IfModule directive being allowed. It probably usually is, as it is harmless.
# Also, it is good practice to use it, so in most cases it is good that this is checked
# too. Actually, the <IfModule> wrap isn't neccessary for our test to work, as the test
# identifies a 500 Internal Error as test failure. However, not having the wrap would
# cause the test to generate an entry in the error log when mod_rewrite isn't installed
# (regardless if configured to Nonfatal or not): "Invalid command 'RewriteEngine', perhaps
# misspelled or defined by a module not included
# in the server configuration"
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^0\.txt$ 1\.txt [L]
</IfModule>
EOD;
$test = [
'subdir' => 'rewrite',
'files' => [
['.htaccess', $htaccessFile],
['0.txt', "0"],
['1.txt', "1"]
],
'request' => '0.txt',
'interpretation' => [
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
]
];
parent::__construct($test);
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace HtaccessCapabilityTester\Testers;
/**
* Class for testing if ServerSignature works
*
* Testing the ServerSignature directive is of interest because the directive is a core feature.
* If a core feature doesn't work, well, it it would seem that .htaccess files are disabled completely.
* The test is thus special. If it returns *failure* it is highly probable that the .htaccess file has
* not been read.
*
* Unfortunately, the test requires PHP to examine if a server variable has been set. So the test is not
* unlikely to come out inconclusive due to a 403 Forbidden.
*
* Note that the test assumes that the ServerSignature directive has not been disallowed even though
* it is technically possible to do so by setting *AllowOverride* to *None* and by setting *AllowOverrideList*
* to a list that does not include *ServerSignature*.
*
* @package HtaccessCapabilityTester
* @author Bjørn Rosell <it@rosell.dk>
* @since Class available since 0.7
*/
class ServerSignatureTester extends CustomTester
{
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$phpOn = <<<'EOD'
<?php
if (isset($_SERVER['SERVER_SIGNATURE']) && ($_SERVER['SERVER_SIGNATURE'] != '')) {
echo 1;
} else {
echo 0;
}
EOD;
$phpOff = <<<'EOD'
<?php
if (isset($_SERVER['SERVER_SIGNATURE']) && ($_SERVER['SERVER_SIGNATURE'] != '')) {
echo 0;
} else {
echo 1;
}
EOD;
// PS:
// There is a little edge case: When .htaccess is disabled AND phps are either not processed
// or access is denied. This ought to return *failure*, but it currently returns *inconclusive*.
$test = [
'subdir' => 'server-signature',
'subtests' => [
[
'subdir' => 'on',
'files' => [
['.htaccess', 'ServerSignature On'],
['test.php', $phpOn],
],
'request' => [
'url' => 'test.php',
],
'interpretation' => [
['inconclusive', 'body', 'isEmpty'],
['inconclusive', 'status-code', 'not-equals', '200'],
['failure', 'body', 'equals', '0'],
],
],
[
'subdir' => 'off',
'files' => [
['.htaccess', 'ServerSignature Off'],
['test.php', $phpOff],
],
'request' => 'test.php',
'interpretation' => [
['inconclusive', 'body', 'isEmpty'],
['success', 'body', 'equals', '1'],
['failure', 'body', 'equals', '0'],
['inconclusive']
]
]
]
];
parent::__construct($test);
}
}