diff --git a/.gitignore b/.gitignore index 1d9ec6a..243f776 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ test/composer.lock test/vendor .unfinished +.vscode + +# using the docker-compose example will auto-extract some embedded images. ignore. +test/fixtures/id3v2_artist_album_title_cover.jpg +test/fixtures/tagged_with_cover.jpg diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 48ab8ba..e0d3353 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,14 @@ Changelog ========= +1.37 2022-10-27 * Errors now return an HTTP status code 500 by default. + * If the error is due to no content, or a bad URL passed to + ?dir=, then it will be a 404 and no information about + the server paths will be returned in the output. Thanks + to @EdwarDDay for this security suggestion. (#64) + * fix nasty bug where paths were sometimes invalid due to + mishandling of trailing slashes (#55) + 1.36 2022-08-25 * Fix bug where podcasts with autosaved cover art would end up with duplicated iTunes metadata tags. Thanks once again to @EdwarDDay for the bug report. diff --git a/README.md b/README.md index 8d3cda5..5439d24 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Testing dir2cast](https://github.com/ben-xo/dir2cast/actions/workflows/testing.yml/badge.svg)](https://github.com/ben-xo/dir2cast/actions/workflows/testing.yml) -dir2cast by Ben XO v1.36 (2022-08-25) +dir2cast by Ben XO v1.37 (2022-10-27) ================================================================================ https://github.com/ben-xo/dir2cast/ diff --git a/dir2cast.php b/dir2cast.php index 45107a6..e8debf0 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -56,7 +56,7 @@ /* DEFAULTS *********************************************/ // error handler needs these, so let's set them now. -define('VERSION', '1.36'); +define('VERSION', '1.37'); define('DIR2CAST_HOMEPAGE', 'https://github.com/ben-xo/dir2cast/'); define('GENERATOR', 'dir2cast ' . VERSION . ' by Ben XO (' . DIR2CAST_HOMEPAGE . ')'); @@ -92,6 +92,8 @@ function __autoloader($class_name) /* CLASSES **********************************************/ +class ExitException extends Exception {} + abstract class GetterSetter { protected $parameters = array(); @@ -615,7 +617,7 @@ protected function stripBasePath($filename) { if(strlen(RSS_File_Item::$FILES_DIR) && strpos($filename, RSS_File_Item::$FILES_DIR) === 0) { - return ltrim(substr($filename, strlen(RSS_File_Item::$FILES_DIR)), '/'); + $filename = ltrim(substr($filename, strlen(RSS_File_Item::$FILES_DIR)), '/'); } return $filename; } @@ -1193,7 +1195,10 @@ protected function scan() self::$DEBUG && print("$item_count items added.\n"); if(self::$EMPTY_PODCAST_IS_ERROR && 0 == $item_count) - throw new Exception("No Items found in {$this->source_dir}"); + { + http_response_code(404); + throw new Exception("No content yet."); + } $this->calculateItemHash(); @@ -1573,17 +1578,25 @@ public static function get_primed_error($type) return 'dir2cast requires getID3. You should download this from ' . DIR2CAST_HOMEPAGE .' and install it with dir2cast.'; } } - + public static function display($message, $errfile, $errline) { if(self::$errors) { - if(!defined('CLI_ONLY') && !ini_get('html_errors')) + if(!defined('CLI_ONLY')) + { + if(!http_response_code()) + { + http_response_code(500); + } + } + + if((!defined('CLI_ONLY')) && !ini_get('html_errors')) { header("Content-type: text/plain"); // reset the content-type } - if(!defined('CLI_ONLY') && ini_get('html_errors')) + if((!defined('CLI_ONLY')) && ini_get('html_errors')) { header("Content-type: text/html"); // reset the content-type @@ -1603,14 +1616,16 @@ public static function display($message, $errfile, $errline)
@@ -1629,16 +1644,40 @@ public static function display($message, $errfile, $errline)
echo strip_tags(self::get_primed_error(ErrorHandler::$primer)) . "\n";
}
- exit(-1);
+ exit(-1); // can't throw - this is the exception handler
}
}
-
+
+ public static function display404($message)
+ {
+ if(defined('CLI_ONLY'))
+ {
+ http_response_code(404);
+ header("Content-type: text/plain");
+ }
+ throw new ExitException("Not Found: $message", -2);
+ }
}
class SettingsHandler
{
private static $settings_cache = array();
+ /**
+ * getopt() uses argv directly and is a pain to mock. It's nicer to pass argv in,
+ * but mocking it a pain.
+ */
+ public static function getopt($argv_in, $short_options, $long_options)
+ {
+ if(isset($GLOBALS['argv']) && $argv_in != $GLOBALS['argv'])
+ {
+ return fake_getopt($argv_in, $short_options, $long_options);
+ }
+ return getopt($short_options, $long_options);
+ }
+
+
+
/**
* This method sets up all app-wide settings that are required at initialization time.
*
@@ -1653,7 +1692,8 @@ public static function bootstrap(array $SERVER, array $GET, array $argv)
define('CLI_ONLY', true);
}
- if(defined('CLI_ONLY') && CLI_ONLY) {
+ // do not use DIR2CAST_BASE directly. use DIR2CAST_BASE()
+ if(defined('CLI_ONLY')) {
define('DIR2CAST_BASE', realpath(dirname($argv[0])));
} else {
define('DIR2CAST_BASE', dirname(__FILE__));
@@ -1662,15 +1702,15 @@ public static function bootstrap(array $SERVER, array $GET, array $argv)
// If an installation-wide config file exists, load it now.
// Installation-wide config can contain TMP_DIR, MP3_DIR and MIN_CACHE_TIME.
// Anything else it contains will be used as a fall-back if no dir-specific dir2cast.ini exists
- if(file_exists( DIR2CAST_BASE . '/dir2cast.ini' ))
+ if(file_exists( DIR2CAST_BASE() . 'dir2cast.ini' ))
{
- $ini_file_name = DIR2CAST_BASE . '/dir2cast.ini';
+ $ini_file_name = DIR2CAST_BASE() . 'dir2cast.ini';
self::load_from_ini( $ini_file_name );
self::finalize(array('TMP_DIR', 'MP3_BASE', 'MP3_DIR', 'MIN_CACHE_TIME', 'FORCE_PASSWORD'));
define('INI_FILE', $ini_file_name);
}
- $cli_options = getopt('', array('help', 'media-dir::', 'media-url::', 'output::', 'dont-uncache', 'min-file-age::', 'debug', 'ignore-dir2cast-mtime', 'clock-offset::'));
+ $cli_options = self::getopt($argv, '', array('help', 'media-dir::', 'media-url::', 'output::', 'dont-uncache', 'min-file-age::', 'debug', 'ignore-dir2cast-mtime', 'clock-offset::'));
if($cli_options) {
if(isset($cli_options['help'])) {
print "Usage: php dir2cast.php [--help] [--media-dir=MP3_DIR] [--media-url=MP3_URL] [--output=OUTPUT_FILE]\n";
@@ -1686,7 +1726,13 @@ public static function bootstrap(array $SERVER, array $GET, array $argv)
}
if(!defined('MP3_DIR') && !empty($cli_options['media-dir']))
{
- define('MP3_DIR', realpath($cli_options['media-dir']));
+
+ if(!is_dir($cli_options['media-dir']) or !is_readable($cli_options['media-dir']))
+ {
+ ErrorHandler::display404($cli_options['media-dir']);
+ }
+ // do not use MP3_DIR directly. use MP3_DIR()
+ define('MP3_DIR', slashdir(realpath($cli_options['media-dir'])));
}
if(!defined('MP3_URL') && !empty($cli_options['media-url']))
{
@@ -1725,25 +1771,32 @@ public static function bootstrap(array $SERVER, array $GET, array $argv)
define('FORCE_PASSWORD', '');
if(!defined('TMP_DIR')) {
- define('TMP_DIR', DIR2CAST_BASE . '/temp');
+ define('TMP_DIR', DIR2CAST_BASE() . 'temp');
}
+ // do not use MP3_BASE directly. use MP3_BASE()
if(!defined('MP3_BASE'))
{
if(!empty($SERVER['HTTP_HOST']))
define('MP3_BASE', dirname($SERVER['SCRIPT_FILENAME']));
else
- define('MP3_BASE', DIR2CAST_BASE);
+ define('MP3_BASE', DIR2CAST_BASE());
}
-
+
+ // do not use MP3_DIR directly. use MP3_DIR()
if(!defined('MP3_DIR'))
{
if(!empty($GET['dir']))
- define('MP3_DIR', MP3_BASE . '/' . safe_path(magic_stripslashes($GET['dir'])));
+ {
+ define('MP3_DIR', MP3_BASE() . safe_path(magic_stripslashes($GET['dir'])));
+ if(!is_dir(MP3_DIR()) or !is_readable(MP3_DIR()))
+ {
+ ErrorHandler::display404($GET['dir']);
+ }
+ }
else
- define('MP3_DIR', MP3_BASE);
+ define('MP3_DIR', MP3_BASE());
}
-
}
/**
@@ -1753,15 +1806,14 @@ public static function defaults(array $SERVER)
{
// if an MP3_DIR specific config file exists, load it now, as long as it's not the same file as the global one!
if(
- file_exists( MP3_DIR . '/dir2cast.ini' ) and
- realpath(DIR2CAST_BASE . '/dir2cast.ini') != realpath( MP3_DIR . '/dir2cast.ini' )
+ file_exists( MP3_DIR() . 'dir2cast.ini' ) and
+ realpath(DIR2CAST_BASE() . 'dir2cast.ini') != realpath( MP3_DIR() . 'dir2cast.ini' )
) {
- self::load_from_ini( MP3_DIR . '/dir2cast.ini' );
+ self::load_from_ini( MP3_DIR() . 'dir2cast.ini' );
}
self::finalize();
-
if(!defined('MP3_URL'))
{
# This works on the principle that MP3_DIR must be under DOCUMENT_ROOT (otherwise how will you serve the MP3s?)
@@ -1770,18 +1822,18 @@ public static function defaults(array $SERVER)
if(!empty($SERVER['HTTP_HOST']))
{
- $path_part = substr(MP3_DIR, strlen($SERVER['DOCUMENT_ROOT']));
+ $path_part = substr(MP3_DIR(), strlen(slashdir($SERVER['DOCUMENT_ROOT'])));
define('MP3_URL',
- 'http' . (!empty($SERVER['HTTPS']) ? 's' : '') . '://' . $SERVER['HTTP_HOST'] . '/' . ltrim( rtrim( $path_part, '/' ) . '/', '/' ));
+ 'http' . (!empty($SERVER['HTTPS']) ? 's' : '') . '://' . $SERVER['HTTP_HOST'] . '/' . ltrim( slashdir( $path_part ), '/' ));
}
else
- define('MP3_URL', 'file://' . MP3_DIR );
+ define('MP3_URL', 'file://' . MP3_DIR() );
}
if(!defined('TITLE'))
{
- if(basename(MP3_DIR))
- define('TITLE', basename(MP3_DIR));
+ if(basename(MP3_DIR()))
+ define('TITLE', basename(MP3_DIR()));
else
define('TITLE', 'My First dir2cast Podcast');
}
@@ -1804,10 +1856,10 @@ public static function defaults(array $SERVER)
if(!defined('DESCRIPTION'))
{
- if(file_exists(MP3_DIR . '/description.txt'))
- define('DESCRIPTION', file_get_contents(MP3_DIR . '/description.txt'));
- elseif(file_exists(DIR2CAST_BASE . '/description.txt'))
- define('DESCRIPTION', file_get_contents(DIR2CAST_BASE . '/description.txt'));
+ if(file_exists(MP3_DIR() . 'description.txt'))
+ define('DESCRIPTION', file_get_contents(MP3_DIR() . 'description.txt'));
+ elseif(file_exists(DIR2CAST_BASE() . 'description.txt'))
+ define('DESCRIPTION', file_get_contents(DIR2CAST_BASE() . 'description.txt'));
else
define('DESCRIPTION', 'Podcast');
}
@@ -1829,33 +1881,33 @@ public static function defaults(array $SERVER)
if(!defined('ITUNES_SUBTITLE'))
{
- if(file_exists(MP3_DIR . '/itunes_subtitle.txt'))
- define('ITUNES_SUBTITLE', file_get_contents(MP3_DIR . '/itunes_subtitle.txt'));
- elseif(file_exists(DIR2CAST_BASE . '/itunes_subtitle.txt'))
- define('ITUNES_SUBTITLE', file_get_contents(DIR2CAST_BASE . '/itunes_subtitle.txt'));
+ if(file_exists(MP3_DIR() . 'itunes_subtitle.txt'))
+ define('ITUNES_SUBTITLE', file_get_contents(MP3_DIR() . 'itunes_subtitle.txt'));
+ elseif(file_exists(DIR2CAST_BASE() . 'itunes_subtitle.txt'))
+ define('ITUNES_SUBTITLE', file_get_contents(DIR2CAST_BASE() . 'itunes_subtitle.txt'));
else
define('ITUNES_SUBTITLE', DESCRIPTION);
}
if(!defined('ITUNES_SUMMARY'))
{
- if(file_exists(MP3_DIR . '/itunes_summary.txt'))
- define('ITUNES_SUMMARY', file_get_contents(MP3_DIR . '/itunes_summary.txt'));
- elseif(file_exists(DIR2CAST_BASE . '/itunes_summary.txt'))
- define('ITUNES_SUMMARY', file_get_contents(DIR2CAST_BASE . '/itunes_summary.txt'));
+ if(file_exists(MP3_DIR() . 'itunes_summary.txt'))
+ define('ITUNES_SUMMARY', file_get_contents(MP3_DIR() . 'itunes_summary.txt'));
+ elseif(file_exists(DIR2CAST_BASE() . 'itunes_summary.txt'))
+ define('ITUNES_SUMMARY', file_get_contents(DIR2CAST_BASE() . 'itunes_summary.txt'));
else
define('ITUNES_SUMMARY', DESCRIPTION);
}
if(!defined('IMAGE'))
{
- if(file_exists(rtrim(MP3_DIR, '/') . '/image.jpg'))
+ if(file_exists(MP3_DIR() . 'image.jpg'))
define('IMAGE', rtrim(MP3_URL, '/') . '/image.jpg');
- elseif(file_exists(rtrim(MP3_DIR, '/') . '/image.png'))
+ elseif(file_exists(MP3_DIR() . 'image.png'))
define('IMAGE', rtrim(MP3_URL, '/') . '/image.png');
- elseif(file_exists(DIR2CAST_BASE . '/image.jpg'))
+ elseif(file_exists(DIR2CAST_BASE() . 'image.jpg'))
define('IMAGE', rtrim(MP3_URL, '/') . '/image.jpg');
- elseif(file_exists(DIR2CAST_BASE . '/image.png'))
+ elseif(file_exists(DIR2CAST_BASE() . 'image.png'))
define('IMAGE', rtrim(MP3_URL, '/') . '/image.png');
else
define('IMAGE', '');
@@ -1863,13 +1915,13 @@ public static function defaults(array $SERVER)
if(!defined('ITUNES_IMAGE'))
{
- if(file_exists(rtrim(MP3_DIR, '/') . '/itunes_image.jpg'))
+ if(file_exists(MP3_DIR() . 'itunes_image.jpg'))
define('ITUNES_IMAGE', rtrim(MP3_URL, '/') . '/itunes_image.jpg');
- elseif(file_exists(rtrim(MP3_DIR, '/') . '/itunes_image.png'))
+ elseif(file_exists(MP3_DIR() . 'itunes_image.png'))
define('ITUNES_IMAGE', rtrim(MP3_URL, '/') . '/itunes_image.png');
- elseif(file_exists(DIR2CAST_BASE . '/itunes_image.jpg'))
+ elseif(file_exists(DIR2CAST_BASE() . 'itunes_image.jpg'))
define('ITUNES_IMAGE', rtrim(MP3_URL, '/') . '/itunes_image.jpg');
- elseif(file_exists(DIR2CAST_BASE . '/itunes_image.png'))
+ elseif(file_exists(DIR2CAST_BASE() . 'itunes_image.png'))
define('ITUNES_IMAGE', rtrim(MP3_URL, '/') . '/itunes_image.png');
else
define('ITUNES_IMAGE', '');
@@ -1926,7 +1978,7 @@ public static function defaults(array $SERVER)
define('CLOCK_OFFSET', 0);
// Set up factory settings for Podcast subclasses
- Dir_Podcast::$EMPTY_PODCAST_IS_ERROR = !defined('CLI_ONLY') || !CLI_ONLY;
+ Dir_Podcast::$EMPTY_PODCAST_IS_ERROR = !defined('CLI_ONLY');
Dir_Podcast::$RECURSIVE_DIRECTORY_ITERATOR = RECURSIVE_DIRECTORY_ITERATOR;
Dir_Podcast::$ITEM_COUNT = ITEM_COUNT;
Dir_Podcast::$MIN_FILE_AGE = MIN_FILE_AGE;
@@ -1937,7 +1989,7 @@ public static function defaults(array $SERVER)
// Set up up factory settings for RSS Items
RSS_File_Item::$FILES_URL = MP3_URL; // TODO: rename this to MEDIA_URL
- RSS_File_Item::$FILES_DIR = MP3_DIR; // TODO: rename this to MEDIA_DIR
+ RSS_File_Item::$FILES_DIR = MP3_DIR(); // TODO: rename this to MEDIA_DIR
Media_RSS_Item::$LONG_TITLES = LONG_TITLES;
Media_RSS_Item::$DESCRIPTION_SOURCE = DESCRIPTION_SOURCE;
}
@@ -2019,10 +2071,10 @@ public function update_mtime_if_metadata_files_modified()
);
foreach($metadata_files as $file)
{
- $filepath = rtrim(MP3_DIR, '/') . '/' . $file;
+ $filepath = MP3_DIR() . $file;
if(!file_exists($filepath))
{
- $filepath = DIR2CAST_BASE . '/' . $file;
+ $filepath = DIR2CAST_BASE() . $file;
}
if(!file_exists($filepath))
{
@@ -2077,7 +2129,7 @@ public function output()
if(!defined('OUTPUT_FILE'))
{
$output = $podcast->generate();
- if(!defined('CLI_ONLY') || !CLI_ONLY)
+ if(!defined('CLI_ONLY'))
{
$podcast->http_headers(strlen($output));
}
@@ -2147,6 +2199,23 @@ function utf8_for_xml($s)
return preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', '', $s);
}
+function slashdir($dir)
+{
+ return rtrim($dir, '/') . '/';
+}
+
+function DIR2CAST_BASE() {
+ return slashdir(DIR2CAST_BASE);
+}
+
+function MP3_BASE() {
+ return slashdir(MP3_BASE);
+}
+
+function MP3_DIR() {
+ return slashdir(MP3_DIR);
+}
+
/* DISPATCH *********************************************/
function main($args)
@@ -2161,7 +2230,7 @@ function main($args)
empty($_SERVER) ? array() : $_SERVER
);
- $podcast = new Locking_Cached_Dir_Podcast(MP3_DIR, TMP_DIR);
+ $podcast = new Locking_Cached_Dir_Podcast(MP3_DIR(), TMP_DIR);
$podcast->setClockOffset(CLOCK_OFFSET);
$dispatcher = new Dispatcher($podcast);
@@ -2181,7 +2250,15 @@ function main($args)
if(isset($GLOBALS['argv'])) {
$args = $argv;
}
- exit(main($args));
+ try
+ {
+ exit(main($args));
+ }
+ catch(ExitException $e)
+ {
+ print($e->getMessage()."\n");
+ exit($e->getCode());
+ }
}
/* THE END *********************************************/
diff --git a/docker-compose/nginx/default.conf b/docker-compose/nginx/default.conf
index a918f54..f2ab099 100644
--- a/docker-compose/nginx/default.conf
+++ b/docker-compose/nginx/default.conf
@@ -8,7 +8,7 @@ server {
# Don't allow downloading of dir2cast.ini, as it may contain sensitive
# info such as the refresh password. Also, don't allow downloading of
# dir2cast.php, for security and privacy reasons.
- location ~ /dir2cast\.(ini|php)$ {
+ location ~ \.(ini|php)$ {
return 404;
}
diff --git a/test/FakeGetoptTest.php b/test/FakeGetoptTest.php
new file mode 100644
index 0000000..c3769b2
--- /dev/null
+++ b/test/FakeGetoptTest.php
@@ -0,0 +1,74 @@
+assertEquals(
+ fake_getopt(array('php', '--halp'), '', array()),
+ array()
+ );
+ $this->assertEquals(
+ fake_getopt(array('php'), '', array()),
+ array()
+ );
+ }
+ public function test_fake_getopt_no_match()
+ {
+ $this->assertEquals(
+ fake_getopt(array('php', '--halp'), '', array('help')),
+ array()
+ );
+ $this->assertEquals(
+ fake_getopt(array('php'), '', array('help')),
+ array()
+ );
+ }
+
+ public function test_fake_getopt_bool_arg()
+ {
+ $this->assertEquals(
+ fake_getopt(array('php', '--help'), '', array('help')),
+ array('help' => false)
+ );
+ }
+ public function test_fake_getopt_string_arg()
+ {
+ $this->assertEquals(
+ fake_getopt(array('php', '--media-dir'), '', array('media-dir::')),
+ array('media-dir' => '')
+ );
+ $this->assertEquals(
+ fake_getopt(array('php', '--media-dir='), '', array('media-dir::')),
+ array() // XXX: seems to be a bug in getopt
+ );
+ $this->assertEquals(
+ fake_getopt(array('php', '--media-dir=test'), '', array('media-dir::')),
+ array('media-dir' => 'test')
+ );
+ }
+ public function test_fake_getopt_escaping()
+ {
+ $this->assertEquals(
+ fake_getopt(array('php', "--media-dir= "), '', array('media-dir::')),
+ array('media-dir' => ' ')
+ );
+ $this->assertEquals(
+ fake_getopt(array('php', '--media-dir=""'), '', array('media-dir::')),
+ array('media-dir' => '""')
+ );
+ $this->assertEquals(
+ fake_getopt(array('php', "--media-dir=''"), '', array('media-dir::')),
+ array('media-dir' => "''")
+ );
+ }
+ public function test_fake_getopt_both_arg_types()
+ {
+ $this->assertEquals(
+ fake_getopt(array('php', '--help', '--media-dir'), '', array('help', 'media-dir::')),
+ array('help' => false, 'media-dir' => '')
+ );
+ }
+}
diff --git a/test/FourOhFourTest.php b/test/FourOhFourTest.php
new file mode 100644
index 0000000..3f2214d
--- /dev/null
+++ b/test/FourOhFourTest.php
@@ -0,0 +1,23 @@
+assertEquals("Not Found: dir2cast.ini", implode("\n", $output));
+ $this->assertEquals(254, $returncode); // 254 is -2
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ chdir('..');
+ }
+}
diff --git a/test/SettingsHandlerTest.php b/test/SettingsHandlerTest.php
index dd7f052..9a8699e 100644
--- a/test/SettingsHandlerTest.php
+++ b/test/SettingsHandlerTest.php
@@ -42,7 +42,64 @@ class SettingsHandlerTest extends TestCase
'DONT_UNCACHE_IF_OUTPUT_FILE',
'MIN_FILE_AGE',
);
+
+ public $temp_file = false;
+ public $starting_dir = false;
+
+ public function setUp(): void
+ {
+ $this->temp_file = false;
+ $this->starting_dir = false;
+ }
+ public function test_getopt_hook()
+ {
+ $argv_copy = $GLOBALS['argv'];
+ $argc_copy = $GLOBALS['argc'];
+
+ $short_options = '';
+ $long_options = array('help', 'media-dir::', 'bootstrap');
+
+ $cli_options = SettingsHandler::getopt(
+ array(),
+ $short_options, $long_options
+ );
+ $this->assertEquals($cli_options, array());
+
+ $cli_options = SettingsHandler::getopt(
+ array('dir2cast.php'),
+ $short_options, $long_options
+ );
+ $this->assertEquals($cli_options, array());
+
+ $cli_options = SettingsHandler::getopt(
+ array('dir2cast.php', '--help'),
+ $short_options, $long_options
+ );
+ $this->assertEquals($cli_options, array('help' => false));
+
+ $cli_options = SettingsHandler::getopt(
+ array('dir2cast.php', '--media-dir=test1'),
+ $short_options, $long_options
+ );
+ $this->assertEquals($cli_options, array('media-dir' => 'test1'));
+
+ $cli_options = SettingsHandler::getopt(
+ array('dir2cast.php', '--media-dir=test2', '--bootstrap'),
+ $short_options, $long_options
+ );
+ $this->assertEquals($cli_options, array('media-dir' => 'test2', 'bootstrap' => false));
+
+ $cli_options = SettingsHandler::getopt(
+ array('dir2cast.php', '--bootstrap', '--media-dir=test3'),
+ $short_options, $long_options
+ );
+ $this->assertEquals($cli_options, array('media-dir' => 'test3', 'bootstrap' => false));
+
+ $this->assertEquals($argv_copy, $GLOBALS['argv']);
+ $this->assertEquals($argc_copy, $GLOBALS['argc']);
+ }
+
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
@@ -80,7 +137,34 @@ public function test_default_defines_set()
// should not be defined as $argv was empty
$this->assertFalse(defined('CLI_ONLY'));
- $this->assertEquals(DIR2CAST_BASE, realpath('..')); // from bootstrap.php
+ $this->assertEquals(DIR2CAST_BASE(), slashdir(realpath('..'))); // from bootstrap.php
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_pre_defined_slashed()
+ {
+ define('DIR2CAST_BASE', '/tmp/');
+ $this->assertEquals(DIR2CAST_BASE(), '/tmp/');
+ define('MP3_BASE', '/tmp/');
+ $this->assertEquals(DIR2CAST_BASE(), '/tmp/');
+ define('MP3_PATH', '/tmp/');
+ $this->assertEquals(DIR2CAST_BASE(), '/tmp/');
+ }
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_pre_defined_slashless()
+ {
+ define('DIR2CAST_BASE', '/tmp');
+ $this->assertEquals(DIR2CAST_BASE(), '/tmp/');
+ define('MP3_BASE', '/tmp');
+ $this->assertEquals(DIR2CAST_BASE(), '/tmp/');
+ define('MP3_PATH', '/tmp');
+ $this->assertEquals(DIR2CAST_BASE(), '/tmp/');
}
/**
@@ -115,23 +199,24 @@ public function test_defines_CLI_ONLY_if_argv0()
$this->assertFalse(defined('CLI_ONLY'));
SettingsHandler::bootstrap(array(), array(), array('dir2cast.php'));
$this->assertTrue(defined('CLI_ONLY'));
- $this->assertEquals(DIR2CAST_BASE, getcwd()); // from fake $argv
+ $this->assertEquals(DIR2CAST_BASE(), slashdir(getcwd())); // from fake $argv
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
- * @testWith [null]
- * ["dir2cast.php"]
+ * @testWith [null, null]
+ * ["dir2cast.php", null]
+ * ["dir2cast.php", "--media-dir="]
*/
- public function test_bootstrap_sets_sensible_global_defaults_for_entire_installation($argv0)
+ public function test_bootstrap_sets_sensible_global_defaults_for_entire_installation($argv0, $argv1)
{
- SettingsHandler::bootstrap(array(), array(), array($argv0));
+ SettingsHandler::bootstrap(array(), array(), array($argv0, $argv1));
$this->assertEquals(MIN_CACHE_TIME, 5);
$this->assertEquals(FORCE_PASSWORD, '');
- $this->assertEquals(TMP_DIR, DIR2CAST_BASE . '/temp');
- $this->assertEquals(MP3_BASE, DIR2CAST_BASE);
- $this->assertEquals(MP3_DIR, DIR2CAST_BASE);
+ $this->assertEquals(TMP_DIR, DIR2CAST_BASE() . 'temp');
+ $this->assertEquals(MP3_BASE(), DIR2CAST_BASE());
+ $this->assertEquals(MP3_DIR(), DIR2CAST_BASE());
}
/**
@@ -149,10 +234,255 @@ public function test_when_SERVER_HTTP_HOST_then_MP3_BASE_defaults_to_same_dir()
/* $GET */ array(),
/* $argv */ array()
);
- $this->assertEquals(MP3_BASE, '/var/www');
- $this->assertEquals(MP3_DIR, '/var/www');
+ $this->assertEquals(MP3_BASE(), '/var/www/');
+ $this->assertEquals(MP3_DIR(), '/var/www/');
}
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_cli_media_404()
+ {
+ $this->temp_file = basename(tempnam('./', 'test_cli_media_404'));
+ $this->assertFalse(strpos($this->temp_file, '/'));
+ unlink($this->temp_file);
+
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: {$this->temp_file}");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir={$this->temp_file}"));
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_404()
+ {
+ $this->temp_file = basename(tempnam('../', 'test_GET_media_404'));
+ $this->assertFalse(strpos($this->temp_file, '/'));
+ unlink('../' . $this->temp_file);
+
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: {$this->temp_file}");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array("dir" => $this->temp_file), array());
+ $this->assertEquals(http_response_code(), 404);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_cli_media_not_dir_404()
+ {
+ $this->temp_file = basename(tempnam('./', 'test_cli_media_not_dir_404'));
+
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: {$this->temp_file}");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir={$this->temp_file}"));
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_not_dir_404()
+ {
+ $this->temp_file = basename(tempnam('../', 'test_GET_media_not_dir_404'));
+
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: {$this->temp_file}");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array("dir" => $this->temp_file), array());
+ $this->assertEquals(http_response_code(), 404);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_cli_media_dir_but_no_permissions_404()
+ {
+ $this->temp_file = basename(tempnam('./', 'test_cli_media_dir_but_no_permissions_404'));
+ unlink($this->temp_file);
+ mkdir($this->temp_file);
+ chmod($this->temp_file, 0);
+
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: {$this->temp_file}");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir={$this->temp_file}"));
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_but_no_permissions_404()
+ {
+ $this->temp_file = basename(tempnam('../', 'test_GET_media_dir_but_no_permissions_404'));
+ unlink('../' . $this->temp_file);
+ mkdir('../' . $this->temp_file);
+ chmod('../' . $this->temp_file, 0);
+
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: {$this->temp_file}");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array("dir" => $this->temp_file), array());
+ $this->assertEquals(http_response_code(), 404);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_cli_media_dir_a_ok()
+ {
+ $this->temp_file = basename(tempnam('./', 'test_cli_media_dir_a_ok'));
+ unlink($this->temp_file);
+ mkdir($this->temp_file);
+
+ SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir={$this->temp_file}"));
+ $this->assertEquals(MP3_BASE(), slashdir(realpath('.')));
+ $this->assertEquals(MP3_DIR(), slashdir(realpath($this->temp_file)));
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_a_ok()
+ {
+ $this->temp_file = basename(tempnam('../', 'test_GET_media_dir_a_ok'));
+ unlink('../' . $this->temp_file);
+ mkdir('../' . $this->temp_file);
+
+ SettingsHandler::bootstrap(array(), array("dir" => $this->temp_file), array());
+ $this->assertEquals(MP3_BASE(), slashdir(realpath('..'))); // due to bootstrap.php chdir
+ $this->assertEquals(MP3_DIR(), slashdir(realpath('../' . $this->temp_file)));
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_safe_dot_dot_1()
+ {
+ $this->starting_dir = getcwd();
+ mkdir('deep');
+ mkdir('deep/root');
+ chdir('deep/root');
+ SettingsHandler::bootstrap(array(), array("dir" => ".."), array());
+
+ $this->assertEquals(MP3_BASE(), slashdir(realpath("{$this->starting_dir}/.."))); // due to bootstrap.php chdir
+ $this->assertEquals(MP3_DIR(), MP3_BASE());
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_safe_dot_dot_2()
+ {
+ $this->starting_dir = getcwd();
+ mkdir('deep');
+ mkdir('deep/root');
+ chdir('deep/root');
+ SettingsHandler::bootstrap(array(), array("dir" => "../../.."), array());
+
+ $this->assertEquals(MP3_BASE(), slashdir(realpath("{$this->starting_dir}/.."))); // due to bootstrap.php chdir
+ $this->assertEquals(MP3_DIR(), MP3_BASE());
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_safe_slash_dir()
+ {
+ $this->starting_dir = getcwd();
+ mkdir('deep');
+ mkdir('deep/root');
+ chdir('deep/root');
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: /etc");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array("dir" => "/etc"), array());
+ $this->assertEquals(http_response_code(), 404);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_safe_slash_dir_2()
+ {
+ $this->starting_dir = getcwd();
+ mkdir('deep');
+ mkdir('deep/root');
+ chdir('deep/root');
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: ////etc");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array("dir" => "////etc"), array());
+ $this->assertEquals(http_response_code(), 404);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_safe_dir_with_good_base()
+ {
+ $this->starting_dir = getcwd();
+ mkdir('deep');
+ mkdir('deep/root');
+ chdir('deep/root');
+ define('MP3_BASE', realpath('..'));
+ SettingsHandler::bootstrap(array(), array("dir" => "root"), array());
+
+ $this->assertEquals(MP3_BASE(), realpath("..") . '/');
+ $this->assertEquals(MP3_DIR(), realpath('.') . '/');
+ $this->assertFalse(http_response_code());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function test_GET_media_dir_unsafe_slash_dir_with_good_base()
+ {
+ $this->starting_dir = getcwd();
+ mkdir('deep');
+ mkdir('deep/root');
+ chdir('deep/root');
+ define('MP3_BASE', realpath('..'));
+ $this->expectException("ExitException");
+ $this->expectExceptionMessage("Not Found: ../deep/root");
+ $this->expectExceptionCode(-2);
+ SettingsHandler::bootstrap(array(), array("dir" => "../deep/root"), array());
+ $this->assertEquals(http_response_code(), 404);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ // public function test_cli_arg_parsing()
+ // {
+
+ // }
+
// TODO: test HTTP_HOST + GET dir
/**
@@ -194,7 +524,7 @@ public function test_sensible_defaults($argv0)
$this->assertSame(getID3_Podcast_Helper::$AUTO_SAVE_COVER_ART, AUTO_SAVE_COVER_ART);
$this->assertSame(iTunes_Podcast_Helper::$ITUNES_SUBTITLE_SUFFIX, ITUNES_SUBTITLE_SUFFIX);
$this->assertSame(RSS_File_Item::$FILES_URL, MP3_URL);
- $this->assertSame(RSS_File_Item::$FILES_DIR, MP3_DIR);
+ $this->assertSame(RSS_File_Item::$FILES_DIR, MP3_DIR());
$this->assertSame(Media_RSS_Item::$LONG_TITLES, LONG_TITLES);
$this->assertSame(Media_RSS_Item::$DESCRIPTION_SOURCE, DESCRIPTION_SOURCE);
}
@@ -225,7 +555,7 @@ public function test_CLI_ONLY_sensible_defaults()
SettingsHandler::bootstrap(array(), array(), array('dir2cast.php'));
SettingsHandler::defaults(array());
- $this->assertEquals(MP3_URL, 'file://' . getcwd());
+ $this->assertEquals(MP3_URL, 'file://' . getcwd() . '/');
$this->assertEquals(LINK, 'http://www.example.com/');
$this->assertEquals(RSS_LINK, 'http://www.example.com/rss');
$this->assertEquals(TITLE, 'test'); // name of this folder
@@ -326,11 +656,28 @@ public function test_HTTPS_URLs_exist()
public function tearDown(): void
{
+ if($this->starting_dir) {
+ chdir($this->starting_dir);
+ rmrf('deep');
+ }
file_exists('description.txt') && unlink('description.txt');
file_exists('itunes_subtitle.txt') && unlink('itunes_subtitle.txt');
file_exists('itunes_summary.txt') && unlink('itunes_summary.txt');
file_exists('image.jpg') && unlink('image.jpg');
file_exists('itunes_image.jpg') && unlink('itunes_image.jpg');
+ if($this->temp_file)
+ {
+ if(file_exists($this->temp_file)) {
+ chmod($this->temp_file, 755);
+ if(is_dir($this->temp_file)) rmdir($this->temp_file);
+ else unlink($this->temp_file);
+ }
+ elseif(file_exists('../'.$this->temp_file)) {
+ chmod('../'.$this->temp_file, 755);
+ if(is_dir('../'.$this->temp_file)) rmdir('../'.$this->temp_file);
+ else unlink('../'.$this->temp_file);
+ }
+ }
}
}
diff --git a/test/bootstrap.php b/test/bootstrap.php
index ff103b6..9fe6f3e 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -85,6 +85,44 @@ function temp_xml_glob()
return '.' . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . '*.xml';
}
+function escape_single_quoted_string($string)
+{
+ $string = str_replace('\\', '\\\\', $string);
+ $string = str_replace('\'', '\\\'', $string);
+ return $string;
+}
+
+function fake_getopt_command($argv_in, $short_options, $long_options)
+{
+ $argv_string = "'" . implode("', '", array_map('escape_single_quoted_string', $argv_in) ). "'";
+ $argv_count = count($argv_in);
+ $short_options_string = escape_single_quoted_string($short_options);
+ $long_options_string = "'" . implode("', '", array_map('escape_single_quoted_string', $long_options) ). "'";
+
+ $command_parts = array(
+ 'php', '-d', 'register_argc_argv=false', '-r', escapeshellarg(<<