5656/* DEFAULTS *********************************************/
5757
5858// error handler needs these, so let's set them now.
59- define ('VERSION ' , '1.38 ' );
59+ define ('VERSION ' , '1.39 ' );
6060define ('DIR2CAST_HOMEPAGE ' , 'https://github.com/ben-xo/dir2cast/ ' );
6161define ('GENERATOR ' , 'dir2cast ' . VERSION . ' by Ben XO ( ' . DIR2CAST_HOMEPAGE . ') ' );
6262
@@ -234,7 +234,7 @@ public function appendToChannel(DOMElement $d, DOMDocument $doc) {
234234 }
235235
236236 public function addNamespaceTo (DOMElement $ d , DOMDocument $ doc ) {
237- return $ this ->wrapped_helper ->appendToChannel ($ d , $ doc );
237+ return $ this ->wrapped_helper ->addNamespaceTo ($ d , $ doc );
238238 }
239239
240240 public function appendToItem (DOMElement $ d , DOMDocument $ doc , RSS_Item $ item )
@@ -390,7 +390,9 @@ public function appendToChannel(DOMElement $channel, DOMDocument $doc)
390390 ->appendChild ( new DOMText ( $ this ->explicit ) );
391391 }
392392
393- if (!empty ($ this ->block ))
393+ // Per Apple's spec, <itunes:block> is only meaningful when set to "Yes"/"yes";
394+ // any other value must be ignored, so omit the tag entirely.
395+ if (!empty ($ this ->block ) && strtolower ($ this ->block ) === 'yes ' )
394396 {
395397 $ channel ->appendChild ( $ doc ->createElement ('itunes:block ' ) )
396398 ->appendChild ( new DOMText ( $ this ->block ) );
@@ -1072,11 +1074,32 @@ public function addHelper(Podcast_Helper $helper)
10721074
10731075 public function http_headers ($ content_length )
10741076 {
1075- // The correct content type is application/rss+xml; however, the de-facto standard is now text/xml.
1077+ // The correct content type is application/rss+xml; however, the de-facto standard is now text/xml.
10761078 // See https://stackoverflow.com/questions/595616/what-is-the-correct-mime-type-to-use-for-an-rss-feed
10771079 header ('Content-type: text/xml; charset=UTF-8 ' );
10781080 header ('Content-length: ' . $ content_length );
1079- header ('Last-modified: ' . $ this ->getLastBuildDate ());
1081+ $ http_last_modified = $ this ->httpLastModified ();
1082+ if ($ http_last_modified !== null ) {
1083+ header ('Last-modified: ' . $ http_last_modified );
1084+ }
1085+ }
1086+
1087+ /**
1088+ * Returns the last-modified timestamp formatted as an HTTP-date (IMF-fixdate, RFC 7231)
1089+ * suitable for the HTTP `Last-Modified` header. The channel's <lastBuildDate> uses
1090+ * RFC 2822 format (date('r')) which is correct for RSS but not for HTTP headers.
1091+ */
1092+ public function httpLastModified ()
1093+ {
1094+ $ lbd = $ this ->getLastBuildDate ();
1095+ if (!$ lbd ) {
1096+ return null ;
1097+ }
1098+ $ ts = strtotime ((string )$ lbd );
1099+ if ($ ts === false ) {
1100+ return null ;
1101+ }
1102+ return gmdate ('D, d M Y H:i:s ' , $ ts ) . ' GMT ' ;
10801103 }
10811104
10821105 /**
@@ -1526,6 +1549,12 @@ public function generate()
15261549 {
15271550 $ this ->setLastBuildDate ($ matches [1 ]);
15281551 }
1552+ else
1553+ {
1554+ // Fall back to the cache file's mtime so HTTP Last-Modified is still
1555+ // populated (e.g. if the cached XML predates this format or is corrupt).
1556+ $ this ->setLastBuildDate (date ('r ' , filemtime ($ this ->temp_file )));
1557+ }
15291558 }
15301559 else
15311560 {
@@ -1864,11 +1893,16 @@ public static function bootstrap(array $SERVER, array $GET, array $argv)
18641893 public static function defaults (array $ SERVER )
18651894 {
18661895 // if an MP3_DIR specific config file exists, load it now, as long as it's not the same file as the global one!
1867- if (
1868- file_exists ( MP3_DIR () . 'dir2cast.ini ' ) and
1869- realpath (DIR2CAST_BASE () . 'dir2cast.ini ' ) != realpath ( MP3_DIR () . 'dir2cast.ini ' )
1896+ if (
1897+ file_exists ( MP3_DIR () . 'dir2cast.ini ' ) and
1898+ realpath (DIR2CAST_BASE () . 'dir2cast.ini ' ) != realpath ( MP3_DIR () . 'dir2cast.ini ' )
18701899 ) {
1871- self ::load_from_ini ( MP3_DIR () . 'dir2cast.ini ' );
1900+ $ local_ini = MP3_DIR () . 'dir2cast.ini ' ;
1901+ self ::load_from_ini ( $ local_ini );
1902+ // Track this so the dispatcher can use its mtime for cache invalidation;
1903+ // INI_FILE alone only covers the install-wide ini.
1904+ if (!defined ('LOCAL_INI_FILE ' ))
1905+ define ('LOCAL_INI_FILE ' , $ local_ini );
18721906 }
18731907
18741908 self ::finalize ();
@@ -2100,8 +2134,15 @@ public function __construct(Locking_Cached_Dir_Podcast $podcast)
21002134
21012135 public function uncache_if_forced ($ force_password , $ get )
21022136 {
2103- if ( strlen ($ force_password ) && isset ($ get ['force ' ]) && $ force_password == $ get ['force ' ] )
2104- {
2137+ // Cast both sides to string and use hash_equals to avoid two issues:
2138+ // (1) PHP's `==` does numeric coercion of numeric-looking strings (so e.g.
2139+ // "01" loosely equals "1"); (2) `==`/`===` are not constant-time, so
2140+ // they leak password length / prefix via timing.
2141+ if (
2142+ strlen ((string )$ force_password ) &&
2143+ isset ($ get ['force ' ]) &&
2144+ hash_equals ((string )$ force_password , (string )$ get ['force ' ])
2145+ ) {
21052146 $ this ->podcast ->uncache ();
21062147 }
21072148 }
@@ -2121,6 +2162,8 @@ public function update_mtime_if_dir2cast_or_settings_modified()
21212162 $ this ->podcast ->updateMaxMtime (filemtime (__FILE__ ), __FILE__ );
21222163 if (defined ('INI_FILE ' ))
21232164 $ this ->podcast ->updateMaxMtime (filemtime (INI_FILE ), INI_FILE );
2165+ if (defined ('LOCAL_INI_FILE ' ))
2166+ $ this ->podcast ->updateMaxMtime (filemtime (LOCAL_INI_FILE ), LOCAL_INI_FILE );
21242167 }
21252168
21262169 public function update_mtime_if_metadata_files_modified ()
0 commit comments