Compare commits

..

No commits in common. "52180c9f8f3c06b8bdfb942dd2aa2818e9dd41c6" and "602e8684258062937d7f554ab7889e8e02318c96" have entirely different histories.

22 changed files with 268 additions and 477 deletions

View File

@ -1,36 +0,0 @@
<?php
interface Cache_Adapter {
public function set_dir(string $dir) : void;
public function get_dir(): string;
public function make_dir(): bool;
public function is_writable(?string $filename = null): bool;
public function exists(string $filename): bool;
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
*/
public function get_size(string $filename);
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, timestamp otherwise
*/
public function get_mtime(string $filename);
/**
* @param mixed $data
*
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data);
public function get(string $filename): ?string;
public function get_full_path(string $filename): string;
public function remove(string $filename) : bool;
/**
* @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
*/
public function get_mime_type(string $filename);
/**
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
public function send(string $filename);
/** Catchall function to expire all subfolders/prefixes in the cache, invoked on the backend */
public function expire_all(): void;
}

View File

@ -1,146 +0,0 @@
<?php
class Cache_Local implements Cache_Adapter {
private string $dir;
public function remove(string $filename): bool {
return unlink($this->get_full_path($filename));
}
public function get_mtime(string $filename) {
return filemtime($this->get_full_path($filename));
}
public function set_dir(string $dir) : void {
$this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir));
$this->make_dir();
}
public function get_dir(): string {
return $this->dir;
}
public function make_dir(): bool {
if (!is_dir($this->dir)) {
return mkdir($this->dir);
}
return false;
}
public function is_writable(?string $filename = null): bool {
if ($filename) {
if (file_exists($this->get_full_path($filename)))
return is_writable($this->get_full_path($filename));
else
return is_writable($this->dir);
} else {
return is_writable($this->dir);
}
}
public function exists(string $filename): bool {
return file_exists($this->get_full_path($filename));
}
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
*/
public function get_size(string $filename) {
if ($this->exists($filename))
return filesize($this->get_full_path($filename));
else
return -1;
}
public function get_full_path(string $filename): string {
return $this->dir . "/" . basename(clean($filename));
}
public function get(string $filename): ?string {
if ($this->exists($filename))
return file_get_contents($this->get_full_path($filename));
else
return null;
}
/**
* @param mixed $data
*
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data) {
return file_put_contents($this->get_full_path($filename), $data);
}
/**
* @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
*/
public function get_mime_type(string $filename) {
if ($this->exists($filename))
return mime_content_type($this->get_full_path($filename));
else
return null;
}
/**
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
public function send(string $filename) {
return $this->send_local_file($this->get_full_path($filename));
}
public function expire_all(): void {
$dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir");
foreach ($dirs as $cache_dir) {
$num_deleted = 0;
if (is_writable($cache_dir) && !file_exists("$cache_dir/.no-auto-expiry")) {
$files = glob("$cache_dir/*");
if ($files) {
foreach ($files as $file) {
if (time() - filemtime($file) > 86400 * Config::get(Config::CACHE_MAX_DAYS)) {
unlink($file);
++$num_deleted;
}
}
}
Debug::log("Expired $cache_dir: removed $num_deleted files.");
}
}
}
/**
* this is essentially a wrapper for readfile() which allows plugins to hook
* output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
*
* hook function should return true if request was handled (or at least attempted to)
*
* note that this can be called without user context so the plugin to handle this
* should be loaded systemwide in config.php
*
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
function send_local_file(string $filename) {
if (file_exists($filename)) {
if (is_writable($filename)) touch($filename);
$tmppluginhost = new PluginHost();
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_SYSTEM);
//$tmppluginhost->load_data();
if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename))
return true;
return readfile($filename);
} else {
return false;
}
}
}

View File

@ -17,7 +17,7 @@ class Db_Migrations {
function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql"): void { function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql"): void {
$plugin_dir = PluginHost::getInstance()->get_plugin_dir($plugin); $plugin_dir = PluginHost::getInstance()->get_plugin_dir($plugin);
$this->initialize("{$plugin_dir}/{$schema_suffix}", $this->initialize($plugin_dir . "/${schema_suffix}",
strtolower("ttrss_migrations_plugin_" . get_class($plugin)), strtolower("ttrss_migrations_plugin_" . get_class($plugin)),
$base_is_latest); $base_is_latest);
} }
@ -31,7 +31,7 @@ class Db_Migrations {
} }
private function set_version(int $version): void { private function set_version(int $version): void {
Debug::log("Updating table {$this->migrations_table} with version {$version}...", Debug::LOG_EXTENDED); Debug::log("Updating table {$this->migrations_table} with version ${version}...", Debug::LOG_EXTENDED);
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}"); $sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
@ -170,7 +170,7 @@ class Db_Migrations {
try { try {
$this->migrate_to($i); $this->migrate_to($i);
} catch (PDOException $e) { } catch (PDOException $e) {
user_error("Failed to apply migration {$i} for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING); user_error("Failed to apply migration ${i} for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
return false; return false;
//throw $e; //throw $e;
} }
@ -184,7 +184,7 @@ class Db_Migrations {
*/ */
private function get_lines(int $version) : array { private function get_lines(int $version) : array {
if ($version > 0) if ($version > 0)
$filename = "{$this->migrations_path}/{$version}.sql"; $filename = "{$this->migrations_path}/${version}.sql";
else else
$filename = "{$this->base_path}/{$this->base_filename}"; $filename = "{$this->base_path}/{$this->base_filename}";
@ -196,7 +196,7 @@ class Db_Migrations {
fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"])); fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]));
} else { } else {
user_error("Requested schema file {$filename} not found.", E_USER_ERROR); user_error("Requested schema file ${filename} not found.", E_USER_ERROR);
return []; return [];
} }
} }

View File

@ -1,7 +1,6 @@
<?php <?php
class DiskCache implements Cache_Adapter { class DiskCache {
/** @var Cache_Adapter $adapter */ private string $dir;
private $adapter;
/** /**
* https://stackoverflow.com/a/53662733 * https://stackoverflow.com/a/53662733
@ -196,52 +195,47 @@ class DiskCache implements Cache_Adapter {
]; ];
public function __construct(string $dir) { public function __construct(string $dir) {
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) { $this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir));
if (implements_interface($p, "Cache_Adapter")) {
/** @var Cache_Adapter $p */
$this->adapter = clone $p; // we need separate object instances for separate directories
$this->adapter->set_dir($dir);
return;
}
} }
$this->adapter = new Cache_Local(); public function get_dir(): string {
$this->adapter->set_dir($dir); return $this->dir;
}
public function remove(string $filename): bool {
return $this->adapter->remove($filename);
}
public function set_dir(string $dir) : void {
$this->adapter->set_dir($dir);
}
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, timestamp otherwise
*/
public function get_mtime(string $filename) {
return $this->adapter->get_mtime(basename($filename));
} }
public function make_dir(): bool { public function make_dir(): bool {
return $this->adapter->make_dir(); if (!is_dir($this->dir)) {
return mkdir($this->dir);
}
return false;
} }
public function is_writable(?string $filename = null): bool { public function is_writable(?string $filename = null): bool {
return $this->adapter->is_writable(basename($filename)); if ($filename) {
if (file_exists($this->get_full_path($filename)))
return is_writable($this->get_full_path($filename));
else
return is_writable($this->dir);
} else {
return is_writable($this->dir);
}
} }
public function exists(string $filename): bool { public function exists(string $filename): bool {
return $this->adapter->exists(basename($filename)); return file_exists($this->get_full_path($filename));
} }
/** /**
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise * @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
*/ */
public function get_size(string $filename) { public function get_size(string $filename) {
return $this->adapter->get_size(basename($filename)); if ($this->exists($filename))
return filesize($this->get_full_path($filename));
else
return -1;
}
public function get_full_path(string $filename): string {
return $this->dir . "/" . basename(clean($filename));
} }
/** /**
@ -250,27 +244,11 @@ class DiskCache implements Cache_Adapter {
* @return int|false Bytes written or false if an error occurred. * @return int|false Bytes written or false if an error occurred.
*/ */
public function put(string $filename, $data) { public function put(string $filename, $data) {
return $this->adapter->put(basename($filename), $data); return file_put_contents($this->get_full_path($filename), $data);
} }
/** @deprecated we can't assume cached files are local, and other storages
* might not support this operation (object metadata may be immutable) */
public function touch(string $filename): bool { public function touch(string $filename): bool {
user_error("DiskCache: called unsupported method touch() for $filename", E_USER_DEPRECATED); return touch($this->get_full_path($filename));
return false;
}
public function get(string $filename): ?string {
return $this->adapter->get(basename($filename));
}
public function expire_all(): void {
$this->adapter->expire_all();
}
public function get_dir(): string {
return $this->adapter->get_dir();
} }
/** Downloads $url to cache as $local_filename if its missing (unless $force-ed) /** Downloads $url to cache as $local_filename if its missing (unless $force-ed)
@ -293,69 +271,25 @@ class DiskCache implements Cache_Adapter {
return false; return false;
} }
public function send(string $filename) { public function get(string $filename): ?string {
$filename = basename($filename); if ($this->exists($filename))
return file_get_contents($this->get_full_path($filename));
if (!$this->exists($filename)) { else
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); return null;
echo "File not found.";
return false;
}
$gmt_modified = gmdate("D, d M Y H:i:s", (int)$this->get_mtime($filename)) . " GMT";
if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified) {
header('HTTP/1.1 304 Not Modified');
return false;
}
$mimetype = $this->get_mime_type($filename);
if ($mimetype == "application/octet-stream")
$mimetype = "video/mp4";
# block SVG because of possible embedded javascript (.....)
$mimetype_blacklist = [ "image/svg+xml" ];
/* only serve video and images */
if (!preg_match("/(image|audio|video)\//", (string)$mimetype) || in_array($mimetype, $mimetype_blacklist)) {
http_response_code(400);
header("Content-type: text/plain");
print "Stored file has disallowed content type ($mimetype)";
return false;
}
$fake_extension = $this->get_fake_extension($filename);
if ($fake_extension)
$fake_extension = ".$fake_extension";
header("Content-Disposition: inline; filename=\"{$filename}{$fake_extension}\"");
header("Content-type: $mimetype");
$stamp_expires = gmdate("D, d M Y H:i:s",
(int)$this->get_mtime($filename) + 86400 * Config::get(Config::CACHE_MAX_DAYS)) . " GMT";
header("Expires: $stamp_expires", true);
header("Last-Modified: $gmt_modified", true);
header("Cache-Control: public");
header_remove("Pragma");
return $this->adapter->send($filename);
}
public function get_full_path(string $filename): string {
return $this->adapter->get_full_path(basename($filename));
} }
/**
* @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
*/
public function get_mime_type(string $filename) { public function get_mime_type(string $filename) {
return $this->adapter->get_mime_type(basename($filename)); if ($this->exists($filename))
return mime_content_type($this->get_full_path($filename));
else
return null;
} }
public function get_fake_extension(string $filename): string { public function get_fake_extension(string $filename): string {
$mimetype = $this->adapter->get_mime_type(basename($filename)); $mimetype = $this->get_mime_type($filename);
if ($mimetype) if ($mimetype)
return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : ""; return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : "";
@ -363,8 +297,22 @@ class DiskCache implements Cache_Adapter {
return ""; return "";
} }
/**
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
public function send(string $filename) {
$fake_extension = $this->get_fake_extension($filename);
if ($fake_extension)
$fake_extension = ".$fake_extension";
header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\"");
return $this->send_local_file($this->get_full_path($filename));
}
public function get_url(string $filename): string { public function get_url(string $filename): string {
return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->adapter->get_dir()) . "/" . basename($filename); return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->dir) . "/" . basename($filename);
} }
// check for locally cached (media) URLs and rewrite to local versions // check for locally cached (media) URLs and rewrite to local versions
@ -427,4 +375,84 @@ class DiskCache implements Cache_Adapter {
} }
return $res; return $res;
} }
static function expire(): void {
$dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir");
foreach ($dirs as $cache_dir) {
$num_deleted = 0;
if (is_writable($cache_dir) && !file_exists("$cache_dir/.no-auto-expiry")) {
$files = glob("$cache_dir/*");
if ($files) {
foreach ($files as $file) {
if (time() - filemtime($file) > 86400*Config::get(Config::CACHE_MAX_DAYS)) {
unlink($file);
++$num_deleted;
}
}
}
Debug::log("Expired $cache_dir: removed $num_deleted files.");
}
}
}
/* */
/**
* this is essentially a wrapper for readfile() which allows plugins to hook
* output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
*
* hook function should return true if request was handled (or at least attempted to)
*
* note that this can be called without user context so the plugin to handle this
* should be loaded systemwide in config.php
*
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
function send_local_file(string $filename) {
if (file_exists($filename)) {
if (is_writable($filename)) touch($filename);
$mimetype = mime_content_type($filename);
// this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
// video files are detected as octet-stream by mime_content_type()
if ($mimetype == "application/octet-stream")
$mimetype = "video/mp4";
# block SVG because of possible embedded javascript (.....)
$mimetype_blacklist = [ "image/svg+xml" ];
/* only serve video and images */
if (!preg_match("/(image|audio|video)\//", (string)$mimetype) || in_array($mimetype, $mimetype_blacklist)) {
http_response_code(400);
header("Content-type: text/plain");
print "Stored file has disallowed content type ($mimetype)";
return false;
}
$tmppluginhost = new PluginHost();
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_SYSTEM);
//$tmppluginhost->load_data();
if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename))
return true;
header("Content-type: $mimetype");
$stamp = gmdate("D, d M Y H:i:s", (int)filemtime($filename)) . " GMT";
header("Last-Modified: $stamp", true);
return readfile($filename);
} else {
return false;
}
}
} }

View File

@ -1163,28 +1163,11 @@ class Feeds extends Handler_Protected {
} }
static function _get_icon_file(int $feed_id): string { static function _get_icon_file(int $feed_id): string {
$favicon_cache = new DiskCache('feed-icons'); return Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
return $favicon_cache->get_full_path((string)$feed_id);
} }
static function _get_icon_url(int $feed_id, string $fallback_url = "") : string { static function _has_icon(int $id): bool {
if (self::_has_icon($feed_id)) { return is_file(Config::get(Config::ICONS_DIR) . "/$id.ico") && filesize(Config::get(Config::ICONS_DIR) . "/$id.ico") > 0;
$icon_url = Config::get_self_url() . "/public.php?" . http_build_query([
'op' => 'feed_icon',
'id' => $feed_id,
]);
return $icon_url;
}
return $fallback_url;
}
static function _has_icon(int $feed_id): bool {
$favicon_cache = new DiskCache('feed-icons');
return $favicon_cache->exists((string)$feed_id);
} }
/** /**
@ -1208,9 +1191,16 @@ class Feeds extends Handler_Protected {
if ($id < LABEL_BASE_INDEX) { if ($id < LABEL_BASE_INDEX) {
return "label"; return "label";
} else { } else {
return self::_get_icon_url($id); $icon = self::_get_icon_file($id);
if ($icon && file_exists($icon)) {
return Config::get(Config::ICONS_URL) . "/" . basename($icon) . "?" . filemtime($icon);
} }
} }
break;
}
return false;
} }
/** /**
@ -1759,11 +1749,11 @@ class Feeds extends Handler_Protected {
} }
if (!$allow_archived) { if (!$allow_archived) {
$from_qpart = "{$ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id), ttrss_feeds"; $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id), ttrss_feeds";
$feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND"; $feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND";
} else { } else {
$from_qpart = "{$ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id) $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id)
LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)"; LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)";
$feed_check_qpart = ""; $feed_check_qpart = "";
} }
@ -2248,7 +2238,7 @@ class Feeds extends Handler_Protected {
* @return array{0: string, 1: array<int, string>} [$search_query_part, $search_words] * @return array{0: string, 1: array<int, string>} [$search_query_part, $search_words]
*/ */
private static function _search_to_sql(string $search, string $search_language, int $owner_uid): array { private static function _search_to_sql(string $search, string $search_language, int $owner_uid): array {
$keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"{$1}:{$2}', trim($search)), ' '); $keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"${1}:${2}', trim($search)), ' ');
$query_keywords = array(); $query_keywords = array();
$search_words = array(); $search_words = array();
$search_query_leftover = array(); $search_query_leftover = array();

View File

@ -769,18 +769,6 @@ class Handler_Public extends Handler {
} }
} }
function feed_icon() : void {
$id = (int)$_REQUEST['id'];
$cache = new DiskCache('feed-icons');
if ($cache->exists((string)$id)) {
$cache->send((string)$id);
} else {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
echo "File not found.";
}
}
private function _make_article_tag_uri(int $id, string $timestamp): string { private function _make_article_tag_uri(int $id, string $timestamp): string {
$timestamp = date("Y-m-d", strtotime($timestamp)); $timestamp = date("Y-m-d", strtotime($timestamp));

View File

@ -14,15 +14,15 @@ class PluginHandler extends Handler_Protected {
if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) { if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) {
$plugin->$method(); $plugin->$method();
} else { } else {
user_error("Rejected {$plugin_name}->{$method}(): invalid CSRF token.", E_USER_WARNING); user_error("Rejected ${plugin_name}->${method}(): invalid CSRF token.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNAUTHORIZED); print Errors::to_json(Errors::E_UNAUTHORIZED);
} }
} else { } else {
user_error("Rejected {$plugin_name}->{$method}(): unknown method.", E_USER_WARNING); user_error("Rejected ${plugin_name}->${method}(): unknown method.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_METHOD); print Errors::to_json(Errors::E_UNKNOWN_METHOD);
} }
} else { } else {
user_error("Rejected {$plugin_name}->{$method}(): unknown plugin.", E_USER_WARNING); user_error("Rejected ${plugin_name}->${method}(): unknown plugin.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN); print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
} }
} }

View File

@ -454,7 +454,7 @@ class PluginHost {
// WIP hack // WIP hack
// we can't catch incompatible method signatures via Throwable // we can't catch incompatible method signatures via Throwable
// this also enables global tt-rss safe mode in case there are more plugins like this // this also enables global tt-rss safe mode in case there are more plugins like this
if (!getenv('TTRSS_XDEBUG_ENABLED') && ($_SESSION["plugin_blacklist"][$class] ?? 0)) { if (($_SESSION["plugin_blacklist"][$class] ?? 0)) {
// only report once per-plugin per-session // only report once per-plugin per-session
if ($_SESSION["plugin_blacklist"][$class] < 2) { if ($_SESSION["plugin_blacklist"][$class] < 2) {

View File

@ -454,15 +454,14 @@ class Pref_Feeds extends Handler_Protected {
function removeIcon(): void { function removeIcon(): void {
$feed_id = (int) $_REQUEST["feed_id"]; $feed_id = (int) $_REQUEST["feed_id"];
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
$cache = new DiskCache('feed-icons');
$feed = ORM::for_table('ttrss_feeds') $feed = ORM::for_table('ttrss_feeds')
->where('owner_uid', $_SESSION['uid']) ->where('owner_uid', $_SESSION['uid'])
->find_one($feed_id); ->find_one($feed_id);
if ($feed && $cache->exists((string)$feed_id)) { if ($feed && file_exists($icon_file)) {
if ($cache->remove((string)$feed_id)) { if (unlink($icon_file)) {
$feed->set([ $feed->set([
'favicon_avg_color' => null, 'favicon_avg_color' => null,
'favicon_last_checked' => '1970-01-01', 'favicon_last_checked' => '1970-01-01',
@ -487,9 +486,11 @@ class Pref_Feeds extends Handler_Protected {
if ($feed && $tmp_file && move_uploaded_file($_FILES['icon_file']['tmp_name'], $tmp_file)) { if ($feed && $tmp_file && move_uploaded_file($_FILES['icon_file']['tmp_name'], $tmp_file)) {
if (filesize($tmp_file) < Config::get(Config::MAX_FAVICON_FILE_SIZE)) { if (filesize($tmp_file) < Config::get(Config::MAX_FAVICON_FILE_SIZE)) {
$cache = new DiskCache('feed-icons'); $new_filename = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
if ($cache->put((string)$feed_id, file_get_contents($tmp_file))) { if (file_exists($new_filename)) unlink($new_filename);
if (rename($tmp_file, $new_filename)) {
chmod($new_filename, 0644);
$feed->set([ $feed->set([
'favicon_avg_color' => null, 'favicon_avg_color' => null,
@ -503,9 +504,6 @@ class Pref_Feeds extends Handler_Protected {
} else { } else {
$rc = self::E_ICON_RENAME_FAILED; $rc = self::E_ICON_RENAME_FAILED;
} }
@unlink($tmp_file);
} else { } else {
$rc = self::E_ICON_FILE_TOO_LARGE; $rc = self::E_ICON_FILE_TOO_LARGE;
} }
@ -1188,10 +1186,9 @@ class Pref_Feeds extends Handler_Protected {
$pdo->commit(); $pdo->commit();
$favicon_cache = new DiskCache('feed-icons'); if (file_exists(Config::get(Config::ICONS_DIR) . "/$id.ico")) {
unlink(Config::get(Config::ICONS_DIR) . "/$id.ico");
if ($favicon_cache->exists((string)$id)) }
$favicon_cache->remove((string)$id);
} else { } else {
Labels::remove(Labels::feed_to_label_id($id), $owner_uid); Labels::remove(Labels::feed_to_label_id($id), $owner_uid);

View File

@ -61,7 +61,7 @@ class Pref_Labels extends Handler_Protected {
if ($kind == "fg" || $kind == "bg") { if ($kind == "fg" || $kind == "bg") {
$sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET $sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
{$kind}_color = ? WHERE id = ? ${kind}_color = ? WHERE id = ?
AND owner_uid = ?"); AND owner_uid = ?");
$sth->execute([$color, $id, $_SESSION['uid']]); $sth->execute([$color, $id, $_SESSION['uid']]);

View File

@ -240,7 +240,7 @@ class Pref_Prefs extends Handler_Protected {
$user->full_name = clean($_POST['full_name']); $user->full_name = clean($_POST['full_name']);
if ($user->email != $new_email) { if ($user->email != $new_email) {
Logger::log(E_USER_NOTICE, "Email address of user {$user->login} has been changed to {$new_email}."); Logger::log(E_USER_NOTICE, "Email address of user ".$user->login." has been changed to ${new_email}.");
if ($user->email) { if ($user->email) {
$mailer = new Mailer(); $mailer = new Mailer();

View File

@ -82,10 +82,11 @@ class Pref_Users extends Handler_Administrative {
<?php while ($row = $sth->fetch()) { ?> <?php while ($row = $sth->fetch()) { ?>
<li> <li>
<?php <?php
$icon_url = Feeds::_get_icon_url($row['id'], 'images/blank_icon.gif'); $icon_file = Config::get(Config::ICONS_URL) . "/" . $row["id"] . ".ico";
$icon = file_exists($icon_file) ? $icon_file : "images/blank_icon.gif";
?> ?>
<img class="icon" src="<?= htmlspecialchars($icon_url) ?>"> <img class="icon" src="<?= $icon_file ?>">
<a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>"> <a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>">
<?= htmlspecialchars($row["title"]) ?> <?= htmlspecialchars($row["title"]) ?>

View File

@ -451,7 +451,7 @@ class RPC extends Handler_Protected {
$params["safe_mode"] = !empty($_SESSION["safe_mode"]); $params["safe_mode"] = !empty($_SESSION["safe_mode"]);
$params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES); $params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES);
$params["icons_url"] = Config::get(Config::SELF_URL_PATH) . '/public.php'; $params["icons_url"] = Config::get(Config::ICONS_URL);
$params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME); $params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME);
$params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE); $params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE);
$params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT); $params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT);

View File

@ -37,33 +37,24 @@ class RSSUtils {
$pdo = Db::pdo(); $pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ?"); $sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ?");
$cache = new DiskCache('feed-icons'); // check icon files once every Config::get(Config::CACHE_MAX_DAYS) days
$icon_files = array_filter(glob(Config::get(Config::ICONS_DIR) . "/*.ico"),
fn(string $f) => filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS));
if ($cache->is_writable()) { foreach ($icon_files as $icon) {
$dh = opendir($cache->get_full_path("")); $feed_id = basename($icon, ".ico");
if ($dh) { $sth->execute([$feed_id]);
while (($icon = readdir($dh)) !== false) {
if ($cache->get_mtime($icon) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS)) {
$sth->execute([(int)$icon]);
if ($sth->fetch()) { if ($sth->fetch()) {
$cache->put($icon, $cache->get($icon)); @touch($icon);
} else { } else {
$icon_path = $cache->get_full_path($icon); Debug::log("Removing orphaned feed icon: $icon");
Debug::log("Removing orphaned feed icon: $icon_path");
unlink($icon); unlink($icon);
} }
} }
} }
closedir($dh);
}
}
}
/** /**
* @param array<string, false|string> $options * @param array<string, false|string> $options
*/ */
@ -347,9 +338,6 @@ class RSSUtils {
$pdo = Db::pdo(); $pdo = Db::pdo();
/** @var DiskCache $cache */
$cache = new DiskCache('feeds');
if (Config::get(Config::DB_TYPE) == "pgsql") { if (Config::get(Config::DB_TYPE) == "pgsql") {
$favicon_interval_qpart = "favicon_last_checked < NOW() - INTERVAL '12 hour'"; $favicon_interval_qpart = "favicon_last_checked < NOW() - INTERVAL '12 hour'";
} else { } else {
@ -399,7 +387,7 @@ class RSSUtils {
$date_feed_processed = date('Y-m-d H:i'); $date_feed_processed = date('Y-m-d H:i');
$cache_filename = sha1($feed_obj->feed_url) . ".xml"; $cache_filename = Config::get(Config::CACHE_DIR) . "/feeds/" . sha1($feed_obj->feed_url) . ".xml";
$pluginhost = new PluginHost(); $pluginhost = new PluginHost();
$user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $feed_obj->owner_uid); $user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $feed_obj->owner_uid);
@ -435,13 +423,13 @@ class RSSUtils {
// try cache // try cache
if (!$feed_data && if (!$feed_data &&
$cache->exists($cache_filename) && is_readable($cache_filename) &&
!$feed_obj->auth_login && !$feed_obj->auth_pass && !$feed_obj->auth_login && !$feed_obj->auth_pass &&
$cache->get_mtime($cache_filename) > time() - 30) { filemtime($cache_filename) > time() - 30) {
Debug::log("using local cache: {$cache_filename}.", Debug::LOG_VERBOSE); Debug::log("using local cache: {$cache_filename}.", Debug::LOG_VERBOSE);
$feed_data = $cache->get($cache_filename); $feed_data = file_get_contents($cache_filename);
if ($feed_data) { if ($feed_data) {
$rss_hash = sha1($feed_data); $rss_hash = sha1($feed_data);
@ -489,12 +477,12 @@ class RSSUtils {
} }
// cache vanilla feed data for re-use // cache vanilla feed data for re-use
if ($feed_data && !$feed_obj->auth_pass && !$feed_obj->auth_login && $cache->is_writable()) { if ($feed_data && !$feed_obj->auth_pass && !$feed_obj->auth_login && is_writable(Config::get(Config::CACHE_DIR) . "/feeds")) {
$new_rss_hash = sha1($feed_data); $new_rss_hash = sha1($feed_data);
if ($new_rss_hash != $rss_hash) { if ($new_rss_hash != $rss_hash) {
Debug::log("saving to local cache: $cache_filename", Debug::LOG_VERBOSE); Debug::log("saving to local cache: $cache_filename", Debug::LOG_VERBOSE);
$cache->put($cache_filename, $feed_data); file_put_contents($cache_filename, $feed_data);
} }
} }
} }
@ -602,28 +590,21 @@ class RSSUtils {
if ($feed_obj->favicon_needs_check || $force_refetch) { if ($feed_obj->favicon_needs_check || $force_refetch) {
// restrict update attempts to once per 12h /* terrible hack: if we crash on floicon shit here, we won't check
$feed_obj->favicon_last_checked = Db::NOW(); * the icon avgcolor again (unless the icon got updated) */
$feed_obj->save();
$favicon_cache = new DiskCache('feed-icons'); $favicon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
$favicon_modified = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
$favicon_modified = $favicon_cache->exists($feed) ? $favicon_cache->get_mtime($feed) : -1;
// don't try to redownload custom favicons
if (!$feed_obj->favicon_is_custom) { if (!$feed_obj->favicon_is_custom) {
Debug::log("favicon: trying to update favicon...", Debug::LOG_VERBOSE); Debug::log("favicon: trying to update favicon...", Debug::LOG_VERBOSE);
self::update_favicon($feed_obj->site_url, $feed); self::update_favicon($feed_obj->site_url, $feed);
if (!$favicon_cache->exists($feed) || $favicon_cache->get_mtime($feed) > $favicon_modified) { if ((file_exists($favicon_file) ? filemtime($favicon_file) : -1) > $favicon_modified)
$feed_obj->favicon_avg_color = null; $feed_obj->favicon_avg_color = null;
$feed_obj->save();
}
} }
/* terrible hack: if we crash on floicon shit here, we won't check if (is_readable($favicon_file) && function_exists("imagecreatefromstring") && empty($feed_obj->favicon_avg_color)) {
* the icon avgcolor again (unless icon got updated) */
if (file_exists($favicon_cache->get_full_path($feed)) && function_exists("imagecreatefromstring") && empty($feed_obj->favicon_avg_color)) {
require_once "colors.php"; require_once "colors.php";
Debug::log("favicon: trying to calculate average color...", Debug::LOG_VERBOSE); Debug::log("favicon: trying to calculate average color...", Debug::LOG_VERBOSE);
@ -631,13 +612,13 @@ class RSSUtils {
$feed_obj->favicon_avg_color = 'fail'; $feed_obj->favicon_avg_color = 'fail';
$feed_obj->save(); $feed_obj->save();
$feed_obj->favicon_avg_color = \Colors\calculate_avg_color($favicon_cache->get_full_path($feed)); $feed_obj->favicon_avg_color = \Colors\calculate_avg_color($favicon_file);
$feed_obj->save(); $feed_obj->save();
Debug::log("favicon: avg color: {$feed_obj->favicon_avg_color}", Debug::LOG_VERBOSE); Debug::log("favicon: avg color: {$feed_obj->favicon_avg_color}", Debug::LOG_VERBOSE);
} else if ($feed_obj->favicon_avg_color == 'fail') { } else if ($feed_obj->favicon_avg_color == 'fail') {
Debug::log("floicon failed on $feed, not trying to recalculate avg color", Debug::LOG_VERBOSE); Debug::log("floicon failed $favicon_file, not trying to recalculate avg color", Debug::LOG_VERBOSE);
} }
} }
@ -1342,6 +1323,8 @@ class RSSUtils {
} else { } else {
Debug::log("cache_enclosures: failed with ".UrlHelper::$fetch_last_error_code.": ".UrlHelper::$fetch_last_error); Debug::log("cache_enclosures: failed with ".UrlHelper::$fetch_last_error_code.": ".UrlHelper::$fetch_last_error);
} }
} else if (is_writable($local_filename)) {
$cache->touch($local_filename);
} }
} }
} }
@ -1367,6 +1350,8 @@ class RSSUtils {
} else { } else {
Debug::log("cache_media: failed with ".UrlHelper::$fetch_last_error_code.": ".UrlHelper::$fetch_last_error); Debug::log("cache_media: failed with ".UrlHelper::$fetch_last_error_code.": ".UrlHelper::$fetch_last_error);
} }
} else if ($cache->is_writable($local_filename)) {
$cache->touch($local_filename);
} }
} }
@ -1688,36 +1673,9 @@ class RSSUtils {
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); $tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
} }
/** migrates favicons from legacy storage in feed-icons/ to cache/feed-icons/using new naming (sans .ico suffix) */
static function migrate_feed_icons() : void {
$old_dir = Config::get(Config::ICONS_DIR);
$new_dir = Config::get(Config::CACHE_DIR) . '/feed-icons';
$dh = opendir($old_dir);
$cache = new DiskCache('feed-icons');
if ($dh) {
while (($old_filename = readdir($dh)) !== false) {
if (strpos($old_filename, ".ico") !== false) {
$new_filename = str_replace(".ico", "", $old_filename);
$old_full_path = "$old_dir/$old_filename";
if (is_file($old_full_path) && $cache->put($new_filename, file_get_contents($old_full_path))) {
unlink($old_full_path);
}
}
}
closedir($dh);
}
}
static function housekeeping_common(): void { static function housekeeping_common(): void {
$cache = new DiskCache(""); DiskCache::expire();
$cache->expire_all();
self::migrate_feed_icons();
self::expire_lock_files(); self::expire_lock_files();
self::expire_error_log(); self::expire_error_log();
self::expire_feed_archive(); self::expire_feed_archive();
@ -1735,6 +1693,8 @@ class RSSUtils {
* @return false|string * @return false|string
*/ */
static function update_favicon(string $site_url, int $feed) { static function update_favicon(string $site_url, int $feed) {
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
$favicon_urls = self::get_favicon_urls($site_url); $favicon_urls = self::get_favicon_urls($site_url);
if (count($favicon_urls) == 0) { if (count($favicon_urls) == 0) {
@ -1789,18 +1749,21 @@ class RSSUtils {
break; break;
} }
$favicon_cache = new DiskCache('feed-icons'); Debug::log("favicon: $favicon_url looks valid, saving to $icon_file", Debug::LOG_VERBOSE);
if ($favicon_cache->is_writable()) { $fp = @fopen($icon_file, "w");
Debug::log("favicon: $favicon_url looks valid, saving to cache", Debug::LOG_VERBOSE);
// we deal with this manually if ($fp) {
if (!$favicon_cache->exists(".no-auto-expiry"))
$favicon_cache->put(".no-auto-expiry", ""); fwrite($fp, $contents);
fclose($fp);
chmod($icon_file, 0644);
clearstatcache();
return $icon_file;
return $favicon_cache->put((string)$feed, $contents);
} else { } else {
Debug::log("favicon: $favicon_url skipping, local cache is not writable", Debug::LOG_VERBOSE); Debug::log("favicon: failed to open $icon_file for writing", Debug::LOG_VERBOSE);
} }
} }

View File

@ -100,7 +100,7 @@ const Feeds = {
if (id > 0) { if (id > 0) {
if (has_img) { if (has_img) {
this.setIcon(id, false, this.setIcon(id, false,
App.getInitParam("icons_url") + '?' + dojo.objectToQuery({op: 'feed_icon', id: id})); App.getInitParam("icons_url") + "/" + id + ".ico?" + has_img);
} else { } else {
this.setIcon(id, false, 'images/blank_icon.gif'); this.setIcon(id, false, 'images/blank_icon.gif');
} }
@ -678,10 +678,8 @@ const Feeds = {
}); });
}, },
renderIcon: function(feed_id, exists) { renderIcon: function(feed_id, exists) {
const icon_url = App.getInitParam("icons_url") + '?' + dojo.objectToQuery({op: 'feed_icon', id: feed_id});
return feed_id && exists ? return feed_id && exists ?
`<img class="icon" src="${App.escapeHtml(icon_url)}">` : `<img class="icon" src="${App.escapeHtml(App.getInitParam("icons_url"))}/${feed_id}.ico">` :
`<i class='icon-no-feed material-icons'>rss_feed</i>`; `<i class='icon-no-feed material-icons'>rss_feed</i>`;
} }
}; };

View File

@ -88,18 +88,18 @@ function get_list_of_locales($locale) {
if ($modifier) { if ($modifier) {
if ($country) { if ($country) {
if ($charset) if ($charset)
array_push($locale_names, "{$lang}_$country.$charset@$modifier"); array_push($locale_names, "${lang}_$country.$charset@$modifier");
array_push($locale_names, "{$lang}_$country@$modifier"); array_push($locale_names, "${lang}_$country@$modifier");
} elseif ($charset) } elseif ($charset)
array_push($locale_names, "{$lang}.$charset@$modifier"); array_push($locale_names, "${lang}.$charset@$modifier");
array_push($locale_names, "$lang@$modifier"); array_push($locale_names, "$lang@$modifier");
} }
if ($country) { if ($country) {
if ($charset) if ($charset)
array_push($locale_names, "{$lang}_$country.$charset"); array_push($locale_names, "${lang}_$country.$charset");
array_push($locale_names, "{$lang}_$country"); array_push($locale_names, "${lang}_$country");
} elseif ($charset) } elseif ($charset)
array_push($locale_names, "{$lang}.$charset"); array_push($locale_names, "${lang}.$charset");
array_push($locale_names, $lang); array_push($locale_names, $lang);
} }

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-05 11:48+0300\n" "POT-Creation-Date: 2022-06-05 11:48+0300\n"
"PO-Revision-Date: 2022-11-23 11:05+0000\n" "PO-Revision-Date: 2022-06-09 13:15+0000\n"
"Last-Translator: xosé m. <correoxm@disroot.org>\n" "Last-Translator: xosé m. <correoxm@disroot.org>\n"
"Language-Team: Galician <https://weblate.tt-rss.org/projects/tt-rss/messages/" "Language-Team: Galician <https://weblate.tt-rss.org/projects/tt-rss/messages/"
"gl/>\n" "gl/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.14.2\n" "X-Generator: Weblate 4.12.2\n"
#: backend.php:60 #: backend.php:60
msgid "Use default" msgid "Use default"
@ -260,7 +260,7 @@ msgstr "Axuda sobre atallos de teclado"
#: index.php:289 #: index.php:289
msgid "Logout" msgid "Logout"
msgstr "Pechar Sesión" msgstr "Desconectar"
#: prefs.php:20 prefs.php:139 classes/pref/prefs.php:942 classes/rpc.php:646 #: prefs.php:20 prefs.php:139 classes/pref/prefs.php:942 classes/rpc.php:646
msgid "Preferences" msgid "Preferences"
@ -1844,7 +1844,7 @@ msgstr "O artigo compartido aparecerá na fonte Publicados."
#: plugins/bookmarklets/init.php:324 include/login_form.php:177 #: plugins/bookmarklets/init.php:324 include/login_form.php:177
msgid "Log in" msgid "Log in"
msgstr "Acceder" msgstr "Conectar"
#: plugins/bookmarklets/init.php:344 #: plugins/bookmarklets/init.php:344
#, php-format #, php-format
@ -1966,7 +1966,7 @@ msgstr "Fallou a validación da conta (cambiou o contrasinal)"
#: include/sessions.php:49 #: include/sessions.php:49
msgid "Session failed to validate (account is disabled)" msgid "Session failed to validate (account is disabled)"
msgstr "Fallou a validación da sesión (conta está desactivada)" msgstr "Fallou a validación da conta (conta está desactivada)"
#: include/sessions.php:53 #: include/sessions.php:53
msgid "Session failed to validate (user not found)" msgid "Session failed to validate (user not found)"
@ -2908,7 +2908,7 @@ msgid ""
"You will need to log out and back in to disable it." "You will need to log out and back in to disable it."
msgstr "" msgstr ""
"Tiny Tiny RSS estase executando en modo seguro. Tódolos decorados e " "Tiny Tiny RSS estase executando en modo seguro. Tódolos decorados e "
"complementos están desactivados. Terás que pechar sesión e volver a acceder " "complementos están desactivados. Terás que desconectar e volver a conectarte "
"para desactivalo." "para desactivalo."
#: js/CommonDialogs.js:53 #: js/CommonDialogs.js:53
@ -2921,7 +2921,7 @@ msgstr "Fontes dispoñibles"
#: js/CommonDialogs.js:85 js/PrefFeedTree.js:446 #: js/CommonDialogs.js:85 js/PrefFeedTree.js:446
msgid "Login" msgid "Login"
msgstr "Acceso" msgstr "Identificador:"
#: js/CommonDialogs.js:101 #: js/CommonDialogs.js:101
msgid "This feed requires authentication." msgid "This feed requires authentication."

Binary file not shown.

View File

@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: Tiny Tiny RSS\n" "Project-Id-Version: Tiny Tiny RSS\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-05 11:48+0300\n" "POT-Creation-Date: 2022-06-05 11:48+0300\n"
"PO-Revision-Date: 2022-08-25 06:49+0000\n" "PO-Revision-Date: 2021-11-21 17:41+0000\n"
"Last-Translator: TonyRL <tony_lao@outlook.com>\n" "Last-Translator: TonyRL <tony_lao@outlook.com>\n"
"Language-Team: Chinese (Traditional) <https://weblate.tt-rss.org/projects/" "Language-Team: Chinese (Traditional) <https://weblate.tt-rss.org/projects/tt-"
"tt-rss/messages/zh_Hant/>\n" "rss/messages/zh_Hant/>\n"
"Language: zh_TW\n" "Language: zh_TW\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.14\n" "X-Generator: Weblate 4.9.1\n"
#: backend.php:60 #: backend.php:60
msgid "Use default" msgid "Use default"
@ -238,7 +238,7 @@ msgstr "隱藏(顯示)已讀摘要"
#: index.php:273 #: index.php:273
msgid "UI layout:" msgid "UI layout:"
msgstr "使用者介面配置:" msgstr ""
#: index.php:274 classes/rpc.php:639 #: index.php:274 classes/rpc.php:639
msgid "Toggle combined mode" msgid "Toggle combined mode"
@ -1976,12 +1976,16 @@ msgid "Update daemon is not updating feeds."
msgstr "更新守護行程沒有更新摘要。" msgstr "更新守護行程沒有更新摘要。"
#: js/App.js:606 #: js/App.js:606
#, java-printf-format #, fuzzy, java-printf-format
#| msgid ""
#| "URL scheme reported by your browser (%a) doesn't match server-configured "
#| "SELF_URL_PATH (%b), check X-Forwarded-Proto."
msgid "" msgid ""
"URL scheme reported by your browser (%a) doesn't match server-configured " "URL scheme reported by your browser (%a) doesn't match server-configured "
"SELF_URL_PATH (%b), check X-Forwarded-Proto." "SELF_URL_PATH (%b), check X-Forwarded-Proto."
msgstr "瀏覽器報告的 URL 配置(%a與伺服器配置的 SELF_URL_PATH%b不符合請檢查 X" msgstr ""
"-Forwarded-Proto。" "瀏覽器報告的URL配置a與伺服器配置的 SELF_URL_PATHb不符合請檢查 "
"X-Forwarded-Proto。"
#: js/App.js:613 #: js/App.js:613
msgid "Fatal error" msgid "Fatal error"
@ -2620,9 +2624,10 @@ msgid "Reset to defaults?"
msgstr "重設為預設狀態?" msgstr "重設為預設狀態?"
#: js/PrefHelpers.js:373 #: js/PrefHelpers.js:373
#, java-printf-format, javascript-format #, fuzzy, java-printf-format, javascript-format
#| msgid "Error while parsing document."
msgid "Error while loading plugins list: %s." msgid "Error while loading plugins list: %s."
msgstr "載入擴充套件列表時發生錯誤:%s。" msgstr "解析文檔時發生錯誤。"
#: js/PrefHelpers.js:422 #: js/PrefHelpers.js:422
msgid "Clear data" msgid "Clear data"
@ -2861,7 +2866,7 @@ msgstr "堆疊追踪"
#: js/App.js:653 #: js/App.js:653
msgid "Additional information" msgid "Additional information"
msgstr "額外資訊" msgstr ""
#: js/Article.js:207 #: js/Article.js:207
msgid "Attachments" msgid "Attachments"
@ -3019,12 +3024,16 @@ msgid "System plugins are enabled using global configuration."
msgstr "" msgstr ""
#: js/PrefHelpers.js:577 #: js/PrefHelpers.js:577
#, fuzzy
#| msgid "Uninstall"
msgid "Install" msgid "Install"
msgstr "安裝" msgstr "解除安裝"
#: js/PrefHelpers.js:654 #: js/PrefHelpers.js:654
#, fuzzy
#| msgid "Playing..."
msgid "Updating..." msgid "Updating..."
msgstr "更新中……" msgstr "播放中……"
#: js/PrefHelpers.js:687 #: js/PrefHelpers.js:687
msgid "Updates complete" msgid "Updates complete"

View File

@ -1,7 +1,7 @@
parameters: parameters:
level: 6 level: 6
parallel: parallel:
maximumNumberOfProcesses: 4 maximumNumberOfProcesses: 2
reportUnmatchedIgnoredErrors: false reportUnmatchedIgnoredErrors: false
ignoreErrors: ignoreErrors:
- '#Constant.*\b(SUBSTRING_FOR_DATE|SCHEMA_VERSION|SELF_USER_AGENT|LABEL_BASE_INDEX|PLUGIN_FEED_BASE_INDEX)\b.*not found#' - '#Constant.*\b(SUBSTRING_FOR_DATE|SCHEMA_VERSION|SELF_USER_AGENT|LABEL_BASE_INDEX|PLUGIN_FEED_BASE_INDEX)\b.*not found#'
@ -24,6 +24,5 @@ parameters:
- plugins/**/test/* - plugins/**/test/*
- plugins.local/**/test/* - plugins.local/**/test/*
- plugins.local/*/vendor/intervention/* - plugins.local/*/vendor/intervention/*
- plugins.local/cache_s3/vendor/*
paths: paths:
- . - .

View File

@ -31,10 +31,10 @@ class Cache_Starred_Images extends Plugin {
chmod($this->cache_status->get_dir(), 0777); chmod($this->cache_status->get_dir(), 0777);
if (!$this->cache->exists(".no-auto-expiry")) if (!$this->cache->exists(".no-auto-expiry"))
$this->cache->put(".no-auto-expiry", ""); $this->cache->touch(".no-auto-expiry");
if (!$this->cache_status->exists(".no-auto-expiry")) if (!$this->cache_status->exists(".no-auto-expiry"))
$this->cache_status->put(".no-auto-expiry", ""); $this->cache_status->touch(".no-auto-expiry");
if ($this->cache->is_writable() && $this->cache_status->is_writable()) { if ($this->cache->is_writable() && $this->cache_status->is_writable()) {
$host->add_hook($host::HOOK_HOUSE_KEEPING, $this); $host->add_hook($host::HOOK_HOUSE_KEEPING, $this);