mirror of
https://git.tt-rss.org/git/tt-rss.git
synced 2025-12-20 18:21:29 +00:00
Merge branch 'master' of git.fakecake.org:fox/tt-rss into weblate-integration
This commit is contained in:
@@ -2,12 +2,12 @@ module.exports = {
|
|||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"jquery": true,
|
"jquery": false,
|
||||||
"webextensions": true
|
"webextensions": false
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2017
|
"ecmaVersion": 2018
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"accessor-pairs": "error",
|
"accessor-pairs": "error",
|
||||||
|
|||||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,25 +1,13 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
/.app_is_ready
|
/.app_is_ready
|
||||||
/deploy.exclude
|
|
||||||
/deploy.sh
|
|
||||||
/messages.mo
|
/messages.mo
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/locale/**/*.po~
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
*~
|
/plugins.local/*
|
||||||
*.DS_Store
|
/themes.local/*
|
||||||
#*
|
/config.php
|
||||||
.idea/*
|
/feed-icons/*
|
||||||
plugins.local/*
|
/cache/*/*
|
||||||
themes.local/*
|
/lock/*
|
||||||
config.php
|
|
||||||
feed-icons/*
|
|
||||||
cache/*/*
|
|
||||||
lock/*
|
|
||||||
tags
|
|
||||||
cache/htmlpurifier/*/*ser
|
|
||||||
lib/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/*/*ser
|
|
||||||
web.config
|
|
||||||
/.save.cson
|
|
||||||
/.tags*
|
|
||||||
/.gutentags
|
|
||||||
/.vscode/settings.json
|
/.vscode/settings.json
|
||||||
|
|||||||
23
.vscode/launch.json
vendored
Normal file
23
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "chrome",
|
||||||
|
"pathMapping": {
|
||||||
|
"/tt-rss/": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"urlFilter": "*/tt-rss/*",
|
||||||
|
"runtimeExecutable": "chrome.exe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen for XDebug",
|
||||||
|
"type": "php",
|
||||||
|
"request": "launch",
|
||||||
|
"pathMappings": {
|
||||||
|
"/var/www/html/tt-rss": "${workspaceRoot}",
|
||||||
|
},
|
||||||
|
"port": 9000
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -1,55 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
error_reporting(E_ERROR | E_PARSE);
|
error_reporting(E_ERROR | E_PARSE);
|
||||||
|
|
||||||
require_once "../config.php";
|
set_include_path(__DIR__ . PATH_SEPARATOR .
|
||||||
|
dirname(__DIR__) . PATH_SEPARATOR .
|
||||||
set_include_path(dirname(__FILE__) . PATH_SEPARATOR .
|
dirname(__DIR__) . "/include" . PATH_SEPARATOR .
|
||||||
dirname(dirname(__FILE__)) . PATH_SEPARATOR .
|
|
||||||
dirname(dirname(__FILE__)) . "/include" . PATH_SEPARATOR .
|
|
||||||
get_include_path());
|
get_include_path());
|
||||||
|
|
||||||
chdir("..");
|
chdir("..");
|
||||||
|
|
||||||
define('TTRSS_SESSION_NAME', 'ttrss_api_sid');
|
|
||||||
define('NO_SESSION_AUTOSTART', true);
|
define('NO_SESSION_AUTOSTART', true);
|
||||||
|
|
||||||
require_once "autoload.php";
|
require_once "autoload.php";
|
||||||
require_once "db.php";
|
|
||||||
require_once "db-prefs.php";
|
|
||||||
require_once "functions.php";
|
require_once "functions.php";
|
||||||
require_once "sessions.php";
|
require_once "sessions.php";
|
||||||
|
|
||||||
ini_set('session.use_cookies', 0);
|
ini_set('session.use_cookies', "0");
|
||||||
ini_set("session.gc_maxlifetime", 86400);
|
ini_set("session.gc_maxlifetime", "86400");
|
||||||
|
|
||||||
if (defined('ENABLE_GZIP_OUTPUT') && ENABLE_GZIP_OUTPUT &&
|
|
||||||
function_exists("ob_gzhandler")) {
|
|
||||||
|
|
||||||
ob_start("ob_gzhandler");
|
|
||||||
} else {
|
|
||||||
ob_start();
|
ob_start();
|
||||||
}
|
|
||||||
|
|
||||||
$input = file_get_contents("php://input");
|
$_REQUEST = json_decode((string)file_get_contents("php://input"), true);
|
||||||
|
|
||||||
if (defined('_API_DEBUG_HTTP_ENABLED') && _API_DEBUG_HTTP_ENABLED) {
|
|
||||||
// Override $_REQUEST with JSON-encoded data if available
|
|
||||||
// fallback on HTTP parameters
|
|
||||||
if ($input) {
|
|
||||||
$input = json_decode($input, true);
|
|
||||||
if ($input) $_REQUEST = $input;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Accept JSON only
|
|
||||||
$input = json_decode((string)$input, true);
|
|
||||||
$_REQUEST = $input;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($_REQUEST["sid"])) {
|
if (!empty($_REQUEST["sid"])) {
|
||||||
session_id($_REQUEST["sid"]);
|
session_id($_REQUEST["sid"]);
|
||||||
@session_start();
|
@session_start();
|
||||||
} else if (defined('_API_DEBUG_HTTP_ENABLED')) {
|
|
||||||
@session_start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startup_gettext();
|
startup_gettext();
|
||||||
@@ -57,12 +31,14 @@
|
|||||||
if (!init_plugins()) return;
|
if (!init_plugins()) return;
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"])) {
|
if (!empty($_SESSION["uid"])) {
|
||||||
if (!validate_session()) {
|
if (!\Sessions\validate_session()) {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
|
|
||||||
print json_encode(array("seq" => -1,
|
print json_encode([
|
||||||
"status" => 1,
|
"seq" => -1,
|
||||||
"content" => array("error" => "NOT_LOGGED_IN")));
|
"status" => API::STATUS_ERR,
|
||||||
|
"content" => [ "error" => API::E_NOT_LOGGED_IN ]
|
||||||
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
48
backend.php
48
backend.php
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
set_include_path(dirname(__FILE__) ."/include" . PATH_SEPARATOR .
|
set_include_path(__DIR__ ."/include" . PATH_SEPARATOR .
|
||||||
get_include_path());
|
get_include_path());
|
||||||
|
|
||||||
$op = $_REQUEST["op"];
|
$op = $_REQUEST["op"];
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
require_once "autoload.php";
|
require_once "autoload.php";
|
||||||
require_once "sessions.php";
|
require_once "sessions.php";
|
||||||
require_once "functions.php";
|
require_once "functions.php";
|
||||||
require_once "config.php";
|
|
||||||
require_once "db.php";
|
$op = (string)clean($op);
|
||||||
require_once "db-prefs.php";
|
$method = (string)clean($method);
|
||||||
|
|
||||||
startup_gettext();
|
startup_gettext();
|
||||||
|
|
||||||
@@ -38,18 +38,14 @@
|
|||||||
|
|
||||||
header("Content-Type: text/json; charset=utf-8");
|
header("Content-Type: text/json; charset=utf-8");
|
||||||
|
|
||||||
if (ENABLE_GZIP_OUTPUT && function_exists("ob_gzhandler")) {
|
if (Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
ob_start("ob_gzhandler");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SINGLE_USER_MODE) {
|
|
||||||
UserHelper::authenticate( "admin", null);
|
UserHelper::authenticate( "admin", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SESSION["uid"]) {
|
if (!empty($_SESSION["uid"])) {
|
||||||
if (!validate_session()) {
|
if (!\Sessions\validate_session()) {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(6);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UserHelper::load_user_plugins($_SESSION["uid"]);
|
UserHelper::load_user_plugins($_SESSION["uid"]);
|
||||||
@@ -90,12 +86,30 @@
|
|||||||
5 => __("Power User"),
|
5 => __("Power User"),
|
||||||
10 => __("Administrator"));
|
10 => __("Administrator"));
|
||||||
|
|
||||||
|
// shortcut syntax for plugin methods (?op=plugin--pmethod&...params)
|
||||||
|
/* if (strpos($op, PluginHost::PUBLIC_METHOD_DELIMITER) !== false) {
|
||||||
|
list ($plugin, $pmethod) = explode(PluginHost::PUBLIC_METHOD_DELIMITER, $op, 2);
|
||||||
|
|
||||||
|
// TODO: better implementation that won't modify $_REQUEST
|
||||||
|
$_REQUEST["plugin"] = $plugin;
|
||||||
|
$method = $pmethod;
|
||||||
|
$op = "pluginhandler";
|
||||||
|
} */
|
||||||
|
|
||||||
|
// TODO: figure out if is this still needed
|
||||||
$op = str_replace("-", "_", $op);
|
$op = str_replace("-", "_", $op);
|
||||||
|
|
||||||
$override = PluginHost::getInstance()->lookup_handler($op, $method);
|
$override = PluginHost::getInstance()->lookup_handler($op, $method);
|
||||||
|
|
||||||
if (class_exists($op) || $override) {
|
if (class_exists($op) || $override) {
|
||||||
|
|
||||||
|
if (strpos($method, "_") === 0) {
|
||||||
|
user_error("Refusing to invoke method $method of handler $op which starts with underscore.", E_USER_WARNING);
|
||||||
|
header("Content-Type: text/json");
|
||||||
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($override) {
|
if ($override) {
|
||||||
$handler = $override;
|
$handler = $override;
|
||||||
} else {
|
} else {
|
||||||
@@ -114,8 +128,9 @@
|
|||||||
if ($reflection->getNumberOfRequiredParameters() == 0) {
|
if ($reflection->getNumberOfRequiredParameters() == 0) {
|
||||||
$handler->$method();
|
$handler->$method();
|
||||||
} else {
|
} else {
|
||||||
|
user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(6);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (method_exists($handler, "catchall")) {
|
if (method_exists($handler, "catchall")) {
|
||||||
@@ -126,18 +141,19 @@
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(6);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
user_error("Refusing to invoke method $method of handler $op with invalid CSRF token.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(6);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(13);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
238
classes/api.php
238
classes/api.php
@@ -6,23 +6,38 @@ class API extends Handler {
|
|||||||
const STATUS_OK = 0;
|
const STATUS_OK = 0;
|
||||||
const STATUS_ERR = 1;
|
const STATUS_ERR = 1;
|
||||||
|
|
||||||
|
const E_API_DISABLED = "API_DISABLED";
|
||||||
|
const E_NOT_LOGGED_IN = "NOT_LOGGED_IN";
|
||||||
|
const E_LOGIN_ERROR = "LOGIN_ERROR";
|
||||||
|
const E_INCORRECT_USAGE = "INCORRECT_USAGE";
|
||||||
|
const E_UNKNOWN_METHOD = "UNKNOWN_METHOD";
|
||||||
|
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
|
||||||
|
|
||||||
private $seq;
|
private $seq;
|
||||||
|
|
||||||
static function param_to_bool($p) {
|
private static function _param_to_bool($p) {
|
||||||
return $p && ($p !== "f" && $p !== "false");
|
return $p && ($p !== "f" && $p !== "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function _wrap($status, $reply) {
|
||||||
|
print json_encode([
|
||||||
|
"seq" => $this->seq,
|
||||||
|
"status" => $status,
|
||||||
|
"content" => $reply
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
function before($method) {
|
function before($method) {
|
||||||
if (parent::before($method)) {
|
if (parent::before($method)) {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
|
|
||||||
if (empty($_SESSION["uid"]) && $method != "login" && $method != "isloggedin") {
|
if (empty($_SESSION["uid"]) && $method != "login" && $method != "isloggedin") {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'NOT_LOGGED_IN'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_NOT_LOGGED_IN));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"]) && $method != "logout" && !get_pref('ENABLE_API_ACCESS')) {
|
if (!empty($_SESSION["uid"]) && $method != "logout" && !get_pref('ENABLE_API_ACCESS')) {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'API_DISABLED'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +48,14 @@ class API extends Handler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrap($status, $reply) {
|
|
||||||
print json_encode(array("seq" => $this->seq,
|
|
||||||
"status" => $status,
|
|
||||||
"content" => $reply));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersion() {
|
function getVersion() {
|
||||||
$rv = array("version" => get_version());
|
$rv = array("version" => get_version());
|
||||||
$this->wrap(self::STATUS_OK, $rv);
|
$this->_wrap(self::STATUS_OK, $rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApiLevel() {
|
function getApiLevel() {
|
||||||
$rv = array("level" => self::API_LEVEL);
|
$rv = array("level" => self::API_LEVEL);
|
||||||
$this->wrap(self::STATUS_OK, $rv);
|
$this->_wrap(self::STATUS_OK, $rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
function login() {
|
function login() {
|
||||||
@@ -57,36 +66,36 @@ class API extends Handler {
|
|||||||
$password = clean($_REQUEST["password"]);
|
$password = clean($_REQUEST["password"]);
|
||||||
$password_base64 = base64_decode(clean($_REQUEST["password"]));
|
$password_base64 = base64_decode(clean($_REQUEST["password"]));
|
||||||
|
|
||||||
if (SINGLE_USER_MODE) $login = "admin";
|
if (Config::get(Config::SINGLE_USER_MODE)) $login = "admin";
|
||||||
|
|
||||||
if ($uid = UserHelper::find_user_by_login($login)) {
|
if ($uid = UserHelper::find_user_by_login($login)) {
|
||||||
if (get_pref("ENABLE_API_ACCESS", $uid)) {
|
if (get_pref("ENABLE_API_ACCESS", $uid)) {
|
||||||
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password
|
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password
|
||||||
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
|
$this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
|
||||||
"api_level" => self::API_LEVEL));
|
"api_level" => self::API_LEVEL));
|
||||||
} else if (UserHelper::authenticate($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password
|
} else if (UserHelper::authenticate($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password
|
||||||
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
|
$this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
|
||||||
"api_level" => self::API_LEVEL));
|
"api_level" => self::API_LEVEL));
|
||||||
} else { // else we are not logged in
|
} else { // else we are not logged in
|
||||||
user_error("Failed login attempt for $login from " . UserHelper::get_user_ip(), E_USER_WARNING);
|
user_error("Failed login attempt for $login from " . UserHelper::get_user_ip(), E_USER_WARNING);
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR"));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => "API_DISABLED"));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR"));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
Pref_Users::logout_user();
|
UserHelper::logout();
|
||||||
$this->wrap(self::STATUS_OK, array("status" => "OK"));
|
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLoggedIn() {
|
function isLoggedIn() {
|
||||||
$this->wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
|
$this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUnread() {
|
function getUnread() {
|
||||||
@@ -94,33 +103,33 @@ class API extends Handler {
|
|||||||
$is_cat = clean($_REQUEST["is_cat"]);
|
$is_cat = clean($_REQUEST["is_cat"]);
|
||||||
|
|
||||||
if ($feed_id) {
|
if ($feed_id) {
|
||||||
$this->wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat)));
|
$this->_wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat)));
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_OK, array("unread" => Feeds::getGlobalUnread()));
|
$this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_global_unread()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Method added for ttrss-reader for Android */
|
/* Method added for ttrss-reader for Android */
|
||||||
function getCounters() {
|
function getCounters() {
|
||||||
$this->wrap(self::STATUS_OK, Counters::getAllCounters());
|
$this->_wrap(self::STATUS_OK, Counters::get_all());
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeeds() {
|
function getFeeds() {
|
||||||
$cat_id = clean($_REQUEST["cat_id"]);
|
$cat_id = clean($_REQUEST["cat_id"]);
|
||||||
$unread_only = self::param_to_bool(clean($_REQUEST["unread_only"] ?? 0));
|
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? 0));
|
||||||
$limit = (int) clean($_REQUEST["limit"] ?? 0);
|
$limit = (int) clean($_REQUEST["limit"] ?? 0);
|
||||||
$offset = (int) clean($_REQUEST["offset"] ?? 0);
|
$offset = (int) clean($_REQUEST["offset"] ?? 0);
|
||||||
$include_nested = self::param_to_bool(clean($_REQUEST["include_nested"] ?? false));
|
$include_nested = self::_param_to_bool(clean($_REQUEST["include_nested"] ?? false));
|
||||||
|
|
||||||
$feeds = $this->api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
|
$feeds = $this->_api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, $feeds);
|
$this->_wrap(self::STATUS_OK, $feeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategories() {
|
function getCategories() {
|
||||||
$unread_only = self::param_to_bool(clean($_REQUEST["unread_only"] ?? false));
|
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? false));
|
||||||
$enable_nested = self::param_to_bool(clean($_REQUEST["enable_nested"] ?? false));
|
$enable_nested = self::_param_to_bool(clean($_REQUEST["enable_nested"] ?? false));
|
||||||
$include_empty = self::param_to_bool(clean($_REQUEST['include_empty'] ?? false));
|
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty'] ?? false));
|
||||||
|
|
||||||
// TODO do not return empty categories, return Uncategorized and standard virtual cats
|
// TODO do not return empty categories, return Uncategorized and standard virtual cats
|
||||||
|
|
||||||
@@ -147,7 +156,7 @@ class API extends Handler {
|
|||||||
$unread = getFeedUnread($line["id"], true);
|
$unread = getFeedUnread($line["id"], true);
|
||||||
|
|
||||||
if ($enable_nested)
|
if ($enable_nested)
|
||||||
$unread += Feeds::getCategoryChildrenUnread($line["id"]);
|
$unread += Feeds::_get_cat_children_unread($line["id"]);
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
array_push($cats, array("id" => (int) $line["id"],
|
array_push($cats, array("id" => (int) $line["id"],
|
||||||
@@ -160,18 +169,18 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (array(-2,-1,0) as $cat_id) {
|
foreach (array(-2,-1,0) as $cat_id) {
|
||||||
if ($include_empty || !$this->isCategoryEmpty($cat_id)) {
|
if ($include_empty || !$this->_is_cat_empty($cat_id)) {
|
||||||
$unread = getFeedUnread($cat_id, true);
|
$unread = getFeedUnread($cat_id, true);
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
array_push($cats, array("id" => $cat_id,
|
array_push($cats, array("id" => $cat_id,
|
||||||
"title" => Feeds::getCategoryTitle($cat_id),
|
"title" => Feeds::_get_cat_title($cat_id),
|
||||||
"unread" => (int) $unread));
|
"unread" => (int) $unread));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, $cats);
|
$this->_wrap(self::STATUS_OK, $cats);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeadlines() {
|
function getHeadlines() {
|
||||||
@@ -186,42 +195,42 @@ class API extends Handler {
|
|||||||
|
|
||||||
$offset = (int)clean($_REQUEST["skip"]);
|
$offset = (int)clean($_REQUEST["skip"]);
|
||||||
$filter = clean($_REQUEST["filter"] ?? "");
|
$filter = clean($_REQUEST["filter"] ?? "");
|
||||||
$is_cat = self::param_to_bool(clean($_REQUEST["is_cat"] ?? false));
|
$is_cat = self::_param_to_bool(clean($_REQUEST["is_cat"] ?? false));
|
||||||
$show_excerpt = self::param_to_bool(clean($_REQUEST["show_excerpt"] ?? false));
|
$show_excerpt = self::_param_to_bool(clean($_REQUEST["show_excerpt"] ?? false));
|
||||||
$show_content = self::param_to_bool(clean($_REQUEST["show_content"]));
|
$show_content = self::_param_to_bool(clean($_REQUEST["show_content"]));
|
||||||
/* all_articles, unread, adaptive, marked, updated */
|
/* all_articles, unread, adaptive, marked, updated */
|
||||||
$view_mode = clean($_REQUEST["view_mode"] ?? null);
|
$view_mode = clean($_REQUEST["view_mode"] ?? null);
|
||||||
$include_attachments = self::param_to_bool(clean($_REQUEST["include_attachments"] ?? false));
|
$include_attachments = self::_param_to_bool(clean($_REQUEST["include_attachments"] ?? false));
|
||||||
$since_id = (int)clean($_REQUEST["since_id"] ?? 0);
|
$since_id = (int)clean($_REQUEST["since_id"] ?? 0);
|
||||||
$include_nested = self::param_to_bool(clean($_REQUEST["include_nested"] ?? false));
|
$include_nested = self::_param_to_bool(clean($_REQUEST["include_nested"] ?? false));
|
||||||
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
|
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
|
||||||
self::param_to_bool($_REQUEST["sanitize"]);
|
self::_param_to_bool($_REQUEST["sanitize"]);
|
||||||
$force_update = self::param_to_bool(clean($_REQUEST["force_update"] ?? false));
|
$force_update = self::_param_to_bool(clean($_REQUEST["force_update"] ?? false));
|
||||||
$has_sandbox = self::param_to_bool(clean($_REQUEST["has_sandbox"] ?? false));
|
$has_sandbox = self::_param_to_bool(clean($_REQUEST["has_sandbox"] ?? false));
|
||||||
$excerpt_length = (int)clean($_REQUEST["excerpt_length"] ?? 0);
|
$excerpt_length = (int)clean($_REQUEST["excerpt_length"] ?? 0);
|
||||||
$check_first_id = (int)clean($_REQUEST["check_first_id"] ?? 0);
|
$check_first_id = (int)clean($_REQUEST["check_first_id"] ?? 0);
|
||||||
$include_header = self::param_to_bool(clean($_REQUEST["include_header"] ?? false));
|
$include_header = self::_param_to_bool(clean($_REQUEST["include_header"] ?? false));
|
||||||
|
|
||||||
$_SESSION['hasSandbox'] = $has_sandbox;
|
$_SESSION['hasSandbox'] = $has_sandbox;
|
||||||
|
|
||||||
list($override_order, $skip_first_id_check) = Feeds::order_to_override_query(clean($_REQUEST["order_by"] ?? null));
|
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query(clean($_REQUEST["order_by"] ?? null));
|
||||||
|
|
||||||
/* do not rely on params below */
|
/* do not rely on params below */
|
||||||
|
|
||||||
$search = clean($_REQUEST["search"] ?? "");
|
$search = clean($_REQUEST["search"] ?? "");
|
||||||
|
|
||||||
list($headlines, $headlines_header) = $this->api_get_headlines($feed_id, $limit, $offset,
|
list($headlines, $headlines_header) = $this->_api_get_headlines($feed_id, $limit, $offset,
|
||||||
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order,
|
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order,
|
||||||
$include_attachments, $since_id, $search,
|
$include_attachments, $since_id, $search,
|
||||||
$include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check);
|
$include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check);
|
||||||
|
|
||||||
if ($include_header) {
|
if ($include_header) {
|
||||||
$this->wrap(self::STATUS_OK, array($headlines_header, $headlines));
|
$this->_wrap(self::STATUS_OK, array($headlines_header, $headlines));
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_OK, $headlines);
|
$this->_wrap(self::STATUS_OK, $headlines);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,11 +286,11 @@ class API extends Handler {
|
|||||||
|
|
||||||
$num_updated = $sth->rowCount();
|
$num_updated = $sth->rowCount();
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, array("status" => "OK",
|
$this->_wrap(self::STATUS_OK, array("status" => "OK",
|
||||||
"updated" => $num_updated));
|
"updated" => $num_updated));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -290,9 +299,9 @@ class API extends Handler {
|
|||||||
|
|
||||||
$article_ids = explode(",", clean($_REQUEST["article_id"]));
|
$article_ids = explode(",", clean($_REQUEST["article_id"]));
|
||||||
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
|
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
|
||||||
self::param_to_bool($_REQUEST["sanitize"]);
|
self::_param_to_bool($_REQUEST["sanitize"]);
|
||||||
|
|
||||||
if ($article_ids) {
|
if (count($article_ids) > 0) {
|
||||||
|
|
||||||
$article_qmarks = arr_qmarks($article_ids);
|
$article_qmarks = arr_qmarks($article_ids);
|
||||||
|
|
||||||
@@ -311,22 +320,20 @@ class API extends Handler {
|
|||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
$attachments = Article::get_article_enclosures($line['id']);
|
|
||||||
|
|
||||||
$article = array(
|
$article = array(
|
||||||
"id" => $line["id"],
|
"id" => $line["id"],
|
||||||
"guid" => $line["guid"],
|
"guid" => $line["guid"],
|
||||||
"title" => $line["title"],
|
"title" => $line["title"],
|
||||||
"link" => $line["link"],
|
"link" => $line["link"],
|
||||||
"labels" => Article::get_article_labels($line['id']),
|
"labels" => Article::_get_labels($line['id']),
|
||||||
"unread" => self::param_to_bool($line["unread"]),
|
"unread" => self::_param_to_bool($line["unread"]),
|
||||||
"marked" => self::param_to_bool($line["marked"]),
|
"marked" => self::_param_to_bool($line["marked"]),
|
||||||
"published" => self::param_to_bool($line["published"]),
|
"published" => self::_param_to_bool($line["published"]),
|
||||||
"comments" => $line["comments"],
|
"comments" => $line["comments"],
|
||||||
"author" => $line["author"],
|
"author" => $line["author"],
|
||||||
"updated" => (int) strtotime($line["updated"]),
|
"updated" => (int) strtotime($line["updated"]),
|
||||||
"feed_id" => $line["feed_id"],
|
"feed_id" => $line["feed_id"],
|
||||||
"attachments" => $attachments,
|
"attachments" => Article::_get_enclosures($line['id']),
|
||||||
"score" => (int)$line["score"],
|
"score" => (int)$line["score"],
|
||||||
"feed_title" => $line["feed_title"],
|
"feed_title" => $line["feed_title"],
|
||||||
"note" => $line["note"],
|
"note" => $line["note"],
|
||||||
@@ -336,7 +343,7 @@ class API extends Handler {
|
|||||||
if ($sanitize_content) {
|
if ($sanitize_content) {
|
||||||
$article["content"] = Sanitizer::sanitize(
|
$article["content"] = Sanitizer::sanitize(
|
||||||
$line["content"],
|
$line["content"],
|
||||||
self::param_to_bool($line['hide_images']),
|
self::_param_to_bool($line['hide_images']),
|
||||||
false, $line["site_url"], false, $line["id"]);
|
false, $line["site_url"], false, $line["id"]);
|
||||||
} else {
|
} else {
|
||||||
$article["content"] = $line["content"];
|
$article["content"] = $line["content"];
|
||||||
@@ -350,22 +357,23 @@ class API extends Handler {
|
|||||||
},
|
},
|
||||||
$hook_object);
|
$hook_object);
|
||||||
|
|
||||||
$article['content'] = DiskCache::rewriteUrls($article['content']);
|
$article['content'] = DiskCache::rewrite_urls($article['content']);
|
||||||
|
|
||||||
array_push($articles, $article);
|
array_push($articles, $article);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, $articles);
|
$this->_wrap(self::STATUS_OK, $articles);
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
$config = array(
|
$config = [
|
||||||
"icons_dir" => ICONS_DIR,
|
"icons_dir" => Config::get(Config::ICONS_DIR),
|
||||||
"icons_url" => ICONS_URL);
|
"icons_url" => Config::get(Config::ICONS_URL)
|
||||||
|
];
|
||||||
|
|
||||||
$config["daemon_is_running"] = file_is_locked("update_daemon.lock");
|
$config["daemon_is_running"] = file_is_locked("update_daemon.lock");
|
||||||
|
|
||||||
@@ -376,7 +384,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
$config["num_feeds"] = $row["cf"];
|
$config["num_feeds"] = $row["cf"];
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, $config);
|
$this->_wrap(self::STATUS_OK, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFeed() {
|
function updateFeed() {
|
||||||
@@ -386,7 +394,7 @@ class API extends Handler {
|
|||||||
RSSUtils::update_rss_feed($feed_id);
|
RSSUtils::update_rss_feed($feed_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, array("status" => "OK"));
|
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function catchupFeed() {
|
function catchupFeed() {
|
||||||
@@ -397,15 +405,15 @@ class API extends Handler {
|
|||||||
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
|
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
|
||||||
$mode = "all";
|
$mode = "all";
|
||||||
|
|
||||||
Feeds::catchup_feed($feed_id, $is_cat, $_SESSION["uid"], $mode);
|
Feeds::_catchup($feed_id, $is_cat, $_SESSION["uid"], $mode);
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, array("status" => "OK"));
|
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPref() {
|
function getPref() {
|
||||||
$pref_name = clean($_REQUEST["pref_name"]);
|
$pref_name = clean($_REQUEST["pref_name"]);
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
|
$this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLabels() {
|
function getLabels() {
|
||||||
@@ -419,7 +427,7 @@ class API extends Handler {
|
|||||||
$sth->execute([$_SESSION['uid']]);
|
$sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
if ($article_id)
|
if ($article_id)
|
||||||
$article_labels = Article::get_article_labels($article_id);
|
$article_labels = Article::_get_labels($article_id);
|
||||||
else
|
else
|
||||||
$article_labels = array();
|
$article_labels = array();
|
||||||
|
|
||||||
@@ -441,14 +449,14 @@ class API extends Handler {
|
|||||||
"checked" => $checked));
|
"checked" => $checked));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, $rv);
|
$this->_wrap(self::STATUS_OK, $rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setArticleLabel() {
|
function setArticleLabel() {
|
||||||
|
|
||||||
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
|
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
|
||||||
$label_id = (int) clean($_REQUEST['label_id']);
|
$label_id = (int) clean($_REQUEST['label_id']);
|
||||||
$assign = self::param_to_bool(clean($_REQUEST['assign']));
|
$assign = self::_param_to_bool(clean($_REQUEST['assign']));
|
||||||
|
|
||||||
$label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]);
|
$label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]);
|
||||||
|
|
||||||
@@ -468,7 +476,7 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, array("status" => "OK",
|
$this->_wrap(self::STATUS_OK, array("status" => "OK",
|
||||||
"updated" => $num_updated));
|
"updated" => $num_updated));
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -479,10 +487,10 @@ class API extends Handler {
|
|||||||
if ($plugin && method_exists($plugin, $method)) {
|
if ($plugin && method_exists($plugin, $method)) {
|
||||||
$reply = $plugin->$method();
|
$reply = $plugin->$method();
|
||||||
|
|
||||||
$this->wrap($reply[0], $reply[1]);
|
$this->_wrap($reply[0], $reply[1]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'UNKNOWN_METHOD', "method" => $method));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_UNKNOWN_METHOD, "method" => $method));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,14 +499,14 @@ class API extends Handler {
|
|||||||
$url = strip_tags(clean($_REQUEST["url"]));
|
$url = strip_tags(clean($_REQUEST["url"]));
|
||||||
$content = strip_tags(clean($_REQUEST["content"]));
|
$content = strip_tags(clean($_REQUEST["content"]));
|
||||||
|
|
||||||
if (Article::create_published_article($title, $url, $content, "", $_SESSION["uid"])) {
|
if (Article::_create_published_article($title, $url, $content, "", $_SESSION["uid"])) {
|
||||||
$this->wrap(self::STATUS_OK, array("status" => 'OK'));
|
$this->_wrap(self::STATUS_OK, array("status" => 'OK'));
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'Publishing failed'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
|
private static function _api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
|
||||||
|
|
||||||
$feeds = array();
|
$feeds = array();
|
||||||
|
|
||||||
@@ -512,7 +520,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
/* API only: -4 All feeds, including virtual feeds */
|
/* API only: -4 All feeds, including virtual feeds */
|
||||||
if ($cat_id == -4 || $cat_id == -2) {
|
if ($cat_id == -4 || $cat_id == -2) {
|
||||||
$counters = Counters::getLabelCounters(true);
|
$counters = Counters::get_labels();
|
||||||
|
|
||||||
foreach (array_values($counters) as $cv) {
|
foreach (array_values($counters) as $cv) {
|
||||||
|
|
||||||
@@ -539,7 +547,7 @@ class API extends Handler {
|
|||||||
$unread = getFeedUnread($i);
|
$unread = getFeedUnread($i);
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
$title = Feeds::getFeedTitle($i);
|
$title = Feeds::_get_title($i);
|
||||||
|
|
||||||
$row = array(
|
$row = array(
|
||||||
"id" => $i,
|
"id" => $i,
|
||||||
@@ -564,7 +572,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
$unread = getFeedUnread($line["id"], true) +
|
$unread = getFeedUnread($line["id"], true) +
|
||||||
Feeds::getCategoryChildrenUnread($line["id"]);
|
Feeds::_get_cat_children_unread($line["id"]);
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
$row = array(
|
$row = array(
|
||||||
@@ -612,7 +620,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
$unread = getFeedUnread($line["id"]);
|
$unread = getFeedUnread($line["id"]);
|
||||||
|
|
||||||
$has_icon = Feeds::feedHasIcon($line['id']);
|
$has_icon = Feeds::_has_icon($line['id']);
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
|
|
||||||
@@ -634,7 +642,7 @@ class API extends Handler {
|
|||||||
return $feeds;
|
return $feeds;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function api_get_headlines($feed_id, $limit, $offset,
|
private static function _api_get_headlines($feed_id, $limit, $offset,
|
||||||
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
|
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
|
||||||
$include_attachments, $since_id,
|
$include_attachments, $since_id,
|
||||||
$search = "", $include_nested = false, $sanitize_content = true,
|
$search = "", $include_nested = false, $sanitize_content = true,
|
||||||
@@ -652,7 +660,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$last_updated = strtotime($row["last_updated"]);
|
$last_updated = strtotime($row["last_updated"]);
|
||||||
$cache_images = self::param_to_bool($row["cache_images"]);
|
$cache_images = self::_param_to_bool($row["cache_images"]);
|
||||||
|
|
||||||
if (!$cache_images && time() - $last_updated > 120) {
|
if (!$cache_images && time() - $last_updated > 120) {
|
||||||
RSSUtils::update_rss_feed($feed_id, true);
|
RSSUtils::update_rss_feed($feed_id, true);
|
||||||
@@ -678,7 +686,7 @@ class API extends Handler {
|
|||||||
"skip_first_id_check" => $skip_first_id_check
|
"skip_first_id_check" => $skip_first_id_check
|
||||||
);
|
);
|
||||||
|
|
||||||
$qfh_ret = Feeds::queryFeedHeadlines($params);
|
$qfh_ret = Feeds::_get_headlines($params);
|
||||||
|
|
||||||
$result = $qfh_ret[0];
|
$result = $qfh_ret[0];
|
||||||
$feed_title = $qfh_ret[1];
|
$feed_title = $qfh_ret[1];
|
||||||
@@ -720,14 +728,14 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_array($labels)) $labels = Article::get_article_labels($line["id"]);
|
if (!is_array($labels)) $labels = Article::_get_labels($line["id"]);
|
||||||
|
|
||||||
$headline_row = array(
|
$headline_row = array(
|
||||||
"id" => (int)$line["id"],
|
"id" => (int)$line["id"],
|
||||||
"guid" => $line["guid"],
|
"guid" => $line["guid"],
|
||||||
"unread" => self::param_to_bool($line["unread"]),
|
"unread" => self::_param_to_bool($line["unread"]),
|
||||||
"marked" => self::param_to_bool($line["marked"]),
|
"marked" => self::_param_to_bool($line["marked"]),
|
||||||
"published" => self::param_to_bool($line["published"]),
|
"published" => self::_param_to_bool($line["published"]),
|
||||||
"updated" => (int)strtotime($line["updated"]),
|
"updated" => (int)strtotime($line["updated"]),
|
||||||
"is_updated" => $is_updated,
|
"is_updated" => $is_updated,
|
||||||
"title" => $line["title"],
|
"title" => $line["title"],
|
||||||
@@ -736,7 +744,7 @@ class API extends Handler {
|
|||||||
"tags" => $tags,
|
"tags" => $tags,
|
||||||
);
|
);
|
||||||
|
|
||||||
$enclosures = Article::get_article_enclosures($line['id']);
|
$enclosures = Article::_get_enclosures($line['id']);
|
||||||
|
|
||||||
if ($include_attachments)
|
if ($include_attachments)
|
||||||
$headline_row['attachments'] = $enclosures;
|
$headline_row['attachments'] = $enclosures;
|
||||||
@@ -749,13 +757,11 @@ class API extends Handler {
|
|||||||
if ($sanitize_content) {
|
if ($sanitize_content) {
|
||||||
$headline_row["content"] = Sanitizer::sanitize(
|
$headline_row["content"] = Sanitizer::sanitize(
|
||||||
$line["content"],
|
$line["content"],
|
||||||
self::param_to_bool($line['hide_images']),
|
self::_param_to_bool($line['hide_images']),
|
||||||
false, $line["site_url"], false, $line["id"]);
|
false, $line["site_url"], false, $line["id"]);
|
||||||
} else {
|
} else {
|
||||||
$headline_row["content"] = $line["content"];
|
$headline_row["content"] = $line["content"];
|
||||||
}
|
}
|
||||||
|
|
||||||
$headline_row["content"] = DiskCache::rewriteUrls($headline_row['content']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unify label output to ease parsing
|
// unify label output to ease parsing
|
||||||
@@ -768,7 +774,7 @@ class API extends Handler {
|
|||||||
$headline_row["comments_count"] = (int)$line["num_comments"];
|
$headline_row["comments_count"] = (int)$line["num_comments"];
|
||||||
$headline_row["comments_link"] = $line["comments"];
|
$headline_row["comments_link"] = $line["comments"];
|
||||||
|
|
||||||
$headline_row["always_display_attachments"] = self::param_to_bool($line["always_display_enclosures"]);
|
$headline_row["always_display_attachments"] = self::_param_to_bool($line["always_display_enclosures"]);
|
||||||
|
|
||||||
$headline_row["author"] = $line["author"];
|
$headline_row["author"] = $line["author"];
|
||||||
|
|
||||||
@@ -776,15 +782,12 @@ class API extends Handler {
|
|||||||
$headline_row["note"] = $line["note"];
|
$headline_row["note"] = $line["note"];
|
||||||
$headline_row["lang"] = $line["lang"];
|
$headline_row["lang"] = $line["lang"];
|
||||||
|
|
||||||
|
if ($show_content) {
|
||||||
$hook_object = ["headline" => &$headline_row];
|
$hook_object = ["headline" => &$headline_row];
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
|
list ($flavor_image, $flavor_stream, $flavor_kind) = Article::_get_image($enclosures,
|
||||||
function ($result) use (&$headline_row) {
|
$line["content"], // unsanitized
|
||||||
$headline_row = $result;
|
$line["site_url"]);
|
||||||
},
|
|
||||||
$hook_object);
|
|
||||||
|
|
||||||
list ($flavor_image, $flavor_stream, $flavor_kind) = Article::get_article_image($enclosures, $line["content"], $line["site_url"]);
|
|
||||||
|
|
||||||
$headline_row["flavor_image"] = $flavor_image;
|
$headline_row["flavor_image"] = $flavor_image;
|
||||||
$headline_row["flavor_stream"] = $flavor_stream;
|
$headline_row["flavor_stream"] = $flavor_stream;
|
||||||
@@ -793,6 +796,15 @@ class API extends Handler {
|
|||||||
if ($flavor_kind)
|
if ($flavor_kind)
|
||||||
$headline_row["flavor_kind"] = $flavor_kind;
|
$headline_row["flavor_kind"] = $flavor_kind;
|
||||||
|
|
||||||
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
|
||||||
|
function ($result) use (&$headline_row) {
|
||||||
|
$headline_row = $result;
|
||||||
|
},
|
||||||
|
$hook_object);
|
||||||
|
|
||||||
|
$headline_row["content"] = DiskCache::rewrite_urls($headline_row['content']);
|
||||||
|
}
|
||||||
|
|
||||||
array_push($headlines, $headline_row);
|
array_push($headlines, $headline_row);
|
||||||
}
|
}
|
||||||
} else if (is_numeric($result) && $result == -1) {
|
} else if (is_numeric($result) && $result == -1) {
|
||||||
@@ -811,9 +823,9 @@ class API extends Handler {
|
|||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
Pref_Feeds::remove_feed($feed_id, $_SESSION["uid"]);
|
Pref_Feeds::remove_feed($feed_id, $_SESSION["uid"]);
|
||||||
$this->wrap(self::STATUS_OK, array("status" => "OK"));
|
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => "FEED_NOT_FOUND"));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,28 +836,28 @@ class API extends Handler {
|
|||||||
$password = clean($_REQUEST["password"]);
|
$password = clean($_REQUEST["password"]);
|
||||||
|
|
||||||
if ($feed_url) {
|
if ($feed_url) {
|
||||||
$rc = Feeds::subscribe_to_feed($feed_url, $category_id, $login, $password);
|
$rc = Feeds::_subscribe($feed_url, $category_id, $login, $password);
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK, array("status" => $rc));
|
$this->_wrap(self::STATUS_OK, array("status" => $rc));
|
||||||
} else {
|
} else {
|
||||||
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeedTree() {
|
function getFeedTree() {
|
||||||
$include_empty = self::param_to_bool(clean($_REQUEST['include_empty']));
|
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty']));
|
||||||
|
|
||||||
$pf = new Pref_Feeds($_REQUEST);
|
$pf = new Pref_Feeds($_REQUEST);
|
||||||
|
|
||||||
$_REQUEST['mode'] = 2;
|
$_REQUEST['mode'] = 2;
|
||||||
$_REQUEST['force_show_empty'] = $include_empty;
|
$_REQUEST['force_show_empty'] = $include_empty;
|
||||||
|
|
||||||
$this->wrap(self::STATUS_OK,
|
$this->_wrap(self::STATUS_OK,
|
||||||
array("categories" => $pf->makefeedtree()));
|
array("categories" => $pf->_makefeedtree()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// only works for labels or uncategorized for the time being
|
// only works for labels or uncategorized for the time being
|
||||||
private function isCategoryEmpty($id) {
|
private function _is_cat_empty($id) {
|
||||||
|
|
||||||
if ($id == -2) {
|
if ($id == -2) {
|
||||||
$sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2
|
$sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class Article extends Handler_Protected {
|
|||||||
const ARTICLE_KIND_YOUTUBE = 3;
|
const ARTICLE_KIND_YOUTUBE = 3;
|
||||||
|
|
||||||
function redirect() {
|
function redirect() {
|
||||||
$id = clean($_REQUEST['id']);
|
$id = (int) clean($_REQUEST['id'] ?? 0);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT link FROM ttrss_entries, ttrss_user_entries
|
$sth = $this->pdo->prepare("SELECT link FROM ttrss_entries, ttrss_user_entries
|
||||||
WHERE id = ? AND id = ref_id AND owner_uid = ?
|
WHERE id = ? AND id = ref_id AND owner_uid = ?
|
||||||
@@ -13,18 +13,21 @@ class Article extends Handler_Protected {
|
|||||||
$sth->execute([$id, $_SESSION['uid']]);
|
$sth->execute([$id, $_SESSION['uid']]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$article_url = $row['link'];
|
$article_url = UrlHelper::validate(str_replace("\n", "", $row['link']));
|
||||||
$article_url = str_replace("\n", "", $article_url);
|
|
||||||
|
|
||||||
|
if ($article_url) {
|
||||||
header("Location: $article_url");
|
header("Location: $article_url");
|
||||||
return;
|
} else {
|
||||||
|
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||||
|
print "URL of article $id is blank.";
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
print_error(__("Article not found."));
|
print_error(__("Article not found."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function create_published_article($title, $url, $content, $labels_str,
|
static function _create_published_article($title, $url, $content, $labels_str,
|
||||||
$owner_uid) {
|
$owner_uid) {
|
||||||
|
|
||||||
$guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash
|
$guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash
|
||||||
@@ -82,7 +85,7 @@ class Article extends Handler_Protected {
|
|||||||
content = ?, content_hash = ? WHERE id = ?");
|
content = ?, content_hash = ? WHERE id = ?");
|
||||||
$sth->execute([$content, $content_hash, $ref_id]);
|
$sth->execute([$content, $content_hash, $ref_id]);
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql"){
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_entries
|
$sth = $pdo->prepare("UPDATE ttrss_entries
|
||||||
SET tsvector_combined = to_tsvector( :ts_content)
|
SET tsvector_combined = to_tsvector( :ts_content)
|
||||||
WHERE id = :id");
|
WHERE id = :id");
|
||||||
@@ -127,7 +130,7 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$ref_id = $row["id"];
|
$ref_id = $row["id"];
|
||||||
if (DB_TYPE == "pgsql"){
|
if (Config::get(Config::DB_TYPE) == "pgsql"){
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_entries
|
$sth = $pdo->prepare("UPDATE ttrss_entries
|
||||||
SET tsvector_combined = to_tsvector( :ts_content)
|
SET tsvector_combined = to_tsvector( :ts_content)
|
||||||
WHERE id = :id");
|
WHERE id = :id");
|
||||||
@@ -158,39 +161,15 @@ class Article extends Handler_Protected {
|
|||||||
return $rc;
|
return $rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function editArticleTags() {
|
function printArticleTags() {
|
||||||
|
$id = (int) clean($_REQUEST['id'] ?? 0);
|
||||||
$param = clean($_REQUEST['param']);
|
|
||||||
|
|
||||||
$tags = self::get_article_tags($param);
|
|
||||||
|
|
||||||
$tags_str = join(", ", $tags);
|
|
||||||
|
|
||||||
print_hidden("id", "$param");
|
|
||||||
print_hidden("op", "article");
|
|
||||||
print_hidden("method", "setArticleTags");
|
|
||||||
|
|
||||||
print "<header class='horizontal'>" . __("Tags for this article (separated by commas):")."</header>";
|
|
||||||
|
|
||||||
print "<section>";
|
|
||||||
print "<textarea dojoType='dijit.form.SimpleTextarea' rows='4'
|
|
||||||
style='height : 100px; font-size : 12px; width : 98%' id='tags_str'
|
|
||||||
name='tags_str'>$tags_str</textarea>
|
|
||||||
<div class='autocomplete' id='tags_choices'
|
|
||||||
style='display:none'></div>";
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<footer>";
|
|
||||||
print "<button dojoType='dijit.form.Button'
|
|
||||||
type='submit' class='alt-primary'>".__('Save')."</button> ";
|
|
||||||
print "<button dojoType='dijit.form.Button'
|
|
||||||
onclick='App.dialogOf(this).hide()'>".__('Cancel')."</button>";
|
|
||||||
print "</footer>";
|
|
||||||
|
|
||||||
|
print json_encode(["id" => $id,
|
||||||
|
"tags" => self::_get_tags($id)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setScore() {
|
function setScore() {
|
||||||
$ids = explode(",", clean($_REQUEST['id']));
|
$ids = array_map("intval", clean($_REQUEST['ids'] ?? []));
|
||||||
$score = (int)clean($_REQUEST['score']);
|
$score = (int)clean($_REQUEST['score']);
|
||||||
|
|
||||||
$ids_qmarks = arr_qmarks($ids);
|
$ids_qmarks = arr_qmarks($ids);
|
||||||
@@ -220,8 +199,10 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
$id = clean($_REQUEST["id"]);
|
$id = clean($_REQUEST["id"]);
|
||||||
|
|
||||||
$tags_str = clean($_REQUEST["tags_str"]);
|
//$tags_str = clean($_REQUEST["tags_str"]);
|
||||||
$tags = array_unique(array_map('trim', explode(",", $tags_str)));
|
//$tags = array_unique(array_map('trim', explode(",", $tags_str)));
|
||||||
|
|
||||||
|
$tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"])));
|
||||||
|
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
@@ -246,8 +227,6 @@ class Article extends Handler_Protected {
|
|||||||
(post_int_id, owner_uid, tag_name)
|
(post_int_id, owner_uid, tag_name)
|
||||||
VALUES (?, ?, ?)");
|
VALUES (?, ?, ?)");
|
||||||
|
|
||||||
$tags = FeedItem_Common::normalize_categories($tags);
|
|
||||||
|
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
$csth->execute([$int_id, $_SESSION['uid'], $tag]);
|
$csth->execute([$int_id, $_SESSION['uid'], $tag]);
|
||||||
|
|
||||||
@@ -269,18 +248,12 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
|
|
||||||
$tags = self::get_article_tags($id);
|
// get latest tags from the database, original $tags is sometimes JSON-encoded as a hash ({}) - ???
|
||||||
$tags_str = $this->format_tags_string($tags);
|
print json_encode(["id" => (int)$id, "tags" => $this->_get_tags($id)]);
|
||||||
$tags_str_full = join(", ", $tags);
|
|
||||||
|
|
||||||
if (!$tags_str_full) $tags_str_full = __("no tags");
|
|
||||||
|
|
||||||
print json_encode(array("id" => (int)$id,
|
|
||||||
"content" => $tags_str, "content_full" => $tags_str_full));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function completeTags() {
|
/*function completeTags() {
|
||||||
$search = clean($_REQUEST["search"]);
|
$search = clean($_REQUEST["search"]);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags
|
$sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags
|
||||||
@@ -295,17 +268,17 @@ class Article extends Handler_Protected {
|
|||||||
print "<li>" . $line["tag_name"] . "</li>";
|
print "<li>" . $line["tag_name"] . "</li>";
|
||||||
}
|
}
|
||||||
print "</ul>";
|
print "</ul>";
|
||||||
}
|
}*/
|
||||||
|
|
||||||
function assigntolabel() {
|
function assigntolabel() {
|
||||||
return $this->labelops(true);
|
return $this->_label_ops(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removefromlabel() {
|
function removefromlabel() {
|
||||||
return $this->labelops(false);
|
return $this->_label_ops(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function labelops($assign) {
|
private function _label_ops($assign) {
|
||||||
$reply = array();
|
$reply = array();
|
||||||
|
|
||||||
$ids = explode(",", clean($_REQUEST["ids"]));
|
$ids = explode(",", clean($_REQUEST["ids"]));
|
||||||
@@ -313,22 +286,17 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
|
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
|
||||||
|
|
||||||
$reply["info-for-headlines"] = array();
|
$reply["labels-for"] = [];
|
||||||
|
|
||||||
if ($label) {
|
if ($label) {
|
||||||
|
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
|
|
||||||
if ($assign)
|
if ($assign)
|
||||||
Labels::add_article($id, $label, $_SESSION["uid"]);
|
Labels::add_article($id, $label, $_SESSION["uid"]);
|
||||||
else
|
else
|
||||||
Labels::remove_article($id, $label, $_SESSION["uid"]);
|
Labels::remove_article($id, $label, $_SESSION["uid"]);
|
||||||
|
|
||||||
$labels = $this->get_article_labels($id, $_SESSION["uid"]);
|
array_push($reply["labels-for"],
|
||||||
|
["id" => (int)$id, "labels" => $this->_get_labels($id)]);
|
||||||
array_push($reply["info-for-headlines"],
|
|
||||||
array("id" => $id, "labels" => $this->format_article_labels($labels)));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,163 +305,84 @@ class Article extends Handler_Protected {
|
|||||||
print json_encode($reply);
|
print json_encode($reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArticleFeed($id) {
|
static function _format_enclosures($id,
|
||||||
$sth = $this->pdo->prepare("SELECT feed_id FROM ttrss_user_entries
|
$always_display_enclosures,
|
||||||
WHERE ref_id = ? AND owner_uid = ?");
|
$article_content,
|
||||||
$sth->execute([$id, $_SESSION['uid']]);
|
$hide_images = false) {
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
$enclosures = self::_get_enclosures($id);
|
||||||
return $row["feed_id"];
|
$enclosures_formatted = "";
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function format_article_enclosures($id, $always_display_enclosures,
|
/*foreach ($enclosures as &$enc) {
|
||||||
$article_content, $hide_images = false) {
|
array_push($enclosures, [
|
||||||
|
"type" => $enc["content_type"],
|
||||||
$result = self::get_article_enclosures($id);
|
"filename" => basename($enc["content_url"]),
|
||||||
$rv = '';
|
"url" => $enc["content_url"],
|
||||||
|
"title" => $enc["title"],
|
||||||
|
"width" => (int) $enc["width"],
|
||||||
|
"height" => (int) $enc["height"]
|
||||||
|
]);
|
||||||
|
}*/
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_FORMAT_ENCLOSURES,
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_FORMAT_ENCLOSURES,
|
||||||
function ($result) use (&$rv) {
|
function ($result) use (&$enclosures_formatted, &$enclosures) {
|
||||||
if (is_array($result)) {
|
if (is_array($result)) {
|
||||||
$rv = $result[0];
|
$enclosures_formatted = $result[0];
|
||||||
$result = $result[1];
|
$enclosures = $result[1];
|
||||||
} else {
|
} else {
|
||||||
$rv = $result;
|
$enclosures_formatted = $result;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$rv, $result, $id, $always_display_enclosures, $article_content, $hide_images);
|
$enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images);
|
||||||
|
|
||||||
if ($rv === '' && !empty($result)) {
|
if (!empty($enclosures_formatted)) {
|
||||||
$entries_html = array();
|
return [
|
||||||
$entries = array();
|
'formatted' => $enclosures_formatted,
|
||||||
$entries_inline = array();
|
'entries' => []
|
||||||
|
];
|
||||||
foreach ($result as $line) {
|
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ENCLOSURE_ENTRY,
|
|
||||||
function($result) use (&$line) {
|
|
||||||
$line = $result;
|
|
||||||
},
|
|
||||||
$line, $id);
|
|
||||||
|
|
||||||
$url = $line["content_url"];
|
|
||||||
$ctype = $line["content_type"];
|
|
||||||
$title = $line["title"];
|
|
||||||
$width = $line["width"];
|
|
||||||
$height = $line["height"];
|
|
||||||
|
|
||||||
if (!$ctype) $ctype = __("unknown type");
|
|
||||||
|
|
||||||
//$filename = substr($url, strrpos($url, "/")+1);
|
|
||||||
$filename = basename($url);
|
|
||||||
|
|
||||||
$player = format_inline_player($url, $ctype);
|
|
||||||
|
|
||||||
if ($player) array_push($entries_inline, $player);
|
|
||||||
|
|
||||||
# $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\" rel=\"noopener noreferrer\">" .
|
|
||||||
# $filename . " (" . $ctype . ")" . "</a>";
|
|
||||||
|
|
||||||
$entry = "<div onclick=\"Article.popupOpenUrl('".htmlspecialchars($url)."')\"
|
|
||||||
dojoType=\"dijit.MenuItem\">$filename ($ctype)</div>";
|
|
||||||
|
|
||||||
array_push($entries_html, $entry);
|
|
||||||
|
|
||||||
$entry = array();
|
|
||||||
|
|
||||||
$entry["type"] = $ctype;
|
|
||||||
$entry["filename"] = $filename;
|
|
||||||
$entry["url"] = $url;
|
|
||||||
$entry["title"] = $title;
|
|
||||||
$entry["width"] = $width;
|
|
||||||
$entry["height"] = $height;
|
|
||||||
|
|
||||||
array_push($entries, $entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) {
|
$rv = [
|
||||||
if ($always_display_enclosures ||
|
'formatted' => '',
|
||||||
!preg_match("/<img/i", $article_content)) {
|
'entries' => []
|
||||||
|
];
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
$rv['can_inline'] = isset($_SESSION["uid"]) &&
|
||||||
|
empty($_SESSION["bw_limit"]) &&
|
||||||
|
!get_pref("STRIP_IMAGES") &&
|
||||||
|
($always_display_enclosures || !preg_match("/<img/i", $article_content));
|
||||||
|
|
||||||
$retval = null;
|
$rv['inline_text_only'] = $hide_images && $rv['can_inline'];
|
||||||
|
|
||||||
|
foreach ($enclosures as $enc) {
|
||||||
|
|
||||||
|
// this is highly approximate
|
||||||
|
$enc["filename"] = basename($enc["content_url"]);
|
||||||
|
|
||||||
|
$rendered_enc = "";
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ENCLOSURE,
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ENCLOSURE,
|
||||||
function($result) use (&$retval) {
|
function ($result) use (&$rendered_enc) {
|
||||||
$retval = $result;
|
$rendered_enc = $result;
|
||||||
},
|
},
|
||||||
$entry, $hide_images);
|
$enc, $id, $rv);
|
||||||
|
|
||||||
if (!empty($retval)) {
|
if ($rendered_enc) {
|
||||||
$rv .= $retval;
|
$rv['formatted'] .= $rendered_enc;
|
||||||
} else {
|
} else {
|
||||||
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ENCLOSURE_ENTRY,
|
||||||
|
function ($result) use (&$enc) {
|
||||||
|
$enc = $result;
|
||||||
|
},
|
||||||
|
$enc, $id, $rv);
|
||||||
|
|
||||||
if (preg_match("/image/", $entry["type"])) {
|
array_push($rv['entries'], $enc);
|
||||||
|
|
||||||
if (!$hide_images) {
|
|
||||||
$encsize = '';
|
|
||||||
if ($entry['height'] > 0)
|
|
||||||
$encsize .= ' height="' . intval($entry['height']) . '"';
|
|
||||||
if ($entry['width'] > 0)
|
|
||||||
$encsize .= ' width="' . intval($entry['width']) . '"';
|
|
||||||
$rv .= "<p><img
|
|
||||||
alt=\"".htmlspecialchars($entry["filename"])."\"
|
|
||||||
src=\"" .htmlspecialchars($entry["url"]) . "\"
|
|
||||||
" . $encsize . " /></p>";
|
|
||||||
} else {
|
|
||||||
$rv .= "<p><a target=\"_blank\" rel=\"noopener noreferrer\"
|
|
||||||
href=\"".htmlspecialchars($entry["url"])."\"
|
|
||||||
>" .htmlspecialchars($entry["url"]) . "</a></p>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entry['title']) {
|
|
||||||
$rv.= "<div class=\"enclosure_title\">${entry['title']}</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($entries_inline) > 0) {
|
|
||||||
//$rv .= "<hr clear='both'/>";
|
|
||||||
foreach ($entries_inline as $entry) { $rv .= $entry; };
|
|
||||||
$rv .= "<br clear='both'/>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$rv .= "<div class=\"attachments\" dojoType=\"fox.form.DropDownButton\">".
|
|
||||||
"<span>" . __('Attachments')."</span>";
|
|
||||||
|
|
||||||
$rv .= "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
|
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
|
||||||
if ($entry["title"])
|
|
||||||
$title = " — " . truncate_string($entry["title"], 30);
|
|
||||||
else
|
|
||||||
$title = "";
|
|
||||||
|
|
||||||
if ($entry["filename"])
|
|
||||||
$filename = truncate_middle(htmlspecialchars($entry["filename"]), 60);
|
|
||||||
else
|
|
||||||
$filename = "";
|
|
||||||
|
|
||||||
$rv .= "<div onclick='Article.popupOpenUrl(\"".htmlspecialchars($entry["url"])."\")'
|
|
||||||
dojoType=\"dijit.MenuItem\">".$filename . $title."</div>";
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
$rv .= "</div>";
|
|
||||||
$rv .= "</div>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rv;
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function get_article_tags($id, $owner_uid = 0, $tag_cache = false) {
|
static function _get_tags($id, $owner_uid = 0, $tag_cache = false) {
|
||||||
|
|
||||||
$a_id = $id;
|
$a_id = $id;
|
||||||
|
|
||||||
@@ -543,59 +432,22 @@ class Article extends Handler_Protected {
|
|||||||
return $tags;
|
return $tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function format_tags_string($tags) {
|
function getmetadatabyid() {
|
||||||
if (!is_array($tags) || count($tags) == 0) {
|
$id = clean($_REQUEST['id']);
|
||||||
return __("no tags");
|
|
||||||
} else {
|
|
||||||
$maxtags = min(5, count($tags));
|
|
||||||
$tags_str = "";
|
|
||||||
|
|
||||||
for ($i = 0; $i < $maxtags; $i++) {
|
$sth = $this->pdo->prepare("SELECT link, title FROM ttrss_entries, ttrss_user_entries
|
||||||
$tags_str .= "<a class=\"tag\" href=\"#\" onclick=\"Feeds.open({feed:'".$tags[$i]."'})\">" . $tags[$i] . "</a>, ";
|
WHERE ref_id = ? AND ref_id = id AND owner_uid = ?");
|
||||||
}
|
$sth->execute([$id, $_SESSION['uid']]);
|
||||||
|
|
||||||
$tags_str = mb_substr($tags_str, 0, mb_strlen($tags_str)-2);
|
if ($row = $sth->fetch()) {
|
||||||
|
$link = $row['link'];
|
||||||
|
$title = $row['title'];
|
||||||
|
|
||||||
if (count($tags) > $maxtags)
|
echo json_encode(["link" => $link, "title" => $title]);
|
||||||
$tags_str .= ", …";
|
|
||||||
|
|
||||||
return $tags_str;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function format_article_labels($labels) {
|
static function _get_enclosures($id) {
|
||||||
|
|
||||||
if (!is_array($labels)) return '';
|
|
||||||
|
|
||||||
$labels_str = "";
|
|
||||||
|
|
||||||
foreach ($labels as $l) {
|
|
||||||
$labels_str .= sprintf("<div class='label'
|
|
||||||
style='color : %s; background-color : %s'>%s</div>",
|
|
||||||
$l[2], $l[3], $l[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $labels_str;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static function format_article_note($id, $note, $allow_edit = true) {
|
|
||||||
|
|
||||||
if ($allow_edit) {
|
|
||||||
$onclick = "onclick='Plugins.Note.edit($id)'";
|
|
||||||
$note_class = 'editable';
|
|
||||||
} else {
|
|
||||||
$onclick = '';
|
|
||||||
$note_class = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return "<div class='article-note $note_class'>
|
|
||||||
<i class='material-icons'>note</i>
|
|
||||||
<div $onclick class='body'>$note</div>
|
|
||||||
</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
static function get_article_enclosures($id) {
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
@@ -607,10 +459,10 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
$cache = new DiskCache("images");
|
$cache = new DiskCache("images");
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
|
||||||
if ($cache->exists(sha1($line["content_url"]))) {
|
if ($cache->exists(sha1($line["content_url"]))) {
|
||||||
$line["content_url"] = $cache->getUrl(sha1($line["content_url"]));
|
$line["content_url"] = $cache->get_url(sha1($line["content_url"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
array_push($rv, $line);
|
array_push($rv, $line);
|
||||||
@@ -619,11 +471,11 @@ class Article extends Handler_Protected {
|
|||||||
return $rv;
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function purge_orphans() {
|
static function _purge_orphans() {
|
||||||
|
|
||||||
// purge orphaned posts in main content table
|
// purge orphaned posts in main content table
|
||||||
|
|
||||||
if (DB_TYPE == "mysql")
|
if (Config::get(Config::DB_TYPE) == "mysql")
|
||||||
$limit_qpart = "LIMIT 5000";
|
$limit_qpart = "LIMIT 5000";
|
||||||
else
|
else
|
||||||
$limit_qpart = "";
|
$limit_qpart = "";
|
||||||
@@ -638,7 +490,7 @@ class Article extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function catchupArticlesById($ids, $cmode, $owner_uid = false) {
|
static function _catchup_by_id($ids, $cmode, $owner_uid = false) {
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
||||||
|
|
||||||
@@ -663,21 +515,7 @@ class Article extends Handler_Protected {
|
|||||||
$sth->execute(array_merge($ids, [$owner_uid]));
|
$sth->execute(array_merge($ids, [$owner_uid]));
|
||||||
}
|
}
|
||||||
|
|
||||||
static function getLastArticleId() {
|
static function _get_labels($id, $owner_uid = false) {
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT ref_id AS id FROM ttrss_user_entries
|
|
||||||
WHERE owner_uid = ? ORDER BY ref_id DESC LIMIT 1");
|
|
||||||
$sth->execute([$_SESSION['uid']]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
|
||||||
return $row['id'];
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function get_article_labels($id, $owner_uid = false) {
|
|
||||||
$rv = array();
|
$rv = array();
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
||||||
@@ -724,7 +562,7 @@ class Article extends Handler_Protected {
|
|||||||
return $rv;
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function get_article_image($enclosures, $content, $site_url) {
|
static function _get_image($enclosures, $content, $site_url) {
|
||||||
|
|
||||||
$article_image = "";
|
$article_image = "";
|
||||||
$article_stream = "";
|
$article_stream = "";
|
||||||
@@ -794,12 +632,59 @@ class Article extends Handler_Protected {
|
|||||||
$cache = new DiskCache("images");
|
$cache = new DiskCache("images");
|
||||||
|
|
||||||
if ($article_image && $cache->exists(sha1($article_image)))
|
if ($article_image && $cache->exists(sha1($article_image)))
|
||||||
$article_image = $cache->getUrl(sha1($article_image));
|
$article_image = $cache->get_url(sha1($article_image));
|
||||||
|
|
||||||
if ($article_stream && $cache->exists(sha1($article_stream)))
|
if ($article_stream && $cache->exists(sha1($article_stream)))
|
||||||
$article_stream = $cache->getUrl(sha1($article_stream));
|
$article_stream = $cache->get_url(sha1($article_stream));
|
||||||
|
|
||||||
return [$article_image, $article_stream, $article_kind];
|
return [$article_image, $article_stream, $article_kind];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only cached, returns label ids (not label feed ids)
|
||||||
|
static function _labels_of(array $article_ids) {
|
||||||
|
if (count($article_ids) == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$id_qmarks = arr_qmarks($article_ids);
|
||||||
|
|
||||||
|
$sth = Db::pdo()->prepare("SELECT DISTINCT label_cache FROM ttrss_entries e, ttrss_user_entries ue
|
||||||
|
WHERE ue.ref_id = e.id AND id IN ($id_qmarks)");
|
||||||
|
|
||||||
|
$sth->execute($article_ids);
|
||||||
|
|
||||||
|
$rv = [];
|
||||||
|
|
||||||
|
while ($row = $sth->fetch()) {
|
||||||
|
$labels = json_decode($row["label_cache"]);
|
||||||
|
|
||||||
|
if (isset($labels) && is_array($labels)) {
|
||||||
|
foreach ($labels as $label) {
|
||||||
|
if (empty($label["no-labels"]))
|
||||||
|
array_push($rv, Labels::feed_to_label_id($label[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique($rv);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function _feeds_of(array $article_ids) {
|
||||||
|
if (count($article_ids) == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$id_qmarks = arr_qmarks($article_ids);
|
||||||
|
|
||||||
|
$sth = Db::pdo()->prepare("SELECT DISTINCT feed_id FROM ttrss_entries e, ttrss_user_entries ue
|
||||||
|
WHERE ue.ref_id = e.id AND id IN ($id_qmarks)");
|
||||||
|
|
||||||
|
$sth->execute($article_ids);
|
||||||
|
|
||||||
|
$rv = [];
|
||||||
|
|
||||||
|
while ($row = $sth->fetch()) {
|
||||||
|
array_push($rv, $row["feed_id"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rv;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
|
|||||||
// Auto-creates specified user if allowed by system configuration
|
// Auto-creates specified user if allowed by system configuration
|
||||||
// Can be used instead of find_user_by_login() by external auth modules
|
// Can be used instead of find_user_by_login() by external auth modules
|
||||||
function auto_create_user(string $login, $password = false) {
|
function auto_create_user(string $login, $password = false) {
|
||||||
if ($login && defined('AUTH_AUTO_CREATE') && AUTH_AUTO_CREATE) {
|
if ($login && Config::get(Config::AUTH_AUTO_CREATE)) {
|
||||||
$user_id = UserHelper::find_user_by_login($login);
|
$user_id = UserHelper::find_user_by_login($login);
|
||||||
|
|
||||||
if (!$user_id) {
|
if (!$user_id) {
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
<?php
|
|
||||||
class Backend extends Handler_Protected {
|
|
||||||
/* function digestTest() {
|
|
||||||
if (isset($_SESSION['uid'])) {
|
|
||||||
header("Content-type: text/html");
|
|
||||||
|
|
||||||
$rv = Digest::prepare_headlines_digest($_SESSION['uid'], 1, 1000);
|
|
||||||
|
|
||||||
print "<h1>HTML</h1>";
|
|
||||||
print $rv[0];
|
|
||||||
print "<h1>Plain text</h1>";
|
|
||||||
print "<pre>".$rv[3]."</pre>";
|
|
||||||
} else {
|
|
||||||
print error_json(6);
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
||||||
function help() {
|
|
||||||
$topic = basename(clean($_REQUEST["topic"])); // only one for now
|
|
||||||
|
|
||||||
if ($topic == "main") {
|
|
||||||
$info = RPC::get_hotkeys_info();
|
|
||||||
$imap = RPC::get_hotkeys_map();
|
|
||||||
$omap = array();
|
|
||||||
|
|
||||||
foreach ($imap[1] as $sequence => $action) {
|
|
||||||
if (!isset($omap[$action])) $omap[$action] = array();
|
|
||||||
|
|
||||||
array_push($omap[$action], $sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
print "<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>";
|
|
||||||
|
|
||||||
$cur_section = "";
|
|
||||||
foreach ($info as $section => $hotkeys) {
|
|
||||||
|
|
||||||
if ($cur_section) print "<li> </li>";
|
|
||||||
print "<li><h3>" . $section . "</h3></li>";
|
|
||||||
$cur_section = $section;
|
|
||||||
|
|
||||||
foreach ($hotkeys as $action => $description) {
|
|
||||||
|
|
||||||
if (!empty($omap[$action])) {
|
|
||||||
foreach ($omap[$action] as $sequence) {
|
|
||||||
if (strpos($sequence, "|") !== false) {
|
|
||||||
$sequence = substr($sequence,
|
|
||||||
strpos($sequence, "|")+1,
|
|
||||||
strlen($sequence));
|
|
||||||
} else {
|
|
||||||
$keys = explode(" ", $sequence);
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($keys); $i++) {
|
|
||||||
if (strlen($keys[$i]) > 1) {
|
|
||||||
$tmp = '';
|
|
||||||
foreach (str_split($keys[$i]) as $c) {
|
|
||||||
switch ($c) {
|
|
||||||
case '*':
|
|
||||||
$tmp .= __('Shift') . '+';
|
|
||||||
break;
|
|
||||||
case '^':
|
|
||||||
$tmp .= __('Ctrl') . '+';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$tmp .= $c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$keys[$i] = $tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$sequence = join(" ", $keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
print "<li>";
|
|
||||||
print "<div class='hk'><code>$sequence</code></div>";
|
|
||||||
print "<div class='desc'>$description</div>";
|
|
||||||
print "</li>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</ul>";
|
|
||||||
}
|
|
||||||
|
|
||||||
print "<footer class='text-center'>";
|
|
||||||
print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>".__('Close this window')."</button>";
|
|
||||||
print "</footer>";
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
167
classes/config.php
Normal file
167
classes/config.php
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
class Config {
|
||||||
|
private const _ENVVAR_PREFIX = "TTRSS_";
|
||||||
|
|
||||||
|
const T_BOOL = 1;
|
||||||
|
const T_STRING = 2;
|
||||||
|
const T_INT = 3;
|
||||||
|
|
||||||
|
// override defaults, defined below in _DEFAULTS[], via environment: DB_TYPE becomes TTRSS_DB_TYPE, etc
|
||||||
|
|
||||||
|
const DB_TYPE = "DB_TYPE";
|
||||||
|
const DB_HOST = "DB_HOST";
|
||||||
|
const DB_USER = "DB_USER";
|
||||||
|
const DB_NAME = "DB_NAME";
|
||||||
|
const DB_PASS = "DB_PASS";
|
||||||
|
const DB_PORT = "DB_PORT";
|
||||||
|
const MYSQL_CHARSET = "MYSQL_CHARSET";
|
||||||
|
const SELF_URL_PATH = "SELF_URL_PATH";
|
||||||
|
const SINGLE_USER_MODE = "SINGLE_USER_MODE";
|
||||||
|
const SIMPLE_UPDATE_MODE = "SIMPLE_UPDATE_MODE";
|
||||||
|
const PHP_EXECUTABLE = "PHP_EXECUTABLE";
|
||||||
|
const LOCK_DIRECTORY = "LOCK_DIRECTORY";
|
||||||
|
const CACHE_DIR = "CACHE_DIR";
|
||||||
|
const ICONS_DIR = "ICONS_DIR";
|
||||||
|
const ICONS_URL = "ICONS_URL";
|
||||||
|
const AUTH_AUTO_CREATE = "AUTH_AUTO_CREATE";
|
||||||
|
const AUTH_AUTO_LOGIN = "AUTH_AUTO_LOGIN";
|
||||||
|
const FORCE_ARTICLE_PURGE = "FORCE_ARTICLE_PURGE";
|
||||||
|
const SESSION_COOKIE_LIFETIME = "SESSION_COOKIE_LIFETIME";
|
||||||
|
const SMTP_FROM_NAME = "SMTP_FROM_NAME";
|
||||||
|
const SMTP_FROM_ADDRESS = "SMTP_FROM_ADDRESS";
|
||||||
|
const DIGEST_SUBJECT = "DIGEST_SUBJECT";
|
||||||
|
const CHECK_FOR_UPDATES = "CHECK_FOR_UPDATES";
|
||||||
|
const PLUGINS = "PLUGINS";
|
||||||
|
const LOG_DESTINATION = "LOG_DESTINATION";
|
||||||
|
const LOCAL_OVERRIDE_STYLESHEET = "LOCAL_OVERRIDE_STYLESHEET";
|
||||||
|
const DAEMON_MAX_CHILD_RUNTIME = "DAEMON_MAX_CHILD_RUNTIME";
|
||||||
|
const DAEMON_MAX_JOBS = "DAEMON_MAX_JOBS";
|
||||||
|
const FEED_FETCH_TIMEOUT = "FEED_FETCH_TIMEOUT";
|
||||||
|
const FEED_FETCH_NO_CACHE_TIMEOUT = "FEED_FETCH_NO_CACHE_TIMEOUT";
|
||||||
|
const FILE_FETCH_TIMEOUT = "FILE_FETCH_TIMEOUT";
|
||||||
|
const FILE_FETCH_CONNECT_TIMEOUT = "FILE_FETCH_CONNECT_TIMEOUT";
|
||||||
|
const DAEMON_UPDATE_LOGIN_LIMIT = "DAEMON_UPDATE_LOGIN_LIMIT";
|
||||||
|
const DAEMON_FEED_LIMIT = "DAEMON_FEED_LIMIT";
|
||||||
|
const DAEMON_SLEEP_INTERVAL = "DAEMON_SLEEP_INTERVAL";
|
||||||
|
const MAX_CACHE_FILE_SIZE = "MAX_CACHE_FILE_SIZE";
|
||||||
|
const MAX_DOWNLOAD_FILE_SIZE = "MAX_DOWNLOAD_FILE_SIZE";
|
||||||
|
const MAX_FAVICON_FILE_SIZE = "MAX_FAVICON_FILE_SIZE";
|
||||||
|
const CACHE_MAX_DAYS = "CACHE_MAX_DAYS";
|
||||||
|
const MAX_CONDITIONAL_INTERVAL = "MAX_CONDITIONAL_INTERVAL";
|
||||||
|
const DAEMON_UNSUCCESSFUL_DAYS_LIMIT = "DAEMON_UNSUCCESSFUL_DAYS_LIMIT";
|
||||||
|
const LOG_SENT_MAIL = "LOG_SENT_MAIL";
|
||||||
|
const HTTP_PROXY = "HTTP_PROXY";
|
||||||
|
const FORBID_PASSWORD_CHANGES = "FORBID_PASSWORD_CHANGES";
|
||||||
|
const SESSION_NAME = "SESSION_NAME";
|
||||||
|
|
||||||
|
private const _DEFAULTS = [
|
||||||
|
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
|
||||||
|
Config::DB_HOST => [ "db", Config::T_STRING ],
|
||||||
|
Config::DB_USER => [ "", Config::T_STRING ],
|
||||||
|
Config::DB_NAME => [ "", Config::T_STRING ],
|
||||||
|
Config::DB_PASS => [ "", Config::T_STRING ],
|
||||||
|
Config::DB_PORT => [ "5432", Config::T_STRING ],
|
||||||
|
Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ],
|
||||||
|
Config::SELF_URL_PATH => [ "", Config::T_STRING ],
|
||||||
|
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
|
||||||
|
Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ],
|
||||||
|
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
|
||||||
|
Config::LOCK_DIRECTORY => [ "lock", Config::T_STRING ],
|
||||||
|
Config::CACHE_DIR => [ "cache", Config::T_STRING ],
|
||||||
|
Config::ICONS_DIR => [ "feed-icons", Config::T_STRING ],
|
||||||
|
Config::ICONS_URL => [ "feed-icons", Config::T_STRING ],
|
||||||
|
Config::AUTH_AUTO_CREATE => [ "true", Config::T_BOOL ],
|
||||||
|
Config::AUTH_AUTO_LOGIN => [ "true", Config::T_BOOL ],
|
||||||
|
Config::FORCE_ARTICLE_PURGE => [ 0, Config::T_INT ],
|
||||||
|
Config::SESSION_COOKIE_LIFETIME => [ 86400, Config::T_INT ],
|
||||||
|
Config::SMTP_FROM_NAME => [ "Tiny Tiny RSS", Config::T_STRING ],
|
||||||
|
Config::SMTP_FROM_ADDRESS => [ "noreply@localhost", Config::T_STRING ],
|
||||||
|
Config::DIGEST_SUBJECT => [ "[tt-rss] New headlines for last 24 hours",
|
||||||
|
Config::T_STRING ],
|
||||||
|
Config::CHECK_FOR_UPDATES => [ "true", Config::T_BOOL ],
|
||||||
|
Config::PLUGINS => [ "auth_internal", Config::T_STRING ],
|
||||||
|
Config::LOG_DESTINATION => [ "sql", Config::T_STRING ],
|
||||||
|
Config::LOCAL_OVERRIDE_STYLESHEET => [ "local-overrides.css",
|
||||||
|
Config::T_STRING ],
|
||||||
|
Config::DAEMON_MAX_CHILD_RUNTIME => [ 1800, Config::T_STRING ],
|
||||||
|
Config::DAEMON_MAX_JOBS => [ 2, Config::T_INT ],
|
||||||
|
Config::FEED_FETCH_TIMEOUT => [ 45, Config::T_INT ],
|
||||||
|
Config::FEED_FETCH_NO_CACHE_TIMEOUT => [ 15, Config::T_INT ],
|
||||||
|
Config::FILE_FETCH_TIMEOUT => [ 45, Config::T_INT ],
|
||||||
|
Config::FILE_FETCH_CONNECT_TIMEOUT => [ 15, Config::T_INT ],
|
||||||
|
Config::DAEMON_UPDATE_LOGIN_LIMIT => [ 30, Config::T_INT ],
|
||||||
|
Config::DAEMON_FEED_LIMIT => [ 500, Config::T_INT ],
|
||||||
|
Config::DAEMON_SLEEP_INTERVAL => [ 120, Config::T_INT ],
|
||||||
|
Config::MAX_CACHE_FILE_SIZE => [ 64*1024*1024, Config::T_INT ],
|
||||||
|
Config::MAX_DOWNLOAD_FILE_SIZE => [ 16*1024*1024, Config::T_INT ],
|
||||||
|
Config::MAX_FAVICON_FILE_SIZE => [ 1*1024*1024, Config::T_INT ],
|
||||||
|
Config::CACHE_MAX_DAYS => [ 7, Config::T_INT ],
|
||||||
|
Config::MAX_CONDITIONAL_INTERVAL => [ 3600*12, Config::T_INT ],
|
||||||
|
Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT => [ 30, Config::T_INT ],
|
||||||
|
Config::LOG_SENT_MAIL => [ "", Config::T_BOOL ],
|
||||||
|
Config::HTTP_PROXY => [ "", Config::T_STRING ],
|
||||||
|
Config::FORBID_PASSWORD_CHANGES => [ "", Config::T_BOOL ],
|
||||||
|
Config::SESSION_NAME => [ "ttrss_sid", Config::T_STRING ],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $instance;
|
||||||
|
|
||||||
|
private $params = [];
|
||||||
|
|
||||||
|
public static function get_instance() {
|
||||||
|
if (self::$instance == null)
|
||||||
|
self::$instance = new self();
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __construct() {
|
||||||
|
$ref = new ReflectionClass(get_class($this));
|
||||||
|
|
||||||
|
foreach ($ref->getConstants() as $const => $cvalue) {
|
||||||
|
if (isset($this::_DEFAULTS[$const])) {
|
||||||
|
$override = getenv($this::_ENVVAR_PREFIX . $const);
|
||||||
|
|
||||||
|
list ($defval, $deftype) = $this::_DEFAULTS[$const];
|
||||||
|
|
||||||
|
$this->params[$cvalue] = [ $this->cast_to(!empty($override) ? $override : $defval, $deftype), $deftype ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cast_to(string $value, int $type_hint) {
|
||||||
|
switch ($type_hint) {
|
||||||
|
case self::T_BOOL:
|
||||||
|
return sql_bool_to_bool($value);
|
||||||
|
case self::T_INT:
|
||||||
|
return (int) $value;
|
||||||
|
default:
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _get(string $param) {
|
||||||
|
list ($value, $type_hint) = $this->params[$param];
|
||||||
|
|
||||||
|
return $this->cast_to($value, $type_hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _add(string $param, string $default, int $type_hint) {
|
||||||
|
$override = getenv($this::_ENVVAR_PREFIX . $param);
|
||||||
|
|
||||||
|
$this->params[$param] = [ $this->cast_to(!empty($override) ? $override : $default, $type_hint), $type_hint ];
|
||||||
|
}
|
||||||
|
|
||||||
|
static function add(string $param, string $default, int $type_hint = Config::T_STRING) {
|
||||||
|
$instance = self::get_instance();
|
||||||
|
|
||||||
|
return $instance->_add($param, $default, $type_hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function get(string $param) {
|
||||||
|
$instance = self::get_instance();
|
||||||
|
|
||||||
|
return $instance->_get($param);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
class Counters {
|
class Counters {
|
||||||
|
|
||||||
static function getAllCounters() {
|
static function get_all() {
|
||||||
$data = self::getGlobalCounters();
|
return array_merge(
|
||||||
|
self::get_global(),
|
||||||
$data = array_merge($data, self::getVirtCounters());
|
self::get_virt(),
|
||||||
$data = array_merge($data, self::getLabelCounters());
|
self::get_labels(),
|
||||||
$data = array_merge($data, self::getFeedCounters());
|
self::get_feeds(),
|
||||||
$data = array_merge($data, self::getCategoryCounters());
|
self::get_cats()
|
||||||
|
);
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static private function getCategoryChildrenCounters($cat_id, $owner_uid) {
|
static function get_conditional(array $feed_ids = null, array $label_ids = null) {
|
||||||
|
return array_merge(
|
||||||
|
self::get_global(),
|
||||||
|
self::get_virt(),
|
||||||
|
self::get_labels($label_ids),
|
||||||
|
self::get_feeds($feed_ids),
|
||||||
|
self::get_cats(is_array($feed_ids) ? Feeds::_cats_of($feed_ids, $_SESSION["uid"], true) : null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static private function get_cat_children($cat_id, $owner_uid) {
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE parent_cat = ?
|
$sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE parent_cat = ?
|
||||||
@@ -23,27 +32,60 @@ class Counters {
|
|||||||
$marked = 0;
|
$marked = 0;
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
list ($tmp_unread, $tmp_marked) = self::getCategoryChildrenCounters($line["id"], $owner_uid);
|
list ($tmp_unread, $tmp_marked) = self::get_cat_children($line["id"], $owner_uid);
|
||||||
|
|
||||||
$unread += $tmp_unread + Feeds::getCategoryUnread($line["id"], $owner_uid);
|
$unread += $tmp_unread + Feeds::_get_cat_unread($line["id"], $owner_uid);
|
||||||
$marked += $tmp_marked + Feeds::getCategoryMarked($line["id"], $owner_uid);
|
$marked += $tmp_marked + Feeds::_get_cat_marked($line["id"], $owner_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$unread, $marked];
|
return [$unread, $marked];
|
||||||
}
|
}
|
||||||
|
|
||||||
static function getCategoryCounters() {
|
private static function get_cats(array $cat_ids = null) {
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
/* Labels category */
|
/* Labels category */
|
||||||
|
|
||||||
$cv = array("id" => -2, "kind" => "cat",
|
$cv = array("id" => -2, "kind" => "cat",
|
||||||
"counter" => Feeds::getCategoryUnread(-2));
|
"counter" => Feeds::_get_cat_unread(-2));
|
||||||
|
|
||||||
array_push($ret, $cv);
|
array_push($ret, $cv);
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
if (is_array($cat_ids)) {
|
||||||
|
if (count($cat_ids) == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$cat_ids_qmarks = arr_qmarks($cat_ids);
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT fc.id,
|
||||||
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
|
(SELECT COUNT(id) FROM ttrss_feed_categories fcc
|
||||||
|
WHERE fcc.parent_cat = fc.id) AS num_children
|
||||||
|
FROM ttrss_feed_categories fc
|
||||||
|
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
|
||||||
|
LEFT JOIN ttrss_user_entries ue ON (ue.feed_id = f.id)
|
||||||
|
WHERE fc.owner_uid = ? AND fc.id IN ($cat_ids_qmarks)
|
||||||
|
GROUP BY fc.id
|
||||||
|
UNION
|
||||||
|
SELECT 0,
|
||||||
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
|
0
|
||||||
|
FROM ttrss_feeds f, ttrss_user_entries ue
|
||||||
|
WHERE f.cat_id IS NULL AND
|
||||||
|
ue.feed_id = f.id AND
|
||||||
|
ue.owner_uid = ?");
|
||||||
|
|
||||||
|
$sth->execute(array_merge(
|
||||||
|
[$_SESSION['uid']],
|
||||||
|
$cat_ids,
|
||||||
|
[$_SESSION['uid']]
|
||||||
|
));
|
||||||
|
|
||||||
|
} else {
|
||||||
$sth = $pdo->prepare("SELECT fc.id,
|
$sth = $pdo->prepare("SELECT fc.id,
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
@@ -65,10 +107,11 @@ class Counters {
|
|||||||
ue.owner_uid = :uid");
|
ue.owner_uid = :uid");
|
||||||
|
|
||||||
$sth->execute(["uid" => $_SESSION['uid']]);
|
$sth->execute(["uid" => $_SESSION['uid']]);
|
||||||
|
}
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
if ($line["num_children"] > 0) {
|
if ($line["num_children"] > 0) {
|
||||||
list ($child_counter, $child_marked_counter) = self::getCategoryChildrenCounters($line["id"], $_SESSION["uid"]);
|
list ($child_counter, $child_marked_counter) = self::get_cat_children($line["id"], $_SESSION["uid"]);
|
||||||
} else {
|
} else {
|
||||||
$child_counter = 0;
|
$child_counter = 0;
|
||||||
$child_marked_counter = 0;
|
$child_marked_counter = 0;
|
||||||
@@ -84,18 +127,33 @@ class Counters {
|
|||||||
array_push($ret, $cv);
|
array_push($ret, $cv);
|
||||||
}
|
}
|
||||||
|
|
||||||
array_push($ret, $cv);
|
|
||||||
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function get_feeds(array $feed_ids = null) {
|
||||||
static function getFeedCounters($active_feed = false) {
|
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
if (is_array($feed_ids)) {
|
||||||
|
if (count($feed_ids) == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$feed_ids_qmarks = arr_qmarks($feed_ids);
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT f.id,
|
||||||
|
f.title,
|
||||||
|
".SUBSTRING_FOR_DATE."(f.last_updated,1,19) AS last_updated,
|
||||||
|
f.last_error,
|
||||||
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked
|
||||||
|
FROM ttrss_feeds f, ttrss_user_entries ue
|
||||||
|
WHERE f.id = ue.feed_id AND ue.owner_uid = ? AND f.id IN ($feed_ids_qmarks)
|
||||||
|
GROUP BY f.id");
|
||||||
|
|
||||||
|
$sth->execute(array_merge([$_SESSION['uid']], $feed_ids));
|
||||||
|
} else {
|
||||||
$sth = $pdo->prepare("SELECT f.id,
|
$sth = $pdo->prepare("SELECT f.id,
|
||||||
f.title,
|
f.title,
|
||||||
".SUBSTRING_FOR_DATE."(f.last_updated,1,19) AS last_updated,
|
".SUBSTRING_FOR_DATE."(f.last_updated,1,19) AS last_updated,
|
||||||
@@ -107,15 +165,15 @@ class Counters {
|
|||||||
GROUP BY f.id");
|
GROUP BY f.id");
|
||||||
|
|
||||||
$sth->execute(["uid" => $_SESSION['uid']]);
|
$sth->execute(["uid" => $_SESSION['uid']]);
|
||||||
|
}
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
$id = $line["id"];
|
$id = $line["id"];
|
||||||
$last_error = htmlspecialchars($line["last_error"]);
|
|
||||||
$last_updated = TimeHelper::make_local_datetime($line['last_updated'], false);
|
$last_updated = TimeHelper::make_local_datetime($line['last_updated'], false);
|
||||||
|
|
||||||
if (Feeds::feedHasIcon($id)) {
|
if (Feeds::_has_icon($id)) {
|
||||||
$has_img = filemtime(Feeds::getIconFile($id));
|
$has_img = filemtime(Feeds::_get_icon_file($id));
|
||||||
} else {
|
} else {
|
||||||
$has_img = false;
|
$has_img = false;
|
||||||
}
|
}
|
||||||
@@ -132,10 +190,7 @@ class Counters {
|
|||||||
"has_img" => (int) $has_img
|
"has_img" => (int) $has_img
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($last_error)
|
$cv["error"] = $line["last_error"];
|
||||||
$cv["error"] = $last_error;
|
|
||||||
|
|
||||||
if ($active_feed && $id == $active_feed)
|
|
||||||
$cv["title"] = truncate_string($line["title"], 30);
|
$cv["title"] = truncate_string($line["title"], 30);
|
||||||
|
|
||||||
array_push($ret, $cv);
|
array_push($ret, $cv);
|
||||||
@@ -145,11 +200,11 @@ class Counters {
|
|||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function getGlobalCounters($global_unread = -1) {
|
private static function get_global($global_unread = -1) {
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
if ($global_unread == -1) {
|
if ($global_unread == -1) {
|
||||||
$global_unread = Feeds::getGlobalUnread();
|
$global_unread = Feeds::_get_global_unread();
|
||||||
}
|
}
|
||||||
|
|
||||||
$cv = [
|
$cv = [
|
||||||
@@ -178,7 +233,7 @@ class Counters {
|
|||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function getVirtCounters() {
|
private static function get_virt() {
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
@@ -187,7 +242,7 @@ class Counters {
|
|||||||
$count = getFeedUnread($i);
|
$count = getFeedUnread($i);
|
||||||
|
|
||||||
if ($i == 0 || $i == -1 || $i == -2)
|
if ($i == 0 || $i == -1 || $i == -2)
|
||||||
$auxctr = Feeds::getFeedArticles($i, false);
|
$auxctr = Feeds::_get_counters($i, false);
|
||||||
else
|
else
|
||||||
$auxctr = 0;
|
$auxctr = 0;
|
||||||
|
|
||||||
@@ -222,12 +277,30 @@ class Counters {
|
|||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function getLabelCounters($descriptions = false) {
|
static function get_labels(array $label_ids = null) {
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
if (is_array($label_ids)) {
|
||||||
|
if (count($label_ids) == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$label_ids_qmarks = arr_qmarks($label_ids);
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT id,
|
||||||
|
caption,
|
||||||
|
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
||||||
|
SUM(CASE WHEN u1.marked = true THEN 1 ELSE 0 END) AS count_marked,
|
||||||
|
COUNT(u1.unread) AS total
|
||||||
|
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
|
||||||
|
(ttrss_labels2.id = label_id)
|
||||||
|
LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id AND u1.owner_uid = ?
|
||||||
|
WHERE ttrss_labels2.owner_uid = ? AND ttrss_labels2.id IN ($label_ids_qmarks)
|
||||||
|
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
|
||||||
|
$sth->execute(array_merge([$_SESSION["uid"], $_SESSION["uid"]], $label_ids));
|
||||||
|
} else {
|
||||||
$sth = $pdo->prepare("SELECT id,
|
$sth = $pdo->prepare("SELECT id,
|
||||||
caption,
|
caption,
|
||||||
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
||||||
@@ -239,6 +312,7 @@ class Counters {
|
|||||||
WHERE ttrss_labels2.owner_uid = :uid
|
WHERE ttrss_labels2.owner_uid = :uid
|
||||||
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
|
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
|
||||||
$sth->execute([":uid" => $_SESSION['uid']]);
|
$sth->execute([":uid" => $_SESSION['uid']]);
|
||||||
|
}
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
@@ -248,12 +322,10 @@ class Counters {
|
|||||||
"id" => $id,
|
"id" => $id,
|
||||||
"counter" => (int) $line["count_unread"],
|
"counter" => (int) $line["count_unread"],
|
||||||
"auxcounter" => (int) $line["total"],
|
"auxcounter" => (int) $line["total"],
|
||||||
"markedcounter" => (int) $line["count_marked"]
|
"markedcounter" => (int) $line["count_marked"],
|
||||||
|
"description" => $line["caption"]
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($descriptions)
|
|
||||||
$cv["description"] = $line["caption"];
|
|
||||||
|
|
||||||
array_push($ret, $cv);
|
array_push($ret, $cv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
class Db
|
class Db
|
||||||
{
|
{
|
||||||
|
|
||||||
/* @var Db $instance */
|
/* @var Db $instance */
|
||||||
private static $instance;
|
private static $instance;
|
||||||
|
|
||||||
/* @var IDb $adapter */
|
|
||||||
private $adapter;
|
|
||||||
|
|
||||||
private $link;
|
private $link;
|
||||||
|
|
||||||
/* @var PDO $pdo */
|
/* @var PDO $pdo */
|
||||||
@@ -17,49 +13,17 @@ class Db
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
private function legacy_connect() {
|
|
||||||
|
|
||||||
user_error("Legacy connect requested to " . DB_TYPE, E_USER_NOTICE);
|
|
||||||
|
|
||||||
$er = error_reporting(E_ALL);
|
|
||||||
|
|
||||||
switch (DB_TYPE) {
|
|
||||||
case "mysql":
|
|
||||||
$this->adapter = new Db_Mysqli();
|
|
||||||
break;
|
|
||||||
case "pgsql":
|
|
||||||
$this->adapter = new Db_Pgsql();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
die("Unknown DB_TYPE: " . DB_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->adapter) {
|
|
||||||
print("Error initializing database adapter for " . DB_TYPE);
|
|
||||||
exit(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->link = $this->adapter->connect(DB_HOST, DB_USER, DB_PASS, DB_NAME, defined('DB_PORT') ? DB_PORT : "");
|
|
||||||
|
|
||||||
if (!$this->link) {
|
|
||||||
print("Error connecting through adapter: " . $this->adapter->last_error());
|
|
||||||
exit(101);
|
|
||||||
}
|
|
||||||
|
|
||||||
error_reporting($er);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this really shouldn't be used unless a separate PDO connection is needed
|
// this really shouldn't be used unless a separate PDO connection is needed
|
||||||
// normal usage is Db::pdo()->prepare(...) etc
|
// normal usage is Db::pdo()->prepare(...) etc
|
||||||
public function pdo_connect() {
|
public function pdo_connect() {
|
||||||
|
|
||||||
$db_port = defined('DB_PORT') && DB_PORT ? ';port=' . DB_PORT : '';
|
$db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : '';
|
||||||
$db_host = defined('DB_HOST') && DB_HOST ? ';host=' . DB_HOST : '';
|
$db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO(DB_TYPE . ':dbname=' . DB_NAME . $db_host . $db_port,
|
$pdo = new PDO(Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port,
|
||||||
DB_USER,
|
Config::get(Config::DB_USER),
|
||||||
DB_PASS);
|
Config::get(Config::DB_PASS));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
print "<pre>Exception while creating PDO object:" . $e->getMessage() . "</pre>";
|
print "<pre>Exception while creating PDO object:" . $e->getMessage() . "</pre>";
|
||||||
exit(101);
|
exit(101);
|
||||||
@@ -67,18 +31,18 @@ class Db
|
|||||||
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
|
|
||||||
$pdo->query("set client_encoding = 'UTF-8'");
|
$pdo->query("set client_encoding = 'UTF-8'");
|
||||||
$pdo->query("set datestyle = 'ISO, european'");
|
$pdo->query("set datestyle = 'ISO, european'");
|
||||||
$pdo->query("set TIME ZONE 0");
|
$pdo->query("set TIME ZONE 0");
|
||||||
$pdo->query("set cpu_tuple_cost = 0.5");
|
$pdo->query("set cpu_tuple_cost = 0.5");
|
||||||
|
|
||||||
} else if (DB_TYPE == "mysql") {
|
} else if (Config::get(Config::DB_TYPE) == "mysql") {
|
||||||
$pdo->query("SET time_zone = '+0:0'");
|
$pdo->query("SET time_zone = '+0:0'");
|
||||||
|
|
||||||
if (defined('MYSQL_CHARSET') && MYSQL_CHARSET) {
|
if (Config::get(Config::MYSQL_CHARSET)) {
|
||||||
$pdo->query("SET NAMES " . MYSQL_CHARSET);
|
$pdo->query("SET NAMES " . Config::get(Config::MYSQL_CHARSET));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,17 +56,6 @@ class Db
|
|||||||
return self::$instance;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function get() : Db {
|
|
||||||
if (self::$instance == null)
|
|
||||||
self::$instance = new self();
|
|
||||||
|
|
||||||
if (!self::$instance->adapter) {
|
|
||||||
self::$instance->legacy_connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$instance->adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function pdo() : PDO {
|
public static function pdo() : PDO {
|
||||||
if (self::$instance == null)
|
if (self::$instance == null)
|
||||||
self::$instance = new self();
|
self::$instance = new self();
|
||||||
@@ -115,7 +68,7 @@ class Db
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function sql_random_function() {
|
public static function sql_random_function() {
|
||||||
if (DB_TYPE == "mysql") {
|
if (Config::get(Config::DB_TYPE) == "mysql") {
|
||||||
return "RAND()";
|
return "RAND()";
|
||||||
} else {
|
} else {
|
||||||
return "RANDOM()";
|
return "RANDOM()";
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
class Db_Mysqli implements IDb {
|
|
||||||
private $link;
|
|
||||||
private $last_error;
|
|
||||||
|
|
||||||
function connect($host, $user, $pass, $db, $port) {
|
|
||||||
if ($port)
|
|
||||||
$this->link = mysqli_connect($host, $user, $pass, $db, $port);
|
|
||||||
else
|
|
||||||
$this->link = mysqli_connect($host, $user, $pass, $db);
|
|
||||||
|
|
||||||
if ($this->link) {
|
|
||||||
$this->init();
|
|
||||||
|
|
||||||
return $this->link;
|
|
||||||
} else {
|
|
||||||
print("Unable to connect to database (as $user to $host, database $db): " . mysqli_connect_error());
|
|
||||||
exit(102);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escape_string($s, $strip_tags = true) {
|
|
||||||
if ($strip_tags) $s = strip_tags($s);
|
|
||||||
|
|
||||||
return mysqli_real_escape_string($this->link, $s);
|
|
||||||
}
|
|
||||||
|
|
||||||
function query($query, $die_on_error = true) {
|
|
||||||
$result = @mysqli_query($this->link, $query);
|
|
||||||
if (!$result) {
|
|
||||||
$this->last_error = @mysqli_error($this->link);
|
|
||||||
|
|
||||||
@mysqli_query($this->link, "ROLLBACK");
|
|
||||||
user_error("Query $query failed: " . ($this->link ? $this->last_error : "No connection"),
|
|
||||||
$die_on_error ? E_USER_ERROR : E_USER_WARNING);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetch_assoc($result) {
|
|
||||||
return mysqli_fetch_assoc($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function num_rows($result) {
|
|
||||||
return mysqli_num_rows($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetch_result($result, $row, $param) {
|
|
||||||
if (mysqli_data_seek($result, $row)) {
|
|
||||||
$line = mysqli_fetch_assoc($result);
|
|
||||||
return $line[$param];
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
return mysqli_close($this->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
function affected_rows($result) {
|
|
||||||
return mysqli_affected_rows($this->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
function last_error() {
|
|
||||||
return mysqli_error($this->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
function last_query_error() {
|
|
||||||
return $this->last_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
$this->query("SET time_zone = '+0:0'");
|
|
||||||
|
|
||||||
if (defined('MYSQL_CHARSET') && MYSQL_CHARSET) {
|
|
||||||
mysqli_set_charset($this->link, MYSQL_CHARSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
class Db_Pgsql implements IDb {
|
|
||||||
private $link;
|
|
||||||
private $last_error;
|
|
||||||
|
|
||||||
function connect($host, $user, $pass, $db, $port) {
|
|
||||||
$string = "dbname=$db user=$user";
|
|
||||||
|
|
||||||
if ($pass) {
|
|
||||||
$string .= " password=$pass";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($host) {
|
|
||||||
$string .= " host=$host";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_numeric($port) && $port > 0) {
|
|
||||||
$string = "$string port=" . $port;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->link = pg_connect($string);
|
|
||||||
|
|
||||||
if (!$this->link) {
|
|
||||||
print("Unable to connect to database (as $user to $host, database $db):" . pg_last_error());
|
|
||||||
exit(102);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->init();
|
|
||||||
|
|
||||||
return $this->link;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escape_string($s, $strip_tags = true) {
|
|
||||||
if ($strip_tags) $s = strip_tags($s);
|
|
||||||
|
|
||||||
return pg_escape_string($s);
|
|
||||||
}
|
|
||||||
|
|
||||||
function query($query, $die_on_error = true) {
|
|
||||||
$result = @pg_query($this->link, $query);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
$this->last_error = @pg_last_error($this->link);
|
|
||||||
|
|
||||||
@pg_query($this->link, "ROLLBACK");
|
|
||||||
$query = htmlspecialchars($query); // just in case
|
|
||||||
user_error("Query $query failed: " . ($this->link ? $this->last_error : "No connection"),
|
|
||||||
$die_on_error ? E_USER_ERROR : E_USER_WARNING);
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetch_assoc($result) {
|
|
||||||
return pg_fetch_assoc($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function num_rows($result) {
|
|
||||||
return pg_num_rows($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetch_result($result, $row, $param) {
|
|
||||||
return pg_fetch_result($result, $row, $param);
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
return pg_close($this->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
function affected_rows($result) {
|
|
||||||
return pg_affected_rows($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function last_error() {
|
|
||||||
return pg_last_error($this->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
function last_query_error() {
|
|
||||||
return $this->last_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
$this->query("set client_encoding = 'UTF-8'");
|
|
||||||
pg_set_client_encoding("UNICODE");
|
|
||||||
$this->query("set datestyle = 'ISO, european'");
|
|
||||||
$this->query("set TIME ZONE 0");
|
|
||||||
$this->query("set cpu_tuple_cost = 0.5");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,9 +6,8 @@ class Db_Prefs {
|
|||||||
|
|
||||||
function __construct() {
|
function __construct() {
|
||||||
$this->pdo = Db::pdo();
|
$this->pdo = Db::pdo();
|
||||||
$this->cache = array();
|
$this->cache = [];
|
||||||
|
$this->cache_prefs();
|
||||||
if (!empty($_SESSION["uid"])) $this->cache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function __clone() {
|
private function __clone() {
|
||||||
@@ -22,31 +21,30 @@ class Db_Prefs {
|
|||||||
return self::$instance;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cache() {
|
private function cache_prefs() {
|
||||||
$user_id = $_SESSION["uid"];
|
if (!empty($_SESSION["uid"])) {
|
||||||
$profile = $_SESSION["profile"] ?? false;
|
$profile = $_SESSION["profile"] ?? false;
|
||||||
|
|
||||||
if (!is_numeric($profile) || !$profile || get_schema_version() < 63) $profile = null;
|
if (!is_numeric($profile) || !$profile || get_schema_version() < 63) $profile = null;
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT
|
$sth = $this->pdo->prepare("SELECT up.pref_name, pt.type_name, up.value
|
||||||
value,ttrss_prefs_types.type_name as type_name,ttrss_prefs.pref_name AS pref_name
|
FROM ttrss_user_prefs up
|
||||||
FROM
|
JOIN ttrss_prefs p ON (up.pref_name = p.pref_name)
|
||||||
ttrss_user_prefs,ttrss_prefs,ttrss_prefs_types
|
JOIN ttrss_prefs_types pt ON (p.type_id = pt.id)
|
||||||
WHERE
|
WHERE
|
||||||
|
up.pref_name NOT LIKE '_MOBILE%' AND
|
||||||
(profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND
|
(profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND
|
||||||
ttrss_prefs.pref_name NOT LIKE '_MOBILE%' AND
|
owner_uid = :uid");
|
||||||
ttrss_prefs_types.id = type_id AND
|
|
||||||
owner_uid = :uid AND
|
|
||||||
ttrss_user_prefs.pref_name = ttrss_prefs.pref_name");
|
|
||||||
|
|
||||||
$sth->execute([":profile" => $profile, ":uid" => $user_id]);
|
$sth->execute([":profile" => $profile, ":uid" => $_SESSION["uid"]]);
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
if ($user_id == $_SESSION["uid"]) {
|
$pref_name = $row["pref_name"];
|
||||||
$pref_name = $line["pref_name"];
|
|
||||||
|
|
||||||
$this->cache[$pref_name]["type"] = $line["type_name"];
|
$this->cache[$pref_name] = [
|
||||||
$this->cache[$pref_name]["value"] = $line["value"];
|
"type" => $row["type_name"],
|
||||||
|
"value" => $row["value"]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,35 +65,37 @@ class Db_Prefs {
|
|||||||
|
|
||||||
if (!is_numeric($profile) || !$profile || get_schema_version() < 63) $profile = null;
|
if (!is_numeric($profile) || !$profile || get_schema_version() < 63) $profile = null;
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT
|
$sth = $this->pdo->prepare("SELECT up.pref_name, pt.type_name, up.value
|
||||||
value,ttrss_prefs_types.type_name as type_name
|
FROM ttrss_user_prefs up
|
||||||
FROM
|
JOIN ttrss_prefs p ON (up.pref_name = p.pref_name)
|
||||||
ttrss_user_prefs,ttrss_prefs,ttrss_prefs_types
|
JOIN ttrss_prefs_types pt ON (p.type_id = pt.id)
|
||||||
WHERE
|
WHERE
|
||||||
|
up.pref_name = :pref_name AND
|
||||||
(profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND
|
(profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND
|
||||||
ttrss_user_prefs.pref_name = :pref_name AND
|
owner_uid = :uid");
|
||||||
ttrss_prefs_types.id = type_id AND
|
|
||||||
owner_uid = :uid AND
|
|
||||||
ttrss_user_prefs.pref_name = ttrss_prefs.pref_name");
|
|
||||||
$sth->execute([":uid" => $user_id, ":profile" => $profile, ":pref_name" => $pref_name]);
|
$sth->execute([":uid" => $user_id, ":profile" => $profile, ":pref_name" => $pref_name]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$value = $row["value"];
|
$value = $row["value"];
|
||||||
$type_name = $row["type_name"];
|
$type_name = $row["type_name"];
|
||||||
|
|
||||||
if ($user_id == ($_SESSION["uid"] ?? false)) {
|
if ($user_id == ($_SESSION["uid"] ?? false)) {
|
||||||
$this->cache[$pref_name]["type"] = $type_name;
|
$this->cache[$pref_name] = [
|
||||||
$this->cache[$pref_name]["value"] = $value;
|
"type" => $row["type_name"],
|
||||||
|
"value" => $row["value"]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->convert($value, $type_name);
|
return $this->convert($value, $type_name);
|
||||||
|
|
||||||
} else if ($die_on_error) {
|
} else if ($die_on_error) {
|
||||||
user_error("Fatal error, unknown preferences key: $pref_name (owner: $user_id)", E_USER_ERROR);
|
user_error("Failed retrieving preference $pref_name for user $user_id", E_USER_ERROR);
|
||||||
return null;
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
user_error("Failed retrieving preference $pref_name for user $user_id", E_USER_WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert($value, $type_name) {
|
function convert($value, $type_name) {
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ class DbUpdater {
|
|||||||
$this->need_version = (int) $need_version;
|
$this->need_version = (int) $need_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSchemaVersion() {
|
function get_schema_version() {
|
||||||
$row = $this->pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
|
$row = $this->pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
|
||||||
return (int) $row['schema_version'];
|
return (int) $row['schema_version'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUpdateRequired() {
|
function is_update_required() {
|
||||||
return $this->getSchemaVersion() < $this->need_version;
|
return $this->get_schema_version() < $this->need_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSchemaLines($version) {
|
function get_schema_lines($version) {
|
||||||
$filename = "schema/versions/".$this->db_type."/$version.sql";
|
$filename = "schema/versions/".$this->db_type."/$version.sql";
|
||||||
|
|
||||||
if (file_exists($filename)) {
|
if (file_exists($filename)) {
|
||||||
@@ -31,10 +31,10 @@ class DbUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function performUpdateTo($version, $html_output = true) {
|
function update_to($version, $html_output = true) {
|
||||||
if ($this->getSchemaVersion() == $version - 1) {
|
if ($this->get_schema_version() == $version - 1) {
|
||||||
|
|
||||||
$lines = $this->getSchemaLines($version);
|
$lines = $this->get_schema_lines($version);
|
||||||
|
|
||||||
if (is_array($lines)) {
|
if (is_array($lines)) {
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class DbUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$db_version = $this->getSchemaVersion();
|
$db_version = $this->get_schema_version();
|
||||||
|
|
||||||
if ($db_version == $version) {
|
if ($db_version == $version) {
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
class Digest
|
class Digest
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
* Send by mail a digest of last articles.
|
|
||||||
*
|
|
||||||
* @return boolean Return false if digests are not enabled.
|
|
||||||
*/
|
|
||||||
static function send_headlines_digests() {
|
static function send_headlines_digests() {
|
||||||
|
|
||||||
$user_limit = 15; // amount of users to process (e.g. emails to send out)
|
$user_limit = 15; // amount of users to process (e.g. emails to send out)
|
||||||
@@ -14,9 +8,9 @@ class Digest
|
|||||||
|
|
||||||
Debug::log("Sending digests, batch of max $user_limit users, headline limit = $limit");
|
Debug::log("Sending digests, batch of max $user_limit users, headline limit = $limit");
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$interval_qpart = "last_digest_sent < NOW() - INTERVAL '1 days'";
|
$interval_qpart = "last_digest_sent < NOW() - INTERVAL '1 days'";
|
||||||
} else /* if (DB_TYPE == "mysql") */ {
|
} else /* if (Config::get(Config::DB_TYPE) == "mysql") */ {
|
||||||
$interval_qpart = "last_digest_sent < DATE_SUB(NOW(), INTERVAL 1 DAY)";
|
$interval_qpart = "last_digest_sent < DATE_SUB(NOW(), INTERVAL 1 DAY)";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,11 +48,11 @@ class Digest
|
|||||||
|
|
||||||
$mailer = new Mailer();
|
$mailer = new Mailer();
|
||||||
|
|
||||||
//$rc = $mail->quickMail($line["email"], $line["login"], DIGEST_SUBJECT, $digest, $digest_text);
|
//$rc = $mail->quickMail($line["email"], $line["login"], Config::get(Config::DIGEST_SUBJECT), $digest, $digest_text);
|
||||||
|
|
||||||
$rc = $mailer->mail(["to_name" => $line["login"],
|
$rc = $mailer->mail(["to_name" => $line["login"],
|
||||||
"to_address" => $line["email"],
|
"to_address" => $line["email"],
|
||||||
"subject" => DIGEST_SUBJECT,
|
"subject" => Config::get(Config::DIGEST_SUBJECT),
|
||||||
"message" => $digest_text,
|
"message" => $digest_text,
|
||||||
"message_html" => $digest]);
|
"message_html" => $digest]);
|
||||||
|
|
||||||
@@ -68,7 +62,7 @@ class Digest
|
|||||||
|
|
||||||
if ($rc && $do_catchup) {
|
if ($rc && $do_catchup) {
|
||||||
Debug::log("Marking affected articles as read...");
|
Debug::log("Marking affected articles as read...");
|
||||||
Article::catchupArticlesById($affected_ids, 0, $line["id"]);
|
Article::_catchup_by_id($affected_ids, 0, $line["id"]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Debug::log("No headlines");
|
Debug::log("No headlines");
|
||||||
@@ -81,9 +75,7 @@ class Digest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug::log("All done.");
|
Debug::log("All done.");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static function prepare_headlines_digest($user_id, $days = 1, $limit = 1000) {
|
static function prepare_headlines_digest($user_id, $days = 1, $limit = 1000) {
|
||||||
@@ -99,19 +91,19 @@ class Digest
|
|||||||
|
|
||||||
$tpl->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
|
$tpl->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
|
||||||
$tpl->setVariable('CUR_TIME', date('G:i', $local_ts));
|
$tpl->setVariable('CUR_TIME', date('G:i', $local_ts));
|
||||||
$tpl->setVariable('TTRSS_HOST', SELF_URL_PATH);
|
$tpl->setVariable('TTRSS_HOST', Config::get(Config::get(Config::SELF_URL_PATH)));
|
||||||
|
|
||||||
$tpl_t->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
|
$tpl_t->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
|
||||||
$tpl_t->setVariable('CUR_TIME', date('G:i', $local_ts));
|
$tpl_t->setVariable('CUR_TIME', date('G:i', $local_ts));
|
||||||
$tpl_t->setVariable('TTRSS_HOST', SELF_URL_PATH);
|
$tpl_t->setVariable('TTRSS_HOST', Config::get(Config::get(Config::SELF_URL_PATH)));
|
||||||
|
|
||||||
$affected_ids = array();
|
$affected_ids = array();
|
||||||
|
|
||||||
$days = (int) $days;
|
$days = (int) $days;
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$interval_qpart = "ttrss_entries.date_updated > NOW() - INTERVAL '$days days'";
|
$interval_qpart = "ttrss_entries.date_updated > NOW() - INTERVAL '$days days'";
|
||||||
} else /* if (DB_TYPE == "mysql") */ {
|
} else /* if (Config::get(Config::DB_TYPE) == "mysql") */ {
|
||||||
$interval_qpart = "ttrss_entries.date_updated > DATE_SUB(NOW(), INTERVAL $days DAY)";
|
$interval_qpart = "ttrss_entries.date_updated > DATE_SUB(NOW(), INTERVAL $days DAY)";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +156,7 @@ class Digest
|
|||||||
$line['feed_title'] = $line['cat_title'] . " / " . $line['feed_title'];
|
$line['feed_title'] = $line['cat_title'] . " / " . $line['feed_title'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$article_labels = Article::get_article_labels($line["ref_id"], $user_id);
|
$article_labels = Article::_get_labels($line["ref_id"], $user_id);
|
||||||
$article_labels_formatted = "";
|
$article_labels_formatted = "";
|
||||||
|
|
||||||
if (is_array($article_labels) && count($article_labels) > 0) {
|
if (is_array($article_labels) && count($article_labels) > 0) {
|
||||||
@@ -210,5 +202,4 @@ class Digest
|
|||||||
|
|
||||||
return array($tmp, $headlines_count, $affected_ids, $tmp_t);
|
return array($tmp, $headlines_count, $affected_ids, $tmp_t);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,23 +191,23 @@ class DiskCache {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public function __construct($dir) {
|
public function __construct($dir) {
|
||||||
$this->dir = CACHE_DIR . "/" . basename(clean($dir));
|
$this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDir() {
|
public function get_dir() {
|
||||||
return $this->dir;
|
return $this->dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function makeDir() {
|
public function make_dir() {
|
||||||
if (!is_dir($this->dir)) {
|
if (!is_dir($this->dir)) {
|
||||||
return mkdir($this->dir);
|
return mkdir($this->dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isWritable($filename = "") {
|
public function is_writable($filename = "") {
|
||||||
if ($filename) {
|
if ($filename) {
|
||||||
if (file_exists($this->getFullPath($filename)))
|
if (file_exists($this->get_full_path($filename)))
|
||||||
return is_writable($this->getFullPath($filename));
|
return is_writable($this->get_full_path($filename));
|
||||||
else
|
else
|
||||||
return is_writable($this->dir);
|
return is_writable($this->dir);
|
||||||
} else {
|
} else {
|
||||||
@@ -216,44 +216,44 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function exists($filename) {
|
public function exists($filename) {
|
||||||
return file_exists($this->getFullPath($filename));
|
return file_exists($this->get_full_path($filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSize($filename) {
|
public function get_size($filename) {
|
||||||
if ($this->exists($filename))
|
if ($this->exists($filename))
|
||||||
return filesize($this->getFullPath($filename));
|
return filesize($this->get_full_path($filename));
|
||||||
else
|
else
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFullPath($filename) {
|
public function get_full_path($filename) {
|
||||||
return $this->dir . "/" . basename(clean($filename));
|
return $this->dir . "/" . basename(clean($filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function put($filename, $data) {
|
public function put($filename, $data) {
|
||||||
return file_put_contents($this->getFullPath($filename), $data);
|
return file_put_contents($this->get_full_path($filename), $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function touch($filename) {
|
public function touch($filename) {
|
||||||
return touch($this->getFullPath($filename));
|
return touch($this->get_full_path($filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get($filename) {
|
public function get($filename) {
|
||||||
if ($this->exists($filename))
|
if ($this->exists($filename))
|
||||||
return file_get_contents($this->getFullPath($filename));
|
return file_get_contents($this->get_full_path($filename));
|
||||||
else
|
else
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMimeType($filename) {
|
public function get_mime_type($filename) {
|
||||||
if ($this->exists($filename))
|
if ($this->exists($filename))
|
||||||
return mime_content_type($this->getFullPath($filename));
|
return mime_content_type($this->get_full_path($filename));
|
||||||
else
|
else
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFakeExtension($filename) {
|
public function get_fake_extension($filename) {
|
||||||
$mimetype = $this->getMimeType($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] : "";
|
||||||
@@ -262,25 +262,25 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function send($filename) {
|
public function send($filename) {
|
||||||
$fake_extension = $this->getFakeExtension($filename);
|
$fake_extension = $this->get_fake_extension($filename);
|
||||||
|
|
||||||
if ($fake_extension)
|
if ($fake_extension)
|
||||||
$fake_extension = ".$fake_extension";
|
$fake_extension = ".$fake_extension";
|
||||||
|
|
||||||
header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\"");
|
header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\"");
|
||||||
|
|
||||||
return $this->send_local_file($this->getFullPath($filename));
|
return $this->send_local_file($this->get_full_path($filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUrl($filename) {
|
public function get_url($filename) {
|
||||||
return get_self_url_prefix() . "/public.php?op=cached_url&file=" . basename($this->dir) . "/" . basename($filename);
|
return get_self_url_prefix() . "/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
|
||||||
// this is called separately after sanitize() and plugin render article hooks to allow
|
// this is called separately after sanitize() and plugin render article hooks to allow
|
||||||
// plugins work on original source URLs used before caching
|
// plugins work on original source URLs used before caching
|
||||||
// NOTE: URLs should be already absolutized because this is called after sanitize()
|
// NOTE: URLs should be already absolutized because this is called after sanitize()
|
||||||
static public function rewriteUrls($str)
|
static public function rewrite_urls($str)
|
||||||
{
|
{
|
||||||
$res = trim($str);
|
$res = trim($str);
|
||||||
if (!$res) return '';
|
if (!$res) return '';
|
||||||
@@ -301,7 +301,7 @@ class DiskCache {
|
|||||||
$cached_filename = sha1($url);
|
$cached_filename = sha1($url);
|
||||||
|
|
||||||
if ($cache->exists($cached_filename)) {
|
if ($cache->exists($cached_filename)) {
|
||||||
$url = $cache->getUrl($cached_filename);
|
$url = $cache->get_url($cached_filename);
|
||||||
|
|
||||||
$entry->setAttribute($attr, $url);
|
$entry->setAttribute($attr, $url);
|
||||||
$entry->removeAttribute("srcset");
|
$entry->removeAttribute("srcset");
|
||||||
@@ -318,7 +318,7 @@ class DiskCache {
|
|||||||
$cached_filename = sha1($matches[$i]["url"]);
|
$cached_filename = sha1($matches[$i]["url"]);
|
||||||
|
|
||||||
if ($cache->exists($cached_filename)) {
|
if ($cache->exists($cached_filename)) {
|
||||||
$matches[$i]["url"] = $cache->getUrl($cached_filename);
|
$matches[$i]["url"] = $cache->get_url($cached_filename);
|
||||||
|
|
||||||
$need_saving = true;
|
$need_saving = true;
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static function expire() {
|
static function expire() {
|
||||||
$dirs = array_filter(glob(CACHE_DIR . "/*"), "is_dir");
|
$dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir");
|
||||||
|
|
||||||
foreach ($dirs as $cache_dir) {
|
foreach ($dirs as $cache_dir) {
|
||||||
$num_deleted = 0;
|
$num_deleted = 0;
|
||||||
@@ -349,7 +349,7 @@ class DiskCache {
|
|||||||
|
|
||||||
if ($files) {
|
if ($files) {
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
if (time() - filemtime($file) > 86400*CACHE_MAX_DAYS) {
|
if (time() - filemtime($file) > 86400*Config::get(Config::CACHE_MAX_DAYS)) {
|
||||||
unlink($file);
|
unlink($file);
|
||||||
|
|
||||||
++$num_deleted;
|
++$num_deleted;
|
||||||
@@ -396,7 +396,7 @@ class DiskCache {
|
|||||||
|
|
||||||
$tmppluginhost = new PluginHost();
|
$tmppluginhost = new PluginHost();
|
||||||
|
|
||||||
$tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
|
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_SYSTEM);
|
||||||
//$tmppluginhost->load_data();
|
//$tmppluginhost->load_data();
|
||||||
|
|
||||||
if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename))
|
if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename))
|
||||||
|
|||||||
12
classes/errors.php
Normal file
12
classes/errors.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
class Errors {
|
||||||
|
const E_SUCCESS = "E_SUCCESS";
|
||||||
|
const E_UNAUTHORIZED = "E_UNAUTHORIZED";
|
||||||
|
const E_UNKNOWN_METHOD = "E_UNKNOWN_METHOD";
|
||||||
|
const E_UNKNOWN_PLUGIN = "E_UNKNOWN_PLUGIN";
|
||||||
|
const E_SCHEMA_MISMATCH = "E_SCHEMA_MISMATCH";
|
||||||
|
|
||||||
|
static function to_json(string $code) {
|
||||||
|
return json_encode(["error" => ["code" => $code]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ abstract class FeedItem {
|
|||||||
abstract function get_comments_url();
|
abstract function get_comments_url();
|
||||||
abstract function get_comments_count();
|
abstract function get_comments_count();
|
||||||
abstract function get_categories();
|
abstract function get_categories();
|
||||||
abstract function get_enclosures();
|
abstract function _get_enclosures();
|
||||||
abstract function get_author();
|
abstract function get_author();
|
||||||
abstract function get_language();
|
abstract function get_language();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
return $this->normalize_categories($cats);
|
return $this->normalize_categories($cats);
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_enclosures() {
|
function _get_enclosures() {
|
||||||
$links = $this->elem->getElementsByTagName("link");
|
$links = $this->elem->getElementsByTagName("link");
|
||||||
|
|
||||||
$encs = array();
|
$encs = array();
|
||||||
@@ -138,7 +138,7 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$encs = array_merge($encs, parent::get_enclosures());
|
$encs = array_merge($encs, parent::_get_enclosures());
|
||||||
|
|
||||||
return $encs;
|
return $encs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this is common for both Atom and RSS types and deals with various media: elements
|
// this is common for both Atom and RSS types and deals with various media: elements
|
||||||
function get_enclosures() {
|
function _get_enclosures() {
|
||||||
$encs = [];
|
$encs = [];
|
||||||
|
|
||||||
$enclosures = $this->xpath->query("media:content", $this->elem);
|
$enclosures = $this->xpath->query("media:content", $this->elem);
|
||||||
@@ -179,7 +179,7 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
|
|
||||||
$cat = preg_replace('/[,\'\"]/', "", $cat);
|
$cat = preg_replace('/[,\'\"]/', "", $cat);
|
||||||
|
|
||||||
if (DB_TYPE == "mysql") {
|
if (Config::get(Config::DB_TYPE) == "mysql") {
|
||||||
$cat = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $cat);
|
$cat = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $cat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class FeedItem_RSS extends FeedItem_Common {
|
|||||||
return $this->normalize_categories($cats);
|
return $this->normalize_categories($cats);
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_enclosures() {
|
function _get_enclosures() {
|
||||||
$enclosures = $this->elem->getElementsByTagName("enclosure");
|
$enclosures = $this->elem->getElementsByTagName("enclosure");
|
||||||
|
|
||||||
$encs = array();
|
$encs = array();
|
||||||
@@ -129,7 +129,7 @@ class FeedItem_RSS extends FeedItem_Common {
|
|||||||
array_push($encs, $enc);
|
array_push($encs, $enc);
|
||||||
}
|
}
|
||||||
|
|
||||||
$encs = array_merge($encs, parent::get_enclosures());
|
$encs = array_merge($encs, parent::_get_enclosures());
|
||||||
|
|
||||||
return $encs;
|
return $encs;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
11
classes/handler/administrative.php
Normal file
11
classes/handler/administrative.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
class Handler_Administrative extends Handler_Protected {
|
||||||
|
function before($method) {
|
||||||
|
if (parent::before($method)) {
|
||||||
|
if (($_SESSION["access_level"] ?? 0) >= 10) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
class Handler_Protected extends Handler {
|
class Handler_Protected extends Handler {
|
||||||
|
|
||||||
function before($method) {
|
function before($method) {
|
||||||
return parent::before($method) && $_SESSION['uid'];
|
return parent::before($method) && !empty($_SESSION['uid']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
if (!$limit) $limit = 60;
|
if (!$limit) $limit = 60;
|
||||||
|
|
||||||
list($override_order, $skip_first_id_check) = Feeds::order_to_override_query($order);
|
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query($order);
|
||||||
|
|
||||||
if (!$override_order) {
|
if (!$override_order) {
|
||||||
$override_order = "date_entered DESC, updated DESC";
|
$override_order = "date_entered DESC, updated DESC";
|
||||||
@@ -43,7 +43,7 @@ class Handler_Public extends Handler {
|
|||||||
$user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
|
$user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
|
||||||
|
|
||||||
$tmppluginhost = new PluginHost();
|
$tmppluginhost = new PluginHost();
|
||||||
$tmppluginhost->load(PLUGINS, PluginHost::KIND_ALL);
|
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
|
||||||
$tmppluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
|
$tmppluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
|
||||||
//$tmppluginhost->load_data();
|
//$tmppluginhost->load_data();
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ class Handler_Public extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$qfh_ret = Feeds::queryFeedHeadlines($params);
|
$qfh_ret = Feeds::_get_headlines($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $qfh_ret[0];
|
$result = $qfh_ret[0];
|
||||||
@@ -65,7 +65,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
$feed_self_url = get_self_url_prefix() .
|
$feed_self_url = get_self_url_prefix() .
|
||||||
"/public.php?op=rss&id=$feed&key=" .
|
"/public.php?op=rss&id=$feed&key=" .
|
||||||
Feeds::get_feed_access_key($feed, false, $owner_uid);
|
Feeds::_get_access_key($feed, false, $owner_uid);
|
||||||
|
|
||||||
if (!$feed_site_url) $feed_site_url = get_self_url_prefix();
|
if (!$feed_site_url) $feed_site_url = get_self_url_prefix();
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class Handler_Public extends Handler {
|
|||||||
while ($line = $result->fetch()) {
|
while ($line = $result->fetch()) {
|
||||||
|
|
||||||
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
|
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
|
||||||
$line["tags"] = Article::get_article_tags($line["id"], $owner_uid);
|
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
|
||||||
function ($result) use (&$line) {
|
function ($result) use (&$line) {
|
||||||
@@ -98,7 +98,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
$tpl->setVariable('ARTICLE_ID',
|
$tpl->setVariable('ARTICLE_ID',
|
||||||
htmlspecialchars($orig_guid ? $line['link'] :
|
htmlspecialchars($orig_guid ? $line['link'] :
|
||||||
$this->make_article_tag_uri($line['id'], $line['date_entered'])), true);
|
$this->_make_article_tag_uri($line['id'], $line['date_entered'])), true);
|
||||||
$tpl->setVariable('ARTICLE_LINK', htmlspecialchars($line['link']), true);
|
$tpl->setVariable('ARTICLE_LINK', htmlspecialchars($line['link']), true);
|
||||||
$tpl->setVariable('ARTICLE_TITLE', htmlspecialchars($line['title']), true);
|
$tpl->setVariable('ARTICLE_TITLE', htmlspecialchars($line['title']), true);
|
||||||
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
|
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
|
||||||
@@ -106,7 +106,7 @@ class Handler_Public extends Handler {
|
|||||||
$content = Sanitizer::sanitize($line["content"], false, $owner_uid,
|
$content = Sanitizer::sanitize($line["content"], false, $owner_uid,
|
||||||
$feed_site_url, false, $line["id"]);
|
$feed_site_url, false, $line["id"]);
|
||||||
|
|
||||||
$content = DiskCache::rewriteUrls($content);
|
$content = DiskCache::rewrite_urls($content);
|
||||||
|
|
||||||
if ($line['note']) {
|
if ($line['note']) {
|
||||||
$content = "<div style=\"$note_style\">Article note: " . $line['note'] . "</div>" .
|
$content = "<div style=\"$note_style\">Article note: " . $line['note'] . "</div>" .
|
||||||
@@ -131,7 +131,7 @@ class Handler_Public extends Handler {
|
|||||||
$tpl->addBlock('category');
|
$tpl->addBlock('category');
|
||||||
}
|
}
|
||||||
|
|
||||||
$enclosures = Article::get_article_enclosures($line["id"]);
|
$enclosures = Article::_get_enclosures($line["id"]);
|
||||||
|
|
||||||
if (count($enclosures) > 0) {
|
if (count($enclosures) > 0) {
|
||||||
foreach ($enclosures as $e) {
|
foreach ($enclosures as $e) {
|
||||||
@@ -146,12 +146,12 @@ class Handler_Public extends Handler {
|
|||||||
$tpl->addBlock('enclosure');
|
$tpl->addBlock('enclosure');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$tpl->setVariable('ARTICLE_ENCLOSURE_URL', null, true);
|
$tpl->setVariable('ARTICLE_ENCLOSURE_URL', "", true);
|
||||||
$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', null, true);
|
$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', "", true);
|
||||||
$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', null, true);
|
$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', "", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
list ($og_image, $og_stream) = Article::get_article_image($enclosures, $line['content'], $feed_site_url);
|
list ($og_image, $og_stream) = Article::_get_image($enclosures, $line['content'], $feed_site_url);
|
||||||
|
|
||||||
$tpl->setVariable('ARTICLE_OG_IMAGE', $og_image, true);
|
$tpl->setVariable('ARTICLE_OG_IMAGE', $og_image, true);
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ class Handler_Public extends Handler {
|
|||||||
$tpl->addBlock('feed');
|
$tpl->addBlock('feed');
|
||||||
$tpl->generateOutputToString($tmp);
|
$tpl->generateOutputToString($tmp);
|
||||||
|
|
||||||
if (@!clean($_REQUEST["noxml"])) {
|
if (empty($_REQUEST["noxml"])) {
|
||||||
header("Content-Type: text/xml; charset=utf-8");
|
header("Content-Type: text/xml; charset=utf-8");
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/plain; charset=utf-8");
|
header("Content-Type: text/plain; charset=utf-8");
|
||||||
@@ -184,7 +184,7 @@ class Handler_Public extends Handler {
|
|||||||
while ($line = $result->fetch()) {
|
while ($line = $result->fetch()) {
|
||||||
|
|
||||||
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
|
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
|
||||||
$line["tags"] = Article::get_article_tags($line["id"], $owner_uid);
|
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
|
||||||
function ($result) use (&$line) {
|
function ($result) use (&$line) {
|
||||||
@@ -207,8 +207,8 @@ class Handler_Public extends Handler {
|
|||||||
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
|
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
|
||||||
$article['updated'] = date('c', strtotime($line["updated"]));
|
$article['updated'] = date('c', strtotime($line["updated"]));
|
||||||
|
|
||||||
if ($line['note']) $article['note'] = $line['note'];
|
if (!empty($line['note'])) $article['note'] = $line['note'];
|
||||||
if ($article['author']) $article['author'] = $line['author'];
|
if (!empty($line['author'])) $article['author'] = $line['author'];
|
||||||
|
|
||||||
if (count($line["tags"]) > 0) {
|
if (count($line["tags"]) > 0) {
|
||||||
$article['tags'] = array();
|
$article['tags'] = array();
|
||||||
@@ -218,7 +218,7 @@ class Handler_Public extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$enclosures = Article::get_article_enclosures($line["id"]);
|
$enclosures = Article::_get_enclosures($line["id"]);
|
||||||
|
|
||||||
if (count($enclosures) > 0) {
|
if (count($enclosures) > 0) {
|
||||||
$article['enclosures'] = array();
|
$article['enclosures'] = array();
|
||||||
@@ -240,7 +240,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/plain; charset=utf-8");
|
header("Content-Type: text/plain; charset=utf-8");
|
||||||
print json_encode(array("error" => array("message" => "Unknown format")));
|
print "Unknown format: $format.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,11 +251,11 @@ class Handler_Public extends Handler {
|
|||||||
$uid = UserHelper::find_user_by_login($login);
|
$uid = UserHelper::find_user_by_login($login);
|
||||||
|
|
||||||
if ($uid) {
|
if ($uid) {
|
||||||
print Feeds::getGlobalUnread($uid);
|
print Feeds::_get_global_unread($uid);
|
||||||
|
|
||||||
if ($fresh) {
|
if ($fresh) {
|
||||||
print ";";
|
print ";";
|
||||||
print Feeds::getFeedArticles(-3, false, true, $uid);
|
print Feeds::_get_counters(-3, false, true, $uid);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print "-1;User not found";
|
print "-1;User not found";
|
||||||
@@ -286,195 +286,30 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
if (validate_csrf($_POST["csrf_token"])) {
|
if (validate_csrf($_POST["csrf_token"])) {
|
||||||
Pref_Users::logout_user();
|
UserHelper::logout();
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(6);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function share() {
|
|
||||||
$uuid = clean($_REQUEST["key"]);
|
|
||||||
|
|
||||||
if ($uuid) {
|
|
||||||
$sth = $this->pdo->prepare("SELECT ref_id, owner_uid
|
|
||||||
FROM ttrss_user_entries WHERE uuid = ?");
|
|
||||||
$sth->execute([$uuid]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
|
||||||
header("Content-Type: text/html");
|
|
||||||
|
|
||||||
$id = $row["ref_id"];
|
|
||||||
$owner_uid = $row["owner_uid"];
|
|
||||||
|
|
||||||
print $this->format_article($id, $owner_uid);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
|
||||||
print "Article not found.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private function format_article($id, $owner_uid) {
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT id,title,link,content,feed_id,comments,int_id,lang,
|
|
||||||
".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
|
|
||||||
(SELECT site_url FROM ttrss_feeds WHERE id = feed_id) as site_url,
|
|
||||||
(SELECT title FROM ttrss_feeds WHERE id = feed_id) as feed_title,
|
|
||||||
(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) as hide_images,
|
|
||||||
(SELECT always_display_enclosures FROM ttrss_feeds WHERE id = feed_id) as always_display_enclosures,
|
|
||||||
num_comments,
|
|
||||||
tag_cache,
|
|
||||||
author,
|
|
||||||
guid,
|
|
||||||
note
|
|
||||||
FROM ttrss_entries,ttrss_user_entries
|
|
||||||
WHERE id = ? AND ref_id = id AND owner_uid = ?");
|
|
||||||
$sth->execute([$id, $owner_uid]);
|
|
||||||
|
|
||||||
$rv = '';
|
|
||||||
|
|
||||||
if ($line = $sth->fetch()) {
|
|
||||||
|
|
||||||
$line["tags"] = Article::get_article_tags($id, $owner_uid, $line["tag_cache"]);
|
|
||||||
unset($line["tag_cache"]);
|
|
||||||
|
|
||||||
$line["content"] = Sanitizer::sanitize($line["content"],
|
|
||||||
$line['hide_images'],
|
|
||||||
$owner_uid, $line["site_url"], false, $line["id"]);
|
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE,
|
|
||||||
function ($result) use (&$line) {
|
|
||||||
$line = $result;
|
|
||||||
},
|
|
||||||
$line);
|
|
||||||
|
|
||||||
$line['content'] = DiskCache::rewriteUrls($line['content']);
|
|
||||||
|
|
||||||
$enclosures = Article::get_article_enclosures($line["id"]);
|
|
||||||
|
|
||||||
header("Content-Type: text/html");
|
|
||||||
|
|
||||||
$rv .= "<!DOCTYPE html>
|
|
||||||
<html><head>
|
|
||||||
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
|
|
||||||
<title>".$line["title"]."</title>".
|
|
||||||
javascript_tag("lib/prototype.js").
|
|
||||||
javascript_tag("js/utility.js")."
|
|
||||||
<style type='text/css'>
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background : #222;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
body.css_loading * {
|
|
||||||
display : none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel='shortcut icon' type='image/png' href='images/favicon.png'>
|
|
||||||
<link rel='icon' type='image/png' sizes='72x72' href='images/favicon-72px.png'>";
|
|
||||||
|
|
||||||
$rv .= "<meta property='og:title' content=\"".htmlspecialchars(html_entity_decode($line["title"], ENT_NOQUOTES | ENT_HTML401))."\"/>\n";
|
|
||||||
$rv .= "<meta property='og:description' content=\"".
|
|
||||||
htmlspecialchars(
|
|
||||||
truncate_string(
|
|
||||||
preg_replace("/[\r\n\t]/", "",
|
|
||||||
preg_replace("/ {1,}/", " ",
|
|
||||||
strip_tags(html_entity_decode($line["content"], ENT_NOQUOTES | ENT_HTML401))
|
|
||||||
)
|
|
||||||
), 500, "...")
|
|
||||||
)."\"/>\n";
|
|
||||||
|
|
||||||
$rv .= "</head>";
|
|
||||||
|
|
||||||
list ($og_image, $og_stream) = Article::get_article_image($enclosures, $line['content'], $line["site_url"]);
|
|
||||||
|
|
||||||
if ($og_image) {
|
|
||||||
$rv .= "<meta property='og:image' content=\"" . htmlspecialchars($og_image) . "\"/>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$rv .= "<body class='flat ttrss_utility ttrss_zoom css_loading'>";
|
|
||||||
$rv .= "<div class='container'>";
|
|
||||||
|
|
||||||
if ($line["link"]) {
|
|
||||||
$rv .= "<h1><a target='_blank' rel='noopener noreferrer'
|
|
||||||
title=\"".htmlspecialchars($line['title'])."\"
|
|
||||||
href=\"" .htmlspecialchars($line["link"]) . "\">" . $line["title"] . "</a></h1>";
|
|
||||||
} else {
|
|
||||||
$rv .= "<h1>" . $line["title"] . "</h1>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$rv .= "<div class='content post'>";
|
|
||||||
|
|
||||||
/* header */
|
|
||||||
|
|
||||||
$rv .= "<div class='header'>";
|
|
||||||
$rv .= "<div class='row'>"; # row
|
|
||||||
|
|
||||||
//$entry_author = $line["author"] ? " - " . $line["author"] : "";
|
|
||||||
$parsed_updated = TimeHelper::make_local_datetime($line["updated"], true,
|
|
||||||
$owner_uid, true);
|
|
||||||
|
|
||||||
$rv .= "<div>".$line['author']."</div>";
|
|
||||||
$rv .= "<div>$parsed_updated</div>";
|
|
||||||
|
|
||||||
$rv .= "</div>"; # row
|
|
||||||
|
|
||||||
$rv .= "</div>"; # header
|
|
||||||
|
|
||||||
/* content */
|
|
||||||
|
|
||||||
$lang = $line['lang'] ? $line['lang'] : "en";
|
|
||||||
$rv .= "<div class='content' lang='$lang'>";
|
|
||||||
|
|
||||||
/* content body */
|
|
||||||
|
|
||||||
$rv .= $line["content"];
|
|
||||||
|
|
||||||
$rv .= Article::format_article_enclosures($id,
|
|
||||||
$line["always_display_enclosures"],
|
|
||||||
$line["content"],
|
|
||||||
$line["hide_images"]);
|
|
||||||
|
|
||||||
$rv .= "</div>"; # content
|
|
||||||
|
|
||||||
$rv .= "</div>"; # post
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_FORMAT_ARTICLE,
|
|
||||||
function ($result) use (&$rv) {
|
|
||||||
$rv = $result;
|
|
||||||
},
|
|
||||||
$rv, $line);
|
|
||||||
|
|
||||||
return $rv;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function rss() {
|
function rss() {
|
||||||
$feed = clean($_REQUEST["id"]);
|
$feed = clean($_REQUEST["id"]);
|
||||||
$key = clean($_REQUEST["key"]);
|
$key = clean($_REQUEST["key"]);
|
||||||
$is_cat = clean($_REQUEST["is_cat"]);
|
$is_cat = clean($_REQUEST["is_cat"] ?? false);
|
||||||
$limit = (int)clean($_REQUEST["limit"]);
|
$limit = (int)clean($_REQUEST["limit"] ?? 0);
|
||||||
$offset = (int)clean($_REQUEST["offset"]);
|
$offset = (int)clean($_REQUEST["offset"] ?? 0);
|
||||||
|
|
||||||
$search = clean($_REQUEST["q"]);
|
$search = clean($_REQUEST["q"] ?? "");
|
||||||
$view_mode = clean($_REQUEST["view-mode"]);
|
$view_mode = clean($_REQUEST["view-mode"] ?? "");
|
||||||
$order = clean($_REQUEST["order"]);
|
$order = clean($_REQUEST["order"] ?? "");
|
||||||
$start_ts = clean($_REQUEST["ts"]);
|
$start_ts = (int)clean($_REQUEST["ts"] ?? 0);
|
||||||
|
|
||||||
$format = clean($_REQUEST['format']);
|
$format = clean($_REQUEST['format'] ?? "atom");
|
||||||
$orig_guid = clean($_REQUEST["orig_guid"]);
|
$orig_guid = clean($_REQUEST["orig_guid"] ?? false);
|
||||||
|
|
||||||
if (!$format) $format = 'atom';
|
if (Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
|
|
||||||
if (SINGLE_USER_MODE) {
|
|
||||||
UserHelper::authenticate("admin", null);
|
UserHelper::authenticate("admin", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,169 +346,8 @@ class Handler_Public extends Handler {
|
|||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sharepopup() {
|
|
||||||
if (SINGLE_USER_MODE) {
|
|
||||||
UserHelper::login_sequence();
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title><?php echo __("Share with Tiny Tiny RSS") ?></title>
|
|
||||||
<?php
|
|
||||||
echo javascript_tag("lib/prototype.js");
|
|
||||||
echo javascript_tag("lib/dojo/dojo.js");
|
|
||||||
echo javascript_tag("js/utility.js");
|
|
||||||
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
|
||||||
echo javascript_tag("lib/scriptaculous/scriptaculous.js?load=effects,controls")
|
|
||||||
?>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
|
|
||||||
<style type="text/css">
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background : #303030;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.css_loading * {
|
|
||||||
display : none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class='flat ttrss_utility share_popup css_loading'>
|
|
||||||
<script type="text/javascript">
|
|
||||||
const UtilityApp = {
|
|
||||||
init: function() {
|
|
||||||
require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
|
|
||||||
'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'], function(parser, ready){
|
|
||||||
ready(function() {
|
|
||||||
parser.parse();
|
|
||||||
|
|
||||||
new Ajax.Autocompleter('labels_value', 'labels_choices',
|
|
||||||
"backend.php?op=rpc&method=completeLabels",
|
|
||||||
{ tokens: ',', paramName: "search" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<div class="content">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$action = clean($_REQUEST["action"]);
|
|
||||||
|
|
||||||
if ($_SESSION["uid"]) {
|
|
||||||
|
|
||||||
if ($action == 'share') {
|
|
||||||
|
|
||||||
$title = strip_tags(clean($_REQUEST["title"]));
|
|
||||||
$url = strip_tags(clean($_REQUEST["url"]));
|
|
||||||
$content = strip_tags(clean($_REQUEST["content"]));
|
|
||||||
$labels = strip_tags(clean($_REQUEST["labels"]));
|
|
||||||
|
|
||||||
Article::create_published_article($title, $url, $content, $labels,
|
|
||||||
$_SESSION["uid"]);
|
|
||||||
|
|
||||||
print "<script type='text/javascript'>";
|
|
||||||
print "window.close();";
|
|
||||||
print "</script>";
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$title = htmlspecialchars(clean($_REQUEST["title"]));
|
|
||||||
$url = htmlspecialchars(clean($_REQUEST["url"]));
|
|
||||||
|
|
||||||
?>
|
|
||||||
<form id='share_form' name='share_form'>
|
|
||||||
|
|
||||||
<input type="hidden" name="op" value="sharepopup">
|
|
||||||
<input type="hidden" name="action" value="share">
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label><?php echo __("Title:") ?></label>
|
|
||||||
<input style='width : 270px' dojoType='dijit.form.TextBox' name='title' value="<?php echo $title ?>">
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label><?php echo __("URL:") ?></label>
|
|
||||||
<input style='width : 270px' name='url' dojoType='dijit.form.TextBox' value="<?php echo $url ?>">
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label><?php echo __("Content:") ?></label>
|
|
||||||
<input style='width : 270px' name='content' dojoType='dijit.form.TextBox' value="">
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label><?php echo __("Labels:") ?></label>
|
|
||||||
<input style='width : 270px' name='labels' dojoType='dijit.form.TextBox' id="labels_value"
|
|
||||||
placeholder='Alpha, Beta, Gamma' value="">
|
|
||||||
<div class="autocomplete" id="labels_choices"
|
|
||||||
style="display : block"></div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<button dojoType='dijit.form.Button' class="alt-primary" type="submit"><?php echo __('Share') ?></button>
|
|
||||||
<button dojoType='dijit.form.Button' onclick="return window.close()"><?php echo __('Cancel') ?></button>
|
|
||||||
<span class="text-muted small"><?php echo __("Shared article will appear in the Published feed.") ?></span>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
$return = urlencode(make_self_url());
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php print_error("Not logged in"); ?>
|
|
||||||
|
|
||||||
<form action="public.php?return=<?php echo $return ?>" method="post">
|
|
||||||
|
|
||||||
<input type="hidden" name="op" value="login">
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label><?php echo __("Login:") ?></label>
|
|
||||||
<input name="login" id="login" dojoType="dijit.form.TextBox" type="text"
|
|
||||||
onchange="fetchProfiles()" onfocus="fetchProfiles()" onblur="fetchProfiles()"
|
|
||||||
required="1" value="<?php echo $_SESSION["fake_login"] ?>" />
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label><?php echo __("Password:") ?></label>
|
|
||||||
|
|
||||||
<input type="password" name="password" required="1"
|
|
||||||
dojoType="dijit.form.TextBox"
|
|
||||||
class="input input-text"
|
|
||||||
value="<?php echo $_SESSION["fake_password"] ?>"/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label> </label>
|
|
||||||
|
|
||||||
<button dojoType="dijit.form.Button" type="submit" class="alt-primary"><?php echo __('Log in') ?></button>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</div></body></html>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function login() {
|
function login() {
|
||||||
if (!SINGLE_USER_MODE) {
|
if (!Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
|
|
||||||
$login = clean($_POST["login"]);
|
$login = clean($_POST["login"]);
|
||||||
$password = clean($_POST["password"]);
|
$password = clean($_POST["password"]);
|
||||||
@@ -681,7 +355,7 @@ class Handler_Public extends Handler {
|
|||||||
$safe_mode = checkbox_to_sql_bool(clean($_POST["safe_mode"] ?? false));
|
$safe_mode = checkbox_to_sql_bool(clean($_POST["safe_mode"] ?? false));
|
||||||
|
|
||||||
if ($remember_me) {
|
if ($remember_me) {
|
||||||
@session_set_cookie_params(SESSION_COOKIE_LIFETIME);
|
@session_set_cookie_params(Config::get(Config::SESSION_COOKIE_LIFETIME));
|
||||||
} else {
|
} else {
|
||||||
@session_set_cookie_params(0);
|
@session_set_cookie_params(0);
|
||||||
}
|
}
|
||||||
@@ -724,7 +398,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
$return = clean($_REQUEST['return']);
|
$return = clean($_REQUEST['return']);
|
||||||
|
|
||||||
if ($_REQUEST['return'] && mb_strpos($return, SELF_URL_PATH) === 0) {
|
if ($_REQUEST['return'] && mb_strpos($return, Config::get(Config::SELF_URL_PATH)) === 0) {
|
||||||
header("Location: " . clean($_REQUEST['return']));
|
header("Location: " . clean($_REQUEST['return']));
|
||||||
} else {
|
} else {
|
||||||
header("Location: " . get_self_url_prefix());
|
header("Location: " . get_self_url_prefix());
|
||||||
@@ -732,164 +406,9 @@ class Handler_Public extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribe() {
|
|
||||||
if (SINGLE_USER_MODE) {
|
|
||||||
UserHelper::login_sequence();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"])) {
|
|
||||||
|
|
||||||
$feed_url = clean($_REQUEST["feed_url"] ?? "");
|
|
||||||
$csrf_token = clean($_POST["csrf_token"] ?? "");
|
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Tiny Tiny RSS</title>
|
|
||||||
<?php
|
|
||||||
echo javascript_tag("lib/prototype.js");
|
|
||||||
echo javascript_tag("js/utility.js");
|
|
||||||
echo javascript_tag("lib/dojo/dojo.js");
|
|
||||||
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
|
||||||
?>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
|
|
||||||
<style type="text/css">
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background : #303030;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.css_loading * {
|
|
||||||
display : none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class='flat ttrss_utility css_loading'>
|
|
||||||
<script type="text/javascript">
|
|
||||||
const UtilityApp = {
|
|
||||||
init: function() {
|
|
||||||
require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
|
|
||||||
'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'], function(parser, ready){
|
|
||||||
ready(function() {
|
|
||||||
parser.parse();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<div class="container">
|
|
||||||
<h1><?php echo __("Subscribe to feed...") ?></h1>
|
|
||||||
<div class='content'>
|
|
||||||
<?php
|
|
||||||
|
|
||||||
if (!$feed_url || !validate_csrf($csrf_token)) {
|
|
||||||
?>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="op" value="subscribe">
|
|
||||||
<?php print_hidden("csrf_token", $_SESSION["csrf_token"]) ?>
|
|
||||||
<fieldset>
|
|
||||||
<label>Feed or site URL:</label>
|
|
||||||
<input style="width: 300px" dojoType="dijit.form.ValidationTextBox" required="1" name="feed_url" value="<?php echo htmlspecialchars($feed_url) ?>">
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<button class="alt-primary" dojoType="dijit.form.Button" type="submit">
|
|
||||||
<?php echo __("Subscribe") ?>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="index.php"><?php echo __("Return to Tiny Tiny RSS") ?></a>
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
} else {
|
|
||||||
|
|
||||||
$rc = Feeds::subscribe_to_feed($feed_url);
|
|
||||||
$feed_urls = false;
|
|
||||||
|
|
||||||
switch ($rc['code']) {
|
|
||||||
case 0:
|
|
||||||
print_warning(T_sprintf("Already subscribed to <b>%s</b>.", $feed_url));
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
print_notice(T_sprintf("Subscribed to <b>%s</b>.", $feed_url));
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
print_error(T_sprintf("Could not subscribe to <b>%s</b>.", $feed_url));
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
print_error(T_sprintf("No feeds found in <b>%s</b>.", $feed_url));
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
$feed_urls = $rc["feeds"];
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
print_error(T_sprintf("Could not subscribe to <b>%s</b>.<br>Can't download the Feed URL.", $feed_url));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($feed_urls) {
|
|
||||||
|
|
||||||
print "<form action='public.php'>";
|
|
||||||
print "<input type='hidden' name='op' value='subscribe'>";
|
|
||||||
print_hidden("csrf_token", $_SESSION["csrf_token"]);
|
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
print "<label style='display : inline'>" . __("Multiple feed URLs found:") . "</label>";
|
|
||||||
print "<select name='feed_url' dojoType='dijit.form.Select'>";
|
|
||||||
|
|
||||||
foreach ($feed_urls as $url => $name) {
|
|
||||||
$url = htmlspecialchars($url);
|
|
||||||
$name = htmlspecialchars($name);
|
|
||||||
|
|
||||||
print "<option value=\"$url\">$name</option>";
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</select>";
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "<button class='alt-primary' dojoType='dijit.form.Button' type='submit'>".__("Subscribe to selected feed")."</button>";
|
|
||||||
print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
|
|
||||||
|
|
||||||
print "</form>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$tp_uri = get_self_url_prefix() . "/prefs.php";
|
|
||||||
|
|
||||||
if ($rc['code'] <= 2){
|
|
||||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
|
|
||||||
feed_url = ? AND owner_uid = ?");
|
|
||||||
$sth->execute([$feed_url, $_SESSION['uid']]);
|
|
||||||
$row = $sth->fetch();
|
|
||||||
|
|
||||||
$feed_id = $row["id"];
|
|
||||||
} else {
|
|
||||||
$feed_id = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($feed_id) {
|
|
||||||
print "<form method='GET' action=\"$tp_uri\">
|
|
||||||
<input type='hidden' name='tab' value='feeds'>
|
|
||||||
<input type='hidden' name='method' value='editfeed'>
|
|
||||||
<input type='hidden' name='methodparam' value='$feed_id'>
|
|
||||||
<button dojoType='dijit.form.Button' class='alt-info' type='submit'>".__("Edit subscription options")."</button>
|
|
||||||
<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>
|
|
||||||
</form>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</div></div></body></html>";
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$this->render_login_form();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function index() {
|
function index() {
|
||||||
header("Content-Type: text/plain");
|
header("Content-Type: text/plain");
|
||||||
print error_json(13);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
|
||||||
}
|
}
|
||||||
|
|
||||||
function forgotpass() {
|
function forgotpass() {
|
||||||
@@ -909,7 +428,6 @@ class Handler_Public extends Handler {
|
|||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<?php
|
<?php
|
||||||
echo stylesheet_tag("themes/light.css");
|
echo stylesheet_tag("themes/light.css");
|
||||||
echo javascript_tag("lib/prototype.js");
|
|
||||||
echo javascript_tag("lib/dojo/dojo.js");
|
echo javascript_tag("lib/dojo/dojo.js");
|
||||||
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
||||||
?>
|
?>
|
||||||
@@ -953,7 +471,7 @@ class Handler_Public extends Handler {
|
|||||||
WHERE id = ?");
|
WHERE id = ?");
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
Pref_Users::resetUserPassword($id, true);
|
UserHelper::reset_password($id, true);
|
||||||
|
|
||||||
print "<p>"."Completed."."</p>";
|
print "<p>"."Completed."."</p>";
|
||||||
|
|
||||||
@@ -1041,7 +559,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
$tpl->setVariable('LOGIN', $login);
|
$tpl->setVariable('LOGIN', $login);
|
||||||
$tpl->setVariable('RESETPASS_LINK', $resetpass_link);
|
$tpl->setVariable('RESETPASS_LINK', $resetpass_link);
|
||||||
$tpl->setVariable('TTRSS_HOST', SELF_URL_PATH);
|
$tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
|
||||||
|
|
||||||
$tpl->addBlock('message');
|
$tpl->addBlock('message');
|
||||||
|
|
||||||
@@ -1095,9 +613,9 @@ class Handler_Public extends Handler {
|
|||||||
function dbupdate() {
|
function dbupdate() {
|
||||||
startup_gettext();
|
startup_gettext();
|
||||||
|
|
||||||
if (!SINGLE_USER_MODE && $_SESSION["access_level"] < 10) {
|
if (!Config::get(Config::SINGLE_USER_MODE) && $_SESSION["access_level"] < 10) {
|
||||||
$_SESSION["login_error_msg"] = __("Your access level is insufficient to run this script.");
|
$_SESSION["login_error_msg"] = __("Your access level is insufficient to run this script.");
|
||||||
$this->render_login_form();
|
$this->_render_login_form();
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,12 +625,11 @@ class Handler_Public extends Handler {
|
|||||||
<head>
|
<head>
|
||||||
<title>Database Updater</title>
|
<title>Database Updater</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<?php echo stylesheet_tag("themes/light.css") ?>
|
<?= stylesheet_tag("themes/light.css") ?>
|
||||||
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
|
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
|
||||||
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
|
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
|
||||||
<?php
|
<?php
|
||||||
echo stylesheet_tag("themes/light.css");
|
echo stylesheet_tag("themes/light.css");
|
||||||
echo javascript_tag("lib/prototype.js");
|
|
||||||
echo javascript_tag("lib/dojo/dojo.js");
|
echo javascript_tag("lib/dojo/dojo.js");
|
||||||
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
||||||
?>
|
?>
|
||||||
@@ -1137,26 +654,26 @@ class Handler_Public extends Handler {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1><?php echo __("Database Updater") ?></h1>
|
<h1><?= __("Database Updater") ?></h1>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
@$op = clean($_REQUEST["subop"]);
|
@$op = clean($_REQUEST["subop"] ?? "");
|
||||||
$updater = new DbUpdater(Db::pdo(), DB_TYPE, SCHEMA_VERSION);
|
$updater = new DbUpdater(Db::pdo(), Config::get(Config::DB_TYPE), SCHEMA_VERSION);
|
||||||
|
|
||||||
if ($op == "performupdate") {
|
if ($op == "performupdate") {
|
||||||
if ($updater->isUpdateRequired()) {
|
if ($updater->is_update_required()) {
|
||||||
|
|
||||||
print "<h2>" . T_sprintf("Performing updates to version %d", SCHEMA_VERSION) . "</h2>";
|
print "<h2>" . T_sprintf("Performing updates to version %d", SCHEMA_VERSION) . "</h2>";
|
||||||
|
|
||||||
for ($i = $updater->getSchemaVersion() + 1; $i <= SCHEMA_VERSION; $i++) {
|
for ($i = $updater->get_schema_version() + 1; $i <= SCHEMA_VERSION; $i++) {
|
||||||
print "<ul>";
|
print "<ul>";
|
||||||
|
|
||||||
print "<li class='text-info'>" . T_sprintf("Updating to version %d", $i) . "</li>";
|
print "<li class='text-info'>" . T_sprintf("Updating to version %d", $i) . "</li>";
|
||||||
|
|
||||||
print "<li>";
|
print "<li>";
|
||||||
$result = $updater->performUpdateTo($i, true);
|
$result = $updater->update_to($i, true);
|
||||||
print "</li>";
|
print "</li>";
|
||||||
|
|
||||||
if (!$result) {
|
if (!$result) {
|
||||||
@@ -1187,12 +704,12 @@ class Handler_Public extends Handler {
|
|||||||
print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
|
print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($updater->isUpdateRequired()) {
|
if ($updater->is_update_required()) {
|
||||||
|
|
||||||
print "<h2>".T_sprintf("Tiny Tiny RSS database needs update to the latest version (%d to %d).",
|
print "<h2>".T_sprintf("Tiny Tiny RSS database needs update to the latest version (%d to %d).",
|
||||||
$updater->getSchemaVersion(), SCHEMA_VERSION)."</h2>";
|
$updater->get_schema_version(), SCHEMA_VERSION)."</h2>";
|
||||||
|
|
||||||
if (DB_TYPE == "mysql") {
|
if (Config::get(Config::DB_TYPE) == "mysql") {
|
||||||
print_error("<strong>READ THIS:</strong> Due to MySQL limitations, your database is not completely protected while updating. ".
|
print_error("<strong>READ THIS:</strong> Due to MySQL limitations, your database is not completely protected while updating. ".
|
||||||
"Errors may put it in an inconsistent state requiring manual rollback. <strong>BACKUP YOUR DATABASE BEFORE CONTINUING.</strong>");
|
"Errors may put it in an inconsistent state requiring manual rollback. <strong>BACKUP YOUR DATABASE BEFORE CONTINUING.</strong>");
|
||||||
} else {
|
} else {
|
||||||
@@ -1220,7 +737,28 @@ class Handler_Public extends Handler {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
function cached_url() {
|
function publishOpml() {
|
||||||
|
$key = clean($_REQUEST["key"]);
|
||||||
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
$sth = $pdo->prepare( "SELECT owner_uid
|
||||||
|
FROM ttrss_access_keys WHERE
|
||||||
|
access_key = ? AND feed_id = 'OPML:Publish'");
|
||||||
|
$sth->execute([$key]);
|
||||||
|
|
||||||
|
if ($row = $sth->fetch()) {
|
||||||
|
$owner_uid = $row['owner_uid'];
|
||||||
|
|
||||||
|
$opml = new OPML($_REQUEST);
|
||||||
|
$opml->opml_export("published.opml", $owner_uid, true, false);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||||
|
echo "File not found.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cached() {
|
||||||
list ($cache_dir, $filename) = explode("/", $_GET["file"], 2);
|
list ($cache_dir, $filename) = explode("/", $_GET["file"], 2);
|
||||||
|
|
||||||
// we do not allow files with extensions at the moment
|
// we do not allow files with extensions at the moment
|
||||||
@@ -1236,7 +774,7 @@ class Handler_Public extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function make_article_tag_uri($id, $timestamp) {
|
private function _make_article_tag_uri($id, $timestamp) {
|
||||||
|
|
||||||
$timestamp = date("Y-m-d", strtotime($timestamp));
|
$timestamp = date("Y-m-d", strtotime($timestamp));
|
||||||
|
|
||||||
@@ -1264,21 +802,21 @@ class Handler_Public extends Handler {
|
|||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler[PUBLIC]: Requested private method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("PluginHandler[PUBLIC]: Requested private method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(6);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler[PUBLIC]: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("PluginHandler[PUBLIC]: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(13);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: text/json");
|
||||||
print error_json(14);
|
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function render_login_form() {
|
static function _render_login_form() {
|
||||||
header('Cache-Control: public');
|
header('Cache-Control: public');
|
||||||
|
|
||||||
require_once "login_form.php";
|
require_once "login_form.php";
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
interface IDb {
|
|
||||||
function connect($host, $user, $pass, $db, $port);
|
|
||||||
function escape_string($s, $strip_tags = true);
|
|
||||||
function query($query, $die_on_error = true);
|
|
||||||
function fetch_assoc($result);
|
|
||||||
function num_rows($result);
|
|
||||||
function fetch_result($result, $row, $param);
|
|
||||||
function close();
|
|
||||||
function affected_rows($result);
|
|
||||||
function last_error();
|
|
||||||
function last_query_error();
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,18 @@ class Labels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function get_all_labels($owner_uid) {
|
static function get_as_hash($owner_uid) {
|
||||||
|
$rv = [];
|
||||||
|
$labels = Labels::get_all($owner_uid);
|
||||||
|
|
||||||
|
foreach ($labels as $i => $label) {
|
||||||
|
$rv[$label["id"]] = $labels[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function get_all($owner_uid) {
|
||||||
$rv = array();
|
$rv = array();
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
@@ -46,7 +57,7 @@ class Labels
|
|||||||
WHERE owner_uid = ? ORDER BY caption");
|
WHERE owner_uid = ? ORDER BY caption");
|
||||||
$sth->execute([$owner_uid]);
|
$sth->execute([$owner_uid]);
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
array_push($rv, $line);
|
array_push($rv, $line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +71,7 @@ class Labels
|
|||||||
self::clear_cache($id);
|
self::clear_cache($id);
|
||||||
|
|
||||||
if (!$labels)
|
if (!$labels)
|
||||||
$labels = Article::get_article_labels($id);
|
$labels = Article::_get_labels($id);
|
||||||
|
|
||||||
$labels = json_encode($labels);
|
$labels = json_encode($labels);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __construct() {
|
function __construct() {
|
||||||
switch (LOG_DESTINATION) {
|
switch (Config::get(Config::LOG_DESTINATION)) {
|
||||||
case "sql":
|
case "sql":
|
||||||
$this->adapter = new Logger_SQL();
|
$this->adapter = new Logger_SQL();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ class Mailer {
|
|||||||
$subject = $params["subject"];
|
$subject = $params["subject"];
|
||||||
$message = $params["message"];
|
$message = $params["message"];
|
||||||
$message_html = $params["message_html"];
|
$message_html = $params["message_html"];
|
||||||
$from_name = $params["from_name"] ? $params["from_name"] : SMTP_FROM_NAME;
|
$from_name = $params["from_name"] ? $params["from_name"] : Config::get(Config::SMTP_FROM_NAME);
|
||||||
$from_address = $params["from_address"] ? $params["from_address"] : SMTP_FROM_ADDRESS;
|
$from_address = $params["from_address"] ? $params["from_address"] : Config::get(Config::SMTP_FROM_ADDRESS);
|
||||||
|
|
||||||
$additional_headers = $params["headers"] ? $params["headers"] : [];
|
$additional_headers = $params["headers"] ? $params["headers"] : [];
|
||||||
|
|
||||||
$from_combined = $from_name ? "$from_name <$from_address>" : $from_address;
|
$from_combined = $from_name ? "$from_name <$from_address>" : $from_address;
|
||||||
$to_combined = $to_name ? "$to_name <$to_address>" : $to_address;
|
$to_combined = $to_name ? "$to_name <$to_address>" : $to_address;
|
||||||
|
|
||||||
if (defined('_LOG_SENT_MAIL') && _LOG_SENT_MAIL)
|
if (Config::get(Config::LOG_SENT_MAIL))
|
||||||
Logger::get()->log(E_USER_NOTICE, "Sending mail from $from_combined to $to_combined [$subject]: $message");
|
Logger::get()->log(E_USER_NOTICE, "Sending mail from $from_combined to $to_combined [$subject]: $message");
|
||||||
|
|
||||||
// HOOK_SEND_MAIL plugin instructions:
|
// HOOK_SEND_MAIL plugin instructions:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class OPML extends Handler_Protected {
|
|||||||
<body class='claro ttrss_utility'>
|
<body class='claro ttrss_utility'>
|
||||||
<h1>".__('OPML Utility')."</h1><div class='content'>";
|
<h1>".__('OPML Utility')."</h1><div class='content'>";
|
||||||
|
|
||||||
Feeds::add_feed_category("Imported feeds");
|
Feeds::_add_cat("Imported feeds");
|
||||||
|
|
||||||
$this->opml_notice(__("Importing OPML..."));
|
$this->opml_notice(__("Importing OPML..."));
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ class OPML extends Handler_Protected {
|
|||||||
|
|
||||||
if (!$tmp_line["match_on"]) {
|
if (!$tmp_line["match_on"]) {
|
||||||
if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
|
if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
|
||||||
$tmp_line["feed"] = Feeds::getFeedTitle(
|
$tmp_line["feed"] = Feeds::_get_title(
|
||||||
$cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
|
$cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
|
||||||
$cat_filter);
|
$cat_filter);
|
||||||
} else {
|
} else {
|
||||||
@@ -218,13 +218,13 @@ class OPML extends Handler_Protected {
|
|||||||
if (strpos($feed_id, "CAT:") === 0) {
|
if (strpos($feed_id, "CAT:") === 0) {
|
||||||
$feed_id = (int)substr($feed_id, 4);
|
$feed_id = (int)substr($feed_id, 4);
|
||||||
if ($feed_id) {
|
if ($feed_id) {
|
||||||
array_push($match, [Feeds::getCategoryTitle($feed_id), true, false]);
|
array_push($match, [Feeds::_get_cat_title($feed_id), true, false]);
|
||||||
} else {
|
} else {
|
||||||
array_push($match, [0, true, true]);
|
array_push($match, [0, true, true]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($feed_id) {
|
if ($feed_id) {
|
||||||
array_push($match, [Feeds::getFeedTitle((int)$feed_id), false, false]);
|
array_push($match, [Feeds::_get_title((int)$feed_id), false, false]);
|
||||||
} else {
|
} else {
|
||||||
array_push($match, [0, false, true]);
|
array_push($match, [0, false, true]);
|
||||||
}
|
}
|
||||||
@@ -523,7 +523,7 @@ class OPML extends Handler_Protected {
|
|||||||
$order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue;
|
$order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue;
|
||||||
if (!$order_id) $order_id = 0;
|
if (!$order_id) $order_id = 0;
|
||||||
|
|
||||||
Feeds::add_feed_category($cat_title, $parent_id, $order_id);
|
Feeds::_add_cat($cat_title, $parent_id, $order_id);
|
||||||
$cat_id = $this->get_feed_category($cat_title, $parent_id);
|
$cat_id = $this->get_feed_category($cat_title, $parent_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +594,7 @@ class OPML extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) {
|
if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) {
|
||||||
$tmp_file = (string)tempnam(CACHE_DIR . '/upload', 'opml');
|
$tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml');
|
||||||
|
|
||||||
$result = move_uploaded_file($_FILES['opml_file']['tmp_name'],
|
$result = move_uploaded_file($_FILES['opml_file']['tmp_name'],
|
||||||
$tmp_file);
|
$tmp_file);
|
||||||
@@ -634,13 +634,10 @@ class OPML extends Handler_Protected {
|
|||||||
print "$msg<br/>";
|
print "$msg<br/>";
|
||||||
}
|
}
|
||||||
|
|
||||||
static function opml_publish_url(){
|
static function get_publish_url(){
|
||||||
|
return get_self_url_prefix() .
|
||||||
$url_path = get_self_url_prefix();
|
"/public.php?op=publishOpml&key=" .
|
||||||
$url_path .= "/opml.php?op=publish&key=" .
|
Feeds::_get_access_key('OPML:Publish', false, $_SESSION["uid"]);
|
||||||
Feeds::get_feed_access_key('OPML:Publish', false, $_SESSION["uid"]);
|
|
||||||
|
|
||||||
return $url_path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_feed_category($feed_cat, $parent_cat_id = false) {
|
function get_feed_category($feed_cat, $parent_cat_id = false) {
|
||||||
|
|||||||
@@ -54,4 +54,8 @@ abstract class Plugin {
|
|||||||
|
|
||||||
return vsprintf($this->__($msgid), $args);
|
return vsprintf($this->__($msgid), $args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function csrf_ignore($method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,23 @@ class PluginHandler extends Handler_Protected {
|
|||||||
function catchall($method) {
|
function catchall($method) {
|
||||||
$plugin_name = clean($_REQUEST["plugin"]);
|
$plugin_name = clean($_REQUEST["plugin"]);
|
||||||
$plugin = PluginHost::getInstance()->get_plugin($plugin_name);
|
$plugin = PluginHost::getInstance()->get_plugin($plugin_name);
|
||||||
|
$csrf_token = ($_POST["csrf_token"] ?? "");
|
||||||
|
|
||||||
if ($plugin) {
|
if ($plugin) {
|
||||||
if (method_exists($plugin, $method)) {
|
if (method_exists($plugin, $method)) {
|
||||||
|
if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) {
|
||||||
$plugin->$method();
|
$plugin->$method();
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("Rejected ${plugin_name}->${method}(): invalid CSRF token.", E_USER_WARNING);
|
||||||
print error_json(13);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("Rejected ${plugin_name}->${method}(): unknown method.", E_USER_WARNING);
|
||||||
print error_json(14);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user_error("Rejected ${plugin_name}->${method}(): unknown plugin.", E_USER_WARNING);
|
||||||
|
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PluginHost {
|
|||||||
private static $instance;
|
private static $instance;
|
||||||
|
|
||||||
const API_VERSION = 2;
|
const API_VERSION = 2;
|
||||||
|
const PUBLIC_METHOD_DELIMITER = "--";
|
||||||
|
|
||||||
// Hooks marked with *1 are run in global context and available
|
// Hooks marked with *1 are run in global context and available
|
||||||
// to plugins loaded in config.php only
|
// to plugins loaded in config.php only
|
||||||
@@ -47,14 +48,14 @@ class PluginHost {
|
|||||||
const HOOK_QUERY_HEADLINES = "hook_query_headlines"; // hook_query_headlines($row) (byref)
|
const HOOK_QUERY_HEADLINES = "hook_query_headlines"; // hook_query_headlines($row) (byref)
|
||||||
const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1 // GLOBAL: hook_house_keeping()
|
const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1 // GLOBAL: hook_house_keeping()
|
||||||
const HOOK_SEARCH = "hook_search"; // hook_search($query)
|
const HOOK_SEARCH = "hook_search"; // hook_search($query)
|
||||||
const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures"; // hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref)
|
const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures"; // hook__format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref)
|
||||||
const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed"; // hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref)
|
const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed"; // hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref)
|
||||||
const HOOK_HEADLINES_BEFORE = "hook_headlines_before"; // hook_headlines_before($feed, $is_cat, $qfh_ret)
|
const HOOK_HEADLINES_BEFORE = "hook_headlines_before"; // hook_headlines_before($feed, $is_cat, $qfh_ret)
|
||||||
const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure"; // hook_render_enclosure($entry, $hide_images)
|
const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure"; // hook_render_enclosure($entry, $id, $rv)
|
||||||
const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action"; // hook_article_filter_action($article, $action)
|
const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action"; // hook_article_filter_action($article, $action)
|
||||||
const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed"; // hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref)
|
const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed"; // hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref)
|
||||||
const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button"; // hook_main_toolbar_button()
|
const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button"; // hook_main_toolbar_button()
|
||||||
const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry"; // hook_enclosure_entry($row, $id) (byref)
|
const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry"; // hook_enclosure_entry($entry, $id, $rv) (byref)
|
||||||
const HOOK_FORMAT_ARTICLE = "hook_format_article"; // hook_format_article($html, $row)
|
const HOOK_FORMAT_ARTICLE = "hook_format_article"; // hook_format_article($html, $row)
|
||||||
const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm"; /* RIP */
|
const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm"; /* RIP */
|
||||||
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info"; // hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref)
|
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info"; // hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref)
|
||||||
@@ -107,8 +108,9 @@ class PluginHost {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needed for compatibility with API 2 (?)
|
||||||
function get_dbh() {
|
function get_dbh() {
|
||||||
return Db::get();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_pdo(): PDO {
|
function get_pdo(): PDO {
|
||||||
@@ -598,7 +600,7 @@ class PluginHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handled by classes/pluginhandler.php, requires valid session
|
// handled by classes/pluginhandler.php, requires valid session
|
||||||
function get_method_url(Plugin $sender, string $method, $params) {
|
function get_method_url(Plugin $sender, string $method, $params = []) {
|
||||||
return get_self_url_prefix() . "/backend.php?" .
|
return get_self_url_prefix() . "/backend.php?" .
|
||||||
http_build_query(
|
http_build_query(
|
||||||
array_merge(
|
array_merge(
|
||||||
@@ -610,16 +612,25 @@ class PluginHost {
|
|||||||
$params));
|
$params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shortcut syntax (disabled for now)
|
||||||
|
/* function get_method_url(Plugin $sender, string $method, $params) {
|
||||||
|
return get_self_url_prefix() . "/backend.php?" .
|
||||||
|
http_build_query(
|
||||||
|
array_merge(
|
||||||
|
[
|
||||||
|
"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
|
||||||
|
],
|
||||||
|
$params));
|
||||||
|
} */
|
||||||
|
|
||||||
// WARNING: endpoint in public.php, exposed to unauthenticated users
|
// WARNING: endpoint in public.php, exposed to unauthenticated users
|
||||||
function get_public_method_url(Plugin $sender, string $method, $params) {
|
function get_public_method_url(Plugin $sender, string $method, $params = []) {
|
||||||
if ($sender->is_public_method($method)) {
|
if ($sender->is_public_method($method)) {
|
||||||
return get_self_url_prefix() . "/public.php?" .
|
return get_self_url_prefix() . "/public.php?" .
|
||||||
http_build_query(
|
http_build_query(
|
||||||
array_merge(
|
array_merge(
|
||||||
[
|
[
|
||||||
"op" => "pluginhandler",
|
"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
|
||||||
"plugin" => strtolower(get_class($sender)),
|
|
||||||
"pmethod" => $method
|
|
||||||
],
|
],
|
||||||
$params));
|
$params));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -162,7 +162,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
print json_encode($rv);
|
print json_encode($rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getfilterrules_list($filter_id) {
|
private function _get_rules_list($filter_id) {
|
||||||
$sth = $this->pdo->prepare("SELECT reg_exp,
|
$sth = $this->pdo->prepare("SELECT reg_exp,
|
||||||
inverse,
|
inverse,
|
||||||
match_on,
|
match_on,
|
||||||
@@ -189,10 +189,10 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
if (strpos($feed_id, "CAT:") === 0) {
|
if (strpos($feed_id, "CAT:") === 0) {
|
||||||
$feed_id = (int)substr($feed_id, 4);
|
$feed_id = (int)substr($feed_id, 4);
|
||||||
array_push($feeds_fmt, Feeds::getCategoryTitle($feed_id));
|
array_push($feeds_fmt, Feeds::_get_cat_title($feed_id));
|
||||||
} else {
|
} else {
|
||||||
if ($feed_id)
|
if ($feed_id)
|
||||||
array_push($feeds_fmt, Feeds::getFeedTitle((int)$feed_id));
|
array_push($feeds_fmt, Feeds::_get_title((int)$feed_id));
|
||||||
else
|
else
|
||||||
array_push($feeds_fmt, __("All feeds"));
|
array_push($feeds_fmt, __("All feeds"));
|
||||||
}
|
}
|
||||||
@@ -203,9 +203,9 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
$where = $line["cat_filter"] ?
|
$where = $line["cat_filter"] ?
|
||||||
Feeds::getCategoryTitle($line["cat_id"]) :
|
Feeds::_get_cat_title($line["cat_id"]) :
|
||||||
($line["feed_id"] ?
|
($line["feed_id"] ?
|
||||||
Feeds::getFeedTitle($line["feed_id"]) : __("All feeds"));
|
Feeds::_get_title($line["feed_id"]) : __("All feeds"));
|
||||||
}
|
}
|
||||||
|
|
||||||
# $where = $line["cat_id"] . "/" . $line["feed_id"];
|
# $where = $line["cat_id"] . "/" . $line["feed_id"];
|
||||||
@@ -250,7 +250,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
$name = $this->getFilterName($line["id"]);
|
$name = $this->_get_name($line["id"]);
|
||||||
|
|
||||||
$match_ok = false;
|
$match_ok = false;
|
||||||
if ($filter_search) {
|
if ($filter_search) {
|
||||||
@@ -292,7 +292,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$filter['checkbox'] = false;
|
$filter['checkbox'] = false;
|
||||||
$filter['last_triggered'] = $line["last_triggered"] ? TimeHelper::make_local_datetime($line["last_triggered"], false) : null;
|
$filter['last_triggered'] = $line["last_triggered"] ? TimeHelper::make_local_datetime($line["last_triggered"], false) : null;
|
||||||
$filter['enabled'] = sql_bool_to_bool($line["enabled"]);
|
$filter['enabled'] = sql_bool_to_bool($line["enabled"]);
|
||||||
$filter['rules'] = $this->getfilterrules_list($line['id']);
|
$filter['rules'] = $this->_get_rules_list($line['id']);
|
||||||
|
|
||||||
if (!$filter_search || $match_ok) {
|
if (!$filter_search || $match_ok) {
|
||||||
array_push($folder['items'], $filter);
|
array_push($folder['items'], $filter);
|
||||||
@@ -319,170 +319,94 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$sth->execute([$filter_id, $_SESSION['uid']]);
|
$sth->execute([$filter_id, $_SESSION['uid']]);
|
||||||
|
|
||||||
if (empty($filter_id) || $row = $sth->fetch()) {
|
if (empty($filter_id) || $row = $sth->fetch()) {
|
||||||
|
$rv = [
|
||||||
|
"id" => $filter_id,
|
||||||
|
"enabled" => $row["enabled"] ?? true,
|
||||||
|
"match_any_rule" => $row["match_any_rule"] ?? false,
|
||||||
|
"inverse" => $row["inverse"] ?? false,
|
||||||
|
"title" => $row["title"] ?? "",
|
||||||
|
"rules" => [],
|
||||||
|
"actions" => [],
|
||||||
|
"filter_types" => [],
|
||||||
|
"action_types" => [],
|
||||||
|
"plugin_actions" => [],
|
||||||
|
"labels" => Labels::get_all($_SESSION["uid"])
|
||||||
|
];
|
||||||
|
|
||||||
$enabled = $row["enabled"] ?? true;
|
$res = $this->pdo->query("SELECT id,description
|
||||||
$match_any_rule = $row["match_any_rule"] ?? false;
|
FROM ttrss_filter_types WHERE id != 5 ORDER BY description");
|
||||||
$inverse = $row["inverse"] ?? false;
|
|
||||||
$title = htmlspecialchars($row["title"] ?? "");
|
|
||||||
|
|
||||||
print "<form onsubmit='return false'>";
|
while ($line = $res->fetch()) {
|
||||||
|
$rv["filter_types"][$line["id"]] = __($line["description"]);
|
||||||
print_hidden("op", "pref-filters");
|
|
||||||
|
|
||||||
if ($filter_id) {
|
|
||||||
print_hidden("id", "$filter_id");
|
|
||||||
print_hidden("method", "editSave");
|
|
||||||
} else {
|
|
||||||
print_hidden("method", "add");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print_hidden("csrf_token", $_SESSION['csrf_token']);
|
$res = $this->pdo->query("SELECT id,description FROM ttrss_filter_actions
|
||||||
|
ORDER BY name");
|
||||||
|
|
||||||
print "<header>".__("Caption")."</header>
|
while ($line = $res->fetch()) {
|
||||||
<section>
|
$rv["action_types"][$line["id"]] = __($line["description"]);
|
||||||
<input required='true' dojoType='dijit.form.ValidationTextBox' style='width : 20em;' name=\"title\" value=\"$title\">
|
}
|
||||||
</section>
|
|
||||||
<header class='horizontal'>".__("Match")."</header>
|
|
||||||
<section>
|
|
||||||
<div dojoType='fox.Toolbar'>
|
|
||||||
<div dojoType='fox.form.DropDownButton'>
|
|
||||||
<span>" . __('Select')."</span>
|
|
||||||
<div dojoType='dijit.Menu' style='display: none;'>
|
|
||||||
<!-- can't use App.dialogOf() here because DropDownButton is not a child of the Dialog -->
|
|
||||||
<div onclick='dijit.byId(\"filterEditDlg\").selectRules(true)'
|
|
||||||
dojoType='dijit.MenuItem'>".__('All')."</div>
|
|
||||||
<div onclick='dijit.byId(\"filterEditDlg\").selectRules(false)'
|
|
||||||
dojoType='dijit.MenuItem'>".__('None')."</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).addRule()'>".
|
|
||||||
__('Add')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).deleteRule()'>".
|
|
||||||
__('Delete')."</button>
|
|
||||||
</div>";
|
|
||||||
|
|
||||||
print "<ul id='filterDlg_Matches'>";
|
$filter_actions = PluginHost::getInstance()->get_filter_actions();
|
||||||
|
|
||||||
|
foreach ($filter_actions as $fclass => $factions) {
|
||||||
|
foreach ($factions as $faction) {
|
||||||
|
|
||||||
|
$rv["plugin_actions"][$fclass . ":" . $faction["action"]] =
|
||||||
|
$fclass . ": " . $faction["description"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($filter_id) {
|
if ($filter_id) {
|
||||||
$rules_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_rules
|
$rules_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_rules
|
||||||
WHERE filter_id = ? ORDER BY reg_exp, id");
|
WHERE filter_id = ? ORDER BY reg_exp, id");
|
||||||
$rules_sth->execute([$filter_id]);
|
$rules_sth->execute([$filter_id]);
|
||||||
|
|
||||||
while ($line = $rules_sth->fetch()) {
|
while ($rrow = $rules_sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
if ($line["match_on"]) {
|
if ($rrow["match_on"]) {
|
||||||
$line["feed_id"] = json_decode($line["match_on"], true);
|
$rrow["feed_id"] = json_decode($rrow["match_on"], true);
|
||||||
} else {
|
} else {
|
||||||
if ($line["cat_filter"]) {
|
if ($rrow["cat_filter"]) {
|
||||||
$feed_id = "CAT:" . (int)$line["cat_id"];
|
$feed_id = "CAT:" . (int)$rrow["cat_id"];
|
||||||
} else {
|
} else {
|
||||||
$feed_id = (int)$line["feed_id"];
|
$feed_id = (int)$rrow["feed_id"];
|
||||||
}
|
}
|
||||||
|
|
||||||
$line["feed_id"] = ["" . $feed_id]; // set item type to string for in_array()
|
$rrow["feed_id"] = ["" . $feed_id]; // set item type to string for in_array()
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($line["cat_filter"]);
|
unset($rrow["cat_filter"]);
|
||||||
unset($line["cat_id"]);
|
unset($rrow["cat_id"]);
|
||||||
unset($line["filter_id"]);
|
unset($rrow["filter_id"]);
|
||||||
unset($line["id"]);
|
unset($rrow["id"]);
|
||||||
if (!$line["inverse"]) unset($line["inverse"]);
|
if (!$rrow["inverse"]) unset($rrow["inverse"]);
|
||||||
unset($line["match_on"]);
|
unset($rrow["match_on"]);
|
||||||
|
|
||||||
$data = htmlspecialchars((string)json_encode($line));
|
$rrow["name"] = $this->_get_rule_name($rrow);
|
||||||
|
|
||||||
print "<li><input dojoType='dijit.form.CheckBox' type='checkbox' onclick='Lists.onRowChecked(this)'>
|
array_push($rv["rules"], $rrow);
|
||||||
<span onclick='App.dialogOf(this).editRule(this)'>".$this->getRuleName($line)."</span>".
|
|
||||||
format_hidden("rule[]", $data)."</li>";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print "</ul>
|
|
||||||
</section>";
|
|
||||||
|
|
||||||
print "<header class='horizontal'>".__("Apply actions")."</header>
|
|
||||||
<section>
|
|
||||||
<div dojoType='fox.Toolbar'>
|
|
||||||
<div dojoType='fox.form.DropDownButton'>
|
|
||||||
<span>".__('Select')."</span>
|
|
||||||
<div dojoType='dijit.Menu' style='display: none'>
|
|
||||||
<div onclick='dijit.byId(\"filterEditDlg\").selectActions(true)'
|
|
||||||
dojoType='dijit.MenuItem'>".__('All')."</div>
|
|
||||||
<div onclick='dijit.byId(\"filterEditDlg\").selectActions(false)'
|
|
||||||
dojoType='dijit.MenuItem'>".__('None')."</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).addAction()'>".
|
|
||||||
__('Add')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).deleteAction()'>".
|
|
||||||
__('Delete')."</button>
|
|
||||||
</div>";
|
|
||||||
|
|
||||||
print "<ul id='filterDlg_Actions'>";
|
|
||||||
|
|
||||||
if ($filter_id) {
|
|
||||||
$actions_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
|
$actions_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
|
||||||
WHERE filter_id = ? ORDER BY id");
|
WHERE filter_id = ? ORDER BY id");
|
||||||
$actions_sth->execute([$filter_id]);
|
$actions_sth->execute([$filter_id]);
|
||||||
|
|
||||||
while ($line = $actions_sth->fetch()) {
|
while ($arow = $actions_sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$line["action_param_label"] = $line["action_param"];
|
$arow["action_param_label"] = $arow["action_param"];
|
||||||
|
|
||||||
unset($line["filter_id"]);
|
unset($arow["filter_id"]);
|
||||||
unset($line["id"]);
|
unset($arow["id"]);
|
||||||
|
|
||||||
$data = htmlspecialchars((string)json_encode($line));
|
$arow["name"] = $this->_get_action_name($arow);
|
||||||
|
|
||||||
print "<li><input dojoType='dijit.form.CheckBox' type='checkbox' onclick='Lists.onRowChecked(this)'>
|
array_push($rv["actions"], $arow);
|
||||||
<span onclick='App.dialogOf(this).editAction(this)'>".$this->getActionName($line)."</span>".
|
}
|
||||||
format_hidden("action[]", $data)."</li>";
|
}
|
||||||
|
print json_encode($rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print "</ul>";
|
private function _get_rule_name($rule) {
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<header>".__("Options")."</header>
|
|
||||||
<section>";
|
|
||||||
|
|
||||||
print "<fieldset class='narrow'>
|
|
||||||
<label class='checkbox'>".format_checkbox('enabled', $enabled)." ".__('Enabled')."</label></fieldset>";
|
|
||||||
|
|
||||||
print "<fieldset class='narrow'>
|
|
||||||
<label class='checkbox'>".format_checkbox('match_any_rule', $match_any_rule)." ".__('Match any rule')."</label>
|
|
||||||
</fieldset>";
|
|
||||||
|
|
||||||
print "<fieldset class='narrow'><label class='checkbox'>".format_checkbox('inverse', $inverse)." ".__('Inverse matching')."</label>
|
|
||||||
</fieldset>";
|
|
||||||
|
|
||||||
print "</section>
|
|
||||||
<footer>";
|
|
||||||
|
|
||||||
if ($filter_id) {
|
|
||||||
print "<div style='float : left'>
|
|
||||||
<button dojoType='dijit.form.Button' class='alt-danger' onclick='App.dialogOf(this).removeFilter()'>".
|
|
||||||
__('Remove')."</button>
|
|
||||||
</div>
|
|
||||||
<button dojoType='dijit.form.Button' class='alt-info' onclick='App.dialogOf(this).test()'>".
|
|
||||||
__('Test')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'>".
|
|
||||||
__('Save')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>".
|
|
||||||
__('Cancel')."</button>";
|
|
||||||
} else {
|
|
||||||
print "<button dojoType='dijit.form.Button' class='alt-info' onclick='App.dialogOf(this).test()'>".
|
|
||||||
__('Test')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'>".
|
|
||||||
__('Create')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>".
|
|
||||||
__('Cancel')."</button>";
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</footer></form>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getRuleName($rule) {
|
|
||||||
if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
|
if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
|
||||||
|
|
||||||
$feeds = $rule["feed_id"];
|
$feeds = $rule["feed_id"];
|
||||||
@@ -494,10 +418,10 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
if (strpos($feed_id, "CAT:") === 0) {
|
if (strpos($feed_id, "CAT:") === 0) {
|
||||||
$feed_id = (int)substr($feed_id, 4);
|
$feed_id = (int)substr($feed_id, 4);
|
||||||
array_push($feeds_fmt, Feeds::getCategoryTitle($feed_id));
|
array_push($feeds_fmt, Feeds::_get_cat_title($feed_id));
|
||||||
} else {
|
} else {
|
||||||
if ($feed_id)
|
if ($feed_id)
|
||||||
array_push($feeds_fmt, Feeds::getFeedTitle((int)$feed_id));
|
array_push($feeds_fmt, Feeds::_get_title((int)$feed_id));
|
||||||
else
|
else
|
||||||
array_push($feeds_fmt, __("All feeds"));
|
array_push($feeds_fmt, __("All feeds"));
|
||||||
}
|
}
|
||||||
@@ -523,10 +447,10 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printRuleName() {
|
function printRuleName() {
|
||||||
print $this->getRuleName(json_decode(clean($_REQUEST["rule"]), true));
|
print $this->_get_rule_name(json_decode(clean($_REQUEST["rule"]), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getActionName($action) {
|
private function _get_action_name($action) {
|
||||||
$sth = $this->pdo->prepare("SELECT description FROM
|
$sth = $this->pdo->prepare("SELECT description FROM
|
||||||
ttrss_filter_actions WHERE id = ?");
|
ttrss_filter_actions WHERE id = ?");
|
||||||
$sth->execute([(int)$action["action_id"]]);
|
$sth->execute([(int)$action["action_id"]]);
|
||||||
@@ -561,13 +485,13 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printActionName() {
|
function printActionName() {
|
||||||
print $this->getActionName(json_decode(clean($_REQUEST["action"]), true));
|
print $this->_get_action_name(json_decode(clean($_REQUEST["action"]), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function editSave() {
|
function editSave() {
|
||||||
$filter_id = clean($_REQUEST["id"]);
|
$filter_id = clean($_REQUEST["id"]);
|
||||||
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
|
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
|
||||||
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"]));
|
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
|
||||||
$inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false));
|
$inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false));
|
||||||
$title = clean($_REQUEST["title"]);
|
$title = clean($_REQUEST["title"]);
|
||||||
|
|
||||||
@@ -581,7 +505,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
$sth->execute([$enabled, $match_any_rule, $inverse, $title, $filter_id, $_SESSION['uid']]);
|
$sth->execute([$enabled, $match_any_rule, $inverse, $title, $filter_id, $_SESSION['uid']]);
|
||||||
|
|
||||||
$this->saveRulesAndActions($filter_id);
|
$this->_save_rules_and_actions($filter_id);
|
||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
}
|
}
|
||||||
@@ -596,8 +520,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveRulesAndActions($filter_id)
|
private function _save_rules_and_actions($filter_id) {
|
||||||
{
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
|
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
|
||||||
$sth->execute([$filter_id]);
|
$sth->execute([$filter_id]);
|
||||||
@@ -675,10 +598,10 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function add () {
|
function add () {
|
||||||
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"]));
|
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
|
||||||
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"]));
|
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
|
||||||
$title = clean($_REQUEST["title"]);
|
$title = clean($_REQUEST["title"]);
|
||||||
$inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"]));
|
$inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false));
|
||||||
|
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
@@ -696,7 +619,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$filter_id = $row['id'];
|
$filter_id = $row['id'];
|
||||||
$this->saveRulesAndActions($filter_id);
|
$this->_save_rules_and_actions($filter_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
@@ -710,65 +633,50 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$filter_search = ($_SESSION["prefs_filter_search"] ?? "");
|
$filter_search = ($_SESSION["prefs_filter_search"] ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<div dojoType='dijit.layout.BorderContainer' gutters='false'>";
|
?>
|
||||||
print "<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>";
|
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
|
||||||
print "<div dojoType='fox.Toolbar'>";
|
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
|
||||||
|
<div dojoType='fox.Toolbar'>
|
||||||
|
|
||||||
print "<div style='float : right; padding-right : 4px;'>
|
<div style='float : right; padding-right : 4px;'>
|
||||||
<input dojoType=\"dijit.form.TextBox\" id=\"filter_search\" size=\"20\" type=\"search\"
|
<input dojoType="dijit.form.TextBox" id="filter_search" size="20" type="search"
|
||||||
value=\"$filter_search\">
|
value="<?= htmlspecialchars($filter_search) ?>">
|
||||||
<button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('filterTree').reload()\">".
|
<button dojoType="dijit.form.Button" onclick="dijit.byId('filterTree').reload()">
|
||||||
__('Search')."</button>
|
<?= __('Search') ?></button>
|
||||||
</div>";
|
|
||||||
|
|
||||||
print "<div dojoType=\"fox.form.DropDownButton\">".
|
|
||||||
"<span>" . __('Select')."</span>";
|
|
||||||
print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
|
|
||||||
print "<div onclick=\"dijit.byId('filterTree').model.setAllChecked(true)\"
|
|
||||||
dojoType=\"dijit.MenuItem\">".__('All')."</div>";
|
|
||||||
print "<div onclick=\"dijit.byId('filterTree').model.setAllChecked(false)\"
|
|
||||||
dojoType=\"dijit.MenuItem\">".__('None')."</div>";
|
|
||||||
print "</div></div>";
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"return Filters.edit()\">".
|
|
||||||
__('Create filter')."</button> ";
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('filterTree').joinSelectedFilters()\">".
|
|
||||||
__('Combine')."</button> ";
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('filterTree').editSelectedFilter()\">".
|
|
||||||
__('Edit')."</button> ";
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('filterTree').resetFilterOrder()\">".
|
|
||||||
__('Reset sort order')."</button> ";
|
|
||||||
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('filterTree').removeSelectedFilters()\">".
|
|
||||||
__('Remove')."</button> ";
|
|
||||||
|
|
||||||
print "</div>"; # toolbar
|
|
||||||
print "</div>"; # toolbar-frame
|
|
||||||
print "<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>";
|
|
||||||
|
|
||||||
print "<div id='filterlistLoading'>
|
|
||||||
<img src='images/indicator_tiny.gif'>".
|
|
||||||
__("Loading, please wait...")."</div>";
|
|
||||||
|
|
||||||
print "<div dojoType=\"fox.PrefFilterStore\" jsId=\"filterStore\"
|
|
||||||
url=\"backend.php?op=pref-filters&method=getfiltertree\">
|
|
||||||
</div>
|
</div>
|
||||||
<div dojoType=\"lib.CheckBoxStoreModel\" jsId=\"filterModel\" store=\"filterStore\"
|
|
||||||
query=\"{id:'root'}\" rootId=\"root\" rootLabel=\"Filters\"
|
<div dojoType="fox.form.DropDownButton">
|
||||||
childrenAttrs=\"items\" checkboxStrict=\"false\" checkboxAll=\"false\">
|
<span><?= __('Select') ?></span>
|
||||||
|
<div dojoType="dijit.Menu" style="display: none;">
|
||||||
|
<div onclick="dijit.byId('filterTree').model.setAllChecked(true)"
|
||||||
|
dojoType="dijit.MenuItem"><?= __('All') ?></div>
|
||||||
|
<div onclick="dijit.byId('filterTree').model.setAllChecked(false)"
|
||||||
|
dojoType="dijit.MenuItem"><?= __('None') ?></div>
|
||||||
</div>
|
</div>
|
||||||
<div dojoType=\"fox.PrefFilterTree\" id=\"filterTree\"
|
</div>
|
||||||
dndController=\"dijit.tree.dndSource\"
|
|
||||||
betweenThreshold=\"5\"
|
<button dojoType="dijit.form.Button" onclick="return Filters.edit()">
|
||||||
model=\"filterModel\" openOnClick=\"true\">
|
<?= __('Create filter') ?></button>
|
||||||
<script type=\"dojo/method\" event=\"onLoad\" args=\"item\">
|
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').joinSelectedFilters()">
|
||||||
Element.hide(\"filterlistLoading\");
|
<?= __('Combine') ?></button>
|
||||||
</script>
|
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').resetFilterOrder()">
|
||||||
<script type=\"dojo/method\" event=\"onClick\" args=\"item\">
|
<?= __('Reset sort order') ?></button>
|
||||||
|
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').removeSelectedFilters()">
|
||||||
|
<?= __('Remove') ?></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
|
||||||
|
<div dojoType="fox.PrefFilterStore" jsId="filterStore"
|
||||||
|
url="backend.php?op=pref-filters&method=getfiltertree">
|
||||||
|
</div>
|
||||||
|
<div dojoType="lib.CheckBoxStoreModel" jsId="filterModel" store="filterStore"
|
||||||
|
query="{id:'root'}" rootId="root" rootLabel="Filters"
|
||||||
|
childrenAttrs="items" checkboxStrict="false" checkboxAll="false">
|
||||||
|
</div>
|
||||||
|
<div dojoType="fox.PrefFilterTree" id="filterTree" dndController="dijit.tree.dndSource"
|
||||||
|
betweenThreshold="5" model="filterModel" openOnClick="true">
|
||||||
|
<script type="dojo/method" event="onClick" args="item">
|
||||||
var id = String(item.id);
|
var id = String(item.id);
|
||||||
var bare_id = id.substr(id.indexOf(':')+1);
|
var bare_id = id.substr(id.indexOf(':')+1);
|
||||||
|
|
||||||
@@ -776,191 +684,22 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
Filters.edit(bare_id);
|
Filters.edit(bare_id);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
</div>
|
||||||
</div>";
|
</div>
|
||||||
|
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters") ?>
|
||||||
print "</div>"; #pane
|
</div>
|
||||||
|
<?php
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters");
|
|
||||||
|
|
||||||
print "</div>"; #container
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function newrule() {
|
function editrule() {
|
||||||
$rule = json_decode(clean($_REQUEST["rule"]), true);
|
$feed_ids = explode(",", clean($_REQUEST["ids"]));
|
||||||
|
|
||||||
if ($rule) {
|
print json_encode([
|
||||||
$reg_exp = htmlspecialchars($rule["reg_exp"]);
|
"multiselect" => $this->_feed_multi_select("feed_id", $feed_ids, 'required="1" style="width : 100%; height : 300px" dojoType="fox.form.ValidationMultiSelect"')
|
||||||
$filter_type = $rule["filter_type"];
|
]);
|
||||||
$feed_id = $rule["feed_id"];
|
|
||||||
$inverse_checked = isset($rule["inverse"]) ? "checked" : "";
|
|
||||||
} else {
|
|
||||||
$reg_exp = "";
|
|
||||||
$filter_type = 1;
|
|
||||||
$feed_id = ["0"];
|
|
||||||
$inverse_checked = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<form name='filter_new_rule_form' id='filter_new_rule_form' onsubmit='return false;'>";
|
private function _get_name($id) {
|
||||||
|
|
||||||
$res = $this->pdo->query("SELECT id,description
|
|
||||||
FROM ttrss_filter_types WHERE id != 5 ORDER BY description");
|
|
||||||
|
|
||||||
$filter_types = array();
|
|
||||||
|
|
||||||
while ($line = $res->fetch()) {
|
|
||||||
$filter_types[$line["id"]] = __($line["description"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
print "<header>".__("Match")."</header>";
|
|
||||||
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
print "<textarea dojoType='fox.form.ValidationTextArea'
|
|
||||||
required='true' id='filterDlg_regExp'
|
|
||||||
ValidRegExp='true'
|
|
||||||
rows='4'
|
|
||||||
style='font-size : 14px; width : 490px; word-break: break-all'
|
|
||||||
name='reg_exp'>$reg_exp</textarea>";
|
|
||||||
|
|
||||||
print "<div dojoType='dijit.Tooltip' id='filterDlg_regExp_tip' connectId='filterDlg_regExp' position='below'></div>";
|
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
print "<label class='checkbox'><input id='filterDlg_inverse' dojoType='dijit.form.CheckBox'
|
|
||||||
name='inverse' $inverse_checked/> ".
|
|
||||||
__("Inverse regular expression matching")."</label>";
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
print "<label style='display : inline'>". __("on field") . "</label> ";
|
|
||||||
print_select_hash("filter_type", $filter_type, $filter_types,
|
|
||||||
'dojoType="fox.form.Select"');
|
|
||||||
print "<label style='padding-left : 10px; display : inline'>" . __("in") . "</label> ";
|
|
||||||
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
print "<span id='filterDlg_feeds'>";
|
|
||||||
print_feed_multi_select("feed_id",
|
|
||||||
$feed_id,
|
|
||||||
'style="width : 500px; height : 300px" dojoType="dijit.form.MultiSelect"');
|
|
||||||
print "</span>";
|
|
||||||
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<footer>";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' style='float : left' class='alt-info' onclick='window.open(\"https://tt-rss.org/wiki/ContentFilters\")'>
|
|
||||||
<i class='material-icons'>help</i> ".__("More info...")."</button>";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'>".
|
|
||||||
($rule ? __("Save rule") : __('Add rule'))."</button> ";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>".
|
|
||||||
__('Cancel')."</button>";
|
|
||||||
|
|
||||||
print "</footer>";
|
|
||||||
|
|
||||||
print "</form>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function newaction() {
|
|
||||||
$action = json_decode(clean($_REQUEST["action"]), true);
|
|
||||||
|
|
||||||
if ($action) {
|
|
||||||
$action_param = $action["action_param"];
|
|
||||||
$action_id = (int)$action["action_id"];
|
|
||||||
} else {
|
|
||||||
$action_param = "";
|
|
||||||
$action_id = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
print "<form name='filter_new_action_form' id='filter_new_action_form' onsubmit='return false;'>";
|
|
||||||
|
|
||||||
print "<header>".__("Perform Action")."</header>";
|
|
||||||
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
print "<select name='action_id' dojoType='fox.form.Select'
|
|
||||||
onchange='Filters.filterDlgCheckAction(this)'>";
|
|
||||||
|
|
||||||
$res = $this->pdo->query("SELECT id,description FROM ttrss_filter_actions
|
|
||||||
ORDER BY name");
|
|
||||||
|
|
||||||
while ($line = $res->fetch()) {
|
|
||||||
$is_selected = ($line["id"] == $action_id) ? "selected='1'" : "";
|
|
||||||
printf("<option $is_selected value='%d'>%s</option>", $line["id"], __($line["description"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</select>";
|
|
||||||
|
|
||||||
$param_box_hidden = ($action_id == 7 || $action_id == 4 || $action_id == 6 || $action_id == 9) ?
|
|
||||||
"" : "display : none";
|
|
||||||
|
|
||||||
$param_hidden = ($action_id == 4 || $action_id == 6) ?
|
|
||||||
"" : "display : none";
|
|
||||||
|
|
||||||
$label_param_hidden = ($action_id == 7) ? "" : "display : none";
|
|
||||||
$plugin_param_hidden = ($action_id == 9) ? "" : "display : none";
|
|
||||||
|
|
||||||
print "<span id='filterDlg_paramBox' style=\"$param_box_hidden\">";
|
|
||||||
print " ";
|
|
||||||
//print " " . __("with parameters:") . " ";
|
|
||||||
print "<input dojoType='dijit.form.TextBox'
|
|
||||||
id='filterDlg_actionParam' style=\"$param_hidden\"
|
|
||||||
name='action_param' value=\"$action_param\">";
|
|
||||||
|
|
||||||
print_label_select("action_param_label", $action_param,
|
|
||||||
"id='filterDlg_actionParamLabel' style=\"$label_param_hidden\"
|
|
||||||
dojoType='fox.form.Select'");
|
|
||||||
|
|
||||||
$filter_actions = PluginHost::getInstance()->get_filter_actions();
|
|
||||||
$filter_action_hash = array();
|
|
||||||
|
|
||||||
foreach ($filter_actions as $fclass => $factions) {
|
|
||||||
foreach ($factions as $faction) {
|
|
||||||
|
|
||||||
$filter_action_hash[$fclass . ":" . $faction["action"]] =
|
|
||||||
$fclass . ": " . $faction["description"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($filter_action_hash) == 0) {
|
|
||||||
$filter_plugin_disabled = "disabled";
|
|
||||||
|
|
||||||
$filter_action_hash["no-data"] = __("No actions available");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$filter_plugin_disabled = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
print_select_hash("filterDlg_actionParamPlugin", $action_param, $filter_action_hash,
|
|
||||||
"style=\"$plugin_param_hidden\" dojoType='fox.form.Select' $filter_plugin_disabled",
|
|
||||||
"action_param_plugin");
|
|
||||||
|
|
||||||
print "</span>";
|
|
||||||
|
|
||||||
print " "; // tiny layout hack
|
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<footer>";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'>".
|
|
||||||
($action ? __("Save action") : __('Add action'))."</button> ";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>".
|
|
||||||
__('Cancel')."</button>";
|
|
||||||
|
|
||||||
print "</footer>";
|
|
||||||
|
|
||||||
print "</form>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getFilterName($id) {
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare(
|
$sth = $this->pdo->prepare(
|
||||||
"SELECT title,match_any_rule,f.inverse AS inverse,COUNT(DISTINCT r.id) AS num_rules,COUNT(DISTINCT a.id) AS num_actions
|
"SELECT title,match_any_rule,f.inverse AS inverse,COUNT(DISTINCT r.id) AS num_rules,COUNT(DISTINCT a.id) AS num_actions
|
||||||
@@ -989,7 +728,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$actions = "";
|
$actions = "";
|
||||||
|
|
||||||
if ($line = $sth->fetch()) {
|
if ($line = $sth->fetch()) {
|
||||||
$actions = $this->getActionName($line);
|
$actions = $this->_get_action_name($line);
|
||||||
|
|
||||||
$num_actions -= 1;
|
$num_actions -= 1;
|
||||||
}
|
}
|
||||||
@@ -1031,12 +770,12 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
|
|
||||||
$this->optimizeFilter($base_id);
|
$this->_optimize($base_id);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function optimizeFilter($id) {
|
private function _optimize($id) {
|
||||||
|
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
@@ -1090,4 +829,111 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function _feed_multi_select($id, $default_ids = [],
|
||||||
|
$attributes = "", $include_all_feeds = true,
|
||||||
|
$root_id = null, $nest_level = 0) {
|
||||||
|
|
||||||
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
$rv = "";
|
||||||
|
|
||||||
|
// print_r(in_array("CAT:6",$default_ids));
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
$rv .= "<select multiple=\true\" id=\"$id\" name=\"$id\" $attributes>";
|
||||||
|
if ($include_all_feeds) {
|
||||||
|
$is_selected = (in_array("0", $default_ids)) ? "selected=\"1\"" : "";
|
||||||
|
$rv .= "<option $is_selected value=\"0\">".__('All feeds')."</option>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_pref('ENABLE_FEED_CATS')) {
|
||||||
|
|
||||||
|
if (!$root_id) $root_id = null;
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT id,title,
|
||||||
|
(SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE
|
||||||
|
c2.parent_cat = ttrss_feed_categories.id) AS num_children
|
||||||
|
FROM ttrss_feed_categories
|
||||||
|
WHERE owner_uid = :uid AND
|
||||||
|
(parent_cat = :root_id OR (:root_id IS NULL AND parent_cat IS NULL)) ORDER BY title");
|
||||||
|
|
||||||
|
$sth->execute([":uid" => $_SESSION['uid'], ":root_id" => $root_id]);
|
||||||
|
|
||||||
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$line["title"] = " " . $line["title"];
|
||||||
|
|
||||||
|
$is_selected = in_array("CAT:".$line["id"], $default_ids) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$rv .= sprintf("<option $is_selected value='CAT:%d'>%s</option>",
|
||||||
|
$line["id"], htmlspecialchars($line["title"]));
|
||||||
|
|
||||||
|
if ($line["num_children"] > 0)
|
||||||
|
$rv .= $this->_feed_multi_select($id, $default_ids, $attributes,
|
||||||
|
$include_all_feeds, $line["id"], $nest_level+1);
|
||||||
|
|
||||||
|
$f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
||||||
|
WHERE cat_id = ? AND owner_uid = ? ORDER BY title");
|
||||||
|
|
||||||
|
$f_sth->execute([$line['id'], $_SESSION['uid']]);
|
||||||
|
|
||||||
|
while ($fline = $f_sth->fetch()) {
|
||||||
|
$is_selected = (in_array($fline["id"], $default_ids)) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
$rv .= sprintf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$fline["id"], htmlspecialchars($fline["title"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
$is_selected = in_array("CAT:0", $default_ids) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$rv .= sprintf("<option $is_selected value='CAT:0'>%s</option>",
|
||||||
|
__("Uncategorized"));
|
||||||
|
|
||||||
|
$f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
||||||
|
WHERE cat_id IS NULL AND owner_uid = ? ORDER BY title");
|
||||||
|
$f_sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
|
while ($fline = $f_sth->fetch()) {
|
||||||
|
$is_selected = in_array($fline["id"], $default_ids) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
$rv .= sprintf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$fline["id"], htmlspecialchars($fline["title"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
||||||
|
WHERE owner_uid = ? ORDER BY title");
|
||||||
|
$sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
|
$is_selected = (in_array($line["id"], $default_ids)) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$rv .= sprintf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$line["id"], htmlspecialchars($line["title"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
$rv .= "</select>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rv;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,72 +10,12 @@ class Pref_Labels extends Handler_Protected {
|
|||||||
function edit() {
|
function edit() {
|
||||||
$label_id = clean($_REQUEST['id']);
|
$label_id = clean($_REQUEST['id']);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT * FROM ttrss_labels2 WHERE
|
$sth = $this->pdo->prepare("SELECT id, caption, fg_color, bg_color FROM ttrss_labels2 WHERE
|
||||||
id = ? AND owner_uid = ?");
|
id = ? AND owner_uid = ?");
|
||||||
$sth->execute([$label_id, $_SESSION['uid']]);
|
$sth->execute([$label_id, $_SESSION['uid']]);
|
||||||
|
|
||||||
if ($line = $sth->fetch()) {
|
if ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
print json_encode($line);
|
||||||
print_hidden("id", "$label_id");
|
|
||||||
print_hidden("op", "pref-labels");
|
|
||||||
print_hidden("method", "save");
|
|
||||||
|
|
||||||
print "<form onsubmit='return false;'>";
|
|
||||||
|
|
||||||
print "<header>".__("Caption")."</header>";
|
|
||||||
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
$fg_color = $line['fg_color'];
|
|
||||||
$bg_color = $line['bg_color'] ? $line['bg_color'] : '#fff7d5';
|
|
||||||
|
|
||||||
print "<input style='font-size : 16px; color : $fg_color; background : $bg_color; transition : background 0.1s linear'
|
|
||||||
id='labelEdit_caption' name='caption' dojoType='dijit.form.ValidationTextBox'
|
|
||||||
required='true' value=\"".htmlspecialchars($line['caption'])."\">";
|
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<header>" . __("Colors") . "</header>";
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
print "<table>";
|
|
||||||
print "<tr><th style='text-align : left'>".__("Foreground:")."</th><th style='text-align : left'>".__("Background:")."</th></tr>";
|
|
||||||
print "<tr><td style='padding-right : 10px'>";
|
|
||||||
|
|
||||||
print "<input dojoType='dijit.form.TextBox'
|
|
||||||
style='display : none' id='labelEdit_fgColor'
|
|
||||||
name='fg_color' value='$fg_color'>";
|
|
||||||
print "<input dojoType='dijit.form.TextBox'
|
|
||||||
style='display : none' id='labelEdit_bgColor'
|
|
||||||
name='bg_color' value='$bg_color'>";
|
|
||||||
|
|
||||||
print "<div dojoType='dijit.ColorPalette'>
|
|
||||||
<script type='dojo/method' event='onChange' args='fg_color'>
|
|
||||||
dijit.byId('labelEdit_fgColor').attr('value', fg_color);
|
|
||||||
dijit.byId('labelEdit_caption').domNode.setStyle({color: fg_color});
|
|
||||||
</script>
|
|
||||||
</div>";
|
|
||||||
|
|
||||||
print "</td><td>";
|
|
||||||
|
|
||||||
print "<div dojoType='dijit.ColorPalette'>
|
|
||||||
<script type='dojo/method' event='onChange' args='bg_color'>
|
|
||||||
dijit.byId('labelEdit_bgColor').attr('value', bg_color);
|
|
||||||
dijit.byId('labelEdit_caption').domNode.setStyle({backgroundColor: bg_color});
|
|
||||||
</script>
|
|
||||||
</div>";
|
|
||||||
|
|
||||||
print "</td></tr></table>";
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<footer>";
|
|
||||||
print "<button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'>".
|
|
||||||
__('Save')."</button>";
|
|
||||||
print "<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>".
|
|
||||||
__('Cancel')."</button>";
|
|
||||||
print "</footer>";
|
|
||||||
|
|
||||||
print "</form>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +137,7 @@ class Pref_Labels extends Handler_Protected {
|
|||||||
|
|
||||||
$sth->execute([$caption, $old_caption, $_SESSION['uid']]);
|
$sth->execute([$caption, $old_caption, $_SESSION['uid']]);
|
||||||
|
|
||||||
print clean($_REQUEST["value"]);
|
print clean($_REQUEST["caption"]);
|
||||||
} else {
|
} else {
|
||||||
print $old_caption;
|
print $old_caption;
|
||||||
}
|
}
|
||||||
@@ -225,74 +165,53 @@ class Pref_Labels extends Handler_Protected {
|
|||||||
$output = clean($_REQUEST["output"]);
|
$output = clean($_REQUEST["output"]);
|
||||||
|
|
||||||
if ($caption) {
|
if ($caption) {
|
||||||
|
|
||||||
if (Labels::create($caption)) {
|
if (Labels::create($caption)) {
|
||||||
if (!$output) {
|
if (!$output) {
|
||||||
print T_sprintf("Created label <b>%s</b>", htmlspecialchars($caption));
|
print T_sprintf("Created label <b>%s</b>", htmlspecialchars($caption));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($output == "select") {
|
|
||||||
header("Content-Type: text/xml");
|
|
||||||
|
|
||||||
print "<rpc-reply><payload>";
|
|
||||||
|
|
||||||
print_label_select("select_label",
|
|
||||||
$caption, "");
|
|
||||||
|
|
||||||
print "</payload></rpc-reply>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function index() {
|
function index() {
|
||||||
|
?>
|
||||||
print "<div dojoType='dijit.layout.BorderContainer' gutters='false'>";
|
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
|
||||||
print "<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>";
|
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
|
||||||
print "<div dojoType='fox.Toolbar'>";
|
<div dojoType='fox.Toolbar'>
|
||||||
|
<div dojoType='fox.form.DropDownButton'>
|
||||||
print "<div dojoType='fox.form.DropDownButton'>".
|
<span><?= __('Select') ?></span>
|
||||||
"<span>" . __('Select')."</span>";
|
<div dojoType='dijit.Menu' style='display: none'>
|
||||||
print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
|
<div onclick="dijit.byId('labelTree').model.setAllChecked(true)"
|
||||||
print "<div onclick=\"dijit.byId('labelTree').model.setAllChecked(true)\"
|
dojoType='dijit.MenuItem'><?=('All') ?></div>
|
||||||
dojoType=\"dijit.MenuItem\">".__('All')."</div>";
|
<div onclick="dijit.byId('labelTree').model.setAllChecked(false)"
|
||||||
print "<div onclick=\"dijit.byId('labelTree').model.setAllChecked(false)\"
|
dojoType='dijit.MenuItem'><?=('None') ?></div>
|
||||||
dojoType=\"dijit.MenuItem\">".__('None')."</div>";
|
|
||||||
print "</div></div>";
|
|
||||||
|
|
||||||
print"<button dojoType=\"dijit.form.Button\" onclick=\"CommonDialogs.addLabel()\">".
|
|
||||||
__('Create label')."</button dojoType=\"dijit.form.Button\"> ";
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('labelTree').removeSelected()\">".
|
|
||||||
__('Remove')."</button dojoType=\"dijit.form.Button\"> ";
|
|
||||||
|
|
||||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('labelTree').resetColors()\">".
|
|
||||||
__('Clear colors')."</button dojoType=\"dijit.form.Button\">";
|
|
||||||
|
|
||||||
|
|
||||||
print "</div>"; #toolbar
|
|
||||||
print "</div>"; #pane
|
|
||||||
print "<div style='padding : 0px' dojoType=\"dijit.layout.ContentPane\" region=\"center\">";
|
|
||||||
|
|
||||||
print "<div id=\"labellistLoading\">
|
|
||||||
<img src='images/indicator_tiny.gif'>".
|
|
||||||
__("Loading, please wait...")."</div>";
|
|
||||||
|
|
||||||
print "<div dojoType=\"dojo.data.ItemFileWriteStore\" jsId=\"labelStore\"
|
|
||||||
url=\"backend.php?op=pref-labels&method=getlabeltree\">
|
|
||||||
</div>
|
</div>
|
||||||
<div dojoType=\"lib.CheckBoxStoreModel\" jsId=\"labelModel\" store=\"labelStore\"
|
|
||||||
query=\"{id:'root'}\" rootId=\"root\"
|
|
||||||
childrenAttrs=\"items\" checkboxStrict=\"false\" checkboxAll=\"false\">
|
|
||||||
</div>
|
</div>
|
||||||
<div dojoType=\"fox.PrefLabelTree\" id=\"labelTree\"
|
|
||||||
model=\"labelModel\" openOnClick=\"true\">
|
<button dojoType='dijit.form.Button' onclick='CommonDialogs.addLabel()'>
|
||||||
<script type=\"dojo/method\" event=\"onLoad\" args=\"item\">
|
<?=('Create label') ?></button dojoType='dijit.form.Button'>
|
||||||
Element.hide(\"labellistLoading\");
|
|
||||||
</script>
|
<button dojoType='dijit.form.Button' onclick="dijit.byId('labelTree').removeSelected()">
|
||||||
<script type=\"dojo/method\" event=\"onClick\" args=\"item\">
|
<?=('Remove') ?></button dojoType='dijit.form.Button'>
|
||||||
|
|
||||||
|
<button dojoType='dijit.form.Button' onclick="dijit.byId('labelTree').resetColors()">
|
||||||
|
<?=('Clear colors') ?></button dojoType='dijit.form.Button'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
|
||||||
|
<div dojoType='dojo.data.ItemFileWriteStore' jsId='labelStore'
|
||||||
|
url='backend.php?op=pref-labels&method=getlabeltree'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div dojoType='lib.CheckBoxStoreModel' jsId='labelModel' store='labelStore'
|
||||||
|
query="{id:'root'}" rootId='root'
|
||||||
|
childrenAttrs='items' checkboxStrict='false' checkboxAll='false'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div dojoType='fox.PrefLabelTree' id='labelTree' model='labelModel' openOnClick='true'>
|
||||||
|
<script type='dojo/method' event='onClick' args='item'>
|
||||||
var id = String(item.id);
|
var id = String(item.id);
|
||||||
var bare_id = id.substr(id.indexOf(':')+1);
|
var bare_id = id.substr(id.indexOf(':')+1);
|
||||||
|
|
||||||
@@ -300,13 +219,10 @@ class Pref_Labels extends Handler_Protected {
|
|||||||
dijit.byId('labelTree').editLabel(bare_id);
|
dijit.byId('labelTree').editLabel(bare_id);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</div>";
|
</div>
|
||||||
|
</div>
|
||||||
print "</div>"; #pane
|
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefLabels") ?>
|
||||||
|
</div>
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefLabels");
|
<?php
|
||||||
|
|
||||||
print "</div>"; #container
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class Pref_System extends Handler_Protected {
|
class Pref_System extends Handler_Administrative {
|
||||||
|
|
||||||
private $log_page_limit = 15;
|
private $log_page_limit = 15;
|
||||||
|
|
||||||
function before($method) {
|
|
||||||
if (parent::before($method)) {
|
|
||||||
if ($_SESSION["access_level"] < 10) {
|
|
||||||
print __("Your access level is insufficient to open this tab.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function csrf_ignore($method) {
|
function csrf_ignore($method) {
|
||||||
$csrf_ignored = array("index");
|
$csrf_ignored = array("index");
|
||||||
|
|
||||||
@@ -25,7 +14,16 @@ class Pref_System extends Handler_Protected {
|
|||||||
$this->pdo->query("DELETE FROM ttrss_error_log");
|
$this->pdo->query("DELETE FROM ttrss_error_log");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function log_viewer(int $page, int $severity) {
|
function getphpinfo() {
|
||||||
|
ob_start();
|
||||||
|
phpinfo();
|
||||||
|
$info = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
print preg_replace( '%^.*<body>(.*)</body>.*$%ms','$1', (string)$info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _log_viewer(int $page, int $severity) {
|
||||||
$errno_values = [];
|
$errno_values = [];
|
||||||
|
|
||||||
switch ($severity) {
|
switch ($severity) {
|
||||||
@@ -62,54 +60,58 @@ class Pref_System extends Handler_Protected {
|
|||||||
$total_pages = 0;
|
$total_pages = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<div dojoType='dijit.layout.BorderContainer' gutters='false'>";
|
?>
|
||||||
|
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
|
||||||
|
<div region='top' dojoType='fox.Toolbar'>
|
||||||
|
|
||||||
print "<div region='top' dojoType='fox.Toolbar'>";
|
<button dojoType='dijit.form.Button' onclick='Helpers.EventLog.refresh()'>
|
||||||
|
<?= __('Refresh') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button'
|
<button dojoType='dijit.form.Button' <?= ($page <= 0 ? "disabled" : "") ?>
|
||||||
onclick='Helpers.EventLog.refresh()'>".__('Refresh')."</button>";
|
onclick='Helpers.EventLog.prevPage()'>
|
||||||
|
<?= __('<<') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
$prev_page_disabled = $page <= 0 ? "disabled" : "";
|
<button dojoType='dijit.form.Button' disabled>
|
||||||
|
<?= T_sprintf('Page %d of %d', $page+1, $total_pages+1) ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' $prev_page_disabled
|
<button dojoType='dijit.form.Button' <?= ($page >= $total_pages ? "disabled" : "") ?>
|
||||||
onclick='Helpers.EventLog.prevPage()'>".__('<<')."</button>";
|
onclick='Helpers.EventLog.nextPage()'>
|
||||||
|
<?= __('>>') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' disabled>".T_sprintf('Page %d of %d', $page+1, $total_pages+1)."</button>";
|
<button dojoType='dijit.form.Button'
|
||||||
|
onclick='Helpers.EventLog.clear()'>
|
||||||
|
<?= __('Clear') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
$next_page_disabled = $page >= $total_pages ? "disabled" : "";
|
<div class='pull-right'>
|
||||||
|
<label><?= __("Severity:") ?></label>
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' $next_page_disabled
|
<?= \Controls\select_hash("severity", $severity,
|
||||||
onclick='Helpers.EventLog.nextPage()'>".__('>>')."</button>";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button'
|
|
||||||
onclick='Helpers.EventLog.clear()'>".__('Clear')."</button>";
|
|
||||||
|
|
||||||
print "<div class='pull-right'>";
|
|
||||||
|
|
||||||
print __("Severity:") . " ";
|
|
||||||
print_select_hash("severity", $severity,
|
|
||||||
[
|
[
|
||||||
E_USER_ERROR => __("Errors"),
|
E_USER_ERROR => __("Errors"),
|
||||||
E_USER_WARNING => __("Warnings"),
|
E_USER_WARNING => __("Warnings"),
|
||||||
E_USER_NOTICE => __("Everything")
|
E_USER_NOTICE => __("Everything")
|
||||||
], 'dojoType="fox.form.Select" onchange="Helpers.EventLog.refresh()"');
|
], ["onchange"=> "Helpers.EventLog.refresh()"], "severity") ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
print "</div>"; # pull-right
|
<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">
|
||||||
|
|
||||||
print "</div>"; # toolbar
|
<table width='100%' class='event-log'>
|
||||||
|
|
||||||
print '<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">';
|
<tr class='title'>
|
||||||
|
<td width='5%'><?= __("Error") ?></td>
|
||||||
print "<table width='100%' class='event-log'>";
|
<td><?= __("Filename") ?></td>
|
||||||
|
<td><?= __("Message") ?></td>
|
||||||
print "<tr class='title'>
|
<td width='5%'><?= __("User") ?></td>
|
||||||
<td width='5%'>".__("Error")."</td>
|
<td width='5%'><?= __("Date") ?></td>
|
||||||
<td>".__("Filename")."</td>
|
</tr>
|
||||||
<td>".__("Message")."</td>
|
|
||||||
<td width='5%'>".__("User")."</td>
|
|
||||||
<td width='5%'>".__("Date")."</td>
|
|
||||||
</tr>";
|
|
||||||
|
|
||||||
|
<?php
|
||||||
$sth = $this->pdo->prepare("SELECT
|
$sth = $this->pdo->prepare("SELECT
|
||||||
errno, errstr, filename, lineno, created_at, login, context
|
errno, errstr, filename, lineno, created_at, login, context
|
||||||
FROM
|
FROM
|
||||||
@@ -123,64 +125,56 @@ class Pref_System extends Handler_Protected {
|
|||||||
$sth->execute($errno_values);
|
$sth->execute($errno_values);
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
print "<tr>";
|
foreach ($line as $k => $v) { $line[$k] = htmlspecialchars($v); }
|
||||||
|
?>
|
||||||
foreach ($line as $k => $v) {
|
<tr>
|
||||||
$line[$k] = htmlspecialchars($v);
|
<td class='errno'>
|
||||||
}
|
<?= Logger::$errornames[$line["errno"]] . " (" . $line["errno"] . ")" ?>
|
||||||
|
</td>
|
||||||
print "<td class='errno'>" . Logger::$errornames[$line["errno"]] . " (" . $line["errno"] . ")</td>";
|
<td class='filename'><?= $line["filename"] . ":" . $line["lineno"] ?></td>
|
||||||
print "<td class='filename'>" . $line["filename"] . ":" . $line["lineno"] . "</td>";
|
<td class='errstr'><?= $line["errstr"] . "\n" . $line["context"] ?></td>
|
||||||
print "<td class='errstr'>" . $line["errstr"] . "\n" . $line["context"] . "</td>";
|
<td class='login'><?= $line["login"] ?></td>
|
||||||
print "<td class='login'>" . $line["login"] . "</td>";
|
<td class='timestamp'>
|
||||||
|
<?= TimeHelper::make_local_datetime($line["created_at"], false) ?>
|
||||||
print "<td class='timestamp'>" .
|
</td>
|
||||||
TimeHelper::make_local_datetime($line["created_at"], false) . "</td>";
|
</tr>
|
||||||
|
<?php } ?>
|
||||||
print "</tr>";
|
</table>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
print "</table>";
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
function index() {
|
function index() {
|
||||||
|
|
||||||
$severity = (int) ($_REQUEST["severity"] ?? E_USER_WARNING);
|
$severity = (int) ($_REQUEST["severity"] ?? E_USER_WARNING);
|
||||||
$page = (int) ($_REQUEST["page"] ?? 0);
|
$page = (int) ($_REQUEST["page"] ?? 0);
|
||||||
|
?>
|
||||||
print "<div dojoType='dijit.layout.AccordionContainer' region='center'>";
|
<div dojoType='dijit.layout.AccordionContainer' region='center'>
|
||||||
print "<div dojoType='dijit.layout.AccordionPane' style='padding : 0'
|
<div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">report</i> <?= __('Event Log') ?>'>
|
||||||
title='<i class=\"material-icons\">report</i> ".__('Event Log')."'>";
|
<?php
|
||||||
|
if (Config::get(Config::LOG_DESTINATION) == "sql") {
|
||||||
if (LOG_DESTINATION == "sql") {
|
$this->_log_viewer($page, $severity);
|
||||||
|
|
||||||
$this->log_viewer($page, $severity);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
print_notice("Please set LOG_DESTINATION to 'sql' in config.php to enable database logging.");
|
print_notice("Please set Config::get(Config::LOG_DESTINATION) to 'sql' in config.php to enable database logging.");
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
print "</div>"; # content pane
|
<div dojoType='dijit.layout.AccordionPane' title='<i class="material-icons">info</i> <?= __('PHP Information') ?>'>
|
||||||
print "</div>"; # container
|
<script type='dojo/method' event='onSelected' args='evt'>
|
||||||
print "</div>"; # accordion pane
|
if (this.domNode.querySelector('.loading'))
|
||||||
|
window.setTimeout(() => {
|
||||||
|
xhr.post("backend.php", {op: 'pref-system', method: 'getphpinfo'}, (reply) => {
|
||||||
|
this.attr('content', `<div class='phpinfo'>${reply}</div>`);
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
</script>
|
||||||
|
<span class='loading'><?= __("Loading, please wait...") ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
print "<div dojoType='dijit.layout.AccordionPane'
|
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefSystem") ?>
|
||||||
title='<i class=\"material-icons\">info</i> ".__('PHP Information')."'>";
|
</div>
|
||||||
|
<?php
|
||||||
ob_start();
|
|
||||||
phpinfo();
|
|
||||||
$info = ob_get_contents();
|
|
||||||
ob_end_clean();
|
|
||||||
|
|
||||||
print "<div class='phpinfo'>";
|
|
||||||
print preg_replace( '%^.*<body>(.*)</body>.*$%ms','$1', $info);
|
|
||||||
print "</div>";
|
|
||||||
|
|
||||||
print "</div>"; # accordion pane
|
|
||||||
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefSystem");
|
|
||||||
|
|
||||||
print "</div>"; #container
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
class Pref_Users extends Handler_Protected {
|
class Pref_Users extends Handler_Administrative {
|
||||||
function before($method) {
|
|
||||||
if (parent::before($method)) {
|
|
||||||
if ($_SESSION["access_level"] < 10) {
|
|
||||||
print __("Your access level is insufficient to open this tab.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function csrf_ignore($method) {
|
function csrf_ignore($method) {
|
||||||
$csrf_ignored = array("index", "userdetails");
|
$csrf_ignored = array("index");
|
||||||
|
|
||||||
return array_search($method, $csrf_ignored) !== false;
|
return array_search($method, $csrf_ignored) !== false;
|
||||||
}
|
}
|
||||||
@@ -20,105 +9,17 @@ class Pref_Users extends Handler_Protected {
|
|||||||
function edit() {
|
function edit() {
|
||||||
global $access_level_names;
|
global $access_level_names;
|
||||||
|
|
||||||
print "<form id='user_edit_form' onsubmit='return false' dojoType='dijit.form.Form'>";
|
|
||||||
|
|
||||||
print '<div dojoType="dijit.layout.TabContainer" style="height : 400px">
|
|
||||||
<div dojoType="dijit.layout.ContentPane" title="'.__('Edit user').'">';
|
|
||||||
|
|
||||||
//print "<form id=\"user_edit_form\" onsubmit='return false' dojoType=\"dijit.form.Form\">";
|
|
||||||
|
|
||||||
$id = (int)clean($_REQUEST["id"]);
|
$id = (int)clean($_REQUEST["id"]);
|
||||||
|
|
||||||
print_hidden("id", "$id");
|
$sth = $this->pdo->prepare("SELECT id, login, access_level, email FROM ttrss_users WHERE id = ?");
|
||||||
print_hidden("op", "pref-users");
|
|
||||||
print_hidden("method", "editSave");
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT * FROM ttrss_users WHERE id = ?");
|
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
print json_encode([
|
||||||
$login = $row["login"];
|
"user" => $row,
|
||||||
$access_level = $row["access_level"];
|
"access_level_names" => $access_level_names
|
||||||
$email = $row["email"];
|
]);
|
||||||
|
|
||||||
$sel_disabled = ($id == $_SESSION["uid"] || $login == "admin") ? "disabled" : "";
|
|
||||||
|
|
||||||
print "<header>".__("User")."</header>";
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
if ($sel_disabled) {
|
|
||||||
print_hidden("login", "$login");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
print "<label>" . __("Login:") . "</label>";
|
|
||||||
print "<input style='font-size : 16px'
|
|
||||||
dojoType='dijit.form.ValidationTextBox' required='1'
|
|
||||||
$sel_disabled name='login' value=\"$login\">";
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<header>".__("Authentication")."</header>";
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
|
|
||||||
print "<label>" . __('Access level: ') . "</label> ";
|
|
||||||
|
|
||||||
if (!$sel_disabled) {
|
|
||||||
print_select_hash("access_level", $access_level, $access_level_names,
|
|
||||||
"dojoType=\"fox.form.Select\" $sel_disabled");
|
|
||||||
} else {
|
|
||||||
print_select_hash("", $access_level, $access_level_names,
|
|
||||||
"dojoType=\"fox.form.Select\" $sel_disabled");
|
|
||||||
print_hidden("access_level", "$access_level");
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</fieldset>";
|
|
||||||
print "<fieldset>";
|
|
||||||
|
|
||||||
print "<label>" . __("New password:") . "</label> ";
|
|
||||||
print "<input dojoType='dijit.form.TextBox' type='password' size='20' placeholder='Change password'
|
|
||||||
name='password'>";
|
|
||||||
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "<header>".__("Options")."</header>";
|
|
||||||
print "<section>";
|
|
||||||
|
|
||||||
print "<fieldset>";
|
|
||||||
print "<label>" . __("E-mail:") . "</label> ";
|
|
||||||
print "<input dojoType='dijit.form.TextBox' size='30' name='email'
|
|
||||||
value=\"$email\">";
|
|
||||||
print "</fieldset>";
|
|
||||||
|
|
||||||
print "</section>";
|
|
||||||
|
|
||||||
print "</table>";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
print '</div>'; #tab
|
|
||||||
print "<div href=\"backend.php?op=pref-users&method=userdetails&id=$id\"
|
|
||||||
dojoType=\"dijit.layout.ContentPane\" title=\"".__('User details')."\">";
|
|
||||||
|
|
||||||
print '</div>';
|
|
||||||
print '</div>';
|
|
||||||
|
|
||||||
print "<footer>
|
|
||||||
<button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'>".
|
|
||||||
__('Save')."</button>
|
|
||||||
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>".
|
|
||||||
__('Cancel')."</button>
|
|
||||||
</footer>";
|
|
||||||
|
|
||||||
print "</form>";
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function userdetails() {
|
function userdetails() {
|
||||||
@@ -135,7 +36,6 @@ class Pref_Users extends Handler_Protected {
|
|||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
print "<table width='100%'>";
|
|
||||||
|
|
||||||
$last_login = TimeHelper::make_local_datetime(
|
$last_login = TimeHelper::make_local_datetime(
|
||||||
$row["last_login"], true);
|
$row["last_login"], true);
|
||||||
@@ -145,47 +45,62 @@ class Pref_Users extends Handler_Protected {
|
|||||||
|
|
||||||
$stored_articles = $row["stored_articles"];
|
$stored_articles = $row["stored_articles"];
|
||||||
|
|
||||||
print "<tr><td>".__('Registered')."</td><td>$created</td></tr>";
|
|
||||||
print "<tr><td>".__('Last logged in')."</td><td>$last_login</td></tr>";
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT COUNT(id) as num_feeds FROM ttrss_feeds
|
$sth = $this->pdo->prepare("SELECT COUNT(id) as num_feeds FROM ttrss_feeds
|
||||||
WHERE owner_uid = ?");
|
WHERE owner_uid = ?");
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
$row = $sth->fetch();
|
$row = $sth->fetch();
|
||||||
|
|
||||||
$num_feeds = $row["num_feeds"];
|
$num_feeds = $row["num_feeds"];
|
||||||
|
|
||||||
print "<tr><td>".__('Subscribed feeds count')."</td><td>$num_feeds</td></tr>";
|
?>
|
||||||
print "<tr><td>".__('Stored articles')."</td><td>$stored_articles</td></tr>";
|
|
||||||
|
|
||||||
print "</table>";
|
<fieldset>
|
||||||
|
<label><?= __('Registered') ?>:</label>
|
||||||
|
<?= $created ?>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
print "<h1>".__('Subscribed feeds')."</h1>";
|
<fieldset>
|
||||||
|
<label><?= __('Last logged in') ?>:</label>
|
||||||
|
<?= $last_login ?>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label><?= __('Subscribed feeds') ?>:</label>
|
||||||
|
<?= $num_feeds ?>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label><?= __('Stored articles') ?>:</label>
|
||||||
|
<?= $stored_articles ?>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<?php
|
||||||
$sth = $this->pdo->prepare("SELECT id,title,site_url FROM ttrss_feeds
|
$sth = $this->pdo->prepare("SELECT id,title,site_url FROM ttrss_feeds
|
||||||
WHERE owner_uid = ? ORDER BY title");
|
WHERE owner_uid = ? ORDER BY title");
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
?>
|
||||||
|
|
||||||
print "<ul class=\"panel panel-scrollable list list-unstyled\">";
|
<ul class="panel panel-scrollable list list-unstyled">
|
||||||
|
<?php while ($row = $sth->fetch()) { ?>
|
||||||
|
<li>
|
||||||
|
<?php
|
||||||
|
$icon_file = Config::get(Config::ICONS_URL) . "/" . $row["id"] . ".ico";
|
||||||
|
$icon = file_exists($icon_file) ? $icon_file : "images/blank_icon.gif";
|
||||||
|
?>
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
<img class="icon" src="<?= $icon_file ?>">
|
||||||
|
|
||||||
$icon_file = ICONS_URL."/".$line["id"].".ico";
|
<a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>">
|
||||||
|
<?= htmlspecialchars($row["title"]) ?>
|
||||||
if (file_exists($icon_file) && filesize($icon_file) > 0) {
|
</a>
|
||||||
$feed_icon = "<img class=\"icon\" src=\"$icon_file\">";
|
</li>
|
||||||
} else {
|
<?php } ?>
|
||||||
$feed_icon = "<img class=\"icon\" src=\"images/blank_icon.gif\">";
|
</ul>
|
||||||
}
|
|
||||||
|
|
||||||
print "<li>$feed_icon <a href=\"".$line["site_url"]."\">".$line["title"]."</a></li>";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</ul>";
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
print "<h1>".__('User not found')."</h1>";
|
print_error(__('User not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -197,6 +112,12 @@ class Pref_Users extends Handler_Protected {
|
|||||||
$email = clean($_REQUEST["email"]);
|
$email = clean($_REQUEST["email"]);
|
||||||
$password = clean($_REQUEST["password"]);
|
$password = clean($_REQUEST["password"]);
|
||||||
|
|
||||||
|
// no blank usernames
|
||||||
|
if (!$login) return;
|
||||||
|
|
||||||
|
// forbid renaming admin
|
||||||
|
if ($uid == 1) $login = "admin";
|
||||||
|
|
||||||
if ($password) {
|
if ($password) {
|
||||||
$salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
|
$salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
|
||||||
$pwd_hash = encrypt_password($password, $salt, true);
|
$pwd_hash = encrypt_password($password, $salt, true);
|
||||||
@@ -246,67 +167,25 @@ class Pref_Users extends Handler_Protected {
|
|||||||
|
|
||||||
if ($new_uid = UserHelper::find_user_by_login($login)) {
|
if ($new_uid = UserHelper::find_user_by_login($login)) {
|
||||||
|
|
||||||
$new_uid = $row['id'];
|
|
||||||
|
|
||||||
print T_sprintf("Added user %s with password %s",
|
print T_sprintf("Added user %s with password %s",
|
||||||
$login, $tmp_user_pwd);
|
$login, $tmp_user_pwd);
|
||||||
|
|
||||||
$this->initialize_user($new_uid);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
print T_sprintf("Could not create user %s", $login);
|
print T_sprintf("Could not create user %s", $login);
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print T_sprintf("User %s already exists.", $login);
|
print T_sprintf("User %s already exists.", $login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function resetUserPassword($uid, $format_output = false) {
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT login FROM ttrss_users WHERE id = ?");
|
|
||||||
$sth->execute([$uid]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
|
||||||
|
|
||||||
$login = $row["login"];
|
|
||||||
|
|
||||||
$new_salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
|
|
||||||
$tmp_user_pwd = make_password();
|
|
||||||
|
|
||||||
$pwd_hash = encrypt_password($tmp_user_pwd, $new_salt, true);
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_users
|
|
||||||
SET pwd_hash = ?, salt = ?, otp_enabled = false
|
|
||||||
WHERE id = ?");
|
|
||||||
$sth->execute([$pwd_hash, $new_salt, $uid]);
|
|
||||||
|
|
||||||
$message = T_sprintf("Changed password of user %s to %s", "<strong>$login</strong>", "<strong>$tmp_user_pwd</strong>");
|
|
||||||
|
|
||||||
if ($format_output)
|
|
||||||
print_notice($message);
|
|
||||||
else
|
|
||||||
print $message;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPass() {
|
function resetPass() {
|
||||||
$uid = clean($_REQUEST["id"]);
|
UserHelper::reset_password(clean($_REQUEST["id"]));
|
||||||
self::resetUserPassword($uid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function index() {
|
function index() {
|
||||||
|
|
||||||
global $access_level_names;
|
global $access_level_names;
|
||||||
|
|
||||||
print "<div dojoType='dijit.layout.BorderContainer' gutters='false'>";
|
|
||||||
print "<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>";
|
|
||||||
print "<div dojoType='fox.Toolbar'>";
|
|
||||||
|
|
||||||
$user_search = clean($_REQUEST["search"] ?? "");
|
$user_search = clean($_REQUEST["search"] ?? "");
|
||||||
|
|
||||||
if (array_key_exists("search", $_REQUEST)) {
|
if (array_key_exists("search", $_REQUEST)) {
|
||||||
@@ -315,49 +194,71 @@ class Pref_Users extends Handler_Protected {
|
|||||||
$user_search = ($_SESSION["prefs_user_search"] ?? "");
|
$user_search = ($_SESSION["prefs_user_search"] ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<div style='float : right; padding-right : 4px;'>
|
|
||||||
<input dojoType='dijit.form.TextBox' id='user_search' size='20' type='search'
|
|
||||||
value=\"$user_search\">
|
|
||||||
<button dojoType='dijit.form.Button' onclick='Users.reload()'>".
|
|
||||||
__('Search')."</button>
|
|
||||||
</div>";
|
|
||||||
|
|
||||||
$sort = clean($_REQUEST["sort"] ?? "");
|
$sort = clean($_REQUEST["sort"] ?? "");
|
||||||
|
|
||||||
if (!$sort || $sort == "undefined") {
|
if (!$sort || $sort == "undefined") {
|
||||||
$sort = "login";
|
$sort = "login";
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<div dojoType='fox.form.DropDownButton'>".
|
$sort = $this->_validate_field($sort,
|
||||||
"<span>" . __('Select')."</span>";
|
|
||||||
print "<div dojoType='dijit.Menu' style='display: none'>";
|
|
||||||
print "<div onclick=\"Tables.select('users-list', true)\"
|
|
||||||
dojoType='dijit.MenuItem'>".__('All')."</div>";
|
|
||||||
print "<div onclick=\"Tables.select('users-list', false)\"
|
|
||||||
dojoType='dijit.MenuItem'>".__('None')."</div>";
|
|
||||||
print "</div></div>";
|
|
||||||
|
|
||||||
print "<button dojoType='dijit.form.Button' onclick='Users.add()'>".__('Create user')."</button>";
|
|
||||||
|
|
||||||
print "
|
|
||||||
<button dojoType='dijit.form.Button' onclick='Users.editSelected()'>".
|
|
||||||
__('Edit')."</button dojoType=\"dijit.form.Button\">
|
|
||||||
<button dojoType='dijit.form.Button' onclick='Users.removeSelected()'>".
|
|
||||||
__('Remove')."</button dojoType=\"dijit.form.Button\">
|
|
||||||
<button dojoType='dijit.form.Button' onclick='Users.resetSelected()'>".
|
|
||||||
__('Reset password')."</button dojoType=\"dijit.form.Button\">";
|
|
||||||
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefUsersToolbar");
|
|
||||||
|
|
||||||
print "</div>"; #toolbar
|
|
||||||
print "</div>"; #pane
|
|
||||||
print "<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>";
|
|
||||||
|
|
||||||
$sort = $this->validate_field($sort,
|
|
||||||
["login", "access_level", "created", "num_feeds", "created", "last_login"], "login");
|
["login", "access_level", "created", "num_feeds", "created", "last_login"], "login");
|
||||||
|
|
||||||
if ($sort != "login") $sort = "$sort DESC";
|
if ($sort != "login") $sort = "$sort DESC";
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
|
||||||
|
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
|
||||||
|
<div dojoType='fox.Toolbar'>
|
||||||
|
|
||||||
|
<div style='float : right'>
|
||||||
|
<input dojoType='dijit.form.TextBox' id='user_search' size='20' type='search'
|
||||||
|
value="<?= htmlspecialchars($user_search) ?>">
|
||||||
|
<button dojoType='dijit.form.Button' onclick='Users.reload()'>
|
||||||
|
<?= __('Search') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div dojoType='fox.form.DropDownButton'>
|
||||||
|
<span><?= __('Select') ?></span>
|
||||||
|
<div dojoType='dijit.Menu' style='display: none'>
|
||||||
|
<div onclick="Tables.select('users-list', true)"
|
||||||
|
dojoType='dijit.MenuItem'><?= __('All') ?></div>
|
||||||
|
<div onclick="Tables.select('users-list', false)"
|
||||||
|
dojoType='dijit.MenuItem'><?= __('None') ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button dojoType='dijit.form.Button' onclick='Users.add()'>
|
||||||
|
<?= __('Create user') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button dojoType='dijit.form.Button' onclick='Users.removeSelected()'>
|
||||||
|
<?= __('Remove') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button dojoType='dijit.form.Button' onclick='Users.resetSelected()'>
|
||||||
|
<?= __('Reset password') ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefUsersToolbar") ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
|
||||||
|
|
||||||
|
<table width='100%' class='users-list' id='users-list'>
|
||||||
|
|
||||||
|
<tr class='title'>
|
||||||
|
<td align='center' width='5%'> </td>
|
||||||
|
<td width='20%'><a href='#' onclick="Users.reload('login')"><?= ('Login') ?></a></td>
|
||||||
|
<td width='20%'><a href='#' onclick="Users.reload('access_level')"><?= ('Access Level') ?></a></td>
|
||||||
|
<td width='10%'><a href='#' onclick="Users.reload('num_feeds')"><?= ('Subscribed feeds') ?></a></td>
|
||||||
|
<td width='20%'><a href='#' onclick="Users.reload('created')"><?= ('Registered') ?></a></td>
|
||||||
|
<td width='20%'><a href='#' onclick="Users.reload('last_login')"><?= ('Last login') ?></a></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<?php
|
||||||
$sth = $this->pdo->prepare("SELECT
|
$sth = $this->pdo->prepare("SELECT
|
||||||
tu.id,
|
tu.id,
|
||||||
login,access_level,email,
|
login,access_level,email,
|
||||||
@@ -371,90 +272,33 @@ class Pref_Users extends Handler_Protected {
|
|||||||
ORDER BY $sort");
|
ORDER BY $sort");
|
||||||
$sth->execute([":search" => $user_search ? "%$user_search%" : ""]);
|
$sth->execute([":search" => $user_search ? "%$user_search%" : ""]);
|
||||||
|
|
||||||
print "<table width='100%' class='users-list' id='users-list'>";
|
while ($row = $sth->fetch()) { ?>
|
||||||
|
|
||||||
print "<tr class='title'>
|
<tr data-row-id='<?= $row["id"] ?>' onclick='Users.edit(<?= $row["id"] ?>)' title="<?= __('Click to edit') ?>">
|
||||||
<td align='center' width='5%'> </td>
|
<td align='center'>
|
||||||
<td width='20%'><a href='#' onclick=\"Users.reload('login')\">".__('Login')."</a></td>
|
<input onclick='Tables.onRowChecked(this); event.stopPropagation();'
|
||||||
<td width='20%'><a href='#' onclick=\"Users.reload('access_level')\">".__('Access Level')."</a></td>
|
dojoType='dijit.form.CheckBox' type='checkbox'>
|
||||||
<td width='10%'><a href='#' onclick=\"Users.reload('num_feeds')\">".__('Subscribed feeds')."</a></td>
|
</td>
|
||||||
<td width='20%'><a href='#' onclick=\"Users.reload('created')\">".__('Registered')."</a></td>
|
|
||||||
<td width='20%'><a href='#' onclick=\"Users.reload('last_login')\">".__('Last login')."</a></td></tr>";
|
|
||||||
|
|
||||||
$lnum = 0;
|
<td><i class='material-icons'>person</i> <?= htmlspecialchars($row["login"]) ?></td>
|
||||||
|
<td><?= $access_level_names[$row["access_level"]] ?></td>
|
||||||
while ($line = $sth->fetch()) {
|
<td><?= $row["num_feeds"] ?></td>
|
||||||
|
<td><?= TimeHelper::make_local_datetime($row["created"], false) ?></td>
|
||||||
$uid = $line["id"];
|
<td><?= TimeHelper::make_local_datetime($row["last_login"], false) ?></td>
|
||||||
|
</tr>
|
||||||
print "<tr data-row-id='$uid' onclick='Users.edit($uid)'>";
|
<?php } ?>
|
||||||
|
</table>
|
||||||
$line["login"] = htmlspecialchars($line["login"]);
|
</div>
|
||||||
$line["created"] = TimeHelper::make_local_datetime($line["created"], false);
|
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefUsers") ?>
|
||||||
$line["last_login"] = TimeHelper::make_local_datetime($line["last_login"], false);
|
</div>
|
||||||
|
<?php
|
||||||
print "<td align='center'><input onclick='Tables.onRowChecked(this); event.stopPropagation();'
|
|
||||||
dojoType='dijit.form.CheckBox' type='checkbox'></td>";
|
|
||||||
|
|
||||||
print "<td title='".__('Click to edit')."'><i class='material-icons'>person</i> " . $line["login"] . "</td>";
|
|
||||||
|
|
||||||
print "<td>" . $access_level_names[$line["access_level"]] . "</td>";
|
|
||||||
print "<td>" . $line["num_feeds"] . "</td>";
|
|
||||||
print "<td>" . $line["created"] . "</td>";
|
|
||||||
print "<td>" . $line["last_login"] . "</td>";
|
|
||||||
|
|
||||||
print "</tr>";
|
|
||||||
|
|
||||||
++$lnum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print "</table>";
|
private function _validate_field($string, $allowed, $default = "") {
|
||||||
|
|
||||||
if ($lnum == 0) {
|
|
||||||
if (!$user_search) {
|
|
||||||
print_warning(__('No users defined.'));
|
|
||||||
} else {
|
|
||||||
print_warning(__('No matching users found.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print "</div>"; #pane
|
|
||||||
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefUsers");
|
|
||||||
|
|
||||||
print "</div>"; #container
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate_field($string, $allowed, $default = "") {
|
|
||||||
if (in_array($string, $allowed))
|
if (in_array($string, $allowed))
|
||||||
return $string;
|
return $string;
|
||||||
else
|
else
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is called after user is created to initialize default feeds, labels
|
|
||||||
// or whatever else
|
|
||||||
// user preferences are checked on every login, not here
|
|
||||||
static function initialize_user($uid) {
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
|
|
||||||
values (?, 'Tiny Tiny RSS: Forum',
|
|
||||||
'https://tt-rss.org/forum/rss.php')");
|
|
||||||
$sth->execute([$uid]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static function logout_user() {
|
|
||||||
if (session_status() === PHP_SESSION_ACTIVE)
|
|
||||||
session_destroy();
|
|
||||||
|
|
||||||
if (isset($_COOKIE[session_name()])) {
|
|
||||||
setcookie(session_name(), '', time()-42000, '/');
|
|
||||||
|
|
||||||
}
|
|
||||||
session_commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
297
classes/rpc.php
297
classes/rpc.php
@@ -1,96 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
class RPC extends Handler_Protected {
|
class RPC extends Handler_Protected {
|
||||||
|
|
||||||
function csrf_ignore($method) {
|
/*function csrf_ignore($method) {
|
||||||
$csrf_ignored = array("completelabels", "saveprofile");
|
$csrf_ignored = array("completelabels");
|
||||||
|
|
||||||
return array_search($method, $csrf_ignored) !== false;
|
return array_search($method, $csrf_ignored) !== false;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
function setprofile() {
|
|
||||||
$_SESSION["profile"] = (int) clean($_REQUEST["id"]);
|
|
||||||
|
|
||||||
// default value
|
|
||||||
if (!$_SESSION["profile"]) $_SESSION["profile"] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function remprofiles() {
|
|
||||||
$ids = explode(",", clean($_REQUEST["ids"]));
|
|
||||||
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
if ($_SESSION["profile"] != $id) {
|
|
||||||
$sth = $this->pdo->prepare("DELETE FROM ttrss_settings_profiles WHERE id = ? AND
|
|
||||||
owner_uid = ?");
|
|
||||||
$sth->execute([$id, $_SESSION['uid']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Silent
|
|
||||||
function addprofile() {
|
|
||||||
$title = clean($_REQUEST["title"]);
|
|
||||||
|
|
||||||
if ($title) {
|
|
||||||
$this->pdo->beginTransaction();
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_settings_profiles
|
|
||||||
WHERE title = ? AND owner_uid = ?");
|
|
||||||
$sth->execute([$title, $_SESSION['uid']]);
|
|
||||||
|
|
||||||
if (!$sth->fetch()) {
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("INSERT INTO ttrss_settings_profiles (title, owner_uid)
|
|
||||||
VALUES (?, ?)");
|
|
||||||
|
|
||||||
$sth->execute([$title, $_SESSION['uid']]);
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_settings_profiles WHERE
|
|
||||||
title = ? AND owner_uid = ?");
|
|
||||||
$sth->execute([$title, $_SESSION['uid']]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
|
||||||
$profile_id = $row['id'];
|
|
||||||
|
|
||||||
if ($profile_id) {
|
|
||||||
Pref_Prefs::initialize_user_prefs($_SESSION["uid"], $profile_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->pdo->commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveprofile() {
|
|
||||||
$id = clean($_REQUEST["id"]);
|
|
||||||
$title = clean($_REQUEST["value"]);
|
|
||||||
|
|
||||||
if ($id == 0) {
|
|
||||||
print __("Default profile");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($title) {
|
|
||||||
$sth = $this->pdo->prepare("UPDATE ttrss_settings_profiles
|
|
||||||
SET title = ? WHERE id = ? AND
|
|
||||||
owner_uid = ?");
|
|
||||||
|
|
||||||
$sth->execute([$title, $id, $_SESSION['uid']]);
|
|
||||||
print $title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addfeed() {
|
|
||||||
$feed = clean($_REQUEST['feed']);
|
|
||||||
$cat = clean($_REQUEST['cat']);
|
|
||||||
$need_auth = isset($_REQUEST['need_auth']);
|
|
||||||
$login = $need_auth ? clean($_REQUEST['login']) : '';
|
|
||||||
$pass = $need_auth ? clean($_REQUEST['pass']) : '';
|
|
||||||
|
|
||||||
$rc = Feeds::subscribe_to_feed($feed, $cat, $login, $pass);
|
|
||||||
|
|
||||||
print json_encode(array("result" => $rc));
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglepref() {
|
function togglepref() {
|
||||||
$key = clean($_REQUEST["key"]);
|
$key = clean($_REQUEST["key"]);
|
||||||
@@ -131,7 +46,7 @@ class RPC extends Handler_Protected {
|
|||||||
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
|
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
|
||||||
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
||||||
|
|
||||||
Article::purge_orphans();
|
Article::_purge_orphans();
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
||||||
}
|
}
|
||||||
@@ -149,67 +64,100 @@ class RPC extends Handler_Protected {
|
|||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRuntimeInfo() {
|
||||||
|
$reply = [
|
||||||
|
'runtime-info' => $this->make_runtime_info()
|
||||||
|
];
|
||||||
|
|
||||||
|
print json_encode($reply);
|
||||||
|
}
|
||||||
|
|
||||||
function getAllCounters() {
|
function getAllCounters() {
|
||||||
@$seq = (int) $_REQUEST['seq'];
|
@$seq = (int) $_REQUEST['seq'];
|
||||||
|
|
||||||
|
$feed_id_count = (int)$_REQUEST["feed_id_count"];
|
||||||
|
$label_id_count = (int)$_REQUEST["label_id_count"];
|
||||||
|
|
||||||
|
// it seems impossible to distinguish empty array [] from a null - both become unset in $_REQUEST
|
||||||
|
// so, count is >= 0 means we had an array, -1 means null
|
||||||
|
// we need null because it means "return all counters"; [] would return nothing
|
||||||
|
if ($feed_id_count == -1)
|
||||||
|
$feed_ids = null;
|
||||||
|
else
|
||||||
|
$feed_ids = array_map("intval", clean($_REQUEST["feed_ids"] ?? []));
|
||||||
|
|
||||||
|
if ($label_id_count == -1)
|
||||||
|
$label_ids = null;
|
||||||
|
else
|
||||||
|
$label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? []));
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$counters = is_array($feed_ids) ? Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all();
|
||||||
|
|
||||||
$reply = [
|
$reply = [
|
||||||
'counters' => Counters::getAllCounters(),
|
'counters' => $counters,
|
||||||
'seq' => $seq
|
'seq' => $seq
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($seq % 2 == 0)
|
|
||||||
$reply['runtime-info'] = $this->make_runtime_info();
|
|
||||||
|
|
||||||
print json_encode($reply);
|
print json_encode($reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */
|
/* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */
|
||||||
function catchupSelected() {
|
function catchupSelected() {
|
||||||
$ids = explode(",", clean($_REQUEST["ids"]));
|
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
|
||||||
$cmode = (int)clean($_REQUEST["cmode"]);
|
$cmode = (int)clean($_REQUEST["cmode"]);
|
||||||
|
|
||||||
Article::catchupArticlesById($ids, $cmode);
|
if (count($ids) > 0)
|
||||||
|
Article::_catchup_by_id($ids, $cmode);
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS", "ids" => $ids));
|
print json_encode(["message" => "UPDATE_COUNTERS",
|
||||||
|
"labels" => Article::_labels_of($ids),
|
||||||
|
"feeds" => Article::_feeds_of($ids)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markSelected() {
|
function markSelected() {
|
||||||
$ids = explode(",", clean($_REQUEST["ids"]));
|
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
|
||||||
$cmode = (int)clean($_REQUEST["cmode"]);
|
$cmode = (int)clean($_REQUEST["cmode"]);
|
||||||
|
|
||||||
|
if (count($ids) > 0)
|
||||||
$this->markArticlesById($ids, $cmode);
|
$this->markArticlesById($ids, $cmode);
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(["message" => "UPDATE_COUNTERS", "feeds" => Article::_feeds_of($ids)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishSelected() {
|
function publishSelected() {
|
||||||
$ids = explode(",", clean($_REQUEST["ids"]));
|
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
|
||||||
$cmode = (int)clean($_REQUEST["cmode"]);
|
$cmode = (int)clean($_REQUEST["cmode"]);
|
||||||
|
|
||||||
|
if (count($ids) > 0)
|
||||||
$this->publishArticlesById($ids, $cmode);
|
$this->publishArticlesById($ids, $cmode);
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(["message" => "UPDATE_COUNTERS", "feeds" => Article::_feeds_of($ids)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanityCheck() {
|
function sanityCheck() {
|
||||||
$_SESSION["hasAudio"] = clean($_REQUEST["hasAudio"]) === "true";
|
|
||||||
$_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
|
$_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
|
||||||
$_SESSION["hasMp3"] = clean($_REQUEST["hasMp3"]) === "true";
|
|
||||||
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
|
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
|
||||||
|
|
||||||
$reply = array();
|
$error = Errors::E_SUCCESS;
|
||||||
|
|
||||||
$reply['error'] = sanity_check();
|
if (get_schema_version(true) != SCHEMA_VERSION) {
|
||||||
|
$error = Errors::E_SCHEMA_MISMATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error == Errors::E_SUCCESS) {
|
||||||
|
$reply = [];
|
||||||
|
|
||||||
if ($reply['error']['code'] == 0) {
|
|
||||||
$reply['init-params'] = $this->make_init_params();
|
$reply['init-params'] = $this->make_init_params();
|
||||||
$reply['runtime-info'] = $this->make_runtime_info();
|
$reply['runtime-info'] = $this->make_runtime_info();
|
||||||
}
|
|
||||||
|
|
||||||
print json_encode($reply);
|
print json_encode($reply);
|
||||||
|
} else {
|
||||||
|
print Errors::to_json($error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeLabels() {
|
/*function completeLabels() {
|
||||||
$search = clean($_REQUEST["search"]);
|
$search = clean($_REQUEST["search"]);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT DISTINCT caption FROM
|
$sth = $this->pdo->prepare("SELECT DISTINCT caption FROM
|
||||||
@@ -224,19 +172,19 @@ class RPC extends Handler_Protected {
|
|||||||
print "<li>" . $line["caption"] . "</li>";
|
print "<li>" . $line["caption"] . "</li>";
|
||||||
}
|
}
|
||||||
print "</ul>";
|
print "</ul>";
|
||||||
}
|
}*/
|
||||||
|
|
||||||
function catchupFeed() {
|
function catchupFeed() {
|
||||||
$feed_id = clean($_REQUEST['feed_id']);
|
$feed_id = clean($_REQUEST['feed_id']);
|
||||||
$is_cat = clean($_REQUEST['is_cat']) == "true";
|
$is_cat = clean($_REQUEST['is_cat']) == "true";
|
||||||
$mode = clean($_REQUEST['mode']);
|
$mode = clean($_REQUEST['mode'] ?? '');
|
||||||
$search_query = clean($_REQUEST['search_query']);
|
$search_query = clean($_REQUEST['search_query']);
|
||||||
$search_lang = clean($_REQUEST['search_lang']);
|
$search_lang = clean($_REQUEST['search_lang']);
|
||||||
|
|
||||||
Feeds::catchup_feed($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]);
|
Feeds::_catchup($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]);
|
||||||
|
|
||||||
// return counters here synchronously so that frontend can figure out next unread feed properly
|
// return counters here synchronously so that frontend can figure out next unread feed properly
|
||||||
print json_encode(['counters' => Counters::getAllCounters()]);
|
print json_encode(['counters' => Counters::get_all()]);
|
||||||
|
|
||||||
//print json_encode(array("message" => "UPDATE_COUNTERS"));
|
//print json_encode(array("message" => "UPDATE_COUNTERS"));
|
||||||
}
|
}
|
||||||
@@ -244,8 +192,9 @@ class RPC extends Handler_Protected {
|
|||||||
function setpanelmode() {
|
function setpanelmode() {
|
||||||
$wide = (int) clean($_REQUEST["wide"]);
|
$wide = (int) clean($_REQUEST["wide"]);
|
||||||
|
|
||||||
|
// FIXME should this use SESSION_COOKIE_LIFETIME and be renewed periodically?
|
||||||
setcookie("ttrss_widescreen", (string)$wide,
|
setcookie("ttrss_widescreen", (string)$wide,
|
||||||
time() + COOKIE_LIFETIME_LONG);
|
time() + 86400*365);
|
||||||
|
|
||||||
print json_encode(array("wide" => $wide));
|
print json_encode(array("wide" => $wide));
|
||||||
}
|
}
|
||||||
@@ -253,7 +202,7 @@ class RPC extends Handler_Protected {
|
|||||||
static function updaterandomfeed_real() {
|
static function updaterandomfeed_real() {
|
||||||
|
|
||||||
// Test if the feed need a update (update interval exceded).
|
// Test if the feed need a update (update interval exceded).
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$update_limit_qpart = "AND ((
|
$update_limit_qpart = "AND ((
|
||||||
ttrss_feeds.update_interval = 0
|
ttrss_feeds.update_interval = 0
|
||||||
AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_user_prefs.value || ' minutes') AS INTERVAL)
|
AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_user_prefs.value || ' minutes') AS INTERVAL)
|
||||||
@@ -278,7 +227,7 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test if feed is currently being updated by another process.
|
// Test if feed is currently being updated by another process.
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < NOW() - INTERVAL '5 minutes')";
|
$updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < NOW() - INTERVAL '5 minutes')";
|
||||||
} else {
|
} else {
|
||||||
$updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))";
|
$updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))";
|
||||||
@@ -324,7 +273,7 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Purge orphans and cleanup tags
|
// Purge orphans and cleanup tags
|
||||||
Article::purge_orphans();
|
Article::_purge_orphans();
|
||||||
//cleanup_tags(14, 50000);
|
//cleanup_tags(14, 50000);
|
||||||
|
|
||||||
if ($num_updated > 0) {
|
if ($num_updated > 0) {
|
||||||
@@ -382,23 +331,6 @@ class RPC extends Handler_Protected {
|
|||||||
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getlinktitlebyid() {
|
|
||||||
$id = clean($_REQUEST['id']);
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT link, title FROM ttrss_entries, ttrss_user_entries
|
|
||||||
WHERE ref_id = ? AND ref_id = id AND owner_uid = ?");
|
|
||||||
$sth->execute([$id, $_SESSION['uid']]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
|
||||||
$link = $row['link'];
|
|
||||||
$title = $row['title'];
|
|
||||||
|
|
||||||
echo json_encode(array("link" => $link, "title" => $title));
|
|
||||||
} else {
|
|
||||||
echo json_encode(array("error" => "ARTICLE_NOT_FOUND"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function log() {
|
function log() {
|
||||||
$msg = clean($_REQUEST['msg']);
|
$msg = clean($_REQUEST['msg']);
|
||||||
$file = basename(clean($_REQUEST['file']));
|
$file = basename(clean($_REQUEST['file']));
|
||||||
@@ -410,10 +342,7 @@ class RPC extends Handler_Protected {
|
|||||||
$msg, 'client-js:' . $file, $line, $context);
|
$msg, 'client-js:' . $file, $line, $context);
|
||||||
|
|
||||||
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
|
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
|
||||||
} else {
|
|
||||||
echo json_encode(array("error" => "MESSAGE_NOT_FOUND"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkforupdates() {
|
function checkforupdates() {
|
||||||
@@ -424,7 +353,7 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
get_version($git_commit, $git_timestamp);
|
get_version($git_commit, $git_timestamp);
|
||||||
|
|
||||||
if (defined('CHECK_FOR_UPDATES') && CHECK_FOR_UPDATES && $_SESSION["access_level"] >= 10 && $git_timestamp) {
|
if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= 10 && $git_timestamp) {
|
||||||
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
|
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
|
||||||
|
|
||||||
if ($content) {
|
if ($content) {
|
||||||
@@ -455,9 +384,9 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$params["safe_mode"] = !empty($_SESSION["safe_mode"]);
|
$params["safe_mode"] = !empty($_SESSION["safe_mode"]);
|
||||||
$params["check_for_updates"] = CHECK_FOR_UPDATES;
|
$params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES);
|
||||||
$params["icons_url"] = ICONS_URL;
|
$params["icons_url"] = Config::get(Config::ICONS_URL);
|
||||||
$params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
|
$params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME);
|
||||||
$params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
|
$params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
|
||||||
$params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
|
$params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
|
||||||
$params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
|
$params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
|
||||||
@@ -486,16 +415,11 @@ class RPC extends Handler_Protected {
|
|||||||
$params["self_url_prefix"] = get_self_url_prefix();
|
$params["self_url_prefix"] = get_self_url_prefix();
|
||||||
$params["max_feed_id"] = (int) $max_feed_id;
|
$params["max_feed_id"] = (int) $max_feed_id;
|
||||||
$params["num_feeds"] = (int) $num_feeds;
|
$params["num_feeds"] = (int) $num_feeds;
|
||||||
|
|
||||||
$params["hotkeys"] = $this->get_hotkeys_map();
|
$params["hotkeys"] = $this->get_hotkeys_map();
|
||||||
|
|
||||||
$params["widescreen"] = (int) ($_COOKIE["ttrss_widescreen"] ?? 0);
|
$params["widescreen"] = (int) ($_COOKIE["ttrss_widescreen"] ?? 0);
|
||||||
|
$params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE);
|
||||||
$params['simple_update'] = SIMPLE_UPDATE_MODE;
|
|
||||||
|
|
||||||
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
|
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
|
||||||
|
$params["labels"] = Labels::get_all($_SESSION["uid"]);
|
||||||
$params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
|
|
||||||
|
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
@@ -526,10 +450,10 @@ class RPC extends Handler_Protected {
|
|||||||
$data["max_feed_id"] = (int) $max_feed_id;
|
$data["max_feed_id"] = (int) $max_feed_id;
|
||||||
$data["num_feeds"] = (int) $num_feeds;
|
$data["num_feeds"] = (int) $num_feeds;
|
||||||
$data['cdm_expanded'] = get_pref('CDM_EXPANDED');
|
$data['cdm_expanded'] = get_pref('CDM_EXPANDED');
|
||||||
$data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
|
$data["labels"] = Labels::get_all($_SESSION["uid"]);
|
||||||
|
|
||||||
if (LOG_DESTINATION == 'sql' && $_SESSION['access_level'] >= 10) {
|
if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= 10) {
|
||||||
if (DB_TYPE == 'pgsql') {
|
if (Config::get(Config::DB_TYPE) == 'pgsql') {
|
||||||
$log_interval = "created_at > NOW() - interval '1 hour'";
|
$log_interval = "created_at > NOW() - interval '1 hour'";
|
||||||
} else {
|
} else {
|
||||||
$log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
|
$log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
|
||||||
@@ -538,7 +462,7 @@ class RPC extends Handler_Protected {
|
|||||||
$sth = $pdo->prepare("SELECT COUNT(id) AS cid
|
$sth = $pdo->prepare("SELECT COUNT(id) AS cid
|
||||||
FROM ttrss_error_log
|
FROM ttrss_error_log
|
||||||
WHERE
|
WHERE
|
||||||
errno != 1024 AND
|
errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND
|
||||||
$log_interval AND
|
$log_interval AND
|
||||||
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
|
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
|
||||||
$sth->execute();
|
$sth->execute();
|
||||||
@@ -548,13 +472,13 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
|
if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) {
|
||||||
|
|
||||||
$data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
|
$data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
|
||||||
|
|
||||||
if (time() - ($_SESSION["daemon_stamp_check"] ?? 0) > 30) {
|
if (time() - ($_SESSION["daemon_stamp_check"] ?? 0) > 30) {
|
||||||
|
|
||||||
$stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
|
$stamp = (int) @file_get_contents(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.stamp");
|
||||||
|
|
||||||
if ($stamp) {
|
if ($stamp) {
|
||||||
$stamp_delta = time() - $stamp;
|
$stamp_delta = time() - $stamp;
|
||||||
@@ -737,4 +661,73 @@ class RPC extends Handler_Protected {
|
|||||||
return array($prefixes, $hotkeys);
|
return array($prefixes, $hotkeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hotkeyHelp() {
|
||||||
|
$info = self::get_hotkeys_info();
|
||||||
|
$imap = self::get_hotkeys_map();
|
||||||
|
$omap = array();
|
||||||
|
|
||||||
|
foreach ($imap[1] as $sequence => $action) {
|
||||||
|
if (!isset($omap[$action])) $omap[$action] = array();
|
||||||
|
|
||||||
|
array_push($omap[$action], $sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ($info as $section => $hotkeys) {
|
||||||
|
?>
|
||||||
|
<li><h3><?= $section ?></h3></li>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ($hotkeys as $action => $description) {
|
||||||
|
|
||||||
|
if (!empty($omap[$action])) {
|
||||||
|
foreach ($omap[$action] as $sequence) {
|
||||||
|
if (strpos($sequence, "|") !== false) {
|
||||||
|
$sequence = substr($sequence,
|
||||||
|
strpos($sequence, "|")+1,
|
||||||
|
strlen($sequence));
|
||||||
|
} else {
|
||||||
|
$keys = explode(" ", $sequence);
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($keys); $i++) {
|
||||||
|
if (strlen($keys[$i]) > 1) {
|
||||||
|
$tmp = '';
|
||||||
|
foreach (str_split($keys[$i]) as $c) {
|
||||||
|
switch ($c) {
|
||||||
|
case '*':
|
||||||
|
$tmp .= __('Shift') . '+';
|
||||||
|
break;
|
||||||
|
case '^':
|
||||||
|
$tmp .= __('Ctrl') . '+';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$tmp .= $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$keys[$i] = $tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sequence = join(" ", $keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<div class='hk'><code><?= $sequence ?></code></div>
|
||||||
|
<div class='desc'><?= $description ?></div>
|
||||||
|
</li>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</ul>
|
||||||
|
<footer class='text-center'>
|
||||||
|
<?= \Controls\submit_tag(__('Close this window')) ?>
|
||||||
|
</footer>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ 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 CACHE_MAX_DAYS days
|
// check icon files once every Config::get(Config::CACHE_MAX_DAYS) days
|
||||||
$icon_files = array_filter(glob(ICONS_DIR . "/*.ico"),
|
$icon_files = array_filter(glob(Config::get(Config::ICONS_DIR) . "/*.ico"),
|
||||||
function($f) { return filemtime($f) < time() - 86400*CACHE_MAX_DAYS; });
|
function($f) { return filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS); });
|
||||||
|
|
||||||
foreach ($icon_files as $icon) {
|
foreach ($icon_files as $icon) {
|
||||||
$feed_id = basename($icon, ".ico");
|
$feed_id = basename($icon, ".ico");
|
||||||
@@ -52,26 +52,28 @@ class RSSUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function update_daemon_common($limit = DAEMON_FEED_LIMIT, $options = []) {
|
static function update_daemon_common($limit = null, $options = []) {
|
||||||
$schema_version = get_schema_version();
|
$schema_version = get_schema_version();
|
||||||
|
|
||||||
|
if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT);
|
||||||
|
|
||||||
if ($schema_version != SCHEMA_VERSION) {
|
if ($schema_version != SCHEMA_VERSION) {
|
||||||
die("Schema version is wrong, please upgrade the database.\n");
|
die("Schema version is wrong, please upgrade the database.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
if (!SINGLE_USER_MODE && DAEMON_UPDATE_LOGIN_LIMIT > 0) {
|
if (!Config::get(Config::SINGLE_USER_MODE) && Config::get(Config::DAEMON_UPDATE_LOGIN_LIMIT) > 0) {
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$login_thresh_qpart = "AND ttrss_users.last_login >= NOW() - INTERVAL '".DAEMON_UPDATE_LOGIN_LIMIT." days'";
|
$login_thresh_qpart = "AND ttrss_users.last_login >= NOW() - INTERVAL '".Config::get(Config::DAEMON_UPDATE_LOGIN_LIMIT)." days'";
|
||||||
} else {
|
} else {
|
||||||
$login_thresh_qpart = "AND ttrss_users.last_login >= DATE_SUB(NOW(), INTERVAL ".DAEMON_UPDATE_LOGIN_LIMIT." DAY)";
|
$login_thresh_qpart = "AND ttrss_users.last_login >= DATE_SUB(NOW(), INTERVAL ".Config::get(Config::DAEMON_UPDATE_LOGIN_LIMIT)." DAY)";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$login_thresh_qpart = "";
|
$login_thresh_qpart = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$update_limit_qpart = "AND ((
|
$update_limit_qpart = "AND ((
|
||||||
ttrss_feeds.update_interval = 0
|
ttrss_feeds.update_interval = 0
|
||||||
AND ttrss_user_prefs.value != '-1'
|
AND ttrss_user_prefs.value != '-1'
|
||||||
@@ -96,7 +98,7 @@ class RSSUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test if feed is currently being updated by another process.
|
// Test if feed is currently being updated by another process.
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '10 minutes')";
|
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '10 minutes')";
|
||||||
} else {
|
} else {
|
||||||
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 10 MINUTE))";
|
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 10 MINUTE))";
|
||||||
@@ -106,7 +108,7 @@ class RSSUtils {
|
|||||||
|
|
||||||
// Update the least recently updated feeds first
|
// Update the least recently updated feeds first
|
||||||
$query_order = "ORDER BY last_updated";
|
$query_order = "ORDER BY last_updated";
|
||||||
if (DB_TYPE == "pgsql") $query_order .= " NULLS FIRST";
|
if (Config::get(Config::DB_TYPE) == "pgsql") $query_order .= " NULLS FIRST";
|
||||||
|
|
||||||
$query = "SELECT DISTINCT ttrss_feeds.feed_url, ttrss_feeds.last_updated
|
$query = "SELECT DISTINCT ttrss_feeds.feed_url, ttrss_feeds.last_updated
|
||||||
FROM
|
FROM
|
||||||
@@ -182,7 +184,7 @@ class RSSUtils {
|
|||||||
if (self::function_enabled('passthru')) {
|
if (self::function_enabled('passthru')) {
|
||||||
$exit_code = 0;
|
$exit_code = 0;
|
||||||
|
|
||||||
passthru(PHP_EXECUTABLE . " update.php --update-feed " . $tline["id"] . " --pidlock feed-" . $tline["id"] . " $quiet $log $log_level", $exit_code);
|
passthru(Config::get(Config::PHP_EXECUTABLE) . " update.php --update-feed " . $tline["id"] . " --pidlock feed-" . $tline["id"] . " $quiet $log $log_level", $exit_code);
|
||||||
|
|
||||||
Debug::log(sprintf("<= %.4f (sec) exit code: %d", microtime(true) - $fstarted, $exit_code));
|
Debug::log(sprintf("<= %.4f (sec) exit code: %d", microtime(true) - $fstarted, $exit_code));
|
||||||
|
|
||||||
@@ -275,7 +277,7 @@ class RSSUtils {
|
|||||||
$pluginhost = new PluginHost();
|
$pluginhost = new PluginHost();
|
||||||
$user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
|
$user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
|
||||||
|
|
||||||
$pluginhost->load(PLUGINS, PluginHost::KIND_ALL);
|
$pluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
|
||||||
$pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
|
$pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
|
||||||
//$pluginhost->load_data();
|
//$pluginhost->load_data();
|
||||||
|
|
||||||
@@ -288,7 +290,7 @@ class RSSUtils {
|
|||||||
if (!$basic_info) {
|
if (!$basic_info) {
|
||||||
$feed_data = UrlHelper::fetch($fetch_url, false,
|
$feed_data = UrlHelper::fetch($fetch_url, false,
|
||||||
$auth_login, $auth_pass, false,
|
$auth_login, $auth_pass, false,
|
||||||
FEED_FETCH_TIMEOUT,
|
Config::get(Config::FEED_FETCH_TIMEOUT),
|
||||||
0);
|
0);
|
||||||
|
|
||||||
$feed_data = trim($feed_data);
|
$feed_data = trim($feed_data);
|
||||||
@@ -395,12 +397,12 @@ class RSSUtils {
|
|||||||
|
|
||||||
$date_feed_processed = date('Y-m-d H:i');
|
$date_feed_processed = date('Y-m-d H:i');
|
||||||
|
|
||||||
$cache_filename = CACHE_DIR . "/feeds/" . sha1($fetch_url) . ".xml";
|
$cache_filename = Config::get(Config::CACHE_DIR) . "/feeds/" . sha1($fetch_url) . ".xml";
|
||||||
|
|
||||||
$pluginhost = new PluginHost();
|
$pluginhost = new PluginHost();
|
||||||
$user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
|
$user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
|
||||||
|
|
||||||
$pluginhost->load(PLUGINS, PluginHost::KIND_ALL);
|
$pluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
|
||||||
$pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
|
$pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
|
||||||
//$pluginhost->load_data();
|
//$pluginhost->load_data();
|
||||||
|
|
||||||
@@ -455,7 +457,7 @@ class RSSUtils {
|
|||||||
Debug::log("not using CURL due to open_basedir restrictions", Debug::$LOG_VERBOSE);
|
Debug::log("not using CURL due to open_basedir restrictions", Debug::$LOG_VERBOSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time() - strtotime($last_unconditional) > MAX_CONDITIONAL_INTERVAL) {
|
if (time() - strtotime($last_unconditional) > Config::get(Config::MAX_CONDITIONAL_INTERVAL)) {
|
||||||
Debug::log("maximum allowed interval for conditional requests exceeded, forcing refetch", Debug::$LOG_VERBOSE);
|
Debug::log("maximum allowed interval for conditional requests exceeded, forcing refetch", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
$force_refetch = true;
|
$force_refetch = true;
|
||||||
@@ -469,7 +471,7 @@ class RSSUtils {
|
|||||||
"url" => $fetch_url,
|
"url" => $fetch_url,
|
||||||
"login" => $auth_login,
|
"login" => $auth_login,
|
||||||
"pass" => $auth_pass,
|
"pass" => $auth_pass,
|
||||||
"timeout" => $no_cache ? FEED_FETCH_NO_CACHE_TIMEOUT : FEED_FETCH_TIMEOUT,
|
"timeout" => $no_cache ? Config::get(Config::FEED_FETCH_NO_CACHE_TIMEOUT) : Config::get(Config::FEED_FETCH_TIMEOUT),
|
||||||
"last_modified" => $force_refetch ? "" : $stored_last_modified
|
"last_modified" => $force_refetch ? "" : $stored_last_modified
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -488,7 +490,7 @@ class RSSUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cache vanilla feed data for re-use
|
// cache vanilla feed data for re-use
|
||||||
if ($feed_data && !$auth_pass && !$auth_login && is_writable(CACHE_DIR . "/feeds")) {
|
if ($feed_data && !$auth_pass && !$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) {
|
||||||
@@ -561,7 +563,7 @@ class RSSUtils {
|
|||||||
Debug::log("language: $feed_language", Debug::$LOG_VERBOSE);
|
Debug::log("language: $feed_language", Debug::$LOG_VERBOSE);
|
||||||
Debug::log("processing feed data...", Debug::$LOG_VERBOSE);
|
Debug::log("processing feed data...", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
if (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 {
|
||||||
$favicon_interval_qpart = "favicon_last_checked < DATE_SUB(NOW(), INTERVAL 12 HOUR)";
|
$favicon_interval_qpart = "favicon_last_checked < DATE_SUB(NOW(), INTERVAL 12 HOUR)";
|
||||||
@@ -591,10 +593,10 @@ class RSSUtils {
|
|||||||
/* terrible hack: if we crash on floicon shit here, we won't check
|
/* terrible hack: if we crash on floicon shit here, we won't check
|
||||||
* the icon avgcolor again (unless the icon got updated) */
|
* the icon avgcolor again (unless the icon got updated) */
|
||||||
|
|
||||||
$favicon_file = ICONS_DIR . "/$feed.ico";
|
$favicon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
|
||||||
$favicon_modified = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
|
$favicon_modified = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
|
||||||
|
|
||||||
Debug::log("checking favicon...", Debug::$LOG_VERBOSE);
|
Debug::log("checking favicon for feed $feed...", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
self::check_feed_favicon($site_url, $feed);
|
self::check_feed_favicon($site_url, $feed);
|
||||||
$favicon_modified_new = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
|
$favicon_modified_new = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
|
||||||
@@ -610,7 +612,7 @@ class RSSUtils {
|
|||||||
id = ?");
|
id = ?");
|
||||||
$sth->execute([$feed]);
|
$sth->execute([$feed]);
|
||||||
|
|
||||||
$favicon_color = calculate_avg_color($favicon_file);
|
$favicon_color = \Colors\calculate_avg_color($favicon_file);
|
||||||
|
|
||||||
$favicon_colorstring = ",favicon_avg_color = " . $pdo->quote($favicon_color);
|
$favicon_colorstring = ",favicon_avg_color = " . $pdo->quote($favicon_color);
|
||||||
|
|
||||||
@@ -723,9 +725,9 @@ class RSSUtils {
|
|||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$base_entry_id = $row["id"];
|
$base_entry_id = $row["id"];
|
||||||
$entry_stored_hash = $row["content_hash"];
|
$entry_stored_hash = $row["content_hash"];
|
||||||
$article_labels = Article::get_article_labels($base_entry_id, $owner_uid);
|
$article_labels = Article::_get_labels($base_entry_id, $owner_uid);
|
||||||
|
|
||||||
$existing_tags = Article::get_article_tags($base_entry_id, $owner_uid);
|
$existing_tags = Article::_get_tags($base_entry_id, $owner_uid);
|
||||||
$entry_tags = array_unique(array_merge($entry_tags, $existing_tags));
|
$entry_tags = array_unique(array_merge($entry_tags, $existing_tags));
|
||||||
} else {
|
} else {
|
||||||
$base_entry_id = false;
|
$base_entry_id = false;
|
||||||
@@ -739,7 +741,7 @@ class RSSUtils {
|
|||||||
|
|
||||||
$enclosures = array();
|
$enclosures = array();
|
||||||
|
|
||||||
$encs = $item->get_enclosures();
|
$encs = $item->_get_enclosures();
|
||||||
|
|
||||||
if (is_array($encs)) {
|
if (is_array($encs)) {
|
||||||
foreach ($encs as $e) {
|
foreach ($encs as $e) {
|
||||||
@@ -755,7 +757,7 @@ class RSSUtils {
|
|||||||
$e->type, $e->length, $e->title, $e->width, $e->height);
|
$e->type, $e->length, $e->title, $e->width, $e->height);
|
||||||
|
|
||||||
// Yet another episode of "mysql utf8_general_ci is gimped"
|
// Yet another episode of "mysql utf8_general_ci is gimped"
|
||||||
if (DB_TYPE == "mysql" && MYSQL_CHARSET != "UTF8MB4") {
|
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET) != "UTF8MB4") {
|
||||||
for ($i = 0; $i < count($e_item); $i++) {
|
for ($i = 0; $i < count($e_item); $i++) {
|
||||||
if (is_string($e_item[$i])) {
|
if (is_string($e_item[$i])) {
|
||||||
$e_item[$i] = self::strip_utf8mb4($e_item[$i]);
|
$e_item[$i] = self::strip_utf8mb4($e_item[$i]);
|
||||||
@@ -833,7 +835,7 @@ class RSSUtils {
|
|||||||
Debug::log("plugin data: $entry_plugin_data", Debug::$LOG_VERBOSE);
|
Debug::log("plugin data: $entry_plugin_data", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
// Workaround: 4-byte unicode requires utf8mb4 in MySQL. See https://tt-rss.org/forum/viewtopic.php?f=1&t=3377&p=20077#p20077
|
// Workaround: 4-byte unicode requires utf8mb4 in MySQL. See https://tt-rss.org/forum/viewtopic.php?f=1&t=3377&p=20077#p20077
|
||||||
if (DB_TYPE == "mysql" && MYSQL_CHARSET != "UTF8MB4") {
|
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET) != "UTF8MB4") {
|
||||||
foreach ($article as $k => $v) {
|
foreach ($article as $k => $v) {
|
||||||
// i guess we'll have to take the risk of 4byte unicode labels & tags here
|
// i guess we'll have to take the risk of 4byte unicode labels & tags here
|
||||||
if (is_string($article[$k])) {
|
if (is_string($article[$k])) {
|
||||||
@@ -1079,7 +1081,7 @@ class RSSUtils {
|
|||||||
|
|
||||||
Debug::log("resulting RID: $entry_ref_id, IID: $entry_int_id", Debug::$LOG_VERBOSE);
|
Debug::log("resulting RID: $entry_ref_id, IID: $entry_int_id", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql")
|
if (Config::get(Config::DB_TYPE) == "pgsql")
|
||||||
$tsvector_qpart = "tsvector_combined = to_tsvector(:ts_lang, :ts_content),";
|
$tsvector_qpart = "tsvector_combined = to_tsvector(:ts_lang, :ts_content),";
|
||||||
else
|
else
|
||||||
$tsvector_qpart = "";
|
$tsvector_qpart = "";
|
||||||
@@ -1107,7 +1109,7 @@ class RSSUtils {
|
|||||||
":lang" => $entry_language,
|
":lang" => $entry_language,
|
||||||
":id" => $ref_id];
|
":id" => $ref_id];
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$params[":ts_lang"] = $feed_language;
|
$params[":ts_lang"] = $feed_language;
|
||||||
$params[":ts_content"] = mb_substr(strip_tags($entry_title . " " . $entry_content), 0, 900000);
|
$params[":ts_content"] = mb_substr(strip_tags($entry_title . " " . $entry_content), 0, 900000);
|
||||||
}
|
}
|
||||||
@@ -1239,7 +1241,7 @@ class RSSUtils {
|
|||||||
|
|
||||||
Debug::log("purging feed...", Debug::$LOG_VERBOSE);
|
Debug::log("purging feed...", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
Feeds::purge_feed($feed, 0);
|
Feeds::_purge($feed, 0);
|
||||||
|
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_feeds SET
|
$sth = $pdo->prepare("UPDATE ttrss_feeds SET
|
||||||
last_updated = NOW(),
|
last_updated = NOW(),
|
||||||
@@ -1281,7 +1283,7 @@ class RSSUtils {
|
|||||||
static function cache_enclosures($enclosures, $site_url) {
|
static function cache_enclosures($enclosures, $site_url) {
|
||||||
$cache = new DiskCache("images");
|
$cache = new DiskCache("images");
|
||||||
|
|
||||||
if ($cache->isWritable()) {
|
if ($cache->is_writable()) {
|
||||||
foreach ($enclosures as $enc) {
|
foreach ($enclosures as $enc) {
|
||||||
|
|
||||||
if (preg_match("/(image|audio|video)/", $enc[1])) {
|
if (preg_match("/(image|audio|video)/", $enc[1])) {
|
||||||
@@ -1298,7 +1300,7 @@ class RSSUtils {
|
|||||||
|
|
||||||
$file_content = UrlHelper::fetch(array("url" => $src,
|
$file_content = UrlHelper::fetch(array("url" => $src,
|
||||||
"http_referrer" => $src,
|
"http_referrer" => $src,
|
||||||
"max_size" => MAX_CACHE_FILE_SIZE));
|
"max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)));
|
||||||
|
|
||||||
if ($file_content) {
|
if ($file_content) {
|
||||||
$cache->put($local_filename, $file_content);
|
$cache->put($local_filename, $file_content);
|
||||||
@@ -1328,14 +1330,14 @@ class RSSUtils {
|
|||||||
|
|
||||||
$file_content = UrlHelper::fetch(array("url" => $url,
|
$file_content = UrlHelper::fetch(array("url" => $url,
|
||||||
"http_referrer" => $url,
|
"http_referrer" => $url,
|
||||||
"max_size" => MAX_CACHE_FILE_SIZE));
|
"max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)));
|
||||||
|
|
||||||
if ($file_content) {
|
if ($file_content) {
|
||||||
$cache->put($local_filename, $file_content);
|
$cache->put($local_filename, $file_content);
|
||||||
} else {
|
} else {
|
||||||
Debug::log("cache_media: failed with $fetch_last_error_code: $fetch_last_error");
|
Debug::log("cache_media: failed with $fetch_last_error_code: $fetch_last_error");
|
||||||
}
|
}
|
||||||
} else if ($cache->isWritable($local_filename)) {
|
} else if ($cache->is_writable($local_filename)) {
|
||||||
$cache->touch($local_filename);
|
$cache->touch($local_filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1344,7 +1346,7 @@ class RSSUtils {
|
|||||||
static function cache_media($html, $site_url) {
|
static function cache_media($html, $site_url) {
|
||||||
$cache = new DiskCache("images");
|
$cache = new DiskCache("images");
|
||||||
|
|
||||||
if ($html && $cache->isWritable()) {
|
if ($html && $cache->is_writable()) {
|
||||||
$doc = new DOMDocument();
|
$doc = new DOMDocument();
|
||||||
if (@$doc->loadHTML($html)) {
|
if (@$doc->loadHTML($html)) {
|
||||||
$xpath = new DOMXPath($doc);
|
$xpath = new DOMXPath($doc);
|
||||||
@@ -1375,7 +1377,7 @@ class RSSUtils {
|
|||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$pdo->query("DELETE FROM ttrss_error_log
|
$pdo->query("DELETE FROM ttrss_error_log
|
||||||
WHERE created_at < NOW() - INTERVAL '7 days'");
|
WHERE created_at < NOW() - INTERVAL '7 days'");
|
||||||
} else {
|
} else {
|
||||||
@@ -1396,8 +1398,8 @@ class RSSUtils {
|
|||||||
|
|
||||||
$num_deleted = 0;
|
$num_deleted = 0;
|
||||||
|
|
||||||
if (is_writable(LOCK_DIRECTORY)) {
|
if (is_writable(Config::get(Config::LOCK_DIRECTORY))) {
|
||||||
$files = glob(LOCK_DIRECTORY . "/*.lock");
|
$files = glob(Config::get(Config::LOCK_DIRECTORY) . "/*.lock");
|
||||||
|
|
||||||
if ($files) {
|
if ($files) {
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
@@ -1581,17 +1583,17 @@ class RSSUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static function disable_failed_feeds() {
|
static function disable_failed_feeds() {
|
||||||
if (defined('DAEMON_UNSUCCESSFUL_DAYS_LIMIT') && DAEMON_UNSUCCESSFUL_DAYS_LIMIT > 0) {
|
if (Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT) > 0) {
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
$days = DAEMON_UNSUCCESSFUL_DAYS_LIMIT;
|
$days = Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT);
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
$interval_query = "last_successful_update < NOW() - INTERVAL '$days days' AND last_updated > NOW() - INTERVAL '1 days'";
|
$interval_query = "last_successful_update < NOW() - INTERVAL '$days days' AND last_updated > NOW() - INTERVAL '1 days'";
|
||||||
} else /* if (DB_TYPE == "mysql") */ {
|
} else /* if (Config::get(Config::DB_TYPE) == "mysql") */ {
|
||||||
$interval_query = "last_successful_update < DATE_SUB(NOW(), INTERVAL $days DAY) AND last_updated > DATE_SUB(NOW(), INTERVAL 1 DAY)";
|
$interval_query = "last_successful_update < DATE_SUB(NOW(), INTERVAL $days DAY) AND last_updated > DATE_SUB(NOW(), INTERVAL 1 DAY)";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1604,10 +1606,10 @@ class RSSUtils {
|
|||||||
while ($row = $sth->fetch()) {
|
while ($row = $sth->fetch()) {
|
||||||
Logger::get()->log(E_USER_NOTICE,
|
Logger::get()->log(E_USER_NOTICE,
|
||||||
sprintf("Auto disabling feed %d (%s, UID: %d) because it failed to update for %d days.",
|
sprintf("Auto disabling feed %d (%s, UID: %d) because it failed to update for %d days.",
|
||||||
$row["id"], clean($row["title"]), $row["owner_uid"], DAEMON_UNSUCCESSFUL_DAYS_LIMIT));
|
$row["id"], clean($row["title"]), $row["owner_uid"], Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT)));
|
||||||
|
|
||||||
Debug::log(sprintf("Auto-disabling feed %d (%s) (failed to update for %d days).", $row["id"],
|
Debug::log(sprintf("Auto-disabling feed %d (%s) (failed to update for %d days).", $row["id"],
|
||||||
clean($row["title"]), DAEMON_UNSUCCESSFUL_DAYS_LIMIT));
|
clean($row["title"]), Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_feeds SET update_interval = -1 WHERE
|
$sth = $pdo->prepare("UPDATE ttrss_feeds SET update_interval = -1 WHERE
|
||||||
@@ -1636,25 +1638,32 @@ class RSSUtils {
|
|||||||
self::cleanup_feed_icons();
|
self::cleanup_feed_icons();
|
||||||
self::disable_failed_feeds();
|
self::disable_failed_feeds();
|
||||||
|
|
||||||
Article::purge_orphans();
|
Article::_purge_orphans();
|
||||||
self::cleanup_counters_cache();
|
self::cleanup_counters_cache();
|
||||||
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
|
||||||
}
|
}
|
||||||
|
|
||||||
static function check_feed_favicon($site_url, $feed) {
|
static function check_feed_favicon($site_url, $feed) {
|
||||||
# print "FAVICON [$site_url]: $favicon_url\n";
|
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
|
||||||
|
|
||||||
$icon_file = ICONS_DIR . "/$feed.ico";
|
|
||||||
|
|
||||||
if (!file_exists($icon_file)) {
|
|
||||||
$favicon_url = self::get_favicon_url($site_url);
|
$favicon_url = self::get_favicon_url($site_url);
|
||||||
|
if (!$favicon_url) {
|
||||||
|
Debug::log("couldn't find favicon URL in $site_url", Debug::$LOG_VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ($favicon_url) {
|
|
||||||
// Limiting to "image" type misses those served with text/plain
|
// Limiting to "image" type misses those served with text/plain
|
||||||
$contents = UrlHelper::fetch($favicon_url); // , "image");
|
$contents = UrlHelper::fetch([
|
||||||
|
'url' => $favicon_url,
|
||||||
|
'max_size' => Config::get(Config::MAX_FAVICON_FILE_SIZE),
|
||||||
|
//'type' => 'image',
|
||||||
|
]);
|
||||||
|
if (!$contents) {
|
||||||
|
Debug::log("fetching favicon $favicon_url failed", Debug::$LOG_VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ($contents) {
|
|
||||||
// Crude image type matching.
|
// Crude image type matching.
|
||||||
// Patterns gleaned from the file(1) source code.
|
// Patterns gleaned from the file(1) source code.
|
||||||
if (preg_match('/^\x00\x00\x01\x00/', $contents)) {
|
if (preg_match('/^\x00\x00\x01\x00/', $contents)) {
|
||||||
@@ -1679,23 +1688,25 @@ class RSSUtils {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//error_log("check_feed_favicon: favicon_url=$favicon_url isa UNKNOWN type");
|
//error_log("check_feed_favicon: favicon_url=$favicon_url isa UNKNOWN type");
|
||||||
$contents = "";
|
Debug::log("favicon $favicon_url type is unknown (not updating)", Debug::$LOG_VERBOSE);
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($contents) {
|
Debug::log("setting contents of $icon_file", Debug::$LOG_VERBOSE);
|
||||||
|
|
||||||
$fp = @fopen($icon_file, "w");
|
$fp = @fopen($icon_file, "w");
|
||||||
|
if (!$fp) {
|
||||||
|
Debug::log("failed to open $icon_file for writing", Debug::$LOG_VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ($fp) {
|
|
||||||
fwrite($fp, $contents);
|
fwrite($fp, $contents);
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
chmod($icon_file, 0644);
|
chmod($icon_file, 0644);
|
||||||
}
|
clearstatcache();
|
||||||
}
|
|
||||||
}
|
|
||||||
return $icon_file;
|
return $icon_file;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static function is_gzipped($feed_data) {
|
static function is_gzipped($feed_data) {
|
||||||
return strpos(substr($feed_data, 0, 3),
|
return strpos(substr($feed_data, 0, 3),
|
||||||
@@ -1706,7 +1717,7 @@ class RSSUtils {
|
|||||||
$filters = array();
|
$filters = array();
|
||||||
|
|
||||||
$feed_id = (int) $feed_id;
|
$feed_id = (int) $feed_id;
|
||||||
$cat_id = (int)Feeds::getFeedCategory($feed_id);
|
$cat_id = (int)Feeds::_cat_of_feed($feed_id);
|
||||||
|
|
||||||
if ($cat_id == 0)
|
if ($cat_id == 0)
|
||||||
$null_cat_qpart = "cat_id IS NULL OR";
|
$null_cat_qpart = "cat_id IS NULL OR";
|
||||||
@@ -1720,7 +1731,7 @@ class RSSUtils {
|
|||||||
$sth->execute([$owner_uid]);
|
$sth->execute([$owner_uid]);
|
||||||
|
|
||||||
$check_cats = array_merge(
|
$check_cats = array_merge(
|
||||||
Feeds::getParentCategories($cat_id, $owner_uid),
|
Feeds::_get_parent_cats($cat_id, $owner_uid),
|
||||||
[$cat_id]);
|
[$cat_id]);
|
||||||
|
|
||||||
$check_cats_str = join(",", $check_cats);
|
$check_cats_str = join(",", $check_cats);
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ class UrlHelper {
|
|||||||
'protocol_version'=> 1.1)
|
'protocol_version'=> 1.1)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (defined('_HTTP_PROXY')) {
|
if (Config::get(Config::HTTP_PROXY)) {
|
||||||
$context_options['http']['request_fulluri'] = true;
|
$context_options['http']['request_fulluri'] = true;
|
||||||
$context_options['http']['proxy'] = _HTTP_PROXY;
|
$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
$context = stream_context_create($context_options);
|
$context = stream_context_create($context_options);
|
||||||
@@ -209,7 +209,7 @@ class UrlHelper {
|
|||||||
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
|
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
|
||||||
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
|
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
|
||||||
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
|
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
|
||||||
$max_size = isset($options["max_size"]) ? $options["max_size"] : MAX_DOWNLOAD_FILE_SIZE; // in bytes
|
$max_size = isset($options["max_size"]) ? $options["max_size"] : Config::get(Config::MAX_DOWNLOAD_FILE_SIZE); // in bytes
|
||||||
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
|
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
|
||||||
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
|
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ class UrlHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
|
if (function_exists('curl_init') && !ini_get("open_basedir")) {
|
||||||
|
|
||||||
$fetch_curl_used = true;
|
$fetch_curl_used = true;
|
||||||
|
|
||||||
@@ -250,8 +250,8 @@ class UrlHelper {
|
|||||||
if (count($curl_http_headers) > 0)
|
if (count($curl_http_headers) > 0)
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
|
||||||
|
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
|
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT));
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
|
||||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
|
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
|
||||||
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
|
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
|
||||||
@@ -283,8 +283,8 @@ class UrlHelper {
|
|||||||
curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
|
curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined('_HTTP_PROXY')) {
|
if (Config::get(Config::HTTP_PROXY)) {
|
||||||
curl_setopt($ch, CURLOPT_PROXY, _HTTP_PROXY);
|
curl_setopt($ch, CURLOPT_PROXY, Config::get(Config::HTTP_PROXY));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($post_query) {
|
if ($post_query) {
|
||||||
@@ -395,7 +395,7 @@ class UrlHelper {
|
|||||||
),
|
),
|
||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
'ignore_errors' => true,
|
'ignore_errors' => true,
|
||||||
'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
|
'timeout' => $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT),
|
||||||
'protocol_version'=> 1.1)
|
'protocol_version'=> 1.1)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -408,16 +408,16 @@ class UrlHelper {
|
|||||||
if ($http_referrer)
|
if ($http_referrer)
|
||||||
array_push($context_options['http']['header'], "Referer: $http_referrer");
|
array_push($context_options['http']['header'], "Referer: $http_referrer");
|
||||||
|
|
||||||
if (defined('_HTTP_PROXY')) {
|
if (Config::get(Config::HTTP_PROXY)) {
|
||||||
$context_options['http']['request_fulluri'] = true;
|
$context_options['http']['request_fulluri'] = true;
|
||||||
$context_options['http']['proxy'] = _HTTP_PROXY;
|
$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
$context = stream_context_create($context_options);
|
$context = stream_context_create($context_options);
|
||||||
|
|
||||||
$old_error = error_get_last();
|
$old_error = error_get_last();
|
||||||
|
|
||||||
$fetch_effective_url = self::resolve_redirects($url, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
|
$fetch_effective_url = self::resolve_redirects($url, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
|
||||||
|
|
||||||
if (!self::validate($fetch_effective_url, true)) {
|
if (!self::validate($fetch_effective_url, true)) {
|
||||||
$fetch_last_error = "URL received after redirection failed extended validation.";
|
$fetch_last_error = "URL received after redirection failed extended validation.";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
class UserHelper {
|
class UserHelper {
|
||||||
|
|
||||||
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null) {
|
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null) {
|
||||||
if (!SINGLE_USER_MODE) {
|
if (!Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
$user_id = false;
|
$user_id = false;
|
||||||
$auth_module = false;
|
$auth_module = false;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class UserHelper {
|
|||||||
$_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
|
$_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
|
||||||
$_SESSION["pwd_hash"] = $row["pwd_hash"];
|
$_SESSION["pwd_hash"] = $row["pwd_hash"];
|
||||||
|
|
||||||
Pref_Prefs::initialize_user_prefs($_SESSION["uid"]);
|
Pref_Prefs::_init_user_prefs($_SESSION["uid"]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ class UserHelper {
|
|||||||
|
|
||||||
$_SESSION["ip_address"] = UserHelper::get_user_ip();
|
$_SESSION["ip_address"] = UserHelper::get_user_ip();
|
||||||
|
|
||||||
Pref_Prefs::initialize_user_prefs($_SESSION["uid"]);
|
Pref_Prefs::_init_user_prefs($_SESSION["uid"]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -88,26 +88,26 @@ class UserHelper {
|
|||||||
static function login_sequence() {
|
static function login_sequence() {
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
if (SINGLE_USER_MODE) {
|
if (Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
@session_start();
|
@session_start();
|
||||||
self::authenticate("admin", null);
|
self::authenticate("admin", null);
|
||||||
startup_gettext();
|
startup_gettext();
|
||||||
self::load_user_plugins($_SESSION["uid"]);
|
self::load_user_plugins($_SESSION["uid"]);
|
||||||
} else {
|
} else {
|
||||||
if (!validate_session()) $_SESSION["uid"] = false;
|
if (!\Sessions\validate_session()) $_SESSION["uid"] = false;
|
||||||
|
|
||||||
if (empty($_SESSION["uid"])) {
|
if (empty($_SESSION["uid"])) {
|
||||||
|
|
||||||
if (AUTH_AUTO_LOGIN && self::authenticate(null, null)) {
|
if (Config::get(Config::AUTH_AUTO_LOGIN) && self::authenticate(null, null)) {
|
||||||
$_SESSION["ref_schema_version"] = get_schema_version(true);
|
$_SESSION["ref_schema_version"] = get_schema_version(true);
|
||||||
} else {
|
} else {
|
||||||
self::authenticate(null, null, true);
|
self::authenticate(null, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($_SESSION["uid"])) {
|
if (empty($_SESSION["uid"])) {
|
||||||
Pref_Users::logout_user();
|
UserHelper::logout();
|
||||||
|
|
||||||
Handler_Public::render_login_form();
|
Handler_Public::_render_login_form();
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,4 +157,46 @@ class UserHelper {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static function logout() {
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE)
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
if (isset($_COOKIE[session_name()])) {
|
||||||
|
setcookie(session_name(), '', time()-42000, '/');
|
||||||
|
|
||||||
|
}
|
||||||
|
session_commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function reset_password($uid, $format_output = false) {
|
||||||
|
|
||||||
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT login FROM ttrss_users WHERE id = ?");
|
||||||
|
$sth->execute([$uid]);
|
||||||
|
|
||||||
|
if ($row = $sth->fetch()) {
|
||||||
|
|
||||||
|
$login = $row["login"];
|
||||||
|
|
||||||
|
$new_salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
|
||||||
|
$tmp_user_pwd = make_password();
|
||||||
|
|
||||||
|
$pwd_hash = encrypt_password($tmp_user_pwd, $new_salt, true);
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("UPDATE ttrss_users
|
||||||
|
SET pwd_hash = ?, salt = ?, otp_enabled = false
|
||||||
|
WHERE id = ?");
|
||||||
|
$sth->execute([$pwd_hash, $new_salt, $uid]);
|
||||||
|
|
||||||
|
$message = T_sprintf("Changed password of user %s to %s", "<strong>$login</strong>", "<strong>$tmp_user_pwd</strong>");
|
||||||
|
|
||||||
|
if ($format_output)
|
||||||
|
print_notice($message);
|
||||||
|
else
|
||||||
|
print $message;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
155
config.php-dist
155
config.php-dist
@@ -1,153 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
// *******************************************
|
/*
|
||||||
// *** Database configuration (important!) ***
|
This file can be used to customize global defaults if environment method is not available (i.e. no Docker).
|
||||||
// *******************************************
|
|
||||||
|
|
||||||
define('DB_TYPE', '%DB_TYPE'); // pgsql or mysql
|
Use the following syntax to override defaults (options are declared in classes/config.php, prefixed by TTRSS_):
|
||||||
define('DB_HOST', '%DB_HOST');
|
|
||||||
define('DB_USER', '%DB_USER');
|
|
||||||
define('DB_NAME', '%DB_NAME');
|
|
||||||
define('DB_PASS', '%DB_PASS');
|
|
||||||
define('DB_PORT', '%DB_PORT'); // usually 5432 for PostgreSQL, 3306 for MySQL
|
|
||||||
|
|
||||||
define('MYSQL_CHARSET', 'UTF8');
|
putenv('TTRSS_DB_HOST=myserver');
|
||||||
// Connection charset for MySQL. If you have a legacy database and/or experience
|
putenv('TTRSS_SELF_URL_PATH=http://example.com/tt-rss');
|
||||||
// garbage unicode characters with this option, try setting it to a blank string.
|
|
||||||
|
|
||||||
// ***********************************
|
Plugin-required constants also go here, using define():
|
||||||
// *** Basic settings (important!) ***
|
|
||||||
// ***********************************
|
|
||||||
|
|
||||||
define('SELF_URL_PATH', '%SELF_URL_PATH');
|
define('LEGACY_CONSTANT', 'value');
|
||||||
// This should be set to a fully qualified URL used to access
|
|
||||||
// your tt-rss instance over the net, such as: https://example.org/tt-rss/
|
|
||||||
// The value should be a constant string literal. Please don't use
|
|
||||||
// PHP server variables here - you might introduce security
|
|
||||||
// issues on your install and cause hard to debug problems.
|
|
||||||
// If your tt-rss instance is behind a reverse proxy, use the external URL.
|
|
||||||
|
|
||||||
define('SINGLE_USER_MODE', false);
|
etc.
|
||||||
// Operate in single user mode, disables all functionality related to
|
|
||||||
// multiple users and authentication. Enabling this assumes you have
|
|
||||||
// your tt-rss directory protected by other means (e.g. http auth).
|
|
||||||
|
|
||||||
define('SIMPLE_UPDATE_MODE', false);
|
See this page for more information: https://tt-rss.org/wiki/GlobalConfig
|
||||||
// Enables fallback update mode where tt-rss tries to update feeds in
|
*/
|
||||||
// background while tt-rss is open in your browser.
|
|
||||||
// If you don't have a lot of feeds and don't want to or can't run
|
|
||||||
// background processes while not running tt-rss, this method is generally
|
|
||||||
// viable to keep your feeds up to date.
|
|
||||||
// Still, there are more robust (and recommended) updating methods
|
|
||||||
// available, you can read about them here: https://tt-rss.org/wiki/UpdatingFeeds
|
|
||||||
|
|
||||||
// *****************************
|
|
||||||
// *** Files and directories ***
|
|
||||||
// *****************************
|
|
||||||
|
|
||||||
define('PHP_EXECUTABLE', '/usr/bin/php');
|
|
||||||
// Path to PHP *COMMAND LINE* executable, used for various command-line tt-rss
|
|
||||||
// programs and update daemon. Do not try to use CGI binary here, it won't work.
|
|
||||||
// If you see HTTP headers being displayed while running tt-rss scripts,
|
|
||||||
// then most probably you are using the CGI binary. If you are unsure what to
|
|
||||||
// put in here, ask your hosting provider.
|
|
||||||
|
|
||||||
define('LOCK_DIRECTORY', 'lock');
|
|
||||||
// Directory for lockfiles, must be writable to the user you run
|
|
||||||
// daemon process or cronjobs under.
|
|
||||||
|
|
||||||
define('CACHE_DIR', 'cache');
|
|
||||||
// Local cache directory for RSS feed content.
|
|
||||||
|
|
||||||
define('ICONS_DIR', "feed-icons");
|
|
||||||
define('ICONS_URL', "feed-icons");
|
|
||||||
// Local and URL path to the directory, where feed favicons are stored.
|
|
||||||
// Unless you really know what you're doing, please keep those relative
|
|
||||||
// to tt-rss main directory.
|
|
||||||
|
|
||||||
// **********************
|
|
||||||
// *** Authentication ***
|
|
||||||
// **********************
|
|
||||||
|
|
||||||
// Please see PLUGINS below to configure various authentication modules.
|
|
||||||
|
|
||||||
define('AUTH_AUTO_CREATE', true);
|
|
||||||
// Allow authentication modules to auto-create users in tt-rss internal
|
|
||||||
// database when authenticated successfully.
|
|
||||||
|
|
||||||
define('AUTH_AUTO_LOGIN', true);
|
|
||||||
// Automatically login user on remote or other kind of externally supplied
|
|
||||||
// authentication, otherwise redirect to login form as normal.
|
|
||||||
// If set to true, users won't be able to set application language
|
|
||||||
// and settings profile.
|
|
||||||
|
|
||||||
// *********************
|
|
||||||
// *** Feed settings ***
|
|
||||||
// *********************
|
|
||||||
|
|
||||||
define('FORCE_ARTICLE_PURGE', 0);
|
|
||||||
// When this option is not 0, users ability to control feed purging
|
|
||||||
// intervals is disabled and all articles (which are not starred)
|
|
||||||
// older than this amount of days are purged.
|
|
||||||
|
|
||||||
// **********************************
|
|
||||||
// *** Cookies and login sessions ***
|
|
||||||
// **********************************
|
|
||||||
|
|
||||||
define('SESSION_COOKIE_LIFETIME', 86400);
|
|
||||||
// Default lifetime of a session (e.g. login) cookie. In seconds,
|
|
||||||
// 0 means cookie will be deleted when browser closes.
|
|
||||||
|
|
||||||
// *********************************
|
|
||||||
// *** Email and digest settings ***
|
|
||||||
// *********************************
|
|
||||||
|
|
||||||
// Tiny Tiny RSS sends mail via PHP mail() function, unless handled
|
|
||||||
// by a plugin.
|
|
||||||
|
|
||||||
// If you need SMTP support, take a look here:
|
|
||||||
// https://git.tt-rss.org/fox/ttrss-mailer-smtp
|
|
||||||
|
|
||||||
define('SMTP_FROM_NAME', 'Tiny Tiny RSS');
|
|
||||||
define('SMTP_FROM_ADDRESS', 'noreply@your.domain.dom');
|
|
||||||
// Name, address and subject for sending outgoing mail. This applies
|
|
||||||
// to password reset notifications, digest emails and any other mail.
|
|
||||||
|
|
||||||
define('DIGEST_SUBJECT', '[tt-rss] New headlines for last 24 hours');
|
|
||||||
// Subject line for email digests
|
|
||||||
|
|
||||||
// ***************************************
|
|
||||||
// *** Other settings (less important) ***
|
|
||||||
// ***************************************
|
|
||||||
|
|
||||||
define('CHECK_FOR_UPDATES', true);
|
|
||||||
// Check for updates automatically if running Git version
|
|
||||||
|
|
||||||
define('ENABLE_GZIP_OUTPUT', false);
|
|
||||||
// Selectively gzip output to improve wire performance. This requires
|
|
||||||
// PHP Zlib extension on the server.
|
|
||||||
// Enabling this can break tt-rss in several httpd/php configurations,
|
|
||||||
// if you experience weird errors and tt-rss failing to start, blank pages
|
|
||||||
// after login, or content encoding errors, disable it.
|
|
||||||
|
|
||||||
define('PLUGINS', 'auth_internal, note');
|
|
||||||
// Comma-separated list of plugins to load automatically for all users.
|
|
||||||
// System plugins have to be specified here. Please enable at least one
|
|
||||||
// authentication plugin here (auth_*).
|
|
||||||
// Users may enable other user plugins from Preferences/Plugins but may not
|
|
||||||
// disable plugins specified in this list.
|
|
||||||
// Disabling auth_internal in this list would automatically disable
|
|
||||||
// reset password link on the login form.
|
|
||||||
|
|
||||||
define('LOG_DESTINATION', 'sql');
|
|
||||||
// Error log destination to use. Possible values: sql (uses internal logging
|
|
||||||
// you can read in Preferences -> System), syslog - logs to system log.
|
|
||||||
// Setting this to blank uses PHP logging (usually to http server
|
|
||||||
// error.log).
|
|
||||||
// Note that feed updating daemons don't use this logging facility
|
|
||||||
// for normal output.
|
|
||||||
|
|
||||||
define('CONFIG_VERSION', 26);
|
|
||||||
// Expected config version. Please update this option in config.php
|
|
||||||
// if necessary (after migrating all new options from this file).
|
|
||||||
|
|
||||||
// vim:ft=php
|
|
||||||
|
|||||||
60
errors.php
60
errors.php
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
set_include_path(dirname(__FILE__) ."/include" . PATH_SEPARATOR .
|
|
||||||
get_include_path());
|
|
||||||
|
|
||||||
require_once "functions.php";
|
|
||||||
|
|
||||||
function get_error_types() {
|
|
||||||
$ERRORS[0] = "";
|
|
||||||
|
|
||||||
$ERRORS[1] = __("This program requires XmlHttpRequest " .
|
|
||||||
"to function properly. Your browser doesn't seem to support it.");
|
|
||||||
|
|
||||||
$ERRORS[2] = __("This program requires cookies " .
|
|
||||||
"to function properly. Your browser doesn't seem to support them.");
|
|
||||||
|
|
||||||
$ERRORS[3] = __("Backend sanity check failed.");
|
|
||||||
|
|
||||||
$ERRORS[4] = __("Frontend sanity check failed.");
|
|
||||||
|
|
||||||
$ERRORS[5] = __("Incorrect database schema version. <a href='db-updater.php'>Please update</a>.");
|
|
||||||
|
|
||||||
$ERRORS[6] = __("Request not authorized.");
|
|
||||||
|
|
||||||
$ERRORS[7] = __("No operation to perform.");
|
|
||||||
|
|
||||||
$ERRORS[8] = __("Could not display feed: query failed. Please check label match syntax or local configuration.");
|
|
||||||
|
|
||||||
$ERRORS[8] = __("Denied. Your access level is insufficient to access this page.");
|
|
||||||
|
|
||||||
$ERRORS[9] = __("Configuration check failed");
|
|
||||||
|
|
||||||
$ERRORS[10] = __("Your version of MySQL is not currently supported. Please see official site for more information.");
|
|
||||||
|
|
||||||
$ERRORS[11] = "[This error is not returned by server]";
|
|
||||||
|
|
||||||
$ERRORS[12] = __("SQL escaping test failed, check your database and PHP configuration");
|
|
||||||
|
|
||||||
$ERRORS[13] = __("Method not found");
|
|
||||||
|
|
||||||
$ERRORS[14] = __("Plugin not found");
|
|
||||||
|
|
||||||
$ERRORS[15] = __("Encoding data as JSON failed");
|
|
||||||
|
|
||||||
return $ERRORS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_REQUEST['mode'] ?? "" == 'js') {
|
|
||||||
header("Content-Type: text/javascript; charset=UTF-8");
|
|
||||||
|
|
||||||
print "var ERRORS = [];\n";
|
|
||||||
|
|
||||||
foreach (get_error_types() as $id => $error) {
|
|
||||||
|
|
||||||
$error = preg_replace("/\n/", "", $error);
|
|
||||||
$error = preg_replace("/\"/", "\\\"", $error);
|
|
||||||
|
|
||||||
print "ERRORS[$id] = \"$error\";\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once "functions.php";
|
|
||||||
|
|
||||||
spl_autoload_register(function($class) {
|
spl_autoload_register(function($class) {
|
||||||
$namespace = '';
|
$namespace = '';
|
||||||
$class_name = $class;
|
$class_name = $class;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
namespace Colors;
|
||||||
|
|
||||||
if (file_exists("lib/floIcon.php")) {
|
if (file_exists("lib/floIcon.php")) {
|
||||||
require_once "lib/floIcon.php";
|
require_once "lib/floIcon.php";
|
||||||
@@ -297,7 +298,7 @@ function hsl2rgb($arr) {
|
|||||||
|
|
||||||
if (class_exists("floIcon")) {
|
if (class_exists("floIcon")) {
|
||||||
|
|
||||||
$ico = new floIcon();
|
$ico = new \floIcon();
|
||||||
@$ico->readICO($imageFile);
|
@$ico->readICO($imageFile);
|
||||||
|
|
||||||
if(count($ico->images)==0)
|
if(count($ico->images)==0)
|
||||||
|
|||||||
@@ -1,194 +1,133 @@
|
|||||||
<?php
|
<?php
|
||||||
|
namespace Controls;
|
||||||
|
|
||||||
function print_select($id, $default, $values, $attributes = "", $name = "") {
|
function attributes_to_string(array $attributes) {
|
||||||
if (!$name) $name = $id;
|
$rv = "";
|
||||||
|
|
||||||
|
foreach ($attributes as $k => $v) {
|
||||||
|
|
||||||
|
// special handling for "disabled"
|
||||||
|
if ($k === "disabled" && !sql_bool_to_bool($v))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$rv .= "$k=\"" . htmlspecialchars($v) . "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortcut syntax (disabled)
|
||||||
|
/* function pluginhandler_tags(\Plugin $plugin, string $method) {
|
||||||
|
return hidden_tag("op", strtolower(get_class($plugin) . \PluginHost::PUBLIC_METHOD_DELIMITER . $method));
|
||||||
|
} */
|
||||||
|
|
||||||
|
function public_method_tags(\Plugin $plugin, string $method) {
|
||||||
|
return hidden_tag("op", strtolower(get_class($plugin) . \PluginHost::PUBLIC_METHOD_DELIMITER . $method));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluginhandler_tags(\Plugin $plugin, string $method) {
|
||||||
|
return hidden_tag("op", "pluginhandler") .
|
||||||
|
hidden_tag("plugin", strtolower(get_class($plugin))) .
|
||||||
|
hidden_tag("method", $method);
|
||||||
|
}
|
||||||
|
|
||||||
|
function button_tag(string $value, string $type, array $attributes = []) {
|
||||||
|
return "<button dojoType=\"dijit.form.Button\" ".attributes_to_string($attributes)." type=\"$type\">$value</button>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function input_tag(string $name, string $value, string $type = "text", array $attributes = [], string $id = "") {
|
||||||
|
$attributes_str = attributes_to_string($attributes);
|
||||||
|
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='dijit.form.TextBox'" : "";
|
||||||
|
|
||||||
|
return "<input name=\"".htmlspecialchars($name)."\" $dojo_type ".attributes_to_string($attributes)." id=\"".htmlspecialchars($id)."\"
|
||||||
|
type=\"$type\" value=\"".htmlspecialchars($value)."\">";
|
||||||
|
}
|
||||||
|
|
||||||
|
function number_spinner_tag(string $name, string $value, array $attributes = [], string $id = "") {
|
||||||
|
return input_tag($name, $value, "text", array_merge(["dojoType" => "dijit.form.NumberSpinner"], $attributes), $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit_tag(string $value, array $attributes = []) {
|
||||||
|
return button_tag($value, "submit", array_merge(["class" => "alt-primary"], $attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel_dialog_tag(string $value, array $attributes = []) {
|
||||||
|
return button_tag($value, "", array_merge(["onclick" => "App.dialogOf(this).hide()"], $attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
function icon(string $icon, array $attributes = []) {
|
||||||
|
return "<i class=\"material-icons\" ".attributes_to_string($attributes).">$icon</i>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function select_tag(string $name, $value, array $values, array $attributes = [], string $id = "") {
|
||||||
|
$attributes_str = attributes_to_string($attributes);
|
||||||
|
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='fox.form.Select'" : "";
|
||||||
|
|
||||||
|
$rv = "<select $dojo_type name=\"".htmlspecialchars($name)."\"
|
||||||
|
id=\"".htmlspecialchars($id)."\" name=\"".htmlspecialchars($name)."\" $attributes_str>";
|
||||||
|
|
||||||
print "<select name=\"$name\" id=\"$id\" $attributes>";
|
|
||||||
foreach ($values as $v) {
|
foreach ($values as $v) {
|
||||||
if ($v == $default)
|
$is_sel = ($v == $value) ? "selected=\"selected\"" : "";
|
||||||
$sel = "selected=\"1\"";
|
|
||||||
else
|
|
||||||
$sel = "";
|
|
||||||
|
|
||||||
$v = trim($v);
|
$rv .= "<option value=\"".htmlspecialchars($v)."\" $is_sel>".htmlspecialchars($v)."</option>";
|
||||||
|
|
||||||
print "<option value=\"$v\" $sel>$v</option>";
|
|
||||||
}
|
|
||||||
print "</select>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function print_select_hash($id, $default, $values, $attributes = "", $name = "") {
|
$rv .= "</select>";
|
||||||
if (!$name) $name = $id;
|
|
||||||
|
|
||||||
print "<select name=\"$name\" id='$id' $attributes>";
|
return $rv;
|
||||||
foreach (array_keys($values) as $v) {
|
|
||||||
if ($v == $default)
|
|
||||||
$sel = 'selected="selected"';
|
|
||||||
else
|
|
||||||
$sel = "";
|
|
||||||
|
|
||||||
$v = trim($v);
|
|
||||||
|
|
||||||
print "<option $sel value=\"$v\">".$values[$v]."</option>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print "</select>";
|
/*function select_labels(string $name, string $value, array $attributes = [], string $id = "") {
|
||||||
|
$values = \Labels::get_as_hash($_SESSION["uid"]);
|
||||||
|
|
||||||
|
return select_tag($name, $value, $values, $attributes, $id);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
function select_hash(string $name, $value, array $values, array $attributes = [], string $id = "") {
|
||||||
|
$attributes_str = attributes_to_string($attributes);
|
||||||
|
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='fox.form.Select'" : "";
|
||||||
|
|
||||||
|
$rv = "<select $dojo_type name=\"".htmlspecialchars($name)."\"
|
||||||
|
id=\"".htmlspecialchars($id)."\" name=\"".htmlspecialchars($name)."\" $attributes_str>";
|
||||||
|
|
||||||
|
foreach ($values as $k => $v) {
|
||||||
|
$is_sel = ($k == $value) ? "selected=\"selected\"" : "";
|
||||||
|
|
||||||
|
$rv .= "<option value=\"".htmlspecialchars($k)."\" $is_sel>".htmlspecialchars($v)."</option>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function format_hidden($name, $value) {
|
$rv .= "</select>";
|
||||||
return "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"$name\" value=\"$value\">";
|
|
||||||
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function print_hidden($name, $value) {
|
function hidden_tag(string $name, string $value, array $attributes = []) {
|
||||||
print format_hidden($name, $value);
|
return "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\"
|
||||||
|
".attributes_to_string($attributes)." name=\"".htmlspecialchars($name)."\"
|
||||||
|
value=\"".htmlspecialchars($value)."\">";
|
||||||
}
|
}
|
||||||
|
|
||||||
function format_checkbox($id, $checked, $value = "", $attributes = "") {
|
function checkbox_tag(string $name, bool $checked = false, string $value = "", array $attributes = [], string $id = "") {
|
||||||
$checked_str = $checked ? "checked" : "";
|
$is_checked = $checked ? "checked" : "";
|
||||||
$value_str = $value ? "value=\"$value\"" : "";
|
$value_str = $value ? "value=\"".htmlspecialchars($value)."\"" : "";
|
||||||
|
|
||||||
return "<input dojoType=\"dijit.form.CheckBox\" id=\"$id\" $value_str $checked_str $attributes name=\"$id\">";
|
return "<input dojoType='dijit.form.CheckBox' name=\"".htmlspecialchars($name)."\"
|
||||||
|
$value_str $is_checked ".attributes_to_string($attributes)." id=\"".htmlspecialchars($id)."\">";
|
||||||
}
|
}
|
||||||
|
|
||||||
function print_checkbox($id, $checked, $value = "", $attributes = "") {
|
function select_feeds_cats(string $name, int $default_id = null, array $attributes = [],
|
||||||
print format_checkbox($id, $checked, $value, $attributes);
|
bool $include_all_cats = true, string $root_id = null, int $nest_level = 0, string $id = "") {
|
||||||
}
|
|
||||||
|
|
||||||
function print_button($type, $value, $attributes = "") {
|
$ret = "";
|
||||||
print "<p><button dojoType=\"dijit.form.Button\" $attributes type=\"$type\">$value</button>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_radio($id, $default, $true_is, $values, $attributes = "") {
|
|
||||||
foreach ($values as $v) {
|
|
||||||
|
|
||||||
if ($v == $default)
|
|
||||||
$sel = "checked";
|
|
||||||
else
|
|
||||||
$sel = "";
|
|
||||||
|
|
||||||
if ($v == $true_is) {
|
|
||||||
$sel .= " value=\"1\"";
|
|
||||||
} else {
|
|
||||||
$sel .= " value=\"0\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
print "<input class=\"noborder\" dojoType=\"dijit.form.RadioButton\"
|
|
||||||
type=\"radio\" $sel $attributes name=\"$id\"> $v ";
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_feed_multi_select($id, $default_ids = [],
|
|
||||||
$attributes = "", $include_all_feeds = true,
|
|
||||||
$root_id = null, $nest_level = 0) {
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
print_r(in_array("CAT:6",$default_ids));
|
|
||||||
|
|
||||||
if (!$root_id) {
|
if (!$root_id) {
|
||||||
print "<select multiple=\true\" id=\"$id\" name=\"$id\" $attributes>";
|
$ret .= "<select name=\"".htmlspecialchars($name)."\"
|
||||||
if ($include_all_feeds) {
|
id=\"".htmlspecialchars($id)."\"
|
||||||
$is_selected = (in_array("0", $default_ids)) ? "selected=\"1\"" : "";
|
default=\"".((string)$default_id)."\"
|
||||||
print "<option $is_selected value=\"0\">".__('All feeds')."</option>";
|
dojoType=\"fox.form.Select\" ".attributes_to_string($attributes).">";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_pref('ENABLE_FEED_CATS')) {
|
$pdo = \Db::pdo();
|
||||||
|
|
||||||
if (!$root_id) $root_id = null;
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT id,title,
|
|
||||||
(SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE
|
|
||||||
c2.parent_cat = ttrss_feed_categories.id) AS num_children
|
|
||||||
FROM ttrss_feed_categories
|
|
||||||
WHERE owner_uid = :uid AND
|
|
||||||
(parent_cat = :root_id OR (:root_id IS NULL AND parent_cat IS NULL)) ORDER BY title");
|
|
||||||
|
|
||||||
$sth->execute([":uid" => $_SESSION['uid'], ":root_id" => $root_id]);
|
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
|
||||||
|
|
||||||
for ($i = 0; $i < $nest_level; $i++)
|
|
||||||
$line["title"] = " " . $line["title"];
|
|
||||||
|
|
||||||
$is_selected = in_array("CAT:".$line["id"], $default_ids) ? "selected=\"1\"" : "";
|
|
||||||
|
|
||||||
printf("<option $is_selected value='CAT:%d'>%s</option>",
|
|
||||||
$line["id"], htmlspecialchars($line["title"]));
|
|
||||||
|
|
||||||
if ($line["num_children"] > 0)
|
|
||||||
print_feed_multi_select($id, $default_ids, $attributes,
|
|
||||||
$include_all_feeds, $line["id"], $nest_level+1);
|
|
||||||
|
|
||||||
$f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
|
||||||
WHERE cat_id = ? AND owner_uid = ? ORDER BY title");
|
|
||||||
|
|
||||||
$f_sth->execute([$line['id'], $_SESSION['uid']]);
|
|
||||||
|
|
||||||
while ($fline = $f_sth->fetch()) {
|
|
||||||
$is_selected = (in_array($fline["id"], $default_ids)) ? "selected=\"1\"" : "";
|
|
||||||
|
|
||||||
$fline["title"] = " " . $fline["title"];
|
|
||||||
|
|
||||||
for ($i = 0; $i < $nest_level; $i++)
|
|
||||||
$fline["title"] = " " . $fline["title"];
|
|
||||||
|
|
||||||
printf("<option $is_selected value='%d'>%s</option>",
|
|
||||||
$fline["id"], htmlspecialchars($fline["title"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$root_id) {
|
|
||||||
$is_selected = in_array("CAT:0", $default_ids) ? "selected=\"1\"" : "";
|
|
||||||
|
|
||||||
printf("<option $is_selected value='CAT:0'>%s</option>",
|
|
||||||
__("Uncategorized"));
|
|
||||||
|
|
||||||
$f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
|
||||||
WHERE cat_id IS NULL AND owner_uid = ? ORDER BY title");
|
|
||||||
$f_sth->execute([$_SESSION['uid']]);
|
|
||||||
|
|
||||||
while ($fline = $f_sth->fetch()) {
|
|
||||||
$is_selected = in_array($fline["id"], $default_ids) ? "selected=\"1\"" : "";
|
|
||||||
|
|
||||||
$fline["title"] = " " . $fline["title"];
|
|
||||||
|
|
||||||
for ($i = 0; $i < $nest_level; $i++)
|
|
||||||
$fline["title"] = " " . $fline["title"];
|
|
||||||
|
|
||||||
printf("<option $is_selected value='%d'>%s</option>",
|
|
||||||
$fline["id"], htmlspecialchars($fline["title"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
|
||||||
WHERE owner_uid = ? ORDER BY title");
|
|
||||||
$sth->execute([$_SESSION['uid']]);
|
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
|
||||||
|
|
||||||
$is_selected = (in_array($line["id"], $default_ids)) ? "selected=\"1\"" : "";
|
|
||||||
|
|
||||||
printf("<option $is_selected value='%d'>%s</option>",
|
|
||||||
$line["id"], htmlspecialchars($line["title"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$root_id) {
|
|
||||||
print "</select>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_feed_cat_select($id, $default_id,
|
|
||||||
$attributes, $include_all_cats = true, $root_id = null, $nest_level = 0) {
|
|
||||||
|
|
||||||
if (!$root_id) {
|
|
||||||
print "<select id=\"$id\" name=\"$id\" default=\"$default_id\" $attributes>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
if (!$root_id) $root_id = null;
|
if (!$root_id) $root_id = null;
|
||||||
|
|
||||||
@@ -215,18 +154,18 @@ function print_feed_cat_select($id, $default_id,
|
|||||||
$line["title"] = " " . $line["title"];
|
$line["title"] = " " . $line["title"];
|
||||||
|
|
||||||
if ($line["title"])
|
if ($line["title"])
|
||||||
printf("<option $is_selected value='%d'>%s</option>",
|
$ret .= sprintf("<option $is_selected value='%d'>%s</option>",
|
||||||
$line["id"], htmlspecialchars($line["title"]));
|
$line["id"], htmlspecialchars($line["title"]));
|
||||||
|
|
||||||
if ($line["num_children"] > 0)
|
if ($line["num_children"] > 0)
|
||||||
print_feed_cat_select($id, $default_id, $attributes,
|
$ret .= select_feeds_cats($id, $default_id, $attributes,
|
||||||
$include_all_cats, $line["id"], $nest_level+1);
|
$include_all_cats, $line["id"], $nest_level+1, $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$root_id) {
|
if (!$root_id) {
|
||||||
if ($include_all_cats) {
|
if ($include_all_cats) {
|
||||||
if ($found > 0) {
|
if ($found > 0) {
|
||||||
print "<option disabled=\"1\">―――――――――――――――</option>";
|
$ret .= "<option disabled=\"1\">―――――――――――――――</option>";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($default_id == 0) {
|
if ($default_id == 0) {
|
||||||
@@ -235,113 +174,10 @@ function print_feed_cat_select($id, $default_id,
|
|||||||
$is_selected = "";
|
$is_selected = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
print "<option $is_selected value=\"0\">".__('Uncategorized')."</option>";
|
$ret .= "<option $is_selected value=\"0\">".__('Uncategorized')."</option>";
|
||||||
}
|
|
||||||
print "</select>";
|
|
||||||
}
|
}
|
||||||
|
$ret .= "</select>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function stylesheet_tag($filename, $id = false) {
|
return $ret;
|
||||||
$timestamp = filemtime($filename);
|
|
||||||
|
|
||||||
$id_part = $id ? "id=\"$id\"" : "";
|
|
||||||
|
|
||||||
return "<link rel=\"stylesheet\" $id_part type=\"text/css\" data-orig-href=\"$filename\" href=\"$filename?$timestamp\"/>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function javascript_tag($filename) {
|
|
||||||
$query = "";
|
|
||||||
|
|
||||||
if (!(strpos($filename, "?") === false)) {
|
|
||||||
$query = substr($filename, strpos($filename, "?")+1);
|
|
||||||
$filename = substr($filename, 0, strpos($filename, "?"));
|
|
||||||
}
|
|
||||||
|
|
||||||
$timestamp = filemtime($filename);
|
|
||||||
|
|
||||||
if ($query) $timestamp .= "&$query";
|
|
||||||
|
|
||||||
return "<script type=\"text/javascript\" charset=\"utf-8\" src=\"$filename?$timestamp\"></script>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_warning($msg, $id = "") {
|
|
||||||
return "<div class=\"alert\" id=\"$id\">$msg</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_notice($msg, $id = "") {
|
|
||||||
return "<div class=\"alert alert-info\" id=\"$id\">$msg</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_error($msg, $id = "") {
|
|
||||||
return "<div class=\"alert alert-danger\" id=\"$id\">$msg</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_notice($msg) {
|
|
||||||
return print format_notice($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_warning($msg) {
|
|
||||||
return print format_warning($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_error($msg) {
|
|
||||||
return print format_error($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_inline_player($url, $ctype) {
|
|
||||||
|
|
||||||
$entry = "";
|
|
||||||
|
|
||||||
$url = htmlspecialchars($url);
|
|
||||||
|
|
||||||
if (strpos($ctype, "audio/") === 0) {
|
|
||||||
|
|
||||||
$entry .= "<div class='inline-player'>";
|
|
||||||
|
|
||||||
if ($_SESSION["hasAudio"] && (strpos($ctype, "ogg") !== false ||
|
|
||||||
$_SESSION["hasMp3"])) {
|
|
||||||
|
|
||||||
$entry .= "<audio preload=\"none\" controls>
|
|
||||||
<source type=\"$ctype\" src=\"$url\"/>
|
|
||||||
</audio> ";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($entry) $entry .= "<a target=\"_blank\" rel=\"noopener noreferrer\"
|
|
||||||
href=\"$url\">" . basename($url) . "</a>";
|
|
||||||
|
|
||||||
$entry .= "</div>";
|
|
||||||
|
|
||||||
return $entry;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_label_select($name, $value, $attributes = "") {
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2
|
|
||||||
WHERE owner_uid = ? ORDER BY caption");
|
|
||||||
$sth->execute([$_SESSION['uid']]);
|
|
||||||
|
|
||||||
print "<select default=\"$value\" name=\"" . htmlspecialchars($name) .
|
|
||||||
"\" $attributes>";
|
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
|
||||||
|
|
||||||
$issel = ($line["caption"] == $value) ? "selected=\"1\"" : "";
|
|
||||||
|
|
||||||
print "<option value=\"".htmlspecialchars($line["caption"])."\"
|
|
||||||
$issel>" . htmlspecialchars($line["caption"]) . "</option>";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
# print "<option value=\"ADD_LABEL\">" .__("Add label...") . "</option>";
|
|
||||||
|
|
||||||
print "</select>";
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
314
include/controls_compat.php
Normal file
314
include/controls_compat.php
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
function stylesheet_tag($filename, $id = false) {
|
||||||
|
$timestamp = filemtime($filename);
|
||||||
|
|
||||||
|
$id_part = $id ? "id=\"$id\"" : "";
|
||||||
|
|
||||||
|
return "<link rel=\"stylesheet\" $id_part type=\"text/css\" data-orig-href=\"$filename\" href=\"$filename?$timestamp\"/>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function javascript_tag($filename) {
|
||||||
|
$query = "";
|
||||||
|
|
||||||
|
if (!(strpos($filename, "?") === false)) {
|
||||||
|
$query = substr($filename, strpos($filename, "?")+1);
|
||||||
|
$filename = substr($filename, 0, strpos($filename, "?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = filemtime($filename);
|
||||||
|
|
||||||
|
if ($query) $timestamp .= "&$query";
|
||||||
|
|
||||||
|
return "<script type=\"text/javascript\" charset=\"utf-8\" src=\"$filename?$timestamp\"></script>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_warning($msg, $id = "") {
|
||||||
|
return "<div class=\"alert\" id=\"$id\">$msg</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_notice($msg, $id = "") {
|
||||||
|
return "<div class=\"alert alert-info\" id=\"$id\">$msg</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_error($msg, $id = "") {
|
||||||
|
return "<div class=\"alert alert-danger\" id=\"$id\">$msg</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_notice($msg) {
|
||||||
|
return print format_notice($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_warning($msg) {
|
||||||
|
return print format_warning($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_error($msg) {
|
||||||
|
return print format_error($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the following is deprecated and will be eventually removed
|
||||||
|
|
||||||
|
/*function print_select($id, $default, $values, $attributes = "", $name = "") {
|
||||||
|
if (!$name) $name = $id;
|
||||||
|
|
||||||
|
print "<select name=\"$name\" id=\"$id\" $attributes>";
|
||||||
|
foreach ($values as $v) {
|
||||||
|
if ($v == $default)
|
||||||
|
$sel = "selected=\"1\"";
|
||||||
|
else
|
||||||
|
$sel = "";
|
||||||
|
|
||||||
|
$v = trim($v);
|
||||||
|
|
||||||
|
print "<option value=\"$v\" $sel>$v</option>";
|
||||||
|
}
|
||||||
|
print "</select>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_select_hash($id, $default, $values, $attributes = "", $name = "") {
|
||||||
|
if (!$name) $name = $id;
|
||||||
|
|
||||||
|
print "<select name=\"$name\" id='$id' $attributes>";
|
||||||
|
foreach (array_keys($values) as $v) {
|
||||||
|
if ($v == $default)
|
||||||
|
$sel = 'selected="selected"';
|
||||||
|
else
|
||||||
|
$sel = "";
|
||||||
|
|
||||||
|
$v = trim($v);
|
||||||
|
|
||||||
|
print "<option $sel value=\"$v\">".$values[$v]."</option>";
|
||||||
|
}
|
||||||
|
|
||||||
|
print "</select>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_hidden($name, $value) {
|
||||||
|
return "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"$name\" value=\"$value\">";
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_hidden($name, $value) {
|
||||||
|
print format_hidden($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_checkbox($id, $checked, $value = "", $attributes = "") {
|
||||||
|
$checked_str = $checked ? "checked" : "";
|
||||||
|
$value_str = $value ? "value=\"$value\"" : "";
|
||||||
|
|
||||||
|
return "<input dojoType=\"dijit.form.CheckBox\" id=\"$id\" $value_str $checked_str $attributes name=\"$id\">";
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_checkbox($id, $checked, $value = "", $attributes = "") {
|
||||||
|
print format_checkbox($id, $checked, $value, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_button($type, $value, $attributes = "") {
|
||||||
|
return "<button dojoType=\"dijit.form.Button\" $attributes type=\"$type\">$value</button>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_button($type, $value, $attributes = "") {
|
||||||
|
print format_button($type, $value, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_feed_multi_select($id, $default_ids = [],
|
||||||
|
$attributes = "", $include_all_feeds = true,
|
||||||
|
$root_id = null, $nest_level = 0) {
|
||||||
|
|
||||||
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
print_r(in_array("CAT:6",$default_ids));
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
print "<select multiple=\true\" id=\"$id\" name=\"$id\" $attributes>";
|
||||||
|
if ($include_all_feeds) {
|
||||||
|
$is_selected = (in_array("0", $default_ids)) ? "selected=\"1\"" : "";
|
||||||
|
print "<option $is_selected value=\"0\">".__('All feeds')."</option>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_pref('ENABLE_FEED_CATS')) {
|
||||||
|
|
||||||
|
if (!$root_id) $root_id = null;
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT id,title,
|
||||||
|
(SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE
|
||||||
|
c2.parent_cat = ttrss_feed_categories.id) AS num_children
|
||||||
|
FROM ttrss_feed_categories
|
||||||
|
WHERE owner_uid = :uid AND
|
||||||
|
(parent_cat = :root_id OR (:root_id IS NULL AND parent_cat IS NULL)) ORDER BY title");
|
||||||
|
|
||||||
|
$sth->execute([":uid" => $_SESSION['uid'], ":root_id" => $root_id]);
|
||||||
|
|
||||||
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$line["title"] = " " . $line["title"];
|
||||||
|
|
||||||
|
$is_selected = in_array("CAT:".$line["id"], $default_ids) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
printf("<option $is_selected value='CAT:%d'>%s</option>",
|
||||||
|
$line["id"], htmlspecialchars($line["title"]));
|
||||||
|
|
||||||
|
if ($line["num_children"] > 0)
|
||||||
|
print_feed_multi_select($id, $default_ids, $attributes,
|
||||||
|
$include_all_feeds, $line["id"], $nest_level+1);
|
||||||
|
|
||||||
|
$f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
||||||
|
WHERE cat_id = ? AND owner_uid = ? ORDER BY title");
|
||||||
|
|
||||||
|
$f_sth->execute([$line['id'], $_SESSION['uid']]);
|
||||||
|
|
||||||
|
while ($fline = $f_sth->fetch()) {
|
||||||
|
$is_selected = (in_array($fline["id"], $default_ids)) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
printf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$fline["id"], htmlspecialchars($fline["title"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
$is_selected = in_array("CAT:0", $default_ids) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
printf("<option $is_selected value='CAT:0'>%s</option>",
|
||||||
|
__("Uncategorized"));
|
||||||
|
|
||||||
|
$f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
||||||
|
WHERE cat_id IS NULL AND owner_uid = ? ORDER BY title");
|
||||||
|
$f_sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
|
while ($fline = $f_sth->fetch()) {
|
||||||
|
$is_selected = in_array($fline["id"], $default_ids) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$fline["title"] = " " . $fline["title"];
|
||||||
|
|
||||||
|
printf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$fline["id"], htmlspecialchars($fline["title"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
|
||||||
|
WHERE owner_uid = ? ORDER BY title");
|
||||||
|
$sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
|
$is_selected = (in_array($line["id"], $default_ids)) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
printf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$line["id"], htmlspecialchars($line["title"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
print "</select>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_feed_cat_select($id, $default_id, $attributes, $include_all_cats = true,
|
||||||
|
$root_id = null, $nest_level = 0) {
|
||||||
|
|
||||||
|
print format_feed_cat_select($id, $default_id, $attributes, $include_all_cats, $root_id, $nest_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_feed_cat_select($id, $default_id, $attributes, $include_all_cats = true,
|
||||||
|
$root_id = null, $nest_level = 0) {
|
||||||
|
|
||||||
|
$ret = "";
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
$ret .= "<select id=\"$id\" name=\"$id\" default=\"$default_id\" $attributes>";
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
if (!$root_id) $root_id = null;
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT id,title,
|
||||||
|
(SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE
|
||||||
|
c2.parent_cat = ttrss_feed_categories.id) AS num_children
|
||||||
|
FROM ttrss_feed_categories
|
||||||
|
WHERE owner_uid = :uid AND
|
||||||
|
(parent_cat = :root_id OR (:root_id IS NULL AND parent_cat IS NULL)) ORDER BY title");
|
||||||
|
$sth->execute([":uid" => $_SESSION['uid'], ":root_id" => $root_id]);
|
||||||
|
|
||||||
|
$found = 0;
|
||||||
|
|
||||||
|
while ($line = $sth->fetch()) {
|
||||||
|
++$found;
|
||||||
|
|
||||||
|
if ($line["id"] == $default_id) {
|
||||||
|
$is_selected = "selected=\"1\"";
|
||||||
|
} else {
|
||||||
|
$is_selected = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nest_level; $i++)
|
||||||
|
$line["title"] = " " . $line["title"];
|
||||||
|
|
||||||
|
if ($line["title"])
|
||||||
|
$ret .= sprintf("<option $is_selected value='%d'>%s</option>",
|
||||||
|
$line["id"], htmlspecialchars($line["title"]));
|
||||||
|
|
||||||
|
if ($line["num_children"] > 0)
|
||||||
|
$ret .= format_feed_cat_select($id, $default_id, $attributes,
|
||||||
|
$include_all_cats, $line["id"], $nest_level+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$root_id) {
|
||||||
|
if ($include_all_cats) {
|
||||||
|
if ($found > 0) {
|
||||||
|
$ret .= "<option disabled=\"1\">―――――――――――――――</option>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($default_id == 0) {
|
||||||
|
$is_selected = "selected=\"1\"";
|
||||||
|
} else {
|
||||||
|
$is_selected = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$ret .= "<option $is_selected value=\"0\">".__('Uncategorized')."</option>";
|
||||||
|
}
|
||||||
|
$ret .= "</select>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_label_select($name, $value, $attributes = "") {
|
||||||
|
|
||||||
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
|
$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2
|
||||||
|
WHERE owner_uid = ? ORDER BY caption");
|
||||||
|
$sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
|
print "<select default=\"$value\" name=\"" . htmlspecialchars($name) .
|
||||||
|
"\" $attributes>";
|
||||||
|
|
||||||
|
while ($line = $sth->fetch()) {
|
||||||
|
|
||||||
|
$issel = ($line["caption"] == $value) ? "selected=\"1\"" : "";
|
||||||
|
|
||||||
|
print "<option value=\"".htmlspecialchars($line["caption"])."\"
|
||||||
|
$issel>" . htmlspecialchars($line["caption"]) . "</option>";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# print "<option value=\"ADD_LABEL\">" .__("Add label...") . "</option>";
|
||||||
|
|
||||||
|
print "</select>";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once "db.php";
|
|
||||||
|
|
||||||
function get_pref($pref_name, $user_id = false, $die_on_error = false) {
|
|
||||||
return Db_Prefs::get()->read($pref_name, $user_id, $die_on_error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_pref($pref_name, $value, $user_id = false, $strip_tags = true) {
|
|
||||||
return Db_Prefs::get()->write($pref_name, $value, $user_id, $strip_tags);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
function db_escape_string($s, $strip_tags = true) {
|
|
||||||
return Db::get()->escape_string($s, $strip_tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_query($query, $die_on_error = true) {
|
|
||||||
return Db::get()->query($query, $die_on_error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_fetch_assoc($result) {
|
|
||||||
return Db::get()->fetch_assoc($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function db_num_rows($result) {
|
|
||||||
return Db::get()->num_rows($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_fetch_result($result, $row, $param) {
|
|
||||||
return Db::get()->fetch_result($result, $row, $param);
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_affected_rows($result) {
|
|
||||||
return Db::get()->affected_rows($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_last_error() {
|
|
||||||
return Db::get()->last_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_last_query_error() {
|
|
||||||
return Db::get()->last_query_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
function db_quote($str){
|
|
||||||
return Db::get()->quote($str);
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ function ttrss_error_handler($errno, $errstr, $file, $line) {
|
|||||||
|
|
||||||
if (error_reporting() == 0 || !$errno) return false;
|
if (error_reporting() == 0 || !$errno) return false;
|
||||||
|
|
||||||
$file = substr(str_replace(dirname(dirname(__FILE__)), "", $file), 1);
|
$file = substr(str_replace(dirname(__DIR__), "", $file), 1);
|
||||||
|
|
||||||
$context = format_backtrace(debug_backtrace());
|
$context = format_backtrace(debug_backtrace());
|
||||||
$errstr = truncate_middle($errstr, 16384, " (...) ");
|
$errstr = truncate_middle($errstr, 16384, " (...) ");
|
||||||
@@ -72,7 +72,7 @@ function ttrss_fatal_handler() {
|
|||||||
|
|
||||||
$context = format_backtrace(debug_backtrace());
|
$context = format_backtrace(debug_backtrace());
|
||||||
|
|
||||||
$file = substr(str_replace(dirname(dirname(__FILE__)), "", $file), 1);
|
$file = substr(str_replace(dirname(__DIR__), "", $file), 1);
|
||||||
|
|
||||||
if ($last_query) $errstr .= " [Last query: $last_query]";
|
if ($last_query) $errstr .= " [Last query: $last_query]";
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
define('EXPECTED_CONFIG_VERSION', 26);
|
|
||||||
define('SCHEMA_VERSION', 140);
|
define('SCHEMA_VERSION', 140);
|
||||||
|
|
||||||
define('LABEL_BASE_INDEX', -1024);
|
define('LABEL_BASE_INDEX', -1024);
|
||||||
define('PLUGIN_FEED_BASE_INDEX', -128);
|
define('PLUGIN_FEED_BASE_INDEX', -128);
|
||||||
|
|
||||||
define('COOKIE_LIFETIME_LONG', 86400*365);
|
|
||||||
|
|
||||||
// this CSS file is included for everyone (if it exists in themes.local)
|
|
||||||
// on login, registration, and main (index and prefs) pages
|
|
||||||
define('LOCAL_OVERRIDE_STYLESHEET', '.local-overrides.css');
|
|
||||||
|
|
||||||
$fetch_last_error = false;
|
$fetch_last_error = false;
|
||||||
$fetch_last_error_code = false;
|
$fetch_last_error_code = false;
|
||||||
$fetch_last_content_type = false;
|
$fetch_last_content_type = false;
|
||||||
@@ -34,71 +27,31 @@
|
|||||||
error_reporting(E_ALL & ~E_NOTICE);
|
error_reporting(E_ALL & ~E_NOTICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
ini_set('display_errors', 0);
|
ini_set('display_errors', "false");
|
||||||
ini_set('display_startup_errors', 0);
|
ini_set('display_startup_errors', "false");
|
||||||
|
|
||||||
require_once 'config.php';
|
// config.php is optional
|
||||||
|
if (stream_resolve_include_path("config.php"))
|
||||||
|
require_once "config.php";
|
||||||
|
|
||||||
/**
|
require_once "autoload.php";
|
||||||
* Define a constant if not already defined
|
|
||||||
*/
|
|
||||||
function define_default($name, $value) {
|
|
||||||
defined($name) or define($name, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Some tunables you can override in config.php using define(): */
|
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||||
|
|
||||||
define_default('FEED_FETCH_TIMEOUT', 45);
|
|
||||||
// How may seconds to wait for response when requesting feed from a site
|
|
||||||
define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
|
|
||||||
// How may seconds to wait for response when requesting feed from a
|
|
||||||
// site when that feed wasn't cached before
|
|
||||||
define_default('FILE_FETCH_TIMEOUT', 45);
|
|
||||||
// Default timeout when fetching files from remote sites
|
|
||||||
define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
|
|
||||||
// How many seconds to wait for initial response from website when
|
|
||||||
// fetching files from remote sites
|
|
||||||
define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
|
|
||||||
// stop updating feeds if users haven't logged in for X days
|
|
||||||
define_default('DAEMON_FEED_LIMIT', 500);
|
|
||||||
// feed limit for one update batch
|
|
||||||
define_default('DAEMON_SLEEP_INTERVAL', 120);
|
|
||||||
// default sleep interval between feed updates (sec)
|
|
||||||
define_default('MAX_CACHE_FILE_SIZE', 64*1024*1024);
|
|
||||||
// do not cache files larger than that (bytes)
|
|
||||||
define_default('MAX_DOWNLOAD_FILE_SIZE', 16*1024*1024);
|
|
||||||
// do not download general files larger than that (bytes)
|
|
||||||
define_default('CACHE_MAX_DAYS', 7);
|
|
||||||
// max age in days for various automatically cached (temporary) files
|
|
||||||
define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
|
|
||||||
// max interval between forced unconditional updates for servers
|
|
||||||
// not complying with http if-modified-since (seconds)
|
|
||||||
// define_default('MAX_FETCH_REQUESTS_PER_HOST', 25);
|
|
||||||
// a maximum amount of allowed HTTP requests per destination host
|
|
||||||
// during a single update (i.e. within PHP process lifetime)
|
|
||||||
// this is used to not cause excessive load on the origin server on
|
|
||||||
// e.g. feed subscription when all articles are being processes
|
|
||||||
// (not implemented)
|
|
||||||
define_default('DAEMON_UNSUCCESSFUL_DAYS_LIMIT', 30);
|
|
||||||
// automatically disable updates for feeds which failed to
|
|
||||||
// update for this amount of days; 0 disables
|
|
||||||
|
|
||||||
/* tunables end here */
|
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql") {
|
|
||||||
define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
|
define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
|
||||||
} else {
|
} else {
|
||||||
define('SUBSTRING_FOR_DATE', 'SUBSTRING');
|
define('SUBSTRING_FOR_DATE', 'SUBSTRING');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function get_pref($pref_name, $user_id = false, $die_on_error = false) {
|
||||||
* Return available translations names.
|
return Db_Prefs::get()->read($pref_name, $user_id, $die_on_error);
|
||||||
*
|
}
|
||||||
* @access public
|
|
||||||
* @return array A array of available translations.
|
function set_pref($pref_name, $value, $user_id = false, $strip_tags = true) {
|
||||||
*/
|
return Db_Prefs::get()->write($pref_name, $value, $user_id, $strip_tags);
|
||||||
|
}
|
||||||
|
|
||||||
function get_translations() {
|
function get_translations() {
|
||||||
$tr = array(
|
$t = array(
|
||||||
"auto" => __("Detect automatically"),
|
"auto" => __("Detect automatically"),
|
||||||
"ar_SA" => "العربيّة (Arabic)",
|
"ar_SA" => "العربيّة (Arabic)",
|
||||||
"bg_BG" => "Bulgarian",
|
"bg_BG" => "Bulgarian",
|
||||||
@@ -129,38 +82,76 @@
|
|||||||
"fi_FI" => "Suomi",
|
"fi_FI" => "Suomi",
|
||||||
"tr_TR" => "Türkçe");
|
"tr_TR" => "Türkçe");
|
||||||
|
|
||||||
return $tr;
|
return $t;
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once "lib/accept-to-gettext.php";
|
|
||||||
require_once "lib/gettext/gettext.inc.php";
|
require_once "lib/gettext/gettext.inc.php";
|
||||||
|
|
||||||
function startup_gettext() {
|
function startup_gettext() {
|
||||||
|
|
||||||
# Get locale from Accept-Language header
|
$selected_locale = "";
|
||||||
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
|
||||||
$lang = al2gt(array_keys(get_translations()), "text/html");
|
// https://www.codingwithjesse.com/blog/use-accept-language-header/
|
||||||
} else {
|
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||||
$lang = ""; // FIXME: do something with accept-to-gettext.php
|
$valid_langs = [];
|
||||||
|
$translations = array_keys(get_translations());
|
||||||
|
|
||||||
|
array_shift($translations); // remove "auto"
|
||||||
|
|
||||||
|
// full locale first
|
||||||
|
foreach ($translations as $t) {
|
||||||
|
$lang = strtolower(str_replace("_", "-", (string)$t));
|
||||||
|
$valid_langs[$lang] = $t;
|
||||||
|
|
||||||
|
$lang = substr($lang, 0, 2);
|
||||||
|
if (!isset($valid_langs[$lang]))
|
||||||
|
$valid_langs[$lang] = $t;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
|
// break up string into pieces (languages and q factors)
|
||||||
$lang = _TRANSLATION_OVERRIDE_DEFAULT;
|
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
|
||||||
|
$_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse);
|
||||||
|
|
||||||
|
if (count($lang_parse[1])) {
|
||||||
|
// create a list like "en" => 0.8
|
||||||
|
$langs = array_combine($lang_parse[1], $lang_parse[4]);
|
||||||
|
|
||||||
|
if (is_array($langs)) {
|
||||||
|
// set default to 1 for any without q factor
|
||||||
|
foreach ($langs as $lang => $val) {
|
||||||
|
if ($val === '') $langs[$lang] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort list based on value
|
||||||
|
arsort($langs, SORT_NUMERIC);
|
||||||
|
|
||||||
|
foreach (array_keys($langs) as $lang) {
|
||||||
|
$lang = strtolower($lang);
|
||||||
|
|
||||||
|
foreach ($valid_langs as $vlang => $vlocale) {
|
||||||
|
if ($vlang == $lang) {
|
||||||
|
$selected_locale = $vlocale;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"]) && get_schema_version() >= 120) {
|
if (!empty($_SESSION["uid"]) && get_schema_version() >= 120) {
|
||||||
$pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
|
$pref_locale = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
|
||||||
|
|
||||||
if ($pref_lang && $pref_lang != 'auto') {
|
if (!empty($pref_locale) && $pref_locale != 'auto') {
|
||||||
$lang = $pref_lang;
|
$selected_locale = $pref_locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($lang) {
|
if ($selected_locale) {
|
||||||
if (defined('LC_MESSAGES')) {
|
if (defined('LC_MESSAGES')) {
|
||||||
_setlocale(LC_MESSAGES, $lang);
|
_setlocale(LC_MESSAGES, $selected_locale);
|
||||||
} else if (defined('LC_ALL')) {
|
} else if (defined('LC_ALL')) {
|
||||||
_setlocale(LC_ALL, $lang);
|
_setlocale(LC_ALL, $selected_locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
_bindtextdomain("messages", "locale");
|
_bindtextdomain("messages", "locale");
|
||||||
@@ -169,8 +160,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once 'db-prefs.php';
|
|
||||||
require_once 'controls.php';
|
require_once 'controls.php';
|
||||||
|
require_once 'controls_compat.php';
|
||||||
|
|
||||||
define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . get_version() . ' (http://tt-rss.org/)');
|
define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . get_version() . ' (http://tt-rss.org/)');
|
||||||
ini_set('user_agent', SELF_USER_AGENT);
|
ini_set('user_agent', SELF_USER_AGENT);
|
||||||
@@ -185,7 +176,7 @@
|
|||||||
|
|
||||||
// @deprecated
|
// @deprecated
|
||||||
function getFeedUnread($feed, $is_cat = false) {
|
function getFeedUnread($feed, $is_cat = false) {
|
||||||
return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
|
return Feeds::_get_counters($feed, $is_cat, true, $_SESSION["uid"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @deprecated
|
// @deprecated
|
||||||
@@ -248,7 +239,7 @@
|
|||||||
} else if (is_string($param)) {
|
} else if (is_string($param)) {
|
||||||
return trim(strip_tags($param));
|
return trim(strip_tags($param));
|
||||||
} else {
|
} else {
|
||||||
return trim($param);
|
return $param;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +269,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validate_csrf($csrf_token) {
|
function validate_csrf($csrf_token) {
|
||||||
return isset($csrf_token) && hash_equals($_SESSION['csrf_token'], $csrf_token);
|
return isset($csrf_token) && hash_equals($_SESSION['csrf_token'] ?? "", $csrf_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncate_string($str, $max_len, $suffix = '…') {
|
function truncate_string($str, $max_len, $suffix = '…') {
|
||||||
@@ -332,24 +323,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanity_check() {
|
|
||||||
require_once 'errors.php';
|
|
||||||
$ERRORS = get_error_types();
|
|
||||||
|
|
||||||
$error_code = 0;
|
|
||||||
$schema_version = get_schema_version(true);
|
|
||||||
|
|
||||||
if ($schema_version != SCHEMA_VERSION) {
|
|
||||||
$error_code = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array("code" => $error_code, "message" => $ERRORS[$error_code]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function file_is_locked($filename) {
|
function file_is_locked($filename) {
|
||||||
if (file_exists(LOCK_DIRECTORY . "/$filename")) {
|
if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/$filename")) {
|
||||||
if (function_exists('flock')) {
|
if (function_exists('flock')) {
|
||||||
$fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
|
$fp = @fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "r");
|
||||||
if ($fp) {
|
if ($fp) {
|
||||||
if (flock($fp, LOCK_EX | LOCK_NB)) {
|
if (flock($fp, LOCK_EX | LOCK_NB)) {
|
||||||
flock($fp, LOCK_UN);
|
flock($fp, LOCK_UN);
|
||||||
@@ -369,11 +346,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function make_lockfile($filename) {
|
function make_lockfile($filename) {
|
||||||
$fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
|
$fp = fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "w");
|
||||||
|
|
||||||
if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
|
if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
|
||||||
$stat_h = fstat($fp);
|
$stat_h = fstat($fp);
|
||||||
$stat_f = stat(LOCK_DIRECTORY . "/$filename");
|
$stat_f = stat(Config::get(Config::LOCK_DIRECTORY) . "/$filename");
|
||||||
|
|
||||||
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||||
if ($stat_h["ino"] != $stat_f["ino"] ||
|
if ($stat_h["ino"] != $stat_f["ino"] ||
|
||||||
@@ -397,7 +374,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function uniqid_short() {
|
function uniqid_short() {
|
||||||
return uniqid(base_convert(rand(), 10, 36));
|
return uniqid(base_convert((string)rand(), 10, 36));
|
||||||
}
|
}
|
||||||
|
|
||||||
function T_sprintf() {
|
function T_sprintf() {
|
||||||
@@ -416,15 +393,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function is_prefix_https() {
|
function is_prefix_https() {
|
||||||
return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
|
return parse_url(Config::get(Config::SELF_URL_PATH), PHP_URL_SCHEME) == 'https';
|
||||||
}
|
}
|
||||||
|
|
||||||
// this returns SELF_URL_PATH sans ending slash
|
// this returns Config::get(Config::SELF_URL_PATH) sans ending slash
|
||||||
function get_self_url_prefix() {
|
function get_self_url_prefix() {
|
||||||
if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
|
if (strrpos(Config::get(Config::SELF_URL_PATH), "/") === strlen(Config::get(Config::SELF_URL_PATH))-1) {
|
||||||
return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
|
return substr(Config::get(Config::SELF_URL_PATH), 0, strlen(Config::get(Config::SELF_URL_PATH))-1);
|
||||||
} else {
|
} else {
|
||||||
return SELF_URL_PATH;
|
return Config::get(Config::SELF_URL_PATH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +416,7 @@
|
|||||||
} // function encrypt_password
|
} // function encrypt_password
|
||||||
|
|
||||||
function init_plugins() {
|
function init_plugins() {
|
||||||
PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
|
PluginHost::getInstance()->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -542,20 +519,6 @@
|
|||||||
return file_exists("themes/$theme") || file_exists("themes.local/$theme");
|
return file_exists("themes/$theme") || file_exists("themes.local/$theme");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @SuppressWarnings(unused)
|
|
||||||
*/
|
|
||||||
function error_json($code) {
|
|
||||||
require_once "errors.php";
|
|
||||||
$ERRORS = get_error_types();
|
|
||||||
|
|
||||||
@$message = $ERRORS[$code];
|
|
||||||
|
|
||||||
return json_encode(array("error" =>
|
|
||||||
array("code" => $code, "message" => $message)));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function arr_qmarks($arr) {
|
function arr_qmarks($arr) {
|
||||||
return str_repeat('?,', count($arr) - 1) . '?';
|
return str_repeat('?,', count($arr) - 1) . '?';
|
||||||
}
|
}
|
||||||
@@ -592,7 +555,7 @@
|
|||||||
$ttrss_version['version'] = "UNKNOWN (Unsupported)";
|
$ttrss_version['version'] = "UNKNOWN (Unsupported)";
|
||||||
|
|
||||||
date_default_timezone_set('UTC');
|
date_default_timezone_set('UTC');
|
||||||
$root_dir = dirname(dirname(__FILE__));
|
$root_dir = dirname(__DIR__);
|
||||||
|
|
||||||
if (PHP_OS === "Darwin") {
|
if (PHP_OS === "Darwin") {
|
||||||
$ttrss_version['version'] = "UNKNOWN (Unsupported, Darwin)";
|
$ttrss_version['version'] = "UNKNOWN (Unsupported, Darwin)";
|
||||||
|
|||||||
@@ -6,20 +6,17 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
|
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<?php
|
<?php
|
||||||
foreach (array("lib/prototype.js",
|
foreach (["lib/dojo/dojo.js",
|
||||||
"lib/dojo/dojo.js",
|
|
||||||
"lib/dojo/tt-rss-layer.js",
|
"lib/dojo/tt-rss-layer.js",
|
||||||
"lib/prototype.js",
|
|
||||||
"js/common.js",
|
"js/common.js",
|
||||||
"js/utility.js",
|
"js/utility.js"] as $jsfile) {
|
||||||
"errors.php?mode=js") as $jsfile) {
|
|
||||||
|
|
||||||
echo javascript_tag($jsfile);
|
echo javascript_tag($jsfile);
|
||||||
|
|
||||||
} ?>
|
} ?>
|
||||||
|
|
||||||
<?php if (theme_exists(LOCAL_OVERRIDE_STYLESHEET)) {
|
<?php if (theme_exists(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET))) {
|
||||||
echo stylesheet_tag(get_theme_path(LOCAL_OVERRIDE_STYLESHEET));
|
echo stylesheet_tag(get_theme_path(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET)));
|
||||||
} ?>
|
} ?>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@@ -61,13 +58,13 @@
|
|||||||
if (login && login != this.previousLogin) {
|
if (login && login != this.previousLogin) {
|
||||||
this.previousLogin = login;
|
this.previousLogin = login;
|
||||||
|
|
||||||
xhrJson("public.php", {op: "getprofiles", login: login},
|
xhr.json("public.php", {op: "getprofiles", login: login},
|
||||||
(reply) => {
|
(reply) => {
|
||||||
const profile = dijit.byId('profile');
|
const profile = dijit.byId('profile');
|
||||||
|
|
||||||
profile.removeOption(profile.getOptions());
|
profile.removeOption(profile.getOptions());
|
||||||
|
|
||||||
reply.each((p) => {
|
reply.forEach((p) => {
|
||||||
profile
|
profile
|
||||||
.attr("disabled", false)
|
.attr("disabled", false)
|
||||||
.addOption(p);
|
.addOption(p);
|
||||||
@@ -81,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
bwLimitChange: function(elem) {
|
bwLimitChange: function(elem) {
|
||||||
Cookie.set("ttrss_bwlimit", elem.checked,
|
Cookie.set("ttrss_bwlimit", elem.checked,
|
||||||
<?php print SESSION_COOKIE_LIFETIME ?>);
|
<?php print Config::get(Config::SESSION_COOKIE_LIFETIME) ?>);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,29 +89,29 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<h1><?php echo "Authentication" ?></h1>
|
<h1><?= "Authentication" ?></h1>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="public.php?return=<?php echo $return ?>"
|
<form action="public.php?return=<?= $return ?>"
|
||||||
dojoType="dijit.form.Form" method="POST">
|
dojoType="dijit.form.Form" method="POST">
|
||||||
|
|
||||||
<?php print_hidden("op", "login"); ?>
|
<?= \Controls\hidden_tag("op", "login"); ?>
|
||||||
|
|
||||||
<?php if (!empty($_SESSION["login_error_msg"])) { ?>
|
<?php if (!empty($_SESSION["login_error_msg"])) { ?>
|
||||||
<?php echo format_error($_SESSION["login_error_msg"]) ?>
|
<?= format_error($_SESSION["login_error_msg"]) ?>
|
||||||
<?php $_SESSION["login_error_msg"] = ""; ?>
|
<?php $_SESSION["login_error_msg"] = ""; ?>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?php echo __("Login:") ?></label>
|
<label><?= __("Login:") ?></label>
|
||||||
<input name="login" id="login" dojoType="dijit.form.TextBox" type="text"
|
<input name="login" id="login" dojoType="dijit.form.TextBox" type="text"
|
||||||
onchange="UtilityApp.fetchProfiles()"
|
onchange="UtilityApp.fetchProfiles()"
|
||||||
onfocus="UtilityApp.fetchProfiles()"
|
onfocus="UtilityApp.fetchProfiles()"
|
||||||
onblur="UtilityApp.fetchProfiles()"
|
onblur="UtilityApp.fetchProfiles()"
|
||||||
required="1" value="<?php echo $_SESSION["fake_login"] ?? "" ?>" />
|
required="1" value="<?= $_SESSION["fake_login"] ?? "" ?>" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?php echo __("Password:") ?></label>
|
<label><?= __("Password:") ?></label>
|
||||||
|
|
||||||
<input type="password" name="password" required="1"
|
<input type="password" name="password" required="1"
|
||||||
dojoType="dijit.form.TextBox"
|
dojoType="dijit.form.TextBox"
|
||||||
@@ -122,52 +119,54 @@
|
|||||||
onchange="UtilityApp.fetchProfiles()"
|
onchange="UtilityApp.fetchProfiles()"
|
||||||
onfocus="UtilityApp.fetchProfiles()"
|
onfocus="UtilityApp.fetchProfiles()"
|
||||||
onblur="UtilityApp.fetchProfiles()"
|
onblur="UtilityApp.fetchProfiles()"
|
||||||
value="<?php echo $_SESSION["fake_password"] ?? "" ?>"/>
|
value="<?= $_SESSION["fake_password"] ?? "" ?>"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<?php if (strpos(PLUGINS, "auth_internal") !== false) { ?>
|
<?php if (strpos(Config::get(Config::PLUGINS), "auth_internal") !== false) { ?>
|
||||||
<fieldset class="align-right">
|
<fieldset class="align-right">
|
||||||
<a href="public.php?op=forgotpass"><?php echo __("I forgot my password") ?></a>
|
<a href="public.php?op=forgotpass"><?= __("I forgot my password") ?></a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?php echo __("Profile:") ?></label>
|
<label><?= __("Profile:") ?></label>
|
||||||
|
|
||||||
<select disabled='disabled' name="profile" id="profile" dojoType='dijit.form.Select'>
|
<select disabled='disabled' name="profile" id="profile" dojoType='dijit.form.Select'>
|
||||||
<option><?php echo __("Default profile") ?></option>
|
<option><?= __("Default profile") ?></option>
|
||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="narrow">
|
<fieldset class="narrow">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
|
|
||||||
<label id="bw_limit_label"><input dojoType="dijit.form.CheckBox" name="bw_limit" id="bw_limit"
|
<label id="bw_limit_label">
|
||||||
type="checkbox" onchange="UtilityApp.bwLimitChange(this)">
|
<?= \Controls\checkbox_tag("bw_limit", false, "",
|
||||||
<?php echo __("Use less traffic") ?></label>
|
["onchange" => 'UtilityApp.bwLimitChange(this)'], 'bw_limit') ?>
|
||||||
|
<?= __("Use less traffic") ?></label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div dojoType="dijit.Tooltip" connectId="bw_limit_label" position="below" style="display:none">
|
<div dojoType="dijit.Tooltip" connectId="bw_limit_label" position="below" style="display:none">
|
||||||
<?php echo __("Does not display images in articles, reduces automatic refreshes."); ?>
|
<?= __("Does not display images in articles, reduces automatic refreshes."); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="narrow">
|
<fieldset class="narrow">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
|
|
||||||
<label id="safe_mode_label"><input dojoType="dijit.form.CheckBox" name="safe_mode" id="safe_mode"
|
<label id="safe_mode_label">
|
||||||
type="checkbox">
|
<?= \Controls\checkbox_tag("safe_mode") ?>
|
||||||
<?php echo __("Safe mode") ?></label>
|
<?= __("Safe mode") ?>
|
||||||
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div dojoType="dijit.Tooltip" connectId="safe_mode_label" position="below" style="display:none">
|
<div dojoType="dijit.Tooltip" connectId="safe_mode_label" position="below" style="display:none">
|
||||||
<?php echo __("Uses default theme and prevents all plugins from loading."); ?>
|
<?= __("Uses default theme and prevents all plugins from loading."); ?>
|
||||||
</div>
|
</div>
|
||||||
<?php if (SESSION_COOKIE_LIFETIME > 0) { ?>
|
<?php if (Config::get(Config::SESSION_COOKIE_LIFETIME) > 0) { ?>
|
||||||
|
|
||||||
<fieldset class="narrow">
|
<fieldset class="narrow">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
<label>
|
<label>
|
||||||
<input dojoType="dijit.form.CheckBox" name="remember_me" id="remember_me" type="checkbox">
|
<?= \Controls\checkbox_tag("remember_me") ?>
|
||||||
<?php echo __("Remember me") ?>
|
<?= __("Remember me") ?>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -177,7 +176,7 @@
|
|||||||
|
|
||||||
<fieldset class="align-right">
|
<fieldset class="align-right">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
<button dojoType="dijit.form.Button" type="submit" class="alt-primary"><?php echo __('Log in') ?></button>
|
<?= \Controls\submit_tag(__('Log in')) ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -185,7 +184,7 @@
|
|||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="https://tt-rss.org/">Tiny Tiny RSS</a>
|
<a href="https://tt-rss.org/">Tiny Tiny RSS</a>
|
||||||
© 2005–<?php echo date('Y') ?> <a href="https://fakecake.org/">Andrew Dolgov</a>
|
© 2005–<?= date('Y') ?> <a href="https://fakecake.org/">Andrew Dolgov</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
$sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
|
$sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
|
||||||
table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
|
table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
|
||||||
$sth->execute([DB_NAME]);
|
$sth->execute([Config::get(Config::DB_NAME)]);
|
||||||
|
|
||||||
$bad_tables = [];
|
$bad_tables = [];
|
||||||
|
|
||||||
@@ -40,14 +40,12 @@
|
|||||||
array_push($errors, "Configuration file not found. Looks like you forgot to copy config.php-dist to config.php and edit it.");
|
array_push($errors, "Configuration file not found. Looks like you forgot to copy config.php-dist to config.php and edit it.");
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
require_once "sanity_config.php";
|
|
||||||
|
|
||||||
if (!file_exists("config.php")) {
|
if (!file_exists("config.php")) {
|
||||||
array_push($errors, "Please copy config.php-dist to config.php");
|
array_push($errors, "Please copy config.php-dist to config.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strpos(PLUGINS, "auth_") === false) {
|
if (strpos(Config::get(Config::PLUGINS), "auth_") === false) {
|
||||||
array_push($errors, "Please enable at least one authentication module via PLUGINS constant in config.php");
|
array_push($errors, "Please enable at least one authentication module via Config::get(Config::PLUGINS) constant in config.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (function_exists('posix_getuid') && posix_getuid() == 0) {
|
if (function_exists('posix_getuid') && posix_getuid() == 0) {
|
||||||
@@ -62,41 +60,25 @@
|
|||||||
array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module.");
|
array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CONFIG_VERSION != EXPECTED_CONFIG_VERSION) {
|
if (!is_writable(Config::get(Config::CACHE_DIR) . "/images")) {
|
||||||
array_push($errors, "Configuration file (config.php) has incorrect version. Update it with new options from config.php-dist and set CONFIG_VERSION to the correct value.");
|
array_push($errors, "Image cache is not writable (chmod -R 777 ".Config::get(Config::CACHE_DIR)."/images)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable(CACHE_DIR . "/images")) {
|
if (!is_writable(Config::get(Config::CACHE_DIR) . "/upload")) {
|
||||||
array_push($errors, "Image cache is not writable (chmod -R 777 ".CACHE_DIR."/images)");
|
array_push($errors, "Upload cache is not writable (chmod -R 777 ".Config::get(Config::CACHE_DIR)."/upload)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable(CACHE_DIR . "/upload")) {
|
if (!is_writable(Config::get(Config::CACHE_DIR) . "/export")) {
|
||||||
array_push($errors, "Upload cache is not writable (chmod -R 777 ".CACHE_DIR."/upload)");
|
array_push($errors, "Data export cache is not writable (chmod -R 777 ".Config::get(Config::CACHE_DIR)."/export)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable(CACHE_DIR . "/export")) {
|
if (Config::get(Config::SINGLE_USER_MODE) && class_exists("PDO")) {
|
||||||
array_push($errors, "Data export cache is not writable (chmod -R 777 ".CACHE_DIR."/export)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GENERATED_CONFIG_CHECK != EXPECTED_CONFIG_VERSION) {
|
|
||||||
array_push($errors,
|
|
||||||
"Configuration option checker sanity_config.php is outdated, please recreate it using ./utils/regen_config_checks.sh");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($required_defines as $d) {
|
|
||||||
if (!defined($d)) {
|
|
||||||
array_push($errors,
|
|
||||||
"Required configuration file parameter $d is not defined in config.php. You might need to copy it from config.php-dist.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SINGLE_USER_MODE && class_exists("PDO")) {
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
$res = $pdo->query("SELECT id FROM ttrss_users WHERE id = 1");
|
$res = $pdo->query("SELECT id FROM ttrss_users WHERE id = 1");
|
||||||
|
|
||||||
if (!$res->fetch()) {
|
if (!$res->fetch()) {
|
||||||
array_push($errors, "SINGLE_USER_MODE is enabled in config.php but default admin account is not found.");
|
array_push($errors, "Config::get(Config::SINGLE_USER_MODE) is enabled in config.php but default admin account is not found.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,26 +89,26 @@
|
|||||||
$ref_self_url_path = preg_replace("/\w+\.php$/", "", $ref_self_url_path);
|
$ref_self_url_path = preg_replace("/\w+\.php$/", "", $ref_self_url_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SELF_URL_PATH == "http://example.org/tt-rss/") {
|
if (Config::get(Config::SELF_URL_PATH) == "http://example.org/tt-rss/") {
|
||||||
$hint = $ref_self_url_path ? "(possible value: <b>$ref_self_url_path</b>)" : "";
|
$hint = $ref_self_url_path ? "(possible value: <b>$ref_self_url_path</b>)" : "";
|
||||||
array_push($errors,
|
array_push($errors,
|
||||||
"Please set SELF_URL_PATH to the correct value for your server: $hint");
|
"Please set Config::get(Config::SELF_URL_PATH) to the correct value for your server: $hint");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ref_self_url_path &&
|
if ($ref_self_url_path &&
|
||||||
(!defined('_SKIP_SELF_URL_PATH_CHECKS') || !_SKIP_SELF_URL_PATH_CHECKS) &&
|
(!defined('_SKIP_SELF_URL_PATH_CHECKS') || !_SKIP_SELF_URL_PATH_CHECKS) &&
|
||||||
SELF_URL_PATH != $ref_self_url_path && SELF_URL_PATH != mb_substr($ref_self_url_path, 0, mb_strlen($ref_self_url_path)-1)) {
|
Config::get(Config::SELF_URL_PATH) != $ref_self_url_path && Config::get(Config::SELF_URL_PATH) != mb_substr($ref_self_url_path, 0, mb_strlen($ref_self_url_path)-1)) {
|
||||||
array_push($errors,
|
array_push($errors,
|
||||||
"Please set SELF_URL_PATH to the correct value detected for your server: <b>$ref_self_url_path</b> (you're using: <b>" . SELF_URL_PATH . "</b>)");
|
"Please set Config::get(Config::SELF_URL_PATH) to the correct value detected for your server: <b>$ref_self_url_path</b> (you're using: <b>" . Config::get(Config::SELF_URL_PATH) . "</b>)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable(ICONS_DIR)) {
|
if (!is_writable(Config::get(Config::ICONS_DIR))) {
|
||||||
array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".ICONS_DIR.").\n");
|
array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".Config::get(Config::ICONS_DIR).").\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable(LOCK_DIRECTORY)) {
|
if (!is_writable(Config::get(Config::LOCK_DIRECTORY))) {
|
||||||
array_push($errors, "LOCK_DIRECTORY defined in config.php is not writable (chmod -R 777 ".LOCK_DIRECTORY.").\n");
|
array_push($errors, "Config::get(Config::LOCK_DIRECTORY) defined in config.php is not writable (chmod -R 777 ".Config::get(Config::LOCK_DIRECTORY).").\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) {
|
if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) {
|
||||||
@@ -137,14 +119,6 @@
|
|||||||
array_push($errors, "PHP support for JSON is required, but was not found.");
|
array_push($errors, "PHP support for JSON is required, but was not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DB_TYPE == "mysql" && !function_exists("mysqli_connect")) {
|
|
||||||
array_push($errors, "PHP support for MySQL is required for configured DB_TYPE in config.php.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DB_TYPE == "pgsql" && !function_exists("pg_connect")) {
|
|
||||||
array_push($errors, "PHP support for PostgreSQL is required for configured DB_TYPE in config.php");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists("PDO")) {
|
if (!class_exists("PDO")) {
|
||||||
array_push($errors, "PHP support for PDO is required but was not found.");
|
array_push($errors, "PHP support for PDO is required but was not found.");
|
||||||
}
|
}
|
||||||
@@ -169,7 +143,7 @@
|
|||||||
array_push($errors, "PHP support for DOMDocument is required, but was not found.");
|
array_push($errors, "PHP support for DOMDocument is required, but was not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DB_TYPE == "mysql") {
|
if (Config::get(Config::DB_TYPE) == "mysql") {
|
||||||
$bad_tables = check_mysql_tables();
|
$bad_tables = check_mysql_tables();
|
||||||
|
|
||||||
if (count($bad_tables) > 0) {
|
if (count($bad_tables) > 0) {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<?php # This file has been generated at: Fri Feb 12 15:56:39 MSK 2021
|
|
||||||
define('GENERATED_CONFIG_CHECK', 26);
|
|
||||||
$required_defines = array( 'DB_TYPE', 'DB_HOST', 'DB_USER', 'DB_NAME', 'DB_PASS', 'MYSQL_CHARSET', 'SELF_URL_PATH', 'SINGLE_USER_MODE', 'SIMPLE_UPDATE_MODE', 'PHP_EXECUTABLE', 'LOCK_DIRECTORY', 'CACHE_DIR', 'ICONS_DIR', 'ICONS_URL', 'AUTH_AUTO_CREATE', 'AUTH_AUTO_LOGIN', 'FORCE_ARTICLE_PURGE', 'SESSION_COOKIE_LIFETIME', 'SMTP_FROM_NAME', 'SMTP_FROM_ADDRESS', 'DIGEST_SUBJECT', 'CHECK_FOR_UPDATES', 'ENABLE_GZIP_OUTPUT', 'PLUGINS', 'LOG_DESTINATION', 'CONFIG_VERSION'); ?>
|
|
||||||
@@ -1,31 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
// Original from http://www.daniweb.com/code/snippet43.html
|
namespace Sessions;
|
||||||
|
|
||||||
require_once "config.php";
|
|
||||||
require_once "classes/db.php";
|
|
||||||
require_once "autoload.php";
|
require_once "autoload.php";
|
||||||
|
require_once "functions.php";
|
||||||
require_once "errorhandler.php";
|
require_once "errorhandler.php";
|
||||||
require_once "lib/accept-to-gettext.php";
|
|
||||||
require_once "lib/gettext/gettext.inc.php";
|
require_once "lib/gettext/gettext.inc.php";
|
||||||
|
|
||||||
$session_expire = min(2147483647 - time() - 1, max(SESSION_COOKIE_LIFETIME, 86400));
|
$session_expire = min(2147483647 - time() - 1, max(\Config::get(\Config::SESSION_COOKIE_LIFETIME), 86400));
|
||||||
$session_name = (!defined('TTRSS_SESSION_NAME')) ? "ttrss_sid" : TTRSS_SESSION_NAME;
|
$session_name = \Config::get(\Config::SESSION_NAME);
|
||||||
|
|
||||||
if (is_server_https()) {
|
if (is_server_https()) {
|
||||||
ini_set("session.cookie_secure", true);
|
ini_set("session.cookie_secure", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
ini_set("session.gc_probability", 75);
|
ini_set("session.gc_probability", "75");
|
||||||
ini_set("session.name", $session_name);
|
ini_set("session.name", $session_name);
|
||||||
ini_set("session.use_only_cookies", true);
|
ini_set("session.use_only_cookies", "true");
|
||||||
ini_set("session.gc_maxlifetime", $session_expire);
|
ini_set("session.gc_maxlifetime", $session_expire);
|
||||||
ini_set("session.cookie_lifetime", 0);
|
ini_set("session.cookie_lifetime", "0");
|
||||||
|
|
||||||
function session_get_schema_version() {
|
function session_get_schema_version() {
|
||||||
global $schema_version;
|
global $schema_version;
|
||||||
|
|
||||||
if (!$schema_version) {
|
if (!$schema_version) {
|
||||||
$row = Db::pdo()->query("SELECT schema_version FROM ttrss_version")->fetch();
|
$row = \Db::pdo()->query("SELECT schema_version FROM ttrss_version")->fetch();
|
||||||
|
|
||||||
$version = $row["schema_version"];
|
$version = $row["schema_version"];
|
||||||
|
|
||||||
@@ -37,18 +35,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validate_session() {
|
function validate_session() {
|
||||||
if (SINGLE_USER_MODE) return true;
|
if (\Config::get(\Config::SINGLE_USER_MODE)) return true;
|
||||||
|
|
||||||
if (isset($_SESSION["ref_schema_version"]) && $_SESSION["ref_schema_version"] != session_get_schema_version()) {
|
if (isset($_SESSION["ref_schema_version"]) && $_SESSION["ref_schema_version"] != session_get_schema_version()) {
|
||||||
$_SESSION["login_error_msg"] =
|
$_SESSION["login_error_msg"] =
|
||||||
__("Session failed to validate (schema version changed)");
|
__("Session failed to validate (schema version changed)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$pdo = Db::pdo();
|
$pdo = \Db::pdo();
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"])) {
|
if (!empty($_SESSION["uid"])) {
|
||||||
|
|
||||||
if (!defined('_SESSION_SKIP_UA_CHECKS') && $_SESSION["user_agent"] != sha1($_SERVER['HTTP_USER_AGENT'])) {
|
if ($_SESSION["user_agent"] != sha1($_SERVER['HTTP_USER_AGENT'])) {
|
||||||
$_SESSION["login_error_msg"] = __("Session failed to validate (UA changed).");
|
$_SESSION["login_error_msg"] = __("Session failed to validate (UA changed).");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -87,7 +85,7 @@
|
|||||||
function ttrss_read ($id){
|
function ttrss_read ($id){
|
||||||
global $session_expire;
|
global $session_expire;
|
||||||
|
|
||||||
$sth = Db::pdo()->prepare("SELECT data FROM ttrss_sessions WHERE id=?");
|
$sth = \Db::pdo()->prepare("SELECT data FROM ttrss_sessions WHERE id=?");
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
@@ -96,7 +94,7 @@
|
|||||||
} else {
|
} else {
|
||||||
$expire = time() + $session_expire;
|
$expire = time() + $session_expire;
|
||||||
|
|
||||||
$sth = Db::pdo()->prepare("INSERT INTO ttrss_sessions (id, data, expire)
|
$sth = \Db::pdo()->prepare("INSERT INTO ttrss_sessions (id, data, expire)
|
||||||
VALUES (?, '', ?)");
|
VALUES (?, '', ?)");
|
||||||
$sth->execute([$id, $expire]);
|
$sth->execute([$id, $expire]);
|
||||||
|
|
||||||
@@ -112,14 +110,14 @@
|
|||||||
$data = base64_encode($data);
|
$data = base64_encode($data);
|
||||||
$expire = time() + $session_expire;
|
$expire = time() + $session_expire;
|
||||||
|
|
||||||
$sth = Db::pdo()->prepare("SELECT id FROM ttrss_sessions WHERE id=?");
|
$sth = \Db::pdo()->prepare("SELECT id FROM ttrss_sessions WHERE id=?");
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$sth = Db::pdo()->prepare("UPDATE ttrss_sessions SET data=?, expire=? WHERE id=?");
|
$sth = \Db::pdo()->prepare("UPDATE ttrss_sessions SET data=?, expire=? WHERE id=?");
|
||||||
$sth->execute([$data, $expire, $id]);
|
$sth->execute([$data, $expire, $id]);
|
||||||
} else {
|
} else {
|
||||||
$sth = Db::pdo()->prepare("INSERT INTO ttrss_sessions (id, data, expire)
|
$sth = \Db::pdo()->prepare("INSERT INTO ttrss_sessions (id, data, expire)
|
||||||
VALUES (?, ?, ?)");
|
VALUES (?, ?, ?)");
|
||||||
$sth->execute([$id, $data, $expire]);
|
$sth->execute([$id, $data, $expire]);
|
||||||
}
|
}
|
||||||
@@ -132,22 +130,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ttrss_destroy($id) {
|
function ttrss_destroy($id) {
|
||||||
$sth = Db::pdo()->prepare("DELETE FROM ttrss_sessions WHERE id = ?");
|
$sth = \Db::pdo()->prepare("DELETE FROM ttrss_sessions WHERE id = ?");
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ttrss_gc ($expire) {
|
function ttrss_gc ($expire) {
|
||||||
Db::pdo()->query("DELETE FROM ttrss_sessions WHERE expire < " . time());
|
\Db::pdo()->query("DELETE FROM ttrss_sessions WHERE expire < " . time());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SINGLE_USER_MODE /* && DB_TYPE == "pgsql" */) {
|
if (!\Config::get(\Config::SINGLE_USER_MODE)) {
|
||||||
session_set_save_handler("ttrss_open",
|
session_set_save_handler('\Sessions\ttrss_open',
|
||||||
"ttrss_close", "ttrss_read", "ttrss_write",
|
'\Sessions\ttrss_close', '\Sessions\ttrss_read',
|
||||||
"ttrss_destroy", "ttrss_gc");
|
'\Sessions\ttrss_write', '\Sessions\ttrss_destroy',
|
||||||
|
'\Sessions\ttrss_gc');
|
||||||
register_shutdown_function('session_write_close');
|
register_shutdown_function('session_write_close');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
102
index.php
102
index.php
@@ -1,9 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
if (!file_exists("config.php")) {
|
|
||||||
print "<b>Fatal Error</b>: You forgot to copy
|
|
||||||
<b>config.php-dist</b> to <b>config.php</b> and edit it.\n";
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need a separate check here because functions.php might get parsed
|
// we need a separate check here because functions.php might get parsed
|
||||||
// incorrectly before 5.3 because of :: syntax.
|
// incorrectly before 5.3 because of :: syntax.
|
||||||
@@ -12,15 +7,13 @@
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_include_path(dirname(__FILE__) ."/include" . PATH_SEPARATOR .
|
set_include_path(__DIR__ ."/include" . PATH_SEPARATOR .
|
||||||
get_include_path());
|
get_include_path());
|
||||||
|
|
||||||
require_once "autoload.php";
|
require_once "autoload.php";
|
||||||
require_once "sessions.php";
|
require_once "sessions.php";
|
||||||
require_once "functions.php";
|
require_once "functions.php";
|
||||||
require_once "sanity_check.php";
|
require_once "sanity_check.php";
|
||||||
require_once "config.php";
|
|
||||||
require_once "db-prefs.php";
|
|
||||||
|
|
||||||
if (!init_plugins()) return;
|
if (!init_plugins()) return;
|
||||||
|
|
||||||
@@ -42,12 +35,12 @@
|
|||||||
}
|
}
|
||||||
} ?>
|
} ?>
|
||||||
|
|
||||||
<?php if (theme_exists(LOCAL_OVERRIDE_STYLESHEET)) {
|
<?php if (theme_exists(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET))) {
|
||||||
echo stylesheet_tag(get_theme_path(LOCAL_OVERRIDE_STYLESHEET));
|
echo stylesheet_tag(get_theme_path(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET)));
|
||||||
} ?>
|
} ?>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const __csrf_token = "<?php echo $_SESSION["csrf_token"]; ?>";
|
const __csrf_token = "<?= $_SESSION["csrf_token"]; ?>";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php UserHelper::print_user_stylesheet() ?>
|
<?php UserHelper::print_user_stylesheet() ?>
|
||||||
@@ -68,7 +61,7 @@
|
|||||||
<script>
|
<script>
|
||||||
dojoConfig = {
|
dojoConfig = {
|
||||||
async: true,
|
async: true,
|
||||||
cacheBust: "<?php echo get_scripts_timestamp(); ?>",
|
cacheBust: "<?= get_scripts_timestamp(); ?>",
|
||||||
packages: [
|
packages: [
|
||||||
{ name: "fox", location: "../../js" },
|
{ name: "fox", location: "../../js" },
|
||||||
]
|
]
|
||||||
@@ -76,13 +69,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
foreach (array("lib/prototype.js",
|
foreach (["lib/dojo/dojo.js",
|
||||||
"lib/scriptaculous/scriptaculous.js?load=effects,controls",
|
|
||||||
"lib/dojo/dojo.js",
|
|
||||||
"lib/dojo/tt-rss-layer.js",
|
"lib/dojo/tt-rss-layer.js",
|
||||||
"js/tt-rss.js",
|
"js/tt-rss.js",
|
||||||
"js/common.js",
|
"js/common.js"] as $jsfile) {
|
||||||
"errors.php?mode=js") as $jsfile) {
|
|
||||||
|
|
||||||
echo javascript_tag($jsfile);
|
echo javascript_tag($jsfile);
|
||||||
|
|
||||||
@@ -132,7 +122,7 @@
|
|||||||
|
|
||||||
<div id="overlay" style="display : block">
|
<div id="overlay" style="display : block">
|
||||||
<div id="overlay_inner">
|
<div id="overlay_inner">
|
||||||
<?php echo __("Loading, please wait...") ?>
|
<?= __("Loading, please wait...") ?>
|
||||||
<div dojoType="dijit.ProgressBar" places="0" style="width : 300px" id="loading_bar"
|
<div dojoType="dijit.ProgressBar" places="0" style="width : 300px" id="loading_bar"
|
||||||
progress="0" maximum="100">
|
progress="0" maximum="100">
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +137,7 @@
|
|||||||
<div id="feeds-holder" dojoType="dijit.layout.ContentPane" region="leading" style="width : 20%" splitter="true">
|
<div id="feeds-holder" dojoType="dijit.layout.ContentPane" region="leading" style="width : 20%" splitter="true">
|
||||||
<div id="feedlistLoading">
|
<div id="feedlistLoading">
|
||||||
<img src='images/indicator_tiny.gif'/>
|
<img src='images/indicator_tiny.gif'/>
|
||||||
<?php echo __("Loading, please wait..."); ?></div>
|
<?= __("Loading, please wait..."); ?></div>
|
||||||
<?php
|
<?php
|
||||||
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_FEED_TREE, function ($result) {
|
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_FEED_TREE, function ($result) {
|
||||||
echo $result;
|
echo $result;
|
||||||
@@ -161,13 +151,13 @@
|
|||||||
<div id="toolbar" dojoType="fox.Toolbar">
|
<div id="toolbar" dojoType="fox.Toolbar">
|
||||||
|
|
||||||
<i class="material-icons net-alert" style="display : none"
|
<i class="material-icons net-alert" style="display : none"
|
||||||
title="<?php echo __("Communication problem with server.") ?>">error_outline</i>
|
title="<?= __("Communication problem with server.") ?>">error_outline</i>
|
||||||
|
|
||||||
<i class="material-icons log-alert" style="display : none" onclick="App.openPreferences('system')"
|
<i class="material-icons log-alert" style="display : none" onclick="App.openPreferences('system')"
|
||||||
title="<?php echo __("Recent entries found in event log.") ?>">warning</i>
|
title="<?= __("Recent entries found in event log.") ?>">warning</i>
|
||||||
|
|
||||||
<i id="updates-available" class="material-icons icon-new-version" style="display : none"
|
<i id="updates-available" class="material-icons icon-new-version" style="display : none"
|
||||||
title="<?php echo __('Updates are available from Git.') ?>">new_releases</i>
|
title="<?= __('Updates are available from Git.') ?>">new_releases</i>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
@@ -176,32 +166,32 @@
|
|||||||
});
|
});
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<form id="toolbar-headlines" action="" style="order : 10" onsubmit='return false'>
|
<div id="toolbar-headlines" dojoType="fox.Toolbar" style="order : 10">
|
||||||
|
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<form id="toolbar-main" action="" style="order : 20" onsubmit='return false'>
|
<form id="toolbar-main" action="" style="order : 20" onsubmit='return false'>
|
||||||
|
|
||||||
<select name="view_mode" title="<?php echo __('Show articles') ?>"
|
<select name="view_mode" title="<?= __('Show articles') ?>"
|
||||||
onchange="App.onViewModeChanged()"
|
onchange="App.onViewModeChanged()"
|
||||||
dojoType="fox.form.Select">
|
dojoType="fox.form.Select">
|
||||||
<option selected="selected" value="adaptive"><?php echo __('Adaptive') ?></option>
|
<option selected="selected" value="adaptive"><?= __('Adaptive') ?></option>
|
||||||
<option value="all_articles"><?php echo __('All Articles') ?></option>
|
<option value="all_articles"><?= __('All Articles') ?></option>
|
||||||
<option value="marked"><?php echo __('Starred') ?></option>
|
<option value="marked"><?= __('Starred') ?></option>
|
||||||
<option value="published"><?php echo __('Published') ?></option>
|
<option value="published"><?= __('Published') ?></option>
|
||||||
<option value="unread"><?php echo __('Unread') ?></option>
|
<option value="unread"><?= __('Unread') ?></option>
|
||||||
<option value="has_note"><?php echo __('With Note') ?></option>
|
<option value="has_note"><?= __('With Note') ?></option>
|
||||||
<!-- <option value="noscores"><?php echo __('Ignore Scoring') ?></option> -->
|
<!-- <option value="noscores"><?= __('Ignore Scoring') ?></option> -->
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select title="<?php echo __('Sort articles') ?>"
|
<select title="<?= __('Sort articles') ?>"
|
||||||
onchange="App.onViewModeChanged()"
|
onchange="App.onViewModeChanged()"
|
||||||
dojoType="fox.form.Select" name="order_by">
|
dojoType="fox.form.Select" name="order_by">
|
||||||
|
|
||||||
<option selected="selected" value="default"><?php echo __('Default') ?></option>
|
<option selected="selected" value="default"><?= __('Default') ?></option>
|
||||||
<option value="feed_dates"><?php echo __('Newest first') ?></option>
|
<option value="feed_dates"><?= __('Newest first') ?></option>
|
||||||
<option value="date_reverse"><?php echo __('Oldest first') ?></option>
|
<option value="date_reverse"><?= __('Oldest first') ?></option>
|
||||||
<option value="title"><?php echo __('Title') ?></option>
|
<option value="title"><?= __('Title') ?></option>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) {
|
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) {
|
||||||
@@ -213,16 +203,16 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div dojoType="fox.form.ComboButton" onclick="Feeds.catchupCurrent()">
|
<div dojoType="fox.form.ComboButton" onclick="Feeds.catchupCurrent()">
|
||||||
<span><?php echo __('Mark as read') ?></span>
|
<span><?= __('Mark as read') ?></span>
|
||||||
<div dojoType="dijit.DropDownMenu">
|
<div dojoType="dijit.DropDownMenu">
|
||||||
<div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1day')">
|
<div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1day')">
|
||||||
<?php echo __('Older than one day') ?>
|
<?= __('Older than one day') ?>
|
||||||
</div>
|
</div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1week')">
|
<div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1week')">
|
||||||
<?php echo __('Older than one week') ?>
|
<?= __('Older than one week') ?>
|
||||||
</div>
|
</div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('2week')">
|
<div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('2week')">
|
||||||
<?php echo __('Older than two weeks') ?>
|
<?= __('Older than two weeks') ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,21 +227,21 @@
|
|||||||
});
|
});
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div dojoType="fox.form.DropDownButton" class="action-button" title="<?php echo __('Actions...') ?>">
|
<div dojoType="fox.form.DropDownButton" class="action-button" title="<?= __('Actions...') ?>">
|
||||||
<span><i class="material-icons">menu</i></span>
|
<span><i class="material-icons">menu</i></span>
|
||||||
<div dojoType="dijit.Menu" style="display: none">
|
<div dojoType="dijit.Menu" style="display: none">
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcPrefs')"><?php echo __('Preferences...') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcPrefs')"><?= __('Preferences...') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcSearch')"><?php echo __('Search...') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcSearch')"><?= __('Search...') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" disabled="1"><?php echo __('Feed actions:') ?></div>
|
<div dojoType="dijit.MenuItem" disabled="1"><?= __('Feed actions:') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcAddFeed')"><?php echo __('Subscribe to feed...') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcAddFeed')"><?= __('Subscribe to feed...') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcEditFeed')"><?php echo __('Edit this feed...') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcEditFeed')"><?= __('Edit this feed...') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcRemoveFeed')"><?php echo __('Unsubscribe') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcRemoveFeed')"><?= __('Unsubscribe') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" disabled="1"><?php echo __('All feeds:') ?></div>
|
<div dojoType="dijit.MenuItem" disabled="1"><?= __('All feeds:') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcCatchupAll')"><?php echo __('Mark as read') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcCatchupAll')"><?= __('Mark as read') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcShowOnlyUnread')"><?php echo __('(Un)hide read feeds') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcShowOnlyUnread')"><?= __('(Un)hide read feeds') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" disabled="1"><?php echo __('Other actions:') ?></div>
|
<div dojoType="dijit.MenuItem" disabled="1"><?= __('Other actions:') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcToggleWidescreen')"><?php echo __('Toggle widescreen mode') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcToggleWidescreen')"><?= __('Toggle widescreen mode') ?></div>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcHKhelp')"><?php echo __('Keyboard shortcuts help') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcHKhelp')"><?= __('Keyboard shortcuts help') ?></div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_ACTION_ITEM, function ($result) {
|
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_ACTION_ITEM, function ($result) {
|
||||||
@@ -260,7 +250,7 @@
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if (empty($_SESSION["hide_logout"])) { ?>
|
<?php if (empty($_SESSION["hide_logout"])) { ?>
|
||||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcLogout')"><?php echo __('Logout') ?></div>
|
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcLogout')"><?= __('Logout') ?></div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +261,7 @@
|
|||||||
<div id="headlines-frame" dojoType="dijit.layout.ContentPane" tabindex="0"
|
<div id="headlines-frame" dojoType="dijit.layout.ContentPane" tabindex="0"
|
||||||
region="center">
|
region="center">
|
||||||
<div id="headlinesInnerContainer">
|
<div id="headlinesInnerContainer">
|
||||||
<div class="whiteBox"><?php echo __('Loading, please wait...') ?></div>
|
<div class="whiteBox"><?= __('Loading, please wait...') ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="content-insert" dojoType="dijit.layout.ContentPane" region="bottom"
|
<div id="content-insert" dojoType="dijit.layout.ContentPane" region="bottom"
|
||||||
|
|||||||
434
js/App.js
434
js/App.js
@@ -1,9 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* eslint-disable new-cap */
|
/* eslint-disable new-cap */
|
||||||
/* global __, Article, Ajax, Headlines, Filters, fox */
|
/* global __, Article, Headlines, Filters, fox */
|
||||||
/* global xhrPost, xhrJson, dojo, dijit, PluginHost, Notify, $$, Feeds, Cookie */
|
/* global xhr, dojo, dijit, PluginHost, Notify, Feeds, Cookie */
|
||||||
/* global CommonDialogs, Plugins, Effect */
|
/* global CommonDialogs, Plugins */
|
||||||
|
|
||||||
const App = {
|
const App = {
|
||||||
_initParams: [],
|
_initParams: [],
|
||||||
@@ -18,8 +18,53 @@ const App = {
|
|||||||
is_prefs: false,
|
is_prefs: false,
|
||||||
LABEL_BASE_INDEX: -1024,
|
LABEL_BASE_INDEX: -1024,
|
||||||
FormFields: {
|
FormFields: {
|
||||||
hidden: function(name, value) {
|
attributes_to_string: function(attributes) {
|
||||||
return `<input dojoType="dijit.form.TextBox" style="display : none" name="${name}" value="${App.escapeHtml(value)}"></input>`
|
return Object.keys(attributes).map((k) =>
|
||||||
|
`${App.escapeHtml(k)}="${App.escapeHtml(attributes[k])}"`)
|
||||||
|
.join(" ");
|
||||||
|
},
|
||||||
|
hidden_tag: function(name, value, attributes = {}, id = "") {
|
||||||
|
return `<input id="${App.escapeHtml(id)}" dojoType="dijit.form.TextBox" ${this.attributes_to_string(attributes)}
|
||||||
|
style="display : none" name="${name}" value="${App.escapeHtml(value)}"></input>`
|
||||||
|
},
|
||||||
|
// allow html inside because of icons
|
||||||
|
button_tag: function(value, type, attributes = {}) {
|
||||||
|
return `<button dojoType="dijit.form.Button" ${this.attributes_to_string(attributes)}
|
||||||
|
type="${type}">${value}</button>`
|
||||||
|
|
||||||
|
},
|
||||||
|
icon: function(icon, attributes = {}) {
|
||||||
|
return `<i class="material-icons" ${this.attributes_to_string(attributes)}>${icon}</i>`;
|
||||||
|
},
|
||||||
|
submit_tag: function(value, attributes = {}) {
|
||||||
|
return this.button_tag(value, "submit", {...{class: "alt-primary"}, ...attributes});
|
||||||
|
},
|
||||||
|
cancel_dialog_tag: function(value, attributes = {}) {
|
||||||
|
return this.button_tag(value, "", {...{onclick: "App.dialogOf(this).hide()"}, ...attributes});
|
||||||
|
},
|
||||||
|
checkbox_tag: function(name, checked = false, value = "", attributes = {}, id = "") {
|
||||||
|
return `<input dojoType="dijit.form.CheckBox" type="checkbox" name="${App.escapeHtml(name)}"
|
||||||
|
${checked ? "checked" : ""}
|
||||||
|
${value ? `value="${App.escapeHtml(value)}"` : ""}
|
||||||
|
${this.attributes_to_string(attributes)} id="${App.escapeHtml(id)}">`
|
||||||
|
},
|
||||||
|
select_tag: function(name, value, values = [], attributes = {}, id = "") {
|
||||||
|
return `
|
||||||
|
<select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
|
||||||
|
${values.map((v) =>
|
||||||
|
`<option ${v == value ? 'selected="selected"' : ''} value="${App.escapeHtml(v)}">${App.escapeHtml(v)}</option>`
|
||||||
|
).join("")}
|
||||||
|
</select>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
select_hash: function(name, value, values = {}, attributes = {}, id = "") {
|
||||||
|
return `
|
||||||
|
<select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
|
||||||
|
${Object.keys(values).map((vk) =>
|
||||||
|
`<option ${vk == value ? 'selected="selected"' : ''} value="${App.escapeHtml(vk)}">${App.escapeHtml(values[vk])}</option>`
|
||||||
|
).join("")}
|
||||||
|
</select>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Scrollable: {
|
Scrollable: {
|
||||||
@@ -53,7 +98,25 @@ const App = {
|
|||||||
|
|
||||||
return elem.offsetTop + elem.offsetHeight <= ctr.scrollTop + ctr.offsetHeight &&
|
return elem.offsetTop + elem.offsetHeight <= ctr.scrollTop + ctr.offsetHeight &&
|
||||||
elem.offsetTop >= ctr.scrollTop;
|
elem.offsetTop >= ctr.scrollTop;
|
||||||
|
},
|
||||||
|
scrollTo: function (elem, ctr, params = {}) {
|
||||||
|
const force_to_top = params.force_to_top || false;
|
||||||
|
|
||||||
|
if (!elem || !ctr) return;
|
||||||
|
|
||||||
|
if (force_to_top || !App.Scrollable.fitsInContainer(elem, ctr)) {
|
||||||
|
ctr.scrollTop = elem.offsetTop;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
byId: function(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
},
|
||||||
|
find: function(query) {
|
||||||
|
return document.querySelector(query)
|
||||||
|
},
|
||||||
|
findAll: function(query) {
|
||||||
|
return document.querySelectorAll(query);
|
||||||
},
|
},
|
||||||
dialogOf: function (elem) {
|
dialogOf: function (elem) {
|
||||||
|
|
||||||
@@ -62,6 +125,9 @@ const App = {
|
|||||||
|
|
||||||
return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
|
return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
|
||||||
},
|
},
|
||||||
|
getPhArgs(plugin, method, args = {}) {
|
||||||
|
return {...{op: "pluginhandler", plugin: plugin, method: method}, ...args};
|
||||||
|
},
|
||||||
label_to_feed_id: function(label) {
|
label_to_feed_id: function(label) {
|
||||||
return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
|
return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
|
||||||
},
|
},
|
||||||
@@ -83,21 +149,20 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setupNightModeDetection: function(callback) {
|
setupNightModeDetection: function(callback) {
|
||||||
if (!$("theme_css")) {
|
if (!App.byId("theme_css")) {
|
||||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mql.addEventListener("change", () => {
|
mql.addEventListener("change", () => {
|
||||||
this.nightModeChanged(mql.matches, $("theme_auto_css"));
|
this.nightModeChanged(mql.matches, App.byId("theme_auto_css"));
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("exception while trying to set MQL event listener");
|
console.warn("exception while trying to set MQL event listener");
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = new Element("link", {
|
const link = document.createElement("link");
|
||||||
rel: "stylesheet",
|
link.rel = "stylesheet";
|
||||||
id: "theme_auto_css"
|
link.id = "theme_auto_css";
|
||||||
});
|
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
link.onload = function() {
|
link.onload = function() {
|
||||||
@@ -119,27 +184,6 @@ const App = {
|
|||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enableCsrfSupport: function() {
|
|
||||||
const _this = this;
|
|
||||||
|
|
||||||
Ajax.Base.prototype.initialize = Ajax.Base.prototype.initialize.wrap(
|
|
||||||
function (callOriginal, options) {
|
|
||||||
|
|
||||||
if (_this.getInitParam("csrf_token") != undefined) {
|
|
||||||
Object.extend(options, options || { });
|
|
||||||
|
|
||||||
if (Object.isString(options.parameters))
|
|
||||||
options.parameters = options.parameters.toQueryParams();
|
|
||||||
else if (Object.isHash(options.parameters))
|
|
||||||
options.parameters = options.parameters.toObject();
|
|
||||||
|
|
||||||
options.parameters["csrf_token"] = _this.getInitParam("csrf_token");
|
|
||||||
}
|
|
||||||
|
|
||||||
return callOriginal(options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
postCurrentWindow: function(target, params) {
|
postCurrentWindow: function(target, params) {
|
||||||
const form = document.createElement("form");
|
const form = document.createElement("form");
|
||||||
|
|
||||||
@@ -188,8 +232,13 @@ const App = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
urlParam: function(param) {
|
urlParam: function(name) {
|
||||||
return String(window.location.href).parseQuery()[param];
|
try {
|
||||||
|
const results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
|
||||||
|
return decodeURIComponent(results[1].replace(/\+/g, " ")) || 0;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
next_seq: function() {
|
next_seq: function() {
|
||||||
this._rpc_seq += 1;
|
this._rpc_seq += 1;
|
||||||
@@ -205,7 +254,7 @@ const App = {
|
|||||||
dijit.byId("loading_bar").update({progress: this._loading_progress});
|
dijit.byId("loading_bar").update({progress: this._loading_progress});
|
||||||
|
|
||||||
if (this._loading_progress >= 90) {
|
if (this._loading_progress >= 90) {
|
||||||
$("overlay").hide();
|
App.byId("overlay").hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -236,7 +285,7 @@ const App = {
|
|||||||
if (!this.hotkey_prefix && hotkeys_map[0].indexOf(keychar) != -1) {
|
if (!this.hotkey_prefix && hotkeys_map[0].indexOf(keychar) != -1) {
|
||||||
|
|
||||||
this.hotkey_prefix = keychar;
|
this.hotkey_prefix = keychar;
|
||||||
$("cmdline").innerHTML = keychar;
|
App.byId("cmdline").innerHTML = keychar;
|
||||||
Element.show("cmdline");
|
Element.show("cmdline");
|
||||||
|
|
||||||
window.clearTimeout(this.hotkey_prefix_timeout);
|
window.clearTimeout(this.hotkey_prefix_timeout);
|
||||||
@@ -285,16 +334,17 @@ const App = {
|
|||||||
cleanupMemory: function(root) {
|
cleanupMemory: function(root) {
|
||||||
const dijits = dojo.query("[widgetid]", dijit.byId(root).domNode).map(dijit.byNode);
|
const dijits = dojo.query("[widgetid]", dijit.byId(root).domNode).map(dijit.byNode);
|
||||||
|
|
||||||
dijits.each(function (d) {
|
dijits.forEach(function (d) {
|
||||||
dojo.destroy(d.domNode);
|
dojo.destroy(d.domNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
$$("#" + root + " *").each(function (i) {
|
App.findAll("#" + root + " *").forEach(function (i) {
|
||||||
i.parentNode ? i.parentNode.removeChild(i) : true;
|
i.parentNode ? i.parentNode.removeChild(i) : true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// htmlspecialchars()-alike for headlines data-content attribute
|
// htmlspecialchars()-alike for headlines data-content attribute
|
||||||
escapeHtml: function(text) {
|
escapeHtml: function(p) {
|
||||||
|
if (typeof p == "string") {
|
||||||
const map = {
|
const map = {
|
||||||
'&': '&',
|
'&': '&',
|
||||||
'<': '<',
|
'<': '<',
|
||||||
@@ -303,95 +353,98 @@ const App = {
|
|||||||
"'": '''
|
"'": '''
|
||||||
};
|
};
|
||||||
|
|
||||||
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
return p.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||||
|
} else {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac
|
||||||
|
getSelectedText: function() {
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
if (typeof window.getSelection != "undefined") {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (sel.rangeCount) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
for (let i = 0, len = sel.rangeCount; i < len; ++i) {
|
||||||
|
container.appendChild(sel.getRangeAt(i).cloneContents());
|
||||||
|
}
|
||||||
|
text = container.innerHTML;
|
||||||
|
}
|
||||||
|
} else if (typeof document.selection != "undefined") {
|
||||||
|
if (document.selection.type == "Text") {
|
||||||
|
text = document.selection.createRange().textText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.stripTags();
|
||||||
},
|
},
|
||||||
displayIfChecked: function(checkbox, elemId) {
|
displayIfChecked: function(checkbox, elemId) {
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
Effect.Appear(elemId, {duration : 0.5});
|
Element.show(elemId);
|
||||||
} else {
|
} else {
|
||||||
Effect.Fade(elemId, {duration : 0.5});
|
Element.hide(elemId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
helpDialog: function(topic) {
|
hotkeyHelp: function() {
|
||||||
xhrPost("backend.php", {op: "backend", method: "help", topic: topic}, (transport) => {
|
xhr.post("backend.php", {op: "rpc", method: "hotkeyHelp"}, (reply) => {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
title: __("Help"),
|
title: __("Keyboard shortcuts"),
|
||||||
content: transport.responseText,
|
content: reply,
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleRpcJson: function(transport) {
|
handleRpcJson: function(reply) {
|
||||||
|
|
||||||
const netalert = $$("#toolbar .net-alert")[0];
|
const netalert = App.find(".net-alert");
|
||||||
|
|
||||||
try {
|
|
||||||
const reply = JSON.parse(transport.responseText);
|
|
||||||
|
|
||||||
if (reply) {
|
if (reply) {
|
||||||
const error = reply['error'];
|
const error = reply['error'];
|
||||||
|
const seq = reply['seq'];
|
||||||
|
const message = reply['message'];
|
||||||
|
const counters = reply['counters'];
|
||||||
|
const runtime_info = reply['runtime-info'];
|
||||||
|
|
||||||
if (error) {
|
if (error && error.code && error.code != App.Error.E_SUCCESS) {
|
||||||
const code = error['code'];
|
console.warn("handleRpcJson: fatal error", error);
|
||||||
const msg = error['message'];
|
this.Error.fatal(error.code);
|
||||||
|
|
||||||
console.warn("[handleRpcJson] received fatal error ", code, msg);
|
|
||||||
|
|
||||||
if (code != 0) {
|
|
||||||
/* global ERRORS */
|
|
||||||
this.Error.fatal(ERRORS[code], {info: msg, code: code});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const seq = reply['seq'];
|
|
||||||
|
|
||||||
if (seq && this.get_seq() != seq) {
|
if (seq && this.get_seq() != seq) {
|
||||||
console.log("[handleRpcJson] sequence mismatch: ", seq, '!=', this.get_seq());
|
console.warn("handleRpcJson: sequence mismatch: ", seq, '!=', this.get_seq());
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = reply['message'];
|
// not in preferences
|
||||||
|
if (typeof Feeds != "undefined") {
|
||||||
if (message == "UPDATE_COUNTERS") {
|
if (message == "UPDATE_COUNTERS") {
|
||||||
console.log("need to refresh counters...");
|
console.log("need to refresh counters for", reply.feeds);
|
||||||
Feeds.requestCounters(true);
|
Feeds.requestCounters(reply.feeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
const counters = reply['counters'];
|
|
||||||
|
|
||||||
if (counters)
|
if (counters)
|
||||||
Feeds.parseCounters(counters);
|
Feeds.parseCounters(counters);
|
||||||
|
}
|
||||||
const runtime_info = reply['runtime-info'];
|
|
||||||
|
|
||||||
if (runtime_info)
|
if (runtime_info)
|
||||||
this.parseRuntimeInfo(runtime_info);
|
this.parseRuntimeInfo(runtime_info);
|
||||||
|
|
||||||
if (netalert) netalert.hide();
|
if (netalert) netalert.hide();
|
||||||
|
|
||||||
return reply;
|
return true;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (netalert) netalert.show();
|
if (netalert) netalert.show();
|
||||||
|
|
||||||
Notify.error("Communication problem with server.");
|
Notify.error("Communication problem with server.");
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
if (netalert) netalert.show();
|
|
||||||
|
|
||||||
Notify.error("Communication problem with server.");
|
|
||||||
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
parseRuntimeInfo: function(data) {
|
parseRuntimeInfo: function(data) {
|
||||||
for (const k in data) {
|
Object.keys(data).forEach((k) => {
|
||||||
if (data.hasOwnProperty(k)) {
|
|
||||||
const v = data[k];
|
const v = data[k];
|
||||||
|
|
||||||
console.log("RI:", k, "=>", v);
|
console.log("RI:", k, "=>", v);
|
||||||
@@ -402,7 +455,7 @@ const App = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (k == "recent_log_events") {
|
if (k == "recent_log_events") {
|
||||||
const alert = $$(".log-alert")[0];
|
const alert = App.find(".log-alert");
|
||||||
|
|
||||||
if (alert) {
|
if (alert) {
|
||||||
v > 0 ? alert.show() : alert.hide();
|
v > 0 ? alert.show() : alert.hide();
|
||||||
@@ -414,36 +467,21 @@ const App = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof Feeds != "undefined") {
|
||||||
if (k == "max_feed_id" || k == "num_feeds") {
|
if (k == "max_feed_id" || k == "num_feeds") {
|
||||||
if (this.getInitParam(k) != v) {
|
if (this.getInitParam(k) && this.getInitParam(k) != v) {
|
||||||
console.log("feed count changed, need to reload feedlist.");
|
console.log("feed count changed, need to reload feedlist:", this.getInitParam(k), v);
|
||||||
Feeds.reload();
|
Feeds.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setInitParam(k, v);
|
this.setInitParam(k, v);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
|
PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
|
||||||
},
|
},
|
||||||
backendSanityCallback: function(transport) {
|
backendSanityCallback: function(reply) {
|
||||||
const reply = JSON.parse(transport.responseText);
|
|
||||||
|
|
||||||
if (!reply) {
|
|
||||||
this.Error.fatal(ERRORS[3], {info: transport.responseText});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reply['error']) {
|
|
||||||
const code = reply['error']['code'];
|
|
||||||
|
|
||||||
if (code && code != 0) {
|
|
||||||
return this.Error.fatal(ERRORS[code],
|
|
||||||
{code: code, info: reply['error']['message']});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("sanity check ok");
|
console.log("sanity check ok");
|
||||||
|
|
||||||
const params = reply['init-params'];
|
const params = reply['init-params'];
|
||||||
@@ -451,15 +489,14 @@ const App = {
|
|||||||
if (params) {
|
if (params) {
|
||||||
console.log('reading init-params...');
|
console.log('reading init-params...');
|
||||||
|
|
||||||
for (const k in params) {
|
Object.keys(params).forEach((k) => {
|
||||||
if (params.hasOwnProperty(k)) {
|
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case "label_base_index":
|
case "label_base_index":
|
||||||
this.LABEL_BASE_INDEX = parseInt(params[k]);
|
this.LABEL_BASE_INDEX = parseInt(params[k]);
|
||||||
break;
|
break;
|
||||||
case "cdm_auto_catchup":
|
case "cdm_auto_catchup":
|
||||||
if (params[k] == 1) {
|
if (params[k] == 1) {
|
||||||
const hl = $("headlines-frame");
|
const hl = App.byId("headlines-frame");
|
||||||
if (hl) hl.addClassName("auto_catchup");
|
if (hl) hl.addClassName("auto_catchup");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -468,12 +505,11 @@ const App = {
|
|||||||
// i.e. *(191)|Ctrl-/ -> *(191)
|
// i.e. *(191)|Ctrl-/ -> *(191)
|
||||||
{
|
{
|
||||||
const tmp = [];
|
const tmp = [];
|
||||||
for (const sequence in params[k][1]) {
|
|
||||||
if (params[k][1].hasOwnProperty(sequence)) {
|
Object.keys(params[k][1]).forEach((sequence) => {
|
||||||
const filtered = sequence.replace(/\|.*$/, "");
|
const filtered = sequence.replace(/\|.*$/, "");
|
||||||
tmp[filtered] = params[k][1][sequence];
|
tmp[filtered] = params[k][1][sequence];
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
params[k][1] = tmp;
|
params[k][1] = tmp;
|
||||||
}
|
}
|
||||||
@@ -482,8 +518,7 @@ const App = {
|
|||||||
|
|
||||||
console.log("IP:", k, "=>", params[k]);
|
console.log("IP:", k, "=>", params[k]);
|
||||||
this.setInitParam(k, params[k]);
|
this.setInitParam(k, params[k]);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// PluginHost might not be available on non-index pages
|
// PluginHost might not be available on non-index pages
|
||||||
if (typeof PluginHost !== 'undefined')
|
if (typeof PluginHost !== 'undefined')
|
||||||
@@ -493,69 +528,68 @@ const App = {
|
|||||||
this.initSecondStage();
|
this.initSecondStage();
|
||||||
},
|
},
|
||||||
Error: {
|
Error: {
|
||||||
fatal: function (error, params) {
|
E_SUCCESS: "E_SUCCESS",
|
||||||
params = params || {};
|
E_UNAUTHORIZED: "E_UNAUTHORIZED",
|
||||||
|
E_SCHEMA_MISMATCH: "E_SCHEMA_MISMATCH",
|
||||||
if (params.code) {
|
fatal: function (error, params = {}) {
|
||||||
if (params.code == 6) {
|
if (error == App.Error.E_UNAUTHORIZED) {
|
||||||
window.location.href = "index.php";
|
window.location.href = "index.php";
|
||||||
return;
|
return;
|
||||||
} else if (params.code == 5) {
|
} else if (error == App.Error.E_SCHEMA_MISMATCH) {
|
||||||
window.location.href = "public.php?op=dbupdate";
|
window.location.href = "public.php?op=dbupdate";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return this.report(error,
|
return this.report(__("Fatal error: %s").replace("%s", error),
|
||||||
Object.extend({title: __("Fatal error")}, params));
|
{...{title: __("Fatal error")}, ...params});
|
||||||
},
|
},
|
||||||
report: function(error, params) {
|
report: function(error, params = {}) {
|
||||||
params = params || {};
|
|
||||||
|
|
||||||
if (!error) return;
|
if (!error) return;
|
||||||
|
|
||||||
console.error("[Error.report]", error, params);
|
console.error("error.report:", error, params);
|
||||||
|
|
||||||
const message = params.message ? params.message : error.toString();
|
const message = params.message ? params.message : error.toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
xhrPost("backend.php",
|
xhr.post("backend.php",
|
||||||
{op: "rpc", method: "log",
|
{op: "rpc", method: "log",
|
||||||
file: params.filename ? params.filename : error.fileName,
|
file: params.filename ? params.filename : error.fileName,
|
||||||
line: params.lineno ? params.lineno : error.lineNumber,
|
line: params.lineno ? params.lineno : error.lineNumber,
|
||||||
msg: message,
|
msg: message,
|
||||||
context: error.stack},
|
context: error.stack},
|
||||||
(transport) => {
|
(reply) => {
|
||||||
console.warn("[Error.report] log response", transport.responseText);
|
console.warn("[Error.report] log response", reply);
|
||||||
});
|
});
|
||||||
} catch (re) {
|
} catch (re) {
|
||||||
console.error("[Error.report] exception while saving logging error on server", re);
|
console.error("[Error.report] exception while saving logging error on server", re);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let stack_msg = "";
|
|
||||||
|
|
||||||
if (error.stack)
|
|
||||||
stack_msg += `<div><b>Stack trace:</b></div>
|
|
||||||
<textarea name="stack" readonly="1">${error.stack}</textarea>`;
|
|
||||||
|
|
||||||
if (params.info)
|
|
||||||
stack_msg += `<div><b>Additional information:</b></div>
|
|
||||||
<textarea name="stack" readonly="1">${params.info}</textarea>`;
|
|
||||||
|
|
||||||
const content = `<div class="error-contents">
|
|
||||||
<p class="message">${message}</p>
|
|
||||||
${stack_msg}
|
|
||||||
<div class="dlgButtons">
|
|
||||||
<button dojoType="dijit.form.Button"
|
|
||||||
onclick="dijit.byId('exceptionDlg').hide()">${__('Close this window')}</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "exceptionDlg",
|
|
||||||
title: params.title || __("Unhandled exception"),
|
title: params.title || __("Unhandled exception"),
|
||||||
content: content
|
content: `
|
||||||
|
<div class='exception-contents'>
|
||||||
|
<h3>${message}</h3>
|
||||||
|
|
||||||
|
<header>${__('Stack trace')}</header>
|
||||||
|
<section>
|
||||||
|
<textarea readonly='readonly'>${error.stack}</textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
${params && params.info ?
|
||||||
|
`
|
||||||
|
<header>${__('Additional information')}</header>
|
||||||
|
<section>
|
||||||
|
<textarea readonly='readonly'>${params.info}</textarea>
|
||||||
|
</section>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<footer class='text-center'>
|
||||||
|
<button dojoType="dijit.form.Button" class='alt-primary' type='submit'>
|
||||||
|
${__('Close this window')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>`
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -575,6 +609,10 @@ const App = {
|
|||||||
isPrefs() {
|
isPrefs() {
|
||||||
return this.is_prefs;
|
return this.is_prefs;
|
||||||
},
|
},
|
||||||
|
audioCanPlay: function(ctype) {
|
||||||
|
const a = document.createElement('audio');
|
||||||
|
return a.canPlayType(ctype);
|
||||||
|
},
|
||||||
init: function(parser, is_prefs) {
|
init: function(parser, is_prefs) {
|
||||||
this.is_prefs = is_prefs;
|
this.is_prefs = is_prefs;
|
||||||
window.onerror = this.Error.onWindowError;
|
window.onerror = this.Error.onWindowError;
|
||||||
@@ -591,24 +629,17 @@ const App = {
|
|||||||
|
|
||||||
this.setLoadingProgress(30);
|
this.setLoadingProgress(30);
|
||||||
this.initHotkeyActions();
|
this.initHotkeyActions();
|
||||||
this.enableCsrfSupport();
|
|
||||||
|
|
||||||
const a = document.createElement('audio');
|
|
||||||
const hasAudio = !!a.canPlayType;
|
|
||||||
const hasSandbox = "sandbox" in document.createElement("iframe");
|
|
||||||
const hasMp3 = !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
|
|
||||||
const clientTzOffset = new Date().getTimezoneOffset() * 60;
|
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
op: "rpc", method: "sanityCheck", hasAudio: hasAudio,
|
op: "rpc",
|
||||||
hasMp3: hasMp3,
|
method: "sanityCheck",
|
||||||
clientTzOffset: clientTzOffset,
|
clientTzOffset: new Date().getTimezoneOffset() * 60,
|
||||||
hasSandbox: hasSandbox
|
hasSandbox: "sandbox" in document.createElement("iframe")
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", params, (transport) => {
|
xhr.json("backend.php", params, (reply) => {
|
||||||
try {
|
try {
|
||||||
this.backendSanityCallback(transport);
|
this.backendSanityCallback(reply);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.Error.report(e);
|
this.Error.report(e);
|
||||||
}
|
}
|
||||||
@@ -618,7 +649,7 @@ const App = {
|
|||||||
checkBrowserFeatures: function() {
|
checkBrowserFeatures: function() {
|
||||||
let errorMsg = "";
|
let errorMsg = "";
|
||||||
|
|
||||||
['MutationObserver'].each(function(wf) {
|
['MutationObserver'].forEach(function(wf) {
|
||||||
if (!(wf in window)) {
|
if (!(wf in window)) {
|
||||||
errorMsg = `Browser feature check failed: <code>window.${wf}</code> not found.`;
|
errorMsg = `Browser feature check failed: <code>window.${wf}</code> not found.`;
|
||||||
throw new Error(errorMsg);
|
throw new Error(errorMsg);
|
||||||
@@ -631,6 +662,11 @@ const App = {
|
|||||||
|
|
||||||
return errorMsg == "";
|
return errorMsg == "";
|
||||||
},
|
},
|
||||||
|
updateRuntimeInfo: function() {
|
||||||
|
xhr.json("backend.php", {op: "rpc", method: "getruntimeinfo"}, () => {
|
||||||
|
// handled by xhr.json()
|
||||||
|
});
|
||||||
|
},
|
||||||
initSecondStage: function() {
|
initSecondStage: function() {
|
||||||
|
|
||||||
document.onkeydown = (event) => this.hotkeyHandler(event);
|
document.onkeydown = (event) => this.hotkeyHandler(event);
|
||||||
@@ -648,14 +684,18 @@ const App = {
|
|||||||
if (tab) {
|
if (tab) {
|
||||||
dijit.byId("pref-tabs").selectChild(tab);
|
dijit.byId("pref-tabs").selectChild(tab);
|
||||||
|
|
||||||
switch (this.urlParam('method')) {
|
const method = this.urlParam("method");
|
||||||
|
|
||||||
|
if (method) {
|
||||||
|
switch (method) {
|
||||||
case "editfeed":
|
case "editfeed":
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
CommonDialogs.editFeed(this.urlParam('methodparam'))
|
CommonDialogs.editFeed(this.urlParam('methodparam'))
|
||||||
}, 100);
|
}, 100);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn("initSecondStage, unknown method:", this.urlParam("method"));
|
console.warn("initSecondStage, unknown method:", method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -671,6 +711,7 @@ const App = {
|
|||||||
|
|
||||||
dojo.connect(dijit.byId("pref-tabs"), "selectChild", function (elem) {
|
dojo.connect(dijit.byId("pref-tabs"), "selectChild", function (elem) {
|
||||||
localStorage.setItem("ttrss:prefs-tab", elem.id);
|
localStorage.setItem("ttrss:prefs-tab", elem.id);
|
||||||
|
App.updateRuntimeInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -726,24 +767,28 @@ const App = {
|
|||||||
}, 3600 * 1000);
|
}, 3600 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("second stage ok");
|
|
||||||
|
|
||||||
PluginHost.run(PluginHost.HOOK_INIT_COMPLETE, null);
|
PluginHost.run(PluginHost.HOOK_INIT_COMPLETE, null);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.getInitParam("bw_limit"))
|
||||||
|
window.setInterval(() => {
|
||||||
|
App.updateRuntimeInfo();
|
||||||
|
}, 60 * 1000)
|
||||||
|
|
||||||
|
console.log("second stage ok");
|
||||||
|
|
||||||
},
|
},
|
||||||
checkForUpdates: function() {
|
checkForUpdates: function() {
|
||||||
console.log('checking for updates...');
|
console.log('checking for updates...');
|
||||||
|
|
||||||
xhrJson("backend.php", {op: 'rpc', method: 'checkforupdates'})
|
xhr.json("backend.php", {op: 'rpc', method: 'checkforupdates'})
|
||||||
.then((reply) => {
|
.then((reply) => {
|
||||||
console.log('update reply', reply);
|
console.log('update reply', reply);
|
||||||
|
|
||||||
if (reply.id) {
|
if (reply.id) {
|
||||||
$("updates-available").show();
|
App.byId("updates-available").show();
|
||||||
} else {
|
} else {
|
||||||
$("updates-available").hide();
|
App.byId("updates-available").hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -759,7 +804,7 @@ const App = {
|
|||||||
onViewModeChanged: function() {
|
onViewModeChanged: function() {
|
||||||
const view_mode = document.forms["toolbar-main"].view_mode.value;
|
const view_mode = document.forms["toolbar-main"].view_mode.value;
|
||||||
|
|
||||||
$$("body")[0].setAttribute("view-mode", view_mode);
|
App.findAll("body")[0].setAttribute("view-mode", view_mode);
|
||||||
|
|
||||||
return Feeds.reloadCurrent('');
|
return Feeds.reloadCurrent('');
|
||||||
},
|
},
|
||||||
@@ -798,8 +843,8 @@ const App = {
|
|||||||
{width: Cookie.get("ttrss_ci_width") + "px" });
|
{width: Cookie.get("ttrss_ci_width") + "px" });
|
||||||
}
|
}
|
||||||
|
|
||||||
$("headlines-frame").setStyle({ borderBottomWidth: '0px' });
|
App.byId("headlines-frame").setStyle({ borderBottomWidth: '0px' });
|
||||||
$("headlines-frame").addClassName("wide");
|
App.byId("headlines-frame").addClassName("wide");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@@ -814,8 +859,8 @@ const App = {
|
|||||||
{height: Cookie.get("ttrss_ci_height") + "px" });
|
{height: Cookie.get("ttrss_ci_height") + "px" });
|
||||||
}
|
}
|
||||||
|
|
||||||
$("headlines-frame").setStyle({ borderBottomWidth: '1px' });
|
App.byId("headlines-frame").setStyle({ borderBottomWidth: '1px' });
|
||||||
$("headlines-frame").removeClassName("wide");
|
App.byId("headlines-frame").removeClassName("wide");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,13 +868,13 @@ const App = {
|
|||||||
|
|
||||||
if (article_id) Article.view(article_id);
|
if (article_id) Article.view(article_id);
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0});
|
xhr.post("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0});
|
||||||
},
|
},
|
||||||
initHotkeyActions: function() {
|
initHotkeyActions: function() {
|
||||||
if (this.is_prefs) {
|
if (this.is_prefs) {
|
||||||
|
|
||||||
this.hotkey_actions["feed_subscribe"] = () => {
|
this.hotkey_actions["feed_subscribe"] = () => {
|
||||||
CommonDialogs.quickAddFeed();
|
CommonDialogs.subscribeToFeed();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.hotkey_actions["create_label"] = () => {
|
this.hotkey_actions["create_label"] = () => {
|
||||||
@@ -841,7 +886,7 @@ const App = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.hotkey_actions["help_dialog"] = () => {
|
this.hotkey_actions["help_dialog"] = () => {
|
||||||
this.helpDialog("main");
|
this.hotkeyHelp();
|
||||||
};
|
};
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -985,14 +1030,13 @@ const App = {
|
|||||||
Feeds.toggleUnread();
|
Feeds.toggleUnread();
|
||||||
};
|
};
|
||||||
this.hotkey_actions["feed_subscribe"] = () => {
|
this.hotkey_actions["feed_subscribe"] = () => {
|
||||||
CommonDialogs.quickAddFeed();
|
CommonDialogs.subscribeToFeed();
|
||||||
};
|
};
|
||||||
this.hotkey_actions["feed_debug_update"] = () => {
|
this.hotkey_actions["feed_debug_update"] = () => {
|
||||||
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
|
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
|
||||||
//window.open("backend.php?op=feeds&method=update_debugger&feed_id=" + Feeds.getActive());
|
|
||||||
|
|
||||||
/* global __csrf_token */
|
/* global __csrf_token */
|
||||||
App.postOpenWindow("backend.php", {op: "feeds", method: "update_debugger",
|
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
|
||||||
feed_id: Feeds.getActive(), csrf_token: __csrf_token});
|
feed_id: Feeds.getActive(), csrf_token: __csrf_token});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -1001,8 +1045,6 @@ const App = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.hotkey_actions["feed_debug_viewfeed"] = () => {
|
this.hotkey_actions["feed_debug_viewfeed"] = () => {
|
||||||
//Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), viewfeed_debug: true});
|
|
||||||
|
|
||||||
App.postOpenWindow("backend.php", {op: "feeds", method: "view",
|
App.postOpenWindow("backend.php", {op: "feeds", method: "view",
|
||||||
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
|
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
|
||||||
};
|
};
|
||||||
@@ -1022,7 +1064,7 @@ const App = {
|
|||||||
Headlines.reverse();
|
Headlines.reverse();
|
||||||
};
|
};
|
||||||
this.hotkey_actions["feed_toggle_vgroup"] = () => {
|
this.hotkey_actions["feed_toggle_vgroup"] = () => {
|
||||||
xhrPost("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
|
xhr.post("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
|
||||||
Feeds.reloadCurrent();
|
Feeds.reloadCurrent();
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -1055,7 +1097,7 @@ const App = {
|
|||||||
this.hotkey_actions["select_article_cursor"] = () => {
|
this.hotkey_actions["select_article_cursor"] = () => {
|
||||||
const id = Article.getUnderPointer();
|
const id = Article.getUnderPointer();
|
||||||
if (id) {
|
if (id) {
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row)
|
if (row)
|
||||||
row.toggleClassName("Selected");
|
row.toggleClassName("Selected");
|
||||||
@@ -1092,12 +1134,12 @@ const App = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.hotkey_actions["help_dialog"] = () => {
|
this.hotkey_actions["help_dialog"] = () => {
|
||||||
this.helpDialog("main");
|
this.hotkeyHelp();
|
||||||
};
|
};
|
||||||
this.hotkey_actions["toggle_combined_mode"] = () => {
|
this.hotkey_actions["toggle_combined_mode"] = () => {
|
||||||
const value = this.isCombinedMode() ? "false" : "true";
|
const value = this.isCombinedMode() ? "false" : "true";
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
|
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
|
||||||
this.setInitParam("combined_display_mode",
|
this.setInitParam("combined_display_mode",
|
||||||
!this.getInitParam("combined_display_mode"));
|
!this.getInitParam("combined_display_mode"));
|
||||||
|
|
||||||
@@ -1108,7 +1150,7 @@ const App = {
|
|||||||
this.hotkey_actions["toggle_cdm_expanded"] = () => {
|
this.hotkey_actions["toggle_cdm_expanded"] = () => {
|
||||||
const value = this.getInitParam("cdm_expanded") ? "false" : "true";
|
const value = this.getInitParam("cdm_expanded") ? "false" : "true";
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
|
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
|
||||||
this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
|
this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
|
||||||
Headlines.renderAgain();
|
Headlines.renderAgain();
|
||||||
});
|
});
|
||||||
@@ -1130,7 +1172,7 @@ const App = {
|
|||||||
Feeds.search();
|
Feeds.search();
|
||||||
break;
|
break;
|
||||||
case "qmcAddFeed":
|
case "qmcAddFeed":
|
||||||
CommonDialogs.quickAddFeed();
|
CommonDialogs.subscribeToFeed();
|
||||||
break;
|
break;
|
||||||
case "qmcDigest":
|
case "qmcDigest":
|
||||||
window.location.href = "backend.php?op=digest";
|
window.location.href = "backend.php?op=digest";
|
||||||
@@ -1182,7 +1224,7 @@ const App = {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "qmcHKhelp":
|
case "qmcHKhelp":
|
||||||
this.helpDialog("main");
|
this.hotkeyHelp()
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("quickMenuGo: unknown action: " + opid);
|
console.log("quickMenuGo: unknown action: " + opid);
|
||||||
|
|||||||
182
js/Article.js
182
js/Article.js
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
/* global __, ngettext, App, Headlines, xhrPost, xhrJson, dojo, dijit, PluginHost, Notify, $$, Ajax, fox */
|
/* global __, ngettext, App, Headlines, xhr, dojo, dijit, PluginHost, Notify, fox */
|
||||||
|
|
||||||
const Article = {
|
const Article = {
|
||||||
_scroll_reset_timeout: false,
|
_scroll_reset_timeout: false,
|
||||||
@@ -36,19 +36,19 @@ const Article = {
|
|||||||
const score = prompt(__("Please enter new score for selected articles:"));
|
const score = prompt(__("Please enter new score for selected articles:"));
|
||||||
|
|
||||||
if (!isNaN(parseInt(score))) {
|
if (!isNaN(parseInt(score))) {
|
||||||
ids.each((id) => {
|
ids.forEach((id) => {
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
row.setAttribute("data-score", score);
|
row.setAttribute("data-score", score);
|
||||||
|
|
||||||
const pic = row.select(".icon-score")[0];
|
const pic = row.querySelector(".icon-score");
|
||||||
|
|
||||||
pic.innerHTML = Article.getScorePic(score);
|
pic.innerHTML = Article.getScorePic(score);
|
||||||
pic.setAttribute("title", score);
|
pic.setAttribute("title", score);
|
||||||
|
|
||||||
["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"]
|
["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"]
|
||||||
.each(function(scl) {
|
.forEach(function(scl) {
|
||||||
if (row.hasClassName(scl))
|
if (row.hasClassName(scl))
|
||||||
row.removeClassName(scl);
|
row.removeClassName(scl);
|
||||||
});
|
});
|
||||||
@@ -63,7 +63,7 @@ const Article = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setScore: function (id, pic) {
|
setScore: function (id, pic) {
|
||||||
const row = pic.up("div[id*=RROW]");
|
const row = pic.closest("div[id*=RROW]");
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
const score_old = row.getAttribute("data-score");
|
const score_old = row.getAttribute("data-score");
|
||||||
@@ -72,13 +72,13 @@ const Article = {
|
|||||||
if (!isNaN(parseInt(score))) {
|
if (!isNaN(parseInt(score))) {
|
||||||
row.setAttribute("data-score", score);
|
row.setAttribute("data-score", score);
|
||||||
|
|
||||||
const pic = row.select(".icon-score")[0];
|
const pic = row.querySelector(".icon-score");
|
||||||
|
|
||||||
pic.innerHTML = Article.getScorePic(score);
|
pic.innerHTML = Article.getScorePic(score);
|
||||||
pic.setAttribute("title", score);
|
pic.setAttribute("title", score);
|
||||||
|
|
||||||
["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"]
|
["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"]
|
||||||
.each(function(scl) {
|
.forEach(function(scl) {
|
||||||
if (row.hasClassName(scl))
|
if (row.hasClassName(scl))
|
||||||
row.removeClassName(scl);
|
row.removeClassName(scl);
|
||||||
});
|
});
|
||||||
@@ -93,18 +93,8 @@ const Article = {
|
|||||||
w.opener = null;
|
w.opener = null;
|
||||||
w.location = url;
|
w.location = url;
|
||||||
},
|
},
|
||||||
/* popupOpenArticle: function(id) {
|
|
||||||
const w = window.open("",
|
|
||||||
"ttrss_article_popup",
|
|
||||||
"height=900,width=900,resizable=yes,status=no,location=no,menubar=no,directories=no,scrollbars=yes,toolbar=no");
|
|
||||||
|
|
||||||
if (w) {
|
|
||||||
w.opener = null;
|
|
||||||
w.location = "backend.php?op=article&method=view&mode=raw&html=1&zoom=1&id=" + id + "&csrf_token=" + App.getInitParam("csrf_token");
|
|
||||||
}
|
|
||||||
}, */
|
|
||||||
cdmUnsetActive: function (event) {
|
cdmUnsetActive: function (event) {
|
||||||
const row = $("RROW-" + Article.getActive());
|
const row = App.byId(`RROW-${Article.getActive()}`);
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
row.removeClassName("active");
|
row.removeClassName("active");
|
||||||
@@ -123,11 +113,13 @@ const Article = {
|
|||||||
Article.setActive(0);
|
Article.setActive(0);
|
||||||
},
|
},
|
||||||
displayUrl: function (id) {
|
displayUrl: function (id) {
|
||||||
const query = {op: "rpc", method: "getlinktitlebyid", id: id};
|
const query = {op: "article", method: "getmetadatabyid", id: id};
|
||||||
|
|
||||||
xhrJson("backend.php", query, (reply) => {
|
xhr.json("backend.php", query, (reply) => {
|
||||||
if (reply && reply.link) {
|
if (reply && reply.link) {
|
||||||
prompt(__("Article URL:"), reply.link);
|
prompt(__("Article URL:"), reply.link);
|
||||||
|
} else {
|
||||||
|
alert(__("No URL could be displayed for this article."));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -138,6 +130,77 @@ const Article = {
|
|||||||
|
|
||||||
Headlines.toggleUnread(id, 0);
|
Headlines.toggleUnread(id, 0);
|
||||||
},
|
},
|
||||||
|
renderNote: function (id, note) {
|
||||||
|
return `<div class="article-note" data-note-for="${id}" style="display : ${note ? "" : "none"}">
|
||||||
|
${App.FormFields.icon('note')} <div onclick class='body'>${note ? App.escapeHtml(note) : ""}</div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
renderTags: function (id, tags) {
|
||||||
|
const tags_short = tags.length > 5 ? tags.slice(0, 5) : tags;
|
||||||
|
|
||||||
|
return `<span class="tags" title="${tags.join(", ")}" data-tags-for="${id}">
|
||||||
|
${tags_short.length > 0 ? tags_short.map((tag) => `
|
||||||
|
<a href="#" onclick="Feeds.open({feed: '${tag.trim()}'})" class="tag">${tag}</a>`
|
||||||
|
).join(", ") : `${__("no tags")}`}</span>`;
|
||||||
|
},
|
||||||
|
renderLabels: function(id, labels) {
|
||||||
|
return `<span class="labels" data-labels-for="${id}">${labels.map((label) => `
|
||||||
|
<span class="label" data-label-id="${label[0]}"
|
||||||
|
style="color : ${label[2]}; background-color : ${label[3]}">${App.escapeHtml(label[1])}</span>`
|
||||||
|
).join("")}</span>`;
|
||||||
|
},
|
||||||
|
renderEnclosures: function (enclosures) {
|
||||||
|
return `
|
||||||
|
${enclosures.formatted}
|
||||||
|
${enclosures.can_inline ?
|
||||||
|
`<div class='attachments-inline'>
|
||||||
|
${enclosures.entries.map((enc) => {
|
||||||
|
if (!enclosures.inline_text_only) {
|
||||||
|
if (enc.content_type && enc.content_type.indexOf("image/") != -1) {
|
||||||
|
return `<p>
|
||||||
|
<img loading="lazy"
|
||||||
|
width="${enc.width ? enc.width : ''}"
|
||||||
|
height="${enc.height ? enc.height : ''}"
|
||||||
|
src="${App.escapeHtml(enc.content_url)}"
|
||||||
|
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"/>
|
||||||
|
</p>`
|
||||||
|
} else if (enc.content_type && enc.content_type.indexOf("audio/") != -1 && App.audioCanPlay(enc.content_type)) {
|
||||||
|
return `<p class='inline-player' title="${App.escapeHtml(enc.content_url)}">
|
||||||
|
<audio preload="none" controls="controls">
|
||||||
|
<source type="${App.escapeHtml(enc.content_type)}" src="${App.escapeHtml(enc.content_url)}"/>
|
||||||
|
</audio>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return `<p>
|
||||||
|
<a target="_blank" href="${App.escapeHtml(enc.content_url)}"
|
||||||
|
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"
|
||||||
|
rel="noopener noreferrer">${App.escapeHtml(enc.content_url)}</a>
|
||||||
|
</p>`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `<p>
|
||||||
|
<a target="_blank" href="${App.escapeHtml(enc.content_url)}"
|
||||||
|
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"
|
||||||
|
rel="noopener noreferrer">${App.escapeHtml(enc.content_url)}</a>
|
||||||
|
</p>`
|
||||||
|
}
|
||||||
|
}).join("")}
|
||||||
|
</div>` : ''}
|
||||||
|
${enclosures.entries.length > 0 ?
|
||||||
|
`<div class="attachments" dojoType="fox.form.DropDownButton">
|
||||||
|
<span>${__('Attachments')}</span>
|
||||||
|
<div dojoType="dijit.Menu" style="display: none">
|
||||||
|
${enclosures.entries.map((enc) => `
|
||||||
|
<div onclick='Article.popupOpenUrl("${App.escapeHtml(enc.content_url)}")'
|
||||||
|
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}" dojoType="dijit.MenuItem">
|
||||||
|
${enc.title ? enc.title : enc.filename}
|
||||||
|
</div>
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
`
|
||||||
|
},
|
||||||
render: function (article) {
|
render: function (article) {
|
||||||
App.cleanupMemory("content-insert");
|
App.cleanupMemory("content-insert");
|
||||||
|
|
||||||
@@ -184,12 +247,14 @@ const Article = {
|
|||||||
|
|
||||||
container.innerHTML = row.getAttribute("data-content").trim();
|
container.innerHTML = row.getAttribute("data-content").trim();
|
||||||
|
|
||||||
|
dojo.parser.parse(container);
|
||||||
|
|
||||||
// blank content element might screw up onclick selection and keyboard moving
|
// blank content element might screw up onclick selection and keyboard moving
|
||||||
if (container.textContent.length == 0)
|
if (container.textContent.length == 0)
|
||||||
container.innerHTML += " ";
|
container.innerHTML += " ";
|
||||||
|
|
||||||
// in expandable mode, save content for later, so that we can pack unfocused rows back
|
// in expandable mode, save content for later, so that we can pack unfocused rows back
|
||||||
if (App.isCombinedMode() && $("main").hasClassName("expandable"))
|
if (App.isCombinedMode() && App.byId("main").hasClassName("expandable"))
|
||||||
row.setAttribute("data-content-original", row.getAttribute("data-content"));
|
row.setAttribute("data-content-original", row.getAttribute("data-content"));
|
||||||
|
|
||||||
row.removeAttribute("data-content");
|
row.removeAttribute("data-content");
|
||||||
@@ -230,16 +295,16 @@ const Article = {
|
|||||||
<div class="comments">${comments}</div>
|
<div class="comments">${comments}</div>
|
||||||
<div class="author">${hl.author}</div>
|
<div class="author">${hl.author}</div>
|
||||||
<i class="material-icons">label_outline</i>
|
<i class="material-icons">label_outline</i>
|
||||||
<span id="ATSTR-${hl.id}">${hl.tags_str}</span>
|
${Article.renderTags(hl.id, hl.tags)}
|
||||||
<a title="${__("Edit tags for this article")}" href="#"
|
<a title="${__("Edit tags for this article")}" href="#"
|
||||||
onclick="Article.editTags(${hl.id})">(+)</a>
|
onclick="Article.editTags(${hl.id})">(+)</a>
|
||||||
<div class="buttons right">${hl.buttons}</div>
|
<div class="buttons right">${hl.buttons}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="POSTNOTE-${hl.id}">${hl.note}</div>
|
${Article.renderNote(hl.id, hl.note)}
|
||||||
<div class="content" lang="${hl.lang ? hl.lang : 'en'}">
|
<div class="content" lang="${hl.lang ? hl.lang : 'en'}">
|
||||||
${hl.content}
|
${hl.content}
|
||||||
${hl.enclosures}
|
${Article.renderEnclosures(hl.enclosures)}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
@@ -252,29 +317,41 @@ const Article = {
|
|||||||
},
|
},
|
||||||
editTags: function (id) {
|
editTags: function (id) {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "editTagsDlg",
|
|
||||||
title: __("Edit article Tags"),
|
title: __("Edit article Tags"),
|
||||||
content: __("Loading, please wait..."),
|
content: `
|
||||||
|
${App.FormFields.hidden_tag("id", id.toString())}
|
||||||
|
${App.FormFields.hidden_tag("op", "article")}
|
||||||
|
${App.FormFields.hidden_tag("method", "setArticleTags")}
|
||||||
|
|
||||||
|
<header class='horizontal'>
|
||||||
|
${__("Tags for this article (separated by commas):")}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<textarea dojoType='dijit.form.SimpleTextarea' rows='4' disabled='true'
|
||||||
|
id='tags_str' name='tags_str'></textarea>
|
||||||
|
<div class='autocomplete' id='tags_choices' style='display:none'></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
|
||||||
|
${__('Save')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
|
||||||
|
${__('Cancel')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
`,
|
||||||
execute: function () {
|
execute: function () {
|
||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
Notify.progress("Saving article tags...", true);
|
Notify.progress("Saving article tags...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", this.attr('value'), (transport) => {
|
xhr.json("backend.php", this.attr('value'), (data) => {
|
||||||
try {
|
try {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
|
||||||
const data = JSON.parse(transport.responseText);
|
Headlines.onTagsUpdated(data);
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const id = data.id;
|
|
||||||
|
|
||||||
const tags = $("ATSTR-" + id);
|
|
||||||
const tooltip = dijit.byId("ATSTRTIP-" + id);
|
|
||||||
|
|
||||||
if (tags) tags.innerHTML = data.content;
|
|
||||||
if (tooltip) tooltip.attr('label', data.content_full);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
App.Error.report(e);
|
App.Error.report(e);
|
||||||
}
|
}
|
||||||
@@ -286,25 +363,26 @@ const Article = {
|
|||||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||||
dojo.disconnect(tmph);
|
dojo.disconnect(tmph);
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "article", method: "editarticletags", param: id}, (transport) => {
|
xhr.json("backend.php", {op: "article", method: "printArticleTags", id: id}, (reply) => {
|
||||||
dialog.attr('content', transport.responseText);
|
|
||||||
|
|
||||||
new Ajax.Autocompleter('tags_str', 'tags_choices',
|
dijit.getEnclosingWidget(App.byId("tags_str"))
|
||||||
|
.attr('value', reply.tags.join(", "))
|
||||||
|
.attr('disabled', false);
|
||||||
|
|
||||||
|
/* new Ajax.Autocompleter("tags_str", "tags_choices",
|
||||||
"backend.php?op=article&method=completeTags",
|
"backend.php?op=article&method=completeTags",
|
||||||
{tokens: ',', paramName: "search"});
|
{tokens: ',', paramName: "search"}); */
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
||||||
},
|
},
|
||||||
cdmMoveToId: function (id, params) {
|
cdmMoveToId: function (id, params = {}) {
|
||||||
params = params || {};
|
|
||||||
|
|
||||||
const force_to_top = params.force_to_top || false;
|
const force_to_top = params.force_to_top || false;
|
||||||
|
|
||||||
const ctr = $("headlines-frame");
|
const ctr = App.byId("headlines-frame");
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (!row || !ctr) return;
|
if (!row || !ctr) return;
|
||||||
|
|
||||||
@@ -316,12 +394,12 @@ const Article = {
|
|||||||
if (id != Article.getActive()) {
|
if (id != Article.getActive()) {
|
||||||
console.log("setActive", id, "was", Article.getActive());
|
console.log("setActive", id, "was", Article.getActive());
|
||||||
|
|
||||||
$$("div[id*=RROW][class*=active]").each((row) => {
|
App.findAll("div[id*=RROW][class*=active]").forEach((row) => {
|
||||||
row.removeClassName("active");
|
row.removeClassName("active");
|
||||||
Article.pack(row);
|
Article.pack(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
Article.unpack(row);
|
Article.unpack(row);
|
||||||
@@ -342,10 +420,10 @@ const Article = {
|
|||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
scrollByPages: function (page_offset) {
|
scrollByPages: function (page_offset) {
|
||||||
App.Scrollable.scrollByPages($("content-insert"), page_offset);
|
App.Scrollable.scrollByPages(App.byId("content-insert"), page_offset);
|
||||||
},
|
},
|
||||||
scroll: function (offset) {
|
scroll: function (offset) {
|
||||||
App.Scrollable.scroll($("content-insert"), offset);
|
App.Scrollable.scroll(App.byId("content-insert"), offset);
|
||||||
},
|
},
|
||||||
mouseIn: function (id) {
|
mouseIn: function (id) {
|
||||||
this.post_under_pointer = id;
|
this.post_under_pointer = id;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
/* eslint-disable new-cap */
|
/* eslint-disable new-cap */
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
|
|
||||||
/* global __, dojo, dijit, Notify, App, Feeds, $$, xhrPost, xhrJson, Tables, Effect, fox */
|
/* global __, dojo, dijit, Notify, App, Feeds, xhrPost, xhr, Tables, fox */
|
||||||
|
|
||||||
/* exported CommonDialogs */
|
/* exported CommonDialogs */
|
||||||
const CommonDialogs = {
|
const CommonDialogs = {
|
||||||
@@ -11,89 +11,99 @@ const CommonDialogs = {
|
|||||||
const dialog = dijit.byId("infoBox");
|
const dialog = dijit.byId("infoBox");
|
||||||
if (dialog) dialog.hide();
|
if (dialog) dialog.hide();
|
||||||
},
|
},
|
||||||
removeFeedIcon: function(id) {
|
subscribeToFeed: function() {
|
||||||
if (confirm(__("Remove stored feed icon?"))) {
|
xhr.json("backend.php",
|
||||||
Notify.progress("Removing feed icon...", true);
|
{op: "feeds", method: "subscribeToFeed"},
|
||||||
|
(reply) => {
|
||||||
const query = {op: "pref-feeds", method: "removeicon", feed_id: id};
|
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
|
||||||
Notify.info("Feed icon removed.");
|
|
||||||
|
|
||||||
if (App.isPrefs())
|
|
||||||
dijit.byId("feedTree").reload();
|
|
||||||
else
|
|
||||||
Feeds.reload();
|
|
||||||
|
|
||||||
const icon = $$(".feed-editor-icon")[0];
|
|
||||||
|
|
||||||
if (icon)
|
|
||||||
icon.src = icon.src.replace(/\?[0-9]+$/, "?" + new Date().getTime());
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
uploadFeedIcon: function() {
|
|
||||||
const file = $("icon_file");
|
|
||||||
|
|
||||||
if (file.value.length == 0) {
|
|
||||||
alert(__("Please select an image file to upload."));
|
|
||||||
} else if (confirm(__("Upload new icon for this feed?"))) {
|
|
||||||
Notify.progress("Uploading, please wait...", true);
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.open( 'POST', 'backend.php', true );
|
|
||||||
xhr.onload = function () {
|
|
||||||
switch (parseInt(this.responseText)) {
|
|
||||||
case 0:
|
|
||||||
{
|
|
||||||
Notify.info("Upload complete.");
|
|
||||||
|
|
||||||
if (App.isPrefs())
|
|
||||||
dijit.byId("feedTree").reload();
|
|
||||||
else
|
|
||||||
Feeds.reload();
|
|
||||||
|
|
||||||
const icon = $$(".feed-editor-icon")[0];
|
|
||||||
|
|
||||||
if (icon)
|
|
||||||
icon.src = icon.src.replace(/\?[0-9]+$/, "?" + new Date().getTime());
|
|
||||||
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
Notify.error("Upload failed: icon is too big.");
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
Notify.error("Upload failed.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send(new FormData($("feed_icon_upload_form")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
quickAddFeed: function() {
|
|
||||||
xhrPost("backend.php",
|
|
||||||
{op: "feeds", method: "quickAddFeed"},
|
|
||||||
(transport) => {
|
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "feedAddDlg",
|
|
||||||
title: __("Subscribe to Feed"),
|
title: __("Subscribe to Feed"),
|
||||||
content: transport.responseText,
|
content: `
|
||||||
|
<form onsubmit='return false'>
|
||||||
|
|
||||||
|
${App.FormFields.hidden_tag("op", "feeds")}
|
||||||
|
${App.FormFields.hidden_tag("method", "add")}
|
||||||
|
|
||||||
|
<div id='fadd_error_message' style='display : none' class='alert alert-danger'></div>
|
||||||
|
|
||||||
|
<div id='fadd_multiple_notify' style='display : none'>
|
||||||
|
<div class='alert alert-info'>
|
||||||
|
${__("Provided URL is a HTML page referencing multiple feeds, please select required feed from the dropdown menu below.")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<div style='float : right'><img style='display : none' id='feed_add_spinner' src='images/indicator_white.gif'></div>
|
||||||
|
<input style='font-size : 16px; width : 500px;'
|
||||||
|
placeHolder="${__("Feed or site URL")}"
|
||||||
|
dojoType='dijit.form.ValidationTextBox'
|
||||||
|
required='1' name='feed' id='feedDlg_feedUrl'>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
${App.getInitParam('enable_feed_cats') ?
|
||||||
|
`
|
||||||
|
<fieldset>
|
||||||
|
<label class='inline'>${__('Place in category:')}</label>
|
||||||
|
${reply.cat_select}
|
||||||
|
</fieldset>
|
||||||
|
` : ''}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id="feedDlg_feedsContainer" style="display : none">
|
||||||
|
<header>${__('Available feeds')}</header>
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<select id="feedDlg_feedContainerSelect"
|
||||||
|
dojoType="fox.form.Select" size="3">
|
||||||
|
<script type="dojo/method" event="onChange" args="value">
|
||||||
|
dijit.byId("feedDlg_feedUrl").attr("value", value);
|
||||||
|
</script>
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='feedDlg_loginContainer' style='display : none'>
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<input dojoType="dijit.form.TextBox" name='login'"
|
||||||
|
placeHolder="${__("Login")}"
|
||||||
|
autocomplete="new-password"
|
||||||
|
style="width : 10em;">
|
||||||
|
<input
|
||||||
|
placeHolder="${__("Password")}"
|
||||||
|
dojoType="dijit.form.TextBox" type='password'
|
||||||
|
autocomplete="new-password"
|
||||||
|
style="width : 10em;" name='pass'">
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<label class='checkbox'>
|
||||||
|
<input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox' id='feedDlg_loginCheck'
|
||||||
|
onclick='App.displayIfChecked(this, "feedDlg_loginContainer")'>
|
||||||
|
${__('This feed requires authentication.')}
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'
|
||||||
|
onclick='App.dialogOf(this).execute()'>
|
||||||
|
${__('Subscribe')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
|
||||||
|
${__('Cancel')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
show_error: function (msg) {
|
show_error: function (msg) {
|
||||||
const elem = $("fadd_error_message");
|
const elem = App.byId("fadd_error_message");
|
||||||
|
|
||||||
elem.innerHTML = msg;
|
elem.innerHTML = msg;
|
||||||
|
|
||||||
if (!Element.visible(elem))
|
Element.show(elem);
|
||||||
new Effect.Appear(elem);
|
|
||||||
|
|
||||||
},
|
},
|
||||||
execute: function () {
|
execute: function () {
|
||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
@@ -104,17 +114,12 @@ const CommonDialogs = {
|
|||||||
Element.show("feed_add_spinner");
|
Element.show("feed_add_spinner");
|
||||||
Element.hide("fadd_error_message");
|
Element.hide("fadd_error_message");
|
||||||
|
|
||||||
xhrPost("backend.php", this.attr('value'), (transport) => {
|
xhr.json("backend.php", this.attr('value'), (reply) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let reply;
|
if (!reply) {
|
||||||
|
|
||||||
try {
|
|
||||||
reply = JSON.parse(transport.responseText);
|
|
||||||
} catch (e) {
|
|
||||||
Element.hide("feed_add_spinner");
|
Element.hide("feed_add_spinner");
|
||||||
alert(__("Failed to parse output. This can indicate server timeout and/or network issues. Backend output was logged to browser console."));
|
alert(__("Failed to parse output. This can indicate server timeout and/or network issues. Backend output was logged to browser console."));
|
||||||
console.log('quickAddFeed, backend returned:' + transport.responseText);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +166,7 @@ const CommonDialogs = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Effect.Appear('feedDlg_feedsContainer', {duration: 0.5});
|
Element.show('feedDlg_feedsContainer');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
@@ -188,6 +193,9 @@ const CommonDialogs = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
showFeedsWithErrors: function() {
|
showFeedsWithErrors: function() {
|
||||||
|
|
||||||
|
xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "errorFeedsDlg",
|
id: "errorFeedsDlg",
|
||||||
title: __("Feeds with update errors"),
|
title: __("Feeds with update errors"),
|
||||||
@@ -206,7 +214,7 @@ const CommonDialogs = {
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
|
||||||
@@ -222,35 +230,66 @@ const CommonDialogs = {
|
|||||||
alert(__("No feeds selected."));
|
alert(__("No feeds selected."));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: __("Loading, please wait...")
|
content: `
|
||||||
});
|
<div dojoType="fox.Toolbar">
|
||||||
|
<div dojoType="fox.form.DropDownButton">
|
||||||
|
<span>${__('Select')}</span>
|
||||||
|
<div dojoType="dijit.Menu" style="display: none">
|
||||||
|
<div onclick="Tables.select('error-feeds-list', true)"
|
||||||
|
dojoType="dijit.MenuItem">${__('All')}</div>
|
||||||
|
<div onclick="Tables.select('error-feeds-list', false)"
|
||||||
|
dojoType="dijit.MenuItem">${__('None')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
<div class='panel panel-scrollable'>
|
||||||
dojo.disconnect(tmph);
|
<table width='100%' id='error-feeds-list'>
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (transport) => {
|
${reply.map((row) => `
|
||||||
dialog.attr('content', transport.responseText);
|
<tr data-row-id='${row.id}'>
|
||||||
})
|
<td width='5%' align='center'>
|
||||||
|
<input onclick='Tables.onRowChecked(this)' dojoType="dijit.form.CheckBox"
|
||||||
|
type="checkbox">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" title="${__("Click to edit feed")}" onclick="CommonDialogs.editFeed(${row.id})">
|
||||||
|
${App.escapeHtml(row.title)}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='text-muted small' align='right' width='50%'>
|
||||||
|
${App.escapeHtml(row.last_error)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join("")}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button style='float : left' class='alt-danger' dojoType='dijit.form.Button' onclick='App.dialogOf(this).removeSelected()'>
|
||||||
|
${__('Unsubscribe from selected feeds')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
|
||||||
|
${__('Close this window')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
})
|
||||||
},
|
},
|
||||||
addLabel: function(select, callback) {
|
addLabel: function() {
|
||||||
const caption = prompt(__("Please enter label caption:"), "");
|
const caption = prompt(__("Please enter label caption:"), "");
|
||||||
|
|
||||||
if (caption != undefined && caption.trim().length > 0) {
|
if (caption != undefined && caption.trim().length > 0) {
|
||||||
|
|
||||||
const query = {op: "pref-labels", method: "add", caption: caption.trim()};
|
const query = {op: "pref-labels", method: "add", caption: caption.trim()};
|
||||||
|
|
||||||
if (select)
|
|
||||||
Object.extend(query, {output: "select"});
|
|
||||||
|
|
||||||
Notify.progress("Loading, please wait...", true);
|
Notify.progress("Loading, please wait...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", query, (transport) => {
|
xhr.post("backend.php", query, () => {
|
||||||
if (callback) {
|
if (dijit.byId("labelTree")) {
|
||||||
callback(transport);
|
|
||||||
} else if (App.isPrefs()) {
|
|
||||||
dijit.byId("labelTree").reload();
|
dijit.byId("labelTree").reload();
|
||||||
} else {
|
} else {
|
||||||
Feeds.reload();
|
Feeds.reload();
|
||||||
@@ -267,13 +306,13 @@ const CommonDialogs = {
|
|||||||
|
|
||||||
const query = {op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id};
|
const query = {op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
if (App.isPrefs()) {
|
if (App.isPrefs()) {
|
||||||
dijit.byId("feedTree").reload();
|
dijit.byId("feedTree").reload();
|
||||||
} else {
|
} else {
|
||||||
if (feed_id == Feeds.getActive())
|
if (feed_id == Feeds.getActive())
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Feeds.open({feed: -5})
|
Feeds.openDefaultFeed();
|
||||||
},
|
},
|
||||||
100);
|
100);
|
||||||
|
|
||||||
@@ -284,28 +323,109 @@ const CommonDialogs = {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
editFeed: function (feed) {
|
editFeed: function (feed_id) {
|
||||||
if (feed <= 0)
|
if (feed_id <= 0)
|
||||||
return alert(__("You can't edit this kind of feed."));
|
return alert(__("You can't edit this kind of feed."));
|
||||||
|
|
||||||
const query = {op: "pref-feeds", method: "editfeed", id: feed};
|
const query = {op: "pref-feeds", method: "editfeed", id: feed_id};
|
||||||
|
|
||||||
console.log("editFeed", query);
|
console.log("editFeed", query);
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "feedEditDlg",
|
id: "feedEditDlg",
|
||||||
title: __("Edit Feed"),
|
title: __("Edit Feed"),
|
||||||
unsubscribeFeed: function(feed_id, title) {
|
feed_title: "",
|
||||||
if (confirm(__("Unsubscribe from %s?").replace("%s", title))) {
|
unsubscribe: function() {
|
||||||
|
if (confirm(__("Unsubscribe from %s?").replace("%s", this.feed_title))) {
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
CommonDialogs.unsubscribeFeed(feed_id);
|
CommonDialogs.unsubscribeFeed(feed_id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
uploadIcon: function(input) {
|
||||||
|
if (input.files.length != 0) {
|
||||||
|
const icon_file = input.files[0];
|
||||||
|
|
||||||
|
if (icon_file.type.indexOf("image/") == -1) {
|
||||||
|
alert(__("Please select an image file."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('icon_file', icon_file)
|
||||||
|
fd.append('feed_id', feed_id);
|
||||||
|
fd.append('op', 'pref-feeds');
|
||||||
|
fd.append('method', 'uploadIcon');
|
||||||
|
fd.append('csrf_token', App.getInitParam("csrf_token"));
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open( 'POST', 'backend.php', true );
|
||||||
|
xhr.onload = function () {
|
||||||
|
console.log(this.responseText);
|
||||||
|
|
||||||
|
// TODO: make a notice box within panel content
|
||||||
|
switch (parseInt(this.responseText)) {
|
||||||
|
case 1:
|
||||||
|
Notify.error("Upload failed: icon is too big.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
Notify.error("Upload failed.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
Notify.info("Upload complete.");
|
||||||
|
|
||||||
|
if (App.isPrefs())
|
||||||
|
dijit.byId("feedTree").reload();
|
||||||
|
else
|
||||||
|
Feeds.reload();
|
||||||
|
|
||||||
|
const icon = dialog.domNode.querySelector(".feedIcon");
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
icon.src = this.responseText;
|
||||||
|
icon.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(fd);
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeIcon: function(id) {
|
||||||
|
if (confirm(__("Remove stored feed icon?"))) {
|
||||||
|
Notify.progress("Removing feed icon...", true);
|
||||||
|
|
||||||
|
const query = {op: "pref-feeds", method: "removeicon", feed_id: id};
|
||||||
|
|
||||||
|
xhr.post("backend.php", query, () => {
|
||||||
|
Notify.info("Feed icon removed.");
|
||||||
|
|
||||||
|
if (App.isPrefs())
|
||||||
|
dijit.byId("feedTree").reload();
|
||||||
|
else
|
||||||
|
Feeds.reload();
|
||||||
|
|
||||||
|
const icon = dialog.domNode.querySelector(".feedIcon");
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
icon.src = "";
|
||||||
|
icon.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
execute: function () {
|
execute: function () {
|
||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
Notify.progress("Saving data...", true);
|
Notify.progress("Saving data...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", dialog.attr('value'), () => {
|
xhr.post("backend.php", dialog.attr('value'), () => {
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
Notify.close();
|
Notify.close();
|
||||||
|
|
||||||
@@ -315,7 +435,9 @@ const CommonDialogs = {
|
|||||||
Feeds.reload();
|
Feeds.reload();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
content: __("Loading, please wait...")
|
content: __("Loading, please wait...")
|
||||||
});
|
});
|
||||||
@@ -323,33 +445,174 @@ const CommonDialogs = {
|
|||||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||||
dojo.disconnect(tmph);
|
dojo.disconnect(tmph);
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "editfeed", id: feed}, (transport) => {
|
xhr.json("backend.php", {op: "pref-feeds", method: "editfeed", id: feed_id}, (reply) => {
|
||||||
dialog.attr('content', transport.responseText);
|
const feed = reply.feed;
|
||||||
|
|
||||||
|
// for unsub prompt
|
||||||
|
dialog.feed_title = feed.title;
|
||||||
|
|
||||||
|
// options tab
|
||||||
|
const options = {
|
||||||
|
include_in_digest: [ feed.include_in_digest, __('Include in e-mail digest') ],
|
||||||
|
always_display_enclosures: [ feed.always_display_enclosures, __('Always display image attachments') ],
|
||||||
|
hide_images: [ feed.hide_images, __('Do not embed media') ],
|
||||||
|
cache_images: [ feed.cache_images, __('Cache media') ],
|
||||||
|
mark_unread_on_update: [ feed.mark_unread_on_update, __('Mark updated articles as unread') ]
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.attr('content',
|
||||||
|
`
|
||||||
|
<form onsubmit="return false">
|
||||||
|
<div dojoType="dijit.layout.TabContainer" style="height : 450px">
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('General')}">
|
||||||
|
|
||||||
|
${App.FormFields.hidden_tag("id", feed_id)}
|
||||||
|
${App.FormFields.hidden_tag("op", "pref-feeds")}
|
||||||
|
${App.FormFields.hidden_tag("method", "editSave")}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<input dojoType='dijit.form.ValidationTextBox' required='1'
|
||||||
|
placeHolder="${__("Feed Title")}"
|
||||||
|
style='font-size : 16px; width: 500px' name='title' value="${App.escapeHtml(feed.title)}">
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label>${__('URL:')}</label>
|
||||||
|
<input dojoType='dijit.form.ValidationTextBox' required='1'
|
||||||
|
placeHolder="${__("Feed URL")}"
|
||||||
|
regExp='^(http|https)://.*' style='width : 300px'
|
||||||
|
name='feed_url' value="${App.escapeHtml(feed.feed_url)}">
|
||||||
|
|
||||||
|
${feed.last_error ?
|
||||||
|
`<i class="material-icons"
|
||||||
|
title="${App.escapeHtml(feed.last_error)}">error</i>
|
||||||
|
` : ""}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
${reply.cats.enabled ?
|
||||||
|
`
|
||||||
|
<fieldset>
|
||||||
|
<label>${__('Place in category:')}</label>
|
||||||
|
${reply.cats.select}
|
||||||
|
</fieldset>
|
||||||
|
` : ""}
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label>${__('Site URL:')}</label>
|
||||||
|
<input dojoType='dijit.form.ValidationTextBox' required='1'
|
||||||
|
placeHolder="${__("Site URL")}"
|
||||||
|
regExp='^(http|https)://.*' style='width : 300px'
|
||||||
|
name='site_url' value="${App.escapeHtml(feed.site_url)}">
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
${reply.lang.enabled ?
|
||||||
|
`
|
||||||
|
<fieldset>
|
||||||
|
<label>${__('Language:')}</label>
|
||||||
|
${App.FormFields.select_tag("feed_language",
|
||||||
|
feed.feed_language ? feed.feed_language : reply.lang.default,
|
||||||
|
reply.lang.all)}
|
||||||
|
</fieldset>
|
||||||
|
` : ""}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("Update interval:")}</label>
|
||||||
|
${App.FormFields.select_hash("update_interval", feed.update_interval, reply.intervals.update)}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__('Article purging:')}</label>
|
||||||
|
|
||||||
|
${App.FormFields.select_hash("purge_interval",
|
||||||
|
feed.purge_interval,
|
||||||
|
reply.intervals.purge,
|
||||||
|
reply.force_purge ? {disabled: 1} : {})}
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Authentication')}">
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("Login:")}</label>
|
||||||
|
<input dojoType='dijit.form.TextBox'
|
||||||
|
autocomplete='new-password'
|
||||||
|
name='auth_login' value="${App.escapeHtml(feed.auth_login)}">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("Password:")}</label>
|
||||||
|
<input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
|
||||||
|
autocomplete='new-password'
|
||||||
|
value="${App.escapeHtml(feed.auth_pass)}">
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Options')}">
|
||||||
|
<section class="narrow">
|
||||||
|
${Object.keys(options).map((name) =>
|
||||||
|
`
|
||||||
|
<fieldset class='narrow'>
|
||||||
|
<label class="checkbox">
|
||||||
|
${App.FormFields.checkbox_tag(name, options[name][0])}
|
||||||
|
${options[name][1]}
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
`).join("")}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Icon')}">
|
||||||
|
<div><img class='feedIcon' style="${feed.icon ? "" : "display : none"}" src="${feed.icon ? App.escapeHtml(feed.icon) : ""}"></div>
|
||||||
|
|
||||||
|
<label class="dijitButton">${__("Upload new icon...")}
|
||||||
|
<input style="display: none" type="file" onchange="App.dialogOf(this).uploadIcon(this)">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
${App.FormFields.submit_tag(__("Remove"), {class: "alt-danger", onclick: "App.dialogOf(this).removeIcon("+feed_id+")"})}
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Plugins')}">
|
||||||
|
${reply.plugin_data}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
${App.FormFields.button_tag(__("Unsubscribe"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).unsubscribe()"})}
|
||||||
|
${App.FormFields.submit_tag(__("Save"), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
},
|
},
|
||||||
genUrlChangeKey: function(feed, is_cat) {
|
generatedFeed: function(feed, is_cat, search = "") {
|
||||||
|
|
||||||
|
Notify.progress("Loading, please wait...", true);
|
||||||
|
|
||||||
|
xhr.json("backend.php", {op: "pref-feeds", method: "getsharedurl", id: feed, is_cat: is_cat, search: search}, (reply) => {
|
||||||
|
try {
|
||||||
|
const dialog = new fox.SingleUseDialog({
|
||||||
|
title: __("Show as feed"),
|
||||||
|
regenFeedKey: function(feed, is_cat) {
|
||||||
if (confirm(__("Generate new syndication address for this feed?"))) {
|
if (confirm(__("Generate new syndication address for this feed?"))) {
|
||||||
|
|
||||||
Notify.progress("Trying to change address...", true);
|
Notify.progress("Trying to change address...", true);
|
||||||
|
|
||||||
const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
|
const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
|
||||||
|
|
||||||
xhrJson("backend.php", query, (reply) => {
|
xhr.json("backend.php", query, (reply) => {
|
||||||
const new_link = reply.link;
|
const new_link = reply.link;
|
||||||
const e = $('gen_feed_url');
|
const target = this.domNode.querySelector(".generated_url");
|
||||||
|
|
||||||
if (new_link) {
|
if (new_link && target) {
|
||||||
e.innerHTML = e.innerHTML.replace(/&key=.*$/,
|
target.innerHTML = target.innerHTML.replace(/&key=.*$/,
|
||||||
"&key=" + new_link);
|
"&key=" + new_link);
|
||||||
|
|
||||||
e.href = e.href.replace(/&key=.*$/,
|
target.href = target.href.replace(/&key=.*$/,
|
||||||
"&key=" + new_link);
|
"&key=" + new_link);
|
||||||
|
|
||||||
new Effect.Highlight(e);
|
|
||||||
|
|
||||||
Notify.close();
|
Notify.close();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -359,66 +622,18 @@ const CommonDialogs = {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
publishedOPML: function() {
|
|
||||||
|
|
||||||
Notify.progress("Loading, please wait...", true);
|
|
||||||
|
|
||||||
xhrJson("backend.php", {op: "pref-feeds", method: "getOPMLKey"}, (reply) => {
|
|
||||||
try {
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
|
||||||
title: __("Public OPML URL"),
|
|
||||||
content: `
|
content: `
|
||||||
<header>${__("Your Public OPML URL is:")}</header>
|
<header>${__("%s can be accessed via the following secret URL:").replace("%s", App.escapeHtml(reply.title))}</header>
|
||||||
<section>
|
<section>
|
||||||
<div class='panel text-center'>
|
<div class='panel text-center'>
|
||||||
<a id='pub_opml_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${reply.link}</a>
|
<a class='generated_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${App.escapeHtml(reply.link)}</a>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class='text-center'>
|
|
||||||
<button dojoType='dijit.form.Button' onclick="return Helpers.OPML.changeKey()">
|
|
||||||
${__('Generate new URL')}
|
|
||||||
</button>
|
|
||||||
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
|
|
||||||
${__('Close this window')}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
Notify.close();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
App.Error.report(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
generatedFeed: function(feed, is_cat, rss_url, feed_title) {
|
|
||||||
|
|
||||||
Notify.progress("Loading, please wait...", true);
|
|
||||||
|
|
||||||
xhrJson("backend.php", {op: "pref-feeds", method: "getFeedKey", id: feed, is_cat: is_cat}, (reply) => {
|
|
||||||
try {
|
|
||||||
if (!feed_title && typeof Feeds != "undefined")
|
|
||||||
feed_title = Feeds.getName(feed, is_cat);
|
|
||||||
|
|
||||||
const secret_url = rss_url + "&key=" + encodeURIComponent(reply.link);
|
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
|
||||||
title: __("Show as feed"),
|
|
||||||
content: `
|
|
||||||
<header>${__("%s can be accessed via the following secret URL:").replace("%s", feed_title)}</header>
|
|
||||||
<section>
|
|
||||||
<div class='panel text-center'>
|
|
||||||
<a id='gen_feed_url' href="${App.escapeHtml(secret_url)}" target='_blank'>${secret_url}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer>
|
<footer>
|
||||||
<button dojoType='dijit.form.Button' style='float : left' class='alt-info'
|
<button dojoType='dijit.form.Button' style='float : left' class='alt-info'
|
||||||
onclick='window.open("https://tt-rss.org/wiki/GeneratedFeeds")'>
|
onclick='window.open("https://tt-rss.org/wiki/GeneratedFeeds")'>
|
||||||
<i class='material-icons'>help</i> ${__("More info...")}</button>
|
<i class='material-icons'>help</i> ${__("More info...")}</button>
|
||||||
<button dojoType='dijit.form.Button' onclick="return CommonDialogs.genUrlChangeKey('${feed}', '${is_cat}')">
|
<button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenFeedKey('${feed}', '${is_cat}')">
|
||||||
${__('Generate new URL')}
|
${__('Generate new URL')}
|
||||||
</button>
|
</button>
|
||||||
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
|
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
|
||||||
|
|||||||
@@ -2,142 +2,24 @@
|
|||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
|
|
||||||
/* global __, App, Article, Lists, Effect, fox */
|
/* global __, App, Article, Lists, fox */
|
||||||
/* global xhrPost, dojo, dijit, Notify, $$, Feeds */
|
/* global xhr, dojo, dijit, Notify, Feeds */
|
||||||
|
|
||||||
|
/* exported Filters */
|
||||||
const Filters = {
|
const Filters = {
|
||||||
filterDlgCheckAction: function(sender) {
|
edit: function(filter_id = null) { // if no id, new filter dialog
|
||||||
const action = sender.value;
|
|
||||||
|
|
||||||
const action_param = $("filterDlg_paramBox");
|
|
||||||
|
|
||||||
if (!action_param) {
|
|
||||||
console.log("filterDlgCheckAction: can't find action param box!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if selected action supports parameters, enable params field
|
|
||||||
if (action == 4 || action == 6 || action == 7 || action == 9) {
|
|
||||||
new Effect.Appear(action_param, {duration: 0.5});
|
|
||||||
|
|
||||||
Element.hide(dijit.byId("filterDlg_actionParam").domNode);
|
|
||||||
Element.hide(dijit.byId("filterDlg_actionParamLabel").domNode);
|
|
||||||
Element.hide(dijit.byId("filterDlg_actionParamPlugin").domNode);
|
|
||||||
|
|
||||||
if (action == 7) {
|
|
||||||
Element.show(dijit.byId("filterDlg_actionParamLabel").domNode);
|
|
||||||
} else if (action == 9) {
|
|
||||||
Element.show(dijit.byId("filterDlg_actionParamPlugin").domNode);
|
|
||||||
} else {
|
|
||||||
Element.show(dijit.byId("filterDlg_actionParam").domNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Element.hide(action_param);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createNewRuleElement: function(parentNode, replaceNode) {
|
|
||||||
const rule = dojo.formToJson("filter_new_rule_form");
|
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (transport) => {
|
|
||||||
try {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
|
|
||||||
li.innerHTML = `<input dojoType='dijit.form.CheckBox' type='checkbox' onclick='Lists.onRowChecked(this)'>
|
|
||||||
<span onclick='App.dialogOf(this).editRule(this)'>${transport.responseText}</span>
|
|
||||||
${App.FormFields.hidden("rule[]", rule)}`;
|
|
||||||
|
|
||||||
dojo.parser.parse(li);
|
|
||||||
|
|
||||||
if (replaceNode) {
|
|
||||||
parentNode.replaceChild(li, replaceNode);
|
|
||||||
} else {
|
|
||||||
parentNode.appendChild(li);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
App.Error.report(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createNewActionElement: function(parentNode, replaceNode) {
|
|
||||||
const form = document.forms["filter_new_action_form"];
|
|
||||||
|
|
||||||
if (form.action_id.value == 7) {
|
|
||||||
form.action_param.value = form.action_param_label.value;
|
|
||||||
} else if (form.action_id.value == 9) {
|
|
||||||
form.action_param.value = form.action_param_plugin.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = dojo.formToJson(form);
|
|
||||||
|
|
||||||
xhrPost("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (transport) => {
|
|
||||||
try {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
|
|
||||||
li.innerHTML = `<input dojoType='dijit.form.CheckBox' type='checkbox' onclick='Lists.onRowChecked(this)'>
|
|
||||||
<span onclick='App.dialogOf(this).editAction(this)'>${transport.responseText}</span>
|
|
||||||
${App.FormFields.hidden("action[]", action)}`;
|
|
||||||
|
|
||||||
dojo.parser.parse(li);
|
|
||||||
|
|
||||||
if (replaceNode) {
|
|
||||||
parentNode.replaceChild(li, replaceNode);
|
|
||||||
} else {
|
|
||||||
parentNode.appendChild(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
App.Error.report(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addFilterRule: function(replaceNode, ruleStr) {
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
|
||||||
id: "filterNewRuleDlg",
|
|
||||||
title: ruleStr ? __("Edit rule") : __("Add rule"),
|
|
||||||
execute: function () {
|
|
||||||
if (this.validate()) {
|
|
||||||
Filters.createNewRuleElement($("filterDlg_Matches"), replaceNode);
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
content: __('Loading, please wait...'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tmph = dojo.connect(dialog, "onShow", null, function (/* e */) {
|
|
||||||
dojo.disconnect(tmph);
|
|
||||||
|
|
||||||
xhrPost("backend.php", {op: 'pref-filters', method: 'newrule', rule: ruleStr}, (transport) => {
|
|
||||||
dialog.attr('content', transport.responseText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
},
|
|
||||||
addFilterAction: function(replaceNode, actionStr) {
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
|
||||||
title: actionStr ? __("Edit action") : __("Add action"),
|
|
||||||
execute: function () {
|
|
||||||
if (this.validate()) {
|
|
||||||
Filters.createNewActionElement($("filterDlg_Actions"), replaceNode);
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tmph = dojo.connect(dialog, "onShow", null, function (/* e */) {
|
|
||||||
dojo.disconnect(tmph);
|
|
||||||
|
|
||||||
xhrPost("backend.php", {op: 'pref-filters', method: 'newaction', action: actionStr}, (transport) => {
|
|
||||||
dialog.attr('content', transport.responseText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
},
|
|
||||||
test: function(params) {
|
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
|
id: "filterEditDlg",
|
||||||
|
title: filter_id ? __("Edit Filter") : __("Create Filter"),
|
||||||
|
ACTION_TAG: 4,
|
||||||
|
ACTION_SCORE: 6,
|
||||||
|
ACTION_LABEL: 7,
|
||||||
|
ACTION_PLUGIN: 9,
|
||||||
|
PARAM_ACTIONS: [4, 6, 7, 9],
|
||||||
|
filter_info: {},
|
||||||
|
test: function() {
|
||||||
|
const test_dialog = new fox.SingleUseDialog({
|
||||||
title: "Test Filter",
|
title: "Test Filter",
|
||||||
results: 0,
|
results: 0,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
@@ -145,36 +27,34 @@ const Filters = {
|
|||||||
getTestResults: function (params, offset) {
|
getTestResults: function (params, offset) {
|
||||||
params.method = 'testFilterDo';
|
params.method = 'testFilterDo';
|
||||||
params.offset = offset;
|
params.offset = offset;
|
||||||
params.limit = dialog.limit;
|
params.limit = test_dialog.limit;
|
||||||
|
|
||||||
console.log("getTestResults:" + offset);
|
console.log("getTestResults:" + offset);
|
||||||
|
|
||||||
xhrPost("backend.php", params, (transport) => {
|
xhr.json("backend.php", params, (result) => {
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(transport.responseText);
|
if (result && test_dialog && test_dialog.open) {
|
||||||
|
test_dialog.results += result.length;
|
||||||
if (result && dialog && dialog.open) {
|
|
||||||
dialog.results += result.length;
|
|
||||||
|
|
||||||
console.log("got results:" + result.length);
|
console.log("got results:" + result.length);
|
||||||
|
|
||||||
$("prefFilterProgressMsg").innerHTML = __("Looking for articles (%d processed, %f found)...")
|
App.byId("prefFilterProgressMsg").innerHTML = __("Looking for articles (%d processed, %f found)...")
|
||||||
.replace("%f", dialog.results)
|
.replace("%f", test_dialog.results)
|
||||||
.replace("%d", offset);
|
.replace("%d", offset);
|
||||||
|
|
||||||
console.log(offset + " " + dialog.max_offset);
|
console.log(offset + " " + test_dialog.max_offset);
|
||||||
|
|
||||||
for (let i = 0; i < result.length; i++) {
|
for (let i = 0; i < result.length; i++) {
|
||||||
const tmp = dojo.create("table", { innerHTML: result[i]});
|
const tmp = dojo.create("table", { innerHTML: result[i]});
|
||||||
|
|
||||||
$("prefFilterTestResultList").innerHTML += tmp.innerHTML;
|
App.byId("prefFilterTestResultList").innerHTML += tmp.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialog.results < 30 && offset < dialog.max_offset) {
|
if (test_dialog.results < 30 && offset < test_dialog.max_offset) {
|
||||||
|
|
||||||
// get the next batch
|
// get the next batch
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
dialog.getTestResults(params, offset + dialog.limit);
|
test_dialog.getTestResults(params, offset + test_dialog.limit);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -182,13 +62,13 @@ const Filters = {
|
|||||||
|
|
||||||
Element.hide("prefFilterLoadingIndicator");
|
Element.hide("prefFilterLoadingIndicator");
|
||||||
|
|
||||||
if (dialog.results == 0) {
|
if (test_dialog.results == 0) {
|
||||||
$("prefFilterTestResultList").innerHTML = `<tr><td align='center'>
|
App.byId("prefFilterTestResultList").innerHTML = `<tr><td align='center'>
|
||||||
${__('No recent articles matching this filter have been found.')}</td></tr>`;
|
${__('No recent articles matching this filter have been found.')}</td></tr>`;
|
||||||
$("prefFilterProgressMsg").innerHTML = "Articles matching this filter:";
|
App.byId("prefFilterProgressMsg").innerHTML = "Articles matching this filter:";
|
||||||
} else {
|
} else {
|
||||||
$("prefFilterProgressMsg").innerHTML = __("Found %d articles matching this filter:")
|
App.byId("prefFilterProgressMsg").innerHTML = __("Found %d articles matching this filter:")
|
||||||
.replace("%d", dialog.results);
|
.replace("%d", test_dialog.results);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -219,33 +99,238 @@ const Filters = {
|
|||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
dojo.connect(dialog, "onShow", null, function (/* e */) {
|
const tmph = dojo.connect(test_dialog, "onShow", null, function (/* e */) {
|
||||||
dialog.getTestResults(params, 0);
|
dojo.disconnect(tmph);
|
||||||
|
|
||||||
|
test_dialog.getTestResults(dialog.attr('value'), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
test_dialog.show();
|
||||||
},
|
},
|
||||||
edit: function(id) { // if no id, new filter dialog
|
insertRule: function(parentNode, replaceNode) {
|
||||||
let query;
|
const rule = dojo.formToJson("filter_new_rule_form");
|
||||||
|
|
||||||
if (!App.isPrefs()) {
|
xhr.post("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (reply) => {
|
||||||
query = {
|
try {
|
||||||
op: "pref-filters", method: "edit",
|
const li = document.createElement('li');
|
||||||
feed: Feeds.getActive(), is_cat: Feeds.activeIsCat()
|
li.addClassName("rule");
|
||||||
};
|
|
||||||
|
li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})}
|
||||||
|
<span class="name" onclick='App.dialogOf(this).onRuleClicked(this)'>${reply}</span>
|
||||||
|
<span class="payload" >${App.FormFields.hidden_tag("rule[]", rule)}</span>`;
|
||||||
|
|
||||||
|
dojo.parser.parse(li);
|
||||||
|
|
||||||
|
if (replaceNode) {
|
||||||
|
parentNode.replaceChild(li, replaceNode);
|
||||||
} else {
|
} else {
|
||||||
query = {op: "pref-filters", method: "edit", id: id};
|
parentNode.appendChild(li);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
App.Error.report(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
insertAction: function(parentNode, replaceNode) {
|
||||||
|
const form = document.forms["filter_new_action_form"];
|
||||||
|
|
||||||
|
if (form.action_id.value == 7) {
|
||||||
|
form.action_param.value = form.action_param_label.value;
|
||||||
|
} else if (form.action_id.value == 9) {
|
||||||
|
form.action_param.value = form.action_param_plugin.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Filters.edit', query);
|
const action = dojo.formToJson(form);
|
||||||
|
|
||||||
xhrPost("backend.php", query, function (transport) {
|
xhr.post("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (reply) => {
|
||||||
try {
|
try {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const li = document.createElement('li');
|
||||||
id: "filterEditDlg",
|
li.addClassName("action");
|
||||||
title: __("Create Filter"),
|
|
||||||
test: function () {
|
li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})}
|
||||||
Filters.test(this.attr('value'));
|
<span class="name" onclick='App.dialogOf(this).onActionClicked(this)'>${reply}</span>
|
||||||
|
<span class="payload">${App.FormFields.hidden_tag("action[]", action)}</span>`;
|
||||||
|
|
||||||
|
dojo.parser.parse(li);
|
||||||
|
|
||||||
|
if (replaceNode) {
|
||||||
|
parentNode.replaceChild(li, replaceNode);
|
||||||
|
} else {
|
||||||
|
parentNode.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
App.Error.report(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
editRule: function(replaceNode, ruleStr = null) {
|
||||||
|
const edit_rule_dialog = new fox.SingleUseDialog({
|
||||||
|
id: "filterNewRuleDlg",
|
||||||
|
title: ruleStr ? __("Edit rule") : __("Add rule"),
|
||||||
|
execute: function () {
|
||||||
|
if (this.validate()) {
|
||||||
|
dialog.insertRule(App.byId("filterDlg_Matches"), replaceNode);
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: __('Loading, please wait...'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmph = dojo.connect(edit_rule_dialog, "onShow", null, function () {
|
||||||
|
dojo.disconnect(tmph);
|
||||||
|
|
||||||
|
let rule;
|
||||||
|
|
||||||
|
if (ruleStr) {
|
||||||
|
rule = JSON.parse(ruleStr);
|
||||||
|
} else {
|
||||||
|
rule = {
|
||||||
|
reg_exp: "",
|
||||||
|
filter_type: 1,
|
||||||
|
feed_id: ["0"],
|
||||||
|
inverse: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(rule, dialog.filter_info);
|
||||||
|
|
||||||
|
xhr.json("backend.php", {op: "pref-filters", method: "editrule", ids: rule.feed_id.join(",")}, function (editrule) {
|
||||||
|
edit_rule_dialog.attr('content',
|
||||||
|
`
|
||||||
|
<form name="filter_new_rule_form" id="filter_new_rule_form" onsubmit="return false">
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<textarea dojoType="fox.form.ValidationTextArea"
|
||||||
|
required="true" id="filterDlg_regExp" ValidRegExp="true"
|
||||||
|
rows="4" style="font-size : 14px; width : 530px; word-break: break-all"
|
||||||
|
name="reg_exp">${rule.reg_exp}</textarea>
|
||||||
|
|
||||||
|
<div dojoType="dijit.Tooltip" id="filterDlg_regExp_tip" connectId="filterDlg_regExp" position="below"></div>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label class="checkbox">
|
||||||
|
${App.FormFields.checkbox_tag("inverse", rule.inverse)}
|
||||||
|
${__("Inverse regular expression matching")}
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label style="display : inline">${__("on")}</label>
|
||||||
|
${App.FormFields.select_hash("filter_type", rule.filter_type, dialog.filter_info.filter_types)}
|
||||||
|
<label style="padding-left : 10px; display : inline">${__("in")}</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<span id="filterDlg_feeds">
|
||||||
|
${editrule.multiselect}
|
||||||
|
</span>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("More info"), "", {class: 'pull-left alt-info',
|
||||||
|
onclick: "window.open('https://tt-rss.org/wiki/ContentFilters')"})}
|
||||||
|
${App.FormFields.submit_tag(__("Save rule"), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
edit_rule_dialog.show();
|
||||||
|
},
|
||||||
|
editAction: function(replaceNode, actionStr) {
|
||||||
|
const edit_action_dialog = new fox.SingleUseDialog({
|
||||||
|
title: actionStr ? __("Edit action") : __("Add action"),
|
||||||
|
select_labels: function(name, value, labels, attributes = {}, id = "") {
|
||||||
|
const values = Object.values(labels).map((label) => label.caption);
|
||||||
|
return App.FormFields.select_tag(name, value, values, attributes, id);
|
||||||
|
},
|
||||||
|
toggleParam: function(sender) {
|
||||||
|
const action = parseInt(sender.value);
|
||||||
|
|
||||||
|
dijit.byId("filterDlg_actionParam").domNode.hide();
|
||||||
|
dijit.byId("filterDlg_actionParamLabel").domNode.hide();
|
||||||
|
dijit.byId("filterDlg_actionParamPlugin").domNode.hide();
|
||||||
|
|
||||||
|
// if selected action supports parameters, enable params field
|
||||||
|
if (action == dialog.ACTION_LABEL) {
|
||||||
|
dijit.byId("filterDlg_actionParamLabel").domNode.show();
|
||||||
|
} else if (action == dialog.ACTION_PLUGIN) {
|
||||||
|
dijit.byId("filterDlg_actionParamPlugin").domNode.show();
|
||||||
|
} else if (dialog.PARAM_ACTIONS.indexOf(action) != -1) {
|
||||||
|
dijit.byId("filterDlg_actionParam").domNode.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
execute: function () {
|
||||||
|
if (this.validate()) {
|
||||||
|
dialog.insertAction(App.byId("filterDlg_Actions"), replaceNode);
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: __("Loading, please wait...")
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmph = dojo.connect(edit_action_dialog, "onShow", null, function () {
|
||||||
|
dojo.disconnect(tmph);
|
||||||
|
|
||||||
|
let action;
|
||||||
|
|
||||||
|
if (actionStr) {
|
||||||
|
action = JSON.parse(actionStr);
|
||||||
|
} else {
|
||||||
|
action = {
|
||||||
|
action_id: 2,
|
||||||
|
action_param: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(action);
|
||||||
|
|
||||||
|
edit_action_dialog.attr('content',
|
||||||
|
`
|
||||||
|
<form name="filter_new_action_form" id="filter_new_action_form" onsubmit="return false;">
|
||||||
|
<section>
|
||||||
|
${App.FormFields.select_hash("action_id", -1,
|
||||||
|
dialog.filter_info.action_types,
|
||||||
|
{onchange: "App.dialogOf(this).toggleParam(this)"},
|
||||||
|
"filterDlg_actionSelect")}
|
||||||
|
|
||||||
|
<input dojoType="dijit.form.TextBox"
|
||||||
|
id="filterDlg_actionParam" style="$param_hidden"
|
||||||
|
name="action_param" value="${App.escapeHtml(action.action_param)}">
|
||||||
|
|
||||||
|
${edit_action_dialog.select_labels("action_param_label", action.action_param,
|
||||||
|
dialog.filter_info.labels,
|
||||||
|
{},
|
||||||
|
"filterDlg_actionParamLabel")}
|
||||||
|
|
||||||
|
${App.FormFields.select_hash("action_param_plugin", action.action_param,
|
||||||
|
dialog.filter_info.plugin_actions,
|
||||||
|
{},
|
||||||
|
"filterDlg_actionParamPlugin")}
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
${App.FormFields.submit_tag(__("Save action"), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
dijit.byId("filterDlg_actionSelect").attr('value', action.action_id);
|
||||||
|
|
||||||
|
/*xhr.post("backend.php", {op: 'pref-filters', method: 'newaction', action: actionStr}, (reply) => {
|
||||||
|
edit_action_dialog.attr('content', reply);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
edit_action_dialog.hideOrShowActionParam(dijit.byId("filterDlg_actionSelect").attr('value'));
|
||||||
|
}, 250);
|
||||||
|
});*/
|
||||||
|
});
|
||||||
|
|
||||||
|
edit_action_dialog.show();
|
||||||
},
|
},
|
||||||
selectRules: function (select) {
|
selectRules: function (select) {
|
||||||
Lists.select("filterDlg_Matches", select);
|
Lists.select("filterDlg_Matches", select);
|
||||||
@@ -253,17 +338,19 @@ const Filters = {
|
|||||||
selectActions: function (select) {
|
selectActions: function (select) {
|
||||||
Lists.select("filterDlg_Actions", select);
|
Lists.select("filterDlg_Actions", select);
|
||||||
},
|
},
|
||||||
editRule: function (e) {
|
onRuleClicked: function (elem) {
|
||||||
const li = e.closest('li');
|
|
||||||
const rule = li.querySelector('input[name="rule[]"]').value
|
|
||||||
|
|
||||||
Filters.addFilterRule(li, rule);
|
const li = elem.closest('li');
|
||||||
|
const rule = li.querySelector('input[name="rule[]"]').value;
|
||||||
|
|
||||||
|
this.editRule(li, rule);
|
||||||
},
|
},
|
||||||
editAction: function (e) {
|
onActionClicked: function (elem) {
|
||||||
const li = e.closest('li');
|
|
||||||
const action = li.querySelector('input[name="action[]"]').value
|
|
||||||
|
|
||||||
Filters.addFilterAction(li, action);
|
const li = elem.closest('li');
|
||||||
|
const action = li.querySelector('input[name="action[]"]').value;
|
||||||
|
|
||||||
|
this.editAction(li, action);
|
||||||
},
|
},
|
||||||
removeFilter: function () {
|
removeFilter: function () {
|
||||||
const msg = __("Remove filter?");
|
const msg = __("Remove filter?");
|
||||||
@@ -275,7 +362,7 @@ const Filters = {
|
|||||||
|
|
||||||
const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id};
|
const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
const tree = dijit.byId("filterTree");
|
const tree = dijit.byId("filterTree");
|
||||||
|
|
||||||
if (tree) tree.reload();
|
if (tree) tree.reload();
|
||||||
@@ -283,18 +370,18 @@ const Filters = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
addAction: function () {
|
addAction: function () {
|
||||||
Filters.addFilterAction();
|
this.editAction();
|
||||||
},
|
},
|
||||||
addRule: function () {
|
addRule: function () {
|
||||||
Filters.addFilterRule();
|
this.editRule();
|
||||||
},
|
},
|
||||||
deleteAction: function () {
|
deleteAction: function () {
|
||||||
$$("#filterDlg_Actions li[class*=Selected]").each(function (e) {
|
App.findAll("#filterDlg_Actions li[class*=Selected]").forEach(function (e) {
|
||||||
e.parentNode.removeChild(e)
|
e.parentNode.removeChild(e)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteRule: function () {
|
deleteRule: function () {
|
||||||
$$("#filterDlg_Matches li[class*=Selected]").each(function (e) {
|
App.findAll("#filterDlg_Matches li[class*=Selected]").forEach(function (e) {
|
||||||
e.parentNode.removeChild(e)
|
e.parentNode.removeChild(e)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -303,7 +390,7 @@ const Filters = {
|
|||||||
|
|
||||||
Notify.progress("Saving data...", true);
|
Notify.progress("Saving data...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", this.attr('value'), () => {
|
xhr.post("backend.php", this.attr('value'), () => {
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
|
||||||
const tree = dijit.byId("filterTree");
|
const tree = dijit.byId("filterTree");
|
||||||
@@ -311,56 +398,164 @@ const Filters = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: transport.responseText
|
content: __("Loading, please wait...")
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!App.isPrefs()) {
|
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||||
/* global getSelectionText */
|
dojo.disconnect(tmph);
|
||||||
const selectedText = getSelectionText();
|
|
||||||
|
|
||||||
const lh = dojo.connect(dialog, "onShow", function () {
|
xhr.json("backend.php", {op: "pref-filters", method: "edit", id: filter_id}, function (filter) {
|
||||||
dojo.disconnect(lh);
|
|
||||||
|
dialog.filter_info = filter;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
enabled: [ filter.enabled, __('Enabled') ],
|
||||||
|
match_any_rule: [ filter.match_any_rule, __('Match any rule') ],
|
||||||
|
inverse: [ filter.inverse, __('Inverse matching') ],
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.attr('content',
|
||||||
|
`
|
||||||
|
<form onsubmit='return false'>
|
||||||
|
|
||||||
|
${App.FormFields.hidden_tag("op", "pref-filters")}
|
||||||
|
${App.FormFields.hidden_tag("id", filter_id)}
|
||||||
|
${App.FormFields.hidden_tag("method", filter_id ? "editSave" : "add")}
|
||||||
|
${App.FormFields.hidden_tag("csrf_token", App.getInitParam('csrf_token'))}
|
||||||
|
|
||||||
|
<section class="horizontal">
|
||||||
|
<input required="true" dojoType="dijit.form.ValidationTextBox" style="width : 100%"
|
||||||
|
placeholder="${__("Title")}" name="title" value="${App.escapeHtml(filter.title)}">
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div dojoType="dijit.layout.TabContainer" style="height : 300px">
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Match')}">
|
||||||
|
<div style="padding : 0" dojoType="dijit.layout.BorderContainer" gutters="false">
|
||||||
|
<div dojoType="fox.Toolbar" region="top">
|
||||||
|
<div dojoType="fox.form.DropDownButton">
|
||||||
|
<span>${__("Select")}</span>
|
||||||
|
<div dojoType="dijit.Menu" style="display: none;">
|
||||||
|
<!-- can"t use App.dialogOf() here because DropDownButton is not a child of the Dialog -->
|
||||||
|
<div onclick="dijit.byId('filterEditDlg').selectRules(true)"
|
||||||
|
dojoType="dijit.MenuItem">${__("All")}</div>
|
||||||
|
<div onclick="dijit.byId('filterEditDlg').selectRules(false)"
|
||||||
|
dojoType="dijit.MenuItem">${__("None")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button dojoType="dijit.form.Button" onclick="App.dialogOf(this).addRule()">
|
||||||
|
${__("Add")}
|
||||||
|
</button>
|
||||||
|
<button dojoType="dijit.form.Button" onclick="App.dialogOf(this).deleteRule()">
|
||||||
|
${__("Delete")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" region="center">
|
||||||
|
<ul id="filterDlg_Matches">
|
||||||
|
${filter.rules.map((rule) => `
|
||||||
|
<li class='rule'>
|
||||||
|
${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})}
|
||||||
|
<span class='name' onclick='App.dialogOf(this).onRuleClicked(this)'>${rule.name}</span>
|
||||||
|
<span class='payload'>${App.FormFields.hidden_tag("rule[]", JSON.stringify(rule))}</span>
|
||||||
|
</li>
|
||||||
|
`).join("")}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Apply actions')}">
|
||||||
|
<div style="padding : 0" dojoType="dijit.layout.BorderContainer" gutters="false">
|
||||||
|
<div dojoType="fox.Toolbar" region="top">
|
||||||
|
<div dojoType="fox.form.DropDownButton">
|
||||||
|
<span>${__("Select")}</span>
|
||||||
|
<div dojoType="dijit.Menu" style="display: none">
|
||||||
|
<div onclick="dijit.byId('filterEditDlg').selectActions(true)"
|
||||||
|
dojoType="dijit.MenuItem">${__("All")}</div>
|
||||||
|
<div onclick="dijit.byId('filterEditDlg').selectActions(false)"
|
||||||
|
dojoType="dijit.MenuItem">${__("None")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button dojoType="dijit.form.Button" onclick="App.dialogOf(this).addAction()">
|
||||||
|
${__("Add")}
|
||||||
|
</button>
|
||||||
|
<button dojoType="dijit.form.Button" onclick="App.dialogOf(this).deleteAction()">
|
||||||
|
${__("Delete")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" region="center">
|
||||||
|
<ul id="filterDlg_Actions">
|
||||||
|
${filter.actions.map((action) => `
|
||||||
|
<li class='rule'>
|
||||||
|
${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})}
|
||||||
|
<span class='name' onclick='App.dialogOf(this).onActionClicked(this)'>${App.escapeHtml(action.name)}</span>
|
||||||
|
<span class='payload'>${App.FormFields.hidden_tag("action[]", JSON.stringify(action))}</span>
|
||||||
|
</li>
|
||||||
|
`).join("")}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="horizontal">
|
||||||
|
${Object.keys(options).map((name) =>
|
||||||
|
`
|
||||||
|
<fieldset class='narrow'>
|
||||||
|
<label class="checkbox">
|
||||||
|
${App.FormFields.checkbox_tag(name, options[name][0])}
|
||||||
|
${options[name][1]}
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
`).join("")}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
${filter_id ?
|
||||||
|
`
|
||||||
|
${App.FormFields.button_tag(__("Remove"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).removeFilter()"})}
|
||||||
|
${App.FormFields.button_tag(__("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
|
||||||
|
${App.FormFields.submit_tag(__("Save"), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
||||||
|
` : `
|
||||||
|
${App.FormFields.button_tag(__("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
|
||||||
|
${App.FormFields.submit_tag(__("Create"), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
||||||
|
`}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!App.isPrefs()) {
|
||||||
|
const selectedText = App.getSelectedText();
|
||||||
|
|
||||||
if (selectedText != "") {
|
if (selectedText != "") {
|
||||||
|
|
||||||
const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
|
const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
|
||||||
Feeds.getActive();
|
Feeds.getActive();
|
||||||
|
|
||||||
const rule = {reg_exp: selectedText, feed_id: [feed_id], filter_type: 1};
|
const rule = {reg_exp: selectedText, feed_id: [feed_id], filter_type: 1};
|
||||||
|
|
||||||
Filters.addFilterRule(null, dojo.toJson(rule));
|
dialog.editRule(null, dojo.toJson(rule));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
const query = {op: "article", method: "getmetadatabyid", id: Article.getActive()};
|
||||||
|
|
||||||
const query = {op: "rpc", method: "getlinktitlebyid", id: Article.getActive()};
|
xhr.json("backend.php", query, (reply) => {
|
||||||
|
let title;
|
||||||
xhrPost("backend.php", query, (transport) => {
|
|
||||||
const reply = JSON.parse(transport.responseText);
|
|
||||||
|
|
||||||
let title = false;
|
|
||||||
|
|
||||||
if (reply && reply.title) title = reply.title;
|
if (reply && reply.title) title = reply.title;
|
||||||
|
|
||||||
if (title || Feeds.getActive() || Feeds.activeIsCat()) {
|
if (title || Feeds.getActive() || Feeds.activeIsCat()) {
|
||||||
|
|
||||||
console.log(title + " " + Feeds.getActive());
|
console.log(title + " " + Feeds.getActive());
|
||||||
|
|
||||||
const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
|
const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
|
||||||
Feeds.getActive();
|
Feeds.getActive();
|
||||||
|
|
||||||
const rule = {reg_exp: title, feed_id: [feed_id], filter_type: 1};
|
const rule = {reg_exp: title, feed_id: [feed_id], filter_type: 1};
|
||||||
|
|
||||||
Filters.addFilterRule(null, dojo.toJson(rule));
|
dialog.editRule(null, dojo.toJson(rule));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
dialog.show();
|
});
|
||||||
|
});
|
||||||
|
|
||||||
} catch (e) {
|
dialog.show();
|
||||||
App.Error.report(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
label: __("Debug feed"),
|
label: __("Debug feed"),
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
/* global __csrf_token */
|
/* global __csrf_token */
|
||||||
App.postOpenWindow("backend.php", {op: "feeds", method: "update_debugger",
|
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
|
||||||
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
|
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
|
||||||
}}));
|
}}));
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
|
|
||||||
// focus headlines to route key events there
|
// focus headlines to route key events there
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$("headlines-frame").focus();
|
App.byId("headlines-frame").focus();
|
||||||
|
|
||||||
if (treeNode) {
|
if (treeNode) {
|
||||||
const node = treeNode.rowNode;
|
const node = treeNode.rowNode;
|
||||||
@@ -295,7 +295,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
if (node && tree) {
|
if (node && tree) {
|
||||||
// scroll tree to selection if needed
|
// scroll tree to selection if needed
|
||||||
if (node.offsetTop < tree.scrollTop || node.offsetTop > tree.scrollTop + tree.clientHeight) {
|
if (node.offsetTop < tree.scrollTop || node.offsetTop > tree.scrollTop + tree.clientHeight) {
|
||||||
$("feedTree").scrollTop = node.offsetTop;
|
App.byId("feedTree").scrollTop = node.offsetTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
150
js/Feeds.js
150
js/Feeds.js
@@ -1,8 +1,9 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/* global __, App, Headlines, xhrPost, dojo, dijit, Form, fox, PluginHost, Notify, $$, fox */
|
/* global __, App, Headlines, xhr, dojo, dijit, fox, PluginHost, Notify, fox */
|
||||||
|
|
||||||
const Feeds = {
|
const Feeds = {
|
||||||
|
_default_feed_id: -3,
|
||||||
counters_last_request: 0,
|
counters_last_request: 0,
|
||||||
_active_feed_id: undefined,
|
_active_feed_id: undefined,
|
||||||
_active_feed_is_cat: false,
|
_active_feed_is_cat: false,
|
||||||
@@ -12,6 +13,19 @@ const Feeds = {
|
|||||||
_search_query: false,
|
_search_query: false,
|
||||||
last_search_query: [],
|
last_search_query: [],
|
||||||
_viewfeed_wait_timeout: false,
|
_viewfeed_wait_timeout: false,
|
||||||
|
_feeds_holder_observer: new IntersectionObserver(
|
||||||
|
(entries/*, observer*/) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
//console.log('feeds',entry.target, entry.intersectionRatio);
|
||||||
|
|
||||||
|
if (entry.intersectionRatio == 0)
|
||||||
|
Feeds.onHide(entry);
|
||||||
|
else
|
||||||
|
Feeds.onShow(entry);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{threshold: [0, 1], root: document.querySelector("body")}
|
||||||
|
),
|
||||||
_counters_prev: [],
|
_counters_prev: [],
|
||||||
// NOTE: this implementation is incomplete
|
// NOTE: this implementation is incomplete
|
||||||
// for general objects but good enough for counters
|
// for general objects but good enough for counters
|
||||||
@@ -109,6 +123,9 @@ const Feeds = {
|
|||||||
}
|
}
|
||||||
return false; // block unneeded form submits
|
return false; // block unneeded form submits
|
||||||
},
|
},
|
||||||
|
openDefaultFeed: function() {
|
||||||
|
this.open({feed: this._default_feed_id});
|
||||||
|
},
|
||||||
openNextUnread: function() {
|
openNextUnread: function() {
|
||||||
const is_cat = this.activeIsCat();
|
const is_cat = this.activeIsCat();
|
||||||
const nuf = this.getNextUnread(this.getActive(), is_cat);
|
const nuf = this.getNextUnread(this.getActive(), is_cat);
|
||||||
@@ -116,23 +133,20 @@ const Feeds = {
|
|||||||
},
|
},
|
||||||
toggle: function() {
|
toggle: function() {
|
||||||
Element.toggle("feeds-holder");
|
Element.toggle("feeds-holder");
|
||||||
|
|
||||||
const splitter = $("feeds-holder_splitter");
|
|
||||||
|
|
||||||
Element.visible("feeds-holder") ? splitter.show() : splitter.hide();
|
|
||||||
|
|
||||||
dijit.byId("main").resize();
|
|
||||||
|
|
||||||
Headlines.updateCurrentUnread();
|
|
||||||
},
|
},
|
||||||
cancelSearch: function() {
|
cancelSearch: function() {
|
||||||
this._search_query = "";
|
this._search_query = "";
|
||||||
this.reloadCurrent();
|
this.reloadCurrent();
|
||||||
},
|
},
|
||||||
requestCounters: function() {
|
// null = get all data, [] would give empty response for specific type
|
||||||
xhrPost("backend.php", {op: "rpc", method: "getAllCounters", seq: App.next_seq()}, (transport) => {
|
requestCounters: function(feed_ids = null, label_ids = null) {
|
||||||
App.handleRpcJson(transport);
|
xhr.json("backend.php", {op: "rpc",
|
||||||
});
|
method: "getAllCounters",
|
||||||
|
"feed_ids[]": feed_ids,
|
||||||
|
"feed_id_count": feed_ids ? feed_ids.length : -1,
|
||||||
|
"label_ids[]": label_ids,
|
||||||
|
"label_id_count": label_ids ? label_ids.length : -1,
|
||||||
|
seq: App.next_seq()});
|
||||||
},
|
},
|
||||||
reload: function() {
|
reload: function() {
|
||||||
try {
|
try {
|
||||||
@@ -180,7 +194,7 @@ const Feeds = {
|
|||||||
dojo.disconnect(tmph);
|
dojo.disconnect(tmph);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("feeds-holder").appendChild(tree.domNode);
|
App.byId("feeds-holder").appendChild(tree.domNode);
|
||||||
|
|
||||||
const tmph2 = dojo.connect(tree, 'onLoad', function () {
|
const tmph2 = dojo.connect(tree, 'onLoad', function () {
|
||||||
dojo.disconnect(tmph2);
|
dojo.disconnect(tmph2);
|
||||||
@@ -199,9 +213,23 @@ const Feeds = {
|
|||||||
App.Error.report(e);
|
App.Error.report(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onHide: function() {
|
||||||
|
App.byId("feeds-holder_splitter").hide();
|
||||||
|
|
||||||
|
dijit.byId("main").resize();
|
||||||
|
Headlines.updateCurrentUnread();
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
App.byId("feeds-holder_splitter").show();
|
||||||
|
|
||||||
|
dijit.byId("main").resize();
|
||||||
|
Headlines.updateCurrentUnread();
|
||||||
|
},
|
||||||
init: function() {
|
init: function() {
|
||||||
console.log("in feedlist init");
|
console.log("in feedlist init");
|
||||||
|
|
||||||
|
this._feeds_holder_observer.observe(App.byId("feeds-holder"));
|
||||||
|
|
||||||
App.setLoadingProgress(50);
|
App.setLoadingProgress(50);
|
||||||
|
|
||||||
//document.onkeydown = (event) => { return App.hotkeyHandler(event) };
|
//document.onkeydown = (event) => { return App.hotkeyHandler(event) };
|
||||||
@@ -215,7 +243,7 @@ const Feeds = {
|
|||||||
if (hash_feed_id != undefined) {
|
if (hash_feed_id != undefined) {
|
||||||
this.open({feed: hash_feed_id, is_cat: hash_feed_is_cat});
|
this.open({feed: hash_feed_id, is_cat: hash_feed_is_cat});
|
||||||
} else {
|
} else {
|
||||||
this.open({feed: -3});
|
this.openDefaultFeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hideOrShowFeeds(App.getInitParam("hide_read_feeds"));
|
this.hideOrShowFeeds(App.getInitParam("hide_read_feeds"));
|
||||||
@@ -260,10 +288,10 @@ const Feeds = {
|
|||||||
|
|
||||||
// bw_limit disables timeout() so we request initial counters separately
|
// bw_limit disables timeout() so we request initial counters separately
|
||||||
if (App.getInitParam("bw_limit")) {
|
if (App.getInitParam("bw_limit")) {
|
||||||
this.requestCounters(true);
|
this.requestCounters();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.requestCounters(true);
|
this.requestCounters();
|
||||||
setInterval(() => { this.requestCounters(); }, 60 * 1000)
|
setInterval(() => { this.requestCounters(); }, 60 * 1000)
|
||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
@@ -284,8 +312,8 @@ const Feeds = {
|
|||||||
this._active_feed_id = id;
|
this._active_feed_id = id;
|
||||||
this._active_feed_is_cat = is_cat;
|
this._active_feed_is_cat = is_cat;
|
||||||
|
|
||||||
$("headlines-frame").setAttribute("feed-id", id);
|
App.byId("headlines-frame").setAttribute("feed-id", id);
|
||||||
$("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0);
|
App.byId("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0);
|
||||||
|
|
||||||
this.select(id, is_cat);
|
this.select(id, is_cat);
|
||||||
|
|
||||||
@@ -299,7 +327,7 @@ const Feeds = {
|
|||||||
toggleUnread: function() {
|
toggleUnread: function() {
|
||||||
const hide = !App.getInitParam("hide_read_feeds");
|
const hide = !App.getInitParam("hide_read_feeds");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
|
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
|
||||||
this.hideOrShowFeeds(hide);
|
this.hideOrShowFeeds(hide);
|
||||||
App.setInitParam("hide_read_feeds", hide);
|
App.setInitParam("hide_read_feeds", hide);
|
||||||
});
|
});
|
||||||
@@ -310,14 +338,13 @@ const Feeds = {
|
|||||||
if (tree)
|
if (tree)
|
||||||
return tree.hideRead(hide, App.getInitParam("hide_read_shows_special"));*/
|
return tree.hideRead(hide, App.getInitParam("hide_read_shows_special"));*/
|
||||||
|
|
||||||
$$("body")[0].setAttribute("hide-read-feeds", !!hide);
|
App.findAll("body")[0].setAttribute("hide-read-feeds", !!hide);
|
||||||
$$("body")[0].setAttribute("hide-read-shows-special", !!App.getInitParam("hide_read_shows_special"));
|
App.findAll("body")[0].setAttribute("hide-read-shows-special", !!App.getInitParam("hide_read_shows_special"));
|
||||||
},
|
},
|
||||||
open: function(params) {
|
open: function(params) {
|
||||||
const feed = params.feed;
|
const feed = params.feed;
|
||||||
const is_cat = !!params.is_cat || false;
|
const is_cat = !!params.is_cat || false;
|
||||||
const offset = params.offset || 0;
|
const offset = params.offset || 0;
|
||||||
const viewfeed_debug = params.viewfeed_debug;
|
|
||||||
const append = params.append || false;
|
const append = params.append || false;
|
||||||
const method = params.method;
|
const method = params.method;
|
||||||
// this is used to quickly switch between feeds, sets active but xhr is on a timeout
|
// this is used to quickly switch between feeds, sets active but xhr is on a timeout
|
||||||
@@ -339,7 +366,7 @@ const Feeds = {
|
|||||||
}, 10 * 1000);
|
}, 10 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
Form.enable("toolbar-main");
|
//Form.enable("toolbar-main");
|
||||||
|
|
||||||
let query = Object.assign({op: "feeds", method: "view", feed: feed},
|
let query = Object.assign({op: "feeds", method: "view", feed: feed},
|
||||||
dojo.formToObject("toolbar-main"));
|
dojo.formToObject("toolbar-main"));
|
||||||
@@ -362,8 +389,6 @@ const Feeds = {
|
|||||||
query.m = "ForceUpdate";
|
query.m = "ForceUpdate";
|
||||||
}
|
}
|
||||||
|
|
||||||
Form.enable("toolbar-main");
|
|
||||||
|
|
||||||
if (!delayed)
|
if (!delayed)
|
||||||
if (!this.setExpando(feed, is_cat,
|
if (!this.setExpando(feed, is_cat,
|
||||||
(is_cat) ? 'images/indicator_tiny.gif' : 'images/indicator_white.gif'))
|
(is_cat) ? 'images/indicator_tiny.gif' : 'images/indicator_white.gif'))
|
||||||
@@ -373,20 +398,13 @@ const Feeds = {
|
|||||||
|
|
||||||
this.setActive(feed, is_cat);
|
this.setActive(feed, is_cat);
|
||||||
|
|
||||||
if (viewfeed_debug) {
|
|
||||||
window.open("backend.php?" +
|
|
||||||
dojo.objectToQuery(
|
|
||||||
Object.assign({csrf_token: App.getInitParam("csrf_token")}, query)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.clearTimeout(this._viewfeed_wait_timeout);
|
window.clearTimeout(this._viewfeed_wait_timeout);
|
||||||
this._viewfeed_wait_timeout = window.setTimeout(() => {
|
this._viewfeed_wait_timeout = window.setTimeout(() => {
|
||||||
xhrPost("backend.php", query, (transport) => {
|
xhr.json("backend.php", query, (reply) => {
|
||||||
try {
|
try {
|
||||||
window.clearTimeout(this._infscroll_timeout);
|
window.clearTimeout(this._infscroll_timeout);
|
||||||
this.setExpando(feed, is_cat, 'images/blank_icon.gif');
|
this.setExpando(feed, is_cat, 'images/blank_icon.gif');
|
||||||
Headlines.onLoaded(transport, offset, append);
|
Headlines.onLoaded(reply, offset, append);
|
||||||
PluginHost.run(PluginHost.HOOK_FEED_LOADED, [feed, is_cat]);
|
PluginHost.run(PluginHost.HOOK_FEED_LOADED, [feed, is_cat]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
App.Error.report(e);
|
App.Error.report(e);
|
||||||
@@ -401,8 +419,7 @@ const Feeds = {
|
|||||||
|
|
||||||
Notify.progress("Marking all feeds as read...");
|
Notify.progress("Marking all feeds as read...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "feeds", method: "catchupAll"}, () => {
|
xhr.json("backend.php", {op: "feeds", method: "catchupAll"}, () => {
|
||||||
this.requestCounters(true);
|
|
||||||
this.reloadCurrent();
|
this.reloadCurrent();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -447,9 +464,7 @@ const Feeds = {
|
|||||||
|
|
||||||
Notify.progress("Loading, please wait...", true);
|
Notify.progress("Loading, please wait...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", catchup_query, (transport) => {
|
xhr.json("backend.php", catchup_query, () => {
|
||||||
App.handleRpcJson(transport);
|
|
||||||
|
|
||||||
const show_next_feed = App.getInitParam("on_catchup_show_next_feed");
|
const show_next_feed = App.getInitParam("on_catchup_show_next_feed");
|
||||||
|
|
||||||
// only select next unread feed if catching up entirely (as opposed to last week etc)
|
// only select next unread feed if catching up entirely (as opposed to last week etc)
|
||||||
@@ -476,9 +491,9 @@ const Feeds = {
|
|||||||
|
|
||||||
if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(str)) {
|
if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(str)) {
|
||||||
|
|
||||||
const rows = $$("#headlines-frame > div[id*=RROW][class*=Unread][data-orig-feed-id='" + id + "']");
|
const rows = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread][data-orig-feed-id='" + id + "']");
|
||||||
|
|
||||||
rows.each((row) => {
|
rows.forEach((row) => {
|
||||||
row.removeClassName("Unread");
|
row.removeClassName("Unread");
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -501,7 +516,7 @@ const Feeds = {
|
|||||||
const tree = dijit.byId("feedTree");
|
const tree = dijit.byId("feedTree");
|
||||||
|
|
||||||
if (tree && tree.model)
|
if (tree && tree.model)
|
||||||
return tree.getFeedCategory(feed);
|
return tree._cat_of_feed(feed);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
@@ -566,14 +581,42 @@ const Feeds = {
|
|||||||
return tree.model.store.getValue(nuf, 'bare_id');
|
return tree.model.store.getValue(nuf, 'bare_id');
|
||||||
},
|
},
|
||||||
search: function() {
|
search: function() {
|
||||||
xhrPost("backend.php",
|
xhr.json("backend.php",
|
||||||
{op: "feeds", method: "search",
|
{op: "feeds", method: "search"},
|
||||||
param: Feeds.getActive() + ":" + Feeds.activeIsCat()},
|
(reply) => {
|
||||||
(transport) => {
|
|
||||||
try {
|
try {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "searchDlg",
|
content: `
|
||||||
content: transport.responseText,
|
<form onsubmit='return false'>
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<input dojoType='dijit.form.ValidationTextBox' id='search_query'
|
||||||
|
style='font-size : 16px; width : 540px;'
|
||||||
|
placeHolder="${__("Search %s...").replace("%s", Feeds.getName(Feeds.getActive(), Feeds.activeIsCat()))}"
|
||||||
|
name='query' type='search' value=''>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
${reply.show_language ?
|
||||||
|
`
|
||||||
|
<fieldset>
|
||||||
|
<label class='inline'>${__("Language:")}</label>
|
||||||
|
${App.FormFields.select_tag("search_language", reply.default_language, reply.all_languages,
|
||||||
|
{title: __('Used for word stemming')}, "search_language")}
|
||||||
|
</fieldset>
|
||||||
|
` : ''}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
${reply.show_syntax_help ?
|
||||||
|
`${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("Search syntax"), "",
|
||||||
|
{class: 'alt-info pull-left', onclick: "window.open('https://tt-rss.org/wiki/SearchSyntax')"})}
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${App.FormFields.submit_tag(__('Search'), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__('Cancel'))}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
title: __("Search"),
|
title: __("Search"),
|
||||||
execute: function () {
|
execute: function () {
|
||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
@@ -613,8 +656,13 @@ const Feeds = {
|
|||||||
updateRandom: function() {
|
updateRandom: function() {
|
||||||
console.log("in update_random_feed");
|
console.log("in update_random_feed");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "rpc", method: "updaterandomfeed"}, (transport) => {
|
xhr.json("backend.php", {op: "rpc", method: "updaterandomfeed"}, () => {
|
||||||
App.handleRpcJson(transport, true);
|
//
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
renderIcon: function(feed_id, exists) {
|
||||||
|
return feed_id && exists ?
|
||||||
|
`<img class="icon" src="${App.escapeHtml(App.getInitParam("icons_url"))}/${feed_id}.ico">` :
|
||||||
|
`<i class='icon-no-feed material-icons'>rss_feed</i>`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
365
js/Headlines.js
365
js/Headlines.js
@@ -1,13 +1,13 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* global __, ngettext, Article, App */
|
/* global __, ngettext, Article, App */
|
||||||
/* global xhrPost, dojo, dijit, PluginHost, Notify, $$, Feeds */
|
/* global dojo, dijit, PluginHost, Notify, xhr, Feeds */
|
||||||
/* global CommonDialogs */
|
/* global CommonDialogs */
|
||||||
|
|
||||||
const Headlines = {
|
const Headlines = {
|
||||||
vgroup_last_feed: undefined,
|
vgroup_last_feed: undefined,
|
||||||
_headlines_scroll_timeout: 0,
|
_headlines_scroll_timeout: 0,
|
||||||
_observer_counters_timeout: 0,
|
//_observer_counters_timeout: 0,
|
||||||
headlines: [],
|
headlines: [],
|
||||||
current_first_id: 0,
|
current_first_id: 0,
|
||||||
_scroll_reset_timeout: false,
|
_scroll_reset_timeout: false,
|
||||||
@@ -44,7 +44,7 @@ const Headlines = {
|
|||||||
row_observer: new MutationObserver((mutations) => {
|
row_observer: new MutationObserver((mutations) => {
|
||||||
const modified = [];
|
const modified = [];
|
||||||
|
|
||||||
mutations.each((m) => {
|
mutations.forEach((m) => {
|
||||||
if (m.type == 'attributes' && ['class', 'data-score'].indexOf(m.attributeName) != -1) {
|
if (m.type == 'attributes' && ['class', 'data-score'].indexOf(m.attributeName) != -1) {
|
||||||
|
|
||||||
const row = m.target;
|
const row = m.target;
|
||||||
@@ -54,7 +54,7 @@ const Headlines = {
|
|||||||
const hl = Headlines.headlines[id];
|
const hl = Headlines.headlines[id];
|
||||||
|
|
||||||
if (hl) {
|
if (hl) {
|
||||||
const hl_old = Object.extend({}, hl);
|
const hl_old = {...{}, ...hl};
|
||||||
|
|
||||||
hl.unread = row.hasClassName("Unread");
|
hl.unread = row.hasClassName("Unread");
|
||||||
hl.marked = row.hasClassName("marked");
|
hl.marked = row.hasClassName("marked");
|
||||||
@@ -94,7 +94,7 @@ const Headlines = {
|
|||||||
rescore: {},
|
rescore: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
modified.each(function (m) {
|
modified.forEach(function (m) {
|
||||||
if (m.old.marked != m.new.marked)
|
if (m.old.marked != m.new.marked)
|
||||||
ops.tmark.push(m.id);
|
ops.tmark.push(m.id);
|
||||||
|
|
||||||
@@ -118,29 +118,29 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ops.select.each((row) => {
|
ops.select.forEach((row) => {
|
||||||
const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
|
const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
|
||||||
|
|
||||||
if (cb)
|
if (cb)
|
||||||
cb.attr('checked', true);
|
cb.attr('checked', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
ops.deselect.each((row) => {
|
ops.deselect.forEach((row) => {
|
||||||
const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
|
const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
|
||||||
|
|
||||||
if (cb && !row.hasClassName("active"))
|
if (cb && !row.hasClassName("active"))
|
||||||
cb.attr('checked', false);
|
cb.attr('checked', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
ops.activate.each((row) => {
|
ops.activate.forEach((row) => {
|
||||||
const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
|
const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
|
||||||
|
|
||||||
if (cb)
|
if (cb)
|
||||||
cb.attr('checked', true);
|
cb.attr('checked', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
ops.deactivate.each((row) => {
|
ops.deactivate.forEach((row) => {
|
||||||
const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
|
const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
|
||||||
|
|
||||||
if (cb && !row.hasClassName("Selected"))
|
if (cb && !row.hasClassName("Selected"))
|
||||||
cb.attr('checked', false);
|
cb.attr('checked', false);
|
||||||
@@ -149,39 +149,56 @@ const Headlines = {
|
|||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
if (ops.tmark.length != 0)
|
if (ops.tmark.length != 0)
|
||||||
promises.push(xhrPost("backend.php",
|
promises.push(xhr.post("backend.php",
|
||||||
{op: "rpc", method: "markSelected", ids: ops.tmark.toString(), cmode: 2}));
|
{op: "rpc", method: "markSelected", "ids[]": ops.tmark, cmode: 2}));
|
||||||
|
|
||||||
if (ops.tpub.length != 0)
|
if (ops.tpub.length != 0)
|
||||||
promises.push(xhrPost("backend.php",
|
promises.push(xhr.post("backend.php",
|
||||||
{op: "rpc", method: "publishSelected", ids: ops.tpub.toString(), cmode: 2}));
|
{op: "rpc", method: "publishSelected", "ids[]": ops.tpub, cmode: 2}));
|
||||||
|
|
||||||
if (ops.read.length != 0)
|
if (ops.read.length != 0)
|
||||||
promises.push(xhrPost("backend.php",
|
promises.push(xhr.post("backend.php",
|
||||||
{op: "rpc", method: "catchupSelected", ids: ops.read.toString(), cmode: 0}));
|
{op: "rpc", method: "catchupSelected", "ids[]": ops.read, cmode: 0}));
|
||||||
|
|
||||||
if (ops.unread.length != 0)
|
if (ops.unread.length != 0)
|
||||||
promises.push(xhrPost("backend.php",
|
promises.push(xhr.post("backend.php",
|
||||||
{op: "rpc", method: "catchupSelected", ids: ops.unread.toString(), cmode: 1}));
|
{op: "rpc", method: "catchupSelected", "ids[]": ops.unread, cmode: 1}));
|
||||||
|
|
||||||
const scores = Object.keys(ops.rescore);
|
const scores = Object.keys(ops.rescore);
|
||||||
|
|
||||||
if (scores.length != 0) {
|
if (scores.length != 0) {
|
||||||
scores.each((score) => {
|
scores.forEach((score) => {
|
||||||
promises.push(xhrPost("backend.php",
|
promises.push(xhr.post("backend.php",
|
||||||
{op: "article", method: "setScore", id: ops.rescore[score].toString(), score: score}));
|
{op: "article", method: "setScore", "ids[]": ops.rescore[score].toString(), score: score}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promises.length > 0)
|
Promise.all(promises).then((results) => {
|
||||||
Promise.all([promises]).then(() => {
|
let feeds = [];
|
||||||
window.clearTimeout(this._observer_counters_timeout);
|
let labels = [];
|
||||||
|
|
||||||
this._observer_counters_timeout = setTimeout(() => {
|
results.forEach((res) => {
|
||||||
Feeds.requestCounters(true);
|
if (res) {
|
||||||
}, 1000);
|
try {
|
||||||
|
const obj = JSON.parse(res);
|
||||||
|
|
||||||
|
if (obj.feeds)
|
||||||
|
feeds = feeds.concat(obj.feeds);
|
||||||
|
|
||||||
|
if (obj.labels)
|
||||||
|
labels = labels.concat(obj.labels);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (feeds.length > 0) {
|
||||||
|
console.log('requesting counters for', feeds, labels);
|
||||||
|
Feeds.requestCounters(feeds, labels);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
click: function (event, id, in_body) {
|
click: function (event, id, in_body) {
|
||||||
in_body = in_body || false;
|
in_body = in_body || false;
|
||||||
@@ -211,7 +228,7 @@ const Headlines = {
|
|||||||
|
|
||||||
Headlines.select('none');
|
Headlines.select('none');
|
||||||
|
|
||||||
const scroll_position_A = $("RROW-" + id).offsetTop - $("headlines-frame").scrollTop;
|
const scroll_position_A = App.byId(`RROW-${id}`).offsetTop - App.byId("headlines-frame").scrollTop;
|
||||||
|
|
||||||
Article.setActive(id);
|
Article.setActive(id);
|
||||||
|
|
||||||
@@ -222,10 +239,10 @@ const Headlines = {
|
|||||||
|
|
||||||
Headlines.toggleUnread(id, 0);
|
Headlines.toggleUnread(id, 0);
|
||||||
} else {
|
} else {
|
||||||
const scroll_position_B = $("RROW-" + id).offsetTop - $("headlines-frame").scrollTop;
|
const scroll_position_B = App.byId(`RROW-${id}`).offsetTop - App.byId("headlines-frame").scrollTop;
|
||||||
|
|
||||||
// this would only work if there's enough space
|
// this would only work if there's enough space
|
||||||
$("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B;
|
App.byId("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B;
|
||||||
|
|
||||||
Article.cdmMoveToId(id);
|
Article.cdmMoveToId(id);
|
||||||
}
|
}
|
||||||
@@ -252,7 +269,7 @@ const Headlines = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
initScrollHandler: function () {
|
initScrollHandler: function () {
|
||||||
$("headlines-frame").onscroll = (event) => {
|
App.byId("headlines-frame").onscroll = (event) => {
|
||||||
clearTimeout(this._headlines_scroll_timeout);
|
clearTimeout(this._headlines_scroll_timeout);
|
||||||
this._headlines_scroll_timeout = window.setTimeout(function () {
|
this._headlines_scroll_timeout = window.setTimeout(function () {
|
||||||
//console.log('done scrolling', event);
|
//console.log('done scrolling', event);
|
||||||
@@ -262,8 +279,8 @@ const Headlines = {
|
|||||||
},
|
},
|
||||||
loadMore: function () {
|
loadMore: function () {
|
||||||
const view_mode = document.forms["toolbar-main"].view_mode.value;
|
const view_mode = document.forms["toolbar-main"].view_mode.value;
|
||||||
const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length;
|
const unread_in_buffer = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread]").length;
|
||||||
const num_all = $$("#headlines-frame > div[id*=RROW]").length;
|
const num_all = App.findAll("#headlines-frame > div[id*=RROW]").length;
|
||||||
const num_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
|
const num_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
|
||||||
|
|
||||||
// TODO implement marked & published
|
// TODO implement marked & published
|
||||||
@@ -289,10 +306,10 @@ const Headlines = {
|
|||||||
Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), offset: offset, append: true});
|
Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), offset: offset, append: true});
|
||||||
},
|
},
|
||||||
isChildVisible: function (elem) {
|
isChildVisible: function (elem) {
|
||||||
return App.Scrollable.isChildVisible(elem, $("headlines-frame"));
|
return App.Scrollable.isChildVisible(elem, App.byId("headlines-frame"));
|
||||||
},
|
},
|
||||||
firstVisible: function () {
|
firstVisible: function () {
|
||||||
const rows = $$("#headlines-frame > div[id*=RROW]");
|
const rows = App.findAll("#headlines-frame > div[id*=RROW]");
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const row = rows[i];
|
const row = rows[i];
|
||||||
@@ -303,7 +320,7 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
unpackVisible: function(container) {
|
unpackVisible: function(container) {
|
||||||
const rows = $$("#headlines-frame > div[id*=RROW][data-content].cdm");
|
const rows = App.findAll("#headlines-frame > div[id*=RROW][data-content].cdm");
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
if (App.Scrollable.isChildVisible(rows[i], container)) {
|
if (App.Scrollable.isChildVisible(rows[i], container)) {
|
||||||
@@ -315,8 +332,8 @@ const Headlines = {
|
|||||||
scrollHandler: function (/*event*/) {
|
scrollHandler: function (/*event*/) {
|
||||||
try {
|
try {
|
||||||
if (!Feeds.infscroll_disabled && !Feeds.infscroll_in_progress) {
|
if (!Feeds.infscroll_disabled && !Feeds.infscroll_in_progress) {
|
||||||
const hsp = $("headlines-spacer");
|
const hsp = App.byId("headlines-spacer");
|
||||||
const container = $("headlines-frame");
|
const container = App.byId("headlines-frame");
|
||||||
|
|
||||||
if (hsp && hsp.previousSibling) {
|
if (hsp && hsp.previousSibling) {
|
||||||
const last_row = hsp.previousSibling;
|
const last_row = hsp.previousSibling;
|
||||||
@@ -333,7 +350,7 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (App.isCombinedMode() && App.getInitParam("cdm_expanded")) {
|
if (App.isCombinedMode() && App.getInitParam("cdm_expanded")) {
|
||||||
const container = $("headlines-frame")
|
const container = App.byId("headlines-frame")
|
||||||
|
|
||||||
/* don't do anything until there was some scrolling */
|
/* don't do anything until there was some scrolling */
|
||||||
if (container.scrollTop > 0)
|
if (container.scrollTop > 0)
|
||||||
@@ -342,12 +359,12 @@ const Headlines = {
|
|||||||
|
|
||||||
if (App.getInitParam("cdm_auto_catchup")) {
|
if (App.getInitParam("cdm_auto_catchup")) {
|
||||||
|
|
||||||
const rows = $$("#headlines-frame > div[id*=RROW][class*=Unread]");
|
const rows = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread]");
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const row = rows[i];
|
const row = rows[i];
|
||||||
|
|
||||||
if ($("headlines-frame").scrollTop > (row.offsetTop + row.offsetHeight / 2)) {
|
if (App.byId("headlines-frame").scrollTop > (row.offsetTop + row.offsetHeight / 2)) {
|
||||||
row.removeClassName("Unread");
|
row.removeClassName("Unread");
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
@@ -362,23 +379,23 @@ const Headlines = {
|
|||||||
return this.headlines[id];
|
return this.headlines[id];
|
||||||
},
|
},
|
||||||
setCommonClasses: function () {
|
setCommonClasses: function () {
|
||||||
$("headlines-frame").removeClassName("cdm");
|
App.byId("headlines-frame").removeClassName("cdm");
|
||||||
$("headlines-frame").removeClassName("normal");
|
App.byId("headlines-frame").removeClassName("normal");
|
||||||
|
|
||||||
$("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal");
|
App.byId("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal");
|
||||||
|
|
||||||
// for floating title because it's placed outside of headlines-frame
|
// for floating title because it's placed outside of headlines-frame
|
||||||
$("main").removeClassName("expandable");
|
App.byId("main").removeClassName("expandable");
|
||||||
$("main").removeClassName("expanded");
|
App.byId("main").removeClassName("expanded");
|
||||||
|
|
||||||
if (App.isCombinedMode())
|
if (App.isCombinedMode())
|
||||||
$("main").addClassName(App.getInitParam("cdm_expanded") ? " expanded" : " expandable");
|
App.byId("main").addClassName(App.getInitParam("cdm_expanded") ? "expanded" : "expandable");
|
||||||
},
|
},
|
||||||
renderAgain: function () {
|
renderAgain: function () {
|
||||||
// TODO: wrap headline elements into a knockoutjs model to prevent all this stuff
|
// TODO: wrap headline elements into a knockoutjs model to prevent all this stuff
|
||||||
Headlines.setCommonClasses();
|
Headlines.setCommonClasses();
|
||||||
|
|
||||||
$$("#headlines-frame > div[id*=RROW]").each((row) => {
|
App.findAll("#headlines-frame > div[id*=RROW]").forEach((row) => {
|
||||||
const id = row.getAttribute("data-article-id");
|
const id = row.getAttribute("data-article-id");
|
||||||
const hl = this.headlines[id];
|
const hl = this.headlines[id];
|
||||||
|
|
||||||
@@ -401,12 +418,12 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$$(".cdm .header-sticky-guard").each((e) => {
|
App.findAll(".cdm .header-sticky-guard").forEach((e) => {
|
||||||
this.sticky_header_observer.observe(e)
|
this.sticky_header_observer.observe(e)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (App.getInitParam("cdm_expanded"))
|
if (App.getInitParam("cdm_expanded"))
|
||||||
$$("#headlines-frame > div[id*=RROW].cdm").each((e) => {
|
App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => {
|
||||||
this.unpack_observer.observe(e)
|
this.unpack_observer.observe(e)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -423,7 +440,7 @@ const Headlines = {
|
|||||||
|
|
||||||
if (headlines.vfeed_group_enabled && hl.feed_title && this.vgroup_last_feed != hl.feed_id) {
|
if (headlines.vfeed_group_enabled && hl.feed_title && this.vgroup_last_feed != hl.feed_id) {
|
||||||
const vgrhdr = `<div data-feed-id='${hl.feed_id}' class='feed-title'>
|
const vgrhdr = `<div data-feed-id='${hl.feed_id}' class='feed-title'>
|
||||||
<div style='float : right'>${hl.feed_icon}</div>
|
<div style='float : right'>${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</div>
|
||||||
<a class="title" href="#" onclick="Feeds.open({feed:${hl.feed_id}})">${hl.feed_title}
|
<a class="title" href="#" onclick="Feeds.open({feed:${hl.feed_id}})">${hl.feed_title}
|
||||||
<a class="catchup" title="${__('mark feed as read')}" onclick="Feeds.catchupFeedInGroup(${hl.feed_id})" href="#"><i class="icon-done material-icons">done_all</i></a>
|
<a class="catchup" title="${__('mark feed as read')}" onclick="Feeds.catchupFeedInGroup(${hl.feed_id})" href="#"><i class="icon-done material-icons">done_all</i></a>
|
||||||
</div>`
|
</div>`
|
||||||
@@ -431,7 +448,7 @@ const Headlines = {
|
|||||||
const tmp = document.createElement("div");
|
const tmp = document.createElement("div");
|
||||||
tmp.innerHTML = vgrhdr;
|
tmp.innerHTML = vgrhdr;
|
||||||
|
|
||||||
$("headlines-frame").appendChild(tmp.firstChild);
|
App.byId("headlines-frame").appendChild(tmp.firstChild);
|
||||||
|
|
||||||
this.vgroup_last_feed = hl.feed_id;
|
this.vgroup_last_feed = hl.feed_id;
|
||||||
}
|
}
|
||||||
@@ -462,7 +479,7 @@ const Headlines = {
|
|||||||
<a class="title" title="${App.escapeHtml(hl.title)}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.link)}">
|
<a class="title" title="${App.escapeHtml(hl.title)}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.link)}">
|
||||||
${hl.title}</a>
|
${hl.title}</a>
|
||||||
<span class="author">${hl.author}</span>
|
<span class="author">${hl.author}</span>
|
||||||
${hl.labels}
|
${Article.renderLabels(hl.id, hl.labels)}
|
||||||
${hl.cdm_excerpt ? hl.cdm_excerpt : ""}
|
${hl.cdm_excerpt ? hl.cdm_excerpt : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -477,25 +494,26 @@ const Headlines = {
|
|||||||
<i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i>
|
<i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i>
|
||||||
|
|
||||||
<span style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}" onclick="Feeds.open({feed:${hl.feed_id}})">
|
<span style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}" onclick="Feeds.open({feed:${hl.feed_id}})">
|
||||||
${hl.feed_icon}</span>
|
${Feeds.renderIcon(hl.feed_id, hl.has_icon)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content" onclick="return Headlines.click(event, ${hl.id}, true);">
|
<div class="content" onclick="return Headlines.click(event, ${hl.id}, true);">
|
||||||
<div id="POSTNOTE-${hl.id}">${hl.note}</div>
|
${Article.renderNote(hl.id, hl.note)}
|
||||||
<div class="content-inner" lang="${hl.lang ? hl.lang : 'en'}">
|
<div class="content-inner" lang="${hl.lang ? hl.lang : 'en'}">
|
||||||
<img src="${App.getInitParam('icon_indicator_white')}">
|
<img src="${App.getInitParam('icon_indicator_white')}">
|
||||||
</div>
|
</div>
|
||||||
<div class="intermediate">
|
<div class="intermediate">
|
||||||
${hl.enclosures}
|
${Article.renderEnclosures(hl.enclosures)}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer" onclick="event.stopPropagation()">
|
<div class="footer" onclick="event.stopPropagation()">
|
||||||
|
|
||||||
<div class="left">
|
<div class="left">
|
||||||
${hl.buttons_left}
|
${hl.buttons_left}
|
||||||
<i class="material-icons">label_outline</i>
|
<i class="material-icons">label_outline</i>
|
||||||
<span id="ATSTR-${hl.id}">${hl.tags_str}</span>
|
${Article.renderTags(hl.id, hl.tags)}
|
||||||
<a title="${__("Edit tags for this article")}" href="#"
|
<a title="${__("Edit tags for this article")}" href="#"
|
||||||
onclick="Article.editTags(${hl.id})">(+)</a>
|
onclick="Article.editTags(${hl.id})">(+)</a>
|
||||||
${comments}
|
${comments}
|
||||||
@@ -527,7 +545,7 @@ const Headlines = {
|
|||||||
<span data-article-id="${hl.id}" class="hl-content hlMenuAttach">
|
<span data-article-id="${hl.id}" class="hl-content hlMenuAttach">
|
||||||
<a class="title" href="${App.escapeHtml(hl.link)}">${hl.title} <span class="preview">${hl.content_preview}</span></a>
|
<a class="title" href="${App.escapeHtml(hl.link)}">${hl.title} <span class="preview">${hl.content_preview}</span></a>
|
||||||
<span class="author">${hl.author}</span>
|
<span class="author">${hl.author}</span>
|
||||||
${hl.labels}
|
${Article.renderLabels(hl.id, hl.labels)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="feed">
|
<span class="feed">
|
||||||
@@ -538,7 +556,7 @@ const Headlines = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i>
|
<i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i>
|
||||||
<span onclick="Feeds.open({feed:${hl.feed_id}})" style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}">${hl.feed_icon}</span>
|
<span onclick="Feeds.open({feed:${hl.feed_id}})" style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -555,20 +573,74 @@ const Headlines = {
|
|||||||
return tmp.firstChild;
|
return tmp.firstChild;
|
||||||
},
|
},
|
||||||
updateCurrentUnread: function () {
|
updateCurrentUnread: function () {
|
||||||
if ($("feed_current_unread")) {
|
if (App.byId("feed_current_unread")) {
|
||||||
const feed_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
|
const feed_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
|
||||||
|
|
||||||
if (feed_unread > 0 && !Element.visible("feeds-holder")) {
|
if (feed_unread > 0 && !Element.visible("feeds-holder")) {
|
||||||
$("feed_current_unread").innerText = feed_unread;
|
App.byId("feed_current_unread").innerText = feed_unread;
|
||||||
Element.show("feed_current_unread");
|
Element.show("feed_current_unread");
|
||||||
} else {
|
} else {
|
||||||
Element.hide("feed_current_unread");
|
Element.hide("feed_current_unread");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoaded: function (transport, offset, append) {
|
renderToolbar: function(headlines) {
|
||||||
const reply = App.handleRpcJson(transport);
|
|
||||||
|
|
||||||
|
const tb = headlines['toolbar'];
|
||||||
|
const search_query = Feeds._search_query ? Feeds._search_query.query : "";
|
||||||
|
const target = dijit.byId('toolbar-headlines');
|
||||||
|
|
||||||
|
if (tb && typeof tb == 'object') {
|
||||||
|
target.attr('innerHTML',
|
||||||
|
`
|
||||||
|
<span class='left'>
|
||||||
|
<a href="#" title="${__("Show as feed")}"
|
||||||
|
onclick='CommonDialogs.generatedFeed("${headlines.id}", ${headlines.is_cat}, "${App.escapeHtml(search_query)}")'>
|
||||||
|
<i class='icon-syndicate material-icons'>rss_feed</i>
|
||||||
|
</a>
|
||||||
|
${tb.site_url ?
|
||||||
|
`<a class="feed_title" target="_blank" href="${App.escapeHtml(tb.site_url)}" title="${tb.last_updated}">${tb.title}</a>` :
|
||||||
|
`<span class="feed_title">${tb.title}</span>`}
|
||||||
|
${search_query ?
|
||||||
|
`
|
||||||
|
<span class='cancel_search'>(<a href='#' onclick='Feeds.cancelSearch()'>${__("Cancel search")}</a>)</span>
|
||||||
|
` : ''}
|
||||||
|
${tb.error ? `<i title="${App.escapeHtml(tb.error)}" class='material-icons icon-error'>error</i>` : ''}
|
||||||
|
<span id='feed_current_unread' style='display: none'></span>
|
||||||
|
</span>
|
||||||
|
<span class='right'>
|
||||||
|
<span id='selected_prompt'></span>
|
||||||
|
<div dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'>
|
||||||
|
<span>${__("Select...")}</span>
|
||||||
|
<div dojoType='dijit.Menu' style='display: none;'>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.select("all")'>${__('All')}</div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.select("unread")'>${__('Unread')}</div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.select("invert")'>${__('Invert')}</div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.select("none")'>${__('None')}</div>
|
||||||
|
<div dojoType='dijit.MenuSeparator'></div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleUnread()'>${__('Toggle unread')}</div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleMarked()'>${__('Toggle starred')}</div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.selectionTogglePublished()'>${__('Toggle published')}</div>
|
||||||
|
<div dojoType='dijit.MenuSeparator'></div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Headlines.catchupSelection()'>${__('Mark as read')}</div>
|
||||||
|
<div dojoType='dijit.MenuItem' onclick='Article.selectionSetScore()'>${__('Set score')}</div>
|
||||||
|
${tb.plugin_menu_items}
|
||||||
|
${headlines.id === 0 && !headlines.is_cat ?
|
||||||
|
`
|
||||||
|
<div dojoType='dijit.MenuSeparator'></div>
|
||||||
|
<div dojoType='dijit.MenuItem' class='text-error' onclick='Headlines.deleteSelection()'>${__('Delete permanently')}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
${tb.plugin_buttons}
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
target.attr('innerHTML', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
dojo.parser.parse(target.domNode);
|
||||||
|
},
|
||||||
|
onLoaded: function (reply, offset, append) {
|
||||||
console.log("Headlines.onLoaded: offset=", offset, "append=", append);
|
console.log("Headlines.onLoaded: offset=", offset, "append=", append);
|
||||||
|
|
||||||
let is_cat = false;
|
let is_cat = false;
|
||||||
@@ -597,15 +669,15 @@ const Headlines = {
|
|||||||
// also called in renderAgain() after view mode switch
|
// also called in renderAgain() after view mode switch
|
||||||
Headlines.setCommonClasses();
|
Headlines.setCommonClasses();
|
||||||
|
|
||||||
$("headlines-frame").setAttribute("is-vfeed",
|
App.byId("headlines-frame").setAttribute("is-vfeed",
|
||||||
reply['headlines']['is_vfeed'] ? 1 : 0);
|
reply['headlines']['is_vfeed'] ? 1 : 0);
|
||||||
|
|
||||||
Article.setActive(0);
|
Article.setActive(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$("headlines-frame").removeClassName("smooth-scroll");
|
App.byId("headlines-frame").removeClassName("smooth-scroll");
|
||||||
$("headlines-frame").scrollTop = 0;
|
App.byId("headlines-frame").scrollTop = 0;
|
||||||
$("headlines-frame").addClassName("smooth-scroll");
|
App.byId("headlines-frame").addClassName("smooth-scroll");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
@@ -613,25 +685,27 @@ const Headlines = {
|
|||||||
this.headlines = [];
|
this.headlines = [];
|
||||||
this.vgroup_last_feed = undefined;
|
this.vgroup_last_feed = undefined;
|
||||||
|
|
||||||
dojo.html.set($("toolbar-headlines"),
|
/*dojo.html.set(App.byId("toolbar-headlines"),
|
||||||
reply['headlines']['toolbar'],
|
reply['headlines']['toolbar'],
|
||||||
{parseContent: true});
|
{parseContent: true});*/
|
||||||
|
|
||||||
|
Headlines.renderToolbar(reply['headlines']);
|
||||||
|
|
||||||
if (typeof reply['headlines']['content'] == 'string') {
|
if (typeof reply['headlines']['content'] == 'string') {
|
||||||
$("headlines-frame").innerHTML = reply['headlines']['content'];
|
App.byId("headlines-frame").innerHTML = reply['headlines']['content'];
|
||||||
} else {
|
} else {
|
||||||
$("headlines-frame").innerHTML = '';
|
App.byId("headlines-frame").innerHTML = '';
|
||||||
|
|
||||||
for (let i = 0; i < reply['headlines']['content'].length; i++) {
|
for (let i = 0; i < reply['headlines']['content'].length; i++) {
|
||||||
const hl = reply['headlines']['content'][i];
|
const hl = reply['headlines']['content'][i];
|
||||||
|
|
||||||
$("headlines-frame").appendChild(this.render(reply['headlines'], hl));
|
App.byId("headlines-frame").appendChild(this.render(reply['headlines'], hl));
|
||||||
|
|
||||||
this.headlines[parseInt(hl.id)] = hl;
|
this.headlines[parseInt(hl.id)] = hl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hsp = $("headlines-spacer");
|
let hsp = App.byId("headlines-spacer");
|
||||||
|
|
||||||
if (!hsp) {
|
if (!hsp) {
|
||||||
hsp = document.createElement("div");
|
hsp = document.createElement("div");
|
||||||
@@ -646,18 +720,19 @@ const Headlines = {
|
|||||||
hsp.innerHTML = "<a href='#' onclick='Feeds.openNextUnread()'>" +
|
hsp.innerHTML = "<a href='#' onclick='Feeds.openNextUnread()'>" +
|
||||||
__("Click to open next unread feed.") + "</a>";
|
__("Click to open next unread feed.") + "</a>";
|
||||||
|
|
||||||
|
/*
|
||||||
if (Feeds._search_query) {
|
if (Feeds._search_query) {
|
||||||
$("feed_title").innerHTML += "<span id='cancel_search'>" +
|
App.byId("feed_title").innerHTML += "<span id='cancel_search'>" +
|
||||||
" (<a href='#' onclick='Feeds.cancelSearch()'>" + __("Cancel search") + "</a>)" +
|
" (<a href='#' onclick='Feeds.cancelSearch()'>" + __("Cancel search") + "</a>)" +
|
||||||
"</span>";
|
"</span>";
|
||||||
}
|
} */
|
||||||
|
|
||||||
Headlines.updateCurrentUnread();
|
Headlines.updateCurrentUnread();
|
||||||
|
|
||||||
} else if (headlines_count > 0 && feed_id == Feeds.getActive() && is_cat == Feeds.activeIsCat()) {
|
} else if (headlines_count > 0 && feed_id == Feeds.getActive() && is_cat == Feeds.activeIsCat()) {
|
||||||
const c = dijit.byId("headlines-frame");
|
const c = dijit.byId("headlines-frame");
|
||||||
|
|
||||||
let hsp = $("headlines-spacer");
|
let hsp = App.byId("headlines-spacer");
|
||||||
|
|
||||||
if (hsp)
|
if (hsp)
|
||||||
c.domNode.removeChild(hsp);
|
c.domNode.removeChild(hsp);
|
||||||
@@ -665,13 +740,13 @@ const Headlines = {
|
|||||||
let headlines_appended = 0;
|
let headlines_appended = 0;
|
||||||
|
|
||||||
if (typeof reply['headlines']['content'] == 'string') {
|
if (typeof reply['headlines']['content'] == 'string') {
|
||||||
$("headlines-frame").innerHTML = reply['headlines']['content'];
|
App.byId("headlines-frame").innerHTML = reply['headlines']['content'];
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < reply['headlines']['content'].length; i++) {
|
for (let i = 0; i < reply['headlines']['content'].length; i++) {
|
||||||
const hl = reply['headlines']['content'][i];
|
const hl = reply['headlines']['content'][i];
|
||||||
|
|
||||||
if (!this.headlines[parseInt(hl.id)]) {
|
if (!this.headlines[parseInt(hl.id)]) {
|
||||||
$("headlines-frame").appendChild(this.render(reply['headlines'], hl));
|
App.byId("headlines-frame").appendChild(this.render(reply['headlines'], hl));
|
||||||
|
|
||||||
this.headlines[parseInt(hl.id)] = hl;
|
this.headlines[parseInt(hl.id)] = hl;
|
||||||
++headlines_appended;
|
++headlines_appended;
|
||||||
@@ -703,7 +778,7 @@ const Headlines = {
|
|||||||
|
|
||||||
console.log("no headlines received, infscroll_disabled=", Feeds.infscroll_disabled, 'first_id_changed=', first_id_changed);
|
console.log("no headlines received, infscroll_disabled=", Feeds.infscroll_disabled, 'first_id_changed=', first_id_changed);
|
||||||
|
|
||||||
const hsp = $("headlines-spacer");
|
const hsp = App.byId("headlines-spacer");
|
||||||
|
|
||||||
if (hsp) {
|
if (hsp) {
|
||||||
if (first_id_changed) {
|
if (first_id_changed) {
|
||||||
@@ -716,17 +791,16 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$$(".cdm .header-sticky-guard").each((e) => {
|
App.findAll(".cdm .header-sticky-guard").forEach((e) => {
|
||||||
this.sticky_header_observer.observe(e)
|
this.sticky_header_observer.observe(e)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (App.getInitParam("cdm_expanded"))
|
if (App.getInitParam("cdm_expanded"))
|
||||||
$$("#headlines-frame > div[id*=RROW].cdm").each((e) => {
|
App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => {
|
||||||
this.unpack_observer.observe(e)
|
this.unpack_observer.observe(e)
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid object received: " + transport.responseText);
|
|
||||||
dijit.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
|
dijit.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
|
||||||
__('Could not update headlines (invalid object received - see error console for details)') +
|
__('Could not update headlines (invalid object received - see error console for details)') +
|
||||||
"</div>");
|
"</div>");
|
||||||
@@ -755,9 +829,7 @@ const Headlines = {
|
|||||||
|
|
||||||
Feeds.reloadCurrent();
|
Feeds.reloadCurrent();
|
||||||
},
|
},
|
||||||
selectionToggleUnread: function (params) {
|
selectionToggleUnread: function (params = {}) {
|
||||||
params = params || {};
|
|
||||||
|
|
||||||
const cmode = params.cmode != undefined ? params.cmode : 2;
|
const cmode = params.cmode != undefined ? params.cmode : 2;
|
||||||
const no_error = params.no_error || false;
|
const no_error = params.no_error || false;
|
||||||
const ids = params.ids || Headlines.getSelected();
|
const ids = params.ids || Headlines.getSelected();
|
||||||
@@ -769,8 +841,8 @@ const Headlines = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ids.each((id) => {
|
ids.forEach((id) => {
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
switch (cmode) {
|
switch (cmode) {
|
||||||
@@ -794,7 +866,7 @@ const Headlines = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ids.each((id) => {
|
ids.forEach((id) => {
|
||||||
this.toggleMark(id);
|
this.toggleMark(id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -806,26 +878,24 @@ const Headlines = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ids.each((id) => {
|
ids.forEach((id) => {
|
||||||
this.togglePub(id);
|
this.togglePub(id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleMark: function (id) {
|
toggleMark: function (id) {
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row)
|
if (row)
|
||||||
row.toggleClassName("marked");
|
row.toggleClassName("marked");
|
||||||
|
|
||||||
},
|
},
|
||||||
togglePub: function (id) {
|
togglePub: function (id) {
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row)
|
if (row)
|
||||||
row.toggleClassName("published");
|
row.toggleClassName("published");
|
||||||
},
|
},
|
||||||
move: function (mode, params) {
|
move: function (mode, params = {}) {
|
||||||
params = params || {};
|
|
||||||
|
|
||||||
const no_expand = params.no_expand || false;
|
const no_expand = params.no_expand || false;
|
||||||
const force_previous = params.force_previous || this.default_force_previous;
|
const force_previous = params.force_previous || this.default_force_previous;
|
||||||
const force_to_top = params.force_to_top || this.default_force_to_top;
|
const force_to_top = params.force_to_top || this.default_force_to_top;
|
||||||
@@ -834,7 +904,7 @@ const Headlines = {
|
|||||||
let next_id = false;
|
let next_id = false;
|
||||||
let current_id = Article.getActive();
|
let current_id = Article.getActive();
|
||||||
|
|
||||||
if (!Headlines.isChildVisible($("RROW-" + current_id))) {
|
if (!Headlines.isChildVisible(App.byId(`RROW-${current_id}`))) {
|
||||||
console.log('active article is obscured, resetting to first visible...');
|
console.log('active article is obscured, resetting to first visible...');
|
||||||
current_id = Headlines.firstVisible();
|
current_id = Headlines.firstVisible();
|
||||||
prev_id = current_id;
|
prev_id = current_id;
|
||||||
@@ -871,13 +941,30 @@ const Headlines = {
|
|||||||
} else {
|
} else {
|
||||||
Article.view(next_id, no_expand);
|
Article.view(next_id, no_expand);
|
||||||
}
|
}
|
||||||
|
} else if (App.isCombinedMode()) {
|
||||||
|
// try to show hsp if no next article exists, in case there's useful information like first_id_changed etc
|
||||||
|
const row = App.byId(`RROW-${current_id}`);
|
||||||
|
const ctr = App.byId("headlines-frame");
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
const next = row.nextSibling;
|
||||||
|
|
||||||
|
// hsp has half-screen height in auto catchup mode therefore we use its first child (normally A element)
|
||||||
|
if (next && Element.visible(next) && next.id == "headlines-spacer" && next.firstChild) {
|
||||||
|
const offset = App.byId("headlines-spacer").offsetTop - App.byId("headlines-frame").offsetHeight + next.firstChild.offsetHeight;
|
||||||
|
|
||||||
|
// don't jump back either
|
||||||
|
if (ctr.scrollTop < offset)
|
||||||
|
ctr.scrollTop = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (mode === "prev") {
|
} else if (mode === "prev") {
|
||||||
if (prev_id || current_id) {
|
if (prev_id || current_id) {
|
||||||
if (App.isCombinedMode()) {
|
if (App.isCombinedMode()) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
const row = $("RROW-" + current_id);
|
const row = App.byId(`RROW-${current_id}`);
|
||||||
const ctr = $("headlines-frame");
|
const ctr = App.byId("headlines-frame");
|
||||||
const delta_px = Math.round(row.offsetTop) - Math.round(ctr.scrollTop);
|
const delta_px = Math.round(row.offsetTop) - Math.round(ctr.scrollTop);
|
||||||
|
|
||||||
console.log('moving back, delta_px', delta_px);
|
console.log('moving back, delta_px', delta_px);
|
||||||
@@ -898,7 +985,7 @@ const Headlines = {
|
|||||||
},
|
},
|
||||||
updateSelectedPrompt: function () {
|
updateSelectedPrompt: function () {
|
||||||
const count = Headlines.getSelected().length;
|
const count = Headlines.getSelected().length;
|
||||||
const elem = $("selected_prompt");
|
const elem = App.byId("selected_prompt");
|
||||||
|
|
||||||
if (elem) {
|
if (elem) {
|
||||||
elem.innerHTML = ngettext("%d article selected",
|
elem.innerHTML = ngettext("%d article selected",
|
||||||
@@ -908,7 +995,7 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleUnread: function (id, cmode) {
|
toggleUnread: function (id, cmode) {
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
if (typeof cmode == "undefined") cmode = 2;
|
if (typeof cmode == "undefined") cmode = 2;
|
||||||
@@ -939,9 +1026,8 @@ const Headlines = {
|
|||||||
ids: ids.toString(), lid: id
|
ids: ids.toString(), lid: id
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, (transport) => {
|
xhr.json("backend.php", query, (reply) => {
|
||||||
App.handleRpcJson(transport);
|
this.onLabelsUpdated(reply);
|
||||||
this.onLabelsUpdated(transport);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
selectionAssignLabel: function (id, ids) {
|
selectionAssignLabel: function (id, ids) {
|
||||||
@@ -957,9 +1043,8 @@ const Headlines = {
|
|||||||
ids: ids.toString(), lid: id
|
ids: ids.toString(), lid: id
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, (transport) => {
|
xhr.json("backend.php", query, (reply) => {
|
||||||
App.handleRpcJson(transport);
|
this.onLabelsUpdated(reply);
|
||||||
this.onLabelsUpdated(transport);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteSelection: function () {
|
deleteSelection: function () {
|
||||||
@@ -988,15 +1073,14 @@ const Headlines = {
|
|||||||
|
|
||||||
const query = {op: "rpc", method: "delete", ids: rows.toString()};
|
const query = {op: "rpc", method: "delete", ids: rows.toString()};
|
||||||
|
|
||||||
xhrPost("backend.php", query, (transport) => {
|
xhr.json("backend.php", query, () => {
|
||||||
App.handleRpcJson(transport);
|
|
||||||
Feeds.reloadCurrent();
|
Feeds.reloadCurrent();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getSelected: function () {
|
getSelected: function () {
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
$$("#headlines-frame > div[id*=RROW][class*=Selected]").each(
|
App.findAll("#headlines-frame > div[id*=RROW][class*=Selected]").forEach(
|
||||||
function (child) {
|
function (child) {
|
||||||
rv.push(child.getAttribute("data-article-id"));
|
rv.push(child.getAttribute("data-article-id"));
|
||||||
});
|
});
|
||||||
@@ -1010,9 +1094,9 @@ const Headlines = {
|
|||||||
getLoaded: function () {
|
getLoaded: function () {
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
const children = $$("#headlines-frame > div[id*=RROW-]");
|
const children = App.findAll("#headlines-frame > div[id*=RROW-]");
|
||||||
|
|
||||||
children.each(function (child) {
|
children.forEach(function (child) {
|
||||||
if (Element.visible(child)) {
|
if (Element.visible(child)) {
|
||||||
rv.push(child.getAttribute("data-article-id"));
|
rv.push(child.getAttribute("data-article-id"));
|
||||||
}
|
}
|
||||||
@@ -1021,7 +1105,7 @@ const Headlines = {
|
|||||||
return rv;
|
return rv;
|
||||||
},
|
},
|
||||||
onRowChecked: function (elem) {
|
onRowChecked: function (elem) {
|
||||||
const row = elem.domNode.up("div[id*=RROW]");
|
const row = elem.domNode.closest("div[id*=RROW]");
|
||||||
|
|
||||||
// do not allow unchecking active article checkbox
|
// do not allow unchecking active article checkbox
|
||||||
if (row.hasClassName("active")) {
|
if (row.hasClassName("active")) {
|
||||||
@@ -1039,7 +1123,7 @@ const Headlines = {
|
|||||||
if (start == stop)
|
if (start == stop)
|
||||||
return [start];
|
return [start];
|
||||||
|
|
||||||
const rows = $$("#headlines-frame > div[id*=RROW]");
|
const rows = App.findAll("#headlines-frame > div[id*=RROW]");
|
||||||
const results = [];
|
const results = [];
|
||||||
let collecting = false;
|
let collecting = false;
|
||||||
|
|
||||||
@@ -1066,7 +1150,7 @@ const Headlines = {
|
|||||||
// mode = all,none,unread,invert,marked,published
|
// mode = all,none,unread,invert,marked,published
|
||||||
let query = "#headlines-frame > div[id*=RROW]";
|
let query = "#headlines-frame > div[id*=RROW]";
|
||||||
|
|
||||||
if (articleId) query += "[data-article-id=" + articleId + "]";
|
if (articleId) query += `[data-article-id="${articleId}"]`;
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -1086,10 +1170,7 @@ const Headlines = {
|
|||||||
console.warn("select: unknown mode", mode);
|
console.warn("select: unknown mode", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = $$(query);
|
App.findAll(query).forEach((row) => {
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
const row = rows[i];
|
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -1101,7 +1182,7 @@ const Headlines = {
|
|||||||
default:
|
default:
|
||||||
row.addClassName("Selected");
|
row.addClassName("Selected");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
catchupSelection: function () {
|
catchupSelection: function () {
|
||||||
const rows = Headlines.getSelected();
|
const rows = Headlines.getSelected();
|
||||||
@@ -1140,7 +1221,7 @@ const Headlines = {
|
|||||||
if (!below) {
|
if (!below) {
|
||||||
for (let i = 0; i < visible_ids.length; i++) {
|
for (let i = 0; i < visible_ids.length; i++) {
|
||||||
if (visible_ids[i] != id) {
|
if (visible_ids[i] != id) {
|
||||||
const e = $("RROW-" + visible_ids[i]);
|
const e = App.byId(`RROW-${visible_ids[i]}`);
|
||||||
|
|
||||||
if (e && e.hasClassName("Unread")) {
|
if (e && e.hasClassName("Unread")) {
|
||||||
ids_to_mark.push(visible_ids[i]);
|
ids_to_mark.push(visible_ids[i]);
|
||||||
@@ -1152,7 +1233,7 @@ const Headlines = {
|
|||||||
} else {
|
} else {
|
||||||
for (let i = visible_ids.length - 1; i >= 0; i--) {
|
for (let i = visible_ids.length - 1; i >= 0; i--) {
|
||||||
if (visible_ids[i] != id) {
|
if (visible_ids[i] != id) {
|
||||||
const e = $("RROW-" + visible_ids[i]);
|
const e = App.byId(`RROW-${visible_ids[i]}`);
|
||||||
|
|
||||||
if (e && e.hasClassName("Unread")) {
|
if (e && e.hasClassName("Unread")) {
|
||||||
ids_to_mark.push(visible_ids[i]);
|
ids_to_mark.push(visible_ids[i]);
|
||||||
@@ -1171,26 +1252,40 @@ const Headlines = {
|
|||||||
if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
|
if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
|
||||||
|
|
||||||
for (let i = 0; i < ids_to_mark.length; i++) {
|
for (let i = 0; i < ids_to_mark.length; i++) {
|
||||||
const e = $("RROW-" + ids_to_mark[i]);
|
const e = App.byId(`RROW-${ids_to_mark[i]}`);
|
||||||
e.removeClassName("Unread");
|
e.removeClassName("Unread");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLabelsUpdated: function (transport) {
|
onTagsUpdated: function (data) {
|
||||||
const data = JSON.parse(transport.responseText);
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
data['info-for-headlines'].each(function (elem) {
|
if (this.headlines[data.id]) {
|
||||||
$$(".HLLCTR-" + elem.id).each(function (ctr) {
|
this.headlines[data.id].tags = data.tags;
|
||||||
ctr.innerHTML = elem.labels;
|
}
|
||||||
|
|
||||||
|
App.findAll(`span[data-tags-for="${data.id}"`).forEach((ctr) => {
|
||||||
|
ctr.innerHTML = Article.renderTags(data.id, data.tags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: maybe this should cause article to be rendered again, although it might cause flicker etc
|
||||||
|
onLabelsUpdated: function (data) {
|
||||||
|
if (data) {
|
||||||
|
data["labels-for"].forEach((row) => {
|
||||||
|
if (this.headlines[row.id]) {
|
||||||
|
this.headlines[row.id].labels = row.labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
App.findAll(`span[data-labels-for="${row.id}"]`).forEach((ctr) => {
|
||||||
|
ctr.innerHTML = Article.renderLabels(row.id, row.labels);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollToArticleId: function (id) {
|
scrollToArticleId: function (id) {
|
||||||
const container = $("headlines-frame");
|
const container = App.byId("headlines-frame");
|
||||||
const row = $("RROW-" + id);
|
const row = App.byId(`RROW-${id}`);
|
||||||
|
|
||||||
if (!container || !row) return;
|
if (!container || !row) return;
|
||||||
|
|
||||||
@@ -1289,7 +1384,7 @@ const Headlines = {
|
|||||||
const labelAddMenu = new dijit.Menu({ownerMenu: menu});
|
const labelAddMenu = new dijit.Menu({ownerMenu: menu});
|
||||||
const labelDelMenu = new dijit.Menu({ownerMenu: menu});
|
const labelDelMenu = new dijit.Menu({ownerMenu: menu});
|
||||||
|
|
||||||
labels.each(function (label) {
|
labels.forEach(function (label) {
|
||||||
const bare_id = label.id;
|
const bare_id = label.id;
|
||||||
const name = label.caption;
|
const name = label.caption;
|
||||||
|
|
||||||
@@ -1337,10 +1432,10 @@ const Headlines = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollByPages: function (page_offset) {
|
scrollByPages: function (page_offset) {
|
||||||
App.Scrollable.scrollByPages($("headlines-frame"), page_offset);
|
App.Scrollable.scrollByPages(App.byId("headlines-frame"), page_offset);
|
||||||
},
|
},
|
||||||
scroll: function (offset) {
|
scroll: function (offset) {
|
||||||
App.Scrollable.scroll($("headlines-frame"), offset);
|
App.Scrollable.scroll(App.byId("headlines-frame"), offset);
|
||||||
},
|
},
|
||||||
initHeadlinesMenu: function () {
|
initHeadlinesMenu: function () {
|
||||||
if (!dijit.byId("headlinesMenu")) {
|
if (!dijit.byId("headlinesMenu")) {
|
||||||
|
|||||||
@@ -1,9 +1,44 @@
|
|||||||
/* eslint-disable prefer-rest-params */
|
/* eslint-disable prefer-rest-params */
|
||||||
/* global __, lib, dijit, define, dojo, CommonDialogs, Notify, Tables, xhrPost, fox, App */
|
/* global __, lib, dijit, define, dojo, CommonDialogs, Notify, Tables, xhrPost, xhr, fox, App */
|
||||||
|
|
||||||
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], function (declare, domConstruct) {
|
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_base/array", "dojo/cookie"],
|
||||||
|
function (declare, domConstruct, checkBoxTree, array, cookie) {
|
||||||
|
|
||||||
return declare("fox.PrefFeedTree", lib.CheckBoxTree, {
|
return declare("fox.PrefFeedTree", lib.CheckBoxTree, {
|
||||||
|
// save state in localStorage instead of cookies
|
||||||
|
// reference: https://stackoverflow.com/a/27968996
|
||||||
|
_saveExpandedNodes: function(){
|
||||||
|
if (this.persist && this.cookieName){
|
||||||
|
const ary = [];
|
||||||
|
for (const id in this._openedNodes){
|
||||||
|
ary.push(id);
|
||||||
|
}
|
||||||
|
// Was:
|
||||||
|
// cookie(this.cookieName, ary.join(","), {expires: 365});
|
||||||
|
localStorage.setItem(this.cookieName, ary.join(","));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_initState: function(){
|
||||||
|
this.cookieName = 'prefs:' + this.cookieName;
|
||||||
|
// summary:
|
||||||
|
// Load in which nodes should be opened automatically
|
||||||
|
this._openedNodes = {};
|
||||||
|
if (this.persist && this.cookieName){
|
||||||
|
// Was:
|
||||||
|
// var oreo = cookie(this.cookieName);
|
||||||
|
let oreo = localStorage.getItem(this.cookieName);
|
||||||
|
// migrate old data if nothing in localStorage
|
||||||
|
if (oreo == null || oreo === '') {
|
||||||
|
oreo = cookie(this.cookieName);
|
||||||
|
cookie(this.cookieName, null, { expires: -1 });
|
||||||
|
}
|
||||||
|
if (oreo){
|
||||||
|
array.forEach(oreo.split(','), function(item){
|
||||||
|
this._openedNodes[item] = true;
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
_createTreeNode: function(args) {
|
_createTreeNode: function(args) {
|
||||||
const tnode = this.inherited(arguments);
|
const tnode = this.inherited(arguments);
|
||||||
|
|
||||||
@@ -91,11 +126,11 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
return (!item || this.model.store.getValue(item, 'type') == 'category') ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "feed-icon";
|
return (!item || this.model.store.getValue(item, 'type') == 'category') ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "feed-icon";
|
||||||
},
|
},
|
||||||
reload: function() {
|
reload: function() {
|
||||||
const searchElem = $("feed_search");
|
const searchElem = App.byId("feed_search");
|
||||||
const search = (searchElem) ? searchElem.value : "";
|
const search = (searchElem) ? searchElem.value : "";
|
||||||
|
|
||||||
xhrPost("backend.php", { op: "pref-feeds", search: search }, (transport) => {
|
xhr.post("backend.php", { op: "pref-feeds", search: search }, (reply) => {
|
||||||
dijit.byId('feedsTab').attr('content', transport.responseText);
|
dijit.byId('feedsTab').attr('content', reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -129,14 +164,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
resetFeedOrder: function() {
|
resetFeedOrder: function() {
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => {
|
xhr.post("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetCatOrder: function() {
|
resetCatOrder: function() {
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => {
|
xhr.post("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -144,7 +179,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
if (confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name))) {
|
if (confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name))) {
|
||||||
Notify.progress("Removing category...");
|
Notify.progress("Removing category...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "removeCat", ids: id}, () => {
|
xhr.post("backend.php", {op: "pref-feeds", method: "removeCat", ids: id}, () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
@@ -163,7 +198,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -174,9 +209,16 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
checkErrorFeeds: function() {
|
||||||
|
xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
|
||||||
|
if (reply.length > 0) {
|
||||||
|
Element.show(dijit.byId("pref_feeds_errors_btn").domNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
checkInactiveFeeds: function() {
|
checkInactiveFeeds: function() {
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "getinactivefeeds"}, (transport) => {
|
xhr.json("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (reply) => {
|
||||||
if (parseInt(transport.responseText) > 0) {
|
if (reply.length > 0) {
|
||||||
Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
|
Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -186,7 +228,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
const items = tree.model.getCheckedItems();
|
const items = tree.model.getCheckedItems();
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
items.each(function (item) {
|
items.forEach(function (item) {
|
||||||
if (item.id[0].match("CAT:"))
|
if (item.id[0].match("CAT:"))
|
||||||
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
||||||
});
|
});
|
||||||
@@ -205,7 +247,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -220,7 +262,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
const items = tree.model.getCheckedItems();
|
const items = tree.model.getCheckedItems();
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
items.each(function (item) {
|
items.forEach(function (item) {
|
||||||
if (item.id[0].match("FEED:"))
|
if (item.id[0].match("FEED:"))
|
||||||
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
||||||
});
|
});
|
||||||
@@ -253,16 +295,15 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
|
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "editfeeds", ids: rows.toString()}, (transport) => {
|
xhr.post("backend.php", {op: "pref-feeds", method: "editfeeds", ids: rows.toString()}, (reply) => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "feedEditDlg",
|
|
||||||
title: __("Edit Multiple Feeds"),
|
title: __("Edit Multiple Feeds"),
|
||||||
getChildByName: function (name) {
|
/*getChildByName: function (name) {
|
||||||
let rv = null;
|
let rv = null;
|
||||||
this.getChildren().each(
|
this.getChildren().forEach(
|
||||||
function (child) {
|
function (child) {
|
||||||
if (child.name == name) {
|
if (child.name == name) {
|
||||||
rv = child;
|
rv = child;
|
||||||
@@ -270,16 +311,22 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return rv;
|
return rv;
|
||||||
},
|
},*/
|
||||||
toggleField: function (checkbox, elem, label) {
|
toggleField: function (checkbox) {
|
||||||
this.getChildByName(elem).attr('disabled', !checkbox.checked);
|
const name = checkbox.attr("data-control-for");
|
||||||
|
const target = dijit.getEnclosingWidget(dialog.domNode.querySelector(`input[name="${name}"]`));
|
||||||
|
|
||||||
if ($(label))
|
target.attr('disabled', !checkbox.attr('checked'));
|
||||||
if (checkbox.checked)
|
console.log(target, target.attr('type'));
|
||||||
$(label).removeClassName('text-muted');
|
|
||||||
|
if (target.attr('type') == "checkbox") {
|
||||||
|
const label = checkbox.domNode.closest("label");
|
||||||
|
|
||||||
|
if (checkbox.attr('checked'))
|
||||||
|
label.removeClassName('text-muted');
|
||||||
else
|
else
|
||||||
$(label).addClassName('text-muted');
|
label.addClassName('text-muted');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
execute: function () {
|
execute: function () {
|
||||||
if (this.validate() && confirm(__("Save changes to selected feeds?"))) {
|
if (this.validate() && confirm(__("Save changes to selected feeds?"))) {
|
||||||
@@ -287,7 +334,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
|
|
||||||
/* normalize unchecked checkboxes because [] is not serialized */
|
/* normalize unchecked checkboxes because [] is not serialized */
|
||||||
|
|
||||||
Object.keys(query).each((key) => {
|
Object.keys(query).forEach((key) => {
|
||||||
const val = query[key];
|
const val = query[key];
|
||||||
|
|
||||||
if (typeof val == "object" && val.length == 0)
|
if (typeof val == "object" && val.length == 0)
|
||||||
@@ -296,7 +343,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
|
|
||||||
Notify.progress("Saving data...", true);
|
Notify.progress("Saving data...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
|
||||||
const tree = dijit.byId("feedTree");
|
const tree = dijit.byId("feedTree");
|
||||||
@@ -305,7 +352,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: transport.responseText
|
content: reply
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -325,7 +372,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
|
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
|
xhr.post("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -336,13 +383,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
if (title) {
|
if (title) {
|
||||||
Notify.progress("Creating category...");
|
Notify.progress("Creating category...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "addCat", cat: title}, () => {
|
xhr.post("backend.php", {op: "pref-feeds", method: "addCat", cat: title}, () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
batchSubscribe: function() {
|
batchSubscribe: function() {
|
||||||
|
xhr.json("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (reply) => {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "batchSubDlg",
|
id: "batchSubDlg",
|
||||||
title: __("Batch subscribe"),
|
title: __("Batch subscribe"),
|
||||||
@@ -350,7 +398,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
Notify.progress(__("Subscribing to feeds..."), true);
|
Notify.progress(__("Subscribing to feeds..."), true);
|
||||||
|
|
||||||
xhrPost("backend.php", this.attr('value'), () => {
|
xhr.post("backend.php", this.attr('value'), () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
|
|
||||||
const tree = dijit.byId("feedTree");
|
const tree = dijit.byId("feedTree");
|
||||||
@@ -360,20 +408,63 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: __("Loading, please wait...")
|
content: `
|
||||||
});
|
<form onsubmit='return false'>
|
||||||
|
${App.FormFields.hidden_tag("op", "pref-feeds")}
|
||||||
|
${App.FormFields.hidden_tag("method", "batchaddfeeds")}
|
||||||
|
|
||||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
<header class='horizontal'>
|
||||||
dojo.disconnect(tmph);
|
${__("One valid feed per line (no detection is done)")}
|
||||||
|
</header>
|
||||||
|
|
||||||
xhrPost("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (transport) => {
|
<section>
|
||||||
dialog.attr('content', transport.responseText);
|
<textarea style='font-size : 12px; width : 98%; height: 200px;'
|
||||||
})
|
dojoType='fox.form.ValidationTextArea' required='1' name='feeds'></textarea>
|
||||||
|
|
||||||
|
${reply.enable_cats ?
|
||||||
|
`<fieldset>
|
||||||
|
<label>${__('Place in category:')}</label>
|
||||||
|
${reply.cat_select}
|
||||||
|
</fieldset>
|
||||||
|
` : ''
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id='feedDlg_loginContainer' style='display : none'>
|
||||||
|
<header>${__("Authentication")}</header>
|
||||||
|
<section>
|
||||||
|
<input dojoType='dijit.form.TextBox' name='login' placeHolder="${__("Login")}">
|
||||||
|
<input placeHolder="${__("Password")}" dojoType="dijit.form.TextBox" type='password'
|
||||||
|
autocomplete='new-password' name='pass'></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class='narrow'>
|
||||||
|
<label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox'
|
||||||
|
onclick='App.displayIfChecked(this, "feedDlg_loginContainer")'>
|
||||||
|
${__('Feeds require authentication.')}
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).execute()' type='submit' class='alt-primary'>
|
||||||
|
${__('Subscribe')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
|
||||||
|
${__('Cancel')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
||||||
|
});
|
||||||
},
|
},
|
||||||
showInactiveFeeds: function() {
|
showInactiveFeeds: function() {
|
||||||
|
xhr.json("backend.php", {op: 'pref-feeds', method: 'inactivefeeds'}, function (reply) {
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "inactiveFeedsDlg",
|
id: "inactiveFeedsDlg",
|
||||||
title: __("Feeds without recent updates"),
|
title: __("Feeds without recent updates"),
|
||||||
@@ -392,7 +483,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
|
|
||||||
const tree = dijit.byId("feedTree");
|
const tree = dijit.byId("feedTree");
|
||||||
@@ -406,18 +497,53 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
alert(__("No feeds selected."));
|
alert(__("No feeds selected."));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: __("Loading, please wait...")
|
content: `
|
||||||
});
|
<div dojoType='fox.Toolbar'>
|
||||||
|
<div dojoType='fox.form.DropDownButton'>
|
||||||
|
<span>${__('Select')}</span>
|
||||||
|
<div dojoType='dijit.Menu' style='display: none'>
|
||||||
|
<div onclick="Tables.select('inactive-feeds-list', true)"
|
||||||
|
dojoType='dijit.MenuItem'>${__('All')}</div>
|
||||||
|
<div onclick="Tables.select('inactive-feeds-list', false)"
|
||||||
|
dojoType='dijit.MenuItem'>${__('None')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
<div class='panel panel-scrollable'>
|
||||||
dojo.disconnect(tmph);
|
<table width='100%' id='inactive-feeds-list'>
|
||||||
|
${reply.map((row) => `<tr data-row-id='${row.id}'>
|
||||||
|
<td width='5%' align='center'>
|
||||||
|
<input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href='#' "title="${__("Click to edit feed")}" onclick="CommonDialogs.editFeed(${row.id})">
|
||||||
|
${App.escapeHtml(row.title)}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='text-muted' align='right'>
|
||||||
|
${row.last_article}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join("")}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (transport) => {
|
<footer>
|
||||||
dialog.attr('content', transport.responseText);
|
<button style='float : left' class='alt-danger' dojoType='dijit.form.Button' onclick='App.dialogOf(this).removeSelected()'>
|
||||||
})
|
${__('Unsubscribe from selected feeds')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
|
||||||
|
${__('Close this window')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable prefer-rest-params */
|
/* eslint-disable prefer-rest-params */
|
||||||
/* global __, define, lib, dijit, dojo, xhrPost, Notify */
|
/* global __, define, lib, dijit, dojo, xhr, App, Notify */
|
||||||
|
|
||||||
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], function (declare, domConstruct) {
|
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], function (declare, domConstruct) {
|
||||||
|
|
||||||
@@ -80,26 +80,26 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
const items = tree.model.getCheckedItems();
|
const items = tree.model.getCheckedItems();
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
items.each(function (item) {
|
items.forEach(function (item) {
|
||||||
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
||||||
});
|
});
|
||||||
|
|
||||||
return rv;
|
return rv;
|
||||||
},
|
},
|
||||||
reload: function() {
|
reload: function() {
|
||||||
const user_search = $("filter_search");
|
const user_search = App.byId("filter_search");
|
||||||
let search = "";
|
let search = "";
|
||||||
if (user_search) { search = user_search.value; }
|
if (user_search) { search = user_search.value; }
|
||||||
|
|
||||||
xhrPost("backend.php", { op: "pref-filters", search: search }, (transport) => {
|
xhr.post("backend.php", { op: "pref-filters", search: search }, (reply) => {
|
||||||
dijit.byId('filtersTab').attr('content', transport.responseText);
|
dijit.byId('filtersTab').attr('content', reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetFilterOrder: function() {
|
resetFilterOrder: function() {
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => {
|
xhr.post("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -114,28 +114,11 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
if (confirm(__("Combine selected filters?"))) {
|
if (confirm(__("Combine selected filters?"))) {
|
||||||
Notify.progress("Joining filters...");
|
Notify.progress("Joining filters...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-filters", method: "join", ids: rows.toString()}, () => {
|
xhr.post("backend.php", {op: "pref-filters", method: "join", ids: rows.toString()}, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editSelectedFilter: function() {
|
|
||||||
const rows = this.getSelectedFilters();
|
|
||||||
|
|
||||||
if (rows.length == 0) {
|
|
||||||
alert(__("No filters selected."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.length > 1) {
|
|
||||||
alert(__("Please select only one filter."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Notify.close();
|
|
||||||
|
|
||||||
this.editFilter(rows[0]);
|
|
||||||
},
|
|
||||||
removeSelectedFilters: function() {
|
removeSelectedFilters: function() {
|
||||||
const sel_rows = this.getSelectedFilters();
|
const sel_rows = this.getSelectedFilters();
|
||||||
|
|
||||||
@@ -148,7 +131,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
/* global __, dijit, dojo, Tables, xhrPost, Notify, xhrJson, App, fox, Effect */
|
/* global __, dijit, dojo, Tables, xhrPost, Notify, xhr, App, fox */
|
||||||
|
|
||||||
const Helpers = {
|
const Helpers = {
|
||||||
AppPasswords: {
|
AppPasswords: {
|
||||||
@@ -9,7 +9,7 @@ const Helpers = {
|
|||||||
return Tables.getSelected("app-password-list");
|
return Tables.getSelected("app-password-list");
|
||||||
},
|
},
|
||||||
updateContent: function(data) {
|
updateContent: function(data) {
|
||||||
$("app_passwords_holder").innerHTML = data;
|
App.byId("app_passwords_holder").innerHTML = data;
|
||||||
dojo.parser.parse("app_passwords_holder");
|
dojo.parser.parse("app_passwords_holder");
|
||||||
},
|
},
|
||||||
removeSelected: function() {
|
removeSelected: function() {
|
||||||
@@ -19,8 +19,8 @@ const Helpers = {
|
|||||||
alert("No passwords selected.");
|
alert("No passwords selected.");
|
||||||
} else if (confirm(__("Remove selected app passwords?"))) {
|
} else if (confirm(__("Remove selected app passwords?"))) {
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (transport) => {
|
xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (reply) => {
|
||||||
this.updateContent(transport.responseText);
|
this.updateContent(reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ const Helpers = {
|
|||||||
const title = prompt("Password description:")
|
const title = prompt("Password description:")
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
xhrPost("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (transport) => {
|
xhr.post("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (reply) => {
|
||||||
this.updateContent(transport.responseText);
|
this.updateContent(reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,17 +40,22 @@ const Helpers = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Feeds: {
|
||||||
clearFeedAccessKeys: function() {
|
clearFeedAccessKeys: function() {
|
||||||
if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
|
if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
|
||||||
Notify.progress("Clearing URLs...");
|
Notify.progress("Clearing URLs...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => {
|
xhr.post("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => {
|
||||||
Notify.info("Generated URLs cleared.");
|
Notify.info("Generated URLs cleared.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
System: {
|
||||||
|
//
|
||||||
|
},
|
||||||
EventLog: {
|
EventLog: {
|
||||||
log_page: 0,
|
log_page: 0,
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
@@ -58,8 +63,13 @@ const Helpers = {
|
|||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
update: function() {
|
update: function() {
|
||||||
xhrPost("backend.php", { op: "pref-system", severity: dijit.byId("severity").attr('value'), page: Helpers.EventLog.log_page }, (transport) => {
|
xhr.post("backend.php", {
|
||||||
dijit.byId('systemTab').attr('content', transport.responseText);
|
op: "pref-system",
|
||||||
|
severity: dijit.byId("severity").attr('value'),
|
||||||
|
page: Helpers.EventLog.log_page
|
||||||
|
}, (reply) => {
|
||||||
|
|
||||||
|
dijit.byId('systemTab').attr('content', reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -77,13 +87,14 @@ const Helpers = {
|
|||||||
|
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-system", method: "clearLog"}, () => {
|
xhr.post("backend.php", {op: "pref-system", method: "clearLog"}, () => {
|
||||||
Helpers.EventLog.refresh();
|
Helpers.EventLog.refresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
editProfiles: function() {
|
Profiles: {
|
||||||
|
edit: function() {
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "profileEditDlg",
|
id: "profileEditDlg",
|
||||||
title: __("Settings Profiles"),
|
title: __("Settings Profiles"),
|
||||||
@@ -98,11 +109,11 @@ const Helpers = {
|
|||||||
Notify.progress("Removing selected profiles...", true);
|
Notify.progress("Removing selected profiles...", true);
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
op: "rpc", method: "remprofiles",
|
op: "pref-prefs", method: "remprofiles",
|
||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
dialog.refresh();
|
dialog.refresh();
|
||||||
});
|
});
|
||||||
@@ -116,9 +127,9 @@ const Helpers = {
|
|||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
Notify.progress("Creating profile...", true);
|
Notify.progress("Creating profile...", true);
|
||||||
|
|
||||||
const query = {op: "rpc", method: "addprofile", title: dialog.attr('value').newprofile};
|
const query = {op: "pref-prefs", method: "addprofile", title: dialog.attr('value').newprofile};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
dialog.refresh();
|
dialog.refresh();
|
||||||
});
|
});
|
||||||
@@ -126,8 +137,59 @@ const Helpers = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
xhrPost("backend.php", {op: 'pref-prefs', method: 'editPrefProfiles'}, (transport) => {
|
xhr.json("backend.php", {op: 'pref-prefs', method: 'getprofiles'}, (reply) => {
|
||||||
dialog.attr('content', transport.responseText);
|
dialog.attr('content', `
|
||||||
|
<div dojoType='fox.Toolbar'>
|
||||||
|
<div dojoType='fox.form.DropDownButton'>
|
||||||
|
<span>${__('Select')}</span>
|
||||||
|
<div dojoType='dijit.Menu' style='display: none'>
|
||||||
|
<div onclick="Tables.select('pref-profiles-list', true)"
|
||||||
|
dojoType='dijit.MenuItem'>${__('All')}</div>
|
||||||
|
<div onclick="Tables.select('pref-profiles-list', false)"
|
||||||
|
dojoType='dijit.MenuItem'>${__('None')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
<input name='newprofile' dojoType='dijit.form.ValidationTextBox' required='1'>
|
||||||
|
${App.FormFields.button_tag(__('Create profile'), "", {onclick: 'App.dialogOf(this).addProfile()'})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onsubmit='return false'>
|
||||||
|
<div class='panel panel-scrollable'>
|
||||||
|
<table width='100%' id='pref-profiles-list'>
|
||||||
|
${reply.map((profile) => `
|
||||||
|
<tr data-row-id="${profile.id}">
|
||||||
|
<td width='5%'>
|
||||||
|
${App.FormFields.checkbox_tag("", false, "", {onclick: 'Tables.onRowChecked(this)'})}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${profile.id > 0 ?
|
||||||
|
`<span dojoType='dijit.InlineEditBox' width='300px' autoSave='false'
|
||||||
|
profile-id='${profile.id}'>${profile.title}
|
||||||
|
<script type='dojo/method' event='onChange' args='value'>
|
||||||
|
xhr.post("backend.php",
|
||||||
|
{op: 'pref-prefs', method: 'saveprofile', value: value, id: this.attr('profile-id')}, () => {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</span>` : `${profile.title}`}
|
||||||
|
${profile.active ? __("(active)") : ""}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join("")}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
${App.FormFields.button_tag(__('Remove selected profiles'), "",
|
||||||
|
{class: 'pull-left alt-danger', onclick: 'App.dialogOf(this).removeSelected()'})}
|
||||||
|
${App.FormFields.submit_tag(__('Activate profile'), {onclick: 'App.dialogOf(this).execute()'})}
|
||||||
|
${App.FormFields.cancel_dialog_tag(__('Cancel'))}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
execute: function () {
|
execute: function () {
|
||||||
@@ -137,7 +199,7 @@ const Helpers = {
|
|||||||
if (confirm(__("Activate selected profile?"))) {
|
if (confirm(__("Activate selected profile?"))) {
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "rpc", method: "setprofile", id: sel_rows.toString()}, () => {
|
xhr.post("backend.php", {op: "pref-prefs", method: "activateprofile", id: sel_rows.toString()}, () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,21 +214,23 @@ const Helpers = {
|
|||||||
dialog.refresh();
|
dialog.refresh();
|
||||||
dialog.show();
|
dialog.show();
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
Prefs: {
|
||||||
customizeCSS: function() {
|
customizeCSS: function() {
|
||||||
xhrJson("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
|
xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
title: __("Customize stylesheet"),
|
title: __("Customize stylesheet"),
|
||||||
apply: function() {
|
apply: function() {
|
||||||
xhrPost("backend.php", this.attr('value'), () => {
|
xhr.post("backend.php", this.attr('value'), () => {
|
||||||
new Effect.Appear("css_edit_apply_msg");
|
Element.show("css_edit_apply_msg");
|
||||||
$("user_css_style").innerText = this.attr('value');
|
App.byId("user_css_style").innerText = this.attr('value');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
execute: function () {
|
execute: function () {
|
||||||
Notify.progress('Saving data...', true);
|
Notify.progress('Saving data...', true);
|
||||||
|
|
||||||
xhrPost("backend.php", this.attr('value'), () => {
|
xhr.post("backend.php", this.attr('value'), () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -175,9 +239,9 @@ const Helpers = {
|
|||||||
${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
|
${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${App.FormFields.hidden('op', 'rpc')}
|
${App.FormFields.hidden_tag('op', 'rpc')}
|
||||||
${App.FormFields.hidden('method', 'setpref')}
|
${App.FormFields.hidden_tag('method', 'setpref')}
|
||||||
${App.FormFields.hidden('key', 'USER_STYLESHEET')}
|
${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
|
||||||
|
|
||||||
<div id='css_edit_apply_msg' style='display : none'>
|
<div id='css_edit_apply_msg' style='display : none'>
|
||||||
<div class='alert alert-warning'>
|
<div class='alert alert-warning'>
|
||||||
@@ -208,9 +272,9 @@ const Helpers = {
|
|||||||
},
|
},
|
||||||
confirmReset: function() {
|
confirmReset: function() {
|
||||||
if (confirm(__("Reset to defaults?"))) {
|
if (confirm(__("Reset to defaults?"))) {
|
||||||
xhrPost("backend.php", {op: "pref-prefs", method: "resetconfig"}, (transport) => {
|
xhr.post("backend.php", {op: "pref-prefs", method: "resetconfig"}, (reply) => {
|
||||||
Helpers.refresh();
|
Helpers.Prefs.refresh();
|
||||||
Notify.info(transport.responseText);
|
Notify.info(reply);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -218,20 +282,21 @@ const Helpers = {
|
|||||||
if (confirm(__("Clear stored data for this plugin?"))) {
|
if (confirm(__("Clear stored data for this plugin?"))) {
|
||||||
Notify.progress("Loading, please wait...");
|
Notify.progress("Loading, please wait...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => {
|
xhr.post("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => {
|
||||||
Helpers.refresh();
|
Helpers.Prefs.refresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
xhrPost("backend.php", { op: "pref-prefs" }, (transport) => {
|
xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
|
||||||
dijit.byId('prefsTab').attr('content', transport.responseText);
|
dijit.byId('prefsTab').attr('content', reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
},
|
||||||
OPML: {
|
OPML: {
|
||||||
import: function() {
|
import: function() {
|
||||||
const opml_file = $("opml_file");
|
const opml_file = App.byId("opml_file");
|
||||||
|
|
||||||
if (opml_file.value.length == 0) {
|
if (opml_file.value.length == 0) {
|
||||||
alert(__("Please choose an OPML file first."));
|
alert(__("Please choose an OPML file first."));
|
||||||
@@ -273,7 +338,7 @@ const Helpers = {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.send(new FormData($("opml_import_form")));
|
xhr.send(new FormData(App.byId("opml_import_form")));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -282,20 +347,25 @@ const Helpers = {
|
|||||||
console.log("export");
|
console.log("export");
|
||||||
window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm"));
|
window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm"));
|
||||||
},
|
},
|
||||||
changeKey: function() {
|
publish: function() {
|
||||||
|
Notify.progress("Loading, please wait...", true);
|
||||||
|
|
||||||
|
xhr.json("backend.php", {op: "pref-feeds", method: "getOPMLKey"}, (reply) => {
|
||||||
|
try {
|
||||||
|
const dialog = new fox.SingleUseDialog({
|
||||||
|
title: __("Public OPML URL"),
|
||||||
|
regenOPMLKey: function() {
|
||||||
if (confirm(__("Replace current OPML publishing address with a new one?"))) {
|
if (confirm(__("Replace current OPML publishing address with a new one?"))) {
|
||||||
Notify.progress("Trying to change address...", true);
|
Notify.progress("Trying to change address...", true);
|
||||||
|
|
||||||
xhrJson("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => {
|
xhr.json("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => {
|
||||||
if (reply) {
|
if (reply) {
|
||||||
const new_link = reply.link;
|
const new_link = reply.link;
|
||||||
const e = $('pub_opml_url');
|
const target = this.domNode.querySelector('.generated_url');
|
||||||
|
|
||||||
if (new_link) {
|
if (new_link && target) {
|
||||||
e.href = new_link;
|
target.href = new_link;
|
||||||
e.innerHTML = new_link;
|
target.innerHTML = new_link;
|
||||||
|
|
||||||
new Effect.Highlight(e);
|
|
||||||
|
|
||||||
Notify.close();
|
Notify.close();
|
||||||
|
|
||||||
@@ -307,5 +377,32 @@ const Helpers = {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
content: `
|
||||||
|
<header>${__("Your Public OPML URL is:")}</header>
|
||||||
|
<section>
|
||||||
|
<div class='panel text-center'>
|
||||||
|
<a class='generated_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${App.escapeHtml(reply.link)}</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer class='text-center'>
|
||||||
|
<button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenOPMLKey()">
|
||||||
|
${__('Generate new URL')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
|
||||||
|
${__('Close this window')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
Notify.close();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
App.Error.report(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable prefer-rest-params */
|
/* eslint-disable prefer-rest-params */
|
||||||
/* global __, define, lib, dijit, dojo, xhrPost, Notify, fox */
|
/* global __, define, lib, dijit, dojo, xhr, Notify, fox, App */
|
||||||
|
|
||||||
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/form/DropDownButton"], function (declare, domConstruct) {
|
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/form/DropDownButton"], function (declare, domConstruct) {
|
||||||
|
|
||||||
@@ -48,19 +48,24 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||||||
const items = tree.model.getCheckedItems();
|
const items = tree.model.getCheckedItems();
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
items.each(function(item) {
|
items.forEach(function(item) {
|
||||||
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
rv.push(tree.model.store.getValue(item, 'bare_id'));
|
||||||
});
|
});
|
||||||
|
|
||||||
return rv;
|
return rv;
|
||||||
},
|
},
|
||||||
reload: function() {
|
reload: function() {
|
||||||
xhrPost("backend.php", { op: "pref-labels" }, (transport) => {
|
xhr.post("backend.php", { op: "pref-labels" }, (reply) => {
|
||||||
dijit.byId('labelsTab').attr('content', transport.responseText);
|
dijit.byId('labelsTab').attr('content', reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
editLabel: function(id) {
|
editLabel: function(id) {
|
||||||
|
xhr.json("backend.php", {op: "pref-labels", method: "edit", id: id}, (reply) => {
|
||||||
|
|
||||||
|
const fg_color = reply['fg_color'];
|
||||||
|
const bg_color = reply['bg_color'] ? reply['bg_color'] : '#fff7d5';
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "labelEditDlg",
|
id: "labelEditDlg",
|
||||||
title: __("Label Editor"),
|
title: __("Label Editor"),
|
||||||
@@ -80,7 +85,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||||||
color = bg;
|
color = bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
const e = $("icon-label-" + id);
|
const e = App.byId(`icon-label-${id}`);
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
if (bg) e.style.color = bg;
|
if (bg) e.style.color = bg;
|
||||||
@@ -91,7 +96,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||||||
ids: id, fg: fg, bg: bg, color: color
|
ids: id, fg: fg, bg: bg, color: color
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
const tree = dijit.byId("filterTree");
|
const tree = dijit.byId("filterTree");
|
||||||
if (tree) tree.reload(); // maybe there's labels in there
|
if (tree) tree.reload(); // maybe there's labels in there
|
||||||
});
|
});
|
||||||
@@ -107,24 +112,76 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||||||
this.setLabelColor(id, fg_color, bg_color);
|
this.setLabelColor(id, fg_color, bg_color);
|
||||||
this.hide();
|
this.hide();
|
||||||
|
|
||||||
xhrPost("backend.php", this.attr('value'), () => {
|
xhr.post("backend.php", this.attr('value'), () => {
|
||||||
const tree = dijit.byId("filterTree");
|
const tree = dijit.byId("filterTree");
|
||||||
if (tree) tree.reload(); // maybe there's labels in there
|
if (tree) tree.reload(); // maybe there's labels in there
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: __("Loading, please wait...")
|
content: `
|
||||||
});
|
<form onsubmit='return false'>
|
||||||
|
|
||||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
<header>${__("Caption")}</header>
|
||||||
dojo.disconnect(tmph);
|
<section>
|
||||||
|
<input style='font-size : 16px; color : ${fg_color}; background : ${bg_color}; transition : background 0.1s linear'
|
||||||
|
id='labelEdit_caption'
|
||||||
|
name='caption'
|
||||||
|
dojoType='dijit.form.ValidationTextBox'
|
||||||
|
required='true'
|
||||||
|
value="${App.escapeHtml(reply.caption)}">
|
||||||
|
</section>
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-labels", method: "edit", id: id}, (transport) => {
|
${App.FormFields.hidden_tag('id', id)}
|
||||||
dialog.attr('content', transport.responseText);
|
${App.FormFields.hidden_tag('op', 'pref-labels')}
|
||||||
})
|
${App.FormFields.hidden_tag('method', 'save')}
|
||||||
|
|
||||||
|
${App.FormFields.hidden_tag('fg_color', fg_color, {}, 'labelEdit_fgColor')}
|
||||||
|
${App.FormFields.hidden_tag('bg_color', bg_color, {}, 'labelEdit_bgColor')}
|
||||||
|
|
||||||
|
<header>${__("Colors")}</header>
|
||||||
|
<section>
|
||||||
|
<table width='100%'>
|
||||||
|
<tr>
|
||||||
|
<th>${__("Foreground:")}</th>
|
||||||
|
<th>${__("Background:")}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class='text-center'>
|
||||||
|
<div dojoType='dijit.ColorPalette'>
|
||||||
|
<script type='dojo/method' event='onChange' args='fg_color'>
|
||||||
|
dijit.byId('labelEdit_fgColor').attr('value', fg_color);
|
||||||
|
dijit.byId('labelEdit_caption').domNode.setStyle({color: fg_color});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<div dojoType='dijit.ColorPalette'>
|
||||||
|
<script type='dojo/method' event='onChange' args='bg_color'>
|
||||||
|
dijit.byId('labelEdit_bgColor').attr('value', bg_color);
|
||||||
|
dijit.byId('labelEdit_caption').domNode.setStyle({backgroundColor: bg_color});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'>
|
||||||
|
${__('Save')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
|
||||||
|
${__('Cancel')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
||||||
|
});
|
||||||
},
|
},
|
||||||
resetColors: function() {
|
resetColors: function() {
|
||||||
const labels = this.getSelectedLabels();
|
const labels = this.getSelectedLabels();
|
||||||
@@ -137,7 +194,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||||||
ids: labels.toString()
|
ids: labels.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,7 +215,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
115
js/PrefUsers.js
115
js/PrefUsers.js
@@ -1,15 +1,15 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/* global __ */
|
/* global __ */
|
||||||
/* global xhrPost, dojo, dijit, Notify, Tables, fox */
|
/* global xhrPost, xhr, dijit, Notify, Tables, App, fox */
|
||||||
|
|
||||||
const Users = {
|
const Users = {
|
||||||
reload: function(sort) {
|
reload: function(sort) {
|
||||||
const user_search = $("user_search");
|
const user_search = App.byId("user_search");
|
||||||
const search = user_search ? user_search.value : "";
|
const search = user_search ? user_search.value : "";
|
||||||
|
|
||||||
xhrPost("backend.php", { op: "pref-users", sort: sort, search: search }, (transport) => {
|
xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => {
|
||||||
dijit.byId('usersTab').attr('content', transport.responseText);
|
dijit.byId('usersTab').attr('content', reply);
|
||||||
Notify.close();
|
Notify.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -19,15 +19,18 @@ const Users = {
|
|||||||
if (login) {
|
if (login) {
|
||||||
Notify.progress("Adding user...");
|
Notify.progress("Adding user...");
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-users", method: "add", login: login}, (transport) => {
|
xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => {
|
||||||
alert(transport.responseText);
|
alert(reply);
|
||||||
Users.reload();
|
Users.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
edit: function(id) {
|
edit: function(id) {
|
||||||
xhrPost('backend.php', {op: 'pref-users', method: 'edit', id: id}, (transport) => {
|
xhr.json('backend.php', {op: 'pref-users', method: 'edit', id: id}, (reply) => {
|
||||||
|
const user = reply.user;
|
||||||
|
const admin_disabled = (user.id == 1);
|
||||||
|
|
||||||
const dialog = new fox.SingleUseDialog({
|
const dialog = new fox.SingleUseDialog({
|
||||||
id: "userEditDlg",
|
id: "userEditDlg",
|
||||||
title: __("User Editor"),
|
title: __("User Editor"),
|
||||||
@@ -35,13 +38,86 @@ const Users = {
|
|||||||
if (this.validate()) {
|
if (this.validate()) {
|
||||||
Notify.progress("Saving data...", true);
|
Notify.progress("Saving data...", true);
|
||||||
|
|
||||||
xhrPost("backend.php", dojo.formToObject("user_edit_form"), (/* transport */) => {
|
xhr.post("backend.php", this.attr('value'), () => {
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
Users.reload();
|
Users.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: transport.responseText
|
content: `
|
||||||
|
<form onsubmit='return false'>
|
||||||
|
|
||||||
|
${App.FormFields.hidden_tag('id', user.id.toString())}
|
||||||
|
${App.FormFields.hidden_tag('op', 'pref-users')}
|
||||||
|
${App.FormFields.hidden_tag('method', 'editSave')}
|
||||||
|
|
||||||
|
<div dojoType="dijit.layout.TabContainer" style="height : 400px">
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('Edit user')}">
|
||||||
|
|
||||||
|
<header>${__("User")}</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("Login:")}</label>
|
||||||
|
<input style='font-size : 16px'
|
||||||
|
${admin_disabled ? "disabled='1'" : ''}
|
||||||
|
dojoType='dijit.form.ValidationTextBox' required='1'
|
||||||
|
name='login' value="${App.escapeHtml(user.login)}">
|
||||||
|
|
||||||
|
${admin_disabled ? App.FormFields.hidden_tag("login", user.login) : ''}
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<header>${__("Authentication")}</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__('Access level: ')}</label>
|
||||||
|
${App.FormFields.select_hash("access_level",
|
||||||
|
user.access_level, reply.access_level_names, {disabled: admin_disabled.toString()})}
|
||||||
|
|
||||||
|
${admin_disabled ? App.FormFields.hidden_tag("access_level",
|
||||||
|
user.access_level.toString()) : ''}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("New password:")}</label>
|
||||||
|
<input dojoType='dijit.form.TextBox' type='password' size='20'
|
||||||
|
placeholder='${__("Change password")}' name='password'>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<header>${__("Options")}</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("E-mail:")}</label>
|
||||||
|
<input dojoType='dijit.form.TextBox' size='30' name='email'
|
||||||
|
value="${App.escapeHtml(user.email)}">
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div dojoType="dijit.layout.ContentPane" title="${__('User details')}">
|
||||||
|
<script type='dojo/method' event='onShow' args='evt'>
|
||||||
|
if (this.domNode.querySelector('.loading')) {
|
||||||
|
xhr.post("backend.php", {op: 'pref-users', method: 'userdetails', id: ${user.id}}, (reply) => {
|
||||||
|
this.attr('content', reply);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<span class='loading'>${__("Loading, please wait...")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'>
|
||||||
|
${__('Save')}
|
||||||
|
</button>
|
||||||
|
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
|
||||||
|
${__('Cancel')}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -65,9 +141,9 @@ const Users = {
|
|||||||
|
|
||||||
const id = rows[0];
|
const id = rows[0];
|
||||||
|
|
||||||
xhrPost("backend.php", {op: "pref-users", method: "resetPass", id: id}, (transport) => {
|
xhr.post("backend.php", {op: "pref-users", method: "resetPass", id: id}, (reply) => {
|
||||||
Notify.close();
|
Notify.close();
|
||||||
Notify.info(transport.responseText, true);
|
Notify.info(reply, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -84,7 +160,7 @@ const Users = {
|
|||||||
ids: sel_rows.toString()
|
ids: sel_rows.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrPost("backend.php", query, () => {
|
xhr.post("backend.php", query, () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -93,21 +169,6 @@ const Users = {
|
|||||||
alert(__("No users selected."));
|
alert(__("No users selected."));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editSelected: function() {
|
|
||||||
const rows = this.getSelection();
|
|
||||||
|
|
||||||
if (rows.length == 0) {
|
|
||||||
alert(__("No users selected."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.length > 1) {
|
|
||||||
alert(__("Please select one user."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.edit(rows[0]);
|
|
||||||
},
|
|
||||||
getSelection :function() {
|
getSelection :function() {
|
||||||
return Tables.getSelected("users-list");
|
return Tables.getSelected("users-list");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
|
/* eslint-disable prefer-rest-params */
|
||||||
/* global dijit, define */
|
/* global dijit, define */
|
||||||
define(["dojo/_base/declare", "dijit/Dialog"], function (declare) {
|
define(["dojo/_base/declare", "dijit/Dialog"], function (declare) {
|
||||||
return declare("fox.SingleUseDialog", dijit.Dialog, {
|
return declare("fox.SingleUseDialog", dijit.Dialog, {
|
||||||
|
create: function(params) {
|
||||||
|
const extant = dijit.byId(params.id);
|
||||||
|
|
||||||
|
if (extant) {
|
||||||
|
console.warn('SingleUseDialog: destroying existing widget:', params.id, '=', extant)
|
||||||
|
extant.destroyRecursive();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.inherited(arguments);
|
||||||
|
},
|
||||||
onHide: function() {
|
onHide: function() {
|
||||||
this.destroyRecursive();
|
this.destroyRecursive();
|
||||||
}
|
}
|
||||||
|
|||||||
277
js/common.js
277
js/common.js
@@ -1,60 +1,225 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* global dijit, __, App, Ajax */
|
/* global dijit, __, App, dojo, __csrf_token */
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
|
|
||||||
/* error reporting shim */
|
/* exported $ */
|
||||||
// TODO: deprecated; remove
|
function $(id) {
|
||||||
/* function exception_error(e, e_compat, filename, lineno, colno) {
|
console.warn("FIXME: please use App.byId() or document.getElementById() instead of $():", id);
|
||||||
if (typeof e == "string")
|
return document.getElementById(id);
|
||||||
e = e_compat;
|
|
||||||
|
|
||||||
App.Error.report(e, {filename: filename, lineno: lineno, colno: colno});
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* xhr shorthand helpers */
|
|
||||||
|
|
||||||
/* exported xhrPost */
|
|
||||||
function xhrPost(url, params, complete) {
|
|
||||||
console.log("xhrPost:", params);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
new Ajax.Request(url, {
|
|
||||||
parameters: params,
|
|
||||||
onComplete: function(reply) {
|
|
||||||
if (complete != undefined) complete(reply);
|
|
||||||
|
|
||||||
resolve(reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* exported $$ */
|
||||||
|
function $$(query) {
|
||||||
|
console.warn("FIXME: please use App.findAll() or document.querySelectorAll() instead of $$():", query);
|
||||||
|
return document.querySelectorAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Element.prototype.hasClassName = function(className) {
|
||||||
|
return this.classList.contains(className);
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.addClassName = function(className) {
|
||||||
|
return this.classList.add(className);
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.removeClassName = function(className) {
|
||||||
|
return this.classList.remove(className);
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.toggleClassName = function(className) {
|
||||||
|
if (this.hasClassName(className))
|
||||||
|
return this.removeClassName(className);
|
||||||
|
else
|
||||||
|
return this.addClassName(className);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Element.prototype.setStyle = function(args) {
|
||||||
|
Object.keys(args).forEach((k) => {
|
||||||
|
this.style[k] = args[k];
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
Element.prototype.show = function() {
|
||||||
|
this.style.display = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.hide = function() {
|
||||||
|
this.style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.toggle = function() {
|
||||||
|
if (this.visible())
|
||||||
|
this.hide();
|
||||||
|
else
|
||||||
|
this.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://gist.github.com/alirezas/c4f9f43e9fe1abba9a4824dd6fc60a55
|
||||||
|
Element.prototype.fadeOut = function() {
|
||||||
|
this.style.opacity = 1;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
(function fade() {
|
||||||
|
if ((self.style.opacity -= 0.1) < 0) {
|
||||||
|
self.style.display = "none";
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(fade);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.fadeIn = function(display = undefined){
|
||||||
|
this.style.opacity = 0;
|
||||||
|
this.style.display = display == undefined ? "block" : display;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
(function fade() {
|
||||||
|
let val = parseFloat(self.style.opacity);
|
||||||
|
if (!((val += 0.1) > 1)) {
|
||||||
|
self.style.opacity = val;
|
||||||
|
requestAnimationFrame(fade);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
};
|
||||||
|
|
||||||
|
Element.prototype.visible = function() {
|
||||||
|
return this.style.display != "none" && this.offsetHeight != 0 && this.offsetWidth != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* exported xhrJson */
|
Element.visible = function(elem) {
|
||||||
function xhrJson(url, params, complete) {
|
if (typeof elem == "string")
|
||||||
return new Promise((resolve, reject) =>
|
elem = document.getElementById(elem);
|
||||||
xhrPost(url, params).then((reply) => {
|
|
||||||
let obj = null;
|
|
||||||
|
|
||||||
try {
|
return elem.visible();
|
||||||
obj = JSON.parse(reply.responseText);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("xhrJson", e, reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (complete != undefined) complete(obj);
|
Element.show = function(elem) {
|
||||||
|
if (typeof elem == "string")
|
||||||
|
elem = document.getElementById(elem);
|
||||||
|
|
||||||
resolve(obj);
|
return elem.show();
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
Element.hide = function(elem) {
|
||||||
|
if (typeof elem == "string")
|
||||||
|
elem = document.getElementById(elem);
|
||||||
|
|
||||||
|
return elem.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
Element.toggle = function(elem) {
|
||||||
|
if (typeof elem == "string")
|
||||||
|
elem = document.getElementById(elem);
|
||||||
|
|
||||||
|
return elem.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Element.hasClassName = function (elem, className) {
|
||||||
|
if (typeof elem == "string")
|
||||||
|
elem = document.getElementById(elem);
|
||||||
|
|
||||||
|
return elem.hasClassName(className);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* add method to remove element from array */
|
|
||||||
Array.prototype.remove = function(s) {
|
Array.prototype.remove = function(s) {
|
||||||
for (let i=0; i < this.length; i++) {
|
for (let i=0; i < this.length; i++) {
|
||||||
if (s == this[i]) this.splice(i, 1);
|
if (s == this[i]) this.splice(i, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Array.prototype.uniq = function() {
|
||||||
|
return this.filter((v, i, a) => a.indexOf(v) === i);
|
||||||
|
};
|
||||||
|
|
||||||
|
String.prototype.stripTags = function() {
|
||||||
|
return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?(\/)?>|<\/\w+>/gi, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* exported xhr */
|
||||||
|
const xhr = {
|
||||||
|
post: function(url, params = {}, complete = undefined) {
|
||||||
|
console.log('xhr.post', '>>>', params);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof __csrf_token != "undefined")
|
||||||
|
params = {...params, ...{csrf_token: __csrf_token}};
|
||||||
|
|
||||||
|
dojo.xhrPost({url: url,
|
||||||
|
postData: dojo.objectToQuery(params),
|
||||||
|
handleAs: "text",
|
||||||
|
error: function(error) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
load: function(data, ioargs) {
|
||||||
|
console.log('xhr.post', '<<<', ioargs.xhr);
|
||||||
|
|
||||||
|
if (complete != undefined)
|
||||||
|
complete(data, ioargs.xhr);
|
||||||
|
|
||||||
|
resolve(data)
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
json: function(url, params = {}, complete = undefined) {
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
this.post(url, params).then((data) => {
|
||||||
|
let obj = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("xhr.json", e, xhr);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('xhr.json', '<<<', obj);
|
||||||
|
|
||||||
|
if (obj && typeof App != "undefined")
|
||||||
|
if (!App.handleRpcJson(obj)) {
|
||||||
|
reject(obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complete != undefined) complete(obj);
|
||||||
|
|
||||||
|
resolve(obj);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* exported xhrPost */
|
||||||
|
function xhrPost(url, params = {}, complete = undefined) {
|
||||||
|
console.log("xhrPost:", params);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof __csrf_token != "undefined")
|
||||||
|
params = {...params, ...{csrf_token: __csrf_token}};
|
||||||
|
|
||||||
|
dojo.xhrPost({url: url,
|
||||||
|
postData: dojo.objectToQuery(params),
|
||||||
|
handleAs: "text",
|
||||||
|
error: function(error) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
load: function(data, ioargs) {
|
||||||
|
if (complete != undefined)
|
||||||
|
complete(ioargs.xhr);
|
||||||
|
|
||||||
|
resolve(ioargs.xhr)
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* exported xhrJson */
|
||||||
|
function xhrJson(url, params = {}, complete = undefined) {
|
||||||
|
return xhr.json(url, params, complete);
|
||||||
|
}
|
||||||
|
|
||||||
/* common helpers not worthy of separate Dojo modules */
|
/* common helpers not worthy of separate Dojo modules */
|
||||||
|
|
||||||
/* exported Lists */
|
/* exported Lists */
|
||||||
@@ -64,14 +229,14 @@ const Lists = {
|
|||||||
// account for dojo checkboxes
|
// account for dojo checkboxes
|
||||||
elem = elem.domNode || elem;
|
elem = elem.domNode || elem;
|
||||||
|
|
||||||
const row = elem.up("li");
|
const row = elem.closest("li");
|
||||||
|
|
||||||
if (row)
|
if (row)
|
||||||
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
|
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
|
||||||
},
|
},
|
||||||
select: function(elemId, selected) {
|
select: function(elemId, selected) {
|
||||||
$(elemId).select("li").each((row) => {
|
$(elemId).querySelectorAll("li").forEach((row) => {
|
||||||
const checkNode = row.select(".dijitCheckBox,input[type=checkbox]")[0];
|
const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]");
|
||||||
if (checkNode) {
|
if (checkNode) {
|
||||||
const widget = dijit.getEnclosingWidget(checkNode);
|
const widget = dijit.getEnclosingWidget(checkNode);
|
||||||
|
|
||||||
@@ -94,15 +259,15 @@ const Tables = {
|
|||||||
const checked = elem.domNode ? elem.attr("checked") : elem.checked;
|
const checked = elem.domNode ? elem.attr("checked") : elem.checked;
|
||||||
elem = elem.domNode || elem;
|
elem = elem.domNode || elem;
|
||||||
|
|
||||||
const row = elem.up("tr");
|
const row = elem.closest("tr");
|
||||||
|
|
||||||
if (row)
|
if (row)
|
||||||
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
|
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
|
||||||
|
|
||||||
},
|
},
|
||||||
select: function(elemId, selected) {
|
select: function(elemId, selected) {
|
||||||
$(elemId).select("tr").each((row) => {
|
$(elemId).querySelectorAll("tr").forEach((row) => {
|
||||||
const checkNode = row.select(".dijitCheckBox,input[type=checkbox]")[0];
|
const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]");
|
||||||
if (checkNode) {
|
if (checkNode) {
|
||||||
const widget = dijit.getEnclosingWidget(checkNode);
|
const widget = dijit.getEnclosingWidget(checkNode);
|
||||||
|
|
||||||
@@ -119,7 +284,7 @@ const Tables = {
|
|||||||
getSelected: function(elemId) {
|
getSelected: function(elemId) {
|
||||||
const rv = [];
|
const rv = [];
|
||||||
|
|
||||||
$(elemId).select("tr").each((row) => {
|
$(elemId).querySelectorAll("tr").forEach((row) => {
|
||||||
if (row.hasClassName("Selected")) {
|
if (row.hasClassName("Selected")) {
|
||||||
// either older prefix-XXX notation or separate attribute
|
// either older prefix-XXX notation or separate attribute
|
||||||
const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, "");
|
const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, "");
|
||||||
@@ -173,7 +338,7 @@ const Notify = {
|
|||||||
kind = kind || this.KIND_GENERIC;
|
kind = kind || this.KIND_GENERIC;
|
||||||
keep = keep || false;
|
keep = keep || false;
|
||||||
|
|
||||||
const notify = $("notify");
|
const notify = App.byId("notify");
|
||||||
|
|
||||||
window.clearTimeout(this.timeout);
|
window.clearTimeout(this.timeout);
|
||||||
|
|
||||||
@@ -238,25 +403,3 @@ const Notify = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac
|
|
||||||
/* exported getSelectionText */
|
|
||||||
function getSelectionText() {
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
if (typeof window.getSelection != "undefined") {
|
|
||||||
const sel = window.getSelection();
|
|
||||||
if (sel.rangeCount) {
|
|
||||||
const container = document.createElement("div");
|
|
||||||
for (let i = 0, len = sel.rangeCount; i < len; ++i) {
|
|
||||||
container.appendChild(sel.getRangeAt(i).cloneContents());
|
|
||||||
}
|
|
||||||
text = container.innerHTML;
|
|
||||||
}
|
|
||||||
} else if (typeof document.selection != "undefined") {
|
|
||||||
if (document.selection.type == "Text") {
|
|
||||||
text = document.selection.createRange().textText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.stripTags();
|
|
||||||
}
|
|
||||||
|
|||||||
20
js/form/ValidationMultiSelect.js
Normal file
20
js/form/ValidationMultiSelect.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* global define */
|
||||||
|
|
||||||
|
// only supports required for the time being
|
||||||
|
// TODO: maybe show dojo native error message? i dunno
|
||||||
|
define(["dojo/_base/declare", "dojo/_base/lang", "dijit/form/MultiSelect", ],
|
||||||
|
function(declare, lang, MultiSelect) {
|
||||||
|
|
||||||
|
return declare('fox.form.ValidationMultiSelect', [MultiSelect], {
|
||||||
|
constructor: function(params){
|
||||||
|
this.constraints = {};
|
||||||
|
this.baseClass += ' dijitValidationMultiSelect';
|
||||||
|
},
|
||||||
|
validate: function(/*Boolean*/ isFocused){
|
||||||
|
if (this.required && this.attr('value').length == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -41,6 +41,7 @@ require(["dojo/_base/kernel",
|
|||||||
"dojo/data/ItemFileWriteStore",
|
"dojo/data/ItemFileWriteStore",
|
||||||
"lib/CheckBoxStoreModel",
|
"lib/CheckBoxStoreModel",
|
||||||
"lib/CheckBoxTree",
|
"lib/CheckBoxTree",
|
||||||
|
"fox/PluginHost",
|
||||||
"fox/CommonDialogs",
|
"fox/CommonDialogs",
|
||||||
"fox/CommonFilters",
|
"fox/CommonFilters",
|
||||||
"fox/PrefUsers",
|
"fox/PrefUsers",
|
||||||
@@ -52,6 +53,7 @@ require(["dojo/_base/kernel",
|
|||||||
"fox/PrefLabelTree",
|
"fox/PrefLabelTree",
|
||||||
"fox/Toolbar",
|
"fox/Toolbar",
|
||||||
"fox/SingleUseDialog",
|
"fox/SingleUseDialog",
|
||||||
|
"fox/form/ValidationMultiSelect",
|
||||||
"fox/form/ValidationTextArea",
|
"fox/form/ValidationTextArea",
|
||||||
"fox/form/Select",
|
"fox/form/Select",
|
||||||
"fox/form/ComboButton",
|
"fox/form/ComboButton",
|
||||||
|
|||||||
13
js/tt-rss.js
13
js/tt-rss.js
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/* global require, App, $H */
|
/* global require, App, dojo */
|
||||||
|
|
||||||
/* exported Plugins */
|
/* exported Plugins */
|
||||||
const Plugins = {};
|
const Plugins = {};
|
||||||
@@ -51,6 +51,7 @@ require(["dojo/_base/kernel",
|
|||||||
"fox/FeedTree",
|
"fox/FeedTree",
|
||||||
"fox/Toolbar",
|
"fox/Toolbar",
|
||||||
"fox/SingleUseDialog",
|
"fox/SingleUseDialog",
|
||||||
|
"fox/form/ValidationMultiSelect",
|
||||||
"fox/form/ValidationTextArea",
|
"fox/form/ValidationTextArea",
|
||||||
"fox/form/Select",
|
"fox/form/Select",
|
||||||
"fox/form/ComboButton",
|
"fox/form/ComboButton",
|
||||||
@@ -70,13 +71,13 @@ require(["dojo/_base/kernel",
|
|||||||
|
|
||||||
/* exported hash_get */
|
/* exported hash_get */
|
||||||
function hash_get(key) {
|
function hash_get(key) {
|
||||||
const kv = window.location.hash.substring(1).toQueryParams();
|
const obj = dojo.queryToObject(window.location.hash.substring(1));
|
||||||
return kv[key];
|
return obj[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* exported hash_set */
|
/* exported hash_set */
|
||||||
function hash_set(key, value) {
|
function hash_set(key, value) {
|
||||||
const kv = window.location.hash.substring(1).toQueryParams();
|
const obj = dojo.queryToObject(window.location.hash.substring(1));
|
||||||
kv[key] = value;
|
obj[key] = value;
|
||||||
window.location.hash = $H(kv).toQueryString();
|
window.location.hash = dojo.objectToQuery(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/* TODO: this should probably be something like night_mode.js since it does nothing specific to utility scripts */
|
/* TODO: this should probably be something like night_mode.js since it does nothing specific to utility scripts */
|
||||||
|
|
||||||
Event.observe(window, "load", function() {
|
window.addEventListener("load", function() {
|
||||||
const UtilityJS = {
|
const UtilityJS = {
|
||||||
apply_night_mode: function (is_night, link) {
|
apply_night_mode: function (is_night, link) {
|
||||||
console.log("night mode changed to", is_night);
|
console.log("night mode changed to", is_night);
|
||||||
@@ -16,10 +16,10 @@ Event.observe(window, "load", function() {
|
|||||||
setup_night_mode: function() {
|
setup_night_mode: function() {
|
||||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
const link = new Element("link", {
|
const link = document.createElement("link");
|
||||||
rel: "stylesheet",
|
|
||||||
id: "theme_auto_css"
|
link.rel = "stylesheet";
|
||||||
});
|
link.id = "theme_auto_css";
|
||||||
|
|
||||||
link.onload = function() {
|
link.onload = function() {
|
||||||
document.querySelector("body").removeClassName("css_loading");
|
document.querySelector("body").removeClassName("css_loading");
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* accept-to-gettext.inc -- convert information in 'Accept-*' headers to
|
|
||||||
* gettext language identifiers.
|
|
||||||
* Copyright (c) 2003, Wouter Verhelst <wouter@debian.org>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* $locale=al2gt(<array of supported languages/charsets in gettext syntax>,
|
|
||||||
* <MIME type of document>);
|
|
||||||
* setlocale('LC_ALL', $locale); // or 'LC_MESSAGES', or whatever...
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* $langs=array('nl_BE.ISO-8859-15','nl_BE.UTF-8','en_US.UTF-8','en_GB.UTF-8');
|
|
||||||
* $locale=al2gt($langs, 'text/html');
|
|
||||||
* setlocale('LC_ALL', $locale);
|
|
||||||
*
|
|
||||||
* Note that this will send out header information (to be
|
|
||||||
* RFC2616-compliant), so it must be called before anything is sent to
|
|
||||||
* the user.
|
|
||||||
*
|
|
||||||
* Assumptions made:
|
|
||||||
* * Charset encodings are written the same way as the Accept-Charset
|
|
||||||
* HTTP header specifies them (RFC2616), except that they're parsed
|
|
||||||
* case-insensitive.
|
|
||||||
* * Country codes and language codes are the same in both gettext and
|
|
||||||
* the Accept-Language syntax (except for the case differences, which
|
|
||||||
* are dealt with easily). If not, some input may be ignored.
|
|
||||||
* * The provided gettext-strings are fully qualified; i.e., no "en_US";
|
|
||||||
* always "en_US.ISO-8859-15" or "en_US.UTF-8", or whichever has been
|
|
||||||
* used. "en.ISO-8859-15" is OK, though.
|
|
||||||
* * The language is more important than the charset; i.e., if the
|
|
||||||
* following is given:
|
|
||||||
*
|
|
||||||
* Accept-Language: nl-be, nl;q=0.8, en-us;q=0.5, en;q=0.3
|
|
||||||
* Accept-Charset: ISO-8859-15, utf-8;q=0.5
|
|
||||||
*
|
|
||||||
* And the supplied parameter contains (amongst others) nl_BE.UTF-8
|
|
||||||
* and nl.ISO-8859-15, then nl_BE.UTF-8 will be picked.
|
|
||||||
*
|
|
||||||
* $Log: accept-to-gettext.inc,v $
|
|
||||||
* Revision 1.1.1.1 2003/11/19 19:31:15 wouter
|
|
||||||
* * moved to new CVS repo after death of the old
|
|
||||||
* * Fixed code to apply a default to both Accept-Charset and
|
|
||||||
* Accept-Language if none of those headers are supplied; patch from
|
|
||||||
* Dominic Chambers <dominic@encasa.com>
|
|
||||||
*
|
|
||||||
* Revision 1.2 2003/08/14 10:23:59 wouter
|
|
||||||
* Removed little error in Content-Type header syntaxis.
|
|
||||||
*
|
|
||||||
* 2007-04-01
|
|
||||||
* add '@' before use of arrays, to avoid PHP warnings.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* not really important, this one; perhaps I could've put it inline with
|
|
||||||
* the rest. */
|
|
||||||
function find_match($curlscore,$curcscore,$curgtlang,$langval,$charval,
|
|
||||||
$gtlang)
|
|
||||||
{
|
|
||||||
if($curlscore < $langval) {
|
|
||||||
$curlscore=$langval;
|
|
||||||
$curcscore=$charval;
|
|
||||||
$curgtlang=$gtlang;
|
|
||||||
} else if ($curlscore == $langval) {
|
|
||||||
if($curcscore < $charval) {
|
|
||||||
$curcscore=$charval;
|
|
||||||
$curgtlang=$gtlang;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array($curlscore, $curcscore, $curgtlang);
|
|
||||||
}
|
|
||||||
|
|
||||||
function al2gt($gettextlangs, $mime) {
|
|
||||||
/* default to "everything is acceptable", as RFC2616 specifies */
|
|
||||||
$acceptLang=(($_SERVER["HTTP_ACCEPT_LANGUAGE"] == '') ? '*' :
|
|
||||||
$_SERVER["HTTP_ACCEPT_LANGUAGE"]);
|
|
||||||
$acceptChar=(($_SERVER["HTTP_ACCEPT_CHARSET"] == '') ? '*' :
|
|
||||||
$_SERVER["HTTP_ACCEPT_CHARSET"]);
|
|
||||||
$alparts=@preg_split("/,/",$acceptLang);
|
|
||||||
$acparts=@preg_split("/,/",$acceptChar);
|
|
||||||
|
|
||||||
/* Parse the contents of the Accept-Language header.*/
|
|
||||||
foreach($alparts as $part) {
|
|
||||||
$part=trim($part);
|
|
||||||
if(preg_match("/;/", $part)) {
|
|
||||||
$lang=@preg_split("/;/",$part);
|
|
||||||
$score=@preg_split("/=/",$lang[1]);
|
|
||||||
$alscores[$lang[0]]=$score[1];
|
|
||||||
} else {
|
|
||||||
$alscores[$part]=1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Do the same for the Accept-Charset header. */
|
|
||||||
|
|
||||||
/* RFC2616: ``If no "*" is present in an Accept-Charset field, then
|
|
||||||
* all character sets not explicitly mentioned get a quality value of
|
|
||||||
* 0, except for ISO-8859-1, which gets a quality value of 1 if not
|
|
||||||
* explicitly mentioned.''
|
|
||||||
*
|
|
||||||
* Making it 2 for the time being, so that we
|
|
||||||
* can distinguish between "not specified" and "specified as 1" later
|
|
||||||
* on. */
|
|
||||||
$acscores["ISO-8859-1"]=2;
|
|
||||||
|
|
||||||
foreach($acparts as $part) {
|
|
||||||
$part=trim($part);
|
|
||||||
if(preg_match("/;/", $part)) {
|
|
||||||
$cs=@preg_split("/;/",$part);
|
|
||||||
$score=@preg_split("/=/",$cs[1]);
|
|
||||||
$acscores[strtoupper($cs[0])]=$score[1];
|
|
||||||
} else {
|
|
||||||
$acscores[strtoupper($part)]=1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if($acscores["ISO-8859-1"]==2) {
|
|
||||||
$acscores["ISO-8859-1"]=(isset($acscores["*"])?$acscores["*"]:1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Loop through the available languages/encodings, and pick the one
|
|
||||||
* with the highest score, excluding the ones with a charset the user
|
|
||||||
* did not include.
|
|
||||||
*/
|
|
||||||
$curlscore=0;
|
|
||||||
$curcscore=0;
|
|
||||||
$curgtlang=NULL;
|
|
||||||
foreach($gettextlangs as $gtlang) {
|
|
||||||
|
|
||||||
$tmp1=preg_replace("/\_/","-",$gtlang);
|
|
||||||
$tmp2=@preg_split("/\./",$tmp1);
|
|
||||||
$allang=strtolower($tmp2[0]);
|
|
||||||
$gtcs=strtoupper($tmp2[1]);
|
|
||||||
$noct=@preg_split("/-/",$allang);
|
|
||||||
|
|
||||||
$testvals=array(
|
|
||||||
array(@$alscores[$allang], @$acscores[$gtcs]),
|
|
||||||
array(@$alscores[$noct[0]], @$acscores[$gtcs]),
|
|
||||||
array(@$alscores[$allang], @$acscores["*"]),
|
|
||||||
array(@$alscores[$noct[0]], @$acscores["*"]),
|
|
||||||
array(@$alscores["*"], @$acscores[$gtcs]),
|
|
||||||
array(@$alscores["*"], @$acscores["*"]));
|
|
||||||
|
|
||||||
$found=FALSE;
|
|
||||||
foreach($testvals as $tval) {
|
|
||||||
if(!$found && isset($tval[0]) && isset($tval[1])) {
|
|
||||||
$arr=find_match($curlscore, $curcscore, $curgtlang, $tval[0],
|
|
||||||
$tval[1], $gtlang);
|
|
||||||
$curlscore=$arr[0];
|
|
||||||
$curcscore=$arr[1];
|
|
||||||
$curgtlang=$arr[2];
|
|
||||||
$found=TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We must re-parse the gettext-string now, since we may have found it
|
|
||||||
* through a "*" qualifier.*/
|
|
||||||
|
|
||||||
$gtparts=@preg_split("/\./",$curgtlang);
|
|
||||||
$tmp=strtolower($gtparts[0]);
|
|
||||||
$lang=preg_replace("/\_/", "-", $tmp);
|
|
||||||
$charset=$gtparts[1];
|
|
||||||
|
|
||||||
header("Content-Language: $lang");
|
|
||||||
header("Content-Type: $mime; charset=$charset");
|
|
||||||
|
|
||||||
return $curgtlang;
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
@@ -10,6 +10,8 @@ class floIconIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class floIcon {
|
class floIcon {
|
||||||
|
public $images = array();
|
||||||
|
|
||||||
function readICO($file) {
|
function readICO($file) {
|
||||||
$jim = new jimIcon();
|
$jim = new jimIcon();
|
||||||
$icon = new floIconIcon();
|
$icon = new floIconIcon();
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ class jimIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// See if we can parse it (might be PNG format here)
|
// See if we can parse it (might be PNG format here)
|
||||||
$i = @imagecreatefromstring($data);
|
if (self::has_parsable_image_type($data)) {
|
||||||
|
if ($i = @imagecreatefromstring($data)) {
|
||||||
if ($i) {
|
|
||||||
imagesavealpha($i, true);
|
imagesavealpha($i, true);
|
||||||
return $i;
|
return $i;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Must be a BMP. Parse it ourselves.
|
// Must be a BMP. Parse it ourselves.
|
||||||
$img = imagecreatetruecolor($e["width"], $e["height"]);
|
$img = imagecreatetruecolor($e["width"], $e["height"]);
|
||||||
@@ -267,5 +267,12 @@ class jimIcon {
|
|||||||
}
|
}
|
||||||
return $img;
|
return $img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks whether the data is a type parsable by imagecreatefromstring()
|
||||||
|
private function has_parsable_image_type($image_data) {
|
||||||
|
$size = getimagesizefromstring($image_data);
|
||||||
|
return $size && in_array($size[2],
|
||||||
|
[IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_BMP, IMAGETYPE_WBMP, IMAGETYPE_WEBP]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|||||||
7590
lib/prototype.js
vendored
7590
lib/prototype.js
vendored
File diff suppressed because it is too large
Load Diff
965
lib/scriptaculous/controls.js
vendored
965
lib/scriptaculous/controls.js
vendored
@@ -1,965 +0,0 @@
|
|||||||
// script.aculo.us controls.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
|
|
||||||
|
|
||||||
// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
|
|
||||||
// (c) 2005-2010 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
|
|
||||||
// (c) 2005-2010 Jon Tirsen (http://www.tirsen.com)
|
|
||||||
// Contributors:
|
|
||||||
// Richard Livsey
|
|
||||||
// Rahul Bhargava
|
|
||||||
// Rob Wills
|
|
||||||
//
|
|
||||||
// script.aculo.us is freely distributable under the terms of an MIT-style license.
|
|
||||||
// For details, see the script.aculo.us web site: http://script.aculo.us/
|
|
||||||
|
|
||||||
// Autocompleter.Base handles all the autocompletion functionality
|
|
||||||
// that's independent of the data source for autocompletion. This
|
|
||||||
// includes drawing the autocompletion menu, observing keyboard
|
|
||||||
// and mouse events, and similar.
|
|
||||||
//
|
|
||||||
// Specific autocompleters need to provide, at the very least,
|
|
||||||
// a getUpdatedChoices function that will be invoked every time
|
|
||||||
// the text inside the monitored textbox changes. This method
|
|
||||||
// should get the text for which to provide autocompletion by
|
|
||||||
// invoking this.getToken(), NOT by directly accessing
|
|
||||||
// this.element.value. This is to allow incremental tokenized
|
|
||||||
// autocompletion. Specific auto-completion logic (AJAX, etc)
|
|
||||||
// belongs in getUpdatedChoices.
|
|
||||||
//
|
|
||||||
// Tokenized incremental autocompletion is enabled automatically
|
|
||||||
// when an autocompleter is instantiated with the 'tokens' option
|
|
||||||
// in the options parameter, e.g.:
|
|
||||||
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
|
|
||||||
// will incrementally autocomplete with a comma as the token.
|
|
||||||
// Additionally, ',' in the above example can be replaced with
|
|
||||||
// a token array, e.g. { tokens: [',', '\n'] } which
|
|
||||||
// enables autocompletion on multiple tokens. This is most
|
|
||||||
// useful when one of the tokens is \n (a newline), as it
|
|
||||||
// allows smart autocompletion after linebreaks.
|
|
||||||
|
|
||||||
if(typeof Effect == 'undefined')
|
|
||||||
throw("controls.js requires including script.aculo.us' effects.js library");
|
|
||||||
|
|
||||||
var Autocompleter = { };
|
|
||||||
Autocompleter.Base = Class.create({
|
|
||||||
baseInitialize: function(element, update, options) {
|
|
||||||
element = $(element);
|
|
||||||
this.element = element;
|
|
||||||
this.update = $(update);
|
|
||||||
this.hasFocus = false;
|
|
||||||
this.changed = false;
|
|
||||||
this.active = false;
|
|
||||||
this.index = 0;
|
|
||||||
this.entryCount = 0;
|
|
||||||
this.oldElementValue = this.element.value;
|
|
||||||
|
|
||||||
if(this.setOptions)
|
|
||||||
this.setOptions(options);
|
|
||||||
else
|
|
||||||
this.options = options || { };
|
|
||||||
|
|
||||||
this.options.paramName = this.options.paramName || this.element.name;
|
|
||||||
this.options.tokens = this.options.tokens || [];
|
|
||||||
this.options.frequency = this.options.frequency || 0.4;
|
|
||||||
this.options.minChars = this.options.minChars || 1;
|
|
||||||
this.options.onShow = this.options.onShow ||
|
|
||||||
function(element, update){
|
|
||||||
if(!update.style.position || update.style.position=='absolute') {
|
|
||||||
update.style.position = 'absolute';
|
|
||||||
Position.clone(element, update, {
|
|
||||||
setHeight: false,
|
|
||||||
offsetTop: element.offsetHeight
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Effect.Appear(update,{duration:0.15});
|
|
||||||
};
|
|
||||||
this.options.onHide = this.options.onHide ||
|
|
||||||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
|
|
||||||
|
|
||||||
if(typeof(this.options.tokens) == 'string')
|
|
||||||
this.options.tokens = new Array(this.options.tokens);
|
|
||||||
// Force carriage returns as token delimiters anyway
|
|
||||||
if (!this.options.tokens.include('\n'))
|
|
||||||
this.options.tokens.push('\n');
|
|
||||||
|
|
||||||
this.observer = null;
|
|
||||||
|
|
||||||
this.element.setAttribute('autocomplete','off');
|
|
||||||
|
|
||||||
Element.hide(this.update);
|
|
||||||
|
|
||||||
Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
|
|
||||||
Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
show: function() {
|
|
||||||
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
|
|
||||||
if(!this.iefix &&
|
|
||||||
(Prototype.Browser.IE) &&
|
|
||||||
(Element.getStyle(this.update, 'position')=='absolute')) {
|
|
||||||
new Insertion.After(this.update,
|
|
||||||
'<iframe id="' + this.update.id + '_iefix" '+
|
|
||||||
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
|
|
||||||
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
|
|
||||||
this.iefix = $(this.update.id+'_iefix');
|
|
||||||
}
|
|
||||||
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
|
|
||||||
},
|
|
||||||
|
|
||||||
fixIEOverlapping: function() {
|
|
||||||
Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
|
|
||||||
this.iefix.style.zIndex = 1;
|
|
||||||
this.update.style.zIndex = 2;
|
|
||||||
Element.show(this.iefix);
|
|
||||||
},
|
|
||||||
|
|
||||||
hide: function() {
|
|
||||||
this.stopIndicator();
|
|
||||||
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
|
|
||||||
if(this.iefix) Element.hide(this.iefix);
|
|
||||||
},
|
|
||||||
|
|
||||||
startIndicator: function() {
|
|
||||||
if(this.options.indicator) Element.show(this.options.indicator);
|
|
||||||
},
|
|
||||||
|
|
||||||
stopIndicator: function() {
|
|
||||||
if(this.options.indicator) Element.hide(this.options.indicator);
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyPress: function(event) {
|
|
||||||
if(this.active)
|
|
||||||
switch(event.keyCode) {
|
|
||||||
case Event.KEY_TAB:
|
|
||||||
case Event.KEY_RETURN:
|
|
||||||
this.selectEntry();
|
|
||||||
Event.stop(event);
|
|
||||||
case Event.KEY_ESC:
|
|
||||||
this.hide();
|
|
||||||
this.active = false;
|
|
||||||
Event.stop(event);
|
|
||||||
return;
|
|
||||||
case Event.KEY_LEFT:
|
|
||||||
case Event.KEY_RIGHT:
|
|
||||||
return;
|
|
||||||
case Event.KEY_UP:
|
|
||||||
this.markPrevious();
|
|
||||||
this.render();
|
|
||||||
Event.stop(event);
|
|
||||||
return;
|
|
||||||
case Event.KEY_DOWN:
|
|
||||||
this.markNext();
|
|
||||||
this.render();
|
|
||||||
Event.stop(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
|
|
||||||
(Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
|
|
||||||
|
|
||||||
this.changed = true;
|
|
||||||
this.hasFocus = true;
|
|
||||||
|
|
||||||
if(this.observer) clearTimeout(this.observer);
|
|
||||||
this.observer =
|
|
||||||
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
activate: function() {
|
|
||||||
this.changed = false;
|
|
||||||
this.hasFocus = true;
|
|
||||||
this.getUpdatedChoices();
|
|
||||||
},
|
|
||||||
|
|
||||||
onHover: function(event) {
|
|
||||||
var element = Event.findElement(event, 'LI');
|
|
||||||
if(this.index != element.autocompleteIndex)
|
|
||||||
{
|
|
||||||
this.index = element.autocompleteIndex;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
Event.stop(event);
|
|
||||||
},
|
|
||||||
|
|
||||||
onClick: function(event) {
|
|
||||||
var element = Event.findElement(event, 'LI');
|
|
||||||
this.index = element.autocompleteIndex;
|
|
||||||
this.selectEntry();
|
|
||||||
this.hide();
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlur: function(event) {
|
|
||||||
// needed to make click events working
|
|
||||||
setTimeout(this.hide.bind(this), 250);
|
|
||||||
this.hasFocus = false;
|
|
||||||
this.active = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
if(this.entryCount > 0) {
|
|
||||||
for (var i = 0; i < this.entryCount; i++)
|
|
||||||
this.index==i ?
|
|
||||||
Element.addClassName(this.getEntry(i),"selected") :
|
|
||||||
Element.removeClassName(this.getEntry(i),"selected");
|
|
||||||
if(this.hasFocus) {
|
|
||||||
this.show();
|
|
||||||
this.active = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.active = false;
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
markPrevious: function() {
|
|
||||||
if(this.index > 0) this.index--;
|
|
||||||
else this.index = this.entryCount-1;
|
|
||||||
this.getEntry(this.index).scrollIntoView(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
markNext: function() {
|
|
||||||
if(this.index < this.entryCount-1) this.index++;
|
|
||||||
else this.index = 0;
|
|
||||||
this.getEntry(this.index).scrollIntoView(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
getEntry: function(index) {
|
|
||||||
return this.update.firstChild.childNodes[index];
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentEntry: function() {
|
|
||||||
return this.getEntry(this.index);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectEntry: function() {
|
|
||||||
this.active = false;
|
|
||||||
this.updateElement(this.getCurrentEntry());
|
|
||||||
},
|
|
||||||
|
|
||||||
updateElement: function(selectedElement) {
|
|
||||||
if (this.options.updateElement) {
|
|
||||||
this.options.updateElement(selectedElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var value = '';
|
|
||||||
if (this.options.select) {
|
|
||||||
var nodes = $(selectedElement).select('.' + this.options.select) || [];
|
|
||||||
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
|
|
||||||
} else
|
|
||||||
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
|
|
||||||
|
|
||||||
var bounds = this.getTokenBounds();
|
|
||||||
if (bounds[0] != -1) {
|
|
||||||
var newValue = this.element.value.substr(0, bounds[0]);
|
|
||||||
var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
|
|
||||||
if (whitespace)
|
|
||||||
newValue += whitespace[0];
|
|
||||||
this.element.value = newValue + value + this.element.value.substr(bounds[1]);
|
|
||||||
} else {
|
|
||||||
this.element.value = value;
|
|
||||||
}
|
|
||||||
this.oldElementValue = this.element.value;
|
|
||||||
this.element.focus();
|
|
||||||
|
|
||||||
if (this.options.afterUpdateElement)
|
|
||||||
this.options.afterUpdateElement(this.element, selectedElement);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateChoices: function(choices) {
|
|
||||||
if(!this.changed && this.hasFocus) {
|
|
||||||
this.update.innerHTML = choices;
|
|
||||||
Element.cleanWhitespace(this.update);
|
|
||||||
Element.cleanWhitespace(this.update.down());
|
|
||||||
|
|
||||||
if(this.update.firstChild && this.update.down().childNodes) {
|
|
||||||
this.entryCount =
|
|
||||||
this.update.down().childNodes.length;
|
|
||||||
for (var i = 0; i < this.entryCount; i++) {
|
|
||||||
var entry = this.getEntry(i);
|
|
||||||
entry.autocompleteIndex = i;
|
|
||||||
this.addObservers(entry);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.entryCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stopIndicator();
|
|
||||||
this.index = 0;
|
|
||||||
|
|
||||||
if(this.entryCount==1 && this.options.autoSelect) {
|
|
||||||
this.selectEntry();
|
|
||||||
this.hide();
|
|
||||||
} else {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addObservers: function(element) {
|
|
||||||
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
|
|
||||||
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
onObserverEvent: function() {
|
|
||||||
this.changed = false;
|
|
||||||
this.tokenBounds = null;
|
|
||||||
if(this.getToken().length>=this.options.minChars) {
|
|
||||||
this.getUpdatedChoices();
|
|
||||||
} else {
|
|
||||||
this.active = false;
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
this.oldElementValue = this.element.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
getToken: function() {
|
|
||||||
var bounds = this.getTokenBounds();
|
|
||||||
return this.element.value.substring(bounds[0], bounds[1]).strip();
|
|
||||||
},
|
|
||||||
|
|
||||||
getTokenBounds: function() {
|
|
||||||
if (null != this.tokenBounds) return this.tokenBounds;
|
|
||||||
var value = this.element.value;
|
|
||||||
if (value.strip().empty()) return [-1, 0];
|
|
||||||
var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
|
|
||||||
var offset = (diff == this.oldElementValue.length ? 1 : 0);
|
|
||||||
var prevTokenPos = -1, nextTokenPos = value.length;
|
|
||||||
var tp;
|
|
||||||
for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
|
|
||||||
tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
|
|
||||||
if (tp > prevTokenPos) prevTokenPos = tp;
|
|
||||||
tp = value.indexOf(this.options.tokens[index], diff + offset);
|
|
||||||
if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
|
|
||||||
}
|
|
||||||
return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
|
|
||||||
var boundary = Math.min(newS.length, oldS.length);
|
|
||||||
for (var index = 0; index < boundary; ++index)
|
|
||||||
if (newS[index] != oldS[index])
|
|
||||||
return index;
|
|
||||||
return boundary;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ajax.Autocompleter = Class.create(Autocompleter.Base, {
|
|
||||||
initialize: function(element, update, url, options) {
|
|
||||||
this.baseInitialize(element, update, options);
|
|
||||||
this.options.asynchronous = true;
|
|
||||||
this.options.onComplete = this.onComplete.bind(this);
|
|
||||||
this.options.defaultParams = this.options.parameters || null;
|
|
||||||
this.url = url;
|
|
||||||
},
|
|
||||||
|
|
||||||
getUpdatedChoices: function() {
|
|
||||||
this.startIndicator();
|
|
||||||
|
|
||||||
var entry = encodeURIComponent(this.options.paramName) + '=' +
|
|
||||||
encodeURIComponent(this.getToken());
|
|
||||||
|
|
||||||
this.options.parameters = this.options.callback ?
|
|
||||||
this.options.callback(this.element, entry) : entry;
|
|
||||||
|
|
||||||
if(this.options.defaultParams)
|
|
||||||
this.options.parameters += '&' + this.options.defaultParams;
|
|
||||||
|
|
||||||
new Ajax.Request(this.url, this.options);
|
|
||||||
},
|
|
||||||
|
|
||||||
onComplete: function(request) {
|
|
||||||
this.updateChoices(request.responseText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// The local array autocompleter. Used when you'd prefer to
|
|
||||||
// inject an array of autocompletion options into the page, rather
|
|
||||||
// than sending out Ajax queries, which can be quite slow sometimes.
|
|
||||||
//
|
|
||||||
// The constructor takes four parameters. The first two are, as usual,
|
|
||||||
// the id of the monitored textbox, and id of the autocompletion menu.
|
|
||||||
// The third is the array you want to autocomplete from, and the fourth
|
|
||||||
// is the options block.
|
|
||||||
//
|
|
||||||
// Extra local autocompletion options:
|
|
||||||
// - choices - How many autocompletion choices to offer
|
|
||||||
//
|
|
||||||
// - partialSearch - If false, the autocompleter will match entered
|
|
||||||
// text only at the beginning of strings in the
|
|
||||||
// autocomplete array. Defaults to true, which will
|
|
||||||
// match text at the beginning of any *word* in the
|
|
||||||
// strings in the autocomplete array. If you want to
|
|
||||||
// search anywhere in the string, additionally set
|
|
||||||
// the option fullSearch to true (default: off).
|
|
||||||
//
|
|
||||||
// - fullSsearch - Search anywhere in autocomplete array strings.
|
|
||||||
//
|
|
||||||
// - partialChars - How many characters to enter before triggering
|
|
||||||
// a partial match (unlike minChars, which defines
|
|
||||||
// how many characters are required to do any match
|
|
||||||
// at all). Defaults to 2.
|
|
||||||
//
|
|
||||||
// - ignoreCase - Whether to ignore case when autocompleting.
|
|
||||||
// Defaults to true.
|
|
||||||
//
|
|
||||||
// It's possible to pass in a custom function as the 'selector'
|
|
||||||
// option, if you prefer to write your own autocompletion logic.
|
|
||||||
// In that case, the other options above will not apply unless
|
|
||||||
// you support them.
|
|
||||||
|
|
||||||
Autocompleter.Local = Class.create(Autocompleter.Base, {
|
|
||||||
initialize: function(element, update, array, options) {
|
|
||||||
this.baseInitialize(element, update, options);
|
|
||||||
this.options.array = array;
|
|
||||||
},
|
|
||||||
|
|
||||||
getUpdatedChoices: function() {
|
|
||||||
this.updateChoices(this.options.selector(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
setOptions: function(options) {
|
|
||||||
this.options = Object.extend({
|
|
||||||
choices: 10,
|
|
||||||
partialSearch: true,
|
|
||||||
partialChars: 2,
|
|
||||||
ignoreCase: true,
|
|
||||||
fullSearch: false,
|
|
||||||
selector: function(instance) {
|
|
||||||
var ret = []; // Beginning matches
|
|
||||||
var partial = []; // Inside matches
|
|
||||||
var entry = instance.getToken();
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < instance.options.array.length &&
|
|
||||||
ret.length < instance.options.choices ; i++) {
|
|
||||||
|
|
||||||
var elem = instance.options.array[i];
|
|
||||||
var foundPos = instance.options.ignoreCase ?
|
|
||||||
elem.toLowerCase().indexOf(entry.toLowerCase()) :
|
|
||||||
elem.indexOf(entry);
|
|
||||||
|
|
||||||
while (foundPos != -1) {
|
|
||||||
if (foundPos == 0 && elem.length != entry.length) {
|
|
||||||
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
|
|
||||||
elem.substr(entry.length) + "</li>");
|
|
||||||
break;
|
|
||||||
} else if (entry.length >= instance.options.partialChars &&
|
|
||||||
instance.options.partialSearch && foundPos != -1) {
|
|
||||||
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
|
|
||||||
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
|
|
||||||
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
|
|
||||||
foundPos + entry.length) + "</li>");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foundPos = instance.options.ignoreCase ?
|
|
||||||
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
|
|
||||||
elem.indexOf(entry, foundPos + 1);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (partial.length)
|
|
||||||
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
|
|
||||||
return "<ul>" + ret.join('') + "</ul>";
|
|
||||||
}
|
|
||||||
}, options || { });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// AJAX in-place editor and collection editor
|
|
||||||
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
|
|
||||||
|
|
||||||
// Use this if you notice weird scrolling problems on some browsers,
|
|
||||||
// the DOM might be a bit confused when this gets called so do this
|
|
||||||
// waits 1 ms (with setTimeout) until it does the activation
|
|
||||||
Field.scrollFreeActivate = function(field) {
|
|
||||||
setTimeout(function() {
|
|
||||||
Field.activate(field);
|
|
||||||
}, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ajax.InPlaceEditor = Class.create({
|
|
||||||
initialize: function(element, url, options) {
|
|
||||||
this.url = url;
|
|
||||||
this.element = element = $(element);
|
|
||||||
this.prepareOptions();
|
|
||||||
this._controls = { };
|
|
||||||
arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
|
|
||||||
Object.extend(this.options, options || { });
|
|
||||||
if (!this.options.formId && this.element.id) {
|
|
||||||
this.options.formId = this.element.id + '-inplaceeditor';
|
|
||||||
if ($(this.options.formId))
|
|
||||||
this.options.formId = '';
|
|
||||||
}
|
|
||||||
if (this.options.externalControl)
|
|
||||||
this.options.externalControl = $(this.options.externalControl);
|
|
||||||
if (!this.options.externalControl)
|
|
||||||
this.options.externalControlOnly = false;
|
|
||||||
this._originalBackground = this.element.getStyle('background-color') || 'transparent';
|
|
||||||
this.element.title = this.options.clickToEditText;
|
|
||||||
this._boundCancelHandler = this.handleFormCancellation.bind(this);
|
|
||||||
this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
|
|
||||||
this._boundFailureHandler = this.handleAJAXFailure.bind(this);
|
|
||||||
this._boundSubmitHandler = this.handleFormSubmission.bind(this);
|
|
||||||
this._boundWrapperHandler = this.wrapUp.bind(this);
|
|
||||||
this.registerListeners();
|
|
||||||
},
|
|
||||||
checkForEscapeOrReturn: function(e) {
|
|
||||||
if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
||||||
if (Event.KEY_ESC == e.keyCode)
|
|
||||||
this.handleFormCancellation(e);
|
|
||||||
else if (Event.KEY_RETURN == e.keyCode)
|
|
||||||
this.handleFormSubmission(e);
|
|
||||||
},
|
|
||||||
createControl: function(mode, handler, extraClasses) {
|
|
||||||
var control = this.options[mode + 'Control'];
|
|
||||||
var text = this.options[mode + 'Text'];
|
|
||||||
if ('button' == control) {
|
|
||||||
var btn = document.createElement('input');
|
|
||||||
btn.type = 'submit';
|
|
||||||
btn.value = text;
|
|
||||||
btn.className = 'editor_' + mode + '_button';
|
|
||||||
if ('cancel' == mode)
|
|
||||||
btn.onclick = this._boundCancelHandler;
|
|
||||||
this._form.appendChild(btn);
|
|
||||||
this._controls[mode] = btn;
|
|
||||||
} else if ('link' == control) {
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.href = '#';
|
|
||||||
link.appendChild(document.createTextNode(text));
|
|
||||||
link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
|
|
||||||
link.className = 'editor_' + mode + '_link';
|
|
||||||
if (extraClasses)
|
|
||||||
link.className += ' ' + extraClasses;
|
|
||||||
this._form.appendChild(link);
|
|
||||||
this._controls[mode] = link;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createEditField: function() {
|
|
||||||
var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
|
|
||||||
var fld;
|
|
||||||
if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
|
|
||||||
fld = document.createElement('input');
|
|
||||||
fld.type = 'text';
|
|
||||||
var size = this.options.size || this.options.cols || 0;
|
|
||||||
if (0 < size) fld.size = size;
|
|
||||||
} else {
|
|
||||||
fld = document.createElement('textarea');
|
|
||||||
fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
|
|
||||||
fld.cols = this.options.cols || 40;
|
|
||||||
}
|
|
||||||
fld.name = this.options.paramName;
|
|
||||||
fld.value = text; // No HTML breaks conversion anymore
|
|
||||||
fld.className = 'editor_field';
|
|
||||||
if (this.options.submitOnBlur)
|
|
||||||
fld.onblur = this._boundSubmitHandler;
|
|
||||||
this._controls.editor = fld;
|
|
||||||
if (this.options.loadTextURL)
|
|
||||||
this.loadExternalText();
|
|
||||||
this._form.appendChild(this._controls.editor);
|
|
||||||
},
|
|
||||||
createForm: function() {
|
|
||||||
var ipe = this;
|
|
||||||
function addText(mode, condition) {
|
|
||||||
var text = ipe.options['text' + mode + 'Controls'];
|
|
||||||
if (!text || condition === false) return;
|
|
||||||
ipe._form.appendChild(document.createTextNode(text));
|
|
||||||
};
|
|
||||||
this._form = $(document.createElement('form'));
|
|
||||||
this._form.id = this.options.formId;
|
|
||||||
this._form.addClassName(this.options.formClassName);
|
|
||||||
this._form.onsubmit = this._boundSubmitHandler;
|
|
||||||
this.createEditField();
|
|
||||||
if ('textarea' == this._controls.editor.tagName.toLowerCase())
|
|
||||||
this._form.appendChild(document.createElement('br'));
|
|
||||||
if (this.options.onFormCustomization)
|
|
||||||
this.options.onFormCustomization(this, this._form);
|
|
||||||
addText('Before', this.options.okControl || this.options.cancelControl);
|
|
||||||
this.createControl('ok', this._boundSubmitHandler);
|
|
||||||
addText('Between', this.options.okControl && this.options.cancelControl);
|
|
||||||
this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
|
|
||||||
addText('After', this.options.okControl || this.options.cancelControl);
|
|
||||||
},
|
|
||||||
destroy: function() {
|
|
||||||
if (this._oldInnerHTML)
|
|
||||||
this.element.innerHTML = this._oldInnerHTML;
|
|
||||||
this.leaveEditMode();
|
|
||||||
this.unregisterListeners();
|
|
||||||
},
|
|
||||||
enterEditMode: function(e) {
|
|
||||||
if (this._saving || this._editing) return;
|
|
||||||
this._editing = true;
|
|
||||||
this.triggerCallback('onEnterEditMode');
|
|
||||||
if (this.options.externalControl)
|
|
||||||
this.options.externalControl.hide();
|
|
||||||
this.element.hide();
|
|
||||||
this.createForm();
|
|
||||||
this.element.parentNode.insertBefore(this._form, this.element);
|
|
||||||
if (!this.options.loadTextURL)
|
|
||||||
this.postProcessEditField();
|
|
||||||
if (e) Event.stop(e);
|
|
||||||
},
|
|
||||||
enterHover: function(e) {
|
|
||||||
if (this.options.hoverClassName)
|
|
||||||
this.element.addClassName(this.options.hoverClassName);
|
|
||||||
if (this._saving) return;
|
|
||||||
this.triggerCallback('onEnterHover');
|
|
||||||
},
|
|
||||||
getText: function() {
|
|
||||||
return this.element.innerHTML.unescapeHTML();
|
|
||||||
},
|
|
||||||
handleAJAXFailure: function(transport) {
|
|
||||||
this.triggerCallback('onFailure', transport);
|
|
||||||
if (this._oldInnerHTML) {
|
|
||||||
this.element.innerHTML = this._oldInnerHTML;
|
|
||||||
this._oldInnerHTML = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleFormCancellation: function(e) {
|
|
||||||
this.wrapUp();
|
|
||||||
if (e) Event.stop(e);
|
|
||||||
},
|
|
||||||
handleFormSubmission: function(e) {
|
|
||||||
var form = this._form;
|
|
||||||
var value = $F(this._controls.editor);
|
|
||||||
this.prepareSubmission();
|
|
||||||
var params = this.options.callback(form, value) || '';
|
|
||||||
if (Object.isString(params))
|
|
||||||
params = params.toQueryParams();
|
|
||||||
params.editorId = this.element.id;
|
|
||||||
if (this.options.htmlResponse) {
|
|
||||||
var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
|
|
||||||
Object.extend(options, {
|
|
||||||
parameters: params,
|
|
||||||
onComplete: this._boundWrapperHandler,
|
|
||||||
onFailure: this._boundFailureHandler
|
|
||||||
});
|
|
||||||
new Ajax.Updater({ success: this.element }, this.url, options);
|
|
||||||
} else {
|
|
||||||
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
|
||||||
Object.extend(options, {
|
|
||||||
parameters: params,
|
|
||||||
onComplete: this._boundWrapperHandler,
|
|
||||||
onFailure: this._boundFailureHandler
|
|
||||||
});
|
|
||||||
new Ajax.Request(this.url, options);
|
|
||||||
}
|
|
||||||
if (e) Event.stop(e);
|
|
||||||
},
|
|
||||||
leaveEditMode: function() {
|
|
||||||
this.element.removeClassName(this.options.savingClassName);
|
|
||||||
this.removeForm();
|
|
||||||
this.leaveHover();
|
|
||||||
this.element.style.backgroundColor = this._originalBackground;
|
|
||||||
this.element.show();
|
|
||||||
if (this.options.externalControl)
|
|
||||||
this.options.externalControl.show();
|
|
||||||
this._saving = false;
|
|
||||||
this._editing = false;
|
|
||||||
this._oldInnerHTML = null;
|
|
||||||
this.triggerCallback('onLeaveEditMode');
|
|
||||||
},
|
|
||||||
leaveHover: function(e) {
|
|
||||||
if (this.options.hoverClassName)
|
|
||||||
this.element.removeClassName(this.options.hoverClassName);
|
|
||||||
if (this._saving) return;
|
|
||||||
this.triggerCallback('onLeaveHover');
|
|
||||||
},
|
|
||||||
loadExternalText: function() {
|
|
||||||
this._form.addClassName(this.options.loadingClassName);
|
|
||||||
this._controls.editor.disabled = true;
|
|
||||||
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
|
||||||
Object.extend(options, {
|
|
||||||
parameters: 'editorId=' + encodeURIComponent(this.element.id),
|
|
||||||
onComplete: Prototype.emptyFunction,
|
|
||||||
onSuccess: function(transport) {
|
|
||||||
this._form.removeClassName(this.options.loadingClassName);
|
|
||||||
var text = transport.responseText;
|
|
||||||
if (this.options.stripLoadedTextTags)
|
|
||||||
text = text.stripTags();
|
|
||||||
this._controls.editor.value = text;
|
|
||||||
this._controls.editor.disabled = false;
|
|
||||||
this.postProcessEditField();
|
|
||||||
}.bind(this),
|
|
||||||
onFailure: this._boundFailureHandler
|
|
||||||
});
|
|
||||||
new Ajax.Request(this.options.loadTextURL, options);
|
|
||||||
},
|
|
||||||
postProcessEditField: function() {
|
|
||||||
var fpc = this.options.fieldPostCreation;
|
|
||||||
if (fpc)
|
|
||||||
$(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
|
|
||||||
},
|
|
||||||
prepareOptions: function() {
|
|
||||||
this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
|
|
||||||
Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
|
|
||||||
[this._extraDefaultOptions].flatten().compact().each(function(defs) {
|
|
||||||
Object.extend(this.options, defs);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
prepareSubmission: function() {
|
|
||||||
this._saving = true;
|
|
||||||
this.removeForm();
|
|
||||||
this.leaveHover();
|
|
||||||
this.showSaving();
|
|
||||||
},
|
|
||||||
registerListeners: function() {
|
|
||||||
this._listeners = { };
|
|
||||||
var listener;
|
|
||||||
$H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
|
|
||||||
listener = this[pair.value].bind(this);
|
|
||||||
this._listeners[pair.key] = listener;
|
|
||||||
if (!this.options.externalControlOnly)
|
|
||||||
this.element.observe(pair.key, listener);
|
|
||||||
if (this.options.externalControl)
|
|
||||||
this.options.externalControl.observe(pair.key, listener);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
removeForm: function() {
|
|
||||||
if (!this._form) return;
|
|
||||||
this._form.remove();
|
|
||||||
this._form = null;
|
|
||||||
this._controls = { };
|
|
||||||
},
|
|
||||||
showSaving: function() {
|
|
||||||
this._oldInnerHTML = this.element.innerHTML;
|
|
||||||
this.element.innerHTML = this.options.savingText;
|
|
||||||
this.element.addClassName(this.options.savingClassName);
|
|
||||||
this.element.style.backgroundColor = this._originalBackground;
|
|
||||||
this.element.show();
|
|
||||||
},
|
|
||||||
triggerCallback: function(cbName, arg) {
|
|
||||||
if ('function' == typeof this.options[cbName]) {
|
|
||||||
this.options[cbName](this, arg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unregisterListeners: function() {
|
|
||||||
$H(this._listeners).each(function(pair) {
|
|
||||||
if (!this.options.externalControlOnly)
|
|
||||||
this.element.stopObserving(pair.key, pair.value);
|
|
||||||
if (this.options.externalControl)
|
|
||||||
this.options.externalControl.stopObserving(pair.key, pair.value);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
wrapUp: function(transport) {
|
|
||||||
this.leaveEditMode();
|
|
||||||
// Can't use triggerCallback due to backward compatibility: requires
|
|
||||||
// binding + direct element
|
|
||||||
this._boundComplete(transport, this.element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.extend(Ajax.InPlaceEditor.prototype, {
|
|
||||||
dispose: Ajax.InPlaceEditor.prototype.destroy
|
|
||||||
});
|
|
||||||
|
|
||||||
Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
|
|
||||||
initialize: function($super, element, url, options) {
|
|
||||||
this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
|
|
||||||
$super(element, url, options);
|
|
||||||
},
|
|
||||||
|
|
||||||
createEditField: function() {
|
|
||||||
var list = document.createElement('select');
|
|
||||||
list.name = this.options.paramName;
|
|
||||||
list.size = 1;
|
|
||||||
this._controls.editor = list;
|
|
||||||
this._collection = this.options.collection || [];
|
|
||||||
if (this.options.loadCollectionURL)
|
|
||||||
this.loadCollection();
|
|
||||||
else
|
|
||||||
this.checkForExternalText();
|
|
||||||
this._form.appendChild(this._controls.editor);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadCollection: function() {
|
|
||||||
this._form.addClassName(this.options.loadingClassName);
|
|
||||||
this.showLoadingText(this.options.loadingCollectionText);
|
|
||||||
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
|
||||||
Object.extend(options, {
|
|
||||||
parameters: 'editorId=' + encodeURIComponent(this.element.id),
|
|
||||||
onComplete: Prototype.emptyFunction,
|
|
||||||
onSuccess: function(transport) {
|
|
||||||
var js = transport.responseText.strip();
|
|
||||||
if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
|
|
||||||
throw('Server returned an invalid collection representation.');
|
|
||||||
this._collection = eval(js);
|
|
||||||
this.checkForExternalText();
|
|
||||||
}.bind(this),
|
|
||||||
onFailure: this.onFailure
|
|
||||||
});
|
|
||||||
new Ajax.Request(this.options.loadCollectionURL, options);
|
|
||||||
},
|
|
||||||
|
|
||||||
showLoadingText: function(text) {
|
|
||||||
this._controls.editor.disabled = true;
|
|
||||||
var tempOption = this._controls.editor.firstChild;
|
|
||||||
if (!tempOption) {
|
|
||||||
tempOption = document.createElement('option');
|
|
||||||
tempOption.value = '';
|
|
||||||
this._controls.editor.appendChild(tempOption);
|
|
||||||
tempOption.selected = true;
|
|
||||||
}
|
|
||||||
tempOption.update((text || '').stripScripts().stripTags());
|
|
||||||
},
|
|
||||||
|
|
||||||
checkForExternalText: function() {
|
|
||||||
this._text = this.getText();
|
|
||||||
if (this.options.loadTextURL)
|
|
||||||
this.loadExternalText();
|
|
||||||
else
|
|
||||||
this.buildOptionList();
|
|
||||||
},
|
|
||||||
|
|
||||||
loadExternalText: function() {
|
|
||||||
this.showLoadingText(this.options.loadingText);
|
|
||||||
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
|
||||||
Object.extend(options, {
|
|
||||||
parameters: 'editorId=' + encodeURIComponent(this.element.id),
|
|
||||||
onComplete: Prototype.emptyFunction,
|
|
||||||
onSuccess: function(transport) {
|
|
||||||
this._text = transport.responseText.strip();
|
|
||||||
this.buildOptionList();
|
|
||||||
}.bind(this),
|
|
||||||
onFailure: this.onFailure
|
|
||||||
});
|
|
||||||
new Ajax.Request(this.options.loadTextURL, options);
|
|
||||||
},
|
|
||||||
|
|
||||||
buildOptionList: function() {
|
|
||||||
this._form.removeClassName(this.options.loadingClassName);
|
|
||||||
this._collection = this._collection.map(function(entry) {
|
|
||||||
return 2 === entry.length ? entry : [entry, entry].flatten();
|
|
||||||
});
|
|
||||||
var marker = ('value' in this.options) ? this.options.value : this._text;
|
|
||||||
var textFound = this._collection.any(function(entry) {
|
|
||||||
return entry[0] == marker;
|
|
||||||
}.bind(this));
|
|
||||||
this._controls.editor.update('');
|
|
||||||
var option;
|
|
||||||
this._collection.each(function(entry, index) {
|
|
||||||
option = document.createElement('option');
|
|
||||||
option.value = entry[0];
|
|
||||||
option.selected = textFound ? entry[0] == marker : 0 == index;
|
|
||||||
option.appendChild(document.createTextNode(entry[1]));
|
|
||||||
this._controls.editor.appendChild(option);
|
|
||||||
}.bind(this));
|
|
||||||
this._controls.editor.disabled = false;
|
|
||||||
Field.scrollFreeActivate(this._controls.editor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
|
|
||||||
//**** This only exists for a while, in order to let ****
|
|
||||||
//**** users adapt to the new API. Read up on the new ****
|
|
||||||
//**** API and convert your code to it ASAP! ****
|
|
||||||
|
|
||||||
Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
|
|
||||||
if (!options) return;
|
|
||||||
function fallback(name, expr) {
|
|
||||||
if (name in options || expr === undefined) return;
|
|
||||||
options[name] = expr;
|
|
||||||
};
|
|
||||||
fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
|
|
||||||
options.cancelLink == options.cancelButton == false ? false : undefined)));
|
|
||||||
fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
|
|
||||||
options.okLink == options.okButton == false ? false : undefined)));
|
|
||||||
fallback('highlightColor', options.highlightcolor);
|
|
||||||
fallback('highlightEndColor', options.highlightendcolor);
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.extend(Ajax.InPlaceEditor, {
|
|
||||||
DefaultOptions: {
|
|
||||||
ajaxOptions: { },
|
|
||||||
autoRows: 3, // Use when multi-line w/ rows == 1
|
|
||||||
cancelControl: 'link', // 'link'|'button'|false
|
|
||||||
cancelText: 'cancel',
|
|
||||||
clickToEditText: 'Click to edit',
|
|
||||||
externalControl: null, // id|elt
|
|
||||||
externalControlOnly: false,
|
|
||||||
fieldPostCreation: 'activate', // 'activate'|'focus'|false
|
|
||||||
formClassName: 'inplaceeditor-form',
|
|
||||||
formId: null, // id|elt
|
|
||||||
highlightColor: '#ffff99',
|
|
||||||
highlightEndColor: '#ffffff',
|
|
||||||
hoverClassName: '',
|
|
||||||
htmlResponse: true,
|
|
||||||
loadingClassName: 'inplaceeditor-loading',
|
|
||||||
loadingText: 'Loading...',
|
|
||||||
okControl: 'button', // 'link'|'button'|false
|
|
||||||
okText: 'ok',
|
|
||||||
paramName: 'value',
|
|
||||||
rows: 1, // If 1 and multi-line, uses autoRows
|
|
||||||
savingClassName: 'inplaceeditor-saving',
|
|
||||||
savingText: 'Saving...',
|
|
||||||
size: 0,
|
|
||||||
stripLoadedTextTags: false,
|
|
||||||
submitOnBlur: false,
|
|
||||||
textAfterControls: '',
|
|
||||||
textBeforeControls: '',
|
|
||||||
textBetweenControls: ''
|
|
||||||
},
|
|
||||||
DefaultCallbacks: {
|
|
||||||
callback: function(form) {
|
|
||||||
return Form.serialize(form);
|
|
||||||
},
|
|
||||||
onComplete: function(transport, element) {
|
|
||||||
// For backward compatibility, this one is bound to the IPE, and passes
|
|
||||||
// the element directly. It was too often customized, so we don't break it.
|
|
||||||
new Effect.Highlight(element, {
|
|
||||||
startcolor: this.options.highlightColor, keepBackgroundImage: true });
|
|
||||||
},
|
|
||||||
onEnterEditMode: null,
|
|
||||||
onEnterHover: function(ipe) {
|
|
||||||
ipe.element.style.backgroundColor = ipe.options.highlightColor;
|
|
||||||
if (ipe._effect)
|
|
||||||
ipe._effect.cancel();
|
|
||||||
},
|
|
||||||
onFailure: function(transport, ipe) {
|
|
||||||
alert('Error communication with the server: ' + transport.responseText.stripTags());
|
|
||||||
},
|
|
||||||
onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
|
|
||||||
onLeaveEditMode: null,
|
|
||||||
onLeaveHover: function(ipe) {
|
|
||||||
ipe._effect = new Effect.Highlight(ipe.element, {
|
|
||||||
startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
|
|
||||||
restorecolor: ipe._originalBackground, keepBackgroundImage: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Listeners: {
|
|
||||||
click: 'enterEditMode',
|
|
||||||
keydown: 'checkForEscapeOrReturn',
|
|
||||||
mouseover: 'enterHover',
|
|
||||||
mouseout: 'leaveHover'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ajax.InPlaceCollectionEditor.DefaultOptions = {
|
|
||||||
loadingCollectionText: 'Loading options...'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delayed observer, like Form.Element.Observer,
|
|
||||||
// but waits for delay after last key input
|
|
||||||
// Ideal for live-search fields
|
|
||||||
|
|
||||||
Form.Element.DelayedObserver = Class.create({
|
|
||||||
initialize: function(element, delay, callback) {
|
|
||||||
this.delay = delay || 0.5;
|
|
||||||
this.element = $(element);
|
|
||||||
this.callback = callback;
|
|
||||||
this.timer = null;
|
|
||||||
this.lastValue = $F(this.element);
|
|
||||||
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
|
|
||||||
},
|
|
||||||
delayedListener: function(event) {
|
|
||||||
if(this.lastValue == $F(this.element)) return;
|
|
||||||
if(this.timer) clearTimeout(this.timer);
|
|
||||||
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
|
|
||||||
this.lastValue = $F(this.element);
|
|
||||||
},
|
|
||||||
onTimerEvent: function() {
|
|
||||||
this.timer = null;
|
|
||||||
this.callback(this.element, $F(this.element));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
1123
lib/scriptaculous/effects.js
vendored
1123
lib/scriptaculous/effects.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
|||||||
// script.aculo.us scriptaculous.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
|
|
||||||
|
|
||||||
// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
// a copy of this software and associated documentation files (the
|
|
||||||
// "Software"), to deal in the Software without restriction, including
|
|
||||||
// without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
// permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
// the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be
|
|
||||||
// included in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
//
|
|
||||||
// For details, see the script.aculo.us web site: http://script.aculo.us/
|
|
||||||
|
|
||||||
var Scriptaculous = {
|
|
||||||
Version: '1.9.0',
|
|
||||||
require: function(libraryName) {
|
|
||||||
try{
|
|
||||||
// inserting via DOM fails in Safari 2.0, so brute force approach
|
|
||||||
document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
|
|
||||||
} catch(e) {
|
|
||||||
// for xhtml+xml served content, fall back to DOM methods
|
|
||||||
var script = document.createElement('script');
|
|
||||||
script.type = 'text/javascript';
|
|
||||||
script.src = libraryName;
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(script);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
REQUIRED_PROTOTYPE: '1.6.0.3',
|
|
||||||
load: function() {
|
|
||||||
function convertVersionString(versionString) {
|
|
||||||
var v = versionString.replace(/_.*|\./g, '');
|
|
||||||
v = parseInt(v + '0'.times(4-v.length));
|
|
||||||
return versionString.indexOf('_') > -1 ? v-1 : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((typeof Prototype=='undefined') ||
|
|
||||||
(typeof Element == 'undefined') ||
|
|
||||||
(typeof Element.Methods=='undefined') ||
|
|
||||||
(convertVersionString(Prototype.Version) <
|
|
||||||
convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
|
|
||||||
throw("script.aculo.us requires the Prototype JavaScript framework >= " +
|
|
||||||
Scriptaculous.REQUIRED_PROTOTYPE);
|
|
||||||
|
|
||||||
var js = /scriptaculous\.js(\?.*)?$/;
|
|
||||||
$$('script[src]').findAll(function(s) {
|
|
||||||
return s.src.match(js);
|
|
||||||
}).each(function(s) {
|
|
||||||
var path = s.src.replace(js, ''),
|
|
||||||
includes = s.src.match(/\?.*load=([a-z,]*)/);
|
|
||||||
(includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
|
|
||||||
function(include) { Scriptaculous.require(path+include+'.js') });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Scriptaculous.load();
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user