Compare commits
13 Commits
602e868425
...
52180c9f8f
Author | SHA1 | Date |
---|---|---|
Andrew Dolgov | 52180c9f8f | |
Andrew Dolgov | 3212c51ce8 | |
Andrew Dolgov | a30b9bb649 | |
Andrew Dolgov | be6bc72a74 | |
Andrew Dolgov | 3180b35807 | |
Andrew Dolgov | 9732d8fc9f | |
Andrew Dolgov | 10a1dd35e3 | |
Andrew Dolgov | 30c04adfa6 | |
xosé m | cb2f1ac2d9 | |
fox | 9a0dcdd6cc | |
wn_ | d376cd6142 | |
xosé m | 413d824f23 | |
TonyRL | e8b3cdcf4a |
|
@ -0,0 +1,36 @@
|
||||||
|
<?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;
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
class DiskCache {
|
class DiskCache implements Cache_Adapter {
|
||||||
private string $dir;
|
/** @var Cache_Adapter $adapter */
|
||||||
|
private $adapter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://stackoverflow.com/a/53662733
|
* https://stackoverflow.com/a/53662733
|
||||||
|
@ -195,47 +196,52 @@ class DiskCache {
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(string $dir) {
|
public function __construct(string $dir) {
|
||||||
$this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir));
|
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
||||||
|
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();
|
||||||
|
$this->adapter->set_dir($dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_dir(): string {
|
public function remove(string $filename): bool {
|
||||||
return $this->dir;
|
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 {
|
||||||
if (!is_dir($this->dir)) {
|
return $this->adapter->make_dir();
|
||||||
return mkdir($this->dir);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function is_writable(?string $filename = null): bool {
|
public function is_writable(?string $filename = null): bool {
|
||||||
if ($filename) {
|
return $this->adapter->is_writable(basename($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 file_exists($this->get_full_path($filename));
|
return $this->adapter->exists(basename($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) {
|
||||||
if ($this->exists($filename))
|
return $this->adapter->get_size(basename($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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -244,11 +250,27 @@ class DiskCache {
|
||||||
* @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 file_put_contents($this->get_full_path($filename), $data);
|
return $this->adapter->put(basename($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 {
|
||||||
return touch($this->get_full_path($filename));
|
user_error("DiskCache: called unsupported method touch() for $filename", E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -271,25 +293,69 @@ class DiskCache {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $filename): ?string {
|
public function send(string $filename) {
|
||||||
if ($this->exists($filename))
|
$filename = basename($filename);
|
||||||
return file_get_contents($this->get_full_path($filename));
|
|
||||||
else
|
if (!$this->exists($filename)) {
|
||||||
return null;
|
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||||
|
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) {
|
||||||
if ($this->exists($filename))
|
return $this->adapter->get_mime_type(basename($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->get_mime_type($filename);
|
$mimetype = $this->adapter->get_mime_type(basename($filename));
|
||||||
|
|
||||||
if ($mimetype)
|
if ($mimetype)
|
||||||
return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : "";
|
return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : "";
|
||||||
|
@ -297,22 +363,8 @@ class DiskCache {
|
||||||
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->dir) . "/" . basename($filename);
|
return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->adapter->get_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
|
||||||
|
@ -375,84 +427,4 @@ class DiskCache {
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1163,11 +1163,28 @@ class Feeds extends Handler_Protected {
|
||||||
}
|
}
|
||||||
|
|
||||||
static function _get_icon_file(int $feed_id): string {
|
static function _get_icon_file(int $feed_id): string {
|
||||||
return Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
|
$favicon_cache = new DiskCache('feed-icons');
|
||||||
|
|
||||||
|
return $favicon_cache->get_full_path((string)$feed_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static function _has_icon(int $id): bool {
|
static function _get_icon_url(int $feed_id, string $fallback_url = "") : string {
|
||||||
return is_file(Config::get(Config::ICONS_DIR) . "/$id.ico") && filesize(Config::get(Config::ICONS_DIR) . "/$id.ico") > 0;
|
if (self::_has_icon($feed_id)) {
|
||||||
|
$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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1191,16 +1208,9 @@ class Feeds extends Handler_Protected {
|
||||||
if ($id < LABEL_BASE_INDEX) {
|
if ($id < LABEL_BASE_INDEX) {
|
||||||
return "label";
|
return "label";
|
||||||
} else {
|
} else {
|
||||||
$icon = self::_get_icon_file($id);
|
return self::_get_icon_url($id);
|
||||||
|
|
||||||
if ($icon && file_exists($icon)) {
|
|
||||||
return Config::get(Config::ICONS_URL) . "/" . basename($icon) . "?" . filemtime($icon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1749,11 +1759,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 = "";
|
||||||
}
|
}
|
||||||
|
@ -2238,7 +2248,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();
|
||||||
|
|
|
@ -769,6 +769,18 @@ 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));
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (($_SESSION["plugin_blacklist"][$class] ?? 0)) {
|
if (!getenv('TTRSS_XDEBUG_ENABLED') && ($_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) {
|
||||||
|
|
|
@ -454,14 +454,15 @@ 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 && file_exists($icon_file)) {
|
if ($feed && $cache->exists((string)$feed_id)) {
|
||||||
if (unlink($icon_file)) {
|
if ($cache->remove((string)$feed_id)) {
|
||||||
$feed->set([
|
$feed->set([
|
||||||
'favicon_avg_color' => null,
|
'favicon_avg_color' => null,
|
||||||
'favicon_last_checked' => '1970-01-01',
|
'favicon_last_checked' => '1970-01-01',
|
||||||
|
@ -486,24 +487,25 @@ 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)) {
|
||||||
|
|
||||||
$new_filename = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
|
$cache = new DiskCache('feed-icons');
|
||||||
|
|
||||||
if (file_exists($new_filename)) unlink($new_filename);
|
if ($cache->put((string)$feed_id, file_get_contents($tmp_file))) {
|
||||||
if (rename($tmp_file, $new_filename)) {
|
|
||||||
chmod($new_filename, 0644);
|
|
||||||
|
|
||||||
$feed->set([
|
$feed->set([
|
||||||
'favicon_avg_color' => null,
|
'favicon_avg_color' => null,
|
||||||
'favicon_is_custom' => true,
|
'favicon_is_custom' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($feed->save()) {
|
if ($feed->save()) {
|
||||||
$rc = self::E_ICON_UPLOAD_SUCCESS;
|
$rc = self::E_ICON_UPLOAD_SUCCESS;
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$rc = self::E_ICON_RENAME_FAILED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$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;
|
||||||
}
|
}
|
||||||
|
@ -1186,9 +1188,10 @@ class Pref_Feeds extends Handler_Protected {
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
if (file_exists(Config::get(Config::ICONS_DIR) . "/$id.ico")) {
|
$favicon_cache = new DiskCache('feed-icons');
|
||||||
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);
|
||||||
|
|
|
@ -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']]);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -82,11 +82,10 @@ class Pref_Users extends Handler_Administrative {
|
||||||
<?php while ($row = $sth->fetch()) { ?>
|
<?php while ($row = $sth->fetch()) { ?>
|
||||||
<li>
|
<li>
|
||||||
<?php
|
<?php
|
||||||
$icon_file = Config::get(Config::ICONS_URL) . "/" . $row["id"] . ".ico";
|
$icon_url = Feeds::_get_icon_url($row['id'], 'images/blank_icon.gif');
|
||||||
$icon = file_exists($icon_file) ? $icon_file : "images/blank_icon.gif";
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<img class="icon" src="<?= $icon_file ?>">
|
<img class="icon" src="<?= htmlspecialchars($icon_url) ?>">
|
||||||
|
|
||||||
<a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>">
|
<a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>">
|
||||||
<?= htmlspecialchars($row["title"]) ?>
|
<?= htmlspecialchars($row["title"]) ?>
|
||||||
|
|
|
@ -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::ICONS_URL);
|
$params["icons_url"] = Config::get(Config::SELF_URL_PATH) . '/public.php';
|
||||||
$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);
|
||||||
|
|
|
@ -37,20 +37,29 @@ 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 = ?");
|
||||||
|
|
||||||
// check icon files once every Config::get(Config::CACHE_MAX_DAYS) days
|
$cache = new DiskCache('feed-icons');
|
||||||
$icon_files = array_filter(glob(Config::get(Config::ICONS_DIR) . "/*.ico"),
|
|
||||||
fn(string $f) => filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS));
|
|
||||||
|
|
||||||
foreach ($icon_files as $icon) {
|
if ($cache->is_writable()) {
|
||||||
$feed_id = basename($icon, ".ico");
|
$dh = opendir($cache->get_full_path(""));
|
||||||
|
|
||||||
$sth->execute([$feed_id]);
|
if ($dh) {
|
||||||
|
while (($icon = readdir($dh)) !== false) {
|
||||||
|
if ($cache->get_mtime($icon) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS)) {
|
||||||
|
|
||||||
if ($sth->fetch()) {
|
$sth->execute([(int)$icon]);
|
||||||
@touch($icon);
|
|
||||||
} else {
|
if ($sth->fetch()) {
|
||||||
Debug::log("Removing orphaned feed icon: $icon");
|
$cache->put($icon, $cache->get($icon));
|
||||||
unlink($icon);
|
} else {
|
||||||
|
$icon_path = $cache->get_full_path($icon);
|
||||||
|
|
||||||
|
Debug::log("Removing orphaned feed icon: $icon_path");
|
||||||
|
unlink($icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir($dh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,6 +347,9 @@ 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 {
|
||||||
|
@ -387,7 +399,7 @@ class RSSUtils {
|
||||||
|
|
||||||
$date_feed_processed = date('Y-m-d H:i');
|
$date_feed_processed = date('Y-m-d H:i');
|
||||||
|
|
||||||
$cache_filename = Config::get(Config::CACHE_DIR) . "/feeds/" . sha1($feed_obj->feed_url) . ".xml";
|
$cache_filename = 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);
|
||||||
|
@ -423,13 +435,13 @@ class RSSUtils {
|
||||||
|
|
||||||
// try cache
|
// try cache
|
||||||
if (!$feed_data &&
|
if (!$feed_data &&
|
||||||
is_readable($cache_filename) &&
|
$cache->exists($cache_filename) &&
|
||||||
!$feed_obj->auth_login && !$feed_obj->auth_pass &&
|
!$feed_obj->auth_login && !$feed_obj->auth_pass &&
|
||||||
filemtime($cache_filename) > time() - 30) {
|
$cache->get_mtime($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 = file_get_contents($cache_filename);
|
$feed_data = $cache->get($cache_filename);
|
||||||
|
|
||||||
if ($feed_data) {
|
if ($feed_data) {
|
||||||
$rss_hash = sha1($feed_data);
|
$rss_hash = sha1($feed_data);
|
||||||
|
@ -477,12 +489,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 && is_writable(Config::get(Config::CACHE_DIR) . "/feeds")) {
|
if ($feed_data && !$feed_obj->auth_pass && !$feed_obj->auth_login && $cache->is_writable()) {
|
||||||
$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);
|
||||||
file_put_contents($cache_filename, $feed_data);
|
$cache->put($cache_filename, $feed_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -590,21 +602,28 @@ class RSSUtils {
|
||||||
|
|
||||||
if ($feed_obj->favicon_needs_check || $force_refetch) {
|
if ($feed_obj->favicon_needs_check || $force_refetch) {
|
||||||
|
|
||||||
/* terrible hack: if we crash on floicon shit here, we won't check
|
// restrict update attempts to once per 12h
|
||||||
* the icon avgcolor again (unless the icon got updated) */
|
$feed_obj->favicon_last_checked = Db::NOW();
|
||||||
|
$feed_obj->save();
|
||||||
|
|
||||||
$favicon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
|
$favicon_cache = new DiskCache('feed-icons');
|
||||||
$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 ((file_exists($favicon_file) ? filemtime($favicon_file) : -1) > $favicon_modified)
|
if (!$favicon_cache->exists($feed) || $favicon_cache->get_mtime($feed) > $favicon_modified) {
|
||||||
$feed_obj->favicon_avg_color = null;
|
$feed_obj->favicon_avg_color = null;
|
||||||
|
$feed_obj->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_readable($favicon_file) && function_exists("imagecreatefromstring") && empty($feed_obj->favicon_avg_color)) {
|
/* terrible hack: if we crash on floicon shit here, we won't check
|
||||||
|
* 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);
|
||||||
|
@ -612,13 +631,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_file);
|
$feed_obj->favicon_avg_color = \Colors\calculate_avg_color($favicon_cache->get_full_path($feed));
|
||||||
$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 $favicon_file, not trying to recalculate avg color", Debug::LOG_VERBOSE);
|
Debug::log("floicon failed on $feed, not trying to recalculate avg color", Debug::LOG_VERBOSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1323,8 +1342,6 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1350,8 +1367,6 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1673,9 +1688,36 @@ class RSSUtils {
|
||||||
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
|
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
|
||||||
}
|
}
|
||||||
|
|
||||||
static function housekeeping_common(): void {
|
/** migrates favicons from legacy storage in feed-icons/ to cache/feed-icons/using new naming (sans .ico suffix) */
|
||||||
DiskCache::expire();
|
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 {
|
||||||
|
$cache = new DiskCache("");
|
||||||
|
$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();
|
||||||
|
@ -1693,8 +1735,6 @@ 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) {
|
||||||
|
@ -1749,21 +1789,18 @@ class RSSUtils {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug::log("favicon: $favicon_url looks valid, saving to $icon_file", Debug::LOG_VERBOSE);
|
$favicon_cache = new DiskCache('feed-icons');
|
||||||
|
|
||||||
$fp = @fopen($icon_file, "w");
|
if ($favicon_cache->is_writable()) {
|
||||||
|
Debug::log("favicon: $favicon_url looks valid, saving to cache", Debug::LOG_VERBOSE);
|
||||||
|
|
||||||
if ($fp) {
|
// we deal with this manually
|
||||||
|
if (!$favicon_cache->exists(".no-auto-expiry"))
|
||||||
fwrite($fp, $contents);
|
$favicon_cache->put(".no-auto-expiry", "");
|
||||||
fclose($fp);
|
|
||||||
chmod($icon_file, 0644);
|
|
||||||
clearstatcache();
|
|
||||||
|
|
||||||
return $icon_file;
|
|
||||||
|
|
||||||
|
return $favicon_cache->put((string)$feed, $contents);
|
||||||
} else {
|
} else {
|
||||||
Debug::log("favicon: failed to open $icon_file for writing", Debug::LOG_VERBOSE);
|
Debug::log("favicon: $favicon_url skipping, local cache is not writable", Debug::LOG_VERBOSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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") + "/" + id + ".ico?" + has_img);
|
App.getInitParam("icons_url") + '?' + dojo.objectToQuery({op: 'feed_icon', id: id}));
|
||||||
} else {
|
} else {
|
||||||
this.setIcon(id, false, 'images/blank_icon.gif');
|
this.setIcon(id, false, 'images/blank_icon.gif');
|
||||||
}
|
}
|
||||||
|
@ -678,8 +678,10 @@ 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(App.getInitParam("icons_url"))}/${feed_id}.ico">` :
|
`<img class="icon" src="${App.escapeHtml(icon_url)}">` :
|
||||||
`<i class='icon-no-feed material-icons'>rss_feed</i>`;
|
`<i class='icon-no-feed material-icons'>rss_feed</i>`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
@ -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-06-09 13:15+0000\n"
|
"PO-Revision-Date: 2022-11-23 11:05+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.12.2\n"
|
"X-Generator: Weblate 4.14.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 "Desconectar"
|
msgstr "Pechar Sesión"
|
||||||
|
|
||||||
#: 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 "Conectar"
|
msgstr "Acceder"
|
||||||
|
|
||||||
#: 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 conta (conta está desactivada)"
|
msgstr "Fallou a validación da sesión (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 desconectar e volver a conectarte "
|
"complementos están desactivados. Terás que pechar sesión e volver a acceder "
|
||||||
"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 "Identificador:"
|
msgstr "Acceso"
|
||||||
|
|
||||||
#: js/CommonDialogs.js:101
|
#: js/CommonDialogs.js:101
|
||||||
msgid "This feed requires authentication."
|
msgid "This feed requires authentication."
|
||||||
|
|
Binary file not shown.
|
@ -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: 2021-11-21 17:41+0000\n"
|
"PO-Revision-Date: 2022-08-25 06:49+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/tt-"
|
"Language-Team: Chinese (Traditional) <https://weblate.tt-rss.org/projects/"
|
||||||
"rss/messages/zh_Hant/>\n"
|
"tt-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.9.1\n"
|
"X-Generator: Weblate 4.14\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,16 +1976,12 @@ msgid "Update daemon is not updating feeds."
|
||||||
msgstr "更新守護行程沒有更新摘要。"
|
msgstr "更新守護行程沒有更新摘要。"
|
||||||
|
|
||||||
#: js/App.js:606
|
#: js/App.js:606
|
||||||
#, fuzzy, java-printf-format
|
#, 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 ""
|
msgstr "瀏覽器報告的 URL 配置(%a)與伺服器配置的 SELF_URL_PATH(%b)不符合,請檢查 X"
|
||||||
"瀏覽器報告的URL配置(%a)與伺服器配置的 SELF_URL_PATH(%b)不符合,請檢查 "
|
"-Forwarded-Proto。"
|
||||||
"X-Forwarded-Proto。"
|
|
||||||
|
|
||||||
#: js/App.js:613
|
#: js/App.js:613
|
||||||
msgid "Fatal error"
|
msgid "Fatal error"
|
||||||
|
@ -2624,10 +2620,9 @@ msgid "Reset to defaults?"
|
||||||
msgstr "重設為預設狀態?"
|
msgstr "重設為預設狀態?"
|
||||||
|
|
||||||
#: js/PrefHelpers.js:373
|
#: js/PrefHelpers.js:373
|
||||||
#, fuzzy, java-printf-format, javascript-format
|
#, 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 "解析文檔時發生錯誤。"
|
msgstr "載入擴充套件列表時發生錯誤:%s。"
|
||||||
|
|
||||||
#: js/PrefHelpers.js:422
|
#: js/PrefHelpers.js:422
|
||||||
msgid "Clear data"
|
msgid "Clear data"
|
||||||
|
@ -2866,7 +2861,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"
|
||||||
|
@ -3024,16 +3019,12 @@ 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"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
parameters:
|
parameters:
|
||||||
level: 6
|
level: 6
|
||||||
parallel:
|
parallel:
|
||||||
maximumNumberOfProcesses: 2
|
maximumNumberOfProcesses: 4
|
||||||
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,5 +24,6 @@ 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:
|
||||||
- .
|
- .
|
||||||
|
|
|
@ -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->touch(".no-auto-expiry");
|
$this->cache->put(".no-auto-expiry", "");
|
||||||
|
|
||||||
if (!$this->cache_status->exists(".no-auto-expiry"))
|
if (!$this->cache_status->exists(".no-auto-expiry"))
|
||||||
$this->cache_status->touch(".no-auto-expiry");
|
$this->cache_status->put(".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);
|
||||||
|
|
Loading…
Reference in New Issue