diff --git a/actions/ConnectivityAction.php b/actions/ConnectivityAction.php index ac86fa1b..9ebd640c 100644 --- a/actions/ConnectivityAction.php +++ b/actions/ConnectivityAction.php @@ -28,23 +28,21 @@ class ConnectivityAction implements ActionInterface public function __construct() { - $this->bridgeFactory = new \BridgeFactory(); + $this->bridgeFactory = new BridgeFactory(); } public function execute(array $request) { if (!Debug::isEnabled()) { - returnError('This action is only available in debug mode!', 400); + throw new \Exception('This action is only available in debug mode!'); } if (!isset($request['bridge'])) { - $this->returnEntryPage(); + print render_template('connectivity.html.php'); return; } - $bridgeName = $request['bridge']; - - $bridgeClassName = $this->bridgeFactory->sanitizeBridgeName($bridgeName); + $bridgeClassName = $this->bridgeFactory->sanitizeBridgeName($request['bridge']); if ($bridgeClassName === null) { throw new \InvalidArgumentException('Bridge name invalid!'); @@ -53,28 +51,12 @@ class ConnectivityAction implements ActionInterface $this->reportBridgeConnectivity($bridgeClassName); } - /** - * Generates a report about the bridge connectivity status and sends it back - * to the user. - * - * The report is generated as Json-formatted string in the format - * { - * "bridge": "", - * "successful": true/false - * } - * - * @param class-string $bridgeClassName Name of the bridge to generate the report for - * @return void - */ private function reportBridgeConnectivity($bridgeClassName) { if (!$this->bridgeFactory->isWhitelisted($bridgeClassName)) { - header('Content-Type: text/html'); - returnServerError('Bridge is not whitelisted!'); + throw new \Exception('Bridge is not whitelisted!'); } - header('Content-Type: text/json'); - $retVal = [ 'bridge' => $bridgeClassName, 'successful' => false, @@ -82,16 +64,9 @@ class ConnectivityAction implements ActionInterface ]; $bridge = $this->bridgeFactory->create($bridgeClassName); - - if ($bridge === false) { - echo json_encode($retVal); - return; - } - $curl_opts = [ CURLOPT_CONNECTTIMEOUT => 5 ]; - try { $reply = getContents($bridge::URI, [], $curl_opts, true); @@ -101,45 +76,11 @@ class ConnectivityAction implements ActionInterface $retVal['http_code'] = 301; } } - } catch (Exception $e) { + } catch (\Exception $e) { $retVal['successful'] = false; } - echo json_encode($retVal); - } - - private function returnEntryPage() - { - echo << - - - - - - - - - -
-
-
-
- - -
- - -EOD; + header('Content-Type: text/json'); + print Json::encode($retVal); } } diff --git a/actions/DetectAction.php b/actions/DetectAction.php index 1ed002af..71060bb8 100644 --- a/actions/DetectAction.php +++ b/actions/DetectAction.php @@ -16,13 +16,17 @@ class DetectAction implements ActionInterface { public function execute(array $request) { - $targetURL = $request['url'] - or returnClientError('You must specify a url!'); + $targetURL = $request['url'] ?? null; + $format = $request['format'] ?? null; - $format = $request['format'] - or returnClientError('You must specify a format!'); + if (!$targetURL) { + throw new \Exception('You must specify a url!'); + } + if (!$format) { + throw new \Exception('You must specify a format!'); + } - $bridgeFactory = new \BridgeFactory(); + $bridgeFactory = new BridgeFactory(); foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) { if (!$bridgeFactory->isWhitelisted($bridgeClassName)) { @@ -31,10 +35,6 @@ class DetectAction implements ActionInterface $bridge = $bridgeFactory->create($bridgeClassName); - if ($bridge === false) { - continue; - } - $bridgeParams = $bridge->detectParameters($targetURL); if (is_null($bridgeParams)) { @@ -45,9 +45,9 @@ class DetectAction implements ActionInterface $bridgeParams['format'] = $format; header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301); - exit; + return; } - returnClientError('No bridge found for given URL: ' . $targetURL); + throw new \Exception('No bridge found for given URL: ' . $targetURL); } } diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php index c2f536d1..e8912f09 100644 --- a/actions/DisplayAction.php +++ b/actions/DisplayAction.php @@ -16,7 +16,7 @@ class DisplayAction implements ActionInterface { public function execute(array $request) { - $bridgeFactory = new \BridgeFactory(); + $bridgeFactory = new BridgeFactory(); $bridgeClassName = null; if (isset($request['bridge'])) { @@ -27,16 +27,14 @@ class DisplayAction implements ActionInterface throw new \InvalidArgumentException('Bridge name invalid!'); } - $format = $request['format'] - or returnClientError('You must specify a format!'); - - // whitelist control + $format = $request['format'] ?? null; + if (!$format) { + throw new \Exception('You must specify a format!'); + } if (!$bridgeFactory->isWhitelisted($bridgeClassName)) { - throw new \Exception('This bridge is not whitelisted', 401); - die; + throw new \Exception('This bridge is not whitelisted'); } - // Data retrieval $bridge = $bridgeFactory->create($bridgeClassName); $bridge->loadConfiguration(); @@ -47,14 +45,12 @@ class DisplayAction implements ActionInterface define('NOPROXY', true); } - // Cache timeout - $cache_timeout = -1; if (array_key_exists('_cache_timeout', $request)) { if (!CUSTOM_CACHE_TIMEOUT) { unset($request['_cache_timeout']); $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request); header('Location: ' . $uri, true, 301); - exit; + return; } $cache_timeout = filter_var($request['_cache_timeout'], FILTER_VALIDATE_INT); @@ -93,7 +89,6 @@ class DisplayAction implements ActionInterface ) ); - // Initialize cache $cacheFactory = new CacheFactory(); $cache = $cacheFactory->create(); @@ -109,15 +104,17 @@ class DisplayAction implements ActionInterface $mtime !== false && (time() - $cache_timeout < $mtime) && !Debug::isEnabled() - ) { // Load cached data + ) { + // Load cached data // Send "Not Modified" response if client supports it // Implementation based on https://stackoverflow.com/a/10847262 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { $stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); - if ($mtime <= $stime) { // Cached data is older or same + if ($mtime <= $stime) { + // Cached data is older or same header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304); - exit; + return; } } @@ -125,27 +122,24 @@ class DisplayAction implements ActionInterface if (isset($cached['items']) && isset($cached['extraInfos'])) { foreach ($cached['items'] as $item) { - $items[] = new \FeedItem($item); + $items[] = new FeedItem($item); } $infos = $cached['extraInfos']; } - } else { // Collect new data + } else { + // Collect new data try { $bridge->setDatas($bridge_params); $bridge->collectData(); $items = $bridge->getItems(); - // Transform "legacy" items to FeedItems if necessary. - // Remove this code when support for "legacy" items ends! if (isset($items[0]) && is_array($items[0])) { $feedItems = []; - foreach ($items as $item) { - $feedItems[] = new \FeedItem($item); + $feedItems[] = new FeedItem($item); } - $items = $feedItems; } @@ -158,18 +152,16 @@ class DisplayAction implements ActionInterface } catch (\Throwable $e) { error_log($e); - if (logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) { + $errorCount = logBridgeError($bridge::NAME, $e->getCode()); + + if ($errorCount >= Configuration::getConfig('error', 'report_limit')) { if (Configuration::getConfig('error', 'output') === 'feed') { - $item = new \FeedItem(); + $item = new FeedItem(); // Create "new" error message every 24 hours $request['_error_time'] = urlencode((int)(time() / 86400)); - $message = sprintf( - 'Bridge returned error %s! (%s)', - $e->getCode(), - $request['_error_time'] - ); + $message = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $request['_error_time']); $item->setTitle($message); $item->setURI( @@ -205,8 +197,8 @@ class DisplayAction implements ActionInterface } $cache->saveData([ - 'items' => array_map(function ($i) { - return $i->toArray(); + 'items' => array_map(function (FeedItem $item) { + return $item->toArray(); }, $items), 'extraInfos' => $infos ]); diff --git a/actions/ListAction.php b/actions/ListAction.php index 5076b6dc..e2b0ccb9 100644 --- a/actions/ListAction.php +++ b/actions/ListAction.php @@ -16,27 +16,17 @@ class ListAction implements ActionInterface { public function execute(array $request) { - $list = new StdClass(); + $list = new \stdClass(); $list->bridges = []; $list->total = 0; - $bridgeFactory = new \BridgeFactory(); + $bridgeFactory = new BridgeFactory(); foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) { $bridge = $bridgeFactory->create($bridgeClassName); - if ($bridge === false) { // Broken bridge, show as inactive - $list->bridges[$bridgeClassName] = [ - 'status' => 'inactive' - ]; - - continue; - } - - $status = $bridgeFactory->isWhitelisted($bridgeClassName) ? 'active' : 'inactive'; - $list->bridges[$bridgeClassName] = [ - 'status' => $status, + 'status' => $bridgeFactory->isWhitelisted($bridgeClassName) ? 'active' : 'inactive', 'uri' => $bridge->getURI(), 'donationUri' => $bridge->getDonationURI(), 'name' => $bridge->getName(), @@ -50,6 +40,6 @@ class ListAction implements ActionInterface $list->total = count($list->bridges); header('Content-Type: application/json'); - echo json_encode($list, JSON_PRETTY_PRINT); + print Json::encode($list); } } diff --git a/bridges/FDroidRepoBridge.php b/bridges/FDroidRepoBridge.php index 74147310..7ce41baf 100644 --- a/bridges/FDroidRepoBridge.php +++ b/bridges/FDroidRepoBridge.php @@ -43,6 +43,25 @@ class FDroidRepoBridge extends BridgeAbstract // Stores repo information private $repo; + public function collectData() + { + if (!extension_loaded('zip')) { + throw new \Exception('FDroidRepoBridge requires the php-zip extension'); + } + + $this->repo = $this->getRepo(); + switch ($this->queriedContext) { + case 'Latest Updates': + $this->getAllUpdates(); + break; + case 'Follow Package': + $this->getPackage($this->getInput('package')); + break; + default: + returnServerError('Unimplemented Context (collectData)'); + } + } + public function getURI() { if (empty($this->queriedContext)) { @@ -70,21 +89,6 @@ class FDroidRepoBridge extends BridgeAbstract } } - public function collectData() - { - $this->repo = $this->getRepo(); - switch ($this->queriedContext) { - case 'Latest Updates': - $this->getAllUpdates(); - break; - case 'Follow Package': - $this->getPackage($this->getInput('package')); - break; - default: - returnServerError('Unimplemented Context (collectData)'); - } - } - private function getRepo() { $url = $this->getURI(); @@ -95,9 +99,10 @@ class FDroidRepoBridge extends BridgeAbstract file_put_contents($jar_loc, $jar); // JAR files are specially formatted ZIP files - $jar = new ZipArchive(); + $jar = new \ZipArchive(); if ($jar->open($jar_loc) !== true) { - returnServerError('Failed to extract archive'); + unlink($jar_loc); + throw new \Exception('Failed to extract archive'); } // Get file pointer to the relevant JSON inside @@ -109,6 +114,7 @@ class FDroidRepoBridge extends BridgeAbstract $data = json_decode(stream_get_contents($fp), true); fclose($fp); $jar->close(); + unlink($jar_loc); return $data; } diff --git a/bridges/PirateCommunityBridge.php b/bridges/PirateCommunityBridge.php index a1a9d8f5..5a617b04 100644 --- a/bridges/PirateCommunityBridge.php +++ b/bridges/PirateCommunityBridge.php @@ -22,7 +22,9 @@ class PirateCommunityBridge extends BridgeAbstract { $parsed_url = parse_url($url); - if ($parsed_url['host'] !== 'raymanpc.com') { + $host = $parsed_url['host'] ?? null; + + if ($host !== 'raymanpc.com') { return null; } diff --git a/bridges/RedditBridge.php b/bridges/RedditBridge.php index e2303987..f02f46a2 100644 --- a/bridges/RedditBridge.php +++ b/bridges/RedditBridge.php @@ -71,7 +71,9 @@ class RedditBridge extends BridgeAbstract { $parsed_url = parse_url($url); - if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') { + $host = $parsed_url['host'] ?? null; + + if ($host != 'www.reddit.com' && $host != 'old.reddit.com') { return null; } diff --git a/bridges/WordPressBridge.php b/bridges/WordPressBridge.php index 5a80c398..ca004547 100644 --- a/bridges/WordPressBridge.php +++ b/bridges/WordPressBridge.php @@ -72,7 +72,7 @@ class WordPressBridge extends FeedExpander } else { $article_image = $article_image->getAttribute('data-lazy-src'); } - $mime_type = getMimeType($article_image); + $mime_type = parse_mime_type($article_image); if (strpos($mime_type, 'image') === false) { $article_image .= '#.image'; // force image } diff --git a/caches/FileCache.php b/caches/FileCache.php index 29f4d78b..fde7d18d 100644 --- a/caches/FileCache.php +++ b/caches/FileCache.php @@ -1,8 +1,5 @@ getCacheFile())) { return unserialize(file_get_contents($this->getCacheFile())); } - return null; } public function saveData($data) { - // Notice: We use plain serialize() here to reduce memory footprint on - // large input data. $writeStream = file_put_contents($this->getCacheFile(), serialize($data)); - if ($writeStream === false) { throw new \Exception('Cannot write the cache... Do you have the right permissions ?'); } - return $this; } @@ -46,7 +35,10 @@ class FileCache implements CacheInterface clearstatcache(false, $cacheFile); if (file_exists($cacheFile)) { $time = filemtime($cacheFile); - return ($time !== false) ? $time : null; + if ($time !== false) { + return $time; + } + return null; } return null; @@ -55,28 +47,25 @@ class FileCache implements CacheInterface public function purgeCache($seconds) { $cachePath = $this->getPath(); - if (file_exists($cachePath)) { - $cacheIterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($cachePath), - RecursiveIteratorIterator::CHILD_FIRST - ); + if (!file_exists($cachePath)) { + return; + } + $cacheIterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($cachePath), + \RecursiveIteratorIterator::CHILD_FIRST + ); - foreach ($cacheIterator as $cacheFile) { - if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) { - continue; - } elseif ($cacheFile->isFile()) { - if (filemtime($cacheFile->getPathname()) < time() - $seconds) { - unlink($cacheFile->getPathname()); - } + foreach ($cacheIterator as $cacheFile) { + if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) { + continue; + } elseif ($cacheFile->isFile()) { + if (filemtime($cacheFile->getPathname()) < time() - $seconds) { + unlink($cacheFile->getPathname()); } } } } - /** - * Set scope - * @return self - */ public function setScope($scope) { if (is_null($scope) || !is_string($scope)) { @@ -88,10 +77,6 @@ class FileCache implements CacheInterface return $this; } - /** - * Set key - * @return self - */ public function setKey($key) { if (!empty($key) && is_array($key)) { @@ -107,10 +92,6 @@ class FileCache implements CacheInterface return $this; } - /** - * Return cache path (and create if not exist) - * @return string Cache path - */ private function getPath() { if (is_null($this->path)) { @@ -119,26 +100,18 @@ class FileCache implements CacheInterface if (!is_dir($this->path)) { if (mkdir($this->path, 0755, true) !== true) { - throw new \Exception('Unable to create ' . $this->path); + throw new \Exception('mkdir: Unable to create file cache folder'); } } return $this->path; } - /** - * Get the file name use for cache store - * @return string Path to the file cache - */ private function getCacheFile() { return $this->getPath() . $this->getCacheName(); } - /** - * Determines file name for store the cache - * return string - */ private function getCacheName() { if (is_null($this->key)) { diff --git a/caches/MemcachedCache.php b/caches/MemcachedCache.php index 42a8ddad..c593ac79 100644 --- a/caches/MemcachedCache.php +++ b/caches/MemcachedCache.php @@ -12,29 +12,33 @@ class MemcachedCache implements CacheInterface public function __construct() { if (!extension_loaded('memcached')) { - returnServerError('"memcached" extension not loaded. Please check "php.ini"'); + throw new \Exception('"memcached" extension not loaded. Please check "php.ini"'); } $section = 'MemcachedCache'; $host = Configuration::getConfig($section, 'host'); $port = Configuration::getConfig($section, 'port'); + if (empty($host) && empty($port)) { - returnServerError('Configuration for ' . $section . ' missing. Please check your ' . FILE_CONFIG); - } elseif (empty($host)) { - returnServerError('"host" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG); - } elseif (empty($port)) { - returnServerError('"port" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG); - } elseif (!ctype_digit($port)) { - returnServerError('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG); + throw new \Exception('Configuration for ' . $section . ' missing. Please check your ' . FILE_CONFIG); + } + if (empty($host)) { + throw new \Exception('"host" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG); + } + if (empty($port)) { + throw new \Exception('"port" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG); + } + if (!ctype_digit($port)) { + throw new \Exception('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG); } $port = intval($port); if ($port < 1 || $port > 65535) { - returnServerError('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG); + throw new \Exception('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG); } - $conn = new Memcached(); + $conn = new \Memcached(); $conn->addServer($host, $port) or returnServerError('Could not connect to memcached server'); $this->conn = $conn; } @@ -64,7 +68,7 @@ class MemcachedCache implements CacheInterface $result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration); if ($result === false) { - returnServerError('Cannot write the cache to memcached server'); + throw new \Exception('Cannot write the cache to memcached server'); } $this->time = $time; @@ -87,20 +91,12 @@ class MemcachedCache implements CacheInterface $this->expiration = $duration; } - /** - * Set scope - * @return self - */ public function setScope($scope) { $this->scope = $scope; return $this; } - /** - * Set key - * @return self - */ public function setKey($key) { if (!empty($key) && is_array($key)) { @@ -119,7 +115,7 @@ class MemcachedCache implements CacheInterface private function getCacheKey() { if (is_null($this->key)) { - returnServerError('Call "setKey" first!'); + throw new \Exception('Call "setKey" first!'); } return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A'); diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php index 2f798714..339e4119 100644 --- a/caches/SQLiteCache.php +++ b/caches/SQLiteCache.php @@ -13,38 +13,32 @@ class SQLiteCache implements CacheInterface public function __construct() { if (!extension_loaded('sqlite3')) { - print render('error.html.php', ['message' => '"sqlite3" extension not loaded. Please check "php.ini"']); - exit; + throw new \Exception('"sqlite3" extension not loaded. Please check "php.ini"'); } if (!is_writable(PATH_CACHE)) { - returnServerError( - 'RSS-Bridge does not have write permissions for ' - . PATH_CACHE . '!' - ); + throw new \Exception('The cache folder is not writable'); } $section = 'SQLiteCache'; $file = Configuration::getConfig($section, 'file'); if (empty($file)) { - $message = sprintf('Configuration for %s missing. Please check your %s', $section, FILE_CONFIG); - print render('error.html.php', ['message' => $message]); - exit; + throw new \Exception(sprintf('Configuration for %s missing.', $section)); } + if (dirname($file) == '.') { $file = PATH_CACHE . $file; } elseif (!is_dir(dirname($file))) { - $message = sprintf('Invalid configuration for %s. Please check your %s', $section, FILE_CONFIG); - print render('error.html.php', ['message' => $message]); - exit; + throw new \Exception(sprintf('Invalid configuration for %s', $section)); } if (!is_file($file)) { - $this->db = new SQLite3($file); + // The instantiation creates the file + $this->db = new \SQLite3($file); $this->db->enableExceptions(true); $this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)"); } else { - $this->db = new SQLite3($file); + $this->db = new \SQLite3($file); $this->db->enableExceptions(true); } $this->db->busyTimeout(5000); @@ -55,8 +49,8 @@ class SQLiteCache implements CacheInterface $Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key'); $Qselect->bindValue(':key', $this->getCacheKey()); $result = $Qselect->execute(); - if ($result instanceof SQLite3Result) { - $data = $result->fetchArray(SQLITE3_ASSOC); + if ($result instanceof \SQLite3Result) { + $data = $result->fetchArray(\SQLITE3_ASSOC); if (isset($data['value'])) { return unserialize($data['value']); } @@ -81,7 +75,7 @@ class SQLiteCache implements CacheInterface $Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key'); $Qselect->bindValue(':key', $this->getCacheKey()); $result = $Qselect->execute(); - if ($result instanceof SQLite3Result) { + if ($result instanceof \SQLite3Result) { $data = $result->fetchArray(SQLITE3_ASSOC); if (isset($data['updated'])) { return $data['updated']; @@ -98,10 +92,6 @@ class SQLiteCache implements CacheInterface $Qdelete->execute(); } - /** - * Set scope - * @return self - */ public function setScope($scope) { if (is_null($scope) || !is_string($scope)) { @@ -112,10 +102,6 @@ class SQLiteCache implements CacheInterface return $this; } - /** - * Set key - * @return self - */ public function setKey($key) { if (!empty($key) && is_array($key)) { @@ -131,8 +117,6 @@ class SQLiteCache implements CacheInterface return $this; } - //////////////////////////////////////////////////////////////////////////// - private function getCacheKey() { if (is_null($this->key)) { diff --git a/composer.json b/composer.json index 4a63e72e..2c0c5038 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "suggest": { "ext-memcached": "Allows to use memcached as cache type", "ext-sqlite3": "Allows to use an SQLite database for caching", + "ext-zip": "Required for FDroidRepoBridge", "ext-dom": "Allows to use some bridges based on XPath expressions" }, "autoload-dev": { diff --git a/config.default.ini.php b/config.default.ini.php index b7f2677b..7ea40bc2 100644 --- a/config.default.ini.php +++ b/config.default.ini.php @@ -65,12 +65,10 @@ by_bridge = false ; false = disabled (default) enable = false -; The username for authentication. Insert this name when prompted for login. -username = "" +username = "admin" -; The password for authentication. Insert this password when prompted for login. -; Use a strong password to prevent others from guessing your login! -password = "" +; This default password is public knowledge. Replace it. +password = "7afbf648a369b261" [error] diff --git a/docs/08_Format_API/02_FormatInterface.md b/docs/08_Format_API/02_FormatInterface.md index 88448c38..f216092c 100644 --- a/docs/08_Format_API/02_FormatInterface.md +++ b/docs/08_Format_API/02_FormatInterface.md @@ -82,7 +82,7 @@ getExtraInfos(): array The `getMimeType` function returns the expected [MIME type](https://en.wikipedia.org/wiki/Media_type#Common_examples) of the format's output. ```PHP -getMimeType(): string +parse_mime_type(): string ``` # Template diff --git a/docs/08_Format_API/03_FormatAbstract.md b/docs/08_Format_API/03_FormatAbstract.md index 8cf0418f..cb206b85 100644 --- a/docs/08_Format_API/03_FormatAbstract.md +++ b/docs/08_Format_API/03_FormatAbstract.md @@ -9,5 +9,5 @@ The `FormatAbstract` class implements the [`FormatInterface`](../08_Format_API/0 The `sanitizeHtml` function receives an HTML formatted string and returns the string with disabled ` + + +
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/templates/error.html.php b/templates/error.html.php index 12f77b0b..2473e5f2 100644 --- a/templates/error.html.php +++ b/templates/error.html.php @@ -10,6 +10,9 @@
+

Stacktrace

+
+ diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 996886a7..3dd389c6 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -16,4 +16,19 @@ final class UtilsTest extends TestCase $this->assertSame('foo', truncate('foo', 4)); $this->assertSame('fo[...]', truncate('foo', 2, '[...]')); } + + public function testFileCache() + { + $sut = new \FileCache(); + $sut->setScope('scope'); + $sut->purgeCache(-1); + $sut->setKey(['key']); + + $this->assertNull($sut->loadData()); + + $sut->saveData('data'); + $this->assertSame('data', $sut->loadData()); + $this->assertIsNumeric($sut->getTime()); + $sut->purgeCache(-1); + } }