From fbf54646d1e86cb1878c4f6ff170782bba7280a2 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:52:57 +0100 Subject: [PATCH 01/16] WIP: tests which involve getopt() are hard and I'm belligerent --- dir2cast.php | 55 +++++++++++++++++++++++--- test/FakeGetoptTest.php | 75 ++++++++++++++++++++++++++++++++++++ test/FourOhFourTest.php | 23 +++++++++++ test/SettingsHandlerTest.php | 73 +++++++++++++++++++++++++++++++++-- test/bootstrap.php | 30 +++++++++++++++ 5 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 test/FakeGetoptTest.php create mode 100644 test/FourOhFourTest.php diff --git a/dir2cast.php b/dir2cast.php index 45107a6..f1cd7b1 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -92,6 +92,8 @@ function __autoloader($class_name) /* CLASSES **********************************************/ +class ExitException extends Exception {} + abstract class GetterSetter { protected $parameters = array(); @@ -1573,7 +1575,7 @@ 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) @@ -1629,16 +1631,41 @@ public static function display($message, $errfile, $errline) echo strip_tags(self::get_primed_error(ErrorHandler::$primer)) . "\n"; } - exit(-1); + throw new ExitException("", -1); } } - + + public static function display404($message) + { + if(defined('CLI_ONLY')) + { + header("HTTP/1.0 404 Not Found"); + header("Content-type: text/plain"); + } + echo "Not Found: $message\n"; + throw new ExitException("", -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($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. * @@ -1670,7 +1697,7 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) 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"; @@ -1687,6 +1714,10 @@ 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(MP3_DIR) or !is_readable(MP3_DIR)) + { + ErrorHandler::display404($cli_options['media-dir']); + } } if(!defined('MP3_URL') && !empty($cli_options['media-url'])) { @@ -1739,11 +1770,16 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) if(!defined('MP3_DIR')) { if(!empty($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); } - } /** @@ -2181,7 +2217,14 @@ function main($args) if(isset($GLOBALS['argv'])) { $args = $argv; } - exit(main($args)); + try + { + exit(main($args)); + } + catch(ExitException $e) + { + exit($e->getCode()); + } } /* THE END *********************************************/ diff --git a/test/FakeGetoptTest.php b/test/FakeGetoptTest.php new file mode 100644 index 0000000..a5bcc80 --- /dev/null +++ b/test/FakeGetoptTest.php @@ -0,0 +1,75 @@ +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' => ' ') + ); + print(fake_getopt_command(array('php', '--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..6b36221 --- /dev/null +++ b/test/FourOhFourTest.php @@ -0,0 +1,23 @@ +assertEquals("Not Found: imaginary-dir", 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..8155795 100644 --- a/test/SettingsHandlerTest.php +++ b/test/SettingsHandlerTest.php @@ -43,6 +43,30 @@ class SettingsHandlerTest extends TestCase 'MIN_FILE_AGE', ); + 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('--help'), $short_options, $long_options); + $this->assertEquals($cli_options, array('help' => true)); + + $cli_options = SettingsHandler::getopt(array('--media-dir='), $short_options, $long_options); + $this->assertEquals($cli_options, array('media-dir' => '')); + + $cli_options = SettingsHandler::getopt(array('--media-dir=test'), $short_options, $long_options); + $this->assertEquals($cli_options, array('media-dir' => 'test')); + + $this->assertEquals($argv_copy, $GLOBALS['argv']); + $this->assertEquals($argc_copy, $GLOBALS['argc']); + } + /** * @runInSeparateProcess * @preserveGlobalState disabled @@ -121,12 +145,13 @@ public function test_defines_CLI_ONLY_if_argv0() /** * @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'); @@ -153,6 +178,46 @@ public function test_when_SERVER_HTTP_HOST_then_MP3_BASE_defaults_to_same_dir() $this->assertEquals(MP3_DIR, '/var/www'); } + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_cli_media_404() + { + $temp = tempnam('./', 'test_cli_media_404'); + try + { + $this->expectException("ExitException"); + $this->expectExceptionCode(-2); + SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir=$temp")); + } + catch(Exception $e) + { + throw $e; + } + finally + { + unlink($temp); + } + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_cli_media_dir_404() + { + + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_cli_arg_parsing() + { + + } // TODO: test HTTP_HOST + GET dir /** diff --git a/test/bootstrap.php b/test/bootstrap.php index ff103b6..6ed07a2 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -85,6 +85,36 @@ function temp_xml_glob() return '.' . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . '*.xml'; } +function escape_single_quoted_string($string) +{ + return str_replace(array("'", '\\'), array("\\'", '\\\\'), $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 = addslashes($short_options); + $long_options_string = "'" . implode("', '", array_map('escape_single_quoted_string', $long_options) ). "'"; + + $command_parts = array( + 'php', '-d', 'register_argc_argv=false', '-r', << Date: Mon, 17 Oct 2022 17:05:16 +0100 Subject: [PATCH 02/16] Finally get getopt media-dir tests working --- dir2cast.php | 4 +- test/FakeGetoptTest.php | 4 +- test/SettingsHandlerTest.php | 182 ++++++++++++++++++++++++++++++----- test/bootstrap.php | 18 ++-- test/run.sh | 1 + 5 files changed, 171 insertions(+), 38 deletions(-) diff --git a/dir2cast.php b/dir2cast.php index f1cd7b1..ff2ab03 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -1642,8 +1642,7 @@ public static function display404($message) header("HTTP/1.0 404 Not Found"); header("Content-type: text/plain"); } - echo "Not Found: $message\n"; - throw new ExitException("", -2); + throw new ExitException("Not Found: $message", -2); } } @@ -2223,6 +2222,7 @@ function main($args) } catch(ExitException $e) { + print($e->getMessage()."\n"); exit($e->getCode()); } } diff --git a/test/FakeGetoptTest.php b/test/FakeGetoptTest.php index a5bcc80..887ff2c 100644 --- a/test/FakeGetoptTest.php +++ b/test/FakeGetoptTest.php @@ -55,14 +55,14 @@ public function test_fake_getopt_escaping() fake_getopt(array('php', "--media-dir= "), '', array('media-dir::')), array('media-dir' => ' ') ); - print(fake_getopt_command(array('php', '--media-dir=""'), '', array('media-dir::'))); $this->assertEquals( fake_getopt(array('php', '--media-dir=""'), '', array('media-dir::')), array('media-dir' => '""') ); + print(fake_getopt(array('php', '--media-dir=\'\''), '', array('media-dir::'))); $this->assertEquals( fake_getopt(array('php', "--media-dir=''"), '', array('media-dir::')), - array('media-dir' => "''") + false // XXX: seems to be a bug in getopt ); } public function test_fake_getopt_both_arg_types() diff --git a/test/SettingsHandlerTest.php b/test/SettingsHandlerTest.php index 8155795..b12ff0b 100644 --- a/test/SettingsHandlerTest.php +++ b/test/SettingsHandlerTest.php @@ -42,6 +42,8 @@ class SettingsHandlerTest extends TestCase 'DONT_UNCACHE_IF_OUTPUT_FILE', 'MIN_FILE_AGE', ); + + public $temp_file = false; public function test_getopt_hook() { @@ -51,17 +53,41 @@ public function test_getopt_hook() $short_options = ''; $long_options = array('help', 'media-dir::', 'bootstrap'); - $cli_options = SettingsHandler::getopt(array(), $short_options, $long_options); + $cli_options = SettingsHandler::getopt( + array(), + $short_options, $long_options + ); $this->assertEquals($cli_options, array()); - $cli_options = SettingsHandler::getopt(array('--help'), $short_options, $long_options); - $this->assertEquals($cli_options, array('help' => true)); + $cli_options = SettingsHandler::getopt( + array('dir2cast.php'), + $short_options, $long_options + ); + $this->assertEquals($cli_options, array()); - $cli_options = SettingsHandler::getopt(array('--media-dir='), $short_options, $long_options); - $this->assertEquals($cli_options, array('media-dir' => '')); + $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('--media-dir=test'), $short_options, $long_options); - $this->assertEquals($cli_options, array('media-dir' => 'test')); + $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']); @@ -184,40 +210,133 @@ public function test_when_SERVER_HTTP_HOST_then_MP3_BASE_defaults_to_same_dir() */ public function test_cli_media_404() { - $temp = tempnam('./', 'test_cli_media_404'); - try - { - $this->expectException("ExitException"); - $this->expectExceptionCode(-2); - SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir=$temp")); - } - catch(Exception $e) - { - throw $e; - } - finally - { - unlink($temp); - } + $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}")); } - + /** * @runInSeparateProcess * @preserveGlobalState disabled */ - public function test_cli_media_dir_404() + 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()); } - + + /** + * @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}")); + } + + /** + * @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()); + } + + /** + * @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}")); + } + + /** + * @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()); + } + /** * @runInSeparateProcess * @preserveGlobalState disabled */ - public function test_cli_arg_parsing() + 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, realpath('.')); + $this->assertEquals(MP3_DIR, realpath($this->temp_file)); } + + /** + * @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, realpath('..')); // due to bootstrap.php chdir + $this->assertEquals(MP3_DIR, realpath('../' . $this->temp_file)); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + // public function test_cli_arg_parsing() + // { + + // } + // TODO: test HTTP_HOST + GET dir /** @@ -396,6 +515,17 @@ public function tearDown(): void 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)) { + if(is_dir($this->temp_file)) rmdir($this->temp_file); + else unlink($this->temp_file); + } + elseif(file_exists('../'.$this->temp_file)) { + 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 6ed07a2..461e017 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -94,25 +94,27 @@ 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 = addslashes($short_options); + $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', <</dev/null", $output, $result_code); return unserialize($output[0]); } diff --git a/test/run.sh b/test/run.sh index 11f01d9..031418f 100755 --- a/test/run.sh +++ b/test/run.sh @@ -38,6 +38,7 @@ if [[ "$PATH_COVERAGE" != '' ]]; then fi vendor/bin/phpunit \ + --colors=always \ --bootstrap "$SCRIPT_DIR/bootstrap.php" \ --coverage-php /tmp/cov-main \ --coverage-filter ../dir2cast.php \ From 71c3983a481a819f2fb7169d5c6337cf14a40aeb Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:11:12 +0100 Subject: [PATCH 03/16] Try again? --- test/bootstrap.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/bootstrap.php b/test/bootstrap.php index 461e017..7b90bed 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -109,11 +109,13 @@ function fake_getopt_command($argv_in, $short_options, $long_options) /** * Dangerous (due to exec()) and unlikely to work properly outside of testing. - * Needed because getopt() can't have its input mocked without register_argc_argv=false ! + * Needed because getopt() can't have its input mocked without register_argc_argv=false */ function fake_getopt($argv_in, $short_options, $long_options) { $command = fake_getopt_command($argv_in, $short_options, $long_options); + $output = null; + $result_code = null; exec($command . " 2>/dev/null", $output, $result_code); return unserialize($output[0]); } From cd3b90270bea781720c5c005a28e7a4fa40e2e4d Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:15:50 +0100 Subject: [PATCH 04/16] Extra debugging? --- test/bootstrap.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/bootstrap.php b/test/bootstrap.php index 7b90bed..cf82cfb 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -116,7 +116,8 @@ function fake_getopt($argv_in, $short_options, $long_options) $command = fake_getopt_command($argv_in, $short_options, $long_options); $output = null; $result_code = null; - exec($command . " 2>/dev/null", $output, $result_code); + exec($command, $output, $result_code); + var_dump($output); return unserialize($output[0]); } From fe522742f8987a69408ea6c2d0c771e3d0453109 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:30:20 +0100 Subject: [PATCH 05/16] I'm sure this is basically the same, but ok! --- test/FakeGetoptTest.php | 3 +-- test/bootstrap.php | 9 ++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/FakeGetoptTest.php b/test/FakeGetoptTest.php index 887ff2c..c3769b2 100644 --- a/test/FakeGetoptTest.php +++ b/test/FakeGetoptTest.php @@ -59,10 +59,9 @@ public function test_fake_getopt_escaping() fake_getopt(array('php', '--media-dir=""'), '', array('media-dir::')), array('media-dir' => '""') ); - print(fake_getopt(array('php', '--media-dir=\'\''), '', array('media-dir::'))); $this->assertEquals( fake_getopt(array('php', "--media-dir=''"), '', array('media-dir::')), - false // XXX: seems to be a bug in getopt + array('media-dir' => "''") ); } public function test_fake_getopt_both_arg_types() diff --git a/test/bootstrap.php b/test/bootstrap.php index cf82cfb..9fe6f3e 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -87,7 +87,9 @@ function temp_xml_glob() function escape_single_quoted_string($string) { - return str_replace(array("'", '\\'), array("\\'", '\\\\'), $string); + $string = str_replace('\\', '\\\\', $string); + $string = str_replace('\'', '\\\'', $string); + return $string; } function fake_getopt_command($argv_in, $short_options, $long_options) @@ -117,8 +119,9 @@ function fake_getopt($argv_in, $short_options, $long_options) $output = null; $result_code = null; exec($command, $output, $result_code); - var_dump($output); - return unserialize($output[0]); + if(count($output) > 0) + return unserialize($output[0]); + return array(); } define('NO_DISPATCHER', true); From 0859d0d741dd55712d78a1a9333a27e9769e5ad1 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:38:19 +0100 Subject: [PATCH 06/16] Add tests that MP3_DIR not found from user input is a 404. --- test/SettingsHandlerTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/SettingsHandlerTest.php b/test/SettingsHandlerTest.php index b12ff0b..0d7f2f2 100644 --- a/test/SettingsHandlerTest.php +++ b/test/SettingsHandlerTest.php @@ -218,6 +218,7 @@ public function test_cli_media_404() $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()); } /** @@ -234,6 +235,7 @@ public function test_GET_media_404() $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); } /** @@ -248,6 +250,7 @@ public function test_cli_media_not_dir_404() $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()); } /** @@ -262,6 +265,7 @@ public function test_GET_media_not_dir_404() $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); } /** @@ -279,6 +283,7 @@ public function test_cli_media_dir_but_no_permissions_404() $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()); } /** @@ -296,6 +301,7 @@ public function test_GET_media_dir_but_no_permissions_404() $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); } /** @@ -311,6 +317,7 @@ public function test_cli_media_dir_a_ok() SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir={$this->temp_file}")); $this->assertEquals(MP3_BASE, realpath('.')); $this->assertEquals(MP3_DIR, realpath($this->temp_file)); + $this->assertFalse(http_response_code()); } /** @@ -326,6 +333,7 @@ public function test_GET_media_dir_a_ok() SettingsHandler::bootstrap(array(), array("dir" => $this->temp_file), array()); $this->assertEquals(MP3_BASE, realpath('..')); // due to bootstrap.php chdir $this->assertEquals(MP3_DIR, realpath('../' . $this->temp_file)); + $this->assertFalse(http_response_code()); } /** From 50cbcd8f8a33cf8088343e404dfc17be3571cfb0 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 18:18:47 +0100 Subject: [PATCH 07/16] More safety tests and fixes --- test/SettingsHandlerTest.php | 117 +++++++++++++++++++++++++++++++++++ test/bootstrap.php | 5 ++ 2 files changed, 122 insertions(+) diff --git a/test/SettingsHandlerTest.php b/test/SettingsHandlerTest.php index 0d7f2f2..51da4e1 100644 --- a/test/SettingsHandlerTest.php +++ b/test/SettingsHandlerTest.php @@ -44,6 +44,13 @@ class SettingsHandlerTest extends TestCase ); 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() { @@ -336,6 +343,110 @@ public function test_GET_media_dir_a_ok() $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, realpath("{$this->starting_dir}/..")); // due to bootstrap.php chdir + $this->assertEquals(slashdir(MP3_DIR), slashdir(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, realpath("{$this->starting_dir}/..")); // due to bootstrap.php chdir + $this->assertEquals(slashdir(MP3_DIR), slashdir(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 @@ -518,6 +629,10 @@ 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'); @@ -526,10 +641,12 @@ public function tearDown(): void 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 9fe6f3e..c2a1b9b 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -124,6 +124,11 @@ function fake_getopt($argv_in, $short_options, $long_options) return array(); } +function slashdir($dir) +{ + return rtrim($dir, '/') . '/'; +} + define('NO_DISPATCHER', true); require_once('../dir2cast.php'); From 4169eb0c0e1f68fa64f1384a7ecbc5de9824c229 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 18:27:06 +0100 Subject: [PATCH 08/16] Fix glaring oops for web environments --- dir2cast.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dir2cast.php b/dir2cast.php index ff2ab03..0629a9a 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -1656,7 +1656,7 @@ class SettingsHandler */ public static function getopt($argv_in, $short_options, $long_options) { - if($argv_in != $GLOBALS['argv']) + if(isset($GLOBALS['argv']) && $argv_in != $GLOBALS['argv']) { return fake_getopt($argv_in, $short_options, $long_options); } From 60a46234290b6f41339327b4dd15285a2dd2e1fb Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 18:49:02 +0100 Subject: [PATCH 09/16] Update errors to be more helpful when accessing valid paths that are empty, as opposed to invalid paths --- dir2cast.php | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/dir2cast.php b/dir2cast.php index 0629a9a..535440e 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -1195,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(); @@ -1580,12 +1583,20 @@ public static function display($message, $errfile, $errline) { if(self::$errors) { - if(!defined('CLI_ONLY') && !ini_get('html_errors')) + if(!defined('CLI_ONLY') || !CLI_ONLY) + { + if(!http_response_code()) + { + http_response_code(500); + }q + } + + if((!defined('CLI_ONLY') || !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') || !CLI_ONLY ) && ini_get('html_errors')) { header("Content-type: text/html"); // reset the content-type @@ -1605,14 +1616,16 @@ public static function display($message, $errfile, $errline)

An error occurred generating your podcast.

-

+

+ +

+
+ This error occurred on line of . +
-
- This error occurred on line of . -

@@ -1631,15 +1644,15 @@ public static function display($message, $errfile, $errline) echo strip_tags(self::get_primed_error(ErrorHandler::$primer)) . "\n"; } - throw new ExitException("", -1); + exit(-1); // can't throw - this is the exception handler } } public static function display404($message) { - if(defined('CLI_ONLY')) + if(defined('CLI_ONLY') && CLI_ONLY) { - header("HTTP/1.0 404 Not Found"); + http_response_code(404); header("Content-type: text/plain"); } throw new ExitException("Not Found: $message", -2); From d9cbd523a9d57cac097d206a9ad5b6eecf59484d Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 18:50:11 +0100 Subject: [PATCH 10/16] Typo --- dir2cast.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dir2cast.php b/dir2cast.php index 535440e..4ff8af7 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -1588,7 +1588,7 @@ public static function display($message, $errfile, $errline) if(!http_response_code()) { http_response_code(500); - }q + } } if((!defined('CLI_ONLY') || !CLI_ONLY) && !ini_get('html_errors')) From f9693c32e808db26b5a22115bd78cec380bc8001 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 21:05:45 +0100 Subject: [PATCH 11/16] Fix nasty broken links bug which depends on whether there's a / on the end of the path defines --- dir2cast.php | 87 ++++++++++++++++--------------- docker-compose/nginx/default.conf | 2 +- test/FourOhFourTest.php | 4 +- test/SettingsHandlerTest.php | 30 +++++------ test/bootstrap.php | 5 -- 5 files changed, 64 insertions(+), 64 deletions(-) diff --git a/dir2cast.php b/dir2cast.php index 4ff8af7..e2f298c 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -617,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; } @@ -1583,7 +1583,7 @@ public static function display($message, $errfile, $errline) { if(self::$errors) { - if(!defined('CLI_ONLY') || !CLI_ONLY) + if(!defined('CLI_ONLY')) { if(!http_response_code()) { @@ -1591,12 +1591,12 @@ public static function display($message, $errfile, $errline) } } - if((!defined('CLI_ONLY') || !CLI_ONLY) && !ini_get('html_errors')) + if((!defined('CLI_ONLY')) && !ini_get('html_errors')) { header("Content-type: text/plain"); // reset the content-type } - if((!defined('CLI_ONLY') || !CLI_ONLY ) && ini_get('html_errors')) + if((!defined('CLI_ONLY')) && ini_get('html_errors')) { header("Content-type: text/html"); // reset the content-type @@ -1650,7 +1650,7 @@ public static function display($message, $errfile, $errline) public static function display404($message) { - if(defined('CLI_ONLY') && CLI_ONLY) + if(defined('CLI_ONLY')) { http_response_code(404); header("Content-type: text/plain"); @@ -1692,18 +1692,18 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) define('CLI_ONLY', true); } - if(defined('CLI_ONLY') && CLI_ONLY) { - define('DIR2CAST_BASE', realpath(dirname($argv[0]))); + if(defined('CLI_ONLY')) { + define('DIR2CAST_BASE', slashdir(realpath(dirname($argv[0])))); } else { - define('DIR2CAST_BASE', dirname(__FILE__)); + define('DIR2CAST_BASE', slashdir(dirname(__FILE__))); } // 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); @@ -1725,11 +1725,12 @@ 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(MP3_DIR) or !is_readable(MP3_DIR)) + + if(!is_dir($cli_options['media-dir']) or !is_readable($cli_options['media-dir'])) { ErrorHandler::display404($cli_options['media-dir']); } + define('MP3_DIR', slashdir(realpath($cli_options['media-dir']))); } if(!defined('MP3_URL') && !empty($cli_options['media-url'])) { @@ -1768,13 +1769,13 @@ 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'); } if(!defined('MP3_BASE')) { if(!empty($SERVER['HTTP_HOST'])) - define('MP3_BASE', dirname($SERVER['SCRIPT_FILENAME'])); + define('MP3_BASE', slashdir(dirname($SERVER['SCRIPT_FILENAME']))); else define('MP3_BASE', DIR2CAST_BASE); } @@ -1783,14 +1784,14 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) { if(!empty($GET['dir'])) { - define('MP3_DIR', MP3_BASE . '/' . safe_path(magic_stripslashes($GET['dir']))); + define('MP3_DIR', slashdir(slashdir(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', slashdir(MP3_BASE)); } } @@ -1801,15 +1802,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?) @@ -1818,9 +1818,9 @@ public static function defaults(array $SERVER) if(!empty($SERVER['HTTP_HOST'])) { - $path_part = substr(MP3_DIR, strlen($SERVER['DOCUMENT_ROOT'])); + $path_part = substr(slashdir(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 ); @@ -1852,10 +1852,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'); } @@ -1877,20 +1877,20 @@ 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); } @@ -1901,9 +1901,9 @@ public static function defaults(array $SERVER) define('IMAGE', rtrim(MP3_URL, '/') . '/image.jpg'); elseif(file_exists(rtrim(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', ''); @@ -1915,9 +1915,9 @@ public static function defaults(array $SERVER) define('ITUNES_IMAGE', rtrim(MP3_URL, '/') . '/itunes_image.jpg'); elseif(file_exists(rtrim(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', ''); @@ -1974,7 +1974,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; @@ -2070,7 +2070,7 @@ public function update_mtime_if_metadata_files_modified() $filepath = rtrim(MP3_DIR, '/') . '/' . $file; if(!file_exists($filepath)) { - $filepath = DIR2CAST_BASE . '/' . $file; + $filepath = DIR2CAST_BASE . $file; } if(!file_exists($filepath)) { @@ -2125,7 +2125,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)); } @@ -2195,6 +2195,11 @@ 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, '/') . '/'; +} + /* DISPATCH *********************************************/ function main($args) 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/FourOhFourTest.php b/test/FourOhFourTest.php index 6b36221..3f2214d 100644 --- a/test/FourOhFourTest.php +++ b/test/FourOhFourTest.php @@ -11,8 +11,8 @@ public static function setUpBeforeClass(): void public function test_non_existent_dir_prints_bare_error_CLI_case(): void { - exec('php dir2cast.php --output=out.xml --media-dir=imaginary-dir', $output, $returncode); - $this->assertEquals("Not Found: imaginary-dir", implode("\n", $output)); + exec('php dir2cast.php --media-dir=dir2cast.ini', $output, $returncode); + $this->assertEquals("Not Found: dir2cast.ini", implode("\n", $output)); $this->assertEquals(254, $returncode); // 254 is -2 } diff --git a/test/SettingsHandlerTest.php b/test/SettingsHandlerTest.php index 51da4e1..401f1d1 100644 --- a/test/SettingsHandlerTest.php +++ b/test/SettingsHandlerTest.php @@ -137,7 +137,7 @@ 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 } /** @@ -172,7 +172,7 @@ 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 } /** @@ -187,7 +187,7 @@ public function test_bootstrap_sets_sensible_global_defaults_for_entire_installa 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(TMP_DIR, DIR2CAST_BASE . 'temp'); $this->assertEquals(MP3_BASE, DIR2CAST_BASE); $this->assertEquals(MP3_DIR, DIR2CAST_BASE); } @@ -207,8 +207,8 @@ 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/'); } /** @@ -322,8 +322,8 @@ public function test_cli_media_dir_a_ok() mkdir($this->temp_file); SettingsHandler::bootstrap(array(), array(), array("dir2cast.php", "--media-dir={$this->temp_file}")); - $this->assertEquals(MP3_BASE, realpath('.')); - $this->assertEquals(MP3_DIR, realpath($this->temp_file)); + $this->assertEquals(MP3_BASE, slashdir(realpath('.'))); + $this->assertEquals(MP3_DIR, slashdir(realpath($this->temp_file))); $this->assertFalse(http_response_code()); } @@ -338,8 +338,8 @@ public function test_GET_media_dir_a_ok() mkdir('../' . $this->temp_file); SettingsHandler::bootstrap(array(), array("dir" => $this->temp_file), array()); - $this->assertEquals(MP3_BASE, realpath('..')); // due to bootstrap.php chdir - $this->assertEquals(MP3_DIR, realpath('../' . $this->temp_file)); + $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()); } @@ -355,8 +355,8 @@ public function test_GET_media_dir_safe_dot_dot_1() chdir('deep/root'); SettingsHandler::bootstrap(array(), array("dir" => ".."), array()); - $this->assertEquals(MP3_BASE, realpath("{$this->starting_dir}/..")); // due to bootstrap.php chdir - $this->assertEquals(slashdir(MP3_DIR), slashdir(MP3_BASE)); + $this->assertEquals(MP3_BASE, slashdir(realpath("{$this->starting_dir}/.."))); // due to bootstrap.php chdir + $this->assertEquals(MP3_DIR, slashdir(MP3_BASE)); $this->assertFalse(http_response_code()); } @@ -372,8 +372,8 @@ public function test_GET_media_dir_safe_dot_dot_2() chdir('deep/root'); SettingsHandler::bootstrap(array(), array("dir" => "../../.."), array()); - $this->assertEquals(MP3_BASE, realpath("{$this->starting_dir}/..")); // due to bootstrap.php chdir - $this->assertEquals(slashdir(MP3_DIR), slashdir(MP3_BASE)); + $this->assertEquals(MP3_BASE, slashdir(realpath("{$this->starting_dir}/.."))); // due to bootstrap.php chdir + $this->assertEquals(MP3_DIR, slashdir(MP3_BASE)); $this->assertFalse(http_response_code()); } @@ -425,7 +425,7 @@ public function test_GET_media_dir_safe_dir_with_good_base() SettingsHandler::bootstrap(array(), array("dir" => "root"), array()); $this->assertEquals(MP3_BASE, realpath("..")); - $this->assertEquals(MP3_DIR, realpath('.')); + $this->assertEquals(MP3_DIR, realpath('.') . '/'); $this->assertFalse(http_response_code()); } @@ -528,7 +528,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 diff --git a/test/bootstrap.php b/test/bootstrap.php index c2a1b9b..9fe6f3e 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -124,11 +124,6 @@ function fake_getopt($argv_in, $short_options, $long_options) return array(); } -function slashdir($dir) -{ - return rtrim($dir, '/') . '/'; -} - define('NO_DISPATCHER', true); require_once('../dir2cast.php'); From be9a457c3671b36c9ed77419ad7c7b0741645697 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 21:27:50 +0100 Subject: [PATCH 12/16] Make all path defines safer and avoid double slashes --- dir2cast.php | 94 ++++++++++++++++++++---------------- test/SettingsHandlerTest.php | 63 +++++++++++++++++------- 2 files changed, 98 insertions(+), 59 deletions(-) diff --git a/dir2cast.php b/dir2cast.php index e2f298c..9cb4bc3 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -1693,17 +1693,17 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) } if(defined('CLI_ONLY')) { - define('DIR2CAST_BASE', slashdir(realpath(dirname($argv[0])))); + define('DIR2CAST_BASE', realpath(dirname($argv[0]))); } else { - define('DIR2CAST_BASE', slashdir(dirname(__FILE__))); + define('DIR2CAST_BASE', dirname(__FILE__)); } // 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); @@ -1769,29 +1769,29 @@ 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'); } if(!defined('MP3_BASE')) { if(!empty($SERVER['HTTP_HOST'])) - define('MP3_BASE', slashdir(dirname($SERVER['SCRIPT_FILENAME']))); + define('MP3_BASE', dirname($SERVER['SCRIPT_FILENAME'])); else - define('MP3_BASE', DIR2CAST_BASE); + define('MP3_BASE', DIR2CAST_BASE()); } if(!defined('MP3_DIR')) { if(!empty($GET['dir'])) { - define('MP3_DIR', slashdir(slashdir(MP3_BASE) . safe_path(magic_stripslashes($GET['dir'])))); - if(!is_dir(MP3_DIR) or !is_readable(MP3_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', slashdir(MP3_BASE)); + define('MP3_DIR', MP3_BASE()); } } @@ -1802,10 +1802,10 @@ 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(); @@ -1818,18 +1818,18 @@ public static function defaults(array $SERVER) if(!empty($SERVER['HTTP_HOST'])) { - $path_part = substr(slashdir(MP3_DIR), strlen(slashdir($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( 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'); } @@ -1852,10 +1852,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'); } @@ -1877,33 +1877,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', ''); @@ -1911,13 +1911,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', ''); @@ -1985,7 +1985,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; } @@ -2067,10 +2067,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)) { @@ -2200,6 +2200,18 @@ 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) @@ -2214,7 +2226,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); diff --git a/test/SettingsHandlerTest.php b/test/SettingsHandlerTest.php index 401f1d1..9a8699e 100644 --- a/test/SettingsHandlerTest.php +++ b/test/SettingsHandlerTest.php @@ -137,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, slashdir(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/'); } /** @@ -172,7 +199,7 @@ 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, slashdir(getcwd())); // from fake $argv + $this->assertEquals(DIR2CAST_BASE(), slashdir(getcwd())); // from fake $argv } /** @@ -187,9 +214,9 @@ public function test_bootstrap_sets_sensible_global_defaults_for_entire_installa 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()); } /** @@ -207,8 +234,8 @@ 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/'); } /** @@ -322,8 +349,8 @@ public function test_cli_media_dir_a_ok() 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->assertEquals(MP3_BASE(), slashdir(realpath('.'))); + $this->assertEquals(MP3_DIR(), slashdir(realpath($this->temp_file))); $this->assertFalse(http_response_code()); } @@ -338,8 +365,8 @@ public function test_GET_media_dir_a_ok() 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->assertEquals(MP3_BASE(), slashdir(realpath('..'))); // due to bootstrap.php chdir + $this->assertEquals(MP3_DIR(), slashdir(realpath('../' . $this->temp_file))); $this->assertFalse(http_response_code()); } @@ -355,8 +382,8 @@ public function test_GET_media_dir_safe_dot_dot_1() 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, slashdir(MP3_BASE)); + $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()); } @@ -372,8 +399,8 @@ public function test_GET_media_dir_safe_dot_dot_2() 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, slashdir(MP3_BASE)); + $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()); } @@ -424,8 +451,8 @@ public function test_GET_media_dir_safe_dir_with_good_base() define('MP3_BASE', realpath('..')); SettingsHandler::bootstrap(array(), array("dir" => "root"), array()); - $this->assertEquals(MP3_BASE, realpath("..")); - $this->assertEquals(MP3_DIR, realpath('.') . '/'); + $this->assertEquals(MP3_BASE(), realpath("..") . '/'); + $this->assertEquals(MP3_DIR(), realpath('.') . '/'); $this->assertFalse(http_response_code()); } @@ -497,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); } From 814e5e17825271e5bc24914c8a8a4ece8e2e847d Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 21:29:47 +0100 Subject: [PATCH 13/16] Update version to 1.37 before merge --- CHANGELOG.txt | 8 ++++++++ README.md | 2 +- dir2cast.php | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 48ab8ba..58d111a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,14 @@ Changelog ========= +1.37 2022-10-17 * 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 related suggestion. + * fix nasty bug where paths were sometimes invalid due to + mishandling of trailing slashes + 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..b1a0d5e 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-17) ================================================================================ https://github.com/ben-xo/dir2cast/ diff --git a/dir2cast.php b/dir2cast.php index 9cb4bc3..9dfc82f 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 . ')'); From 4e5044d695cd11a1289916ac3f61d255b059694d Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Mon, 17 Oct 2022 21:51:24 +0100 Subject: [PATCH 14/16] Some comments --- dir2cast.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dir2cast.php b/dir2cast.php index 9dfc82f..e8debf0 100644 --- a/dir2cast.php +++ b/dir2cast.php @@ -1692,6 +1692,7 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) define('CLI_ONLY', true); } + // do not use DIR2CAST_BASE directly. use DIR2CAST_BASE() if(defined('CLI_ONLY')) { define('DIR2CAST_BASE', realpath(dirname($argv[0]))); } else { @@ -1730,6 +1731,7 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) { 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'])) @@ -1772,6 +1774,7 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) define('TMP_DIR', DIR2CAST_BASE() . 'temp'); } + // do not use MP3_BASE directly. use MP3_BASE() if(!defined('MP3_BASE')) { if(!empty($SERVER['HTTP_HOST'])) @@ -1779,7 +1782,8 @@ public static function bootstrap(array $SERVER, array $GET, array $argv) else define('MP3_BASE', DIR2CAST_BASE()); } - + + // do not use MP3_DIR directly. use MP3_DIR() if(!defined('MP3_DIR')) { if(!empty($GET['dir'])) From e52ab3bf55016f627deca4ff5ddc96ade987bca8 Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Tue, 18 Oct 2022 16:20:46 +0100 Subject: [PATCH 15/16] Update .gitignore with fixture auto-extract images --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) 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 From afd74750fec409c4c20170529c31faff07e4feed Mon Sep 17 00:00:00 2001 From: Ben XO <75862+ben-xo@users.noreply.github.com> Date: Thu, 27 Oct 2022 12:29:23 +0100 Subject: [PATCH 16/16] Update dates and changelog --- CHANGELOG.txt | 6 +++--- README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 58d111a..e0d3353 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,13 +1,13 @@ Changelog ========= -1.37 2022-10-17 * Errors now return an HTTP status code 500 by default. +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 related suggestion. + to @EdwarDDay for this security suggestion. (#64) * fix nasty bug where paths were sometimes invalid due to - mishandling of trailing slashes + 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 diff --git a/README.md b/README.md index b1a0d5e..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.37 (2022-10-17) +dir2cast by Ben XO v1.37 (2022-10-27) ================================================================================ https://github.com/ben-xo/dir2cast/