diff --git a/.eslintrc.js b/.eslintrc.js index 6d76f6082..d2f24d663 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,12 +2,12 @@ module.exports = { "env": { "browser": true, "es6": true, - "jquery": true, - "webextensions": true + "jquery": false, + "webextensions": false }, "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 2017 + "ecmaVersion": 2018 }, "rules": { "accessor-pairs": "error", @@ -106,7 +106,7 @@ module.exports = { "no-catch-shadow": "off", "no-confusing-arrow": "error", "no-continue": "off", - "no-console": "off", + "no-console": "off", "no-div-regex": "error", "no-duplicate-imports": "error", "no-else-return": "off", @@ -187,7 +187,7 @@ module.exports = { "no-trailing-spaces": "error", "no-undef-init": "error", "no-undefined": "off", - "no-undef": "warn", + "no-undef": "warn", "no-underscore-dangle": "off", "no-unmodified-loop-condition": "error", "no-unneeded-ternary": [ @@ -197,7 +197,7 @@ module.exports = { } ], "no-unused-expressions": "off", - "no-unused-vars": "warn", + "no-unused-vars": "warn", "no-use-before-define": "off", "no-useless-call": "error", "no-useless-computed-key": "error", diff --git a/.gitignore b/.gitignore index eaf169cb8..c8aa69a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,13 @@ Thumbs.db /.app_is_ready -/deploy.exclude -/deploy.sh /messages.mo /node_modules +/locale/**/*.po~ /package-lock.json -*~ -*.DS_Store -#* -.idea/* -plugins.local/* -themes.local/* -config.php -feed-icons/* -cache/*/* -lock/* -tags -cache/htmlpurifier/*/*ser -lib/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/*/*ser -web.config -/.save.cson -/.tags* -/.gutentags +/plugins.local/* +/themes.local/* +/config.php +/feed-icons/* +/cache/*/* +/lock/* /.vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..0e10b4b23 --- /dev/null +++ b/.vscode/launch.json @@ -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 + }] +} \ No newline at end of file diff --git a/api/index.php b/api/index.php index 77552af46..d85a1103c 100644 --- a/api/index.php +++ b/api/index.php @@ -1,55 +1,29 @@ -1, - "status" => 1, - "content" => array("error" => "NOT_LOGGED_IN"))); + print json_encode([ + "seq" => -1, + "status" => API::STATUS_ERR, + "content" => [ "error" => API::E_NOT_LOGGED_IN ] + ]); return; } diff --git a/backend.php b/backend.php index dec79f46f..206d866b7 100644 --- a/backend.php +++ b/backend.php @@ -1,5 +1,5 @@ __("Power User"), 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); $override = PluginHost::getInstance()->lookup_handler($op, $method); 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) { $handler = $override; } else { @@ -114,8 +128,9 @@ if ($reflection->getNumberOfRequiredParameters() == 0) { $handler->$method(); } else { + user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING); header("Content-Type: text/json"); - print error_json(6); + print Errors::to_json(Errors::E_UNAUTHORIZED); } } else { if (method_exists($handler, "catchall")) { @@ -126,18 +141,19 @@ return; } else { header("Content-Type: text/json"); - print error_json(6); + print Errors::to_json(Errors::E_UNAUTHORIZED); return; } } else { + user_error("Refusing to invoke method $method of handler $op with invalid CSRF token.", E_USER_WARNING); header("Content-Type: text/json"); - print error_json(6); + print Errors::to_json(Errors::E_UNAUTHORIZED); return; } } } header("Content-Type: text/json"); - print error_json(13); + print Errors::to_json(Errors::E_UNKNOWN_METHOD); ?> diff --git a/classes/api.php b/classes/api.php index 6a919be64..18f9c83b5 100755 --- a/classes/api.php +++ b/classes/api.php @@ -6,23 +6,38 @@ class API extends Handler { const STATUS_OK = 0; 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; - static function param_to_bool($p) { + private static function _param_to_bool($p) { return $p && ($p !== "f" && $p !== "false"); } + private function _wrap($status, $reply) { + print json_encode([ + "seq" => $this->seq, + "status" => $status, + "content" => $reply + ]); + } + function before($method) { if (parent::before($method)) { header("Content-Type: text/json"); 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; } 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; } @@ -33,20 +48,14 @@ class API extends Handler { return false; } - function wrap($status, $reply) { - print json_encode(array("seq" => $this->seq, - "status" => $status, - "content" => $reply)); - } - function getVersion() { $rv = array("version" => get_version()); - $this->wrap(self::STATUS_OK, $rv); + $this->_wrap(self::STATUS_OK, $rv); } function getApiLevel() { $rv = array("level" => self::API_LEVEL); - $this->wrap(self::STATUS_OK, $rv); + $this->_wrap(self::STATUS_OK, $rv); } function login() { @@ -57,36 +66,36 @@ class API extends Handler { $password = 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 (get_pref("ENABLE_API_ACCESS", $uid)) { 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)); } 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)); } else { // else we are not logged in 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 { - $this->wrap(self::STATUS_ERR, array("error" => "API_DISABLED")); + $this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED)); } } else { - $this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR")); + $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR)); return; } } function logout() { - Pref_Users::logout_user(); - $this->wrap(self::STATUS_OK, array("status" => "OK")); + UserHelper::logout(); + $this->_wrap(self::STATUS_OK, array("status" => "OK")); } function isLoggedIn() { - $this->wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != '')); + $this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != '')); } function getUnread() { @@ -94,33 +103,33 @@ class API extends Handler { $is_cat = clean($_REQUEST["is_cat"]); 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 { - $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 */ function getCounters() { - $this->wrap(self::STATUS_OK, Counters::getAllCounters()); + $this->_wrap(self::STATUS_OK, Counters::get_all()); } function getFeeds() { $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); $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() { - $unread_only = self::param_to_bool(clean($_REQUEST["unread_only"] ?? false)); - $enable_nested = self::param_to_bool(clean($_REQUEST["enable_nested"] ?? false)); - $include_empty = self::param_to_bool(clean($_REQUEST['include_empty'] ?? false)); + $unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? false)); + $enable_nested = self::_param_to_bool(clean($_REQUEST["enable_nested"] ?? false)); + $include_empty = self::_param_to_bool(clean($_REQUEST['include_empty'] ?? false)); // 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); if ($enable_nested) - $unread += Feeds::getCategoryChildrenUnread($line["id"]); + $unread += Feeds::_get_cat_children_unread($line["id"]); if ($unread || !$unread_only) { array_push($cats, array("id" => (int) $line["id"], @@ -160,18 +169,18 @@ class API extends Handler { } 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); if ($unread || !$unread_only) { array_push($cats, array("id" => $cat_id, - "title" => Feeds::getCategoryTitle($cat_id), + "title" => Feeds::_get_cat_title($cat_id), "unread" => (int) $unread)); } } } - $this->wrap(self::STATUS_OK, $cats); + $this->_wrap(self::STATUS_OK, $cats); } function getHeadlines() { @@ -186,42 +195,42 @@ class API extends Handler { $offset = (int)clean($_REQUEST["skip"]); $filter = clean($_REQUEST["filter"] ?? ""); - $is_cat = self::param_to_bool(clean($_REQUEST["is_cat"] ?? false)); - $show_excerpt = self::param_to_bool(clean($_REQUEST["show_excerpt"] ?? false)); - $show_content = self::param_to_bool(clean($_REQUEST["show_content"])); + $is_cat = self::_param_to_bool(clean($_REQUEST["is_cat"] ?? false)); + $show_excerpt = self::_param_to_bool(clean($_REQUEST["show_excerpt"] ?? false)); + $show_content = self::_param_to_bool(clean($_REQUEST["show_content"])); /* all_articles, unread, adaptive, marked, updated */ $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); - $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"]) || - self::param_to_bool($_REQUEST["sanitize"]); - $force_update = self::param_to_bool(clean($_REQUEST["force_update"] ?? false)); - $has_sandbox = self::param_to_bool(clean($_REQUEST["has_sandbox"] ?? false)); + self::_param_to_bool($_REQUEST["sanitize"]); + $force_update = self::_param_to_bool(clean($_REQUEST["force_update"] ?? false)); + $has_sandbox = self::_param_to_bool(clean($_REQUEST["has_sandbox"] ?? false)); $excerpt_length = (int)clean($_REQUEST["excerpt_length"] ?? 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; - 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 */ $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, $include_attachments, $since_id, $search, $include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check); if ($include_header) { - $this->wrap(self::STATUS_OK, array($headlines_header, $headlines)); + $this->_wrap(self::STATUS_OK, array($headlines_header, $headlines)); } else { - $this->wrap(self::STATUS_OK, $headlines); + $this->_wrap(self::STATUS_OK, $headlines); } } 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(); - $this->wrap(self::STATUS_OK, array("status" => "OK", + $this->_wrap(self::STATUS_OK, array("status" => "OK", "updated" => $num_updated)); } 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"])); $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); @@ -311,22 +320,20 @@ class API extends Handler { while ($line = $sth->fetch()) { - $attachments = Article::get_article_enclosures($line['id']); - $article = array( "id" => $line["id"], "guid" => $line["guid"], "title" => $line["title"], "link" => $line["link"], - "labels" => Article::get_article_labels($line['id']), - "unread" => self::param_to_bool($line["unread"]), - "marked" => self::param_to_bool($line["marked"]), - "published" => self::param_to_bool($line["published"]), + "labels" => Article::_get_labels($line['id']), + "unread" => self::_param_to_bool($line["unread"]), + "marked" => self::_param_to_bool($line["marked"]), + "published" => self::_param_to_bool($line["published"]), "comments" => $line["comments"], "author" => $line["author"], "updated" => (int) strtotime($line["updated"]), "feed_id" => $line["feed_id"], - "attachments" => $attachments, + "attachments" => Article::_get_enclosures($line['id']), "score" => (int)$line["score"], "feed_title" => $line["feed_title"], "note" => $line["note"], @@ -336,7 +343,7 @@ class API extends Handler { if ($sanitize_content) { $article["content"] = Sanitizer::sanitize( $line["content"], - self::param_to_bool($line['hide_images']), + self::_param_to_bool($line['hide_images']), false, $line["site_url"], false, $line["id"]); } else { $article["content"] = $line["content"]; @@ -350,22 +357,23 @@ class API extends Handler { }, $hook_object); - $article['content'] = DiskCache::rewriteUrls($article['content']); + $article['content'] = DiskCache::rewrite_urls($article['content']); array_push($articles, $article); } - $this->wrap(self::STATUS_OK, $articles); + $this->_wrap(self::STATUS_OK, $articles); } else { - $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE')); + $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE)); } } function getConfig() { - $config = array( - "icons_dir" => ICONS_DIR, - "icons_url" => ICONS_URL); + $config = [ + "icons_dir" => Config::get(Config::ICONS_DIR), + "icons_url" => Config::get(Config::ICONS_URL) + ]; $config["daemon_is_running"] = file_is_locked("update_daemon.lock"); @@ -376,7 +384,7 @@ class API extends Handler { $config["num_feeds"] = $row["cf"]; - $this->wrap(self::STATUS_OK, $config); + $this->_wrap(self::STATUS_OK, $config); } function updateFeed() { @@ -386,7 +394,7 @@ class API extends Handler { RSSUtils::update_rss_feed($feed_id); } - $this->wrap(self::STATUS_OK, array("status" => "OK")); + $this->_wrap(self::STATUS_OK, array("status" => "OK")); } function catchupFeed() { @@ -397,15 +405,15 @@ class API extends Handler { if (!in_array($mode, ["all", "1day", "1week", "2week"])) $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() { $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() { @@ -419,7 +427,7 @@ class API extends Handler { $sth->execute([$_SESSION['uid']]); if ($article_id) - $article_labels = Article::get_article_labels($article_id); + $article_labels = Article::_get_labels($article_id); else $article_labels = array(); @@ -441,14 +449,14 @@ class API extends Handler { "checked" => $checked)); } - $this->wrap(self::STATUS_OK, $rv); + $this->_wrap(self::STATUS_OK, $rv); } function setArticleLabel() { $article_ids = explode(",", clean($_REQUEST["article_ids"])); $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"]); @@ -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)); } @@ -479,10 +487,10 @@ class API extends Handler { if ($plugin && method_exists($plugin, $method)) { $reply = $plugin->$method(); - $this->wrap($reply[0], $reply[1]); + $this->_wrap($reply[0], $reply[1]); } 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"])); $content = strip_tags(clean($_REQUEST["content"])); - if (Article::create_published_article($title, $url, $content, "", $_SESSION["uid"])) { - $this->wrap(self::STATUS_OK, array("status" => 'OK')); + if (Article::_create_published_article($title, $url, $content, "", $_SESSION["uid"])) { + $this->_wrap(self::STATUS_OK, array("status" => 'OK')); } 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(); @@ -512,7 +520,7 @@ class API extends Handler { /* API only: -4 All feeds, including virtual feeds */ if ($cat_id == -4 || $cat_id == -2) { - $counters = Counters::getLabelCounters(true); + $counters = Counters::get_labels(); foreach (array_values($counters) as $cv) { @@ -539,7 +547,7 @@ class API extends Handler { $unread = getFeedUnread($i); if ($unread || !$unread_only) { - $title = Feeds::getFeedTitle($i); + $title = Feeds::_get_title($i); $row = array( "id" => $i, @@ -564,7 +572,7 @@ class API extends Handler { while ($line = $sth->fetch()) { $unread = getFeedUnread($line["id"], true) + - Feeds::getCategoryChildrenUnread($line["id"]); + Feeds::_get_cat_children_unread($line["id"]); if ($unread || !$unread_only) { $row = array( @@ -612,7 +620,7 @@ class API extends Handler { $unread = getFeedUnread($line["id"]); - $has_icon = Feeds::feedHasIcon($line['id']); + $has_icon = Feeds::_has_icon($line['id']); if ($unread || !$unread_only) { @@ -634,7 +642,7 @@ class API extends Handler { 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, $include_attachments, $since_id, $search = "", $include_nested = false, $sanitize_content = true, @@ -652,7 +660,7 @@ class API extends Handler { if ($row = $sth->fetch()) { $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) { RSSUtils::update_rss_feed($feed_id, true); @@ -678,7 +686,7 @@ class API extends Handler { "skip_first_id_check" => $skip_first_id_check ); - $qfh_ret = Feeds::queryFeedHeadlines($params); + $qfh_ret = Feeds::_get_headlines($params); $result = $qfh_ret[0]; $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( "id" => (int)$line["id"], "guid" => $line["guid"], - "unread" => self::param_to_bool($line["unread"]), - "marked" => self::param_to_bool($line["marked"]), - "published" => self::param_to_bool($line["published"]), + "unread" => self::_param_to_bool($line["unread"]), + "marked" => self::_param_to_bool($line["marked"]), + "published" => self::_param_to_bool($line["published"]), "updated" => (int)strtotime($line["updated"]), "is_updated" => $is_updated, "title" => $line["title"], @@ -736,7 +744,7 @@ class API extends Handler { "tags" => $tags, ); - $enclosures = Article::get_article_enclosures($line['id']); + $enclosures = Article::_get_enclosures($line['id']); if ($include_attachments) $headline_row['attachments'] = $enclosures; @@ -749,13 +757,11 @@ class API extends Handler { if ($sanitize_content) { $headline_row["content"] = Sanitizer::sanitize( $line["content"], - self::param_to_bool($line['hide_images']), + self::_param_to_bool($line['hide_images']), false, $line["site_url"], false, $line["id"]); } else { $headline_row["content"] = $line["content"]; } - - $headline_row["content"] = DiskCache::rewriteUrls($headline_row['content']); } // 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_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"]; @@ -776,22 +782,28 @@ class API extends Handler { $headline_row["note"] = $line["note"]; $headline_row["lang"] = $line["lang"]; - $hook_object = ["headline" => &$headline_row]; + if ($show_content) { + $hook_object = ["headline" => &$headline_row]; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API, - function ($result) use (&$headline_row) { - $headline_row = $result; - }, - $hook_object); + list ($flavor_image, $flavor_stream, $flavor_kind) = Article::_get_image($enclosures, + $line["content"], // unsanitized + $line["site_url"]); - 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_stream"] = $flavor_stream; - $headline_row["flavor_image"] = $flavor_image; - $headline_row["flavor_stream"] = $flavor_stream; + /* optional */ + if ($flavor_kind) + $headline_row["flavor_kind"] = $flavor_kind; - /* optional */ - if ($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); } @@ -811,9 +823,9 @@ class API extends Handler { if ($row = $sth->fetch()) { 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 { - $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"]); 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 { - $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE')); + $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE)); } } 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); $_REQUEST['mode'] = 2; $_REQUEST['force_show_empty'] = $include_empty; - $this->wrap(self::STATUS_OK, - array("categories" => $pf->makefeedtree())); + $this->_wrap(self::STATUS_OK, + array("categories" => $pf->_makefeedtree())); } // only works for labels or uncategorized for the time being - private function isCategoryEmpty($id) { + private function _is_cat_empty($id) { if ($id == -2) { $sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2 diff --git a/classes/article.php b/classes/article.php index 6d3746968..6baf8f068 100755 --- a/classes/article.php +++ b/classes/article.php @@ -5,7 +5,7 @@ class Article extends Handler_Protected { const ARTICLE_KIND_YOUTUBE = 3; function redirect() { - $id = clean($_REQUEST['id']); + $id = (int) clean($_REQUEST['id'] ?? 0); $sth = $this->pdo->prepare("SELECT link FROM ttrss_entries, ttrss_user_entries WHERE id = ? AND id = ref_id AND owner_uid = ? @@ -13,18 +13,21 @@ class Article extends Handler_Protected { $sth->execute([$id, $_SESSION['uid']]); if ($row = $sth->fetch()) { - $article_url = $row['link']; - $article_url = str_replace("\n", "", $article_url); + $article_url = UrlHelper::validate(str_replace("\n", "", $row['link'])); - header("Location: $article_url"); - return; + if ($article_url) { + header("Location: $article_url"); + } else { + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + print "URL of article $id is blank."; + } } else { 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) { $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 = ?"); $sth->execute([$content, $content_hash, $ref_id]); - if (DB_TYPE == "pgsql"){ + if (Config::get(Config::DB_TYPE) == "pgsql") { $sth = $pdo->prepare("UPDATE ttrss_entries SET tsvector_combined = to_tsvector( :ts_content) WHERE id = :id"); @@ -127,7 +130,7 @@ class Article extends Handler_Protected { if ($row = $sth->fetch()) { $ref_id = $row["id"]; - if (DB_TYPE == "pgsql"){ + if (Config::get(Config::DB_TYPE) == "pgsql"){ $sth = $pdo->prepare("UPDATE ttrss_entries SET tsvector_combined = to_tsvector( :ts_content) WHERE id = :id"); @@ -158,39 +161,15 @@ class Article extends Handler_Protected { return $rc; } - function editArticleTags() { - - $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 "
" . __("Tags for this article (separated by commas):")."
"; - - print "
"; - print " - "; - print "
"; - - print ""; + function printArticleTags() { + $id = (int) clean($_REQUEST['id'] ?? 0); + print json_encode(["id" => $id, + "tags" => self::_get_tags($id)]); } function setScore() { - $ids = explode(",", clean($_REQUEST['id'])); + $ids = array_map("intval", clean($_REQUEST['ids'] ?? [])); $score = (int)clean($_REQUEST['score']); $ids_qmarks = arr_qmarks($ids); @@ -220,8 +199,10 @@ class Article extends Handler_Protected { $id = clean($_REQUEST["id"]); - $tags_str = clean($_REQUEST["tags_str"]); - $tags = array_unique(array_map('trim', explode(",", $tags_str))); + //$tags_str = clean($_REQUEST["tags_str"]); + //$tags = array_unique(array_map('trim', explode(",", $tags_str))); + + $tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"]))); $this->pdo->beginTransaction(); @@ -246,8 +227,6 @@ class Article extends Handler_Protected { (post_int_id, owner_uid, tag_name) VALUES (?, ?, ?)"); - $tags = FeedItem_Common::normalize_categories($tags); - foreach ($tags as $tag) { $csth->execute([$int_id, $_SESSION['uid'], $tag]); @@ -269,18 +248,12 @@ class Article extends Handler_Protected { $this->pdo->commit(); - $tags = self::get_article_tags($id); - $tags_str = $this->format_tags_string($tags); - $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)); + // get latest tags from the database, original $tags is sometimes JSON-encoded as a hash ({}) - ??? + print json_encode(["id" => (int)$id, "tags" => $this->_get_tags($id)]); } - function completeTags() { + /*function completeTags() { $search = clean($_REQUEST["search"]); $sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags @@ -295,17 +268,17 @@ class Article extends Handler_Protected { print "
  • " . $line["tag_name"] . "
  • "; } print ""; - } + }*/ function assigntolabel() { - return $this->labelops(true); + return $this->_label_ops(true); } function removefromlabel() { - return $this->labelops(false); + return $this->_label_ops(false); } - private function labelops($assign) { + private function _label_ops($assign) { $reply = array(); $ids = explode(",", clean($_REQUEST["ids"])); @@ -313,22 +286,17 @@ class Article extends Handler_Protected { $label = Labels::find_caption($label_id, $_SESSION["uid"]); - $reply["info-for-headlines"] = array(); + $reply["labels-for"] = []; if ($label) { - foreach ($ids as $id) { - if ($assign) Labels::add_article($id, $label, $_SESSION["uid"]); else Labels::remove_article($id, $label, $_SESSION["uid"]); - $labels = $this->get_article_labels($id, $_SESSION["uid"]); - - array_push($reply["info-for-headlines"], - array("id" => $id, "labels" => $this->format_article_labels($labels))); - + array_push($reply["labels-for"], + ["id" => (int)$id, "labels" => $this->_get_labels($id)]); } } @@ -337,163 +305,84 @@ class Article extends Handler_Protected { print json_encode($reply); } - function getArticleFeed($id) { - $sth = $this->pdo->prepare("SELECT feed_id FROM ttrss_user_entries - WHERE ref_id = ? AND owner_uid = ?"); - $sth->execute([$id, $_SESSION['uid']]); + static function _format_enclosures($id, + $always_display_enclosures, + $article_content, + $hide_images = false) { - if ($row = $sth->fetch()) { - return $row["feed_id"]; - } else { - return 0; - } - } + $enclosures = self::_get_enclosures($id); + $enclosures_formatted = ""; - static function format_article_enclosures($id, $always_display_enclosures, - $article_content, $hide_images = false) { - - $result = self::get_article_enclosures($id); - $rv = ''; + /*foreach ($enclosures as &$enc) { + array_push($enclosures, [ + "type" => $enc["content_type"], + "filename" => basename($enc["content_url"]), + "url" => $enc["content_url"], + "title" => $enc["title"], + "width" => (int) $enc["width"], + "height" => (int) $enc["height"] + ]); + }*/ PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_FORMAT_ENCLOSURES, - function ($result) use (&$rv) { + function ($result) use (&$enclosures_formatted, &$enclosures) { if (is_array($result)) { - $rv = $result[0]; - $result = $result[1]; + $enclosures_formatted = $result[0]; + $enclosures = $result[1]; } 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)) { - $entries_html = array(); - $entries = array(); - $entries_inline = array(); + if (!empty($enclosures_formatted)) { + return [ + 'formatted' => $enclosures_formatted, + 'entries' => [] + ]; + } - foreach ($result as $line) { + $rv = [ + 'formatted' => '', + 'entries' => [] + ]; + $rv['can_inline'] = isset($_SESSION["uid"]) && + empty($_SESSION["bw_limit"]) && + !get_pref("STRIP_IMAGES") && + ($always_display_enclosures || !preg_match("/chain_hooks_callback(PluginHost::HOOK_RENDER_ENCLOSURE, + function ($result) use (&$rendered_enc) { + $rendered_enc = $result; + }, + $enc, $id, $rv); + + if ($rendered_enc) { + $rv['formatted'] .= $rendered_enc; + } else { PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ENCLOSURE_ENTRY, - function($result) use (&$line) { - $line = $result; + function ($result) use (&$enc) { + $enc = $result; }, - $line, $id); + $enc, $id, $rv); - $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 .= " " . -# $filename . " (" . $ctype . ")" . ""; - - $entry = "
    $filename ($ctype)
    "; - - 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); + array_push($rv['entries'], $enc); } - - if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) { - if ($always_display_enclosures || - !preg_match("/chain_hooks_callback(PluginHost::HOOK_RENDER_ENCLOSURE, - function($result) use (&$retval) { - $retval = $result; - }, - $entry, $hide_images); - - if (!empty($retval)) { - $rv .= $retval; - } else { - - if (preg_match("/image/", $entry["type"])) { - - if (!$hide_images) { - $encsize = ''; - if ($entry['height'] > 0) - $encsize .= ' height="' . intval($entry['height']) . '"'; - if ($entry['width'] > 0) - $encsize .= ' width="' . intval($entry['width']) . '"'; - $rv .= "

    \"".htmlspecialchars($entry["filename"])."\"

    "; - } else { - $rv .= "

    " .htmlspecialchars($entry["url"]) . "

    "; - } - - if ($entry['title']) { - $rv.= "
    ${entry['title']}
    "; - } - } - } - } - } - } - - if (count($entries_inline) > 0) { - //$rv .= "
    "; - foreach ($entries_inline as $entry) { $rv .= $entry; }; - $rv .= "
    "; - } - - $rv .= "
    ". - "" . __('Attachments').""; - - $rv .= "
    "; - - 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 .= "
    ".$filename . $title."
    "; - - }; - - $rv .= "
    "; - $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; @@ -543,59 +432,22 @@ class Article extends Handler_Protected { return $tags; } - static function format_tags_string($tags) { - if (!is_array($tags) || count($tags) == 0) { - return __("no tags"); - } else { - $maxtags = min(5, count($tags)); - $tags_str = ""; + function getmetadatabyid() { + $id = clean($_REQUEST['id']); - for ($i = 0; $i < $maxtags; $i++) { - $tags_str .= "" . $tags[$i] . ", "; - } + $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']]); - $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) - $tags_str .= ", …"; - - return $tags_str; + echo json_encode(["link" => $link, "title" => $title]); } } - static function format_article_labels($labels) { - - if (!is_array($labels)) return ''; - - $labels_str = ""; - - foreach ($labels as $l) { - $labels_str .= sprintf("
    %s
    ", - $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 "
    - note -
    $note
    -
    "; - } - - static function get_article_enclosures($id) { + static function _get_enclosures($id) { $pdo = Db::pdo(); @@ -607,10 +459,10 @@ class Article extends Handler_Protected { $cache = new DiskCache("images"); - while ($line = $sth->fetch()) { + while ($line = $sth->fetch(PDO::FETCH_ASSOC)) { 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); @@ -619,11 +471,11 @@ class Article extends Handler_Protected { return $rv; } - static function purge_orphans() { + static function _purge_orphans() { // purge orphaned posts in main content table - if (DB_TYPE == "mysql") + if (Config::get(Config::DB_TYPE) == "mysql") $limit_qpart = "LIMIT 5000"; else $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"]; @@ -663,21 +515,7 @@ class Article extends Handler_Protected { $sth->execute(array_merge($ids, [$owner_uid])); } - static function getLastArticleId() { - $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) { + static function _get_labels($id, $owner_uid = false) { $rv = array(); if (!$owner_uid) $owner_uid = $_SESSION["uid"]; @@ -724,7 +562,7 @@ class Article extends Handler_Protected { return $rv; } - static function get_article_image($enclosures, $content, $site_url) { + static function _get_image($enclosures, $content, $site_url) { $article_image = ""; $article_stream = ""; @@ -794,12 +632,59 @@ class Article extends Handler_Protected { $cache = new DiskCache("images"); 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))) - $article_stream = $cache->getUrl(sha1($article_stream)); + $article_stream = $cache->get_url(sha1($article_stream)); 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; + } } diff --git a/classes/auth/base.php b/classes/auth/base.php index d54e9d8a2..f18cc2d2d 100644 --- a/classes/auth/base.php +++ b/classes/auth/base.php @@ -16,7 +16,7 @@ abstract class Auth_Base extends Plugin implements IAuthModule { // Auto-creates specified user if allowed by system configuration // Can be used instead of find_user_by_login() by external auth modules 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); if (!$user_id) { diff --git a/classes/backend.php b/classes/backend.php deleted file mode 100644 index aa1935f23..000000000 --- a/classes/backend.php +++ /dev/null @@ -1,90 +0,0 @@ -HTML"; - print $rv[0]; - print "

    Plain text

    "; - print "
    ".$rv[3]."
    "; - } 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 ""; - } - - print ""; - - } -} diff --git a/classes/config.php b/classes/config.php new file mode 100644 index 000000000..effbb78ad --- /dev/null +++ b/classes/config.php @@ -0,0 +1,167 @@ + [ "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); + } + +} diff --git a/classes/counters.php b/classes/counters.php index 59605df18..b4602825c 100644 --- a/classes/counters.php +++ b/classes/counters.php @@ -1,18 +1,27 @@ prepare("SELECT id FROM ttrss_feed_categories WHERE parent_cat = ? @@ -23,52 +32,86 @@ class Counters { $marked = 0; 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); - $marked += $tmp_marked + Feeds::getCategoryMarked($line["id"], $owner_uid); + $unread += $tmp_unread + Feeds::_get_cat_unread($line["id"], $owner_uid); + $marked += $tmp_marked + Feeds::_get_cat_marked($line["id"], $owner_uid); } return [$unread, $marked]; } - static function getCategoryCounters() { + private static function get_cats(array $cat_ids = null) { $ret = []; /* Labels category */ $cv = array("id" => -2, "kind" => "cat", - "counter" => Feeds::getCategoryUnread(-2)); + "counter" => Feeds::_get_cat_unread(-2)); array_push($ret, $cv); $pdo = Db::pdo(); - $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 = :uid - 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 = :uid"); + if (is_array($cat_ids)) { + if (count($cat_ids) == 0) + return []; - $sth->execute(["uid" => $_SESSION['uid']]); + $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, + 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 = :uid + 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 = :uid"); + + $sth->execute(["uid" => $_SESSION['uid']]); + } while ($line = $sth->fetch()) { 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 { $child_counter = 0; $child_marked_counter = 0; @@ -84,38 +127,53 @@ class Counters { array_push($ret, $cv); } - array_push($ret, $cv); - return $ret; } - - static function getFeedCounters($active_feed = false) { + private static function get_feeds(array $feed_ids = null) { $ret = []; $pdo = Db::pdo(); - $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 = :uid - GROUP BY f.id"); + if (is_array($feed_ids)) { + if (count($feed_ids) == 0) + return []; - $sth->execute(["uid" => $_SESSION['uid']]); + $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, + 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 = :uid + GROUP BY f.id"); + + $sth->execute(["uid" => $_SESSION['uid']]); + } while ($line = $sth->fetch()) { $id = $line["id"]; - $last_error = htmlspecialchars($line["last_error"]); $last_updated = TimeHelper::make_local_datetime($line['last_updated'], false); - if (Feeds::feedHasIcon($id)) { - $has_img = filemtime(Feeds::getIconFile($id)); + if (Feeds::_has_icon($id)) { + $has_img = filemtime(Feeds::_get_icon_file($id)); } else { $has_img = false; } @@ -132,11 +190,8 @@ class Counters { "has_img" => (int) $has_img ]; - if ($last_error) - $cv["error"] = $last_error; - - if ($active_feed && $id == $active_feed) - $cv["title"] = truncate_string($line["title"], 30); + $cv["error"] = $line["last_error"]; + $cv["title"] = truncate_string($line["title"], 30); array_push($ret, $cv); @@ -145,11 +200,11 @@ class Counters { return $ret; } - static function getGlobalCounters($global_unread = -1) { + private static function get_global($global_unread = -1) { $ret = []; if ($global_unread == -1) { - $global_unread = Feeds::getGlobalUnread(); + $global_unread = Feeds::_get_global_unread(); } $cv = [ @@ -178,7 +233,7 @@ class Counters { return $ret; } - static function getVirtCounters() { + private static function get_virt() { $ret = []; @@ -187,7 +242,7 @@ class Counters { $count = getFeedUnread($i); if ($i == 0 || $i == -1 || $i == -2) - $auxctr = Feeds::getFeedArticles($i, false); + $auxctr = Feeds::_get_counters($i, false); else $auxctr = 0; @@ -222,23 +277,42 @@ class Counters { return $ret; } - static function getLabelCounters($descriptions = false) { + static function get_labels(array $label_ids = null) { $ret = []; $pdo = Db::pdo(); - $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 = :uid - WHERE ttrss_labels2.owner_uid = :uid - GROUP BY ttrss_labels2.id, ttrss_labels2.caption"); - $sth->execute([":uid" => $_SESSION['uid']]); + 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, + 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 = :uid + WHERE ttrss_labels2.owner_uid = :uid + GROUP BY ttrss_labels2.id, ttrss_labels2.caption"); + $sth->execute([":uid" => $_SESSION['uid']]); + } while ($line = $sth->fetch()) { @@ -248,12 +322,10 @@ class Counters { "id" => $id, "counter" => (int) $line["count_unread"], "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); } diff --git a/classes/db.php b/classes/db.php index 6199c82bb..a760d4402 100755 --- a/classes/db.php +++ b/classes/db.php @@ -1,13 +1,9 @@ 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 // normal usage is Db::pdo()->prepare(...) etc public function pdo_connect() { - $db_port = defined('DB_PORT') && DB_PORT ? ';port=' . DB_PORT : ''; - $db_host = defined('DB_HOST') && DB_HOST ? ';host=' . DB_HOST : ''; + $db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : ''; + $db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : ''; try { - $pdo = new PDO(DB_TYPE . ':dbname=' . DB_NAME . $db_host . $db_port, - DB_USER, - DB_PASS); + $pdo = new PDO(Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port, + Config::get(Config::DB_USER), + Config::get(Config::DB_PASS)); } catch (Exception $e) { print "
    Exception while creating PDO object:" . $e->getMessage() . "
    "; exit(101); @@ -67,18 +31,18 @@ class Db $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 datestyle = 'ISO, european'"); $pdo->query("set TIME ZONE 0"); $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'"); - if (defined('MYSQL_CHARSET') && MYSQL_CHARSET) { - $pdo->query("SET NAMES " . MYSQL_CHARSET); + if (Config::get(Config::MYSQL_CHARSET)) { + $pdo->query("SET NAMES " . Config::get(Config::MYSQL_CHARSET)); } } @@ -92,17 +56,6 @@ class Db 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 { if (self::$instance == null) self::$instance = new self(); @@ -115,7 +68,7 @@ class Db } public static function sql_random_function() { - if (DB_TYPE == "mysql") { + if (Config::get(Config::DB_TYPE) == "mysql") { return "RAND()"; } else { return "RANDOM()"; diff --git a/classes/db/mysqli.php b/classes/db/mysqli.php deleted file mode 100644 index a05b121fc..000000000 --- a/classes/db/mysqli.php +++ /dev/null @@ -1,85 +0,0 @@ -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; - } - -} diff --git a/classes/db/pgsql.php b/classes/db/pgsql.php deleted file mode 100644 index 98fab6bea..000000000 --- a/classes/db/pgsql.php +++ /dev/null @@ -1,91 +0,0 @@ - 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; - } -} \ No newline at end of file diff --git a/classes/db/prefs.php b/classes/db/prefs.php index 24153b19a..44581dbcb 100644 --- a/classes/db/prefs.php +++ b/classes/db/prefs.php @@ -6,9 +6,8 @@ class Db_Prefs { function __construct() { $this->pdo = Db::pdo(); - $this->cache = array(); - - if (!empty($_SESSION["uid"])) $this->cache(); + $this->cache = []; + $this->cache_prefs(); } private function __clone() { @@ -22,31 +21,30 @@ class Db_Prefs { return self::$instance; } - function cache() { - $user_id = $_SESSION["uid"]; - $profile = $_SESSION["profile"] ?? false; + private function cache_prefs() { + if (!empty($_SESSION["uid"])) { + $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 - value,ttrss_prefs_types.type_name as type_name,ttrss_prefs.pref_name AS pref_name - FROM - ttrss_user_prefs,ttrss_prefs,ttrss_prefs_types - WHERE - (profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND - ttrss_prefs.pref_name NOT LIKE '_MOBILE%' AND - ttrss_prefs_types.id = type_id AND - owner_uid = :uid AND - ttrss_user_prefs.pref_name = ttrss_prefs.pref_name"); + $sth = $this->pdo->prepare("SELECT up.pref_name, pt.type_name, up.value + FROM ttrss_user_prefs up + JOIN ttrss_prefs p ON (up.pref_name = p.pref_name) + JOIN ttrss_prefs_types pt ON (p.type_id = pt.id) + WHERE + up.pref_name NOT LIKE '_MOBILE%' AND + (profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND + owner_uid = :uid"); - $sth->execute([":profile" => $profile, ":uid" => $user_id]); + $sth->execute([":profile" => $profile, ":uid" => $_SESSION["uid"]]); - while ($line = $sth->fetch()) { - if ($user_id == $_SESSION["uid"]) { - $pref_name = $line["pref_name"]; + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $pref_name = $row["pref_name"]; - $this->cache[$pref_name]["type"] = $line["type_name"]; - $this->cache[$pref_name]["value"] = $line["value"]; + $this->cache[$pref_name] = [ + "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; - $sth = $this->pdo->prepare("SELECT - value,ttrss_prefs_types.type_name as type_name - FROM - ttrss_user_prefs,ttrss_prefs,ttrss_prefs_types + $sth = $this->pdo->prepare("SELECT up.pref_name, pt.type_name, up.value + FROM ttrss_user_prefs up + JOIN ttrss_prefs p ON (up.pref_name = p.pref_name) + JOIN ttrss_prefs_types pt ON (p.type_id = pt.id) WHERE + up.pref_name = :pref_name AND (profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND - ttrss_user_prefs.pref_name = :pref_name AND - ttrss_prefs_types.id = type_id AND - owner_uid = :uid AND - ttrss_user_prefs.pref_name = ttrss_prefs.pref_name"); + owner_uid = :uid"); + $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"]; $type_name = $row["type_name"]; if ($user_id == ($_SESSION["uid"] ?? false)) { - $this->cache[$pref_name]["type"] = $type_name; - $this->cache[$pref_name]["value"] = $value; + $this->cache[$pref_name] = [ + "type" => $row["type_name"], + "value" => $row["value"] + ]; } return $this->convert($value, $type_name); } else if ($die_on_error) { - user_error("Fatal error, unknown preferences key: $pref_name (owner: $user_id)", E_USER_ERROR); - return null; + user_error("Failed retrieving preference $pref_name for user $user_id", E_USER_ERROR); } else { - return null; + user_error("Failed retrieving preference $pref_name for user $user_id", E_USER_WARNING); } + + return null; } function convert($value, $type_name) { diff --git a/classes/dbupdater.php b/classes/dbupdater.php index 3cc6e9125..e923c7fcb 100644 --- a/classes/dbupdater.php +++ b/classes/dbupdater.php @@ -11,16 +11,16 @@ class DbUpdater { $this->need_version = (int) $need_version; } - function getSchemaVersion() { + function get_schema_version() { $row = $this->pdo->query("SELECT schema_version FROM ttrss_version")->fetch(); return (int) $row['schema_version']; } - function isUpdateRequired() { - return $this->getSchemaVersion() < $this->need_version; + function is_update_required() { + return $this->get_schema_version() < $this->need_version; } - function getSchemaLines($version) { + function get_schema_lines($version) { $filename = "schema/versions/".$this->db_type."/$version.sql"; if (file_exists($filename)) { @@ -31,10 +31,10 @@ class DbUpdater { } } - function performUpdateTo($version, $html_output = true) { - if ($this->getSchemaVersion() == $version - 1) { + function update_to($version, $html_output = true) { + if ($this->get_schema_version() == $version - 1) { - $lines = $this->getSchemaLines($version); + $lines = $this->get_schema_lines($version); if (is_array($lines)) { @@ -63,7 +63,7 @@ class DbUpdater { } } - $db_version = $this->getSchemaVersion(); + $db_version = $this->get_schema_version(); if ($db_version == $version) { $this->pdo->commit(); diff --git a/classes/digest.php b/classes/digest.php index 7790424ca..a6a0c47de 100644 --- a/classes/digest.php +++ b/classes/digest.php @@ -1,12 +1,6 @@ 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"], "to_address" => $line["email"], - "subject" => DIGEST_SUBJECT, + "subject" => Config::get(Config::DIGEST_SUBJECT), "message" => $digest_text, "message_html" => $digest]); @@ -68,7 +62,7 @@ class Digest if ($rc && $do_catchup) { Debug::log("Marking affected articles as read..."); - Article::catchupArticlesById($affected_ids, 0, $line["id"]); + Article::_catchup_by_id($affected_ids, 0, $line["id"]); } } else { Debug::log("No headlines"); @@ -81,9 +75,7 @@ class Digest } } } - Debug::log("All done."); - } 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_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_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(); $days = (int) $days; - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $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)"; } @@ -164,7 +156,7 @@ class Digest $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 = ""; if (is_array($article_labels) && count($article_labels) > 0) { @@ -210,5 +202,4 @@ class Digest return array($tmp, $headlines_count, $affected_ids, $tmp_t); } - } diff --git a/classes/diskcache.php b/classes/diskcache.php index 3fd099d3c..9c594acc5 100644 --- a/classes/diskcache.php +++ b/classes/diskcache.php @@ -191,23 +191,23 @@ class DiskCache { ]; 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; } - public function makeDir() { + public function make_dir() { if (!is_dir($this->dir)) { return mkdir($this->dir); } } - public function isWritable($filename = "") { + public function is_writable($filename = "") { if ($filename) { - if (file_exists($this->getFullPath($filename))) - return is_writable($this->getFullPath($filename)); + if (file_exists($this->get_full_path($filename))) + return is_writable($this->get_full_path($filename)); else return is_writable($this->dir); } else { @@ -216,44 +216,44 @@ class DiskCache { } 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)) - return filesize($this->getFullPath($filename)); + return filesize($this->get_full_path($filename)); else return -1; } - public function getFullPath($filename) { + public function get_full_path($filename) { return $this->dir . "/" . basename(clean($filename)); } 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) { - return touch($this->getFullPath($filename)); + return touch($this->get_full_path($filename)); } public function get($filename) { if ($this->exists($filename)) - return file_get_contents($this->getFullPath($filename)); + return file_get_contents($this->get_full_path($filename)); else return null; } - public function getMimeType($filename) { + public function get_mime_type($filename) { if ($this->exists($filename)) - return mime_content_type($this->getFullPath($filename)); + return mime_content_type($this->get_full_path($filename)); else return null; } - public function getFakeExtension($filename) { - $mimetype = $this->getMimeType($filename); + public function get_fake_extension($filename) { + $mimetype = $this->get_mime_type($filename); if ($mimetype) return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : ""; @@ -262,25 +262,25 @@ class DiskCache { } public function send($filename) { - $fake_extension = $this->getFakeExtension($filename); + $fake_extension = $this->get_fake_extension($filename); if ($fake_extension) $fake_extension = ".$fake_extension"; header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\""); - return $this->send_local_file($this->getFullPath($filename)); + return $this->send_local_file($this->get_full_path($filename)); } - public function getUrl($filename) { - return get_self_url_prefix() . "/public.php?op=cached_url&file=" . basename($this->dir) . "/" . basename($filename); + public function get_url($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 // this is called separately after sanitize() and plugin render article hooks to allow // plugins work on original source URLs used before caching // 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); if (!$res) return ''; @@ -301,7 +301,7 @@ class DiskCache { $cached_filename = sha1($url); if ($cache->exists($cached_filename)) { - $url = $cache->getUrl($cached_filename); + $url = $cache->get_url($cached_filename); $entry->setAttribute($attr, $url); $entry->removeAttribute("srcset"); @@ -318,7 +318,7 @@ class DiskCache { $cached_filename = sha1($matches[$i]["url"]); if ($cache->exists($cached_filename)) { - $matches[$i]["url"] = $cache->getUrl($cached_filename); + $matches[$i]["url"] = $cache->get_url($cached_filename); $need_saving = true; } @@ -339,7 +339,7 @@ class DiskCache { } 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) { $num_deleted = 0; @@ -349,7 +349,7 @@ class DiskCache { if ($files) { 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); ++$num_deleted; @@ -396,7 +396,7 @@ class DiskCache { $tmppluginhost = new PluginHost(); - $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM); + $tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_SYSTEM); //$tmppluginhost->load_data(); if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename)) diff --git a/classes/errors.php b/classes/errors.php new file mode 100644 index 000000000..be175418e --- /dev/null +++ b/classes/errors.php @@ -0,0 +1,12 @@ + ["code" => $code]]); + } +} diff --git a/classes/feeditem.php b/classes/feeditem.php index 3a5e5dc09..e30df3086 100644 --- a/classes/feeditem.php +++ b/classes/feeditem.php @@ -9,7 +9,7 @@ abstract class FeedItem { abstract function get_comments_url(); abstract function get_comments_count(); abstract function get_categories(); - abstract function get_enclosures(); + abstract function _get_enclosures(); abstract function get_author(); abstract function get_language(); } diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php index a03080981..3e092a048 100755 --- a/classes/feeditem/atom.php +++ b/classes/feeditem/atom.php @@ -119,7 +119,7 @@ class FeedItem_Atom extends FeedItem_Common { return $this->normalize_categories($cats); } - function get_enclosures() { + function _get_enclosures() { $links = $this->elem->getElementsByTagName("link"); $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; } diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php index 1e9d62228..8f2b9188b 100755 --- a/classes/feeditem/common.php +++ b/classes/feeditem/common.php @@ -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 - function get_enclosures() { + function _get_enclosures() { $encs = []; $enclosures = $this->xpath->query("media:content", $this->elem); @@ -179,7 +179,7 @@ abstract class FeedItem_Common extends FeedItem { $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); } diff --git a/classes/feeditem/rss.php b/classes/feeditem/rss.php index 1f7953c51..f103ad787 100755 --- a/classes/feeditem/rss.php +++ b/classes/feeditem/rss.php @@ -112,7 +112,7 @@ class FeedItem_RSS extends FeedItem_Common { return $this->normalize_categories($cats); } - function get_enclosures() { + function _get_enclosures() { $enclosures = $this->elem->getElementsByTagName("enclosure"); $encs = array(); @@ -129,7 +129,7 @@ class FeedItem_RSS extends FeedItem_Common { array_push($encs, $enc); } - $encs = array_merge($encs, parent::get_enclosures()); + $encs = array_merge($encs, parent::_get_enclosures()); return $encs; } diff --git a/classes/feeds.php b/classes/feeds.php index 031a671ae..ba2719f48 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -16,110 +16,13 @@ class Feeds extends Handler_Protected { return array_search($method, $csrf_ignored) !== false; } - private function format_headline_subtoolbar($feed_site_url, $feed_title, - $feed_id, $is_cat, $search, - $error, $feed_last_updated) { - - $cat_q = $is_cat ? "&is_cat=$is_cat" : ""; - - if ($search) { - $search_q = "&q=$search"; - } else { - $search_q = ""; - } - - $reply = ""; - - $rss_link = htmlspecialchars(get_self_url_prefix() . - "/public.php?op=rss&id=${feed_id}${cat_q}${search_q}"); - - $reply .= ""; - - $reply .= " - rss_feed"; - - $reply .= ""; - - if ($feed_site_url) { - $last_updated = T_sprintf("Last updated: %s", $feed_last_updated); - - $reply .= "". - truncate_string(strip_tags($feed_title), 30).""; - } else { - $reply .= strip_tags($feed_title); - } - - if ($error) - $reply .= " error"; - - $reply .= ""; - $reply .= ""; - $reply .= ""; - - $reply .= ""; - $reply .= ""; - $reply .= " "; - - $reply .= "
    - ".__("Select...")." -
    -
    ".__('All')."
    -
    ".__('Unread')."
    -
    ".__('Invert')."
    -
    ".__('None')."
    -
    -
    ".__('Toggle unread')."
    -
    ".__('Toggle starred')."
    -
    ".__('Toggle published')."
    -
    -
    ".__('Mark as read')."
    -
    ".__('Set score')."
    "; - - // TODO: move to mail plugin - if (PluginHost::getInstance()->get_plugin("mail")) { - $reply .= "
    ".__('Forward by email')."
    "; - } - - // TODO: move to mailto plugin - if (PluginHost::getInstance()->get_plugin("mailto")) { - $reply .= "
    ".__('Forward by email')."
    "; - } - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, - function ($result) use (&$reply) { - $reply .= $result; - }, - $feed_id, $is_cat); - - if ($feed_id == 0 && !$is_cat) { - $reply .= "
    -
    ".__('Delete permanently')."
    "; - } - - $reply .= "
    "; /* menu */ - - $reply .= "
    "; /* dropdown */ - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON, - function ($result) use (&$reply) { - $reply .= $result; - }, - $feed_id, $is_cat); - - $reply .= "
    "; - - return $reply; - } - - private function format_headlines_list($feed, $method, $view_mode, $limit, $cat_view, + private function _format_headlines_list($feed, $method, $view_mode, $limit, $cat_view, $offset, $override_order = false, $include_children = false, $check_first_id = false, $skip_first_id_check = false, $order_by = false) { $disable_cache = false; - $this->mark_timestamp("init"); + $this->_mark_timestamp("init"); $reply = []; $rgba_cache = []; @@ -138,7 +41,7 @@ class Feeds extends Handler_Protected { } if ($method_split[0] == "MarkAllReadGR") { - $this->catchup_feed($method_split[1], false); + $this->_catchup($method_split[1], false); } // FIXME: might break tag display? @@ -200,10 +103,10 @@ class Feeds extends Handler_Protected { "order_by" => $order_by ); - $qfh_ret = $this->queryFeedHeadlines($params); + $qfh_ret = $this->_get_headlines($params); } - $this->mark_timestamp("db query"); + $this->_mark_timestamp("db query"); $vfeed_group_enabled = get_pref("VFEED_GROUP_BY_FEED") && !(in_array($feed, self::NEVER_GROUP_FEEDS) && !$cat_view); @@ -222,10 +125,28 @@ class Feeds extends Handler_Protected { $reply['search_query'] = [$search, $search_language]; $reply['vfeed_group_enabled'] = $vfeed_group_enabled; - $reply['toolbar'] = $this->format_headline_subtoolbar($feed_site_url, - $feed_title, - $feed, $cat_view, $search, - $last_error, $last_updated); + $plugin_menu_items = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, + function ($result) use (&$plugin_menu_items) { + $plugin_menu_items .= $result; + }, + $feed, $cat_view); + + $plugin_buttons = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON, + function ($result) use (&$plugin_buttons) { + $plugin_buttons .= $result; + }, + $feed, $cat_view); + + $reply['toolbar'] = [ + 'site_url' => $feed_site_url, + 'title' => strip_tags($feed_title), + 'error' => $last_error, + 'last_updated' => $last_updated, + 'plugin_menu_items' => $plugin_menu_items, + 'plugin_buttons' => $plugin_buttons, + ]; $reply['content'] = []; @@ -236,13 +157,13 @@ class Feeds extends Handler_Protected { }, $feed, $cat_view, $qfh_ret); - $this->mark_timestamp("object header"); + $this->_mark_timestamp("object header"); $headlines_count = 0; if ($result instanceof PDOStatement) { while ($line = $result->fetch(PDO::FETCH_ASSOC)) { - $this->mark_timestamp("article start: " . $line["id"] . " " . $line["title"]); + $this->_mark_timestamp("article start: " . $line["id"] . " " . $line["title"]); ++$headlines_count; @@ -260,12 +181,12 @@ class Feeds extends Handler_Protected { $line, $max_excerpt_length); } - $this->mark_timestamp(" hook_query_headlines"); + $this->_mark_timestamp(" hook_query_headlines"); $id = $line["id"]; // frontend doesn't expect pdo returning booleans as strings on mysql - if (DB_TYPE == "mysql") { + if (Config::get(Config::DB_TYPE) == "mysql") { foreach (["unread", "marked", "published"] as $k) { $line[$k] = $line[$k] === "1"; } @@ -293,19 +214,15 @@ class Feeds extends Handler_Protected { } } - if (!is_array($labels)) $labels = Article::get_article_labels($id); + if (!is_array($labels)) $labels = Article::_get_labels($id); - $labels_str = ""; - $labels_str .= Article::format_article_labels($labels); - $labels_str .= ""; - - $line["labels"] = $labels_str; + $line["labels"] = Article::_get_labels($id); if (count($topmost_article_ids) < 3) { array_push($topmost_article_ids, $id); } - $this->mark_timestamp(" labels"); + $this->_mark_timestamp(" labels"); $line["feed_title"] = $line["feed_title"] ?? ""; @@ -323,32 +240,27 @@ class Feeds extends Handler_Protected { }, $line); - $this->mark_timestamp(" pre-sanitize"); + $this->_mark_timestamp(" pre-sanitize"); $line["content"] = Sanitizer::sanitize($line["content"], $line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]); - $this->mark_timestamp(" sanitize"); + $this->_mark_timestamp(" sanitize"); PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM, function ($result, $plugin) use (&$line) { $line = $result; - $this->mark_timestamp(" hook_render_cdm: " . get_class($plugin)); + $this->_mark_timestamp(" hook_render_cdm: " . get_class($plugin)); }, $line); - $this->mark_timestamp(" hook_render_cdm"); + $this->_mark_timestamp(" hook_render_cdm"); - $line['content'] = DiskCache::rewriteUrls($line['content']); + $line['content'] = DiskCache::rewrite_urls($line['content']); - $this->mark_timestamp(" disk_cache_rewrite"); + $this->_mark_timestamp(" disk_cache_rewrite"); - if ($line['note']) - $line['note'] = Article::format_article_note($id, $line['note']); - else - $line['note'] = ""; - - $this->mark_timestamp(" note"); + $this->_mark_timestamp(" note"); if (!get_pref("CDM_EXPANDED")) { $line["cdm_excerpt"] = " @@ -360,12 +272,14 @@ class Feeds extends Handler_Protected { } } - $this->mark_timestamp(" pre-enclosures"); + $this->_mark_timestamp(" pre-enclosures"); - $line["enclosures"] = Article::format_article_enclosures($id, $line["always_display_enclosures"], - $line["content"], $line["hide_images"]); + $line["enclosures"] = Article::_format_enclosures($id, + $line["always_display_enclosures"], + $line["content"], + $line["hide_images"]); - $this->mark_timestamp(" enclosures"); + $this->_mark_timestamp(" enclosures"); $line["updated_long"] = TimeHelper::make_local_datetime($line["updated"],true); $line["updated"] = TimeHelper::make_local_datetime($line["updated"], false, false, false, true); @@ -373,35 +287,31 @@ class Feeds extends Handler_Protected { $line['imported'] = T_sprintf("Imported at %s", TimeHelper::make_local_datetime($line["date_entered"], false)); - $this->mark_timestamp(" local-datetime"); + $this->_mark_timestamp(" local-datetime"); if ($line["tag_cache"]) $tags = explode(",", $line["tag_cache"]); else $tags = false; - $line["tags_str"] = Article::format_tags_string($tags); + $line["tags"] = Article::_get_tags($line["id"], false, $line["tag_cache"]); - $this->mark_timestamp(" tags"); + $this->_mark_timestamp(" tags"); - if (self::feedHasIcon($feed_id)) { - $line['feed_icon'] = "\"\""; - } else { - $line['feed_icon'] = "rss_feed"; - } + $line['has_icon'] = self::_has_icon($feed_id); //setting feed headline background color, needs to change text color based on dark/light $fav_color = $line['favicon_avg_color'] ?? false; - $this->mark_timestamp(" pre-color"); + $this->_mark_timestamp(" pre-color"); require_once "colors.php"; if (!isset($rgba_cache[$feed_id])) { if ($fav_color && $fav_color != 'fail') { - $rgba_cache[$feed_id] = _color_unpack($fav_color); + $rgba_cache[$feed_id] = \Colors\_color_unpack($fav_color); } else { - $rgba_cache[$feed_id] = _color_unpack($this->color_of($line['feed_title'])); + $rgba_cache[$feed_id] = \Colors\_color_unpack($this->_color_of($line['feed_title'])); } } @@ -409,7 +319,7 @@ class Feeds extends Handler_Protected { $line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)'; } - $this->mark_timestamp(" color"); + $this->_mark_timestamp(" color"); /* we don't need those */ @@ -419,11 +329,11 @@ class Feeds extends Handler_Protected { array_push($reply['content'], $line); - $this->mark_timestamp("article end"); + $this->_mark_timestamp("article end"); } } - $this->mark_timestamp("end of articles"); + $this->_mark_timestamp("end of articles"); if (!$headlines_count) { @@ -485,7 +395,7 @@ class Feeds extends Handler_Protected { } } - $this->mark_timestamp("end"); + $this->_mark_timestamp("end"); return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply); } @@ -494,6 +404,8 @@ class Feeds extends Handler_Protected { $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?"); $sth->execute([$_SESSION['uid']]); + + print json_encode(array("message" => "UPDATE_COUNTERS")); } function view() { @@ -506,7 +418,7 @@ class Feeds extends Handler_Protected { $cat_view = $_REQUEST["cat"] == "true"; $next_unread_feed = $_REQUEST["nuf"] ?? 0; $offset = $_REQUEST["skip"] ?? 0; - $order_by = $_REQUEST["order_by"]; + $order_by = $_REQUEST["order_by"] ?? ""; $check_first_id = $_REQUEST["fid"] ?? 0; if (is_numeric($feed)) $feed = (int) $feed; @@ -515,7 +427,7 @@ class Feeds extends Handler_Protected { * when there's nothing to load - e.g. no stuff in fresh feed */ if ($feed == -5) { - print json_encode($this->generate_dashboard_feed()); + print json_encode($this->_generate_dashboard_feed()); return; } @@ -543,7 +455,7 @@ class Feeds extends Handler_Protected { } if ($sth && !$sth->fetch()) { - print json_encode($this->generate_error_feed(__("Feed not found."))); + print json_encode($this->_generate_error_feed(__("Feed not found."))); return; } @@ -566,9 +478,9 @@ class Feeds extends Handler_Protected { $reply['headlines'] = []; - list($override_order, $skip_first_id_check) = self::order_to_override_query($order_by); + list($override_order, $skip_first_id_check) = self::_order_to_override_query($order_by); - $ret = $this->format_headlines_list($feed, $method, + $ret = $this->_format_headlines_list($feed, $method, $view_mode, $limit, $cat_view, $offset, $override_order, true, $check_first_id, $skip_first_id_check, $order_by); @@ -589,18 +501,10 @@ class Feeds extends Handler_Protected { // this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc $reply['runtime-info'] = RPC::make_runtime_info(); - $reply_json = json_encode($reply); - - if (!$reply_json) { - $reply_json = json_encode(["error" => ["code" => 15, - "message" => json_last_error_msg()]]); - } - - print $reply_json; - + print json_encode($reply); } - private function generate_dashboard_feed() { + private function _generate_dashboard_feed() { $reply = array(); $reply['headlines']['id'] = -5; @@ -642,7 +546,7 @@ class Feeds extends Handler_Protected { return $reply; } - private function generate_error_feed($error) { + private function _generate_error_feed($error) { $reply = array(); $reply['headlines']['id'] = -7; @@ -658,131 +562,22 @@ class Feeds extends Handler_Protected { return $reply; } - function quickAddFeed() { - print "
    "; - - print_hidden("op", "rpc"); - print_hidden("method", "addfeed"); - - print ""; - - print ""; - - print "
    "; - - print "
    "; - print "
    "; - print ""; - - print "
    "; - - print "
    "; - - if (get_pref('ENABLE_FEED_CATS')) { - print " "; - print_feed_cat_select("cat", false, 'dojoType="fox.form.Select"'); - } - - print "
    "; - - print "
    "; - - print ''; - - print ""; - - print "
    "; - print "
    "; - - print "
    "; - print ""; - - print ""; - print "
    "; - - print "
    "; + function subscribeToFeed() { + print json_encode([ + "cat_select" => \Controls\select_feeds_cats("cat") + ]); } function search() { - $this->params = explode(":", $_REQUEST["param"], 2); - - $active_feed_id = sprintf("%d", $this->params[0]); - $is_cat = $this->params[1] != "false"; - - print "
    "; - - print "
    "; - - print "
    "; - print "getFeedTitle($active_feed_id, $is_cat))."\" - name='query' type='search' value=''>"; - print "
    "; - - if (DB_TYPE == "pgsql") { - print "
    "; - print ""; - print_select("search_language", get_pref('DEFAULT_SEARCH_LANGUAGE'), Pref_Feeds::get_ts_languages(), - "dojoType='fox.form.Select' title=\"".__('Used for word stemming')."\""); - print "
    "; - } - - print "
    "; - - print "
    "; - - if (count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0) { - print ""; - } - - print " - "; - - print "
    "; - - print "
    "; + print json_encode([ + "show_language" => Config::get(Config::DB_TYPE) == "pgsql", + "show_syntax_help" => count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0, + "all_languages" => Pref_Feeds::get_ts_languages(), + "default_language" => get_pref('DEFAULT_SEARCH_LANGUAGE') + ]); } - function update_debugger() { + function updatedebugger() { header("Content-type: text/html"); $xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : 1; @@ -801,10 +596,6 @@ class Feeds extends Handler_Protected { print "Access denied."; return; } - - $refetch_checked = isset($_REQUEST["force_refetch"]) ? "checked" : ""; - $rehash_checked = isset($_REQUEST["force_rehash"]) ? "checked" : ""; - ?> @@ -820,16 +611,23 @@ class Feeds extends Handler_Protected { display : none; } - + + + + +
    -

    Feed Debugger: getFeedTitle($feed_id) ?>

    +

    Feed Debugger: _get_title($feed_id) ?>

    -
    - - - - - + + + + + +
    - +
    - +
    - +

    @@ -883,7 +680,7 @@ class Feeds extends Handler_Protected { } - static function catchup_feed($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) { + static function _catchup($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) { if (!$owner_uid) $owner_uid = $_SESSION['uid']; @@ -903,7 +700,7 @@ class Feeds extends Handler_Protected { // fall back in case of no plugins if (empty($search_qpart)) { - list($search_qpart, $search_words) = self::search_to_sql($search[0], $search[1], $owner_uid); + list($search_qpart, $search_words) = self::_search_to_sql($search[0], $search[1], $owner_uid); } } else { $search_qpart = "true"; @@ -913,21 +710,21 @@ class Feeds extends Handler_Protected { switch ($mode) { case "1day": - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $date_qpart = "date_entered < NOW() - INTERVAL '1 day' "; } else { $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 DAY) "; } break; case "1week": - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $date_qpart = "date_entered < NOW() - INTERVAL '1 week' "; } else { $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 WEEK) "; } break; case "2week": - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $date_qpart = "date_entered < NOW() - INTERVAL '2 week' "; } else { $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 2 WEEK) "; @@ -943,7 +740,7 @@ class Feeds extends Handler_Protected { if ($feed >= 0) { if ($feed > 0) { - $children = self::getChildCategories($feed, $owner_uid); + $children = self::_get_child_cats($feed, $owner_uid); array_push($children, $feed); $children = array_map("intval", $children); @@ -1004,7 +801,7 @@ class Feeds extends Handler_Protected { $intl = (int) get_pref("FRESH_ARTICLE_MAX_AGE"); - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $match_part = "date_entered > NOW() - INTERVAL '$intl hour' "; } else { $match_part = "date_entered > DATE_SUB(NOW(), @@ -1054,7 +851,7 @@ class Feeds extends Handler_Protected { } } - static function getFeedArticles($feed, $is_cat = false, $unread_only = false, + static function _get_counters($feed, $is_cat = false, $unread_only = false, $owner_uid = false) { $n_feed = (int) $feed; @@ -1073,7 +870,7 @@ class Feeds extends Handler_Protected { $match_part = ""; if ($is_cat) { - return self::getCategoryUnread($n_feed, $owner_uid); + return self::_get_cat_unread($n_feed, $owner_uid); } else if ($n_feed == -6) { return 0; } else if ($feed != "0" && $n_feed == 0) { @@ -1097,7 +894,7 @@ class Feeds extends Handler_Protected { $intl = (int) get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid); - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $match_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' "; } else { $match_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) "; @@ -1119,7 +916,7 @@ class Feeds extends Handler_Protected { $label_id = Labels::feed_to_label_id($feed); - return self::getLabelUnread($label_id, $owner_uid); + return self::_get_label_unread($label_id, $owner_uid); } if ($match_part) { @@ -1154,6 +951,18 @@ class Feeds extends Handler_Protected { } } + function add() { + $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($feed, $cat, $login, $pass); + + print json_encode(array("result" => $rc)); + } + /** * @return array (code => Status code, message => error message if available) * @@ -1167,7 +976,7 @@ class Feeds extends Handler_Protected { * 5 - Couldn't download the URL content. * 6 - Content is an invalid XML. */ - static function subscribe_to_feed($url, $cat_id = 0, + static function _subscribe($url, $cat_id = 0, $auth_login = '', $auth_pass = '') { global $fetch_last_error; @@ -1196,8 +1005,8 @@ class Feeds extends Handler_Protected { return array("code" => 5, "message" => $fetch_last_error); } - if (mb_strpos($fetch_last_content_type, "html") !== false && self::is_html($contents)) { - $feedUrls = self::get_feeds_from_html($url, $contents); + if (mb_strpos($fetch_last_content_type, "html") !== false && self::_is_html($contents)) { + $feedUrls = self::_get_feeds_from_html($url, $contents); if (count($feedUrls) == 0) { return array("code" => 3); @@ -1240,42 +1049,36 @@ class Feeds extends Handler_Protected { } } - static function getIconFile($feed_id) { - return ICONS_DIR . "/$feed_id.ico"; + static function _get_icon_file($feed_id) { + return Config::get(Config::ICONS_DIR) . "/$feed_id.ico"; } - static function feedHasIcon($id) { - return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0; + static function _has_icon($id) { + return is_file(Config::get(Config::ICONS_DIR) . "/$id.ico") && filesize(Config::get(Config::ICONS_DIR) . "/$id.ico") > 0; } - static function getFeedIcon($id) { + static function _get_icon($id) { switch ($id) { case 0: return "archive"; - break; case -1: return "star"; - break; case -2: return "rss_feed"; - break; case -3: return "whatshot"; - break; case -4: return "inbox"; - break; case -6: return "restore"; - break; default: if ($id < LABEL_BASE_INDEX) { return "label"; } else { - $icon = self::getIconFile($id); + $icon = self::_get_icon_file($id); if ($icon && file_exists($icon)) { - return ICONS_URL . "/" . basename($icon) . "?" . filemtime($icon); + return Config::get(Config::ICONS_URL) . "/" . basename($icon) . "?" . filemtime($icon); } } break; @@ -1284,11 +1087,23 @@ class Feeds extends Handler_Protected { return false; } - static function getFeedTitle($id, $cat = false) { + static function _find_by_url($feed_url, $owner_uid) { + $sth = Db::pdo()->prepare("SELECT id FROM ttrss_feeds WHERE + feed_url = ? AND owner_uid = ?"); + $sth->execute([$feed_url, $owner_uid]); + + if ($row = $sth->fetch()) { + return $row["id"]; + } + + return false; + } + + static function _get_title($id, $cat = false) { $pdo = Db::pdo(); if ($cat) { - return self::getCategoryTitle($id); + return self::_get_cat_title($id); } else if ($id == -1) { return __("Starred articles"); } else if ($id == -2) { @@ -1331,7 +1146,7 @@ class Feeds extends Handler_Protected { } // only real cats - static function getCategoryMarked($cat, $owner_uid = false) { + static function _get_cat_marked($cat, $owner_uid = false) { if (!$owner_uid) $owner_uid = $_SESSION["uid"]; @@ -1354,7 +1169,7 @@ class Feeds extends Handler_Protected { } } - static function getCategoryUnread($cat, $owner_uid = false) { + static function _get_cat_unread($cat, $owner_uid = false) { if (!$owner_uid) $owner_uid = $_SESSION["uid"]; @@ -1388,7 +1203,7 @@ class Feeds extends Handler_Protected { } // only accepts real cats (>= 0) - static function getCategoryChildrenUnread($cat, $owner_uid = false) { + static function _get_cat_children_unread($cat, $owner_uid = false) { if (!$owner_uid) $owner_uid = $_SESSION["uid"]; $pdo = Db::pdo(); @@ -1400,14 +1215,14 @@ class Feeds extends Handler_Protected { $unread = 0; while ($line = $sth->fetch()) { - $unread += self::getCategoryUnread($line["id"], $owner_uid); - $unread += self::getCategoryChildrenUnread($line["id"], $owner_uid); + $unread += self::_get_cat_unread($line["id"], $owner_uid); + $unread += self::_get_cat_children_unread($line["id"], $owner_uid); } return $unread; } - static function getGlobalUnread($user_id = false) { + static function _get_global_unread($user_id = false) { if (!$user_id) $user_id = $_SESSION["uid"]; @@ -1423,7 +1238,7 @@ class Feeds extends Handler_Protected { return $row["count"]; } - static function getCategoryTitle($cat_id) { + static function _get_cat_title($cat_id) { if ($cat_id == -1) { return __("Special"); @@ -1445,7 +1260,7 @@ class Feeds extends Handler_Protected { } } - static function getLabelUnread($label_id, $owner_uid = false) { + private static function _get_label_unread($label_id, $owner_uid = false) { if (!$owner_uid) $owner_uid = $_SESSION["uid"]; $pdo = Db::pdo(); @@ -1462,7 +1277,7 @@ class Feeds extends Handler_Protected { } } - static function queryFeedHeadlines($params) { + static function _get_headlines($params) { $pdo = Db::pdo(); @@ -1508,10 +1323,10 @@ class Feeds extends Handler_Protected { // fall back in case of no plugins if (!$search_query_part) { - list($search_query_part, $search_words) = self::search_to_sql($search, $search_language, $owner_uid); + list($search_query_part, $search_words) = self::_search_to_sql($search, $search_language, $owner_uid); } - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $test_sth = $pdo->prepare("select $search_query_part FROM ttrss_entries, ttrss_user_entries WHERE id = ref_id limit 1"); @@ -1546,7 +1361,7 @@ class Feeds extends Handler_Protected { $unread = getFeedUnread($feed, $cat_view); if ($cat_view && $feed > 0 && $include_children) - $unread += self::getCategoryChildrenUnread($feed); + $unread += self::_get_cat_children_unread($feed); if ($unread > 0) { $view_query_part = " unread = true AND "; @@ -1590,7 +1405,7 @@ class Feeds extends Handler_Protected { if ($feed > 0) { if ($include_children) { # sub-cats - $subcats = self::getChildCategories($feed, $owner_uid); + $subcats = self::_get_child_cats($feed, $owner_uid); array_push($subcats, $feed); $subcats = array_map("intval", $subcats); @@ -1648,7 +1463,7 @@ class Feeds extends Handler_Protected { } else if ($feed == -6) { // recently read $query_strategy_part = "unread = false AND last_read IS NOT NULL"; - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $query_strategy_part .= " AND last_read > NOW() - INTERVAL '1 DAY' "; } else { $query_strategy_part .= " AND last_read > DATE_SUB(NOW(), INTERVAL 1 DAY) "; @@ -1665,7 +1480,7 @@ class Feeds extends Handler_Protected { $intl = (int) get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid); - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $query_strategy_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' "; } else { $query_strategy_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) "; @@ -1714,7 +1529,7 @@ class Feeds extends Handler_Protected { $feed_title = T_sprintf("Search results: %s", $search); } else { if ($cat_view) { - $feed_title = self::getCategoryTitle($feed); + $feed_title = self::_get_cat_title($feed); } else { if (is_numeric($feed) && $feed > 0) { $ssth = $pdo->prepare("SELECT title,site_url,last_error,last_updated @@ -1727,7 +1542,7 @@ class Feeds extends Handler_Protected { $last_error = $row["last_error"]; $last_updated = $row["last_updated"]; } else { - $feed_title = self::getFeedTitle($feed); + $feed_title = self::_get_title($feed); } } } @@ -1784,7 +1599,7 @@ class Feeds extends Handler_Protected { if ($feed == -3) $first_id_query_strategy_part = "true"; - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $sanity_interval_qpart = "date_entered >= NOW() - INTERVAL '1 hour' AND"; $yyiw_qpart = "to_char(date_entered, 'IYYY-IW') AS yyiw"; @@ -1884,7 +1699,7 @@ class Feeds extends Handler_Protected { } else { // browsing by tag - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $distinct_columns = str_replace("desc", "", strtolower($order_by)); $distinct_qpart = "DISTINCT ON (id, $distinct_columns)"; } else { @@ -1942,7 +1757,7 @@ class Feeds extends Handler_Protected { } - static function getParentCategories($cat, $owner_uid) { + static function _get_parent_cats($cat, $owner_uid) { $rv = array(); $pdo = Db::pdo(); @@ -1952,14 +1767,14 @@ class Feeds extends Handler_Protected { $sth->execute([$cat, $owner_uid]); while ($line = $sth->fetch()) { - array_push($rv, $line["parent_cat"]); - $rv = array_merge($rv, self::getParentCategories($line["parent_cat"], $owner_uid)); + array_push($rv, (int)$line["parent_cat"]); + $rv = array_merge($rv, self::_get_parent_cats($line["parent_cat"], $owner_uid)); } return $rv; } - static function getChildCategories($cat, $owner_uid) { + static function _get_child_cats($cat, $owner_uid) { $rv = array(); $pdo = Db::pdo(); @@ -1970,13 +1785,41 @@ class Feeds extends Handler_Protected { while ($line = $sth->fetch()) { array_push($rv, $line["id"]); - $rv = array_merge($rv, self::getChildCategories($line["id"], $owner_uid)); + $rv = array_merge($rv, self::_get_child_cats($line["id"], $owner_uid)); } return $rv; } - static function getFeedCategory($feed) { + static function _cats_of(array $feeds, int $owner_uid, bool $with_parents = false) { + if (count($feeds) == 0) + return []; + + $pdo = Db::pdo(); + + $feeds_qmarks = arr_qmarks($feeds); + + $sth = $pdo->prepare("SELECT DISTINCT cat_id, fc.parent_cat FROM ttrss_feeds f LEFT JOIN ttrss_feed_categories fc + ON (fc.id = f.cat_id) + WHERE f.owner_uid = ? AND f.id IN ($feeds_qmarks)"); + $sth->execute(array_merge([$owner_uid], $feeds)); + + $rv = []; + + if ($row = $sth->fetch()) { + array_push($rv, (int)$row["cat_id"]); + + if ($with_parents && $row["parent_cat"]) + $rv = array_merge($rv, + self::_get_parent_cats($row["cat_id"], $owner_uid)); + } + + $rv = array_unique($rv); + + return $rv; + } + + static function _cat_of_feed($feed) { $pdo = Db::pdo(); $sth = $pdo->prepare("SELECT cat_id FROM ttrss_feeds @@ -1991,7 +1834,7 @@ class Feeds extends Handler_Protected { } - function color_of($name) { + private function _color_of($name) { $colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b", "#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416", "#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ]; @@ -2007,7 +1850,7 @@ class Feeds extends Handler_Protected { return $colormap[$sum]; } - static function get_feeds_from_html($url, $content) { + private static function _get_feeds_from_html($url, $content) { $url = UrlHelper::validate($url); $baseUrl = substr($url, 0, strrpos($url, '/') + 1); @@ -2035,11 +1878,11 @@ class Feeds extends Handler_Protected { return $feedUrls; } - static function is_html($content) { + static function _is_html($content) { return preg_match("/fetch()) { $owner_uid = $row["owner_uid"]; - if (FORCE_ARTICLE_PURGE != 0) { - Debug::log("purge_feed: FORCE_ARTICLE_PURGE is set, overriding interval to " . FORCE_ARTICLE_PURGE, Debug::$LOG_VERBOSE); + if (Config::get(Config::FORCE_ARTICLE_PURGE) != 0) { + Debug::log("purge_feed: FORCE_ARTICLE_PURGE is set, overriding interval to " . Config::get(Config::FORCE_ARTICLE_PURGE), Debug::$LOG_VERBOSE); $purge_unread = true; - $purge_interval = FORCE_ARTICLE_PURGE; + $purge_interval = Config::get(Config::FORCE_ARTICLE_PURGE); } else { $purge_unread = get_pref("PURGE_UNREAD_ARTICLES", $owner_uid, false); } @@ -2149,7 +1992,7 @@ class Feeds extends Handler_Protected { else $query_limit = ""; - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $sth = $pdo->prepare("DELETE FROM ttrss_user_entries USING ttrss_entries WHERE ttrss_entries.id = ref_id AND @@ -2182,7 +2025,7 @@ class Feeds extends Handler_Protected { return $rows_deleted; } - static function feed_purge_interval($feed_id) { + private static function _get_purge_interval($feed_id) { $pdo = Db::pdo(); @@ -2203,7 +2046,7 @@ class Feeds extends Handler_Protected { } } - static function search_to_sql($search, $search_language, $owner_uid) { + private static function _search_to_sql($search, $search_language, $owner_uid) { $keywords = str_getcsv(trim($search), " "); $query_keywords = array(); @@ -2332,7 +2175,7 @@ class Feeds extends Handler_Protected { array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')"); } else { - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $k = mb_strtolower($k); array_push($search_query_leftover, $not ? "!$k" : $k); } else { @@ -2347,7 +2190,7 @@ class Feeds extends Handler_Protected { if (count($search_query_leftover) > 0) { - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { // if there's no joiners consider this a "simple" search and // concatenate everything with &, otherwise don't try to mess with tsquery syntax @@ -2371,7 +2214,7 @@ class Feeds extends Handler_Protected { return array($search_query_part, $search_words); } - static function order_to_override_query($order) { + static function _order_to_override_query($order) { $query = ""; $skip_first_id = false; @@ -2399,7 +2242,7 @@ class Feeds extends Handler_Protected { return [$query, $skip_first_id]; } - function mark_timestamp($label) { + private function _mark_timestamp($label) { if (empty($_REQUEST['timestamps'])) return; diff --git a/classes/handler/administrative.php b/classes/handler/administrative.php new file mode 100644 index 000000000..52dfed8b7 --- /dev/null +++ b/classes/handler/administrative.php @@ -0,0 +1,11 @@ += 10) { + return true; + } + } + return false; + } +} diff --git a/classes/handler/protected.php b/classes/handler/protected.php index 765b17480..8e9e5ca1d 100644 --- a/classes/handler/protected.php +++ b/classes/handler/protected.php @@ -2,6 +2,6 @@ class Handler_Protected extends Handler { function before($method) { - return parent::before($method) && $_SESSION['uid']; + return parent::before($method) && !empty($_SESSION['uid']); } } diff --git a/classes/handler/public.php b/classes/handler/public.php index fca471122..42be6f713 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -12,7 +12,7 @@ class Handler_Public extends Handler { 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) { $override_order = "date_entered DESC, updated DESC"; @@ -43,7 +43,7 @@ class Handler_Public extends Handler { $user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid); $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_data(); @@ -55,7 +55,7 @@ class Handler_Public extends Handler { } } else { - $qfh_ret = Feeds::queryFeedHeadlines($params); + $qfh_ret = Feeds::_get_headlines($params); } $result = $qfh_ret[0]; @@ -65,7 +65,7 @@ class Handler_Public extends Handler { $feed_self_url = get_self_url_prefix() . "/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(); @@ -82,7 +82,7 @@ class Handler_Public extends Handler { while ($line = $result->fetch()) { $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, function ($result) use (&$line) { @@ -98,7 +98,7 @@ class Handler_Public extends Handler { $tpl->setVariable('ARTICLE_ID', 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_TITLE', htmlspecialchars($line['title']), 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, $feed_site_url, false, $line["id"]); - $content = DiskCache::rewriteUrls($content); + $content = DiskCache::rewrite_urls($content); if ($line['note']) { $content = "
    Article note: " . $line['note'] . "
    " . @@ -131,7 +131,7 @@ class Handler_Public extends Handler { $tpl->addBlock('category'); } - $enclosures = Article::get_article_enclosures($line["id"]); + $enclosures = Article::_get_enclosures($line["id"]); if (count($enclosures) > 0) { foreach ($enclosures as $e) { @@ -146,12 +146,12 @@ class Handler_Public extends Handler { $tpl->addBlock('enclosure'); } } else { - $tpl->setVariable('ARTICLE_ENCLOSURE_URL', null, true); - $tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', null, true); - $tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', null, true); + $tpl->setVariable('ARTICLE_ENCLOSURE_URL', "", true); + $tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', "", 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); @@ -163,7 +163,7 @@ class Handler_Public extends Handler { $tpl->addBlock('feed'); $tpl->generateOutputToString($tmp); - if (@!clean($_REQUEST["noxml"])) { + if (empty($_REQUEST["noxml"])) { header("Content-Type: text/xml; charset=utf-8"); } else { header("Content-Type: text/plain; charset=utf-8"); @@ -184,7 +184,7 @@ class Handler_Public extends Handler { while ($line = $result->fetch()) { $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, 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['updated'] = date('c', strtotime($line["updated"])); - if ($line['note']) $article['note'] = $line['note']; - if ($article['author']) $article['author'] = $line['author']; + if (!empty($line['note'])) $article['note'] = $line['note']; + if (!empty($line['author'])) $article['author'] = $line['author']; if (count($line["tags"]) > 0) { $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) { $article['enclosures'] = array(); @@ -240,7 +240,7 @@ class Handler_Public extends Handler { } else { 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); if ($uid) { - print Feeds::getGlobalUnread($uid); + print Feeds::_get_global_unread($uid); if ($fresh) { print ";"; - print Feeds::getFeedArticles(-3, false, true, $uid); + print Feeds::_get_counters(-3, false, true, $uid); } } else { print "-1;User not found"; @@ -286,195 +286,30 @@ class Handler_Public extends Handler { function logout() { if (validate_csrf($_POST["csrf_token"])) { - Pref_Users::logout_user(); + UserHelper::logout(); header("Location: index.php"); } else { 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 .= " - - - ".$line["title"]."". - javascript_tag("lib/prototype.js"). - javascript_tag("js/utility.js")." - - - "; - - $rv .= "\n"; - $rv .= "\n"; - - $rv .= ""; - - list ($og_image, $og_stream) = Article::get_article_image($enclosures, $line['content'], $line["site_url"]); - - if ($og_image) { - $rv .= ""; - } - - $rv .= ""; - $rv .= "
    "; - - if ($line["link"]) { - $rv .= "

    " . $line["title"] . "

    "; - } else { - $rv .= "

    " . $line["title"] . "

    "; - } - - $rv .= "
    "; - - /* header */ - - $rv .= "
    "; - $rv .= "
    "; # row - - //$entry_author = $line["author"] ? " - " . $line["author"] : ""; - $parsed_updated = TimeHelper::make_local_datetime($line["updated"], true, - $owner_uid, true); - - $rv .= "
    ".$line['author']."
    "; - $rv .= "
    $parsed_updated
    "; - - $rv .= "
    "; # row - - $rv .= "
    "; # header - - /* content */ - - $lang = $line['lang'] ? $line['lang'] : "en"; - $rv .= "
    "; - - /* content body */ - - $rv .= $line["content"]; - - $rv .= Article::format_article_enclosures($id, - $line["always_display_enclosures"], - $line["content"], - $line["hide_images"]); - - $rv .= "
    "; # content - - $rv .= "
    "; # post - - } - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_FORMAT_ARTICLE, - function ($result) use (&$rv) { - $rv = $result; - }, - $rv, $line); - - return $rv; - - } - function rss() { $feed = clean($_REQUEST["id"]); $key = clean($_REQUEST["key"]); - $is_cat = clean($_REQUEST["is_cat"]); - $limit = (int)clean($_REQUEST["limit"]); - $offset = (int)clean($_REQUEST["offset"]); + $is_cat = clean($_REQUEST["is_cat"] ?? false); + $limit = (int)clean($_REQUEST["limit"] ?? 0); + $offset = (int)clean($_REQUEST["offset"] ?? 0); - $search = clean($_REQUEST["q"]); - $view_mode = clean($_REQUEST["view-mode"]); - $order = clean($_REQUEST["order"]); - $start_ts = clean($_REQUEST["ts"]); + $search = clean($_REQUEST["q"] ?? ""); + $view_mode = clean($_REQUEST["view-mode"] ?? ""); + $order = clean($_REQUEST["order"] ?? ""); + $start_ts = (int)clean($_REQUEST["ts"] ?? 0); - $format = clean($_REQUEST['format']); - $orig_guid = clean($_REQUEST["orig_guid"]); + $format = clean($_REQUEST['format'] ?? "atom"); + $orig_guid = clean($_REQUEST["orig_guid"] ?? false); - if (!$format) $format = 'atom'; - - if (SINGLE_USER_MODE) { + if (Config::get(Config::SINGLE_USER_MODE)) { UserHelper::authenticate("admin", null); } @@ -511,169 +346,8 @@ class Handler_Public extends Handler { 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'); - ?> - - - - <?php echo __("Share with Tiny Tiny RSS") ?> - - - - - - - - -
    - - "; - print "window.close();"; - print ""; - - } else { - $title = htmlspecialchars(clean($_REQUEST["title"])); - $url = htmlspecialchars(clean($_REQUEST["url"])); - - ?> -
    - - - - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    -
    - -
    - -
    - - - -
    - -
    - - - - -
    - - - -
    - - " /> -
    - -
    - - - "/> -
    - -
    - -
    - - - -
    - -
    - "; - } - function login() { - if (!SINGLE_USER_MODE) { + if (!Config::get(Config::SINGLE_USER_MODE)) { $login = clean($_POST["login"]); $password = clean($_POST["password"]); @@ -681,7 +355,7 @@ class Handler_Public extends Handler { $safe_mode = checkbox_to_sql_bool(clean($_POST["safe_mode"] ?? false)); if ($remember_me) { - @session_set_cookie_params(SESSION_COOKIE_LIFETIME); + @session_set_cookie_params(Config::get(Config::SESSION_COOKIE_LIFETIME)); } else { @session_set_cookie_params(0); } @@ -724,7 +398,7 @@ class Handler_Public extends Handler { $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'])); } else { 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'); - ?> - - - - Tiny Tiny RSS - - - - - - - - -
    -

    -
    - -
    - - -
    - - -
    - - - - -
    - %s.", $feed_url)); - break; - case 1: - print_notice(T_sprintf("Subscribed to %s.", $feed_url)); - break; - case 2: - print_error(T_sprintf("Could not subscribe to %s.", $feed_url)); - break; - case 3: - print_error(T_sprintf("No feeds found in %s.", $feed_url)); - break; - case 4: - $feed_urls = $rc["feeds"]; - break; - case 5: - print_error(T_sprintf("Could not subscribe to %s.
    Can't download the Feed URL.", $feed_url)); - break; - } - - if ($feed_urls) { - - print "
    "; - print ""; - print_hidden("csrf_token", $_SESSION["csrf_token"]); - - print "
    "; - print ""; - print ""; - print "
    "; - - print ""; - print "".__("Return to Tiny Tiny RSS").""; - - print "
    "; - } - - $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 "
    - - - - - ".__("Return to Tiny Tiny RSS")." -
    "; - } - } - - print "
    "; - - } else { - $this->render_login_form(); - } - } - function index() { header("Content-Type: text/plain"); - print error_json(13); + print Errors::to_json(Errors::E_UNKNOWN_METHOD); } function forgotpass() { @@ -909,7 +428,6 @@ class Handler_Public extends Handler { @@ -953,7 +471,7 @@ class Handler_Public extends Handler { WHERE id = ?"); $sth->execute([$id]); - Pref_Users::resetUserPassword($id, true); + UserHelper::reset_password($id, true); print "

    "."Completed."."

    "; @@ -1041,7 +559,7 @@ class Handler_Public extends Handler { $tpl->setVariable('LOGIN', $login); $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'); @@ -1095,9 +613,9 @@ class Handler_Public extends Handler { function dbupdate() { 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."); - $this->render_login_form(); + $this->_render_login_form(); exit; } @@ -1107,12 +625,11 @@ class Handler_Public extends Handler { Database Updater - + @@ -1137,26 +654,26 @@ class Handler_Public extends Handler {
    -

    +

    isUpdateRequired()) { + if ($updater->is_update_required()) { print "

    " . T_sprintf("Performing updates to version %d", SCHEMA_VERSION) . "

    "; - for ($i = $updater->getSchemaVersion() + 1; $i <= SCHEMA_VERSION; $i++) { + for ($i = $updater->get_schema_version() + 1; $i <= SCHEMA_VERSION; $i++) { print "
      "; print "
    • " . T_sprintf("Updating to version %d", $i) . "
    • "; print "
    • "; - $result = $updater->performUpdateTo($i, true); + $result = $updater->update_to($i, true); print "
    • "; if (!$result) { @@ -1187,12 +704,12 @@ class Handler_Public extends Handler { print "".__("Return to Tiny Tiny RSS").""; } } else { - if ($updater->isUpdateRequired()) { + if ($updater->is_update_required()) { print "

      ".T_sprintf("Tiny Tiny RSS database needs update to the latest version (%d to %d).", - $updater->getSchemaVersion(), SCHEMA_VERSION)."

      "; + $updater->get_schema_version(), SCHEMA_VERSION).""; - if (DB_TYPE == "mysql") { + if (Config::get(Config::DB_TYPE) == "mysql") { print_error("READ THIS: Due to MySQL limitations, your database is not completely protected while updating. ". "Errors may put it in an inconsistent state requiring manual rollback. BACKUP YOUR DATABASE BEFORE CONTINUING."); } else { @@ -1220,7 +737,28 @@ class Handler_Public extends Handler { 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); // 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)); @@ -1264,21 +802,21 @@ class Handler_Public extends Handler { } else { user_error("PluginHandler[PUBLIC]: Requested private method '$method' of plugin '$plugin_name'.", E_USER_WARNING); header("Content-Type: text/json"); - print error_json(6); + print Errors::to_json(Errors::E_UNAUTHORIZED); } } else { user_error("PluginHandler[PUBLIC]: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING); header("Content-Type: text/json"); - print error_json(13); + print Errors::to_json(Errors::E_UNKNOWN_METHOD); } } else { user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING); 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'); require_once "login_form.php"; diff --git a/classes/idb.php b/classes/idb.php deleted file mode 100644 index 37fd69906..000000000 --- a/classes/idb.php +++ /dev/null @@ -1,13 +0,0 @@ - $label) { + $rv[$label["id"]] = $labels[$i]; + } + + return $rv; + } + + static function get_all($owner_uid) { $rv = array(); $pdo = Db::pdo(); @@ -46,7 +57,7 @@ class Labels WHERE owner_uid = ? ORDER BY caption"); $sth->execute([$owner_uid]); - while ($line = $sth->fetch()) { + while ($line = $sth->fetch(PDO::FETCH_ASSOC)) { array_push($rv, $line); } @@ -60,7 +71,7 @@ class Labels self::clear_cache($id); if (!$labels) - $labels = Article::get_article_labels($id); + $labels = Article::_get_labels($id); $labels = json_encode($labels); diff --git a/classes/logger.php b/classes/logger.php index cdc6b240a..6cc33314d 100755 --- a/classes/logger.php +++ b/classes/logger.php @@ -42,7 +42,7 @@ class Logger { } function __construct() { - switch (LOG_DESTINATION) { + switch (Config::get(Config::LOG_DESTINATION)) { case "sql": $this->adapter = new Logger_SQL(); break; diff --git a/classes/mailer.php b/classes/mailer.php index 16be16523..93f778210 100644 --- a/classes/mailer.php +++ b/classes/mailer.php @@ -11,15 +11,15 @@ class Mailer { $subject = $params["subject"]; $message = $params["message"]; $message_html = $params["message_html"]; - $from_name = $params["from_name"] ? $params["from_name"] : SMTP_FROM_NAME; - $from_address = $params["from_address"] ? $params["from_address"] : SMTP_FROM_ADDRESS; + $from_name = $params["from_name"] ? $params["from_name"] : Config::get(Config::SMTP_FROM_NAME); + $from_address = $params["from_address"] ? $params["from_address"] : Config::get(Config::SMTP_FROM_ADDRESS); $additional_headers = $params["headers"] ? $params["headers"] : []; $from_combined = $from_name ? "$from_name <$from_address>" : $from_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"); // HOOK_SEND_MAIL plugin instructions: diff --git a/classes/opml.php b/classes/opml.php index aa5e22b80..cbc1269e3 100644 --- a/classes/opml.php +++ b/classes/opml.php @@ -31,7 +31,7 @@ class OPML extends Handler_Protected {

      ".__('OPML Utility')."

      "; - Feeds::add_feed_category("Imported feeds"); + Feeds::_add_cat("Imported feeds"); $this->opml_notice(__("Importing OPML...")); @@ -205,7 +205,7 @@ class OPML extends Handler_Protected { if (!$tmp_line["match_on"]) { 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); } else { @@ -218,13 +218,13 @@ class OPML extends Handler_Protected { if (strpos($feed_id, "CAT:") === 0) { $feed_id = (int)substr($feed_id, 4); if ($feed_id) { - array_push($match, [Feeds::getCategoryTitle($feed_id), true, false]); + array_push($match, [Feeds::_get_cat_title($feed_id), true, false]); } else { array_push($match, [0, true, true]); } } else { 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 { array_push($match, [0, false, true]); } @@ -523,7 +523,7 @@ class OPML extends Handler_Protected { $order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue; 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); } @@ -594,7 +594,7 @@ class OPML extends Handler_Protected { } 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'], $tmp_file); @@ -634,13 +634,10 @@ class OPML extends Handler_Protected { print "$msg
      "; } - static function opml_publish_url(){ - - $url_path = get_self_url_prefix(); - $url_path .= "/opml.php?op=publish&key=" . - Feeds::get_feed_access_key('OPML:Publish', false, $_SESSION["uid"]); - - return $url_path; + static function get_publish_url(){ + return get_self_url_prefix() . + "/public.php?op=publishOpml&key=" . + Feeds::_get_access_key('OPML:Publish', false, $_SESSION["uid"]); } function get_feed_category($feed_cat, $parent_cat_id = false) { diff --git a/classes/plugin.php b/classes/plugin.php index 2416418cd..6c572467a 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -54,4 +54,8 @@ abstract class Plugin { return vsprintf($this->__($msgid), $args); } + + function csrf_ignore($method) { + return false; + } } diff --git a/classes/pluginhandler.php b/classes/pluginhandler.php index 9682e440f..75b823822 100644 --- a/classes/pluginhandler.php +++ b/classes/pluginhandler.php @@ -7,17 +7,23 @@ class PluginHandler extends Handler_Protected { function catchall($method) { $plugin_name = clean($_REQUEST["plugin"]); $plugin = PluginHost::getInstance()->get_plugin($plugin_name); + $csrf_token = ($_POST["csrf_token"] ?? ""); if ($plugin) { if (method_exists($plugin, $method)) { - $plugin->$method(); + if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) { + $plugin->$method(); + } else { + user_error("Rejected ${plugin_name}->${method}(): invalid CSRF token.", E_USER_WARNING); + print Errors::to_json(Errors::E_UNAUTHORIZED); + } } else { - user_error("PluginHandler: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING); - print error_json(13); + user_error("Rejected ${plugin_name}->${method}(): unknown method.", E_USER_WARNING); + print Errors::to_json(Errors::E_UNKNOWN_METHOD); } } else { - user_error("PluginHandler: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING); - print error_json(14); + user_error("Rejected ${plugin_name}->${method}(): unknown plugin.", E_USER_WARNING); + print Errors::to_json(Errors::E_UNKNOWN_PLUGIN); } } } diff --git a/classes/pluginhost.php b/classes/pluginhost.php index 42f7f6bf9..b6f645a9c 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -18,6 +18,7 @@ class PluginHost { private static $instance; const API_VERSION = 2; + const PUBLIC_METHOD_DELIMITER = "--"; // Hooks marked with *1 are run in global context and available // 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_HOUSE_KEEPING = "hook_house_keeping"; //*1 // GLOBAL: hook_house_keeping() 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_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_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_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_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) @@ -107,8 +108,9 @@ class PluginHost { return false; } + // needed for compatibility with API 2 (?) function get_dbh() { - return Db::get(); + return false; } function get_pdo(): PDO { @@ -272,8 +274,8 @@ class PluginHost { $class = trim($class); $class_file = strtolower(basename(clean($class))); - if (!is_dir(__DIR__."/../plugins/$class_file") && - !is_dir(__DIR__."/../plugins.local/$class_file")) continue; + if (!is_dir(__DIR__ . "/../plugins/$class_file") && + !is_dir(__DIR__ . "/../plugins.local/$class_file")) continue; // try system plugin directory first $file = __DIR__ . "/../plugins/$class_file/init.php"; @@ -598,7 +600,7 @@ class PluginHost { } // 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?" . http_build_query( array_merge( @@ -610,16 +612,25 @@ class PluginHost { $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 - 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)) { return get_self_url_prefix() . "/public.php?" . http_build_query( array_merge( [ - "op" => "pluginhandler", - "plugin" => strtolower(get_class($sender)), - "pmethod" => $method + "op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method), ], $params)); } else { diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index 47e5689ec..086c52697 100755 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -1,7 +1,7 @@ query("SELECT cfgname FROM pg_ts_config"); @@ -22,11 +22,6 @@ class Pref_Feeds extends Handler_Protected { return $rv; } - function batch_edit_cbox($elem, $label = false) { - print ""; - } - function renamecat() { $title = clean($_REQUEST['title']); $id = clean($_REQUEST['id']); @@ -98,7 +93,7 @@ class Pref_Feeds extends Handler_Protected { $feed['checkbox'] = false; $feed['unread'] = -1; $feed['error'] = $feed_line['last_error']; - $feed['icon'] = Feeds::getFeedIcon($feed_line['id']); + $feed['icon'] = Feeds::_get_icon($feed_line['id']); $feed['param'] = TimeHelper::make_local_datetime( $feed_line['last_updated'], true); $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0); @@ -110,10 +105,10 @@ class Pref_Feeds extends Handler_Protected { } function getfeedtree() { - print json_encode($this->makefeedtree()); + print json_encode($this->_makefeedtree()); } - function makefeedtree() { + function _makefeedtree() { if (clean($_REQUEST['mode'] ?? 0) != 2) $search = $_SESSION["prefs_feed_search"] ?? ""; @@ -266,7 +261,7 @@ class Pref_Feeds extends Handler_Protected { $feed['name'] = $feed_line['title']; $feed['checkbox'] = false; $feed['error'] = $feed_line['last_error']; - $feed['icon'] = Feeds::getFeedIcon($feed_line['id']); + $feed['icon'] = Feeds::_get_icon($feed_line['id']); $feed['param'] = TimeHelper::make_local_datetime( $feed_line['last_updated'], true); $feed['unread'] = -1; @@ -301,7 +296,7 @@ class Pref_Feeds extends Handler_Protected { $feed['name'] = $feed_line['title']; $feed['checkbox'] = false; $feed['error'] = $feed_line['last_error']; - $feed['icon'] = Feeds::getFeedIcon($feed_line['id']); + $feed['icon'] = Feeds::_get_icon($feed_line['id']); $feed['param'] = TimeHelper::make_local_datetime( $feed_line['last_updated'], true); $feed['unread'] = -1; @@ -446,7 +441,7 @@ class Pref_Feeds extends Handler_Protected { $sth->execute([$feed_id, $_SESSION['uid']]); if ($row = $sth->fetch()) { - @unlink(ICONS_DIR . "/$feed_id.ico"); + @unlink(Config::get(Config::ICONS_DIR) . "/$feed_id.ico"); $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = NULL, favicon_last_checked = '1970-01-01' where id = ?"); @@ -458,10 +453,12 @@ class Pref_Feeds extends Handler_Protected { header("Content-type: text/html"); if (is_uploaded_file($_FILES['icon_file']['tmp_name'])) { - $tmp_file = tempnam(CACHE_DIR . '/upload', 'icon'); + $tmp_file = tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'icon'); - $result = move_uploaded_file($_FILES['icon_file']['tmp_name'], - $tmp_file); + if (!$tmp_file) + return; + + $result = move_uploaded_file($_FILES['icon_file']['tmp_name'], $tmp_file); if (!$result) { return; @@ -474,7 +471,7 @@ class Pref_Feeds extends Handler_Protected { $feed_id = clean($_REQUEST["feed_id"]); $rc = 2; // failed - if (is_file($icon_file) && $feed_id) { + if ($icon_file && is_file($icon_file) && $feed_id) { if (filesize($icon_file) < 65535) { $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds @@ -482,15 +479,19 @@ class Pref_Feeds extends Handler_Protected { $sth->execute([$feed_id, $_SESSION['uid']]); if ($row = $sth->fetch()) { - @unlink(ICONS_DIR . "/$feed_id.ico"); - if (rename($icon_file, ICONS_DIR . "/$feed_id.ico")) { + $new_filename = Config::get(Config::ICONS_DIR) . "/$feed_id.ico"; + + if (file_exists($new_filename)) unlink($new_filename); + + if (rename($icon_file, $new_filename)) { + chmod($new_filename, 644); $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = '' WHERE id = ?"); $sth->execute([$feed_id]); - $rc = 0; + $rc = Feeds::_get_icon($feed_id); } } } else { @@ -498,7 +499,9 @@ class Pref_Feeds extends Handler_Protected { } } - if (is_file($icon_file)) @unlink($icon_file); + if ($icon_file && is_file($icon_file)) { + unlink($icon_file); + } print $rc; return; @@ -508,131 +511,25 @@ class Pref_Feeds extends Handler_Protected { global $purge_intervals; global $update_intervals; - $feed_id = clean($_REQUEST["id"]); + $feed_id = (int)clean($_REQUEST["id"]); $sth = $this->pdo->prepare("SELECT * FROM ttrss_feeds WHERE id = ? AND owner_uid = ?"); $sth->execute([$feed_id, $_SESSION['uid']]); - if ($row = $sth->fetch()) { - print '
      -
      '; + if ($row = $sth->fetch(PDO::FETCH_ASSOC)) { - $title = htmlspecialchars($row["title"]); + ob_start(); + PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_EDIT_FEED, $feed_id); + $plugin_data = trim((string)ob_get_contents()); + ob_end_clean(); - print_hidden("id", "$feed_id"); - print_hidden("op", "pref-feeds"); - print_hidden("method", "editSave"); - - print "
      ".__("Feed")."
      "; - print "
      "; - - /* Title */ - - print "
      "; - - print ""; - - print "
      "; - - /* Feed URL */ - - $feed_url = htmlspecialchars($row["feed_url"]); - - print "
      "; - - print " "; - print ""; - - if (!empty($row["last_error"])) { - print " error"; - } - - print "
      "; - - /* Category */ - - if (get_pref('ENABLE_FEED_CATS')) { - - $cat_id = $row["cat_id"]; - - print "
      "; - - print " "; - - print_feed_cat_select("cat_id", $cat_id, - 'dojoType="fox.form.Select"'); - - print "
      "; - } - - /* Site URL */ - - $site_url = htmlspecialchars($row["site_url"]); - - print "
      "; - - print " "; - print ""; - - print "
      "; - - /* FTS Stemming Language */ - - if (DB_TYPE == "pgsql") { - $feed_language = $row["feed_language"]; - - if (!$feed_language) - $feed_language = get_pref('DEFAULT_SEARCH_LANGUAGE'); - - print "
      "; - - print " "; - print_select("feed_language", $feed_language, $this::get_ts_languages(), - 'dojoType="fox.form.Select"'); - - print "
      "; - } - - print "
      "; - - print "
      ".__("Update")."
      "; - print "
      "; - - /* Update Interval */ - - $update_interval = $row["update_interval"]; - - print "
      "; - - print " "; + $row["icon"] = Feeds::_get_icon($feed_id); $local_update_intervals = $update_intervals; $local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[get_pref("DEFAULT_UPDATE_INTERVAL")]); - print_select_hash("update_interval", $update_interval, $local_update_intervals, - 'dojoType="fox.form.Select"'); - - print "
      "; - - /* Purge intl */ - - $purge_interval = $row["purge_interval"]; - - print "
      "; - - print " "; - - if (FORCE_ARTICLE_PURGE == 0) { + if (Config::get(Config::FORCE_ARTICLE_PURGE) == 0) { $local_purge_intervals = $purge_intervals; $default_purge_interval = get_pref("PURGE_OLD_DAYS"); @@ -642,343 +539,142 @@ class Pref_Feeds extends Handler_Protected { $local_purge_intervals[0] .= " " . sprintf("(%s)", __("Disabled")); } else { - $purge_interval = FORCE_ARTICLE_PURGE; + $purge_interval = Config::get(Config::FORCE_ARTICLE_PURGE); $local_purge_intervals = [ T_nsprintf('%d day', '%d days', $purge_interval, $purge_interval) ]; } - print_select_hash("purge_interval", $purge_interval, $local_purge_intervals, - 'dojoType="fox.form.Select" ' . - ((FORCE_ARTICLE_PURGE == 0) ? "" : 'disabled="1"')); - - print "
      "; - - print "
      "; - - $auth_login = htmlspecialchars($row["auth_login"]); - $auth_pass = htmlspecialchars($row["auth_pass"]); - - $auth_enabled = $auth_login !== '' || $auth_pass !== ''; - - $auth_style = $auth_enabled ? '' : 'display: none'; - print "
      "; - print "
      ".__("Authentication")."
      "; - print "
      "; - - print "
      "; - - print ""; - - print "
      "; - - print ""; - - print "
      - ".__('Hint: you need to fill in your login information if your feed requires authentication, except for Twitter feeds.')." -
      "; - - print "
      "; - - print "
      "; - - $auth_checked = $auth_enabled ? 'checked' : ''; - print ""; - - print '
      '; - - print "
      "; - - $include_in_digest = $row["include_in_digest"]; - - if ($include_in_digest) { - $checked = "checked=\"1\""; - } else { - $checked = ""; - } - - print "
      "; - - print ""; - - print "
      "; - - $always_display_enclosures = $row["always_display_enclosures"]; - - if ($always_display_enclosures) { - $checked = "checked"; - } else { - $checked = ""; - } - - print "
      "; - - print ""; - - print "
      "; - - $hide_images = $row["hide_images"]; - - if ($hide_images) { - $checked = "checked=\"1\""; - } else { - $checked = ""; - } - - print "
      "; - - print ""; - - print "
      "; - - $cache_images = $row["cache_images"]; - - if ($cache_images) { - $checked = "checked=\"1\""; - } else { - $checked = ""; - } - - print "
      "; - - print ""; - - print "
      "; - - $mark_unread_on_update = $row["mark_unread_on_update"]; - - if ($mark_unread_on_update) { - $checked = "checked"; - } else { - $checked = ""; - } - - print "
      "; - - print ""; - - print "
      "; - - print '
      '; - - /* Icon */ - - print ""; - - print "
      - - - - - - - -
      "; - - print ""; - - print '
      '; - - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_EDIT_FEED, $feed_id); - - print "
      "; - - $title = htmlspecialchars($title, ENT_QUOTES); - - print "
      - - - -
      "; + print json_encode([ + "feed" => $row, + "cats" => [ + "enabled" => get_pref('ENABLE_FEED_CATS'), + "select" => \Controls\select_feeds_cats("cat_id", $row["cat_id"]), + ], + "plugin_data" => $plugin_data, + "force_purge" => (int)Config::get(Config::FORCE_ARTICLE_PURGE), + "intervals" => [ + "update" => $local_update_intervals, + "purge" => $local_purge_intervals, + ], + "lang" => [ + "enabled" => Config::get(Config::DB_TYPE) == "pgsql", + "default" => get_pref('DEFAULT_SEARCH_LANGUAGE'), + "all" => $this::get_ts_languages(), + ] + ]); } } + private function _batch_toggle_checkbox($name) { + return \Controls\checkbox_tag("", false, "", + ["data-control-for" => $name, "title" => __("Check to enable field"), "onchange" => "App.dialogOf(this).toggleField(this)"]); + } + function editfeeds() { global $purge_intervals; global $update_intervals; $feed_ids = clean($_REQUEST["ids"]); - print_notice("Enable the options you wish to apply using checkboxes on the right:"); - - print "

      "; - - print_hidden("ids", "$feed_ids"); - print_hidden("op", "pref-feeds"); - print_hidden("method", "batchEditSave"); - - print "

      ".__("Feed")."
      "; - print "
      "; - - /* Category */ - - if (get_pref('ENABLE_FEED_CATS')) { - - print "
      "; - - print " "; - - print_feed_cat_select("cat_id", false, - 'disabled="1" dojoType="fox.form.Select"'); - - $this->batch_edit_cbox("cat_id"); - - print "
      "; - } - - /* FTS Stemming Language */ - - if (DB_TYPE == "pgsql") { - print "
      "; - - print " "; - print_select("feed_language", "", $this::get_ts_languages(), - 'disabled="1" dojoType="fox.form.Select"'); - - $this->batch_edit_cbox("feed_language"); - - print "
      "; - } - - print "
      "; - - print "
      ".__("Update")."
      "; - print "
      "; - - /* Update Interval */ - - print "
      "; - - print " "; - $local_update_intervals = $update_intervals; $local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[get_pref("DEFAULT_UPDATE_INTERVAL")]); - print_select_hash("update_interval", "", $local_update_intervals, - 'disabled="1" dojoType="fox.form.Select"'); + $local_purge_intervals = $purge_intervals; + $default_purge_interval = get_pref("PURGE_OLD_DAYS"); - $this->batch_edit_cbox("update_interval"); + if ($default_purge_interval > 0) + $local_purge_intervals[0] .= " " . T_sprintf("(%d days)", $default_purge_interval); + else + $local_purge_intervals[0] .= " " . sprintf("(%s)", __("Disabled")); - print "
      "; + $options = [ + "include_in_digest" => __('Include in e-mail digest'), + "always_display_enclosures" => __('Always display image attachments'), + "hide_images" => __('Do not embed media'), + "cache_images" => __('Cache media'), + "mark_unread_on_update" => __('Mark updated articles as unread') + ]; - /* Purge intl */ + print_notice("Enable the options you wish to apply using checkboxes on the right."); + ?> - if (FORCE_ARTICLE_PURGE == 0) { + + + - print "
      "; +
      +
      +
      + +
      + + '1']) ?> + _batch_toggle_checkbox("cat_id") ?> +
      + - print " "; + +
      + + 1]) ?> + _batch_toggle_checkbox("feed_language") ?> +
      + +
      - $local_purge_intervals = $purge_intervals; - $default_purge_interval = get_pref("PURGE_OLD_DAYS"); +
      - if ($default_purge_interval > 0) - $local_purge_intervals[0] .= " " . T_sprintf("(%d days)", $default_purge_interval); - else - $local_purge_intervals[0] .= " " . sprintf("(%s)", __("Disabled")); +
      +
      + + 1]) ?> + _batch_toggle_checkbox("update_interval") ?> +
      - print_select_hash("purge_interval", "", $local_purge_intervals, - 'disabled="1" dojoType="fox.form.Select"'); + +
      + + 1]) ?> + _batch_toggle_checkbox("purge_interval") ?> +
      + +
      +
      +
      +
      +
      + + + _batch_toggle_checkbox("auth_login") ?> +
      +
      + + + _batch_toggle_checkbox("auth_pass") ?> +
      +
      +
      +
      + $caption) { + ?> +
      + +
      + +
      +
      - $this->batch_edit_cbox("purge_interval"); - - print "
      "; - } - - print "
      "; - print "
      ".__("Authentication")."
      "; - print "
      "; - - print "
      "; - - print ""; - - $this->batch_edit_cbox("auth_login"); - - print ""; - - $this->batch_edit_cbox("auth_pass"); - - print "
      "; - - print "
      "; - print "
      ".__("Options")."
      "; - print "
      "; - - print "
      "; - print ""; - - print " "; $this->batch_edit_cbox("include_in_digest", "include_in_digest_l"); - - print "
      "; - - print ""; - - print " "; $this->batch_edit_cbox("always_display_enclosures", "always_display_enclosures_l"); - - print "
      "; - - print ""; - - print " "; $this->batch_edit_cbox("hide_images", "hide_images_l"); - - print "
      "; - - print ""; - - print " "; $this->batch_edit_cbox("cache_images", "cache_images_l"); - - print "
      "; - - print ""; - - print " "; $this->batch_edit_cbox("mark_unread_on_update", "mark_unread_on_update_l"); - - print "
      "; - - print "
      "; - - print "
      - - -
      "; - - return; +
      + + +
      + editsaveops(false); } - function editsaveops($batch) { + private function editsaveops($batch) { $feed_title = clean($_POST["title"]); $feed_url = clean($_POST["feed_url"]); @@ -1017,10 +713,6 @@ class Pref_Feeds extends Handler_Protected { $feed_language = clean($_POST["feed_language"]); if (!$batch) { - if (clean($_POST["need_auth"] ?? "") !== 'on') { - $auth_login = ''; - $auth_pass = ''; - } /* $sth = $this->pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?"); $sth->execute([$feed_id]); @@ -1189,7 +881,7 @@ class Pref_Feeds extends Handler_Protected { function addCat() { $feed_cat = clean($_REQUEST["cat"]); - Feeds::add_feed_category($feed_cat); + Feeds::_add_cat($feed_cat); } function importOpml() { @@ -1197,33 +889,15 @@ class Pref_Feeds extends Handler_Protected { $opml->opml_import($_SESSION["uid"]); } - function index() { + private function index_feeds() { + $error_button = ""; - print "
      "; - print "
      rss_feed ".__('Feeds')."\">"; - - $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors - FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - - if ($row = $sth->fetch()) { - $num_errors = $row["num_errors"]; - } else { - $num_errors = 0; - } - - if ($num_errors > 0) { - $error_button = ""; - } else { - $error_button = ""; - } - - $inactive_button = ""; @@ -1235,175 +909,201 @@ class Pref_Feeds extends Handler_Protected { $feed_search = $_SESSION["prefs_feed_search"] ?? ""; } - print '
      '; + ?> - print "
      "; #toolbar +
      +
      +
      + + +
      - print "
      - - -
      "; +
      + +
      +
      +
      +
      +
      - print "
      ". - "" . __('Select').""; - print "
      "; - print "
      ".__('All')."
      "; - print "
      ".__('None')."
      "; - print "
      "; +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      - print "
      ". - "" . __('Feeds').""; - print "
      "; - print "
      ".__('Subscribe to feed')."
      "; - print "
      ".__('Edit selected feeds')."
      "; - print "
      ".__('Reset sort order')."
      "; - print "
      ".__('Batch subscribe')."
      "; - print "
      " - .__('Unsubscribe')."
      "; - print "
      "; + +
      + +
      +
      +
      +
      +
      +
      + + + +
      +
      +
      +
      - if (get_pref('ENABLE_FEED_CATS')) { - print "
      ". - "" . __('Categories').""; - print "
      "; - print "
      ".__('Add category')."
      "; - print "
      ".__('Reset sort order')."
      "; - print "
      ".__('Remove selected')."
      "; - print "
      "; +
      +
      - } +
      " + persist="true" + model="feedModel" + openOnClick="false"> + + +
      +
      -
      -
      -
      - - -
      "; + private function index_opml() { + ?> -# print "
      -# ".__('Hint: you can drag feeds and categories around.')." -#
      "; +

      - print '
      '; - print '
      '; + - print "
      "; # feeds pane - - print "
      "; - - print "

      " . __("Using OPML you can export and import your feeds, filters, labels and Tiny Tiny RSS settings.") . "

      "; - - print_notice("Only main settings profile can be migrated using OPML."); - - print "
      -
      "; # pane + private function index_shared() { + ?> - print "
      share ".__('Published & shared articles / Generated feeds')."\">"; +

      - print "

      " . __('Published articles can be subscribed by anyone who knows the following URL:') . "

      "; + - $rss_url = htmlspecialchars(get_self_url_prefix() . - "/public.php?op=rss&id=-2&view-mode=all_articles");; - - print " - "; + + run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsPublishedGenerated"); + } - print "
      "; #pane + function index() { + ?> - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFeeds"); +
      +
      + index_feeds() ?> +
      - print "
      "; #container +
      + index_opml() ?> +
      + +
      + index_shared() ?> +
      + + run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFeeds"); + $plugin_data = trim((string)ob_get_contents()); + ob_end_clean(); + ?> + + +
      + +
      + +
      +
      + +
      + execute([$_SESSION['uid']]); - print "
      "; - print "
      ". - "" . __('Select').""; - print "
      "; - print "
      ".__('All')."
      "; - print "
      ".__('None')."
      "; - print "
      "; - print "
      "; #toolbar + $rv = []; - print "
      "; - print ""; - - $lnum = 1; - - while ($line = $sth->fetch()) { - - $feed_id = $line["id"]; - - print ""; - - print ""; - print ""; - print ""; - - ++$lnum; + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $row['last_article'] = TimeHelper::make_local_datetime($row['last_article'], false); + array_push($rv, $row); } - print "
      "; - - print "". - htmlspecialchars($line["title"]).""; - - print ""; - print TimeHelper::make_local_datetime($line['last_article'], false); - print "
      "; - print "
      "; - - print "
      - - -
      "; - + print json_encode($rv); } function feedsWithErrors() { @@ -1521,58 +1179,13 @@ class Pref_Feeds extends Handler_Protected { FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); $sth->execute([$_SESSION['uid']]); - print "
      "; - print "
      ". - "" . __('Select').""; - print "
      "; - print "
      ".__('All')."
      "; - print "
      ".__('None')."
      "; - print "
      "; - print "
      "; #toolbar + $rv = []; - print "
      "; - print ""; - - $lnum = 1; - - while ($line = $sth->fetch()) { - - $feed_id = $line["id"]; - - print ""; - - print ""; - print ""; - print ""; - - ++$lnum; + while ($row = $sth->fetch()) { + array_push($rv, $row); } - print "
      "; - - print "". - htmlspecialchars($line["title"]).": "; - - print ""; - print htmlspecialchars($line["last_error"]); - print ""; - - print "
      "; - print "
      "; - - print "
      "; - print " "; - print ""; - - print "
      "; + print json_encode($rv); } private function remove_feed_category($id, $owner_uid) { @@ -1613,8 +1226,8 @@ class Pref_Feeds extends Handler_Protected { $pdo->commit(); - if (file_exists(ICONS_DIR . "/$id.ico")) { - unlink(ICONS_DIR . "/$id.ico"); + if (file_exists(Config::get(Config::ICONS_DIR) . "/$id.ico")) { + unlink(Config::get(Config::ICONS_DIR) . "/$id.ico"); } } else { @@ -1623,52 +1236,10 @@ class Pref_Feeds extends Handler_Protected { } function batchSubscribe() { - print "
      "; - - print_hidden("op", "pref-feeds"); - print_hidden("method", "batchaddfeeds"); - - print "
      ".__("One valid feed per line (no detection is done)")."
      "; - print "
      "; - - print ""; - - if (get_pref('ENABLE_FEED_CATS')) { - print "
      "; - print " "; - print_feed_cat_select("cat", false, 'dojoType="fox.form.Select"'); - print "
      "; - } - - print "
      "; - - print ""; - - print ""; - print "
      "; - - print "
      -
    "; - print ""; - - print "
    - - -
    "; - - print ""; + print json_encode([ + "enable_cats" => (int)get_pref('ENABLE_FEED_CATS'), + "cat_select" => \Controls\select_feeds_cats("cat") + ]); } function batchAddFeeds() { @@ -1703,14 +1274,14 @@ class Pref_Feeds extends Handler_Protected { } function getOPMLKey() { - print json_encode(["link" => OPML::opml_publish_url()]); + print json_encode(["link" => OPML::get_publish_url()]); } function regenOPMLKey() { $this->update_feed_access_key('OPML:Publish', false, $_SESSION["uid"]); - print json_encode(["link" => OPML::opml_publish_url()]); + print json_encode(["link" => OPML::get_publish_url()]); } function regenFeedKey() { @@ -1722,11 +1293,23 @@ class Pref_Feeds extends Handler_Protected { print json_encode(["link" => $new_key]); } - function getFeedKey() { + function getsharedurl() { $feed_id = clean($_REQUEST['id']); - $is_cat = clean($_REQUEST['is_cat']); + $is_cat = clean($_REQUEST['is_cat']) == "true"; + $search = clean($_REQUEST['search']); - print json_encode(["link" => Feeds::get_feed_access_key($feed_id, $is_cat, $_SESSION["uid"])]); + $link = get_self_url_prefix() . "/public.php?" . http_build_query([ + 'op' => 'rss', + 'id' => $feed_id, + 'is_cat' => (int)$is_cat, + 'q' => $search, + 'key' => Feeds::_get_access_key($feed_id, $is_cat, $_SESSION["uid"]) + ]); + + print json_encode([ + "title" => Feeds::_get_title($feed_id, $is_cat), + "link" => $link + ]); } private function update_feed_access_key($feed_id, $is_cat, $owner_uid) { @@ -1736,7 +1319,7 @@ class Pref_Feeds extends Handler_Protected { WHERE feed_id = ? AND is_cat = ? AND owner_uid = ?"); $sth->execute([$feed_id, bool_to_sql_bool($is_cat), $owner_uid]); - return Feeds::get_feed_access_key($feed_id, $is_cat, $owner_uid); + return Feeds::_get_access_key($feed_id, $is_cat, $owner_uid); } // Silent @@ -1760,29 +1343,4 @@ class Pref_Feeds extends Handler_Protected { return $c; } - function getinactivefeeds() { - if (DB_TYPE == "pgsql") { - $interval_qpart = "NOW() - INTERVAL '3 months'"; - } else { - $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)"; - } - - $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_inactive FROM ttrss_feeds WHERE - (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE - ttrss_entries.id = ref_id AND - ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart AND - ttrss_feeds.owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - - if ($row = $sth->fetch()) { - print (int)$row["num_inactive"]; - } - } - - static function subscribe_to_feed_url() { - $url_path = get_self_url_prefix() . - "/public.php?op=subscribe&feed_url=%s"; - return $url_path; - } - } diff --git a/classes/pref/filters.php b/classes/pref/filters.php index a24a05b05..fda4a6513 100755 --- a/classes/pref/filters.php +++ b/classes/pref/filters.php @@ -162,7 +162,7 @@ class Pref_Filters extends Handler_Protected { print json_encode($rv); } - private function getfilterrules_list($filter_id) { + private function _get_rules_list($filter_id) { $sth = $this->pdo->prepare("SELECT reg_exp, inverse, match_on, @@ -189,10 +189,10 @@ class Pref_Filters extends Handler_Protected { if (strpos($feed_id, "CAT:") === 0) { $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 { if ($feed_id) - array_push($feeds_fmt, Feeds::getFeedTitle((int)$feed_id)); + array_push($feeds_fmt, Feeds::_get_title((int)$feed_id)); else array_push($feeds_fmt, __("All feeds")); } @@ -203,9 +203,9 @@ class Pref_Filters extends Handler_Protected { } else { $where = $line["cat_filter"] ? - Feeds::getCategoryTitle($line["cat_id"]) : + Feeds::_get_cat_title($line["cat_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"]; @@ -250,7 +250,7 @@ class Pref_Filters extends Handler_Protected { while ($line = $sth->fetch()) { - $name = $this->getFilterName($line["id"]); + $name = $this->_get_name($line["id"]); $match_ok = false; if ($filter_search) { @@ -292,7 +292,7 @@ class Pref_Filters extends Handler_Protected { $filter['checkbox'] = false; $filter['last_triggered'] = $line["last_triggered"] ? TimeHelper::make_local_datetime($line["last_triggered"], false) : null; $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) { array_push($folder['items'], $filter); @@ -319,170 +319,94 @@ class Pref_Filters extends Handler_Protected { $sth->execute([$filter_id, $_SESSION['uid']]); 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; - $match_any_rule = $row["match_any_rule"] ?? false; - $inverse = $row["inverse"] ?? false; - $title = htmlspecialchars($row["title"] ?? ""); + $res = $this->pdo->query("SELECT id,description + FROM ttrss_filter_types WHERE id != 5 ORDER BY description"); - print "
    "; - - print_hidden("op", "pref-filters"); - - if ($filter_id) { - print_hidden("id", "$filter_id"); - print_hidden("method", "editSave"); - } else { - print_hidden("method", "add"); + while ($line = $res->fetch()) { + $rv["filter_types"][$line["id"]] = __($line["description"]); } - print_hidden("csrf_token", $_SESSION['csrf_token']); + $res = $this->pdo->query("SELECT id,description FROM ttrss_filter_actions + ORDER BY name"); - print "
    ".__("Caption")."
    -
    - -
    -
    ".__("Match")."
    -
    -
    -
    - " . __('Select')." -
    - -
    ".__('All')."
    -
    ".__('None')."
    -
    -
    - - -
    "; + while ($line = $res->fetch()) { + $rv["action_types"][$line["id"]] = __($line["description"]); + } - print "
      "; + $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) { $rules_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_rules WHERE filter_id = ? ORDER BY reg_exp, id"); - $rules_sth->execute([$filter_id]); + $rules_sth->execute([$filter_id]); - while ($line = $rules_sth->fetch()) { - if ($line["match_on"]) { - $line["feed_id"] = json_decode($line["match_on"], true); + while ($rrow = $rules_sth->fetch(PDO::FETCH_ASSOC)) { + if ($rrow["match_on"]) { + $rrow["feed_id"] = json_decode($rrow["match_on"], true); } else { - if ($line["cat_filter"]) { - $feed_id = "CAT:" . (int)$line["cat_id"]; + if ($rrow["cat_filter"]) { + $feed_id = "CAT:" . (int)$rrow["cat_id"]; } 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($line["cat_id"]); - unset($line["filter_id"]); - unset($line["id"]); - if (!$line["inverse"]) unset($line["inverse"]); - unset($line["match_on"]); + unset($rrow["cat_filter"]); + unset($rrow["cat_id"]); + unset($rrow["filter_id"]); + unset($rrow["id"]); + if (!$rrow["inverse"]) unset($rrow["inverse"]); + unset($rrow["match_on"]); - $data = htmlspecialchars((string)json_encode($line)); + $rrow["name"] = $this->_get_rule_name($rrow); - print "
    • - ".$this->getRuleName($line)."". - format_hidden("rule[]", $data)."
    • "; + array_push($rv["rules"], $rrow); } - } - print "
    -
    "; - - print "
    ".__("Apply actions")."
    -
    -
    -
    - ".__('Select')." -
    -
    ".__('All')."
    -
    ".__('None')."
    -
    -
    - - -
    "; - - print "
      "; - - if ($filter_id) { $actions_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions WHERE filter_id = ? ORDER BY id"); $actions_sth->execute([$filter_id]); - while ($line = $actions_sth->fetch()) { - $line["action_param_label"] = $line["action_param"]; + while ($arow = $actions_sth->fetch(PDO::FETCH_ASSOC)) { + $arow["action_param_label"] = $arow["action_param"]; - unset($line["filter_id"]); - unset($line["id"]); + unset($arow["filter_id"]); + unset($arow["id"]); - $data = htmlspecialchars((string)json_encode($line)); + $arow["name"] = $this->_get_action_name($arow); - print "
    • - ".$this->getActionName($line)."". - format_hidden("action[]", $data)."
    • "; + array_push($rv["actions"], $arow); } } - - print "
    "; - - print "
    "; - - print "
    ".__("Options")."
    -
    "; - - print "
    -
    "; - - print "
    - -
    "; - - print "
    -
    "; - - print "
    -
    "; - - if ($filter_id) { - print "
    - -
    - - - "; - } else { - print " - - "; - } - - print "
    "; + print json_encode($rv); } } - private function getRuleName($rule) { + private function _get_rule_name($rule) { if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true); $feeds = $rule["feed_id"]; @@ -494,10 +418,10 @@ class Pref_Filters extends Handler_Protected { if (strpos($feed_id, "CAT:") === 0) { $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 { if ($feed_id) - array_push($feeds_fmt, Feeds::getFeedTitle((int)$feed_id)); + array_push($feeds_fmt, Feeds::_get_title((int)$feed_id)); else array_push($feeds_fmt, __("All feeds")); } @@ -523,10 +447,10 @@ class Pref_Filters extends Handler_Protected { } 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 ttrss_filter_actions WHERE id = ?"); $sth->execute([(int)$action["action_id"]]); @@ -561,13 +485,13 @@ class Pref_Filters extends Handler_Protected { } function printActionName() { - print $this->getActionName(json_decode(clean($_REQUEST["action"]), true)); + print $this->_get_action_name(json_decode(clean($_REQUEST["action"]), true)); } function editSave() { $filter_id = clean($_REQUEST["id"]); $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)); $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']]); - $this->saveRulesAndActions($filter_id); + $this->_save_rules_and_actions($filter_id); $this->pdo->commit(); } @@ -596,8 +520,7 @@ class Pref_Filters extends Handler_Protected { $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->execute([$filter_id]); @@ -674,11 +597,11 @@ class Pref_Filters extends Handler_Protected { } } - function add() { - $enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"])); - $match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"])); + function add () { + $enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false)); + $match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false)); $title = clean($_REQUEST["title"]); - $inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"])); + $inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false)); $this->pdo->beginTransaction(); @@ -696,7 +619,7 @@ class Pref_Filters extends Handler_Protected { if ($row = $sth->fetch()) { $filter_id = $row['id']; - $this->saveRulesAndActions($filter_id); + $this->_save_rules_and_actions($filter_id); } $this->pdo->commit(); @@ -710,257 +633,73 @@ class Pref_Filters extends Handler_Protected { $filter_search = ($_SESSION["prefs_filter_search"] ?? ""); } - print "
    "; - print "
    "; - print "
    "; + ?> +
    +
    +
    - print "
    - - -
    "; +
    + + +
    - print "
    ". - "" . __('Select').""; - print "
    "; - print "
    ".__('All')."
    "; - print "
    ".__('None')."
    "; - print "
    "; +
    + +
    +
    +
    +
    +
    - print " "; + + + + - print " "; +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters") ?>
    -
    -
    -
    - - - -
    "; - - print "
    "; #pane - - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters"); - - print "
    "; #container - + "; - - $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 "
    ".__("Match")."
    "; - - print "
    "; - - print ""; - - print "
    "; - - print "
    "; - print ""; - print "
    "; - - print "
    "; - print " "; - print_select_hash("filter_type", $filter_type, $filter_types, - 'dojoType="fox.form.Select"'); - print " "; - - print "
    "; - - print "
    "; - print ""; - print_feed_multi_select("feed_id", - $feed_id, - 'style="width : 500px; height : 300px" dojoType="dijit.form.MultiSelect"'); - print ""; - - print "
    "; - - print "
    "; - - print "
    "; - - print ""; - - print " "; - - print ""; - - print "
    "; - - print ""; + print json_encode([ + "multiselect" => $this->_feed_multi_select("feed_id", $feed_ids, 'required="1" style="width : 100%; height : 300px" dojoType="fox.form.ValidationMultiSelect"') + ]); } - 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 "
    "; - - print "
    ".__("Perform Action")."
    "; - - print "
    "; - - print ""; - - $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 ""; - print " "; - //print " " . __("with parameters:") . " "; - print ""; - - 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 ""; - - print " "; // tiny layout hack - - print "
    "; - - print "
    "; - - print " "; - - print ""; - - print "
    "; - - print "
    "; - } - - private function getFilterName($id) { + private function _get_name($id) { $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 @@ -989,7 +728,7 @@ class Pref_Filters extends Handler_Protected { $actions = ""; if ($line = $sth->fetch()) { - $actions = $this->getActionName($line); + $actions = $this->_get_action_name($line); $num_actions -= 1; } @@ -1031,12 +770,12 @@ class Pref_Filters extends Handler_Protected { $this->pdo->commit(); - $this->optimizeFilter($base_id); + $this->_optimize($base_id); } } - private function optimizeFilter($id) { + private function _optimize($id) { $this->pdo->beginTransaction(); @@ -1090,4 +829,111 @@ class Pref_Filters extends Handler_Protected { $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 .= ""; + } + + return $rv; + } } diff --git a/classes/pref/labels.php b/classes/pref/labels.php index a787ce388..5bc094d55 100644 --- a/classes/pref/labels.php +++ b/classes/pref/labels.php @@ -10,72 +10,12 @@ class Pref_Labels extends Handler_Protected { function edit() { $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 = ?"); $sth->execute([$label_id, $_SESSION['uid']]); - if ($line = $sth->fetch()) { - - print_hidden("id", "$label_id"); - print_hidden("op", "pref-labels"); - print_hidden("method", "save"); - - print "
    "; - - print "
    ".__("Caption")."
    "; - - print "
    "; - - $fg_color = $line['fg_color']; - $bg_color = $line['bg_color'] ? $line['bg_color'] : '#fff7d5'; - - print ""; - - print "
    "; - - print "
    " . __("Colors") . "
    "; - print "
    "; - - print ""; - print ""; - print "
    ".__("Foreground:")."".__("Background:")."
    "; - - print ""; - print ""; - - print "
    - -
    "; - - print "
    "; - - print "
    - -
    "; - - print "
    "; - print "
    "; - - print "
    "; - print ""; - print ""; - print "
    "; - - print "
    "; + if ($line = $sth->fetch(PDO::FETCH_ASSOC)) { + print json_encode($line); } } @@ -197,7 +137,7 @@ class Pref_Labels extends Handler_Protected { $sth->execute([$caption, $old_caption, $_SESSION['uid']]); - print clean($_REQUEST["value"]); + print clean($_REQUEST["caption"]); } else { print $old_caption; } @@ -225,88 +165,64 @@ class Pref_Labels extends Handler_Protected { $output = clean($_REQUEST["output"]); if ($caption) { - if (Labels::create($caption)) { if (!$output) { print T_sprintf("Created label %s", htmlspecialchars($caption)); } } - - if ($output == "select") { - header("Content-Type: text/xml"); - - print ""; - - print_label_select("select_label", - $caption, ""); - - print ""; - } } - - return; } function index() { + ?> +
    +
    +
    +
    + +
    +
    +
    +
    +
    - print "
    "; - print "
    "; - print "
    "; + - print "
    ". - "" . __('Select').""; - print "
    "; - print "
    ".__('All')."
    "; - print "
    ".__('None')."
    "; - print "
    "; + - print" "; + - print " "; +
    +
    - print ""; +
    +
    +
    +
    +
    - print "
    "; #toolbar - print "
    "; #pane - print "
    "; +
    + +
    +
    + run_hooks(PluginHost::HOOK_PREFS_TAB, "prefLabels") ?>
    -
    -
    -
    - - -
    "; - - print "
    "; #pane - - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefLabels"); - - print "
    "; #container - + setVariable('LOGIN', $row["login"]); $tpl->setVariable('NEWMAIL', $email); - $tpl->setVariable('TTRSS_HOST', SELF_URL_PATH); + $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH)); $tpl->addBlock('message'); @@ -267,39 +268,12 @@ class Pref_Prefs extends Handler_Protected { AND owner_uid = :uid"); $sth->execute([":profile" => $_SESSION['profile'], ":uid" => $_SESSION['uid']]); - $this->initialize_user_prefs($_SESSION["uid"], $_SESSION["profile"]); + $this->_init_user_prefs($_SESSION["uid"], $_SESSION["profile"]); echo __("Your preferences are now set to default values."); } - function index() { - - global $access_level_names; - - $_SESSION["prefs_op_result"] = ""; - - print "
    "; - print "
    person ".__('Personal data / Authentication')."\">"; - - print "
    "; - print "
    "; - - print "
    "; - - print ""; + private function index_auth_personal() { $sth = $this->pdo->prepare("SELECT email,full_name,otp_enabled, access_level FROM ttrss_users @@ -311,179 +285,196 @@ class Pref_Prefs extends Handler_Protected { $full_name = htmlspecialchars($row["full_name"]); $otp_enabled = sql_bool_to_bool($row["otp_enabled"]); - print "
    "; - print ""; - print ""; - print "
    "; + ?> + - print "
    "; - print ""; - print ""; - print "
    "; + + - if (!SINGLE_USER_MODE && !empty($_SESSION["hide_hello"])) { + - $access_level = $row["access_level"]; - print "
    "; - print ""; - print $access_level_names[$access_level]; - print "
    "; - } +
    + + +
    - print_hidden("op", "pref-prefs"); - print_hidden("method", "changeemail"); +
    + + +
    - print "
    "; +
    - print ""; - - print "
    "; - - print "
    "; # content pane + + + get_plugin($_SESSION["auth_module"]); } else { $authenticator = false; } - print "
    "; + $otp_enabled = $this->is_otp_enabled(); if ($authenticator && method_exists($authenticator, "change_password")) { + ?> - print ""; + - print "
    "; + - print " - }}); - this.reset(); - } - "; + - if ($otp_enabled) { - print_notice(__("Changing your current password will disable OTP.")); - } +
    + + +
    - print "
    "; - print ""; - print ""; - print "
    "; +
    + + +
    - print "
    "; - print ""; - print ""; - print "
    "; +
    + + +
    - print "
    "; - print ""; - print ""; - print "
    "; +
    - print_hidden("op", "pref-prefs"); - print_hidden("method", "changepassword"); + +
    - print "
    "; - - print ""; - - print ""; + %s) does not provide an ability to set passwords.", $_SESSION["auth_module"])); } + } - print "
    "; # content pane - - print "
    "; - + private function index_auth_app_passwords() { print_notice("You can create separate passwords for API clients. Using one is required if you enable OTP."); + ?> - print "
    "; - $this->appPasswordList(); - print "
    "; +
    + appPasswordList() ?> +
    - print "
    "; +
    - print " "; + - print ""; + - print "
    "; # content pane + "; + private function is_otp_enabled() { + $sth = $this->pdo->prepare("SELECT otp_enabled FROM ttrss_users + WHERE id = ?"); + $sth->execute([$_SESSION["uid"]]); + + if ($row = $sth->fetch()) { + return sql_bool_to_bool($row["otp_enabled"]); + } + + return false; + } + + private function index_auth_2fa() { + $otp_enabled = $this->is_otp_enabled(); if ($_SESSION["auth_module"] == "auth_internal") { - if ($otp_enabled) { - print_warning("One time passwords are currently enabled. Enter your current password below to disable."); + ?> - print "
    "; + + + - print ""; + if (reply.indexOf('ERROR: ') == 0) { + Notify.error(reply.replace('ERROR: ', '')); + } else { + window.location.reload(); + } + }) + } + - print "
    "; - print ""; - print ""; - print "
    "; +
    + + +
    - print_hidden("op", "pref-prefs"); - print_hidden("method", "otpdisable"); +
    - print "
    "; + - print ""; +
    - print ""; + " . __("Scan the following code by the Authenticator application or copy the key manually") . ""; - $csrf_token_hash = sha1($_SESSION["csrf_token"]); print "otp qr-code"; } else { @@ -500,108 +490,87 @@ class Pref_Prefs extends Handler_Protected { print "

    " . __("Use the following OTP key with a compatible Authenticator application") . "

    "; } - print "
    "; - $otp_secret = $this->otpsecret(); + ?> - print "
    "; - print ""; - print ""; - print "
    "; + - print_hidden("op", "pref-prefs"); - print_hidden("method", "otpenable"); + + - print ""; + if (reply.indexOf('ERROR:') == 0) { + Notify.error(reply.replace('ERROR:', '')); + } else { + window.location.reload(); + } + }) + } + - print "
    "; - print ""; - print ""; - print "
    "; +
    + + +
    - print "
    "; - print ""; - print ""; - print "
    "; +
    + + +
    - print "
    "; - print ""; +
    - print "
    "; + + + auth_internal authentication module."); } + } - print "
    "; # content pane - - print "
    "; # tab container - - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefPrefsAuth"); - - print "
    "; #pane - - print "
    settings ".__('Preferences')."\">"; - - print "
    "; - - print ""; - - print '
    '; - - print '
    '; + function index_auth() { + ?> +
    +
    + index_auth_personal() ?> +
    +
    + index_auth_password() ?> +
    +
    + index_auth_app_passwords() ?> +
    +
    + index_auth_2fa() ?> +
    +
    + initialize_user_prefs($_SESSION["uid"], $profile); + $this->_init_user_prefs($_SESSION["uid"], $profile); } else { - $this->initialize_user_prefs($_SESSION["uid"]); + $this->_init_user_prefs($_SESSION["uid"]); } $prefs_available = []; @@ -632,7 +601,7 @@ class Pref_Prefs extends Handler_Protected { } $pref_name = $line["pref_name"]; - $short_desc = $this->getShortDesc($pref_name); + $short_desc = $this->_get_short_desc($pref_name); if (!$short_desc) continue; @@ -640,7 +609,7 @@ class Pref_Prefs extends Handler_Protected { $prefs_available[$pref_name] = [ 'type_name' => $line["type_name"], 'value' => $line['value'], - 'help_text' => $this->getHelpText($pref_name), + 'help_text' => $this->_get_help_text($pref_name), 'short_desc' => $short_desc ]; } @@ -656,11 +625,13 @@ class Pref_Prefs extends Handler_Protected { continue; } - if ($pref_name == "DEFAULT_SEARCH_LANGUAGE" && DB_TYPE != "pgsql") { + if ($pref_name == "DEFAULT_SEARCH_LANGUAGE" && Config::get(Config::DB_TYPE) != "pgsql") { continue; } - if (isset($prefs_available[$pref_name]) &&$item = $prefs_available[$pref_name]) { + if (isset($prefs_available[$pref_name])) { + + $item = $prefs_available[$pref_name]; print "
    "; @@ -672,14 +643,14 @@ class Pref_Prefs extends Handler_Protected { $type_name = $item['type_name']; if ($pref_name == "USER_LANGUAGE") { - print_select_hash($pref_name, $value, get_translations(), - "style='width : 220px; margin : 0px' dojoType='fox.form.Select'"); + print \Controls\select_hash($pref_name, $value, get_translations(), + ["style" => 'width : 220px; margin : 0px']); } else if ($pref_name == "USER_TIMEZONE") { $timezones = explode("\n", file_get_contents("lib/timezones.txt")); - print_select($pref_name, $value, $timezones, 'dojoType="dijit.form.FilteringSelect"'); + print \Controls\select_tag($pref_name, $value, $timezones, ["dojoType" => "dijit.form.FilteringSelect"]); } else if ($pref_name == "BLACKLISTED_TAGS") { # TODO: other possible `; - - if (params.info) - stack_msg += `
    Additional information:
    - `; - - const content = `
    -

    ${message}

    - ${stack_msg} -
    - -
    -
    `; - const dialog = new fox.SingleUseDialog({ - id: "exceptionDlg", title: params.title || __("Unhandled exception"), - content: content + content: ` +
    +

    ${message}

    + +
    ${__('Stack trace')}
    +
    + +
    + + ${params && params.info ? + ` +
    ${__('Additional information')}
    +
    + +
    + ` : ''} +
    +
    + +
    +
    ` }); dialog.show(); @@ -575,6 +609,10 @@ const App = { isPrefs() { return this.is_prefs; }, + audioCanPlay: function(ctype) { + const a = document.createElement('audio'); + return a.canPlayType(ctype); + }, init: function(parser, is_prefs) { this.is_prefs = is_prefs; window.onerror = this.Error.onWindowError; @@ -591,24 +629,17 @@ const App = { this.setLoadingProgress(30); 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 = { - op: "rpc", method: "sanityCheck", hasAudio: hasAudio, - hasMp3: hasMp3, - clientTzOffset: clientTzOffset, - hasSandbox: hasSandbox + op: "rpc", + method: "sanityCheck", + clientTzOffset: new Date().getTimezoneOffset() * 60, + hasSandbox: "sandbox" in document.createElement("iframe") }; - xhrPost("backend.php", params, (transport) => { + xhr.json("backend.php", params, (reply) => { try { - this.backendSanityCallback(transport); + this.backendSanityCallback(reply); } catch (e) { this.Error.report(e); } @@ -618,7 +649,7 @@ const App = { checkBrowserFeatures: function() { let errorMsg = ""; - ['MutationObserver'].each(function(wf) { + ['MutationObserver'].forEach(function(wf) { if (!(wf in window)) { errorMsg = `Browser feature check failed: window.${wf} not found.`; throw new Error(errorMsg); @@ -631,6 +662,11 @@ const App = { return errorMsg == ""; }, + updateRuntimeInfo: function() { + xhr.json("backend.php", {op: "rpc", method: "getruntimeinfo"}, () => { + // handled by xhr.json() + }); + }, initSecondStage: function() { document.onkeydown = (event) => this.hotkeyHandler(event); @@ -648,14 +684,18 @@ const App = { if (tab) { dijit.byId("pref-tabs").selectChild(tab); - switch (this.urlParam('method')) { - case "editfeed": - window.setTimeout(() => { - CommonDialogs.editFeed(this.urlParam('methodparam')) - }, 100); - break; - default: - console.warn("initSecondStage, unknown method:", this.urlParam("method")); + const method = this.urlParam("method"); + + if (method) { + switch (method) { + case "editfeed": + window.setTimeout(() => { + CommonDialogs.editFeed(this.urlParam('methodparam')) + }, 100); + break; + default: + console.warn("initSecondStage, unknown method:", method); + } } } } else { @@ -671,6 +711,7 @@ const App = { dojo.connect(dijit.byId("pref-tabs"), "selectChild", function (elem) { localStorage.setItem("ttrss:prefs-tab", elem.id); + App.updateRuntimeInfo(); }); } else { @@ -726,24 +767,28 @@ const App = { }, 3600 * 1000); } - console.log("second stage ok"); - 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() { console.log('checking for updates...'); - xhrJson("backend.php", {op: 'rpc', method: 'checkforupdates'}) + xhr.json("backend.php", {op: 'rpc', method: 'checkforupdates'}) .then((reply) => { console.log('update reply', reply); if (reply.id) { - $("updates-available").show(); + App.byId("updates-available").show(); } else { - $("updates-available").hide(); + App.byId("updates-available").hide(); } }); }, @@ -759,7 +804,7 @@ const App = { onViewModeChanged: function() { 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(''); }, @@ -798,8 +843,8 @@ const App = { {width: Cookie.get("ttrss_ci_width") + "px" }); } - $("headlines-frame").setStyle({ borderBottomWidth: '0px' }); - $("headlines-frame").addClassName("wide"); + App.byId("headlines-frame").setStyle({ borderBottomWidth: '0px' }); + App.byId("headlines-frame").addClassName("wide"); } else { @@ -814,8 +859,8 @@ const App = { {height: Cookie.get("ttrss_ci_height") + "px" }); } - $("headlines-frame").setStyle({ borderBottomWidth: '1px' }); - $("headlines-frame").removeClassName("wide"); + App.byId("headlines-frame").setStyle({ borderBottomWidth: '1px' }); + App.byId("headlines-frame").removeClassName("wide"); } @@ -823,13 +868,13 @@ const App = { 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() { if (this.is_prefs) { this.hotkey_actions["feed_subscribe"] = () => { - CommonDialogs.quickAddFeed(); + CommonDialogs.subscribeToFeed(); }; this.hotkey_actions["create_label"] = () => { @@ -841,7 +886,7 @@ const App = { }; this.hotkey_actions["help_dialog"] = () => { - this.helpDialog("main"); + this.hotkeyHelp(); }; } else { @@ -985,14 +1030,13 @@ const App = { Feeds.toggleUnread(); }; this.hotkey_actions["feed_subscribe"] = () => { - CommonDialogs.quickAddFeed(); + CommonDialogs.subscribeToFeed(); }; this.hotkey_actions["feed_debug_update"] = () => { if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) { - //window.open("backend.php?op=feeds&method=update_debugger&feed_id=" + Feeds.getActive()); /* 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}); } else { @@ -1001,8 +1045,6 @@ const App = { }; 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", feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token}); }; @@ -1022,7 +1064,7 @@ const App = { Headlines.reverse(); }; 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(); }) }; @@ -1055,7 +1097,7 @@ const App = { this.hotkey_actions["select_article_cursor"] = () => { const id = Article.getUnderPointer(); if (id) { - const row = $("RROW-" + id); + const row = App.byId(`RROW-${id}`); if (row) row.toggleClassName("Selected"); @@ -1092,12 +1134,12 @@ const App = { } }; this.hotkey_actions["help_dialog"] = () => { - this.helpDialog("main"); + this.hotkeyHelp(); }; this.hotkey_actions["toggle_combined_mode"] = () => { 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.getInitParam("combined_display_mode")); @@ -1108,7 +1150,7 @@ const App = { this.hotkey_actions["toggle_cdm_expanded"] = () => { 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")); Headlines.renderAgain(); }); @@ -1130,7 +1172,7 @@ const App = { Feeds.search(); break; case "qmcAddFeed": - CommonDialogs.quickAddFeed(); + CommonDialogs.subscribeToFeed(); break; case "qmcDigest": window.location.href = "backend.php?op=digest"; @@ -1182,7 +1224,7 @@ const App = { } break; case "qmcHKhelp": - this.helpDialog("main"); + this.hotkeyHelp() break; default: console.log("quickMenuGo: unknown action: " + opid); diff --git a/js/Article.js b/js/Article.js index 61368dfed..5f695561c 100644 --- a/js/Article.js +++ b/js/Article.js @@ -1,7 +1,7 @@ 'use strict' /* 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 = { _scroll_reset_timeout: false, @@ -36,19 +36,19 @@ const Article = { const score = prompt(__("Please enter new score for selected articles:")); if (!isNaN(parseInt(score))) { - ids.each((id) => { - const row = $("RROW-" + id); + ids.forEach((id) => { + const row = App.byId(`RROW-${id}`); if (row) { row.setAttribute("data-score", score); - const pic = row.select(".icon-score")[0]; + const pic = row.querySelector(".icon-score"); pic.innerHTML = Article.getScorePic(score); pic.setAttribute("title", score); ["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"] - .each(function(scl) { + .forEach(function(scl) { if (row.hasClassName(scl)) row.removeClassName(scl); }); @@ -63,7 +63,7 @@ const Article = { } }, setScore: function (id, pic) { - const row = pic.up("div[id*=RROW]"); + const row = pic.closest("div[id*=RROW]"); if (row) { const score_old = row.getAttribute("data-score"); @@ -72,13 +72,13 @@ const Article = { if (!isNaN(parseInt(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.setAttribute("title", score); ["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"] - .each(function(scl) { + .forEach(function(scl) { if (row.hasClassName(scl)) row.removeClassName(scl); }); @@ -93,18 +93,8 @@ const Article = { w.opener = null; 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) { - const row = $("RROW-" + Article.getActive()); + const row = App.byId(`RROW-${Article.getActive()}`); if (row) { row.removeClassName("active"); @@ -123,11 +113,13 @@ const Article = { Article.setActive(0); }, 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) { 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); }, + renderNote: function (id, note) { + return `
    + ${App.FormFields.icon('note')}
    ${note ? App.escapeHtml(note) : ""}
    +
    `; + }, + renderTags: function (id, tags) { + const tags_short = tags.length > 5 ? tags.slice(0, 5) : tags; + + return ` + ${tags_short.length > 0 ? tags_short.map((tag) => ` + ${tag}` + ).join(", ") : `${__("no tags")}`}`; + }, + renderLabels: function(id, labels) { + return `${labels.map((label) => ` + ${App.escapeHtml(label[1])}` + ).join("")}`; + }, + renderEnclosures: function (enclosures) { + return ` + ${enclosures.formatted} + ${enclosures.can_inline ? + `
    + ${enclosures.entries.map((enc) => { + if (!enclosures.inline_text_only) { + if (enc.content_type && enc.content_type.indexOf("image/") != -1) { + return `

    + +

    ` + } else if (enc.content_type && enc.content_type.indexOf("audio/") != -1 && App.audioCanPlay(enc.content_type)) { + return `

    + +

    + `; + } else { + return `

    + ${App.escapeHtml(enc.content_url)} +

    ` + } + } else { + return `

    + ${App.escapeHtml(enc.content_url)} +

    ` + } + }).join("")} +
    ` : ''} + ${enclosures.entries.length > 0 ? + `
    + ${__('Attachments')} +
    + ${enclosures.entries.map((enc) => ` +
    + ${enc.title ? enc.title : enc.filename} +
    + `).join("")} +
    +
    ` : ''} + ` + }, render: function (article) { App.cleanupMemory("content-insert"); @@ -184,12 +247,14 @@ const Article = { container.innerHTML = row.getAttribute("data-content").trim(); + dojo.parser.parse(container); + // blank content element might screw up onclick selection and keyboard moving if (container.textContent.length == 0) container.innerHTML += " "; // 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.removeAttribute("data-content"); @@ -230,16 +295,16 @@ const Article = {
    ${comments}
    ${hl.author}
    label_outline - ${hl.tags_str} + ${Article.renderTags(hl.id, hl.tags)}  (+)
    ${hl.buttons}
    -
    ${hl.note}
    + ${Article.renderNote(hl.id, hl.note)}
    ${hl.content} - ${hl.enclosures} + ${Article.renderEnclosures(hl.enclosures)}
    `; @@ -252,29 +317,41 @@ const Article = { }, editTags: function (id) { const dialog = new fox.SingleUseDialog({ - id: "editTagsDlg", 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")} + +
    + ${__("Tags for this article (separated by commas):")} +
    + +
    + + +
    + +
    + + +
    + `, execute: function () { if (this.validate()) { Notify.progress("Saving article tags...", true); - xhrPost("backend.php", this.attr('value'), (transport) => { + xhr.json("backend.php", this.attr('value'), (data) => { try { Notify.close(); dialog.hide(); - const data = JSON.parse(transport.responseText); - - 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); - } + Headlines.onTagsUpdated(data); } catch (e) { App.Error.report(e); } @@ -286,25 +363,26 @@ const Article = { const tmph = dojo.connect(dialog, 'onShow', function () { dojo.disconnect(tmph); - xhrPost("backend.php", {op: "article", method: "editarticletags", param: id}, (transport) => { - dialog.attr('content', transport.responseText); + xhr.json("backend.php", {op: "article", method: "printArticleTags", id: id}, (reply) => { - 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", - {tokens: ',', paramName: "search"}); + {tokens: ',', paramName: "search"}); */ }); }); dialog.show(); }, - cdmMoveToId: function (id, params) { - params = params || {}; - + cdmMoveToId: function (id, params = {}) { const force_to_top = params.force_to_top || false; - const ctr = $("headlines-frame"); - const row = $("RROW-" + id); + const ctr = App.byId("headlines-frame"); + const row = App.byId(`RROW-${id}`); if (!row || !ctr) return; @@ -316,12 +394,12 @@ const Article = { if (id != 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"); Article.pack(row); }); - const row = $("RROW-" + id); + const row = App.byId(`RROW-${id}`); if (row) { Article.unpack(row); @@ -342,10 +420,10 @@ const Article = { return 0; }, scrollByPages: function (page_offset) { - App.Scrollable.scrollByPages($("content-insert"), page_offset); + App.Scrollable.scrollByPages(App.byId("content-insert"), page_offset); }, scroll: function (offset) { - App.Scrollable.scroll($("content-insert"), offset); + App.Scrollable.scroll(App.byId("content-insert"), offset); }, mouseIn: function (id) { this.post_under_pointer = id; diff --git a/js/CommonDialogs.js b/js/CommonDialogs.js index 70596539b..321ddf6d3 100644 --- a/js/CommonDialogs.js +++ b/js/CommonDialogs.js @@ -3,7 +3,7 @@ /* eslint-disable new-cap */ /* 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 */ const CommonDialogs = { @@ -11,89 +11,99 @@ const CommonDialogs = { const dialog = dijit.byId("infoBox"); if (dialog) dialog.hide(); }, - removeFeedIcon: function(id) { - if (confirm(__("Remove stored feed icon?"))) { - Notify.progress("Removing feed icon...", true); - - 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) => { - + subscribeToFeed: function() { + xhr.json("backend.php", + {op: "feeds", method: "subscribeToFeed"}, + (reply) => { const dialog = new fox.SingleUseDialog({ - id: "feedAddDlg", title: __("Subscribe to Feed"), - content: transport.responseText, + content: ` + + + ${App.FormFields.hidden_tag("op", "feeds")} + ${App.FormFields.hidden_tag("method", "add")} + + + + + +
    +
    +
    + +
    + + ${App.getInitParam('enable_feed_cats') ? + ` +
    + + ${reply.cat_select} +
    + ` : ''} +
    + + + + + +
    + +
    + +
    + + +
    + + `, show_error: function (msg) { - const elem = $("fadd_error_message"); + const elem = App.byId("fadd_error_message"); elem.innerHTML = msg; - if (!Element.visible(elem)) - new Effect.Appear(elem); - + Element.show(elem); }, execute: function () { if (this.validate()) { @@ -104,17 +114,12 @@ const CommonDialogs = { Element.show("feed_add_spinner"); Element.hide("fadd_error_message"); - xhrPost("backend.php", this.attr('value'), (transport) => { + xhr.json("backend.php", this.attr('value'), (reply) => { try { - let reply; - - try { - reply = JSON.parse(transport.responseText); - } catch (e) { + if (!reply) { 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.")); - console.log('quickAddFeed, backend returned:' + transport.responseText); return; } @@ -161,7 +166,7 @@ const CommonDialogs = { } } - Effect.Appear('feedDlg_feedsContainer', {duration: 0.5}); + Element.show('feedDlg_feedsContainer'); } break; case 5: @@ -188,69 +193,103 @@ const CommonDialogs = { }); }, showFeedsWithErrors: function() { - const dialog = new fox.SingleUseDialog({ - id: "errorFeedsDlg", - title: __("Feeds with update errors"), - getSelectedFeeds: function () { - return Tables.getSelected("error-feeds-list"); - }, - removeSelected: function () { - const sel_rows = this.getSelectedFeeds(); - if (sel_rows.length > 0) { - if (confirm(__("Remove selected feeds?"))) { - Notify.progress("Removing selected feeds...", true); + xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => { - const query = { - op: "pref-feeds", method: "remove", - ids: sel_rows.toString() - }; + const dialog = new fox.SingleUseDialog({ + id: "errorFeedsDlg", + title: __("Feeds with update errors"), + getSelectedFeeds: function () { + return Tables.getSelected("error-feeds-list"); + }, + removeSelected: function () { + const sel_rows = this.getSelectedFeeds(); - xhrPost("backend.php", query, () => { - Notify.close(); - dialog.hide(); + if (sel_rows.length > 0) { + if (confirm(__("Remove selected feeds?"))) { + Notify.progress("Removing selected feeds...", true); - if (App.isPrefs()) - dijit.byId("feedTree").reload(); - else - Feeds.reload(); + const query = { + op: "pref-feeds", method: "remove", + ids: sel_rows.toString() + }; - }); + xhr.post("backend.php", query, () => { + Notify.close(); + dialog.hide(); + + if (App.isPrefs()) + dijit.byId("feedTree").reload(); + else + Feeds.reload(); + + }); + } + + } else { + alert(__("No feeds selected.")); } + }, + content: ` +
    +
    + ${__('Select')} +
    +
    ${__('All')}
    +
    ${__('None')}
    +
    +
    +
    - } else { - alert(__("No feeds selected.")); - } - }, - content: __("Loading, please wait...") - }); +
    + - const tmph = dojo.connect(dialog, 'onShow', function () { - dojo.disconnect(tmph); + ${reply.map((row) => ` + + + + + + `).join("")} +
    + + + + ${App.escapeHtml(row.title)} + + + ${App.escapeHtml(row.last_error)} +
    +
    - xhrPost("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (transport) => { - dialog.attr('content', transport.responseText); - }) - }); +
    + + +
    + ` + }); - dialog.show(); + dialog.show(); + }) }, - addLabel: function(select, callback) { + addLabel: function() { const caption = prompt(__("Please enter label caption:"), ""); if (caption != undefined && caption.trim().length > 0) { const query = {op: "pref-labels", method: "add", caption: caption.trim()}; - if (select) - Object.extend(query, {output: "select"}); - Notify.progress("Loading, please wait...", true); - xhrPost("backend.php", query, (transport) => { - if (callback) { - callback(transport); - } else if (App.isPrefs()) { + xhr.post("backend.php", query, () => { + if (dijit.byId("labelTree")) { dijit.byId("labelTree").reload(); } else { Feeds.reload(); @@ -267,13 +306,13 @@ const CommonDialogs = { const query = {op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id}; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { if (App.isPrefs()) { dijit.byId("feedTree").reload(); } else { if (feed_id == Feeds.getActive()) setTimeout(() => { - Feeds.open({feed: -5}) + Feeds.openDefaultFeed(); }, 100); @@ -284,28 +323,109 @@ const CommonDialogs = { return false; }, - editFeed: function (feed) { - if (feed <= 0) + editFeed: function (feed_id) { + if (feed_id <= 0) 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); const dialog = new fox.SingleUseDialog({ id: "feedEditDlg", title: __("Edit Feed"), - unsubscribeFeed: function(feed_id, title) { - if (confirm(__("Unsubscribe from %s?").replace("%s", title))) { + feed_title: "", + unsubscribe: function() { + if (confirm(__("Unsubscribe from %s?").replace("%s", this.feed_title))) { dialog.hide(); 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 () { if (this.validate()) { Notify.progress("Saving data...", true); - xhrPost("backend.php", dialog.attr('value'), () => { + xhr.post("backend.php", dialog.attr('value'), () => { dialog.hide(); Notify.close(); @@ -315,7 +435,9 @@ const CommonDialogs = { Feeds.reload(); }); + return true; } + return false; }, content: __("Loading, please wait...") }); @@ -323,102 +445,195 @@ const CommonDialogs = { const tmph = dojo.connect(dialog, 'onShow', function () { dojo.disconnect(tmph); - xhrPost("backend.php", {op: "pref-feeds", method: "editfeed", id: feed}, (transport) => { - dialog.attr('content', transport.responseText); + xhr.json("backend.php", {op: "pref-feeds", method: "editfeed", id: feed_id}, (reply) => { + 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', + ` +
    +
    +
    + + ${App.FormFields.hidden_tag("id", feed_id)} + ${App.FormFields.hidden_tag("op", "pref-feeds")} + ${App.FormFields.hidden_tag("method", "editSave")} + +
    +
    + +
    + +
    + + + + ${feed.last_error ? + `error + ` : ""} +
    + + ${reply.cats.enabled ? + ` +
    + + ${reply.cats.select} +
    + ` : ""} + +
    + + +
    + + ${reply.lang.enabled ? + ` +
    + + ${App.FormFields.select_tag("feed_language", + feed.feed_language ? feed.feed_language : reply.lang.default, + reply.lang.all)} +
    + ` : ""} + +
    + +
    + + ${App.FormFields.select_hash("update_interval", feed.update_interval, reply.intervals.update)} +
    +
    + + + ${App.FormFields.select_hash("purge_interval", + feed.purge_interval, + reply.intervals.purge, + reply.force_purge ? {disabled: 1} : {})} + +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + ${Object.keys(options).map((name) => + ` +
    + +
    + `).join("")} +
    +
    +
    +
    + + + + ${App.FormFields.submit_tag(__("Remove"), {class: "alt-danger", onclick: "App.dialogOf(this).removeIcon("+feed_id+")"})} +
    +
    + ${reply.plugin_data} +
    +
    +
    + ${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"))} +
    +
    + `); }) }); dialog.show(); }, - genUrlChangeKey: function(feed, is_cat) { - if (confirm(__("Generate new syndication address for this feed?"))) { - - Notify.progress("Trying to change address...", true); - - const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat}; - - xhrJson("backend.php", query, (reply) => { - const new_link = reply.link; - const e = $('gen_feed_url'); - - if (new_link) { - e.innerHTML = e.innerHTML.replace(/&key=.*$/, - "&key=" + new_link); - - e.href = e.href.replace(/&key=.*$/, - "&key=" + new_link); - - new Effect.Highlight(e); - - Notify.close(); - - } else { - Notify.error("Could not change feed URL."); - } - }); - } - return false; - }, - publishedOPML: function() { + generatedFeed: function(feed, is_cat, search = "") { Notify.progress("Loading, please wait...", true); - xhrJson("backend.php", {op: "pref-feeds", method: "getOPMLKey"}, (reply) => { + 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: __("Public OPML URL"), - content: ` -
    ${__("Your Public OPML URL is:")}
    -
    - -
    -
    - - -
    - ` - }); - - 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"), + regenFeedKey: function(feed, is_cat) { + if (confirm(__("Generate new syndication address for this feed?"))) { + + Notify.progress("Trying to change address...", true); + + const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat}; + + xhr.json("backend.php", query, (reply) => { + const new_link = reply.link; + const target = this.domNode.querySelector(".generated_url"); + + if (new_link && target) { + target.innerHTML = target.innerHTML.replace(/&key=.*$/, + "&key=" + new_link); + + target.href = target.href.replace(/&key=.*$/, + "&key=" + new_link); + + Notify.close(); + + } else { + Notify.error("Could not change feed URL."); + } + }); + } + return false; + }, content: ` -
    ${__("%s can be accessed via the following secret URL:").replace("%s", feed_title)}
    +
    ${__("%s can be accessed via the following secret URL:").replace("%s", App.escapeHtml(reply.title))}
    - +
    + ` + }); + + const tmph = dojo.connect(test_dialog, "onShow", null, function (/* e */) { + dojo.disconnect(tmph); + + test_dialog.getTestResults(dialog.attr('value'), 0); + }); + + test_dialog.show(); + }, + insertRule: function(parentNode, replaceNode) { + const rule = dojo.formToJson("filter_new_rule_form"); + + xhr.post("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (reply) => { + try { + const li = document.createElement('li'); + li.addClassName("rule"); + + li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})} + ${reply} + ${App.FormFields.hidden_tag("rule[]", rule)}`; + + dojo.parser.parse(li); + + if (replaceNode) { + parentNode.replaceChild(li, replaceNode); } else { - console.log("getTestResults: dialog closed, bailing out."); + parentNode.appendChild(li); } } catch (e) { App.Error.report(e); } }); }, - content: ` -
    -   - Looking for articles... -
    + 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; + } -
      - -
      - ` + const action = dojo.formToJson(form); + + xhr.post("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (reply) => { + try { + const li = document.createElement('li'); + li.addClassName("action"); + + li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})} + ${reply} + ${App.FormFields.hidden_tag("action[]", action)}`; + + 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', + ` +
      + +
      + + +
      + +
      + +
      +
      + + ${App.FormFields.select_hash("filter_type", rule.filter_type, dialog.filter_info.filter_types)} + +
      +
      + + ${editrule.multiselect} + +
      +
      + +
      + ${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"))} +
      + +
      + `); + }); + + }); + + 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', + ` +
      +
      + ${App.FormFields.select_hash("action_id", -1, + dialog.filter_info.action_types, + {onchange: "App.dialogOf(this).toggleParam(this)"}, + "filterDlg_actionSelect")} + + + + ${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")} +
      +
      + ${App.FormFields.submit_tag(__("Save action"), {onclick: "App.dialogOf(this).execute()"})} + ${App.FormFields.cancel_dialog_tag(__("Cancel"))} +
      +
      + `); + + 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) { + Lists.select("filterDlg_Matches", select); + }, + selectActions: function (select) { + Lists.select("filterDlg_Actions", select); + }, + onRuleClicked: function (elem) { + + const li = elem.closest('li'); + const rule = li.querySelector('input[name="rule[]"]').value; + + this.editRule(li, rule); + }, + onActionClicked: function (elem) { + + const li = elem.closest('li'); + const action = li.querySelector('input[name="action[]"]').value; + + this.editAction(li, action); + }, + removeFilter: function () { + const msg = __("Remove filter?"); + + if (confirm(msg)) { + this.hide(); + + Notify.progress("Removing filter..."); + + const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id}; + + xhr.post("backend.php", query, () => { + const tree = dijit.byId("filterTree"); + + if (tree) tree.reload(); + }); + } + }, + addAction: function () { + this.editAction(); + }, + addRule: function () { + this.editRule(); + }, + deleteAction: function () { + App.findAll("#filterDlg_Actions li[class*=Selected]").forEach(function (e) { + e.parentNode.removeChild(e) + }); + }, + deleteRule: function () { + App.findAll("#filterDlg_Matches li[class*=Selected]").forEach(function (e) { + e.parentNode.removeChild(e) + }); + }, + execute: function () { + if (this.validate()) { + + Notify.progress("Saving data...", true); + + xhr.post("backend.php", this.attr('value'), () => { + dialog.hide(); + + const tree = dijit.byId("filterTree"); + if (tree) tree.reload(); + }); + } + }, + content: __("Loading, please wait...") }); - dojo.connect(dialog, "onShow", null, function (/* e */) { - dialog.getTestResults(params, 0); + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + xhr.json("backend.php", {op: "pref-filters", method: "edit", id: filter_id}, function (filter) { + + 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', + ` +
      + + ${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'))} + +
      + +
      + +
      +
      +
      +
      +
      + ${__("Select")} +
      + +
      ${__("All")}
      +
      ${__("None")}
      +
      +
      + + +
      +
      +
        + ${filter.rules.map((rule) => ` +
      • + ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})} + ${rule.name} + ${App.FormFields.hidden_tag("rule[]", JSON.stringify(rule))} +
      • + `).join("")} +
      +
      +
      +
      +
      +
      +
      +
      + ${__("Select")} +
      +
      ${__("All")}
      +
      ${__("None")}
      +
      +
      + + +
      +
      +
        + ${filter.actions.map((action) => ` +
      • + ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})} + ${App.escapeHtml(action.name)} + ${App.FormFields.hidden_tag("action[]", JSON.stringify(action))} +
      • + `).join("")} +
      +
      +
      +
      +
      + +
      + ${Object.keys(options).map((name) => + ` +
      + +
      + `).join("")} +
      + +
      + ${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"))} + `} +
      +
      + `); + + if (!App.isPrefs()) { + const selectedText = App.getSelectedText(); + + if (selectedText != "") { + const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) : + Feeds.getActive(); + const rule = {reg_exp: selectedText, feed_id: [feed_id], filter_type: 1}; + + dialog.editRule(null, dojo.toJson(rule)); + } else { + const query = {op: "article", method: "getmetadatabyid", id: Article.getActive()}; + + xhr.json("backend.php", query, (reply) => { + let title; + + if (reply && reply.title) title = reply.title; + + if (title || Feeds.getActive() || Feeds.activeIsCat()) { + console.log(title + " " + Feeds.getActive()); + + const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) : + Feeds.getActive(); + const rule = {reg_exp: title, feed_id: [feed_id], filter_type: 1}; + + dialog.editRule(null, dojo.toJson(rule)); + } + }); + } + } + }); }); dialog.show(); }, - edit: function(id) { // if no id, new filter dialog - let query; - - if (!App.isPrefs()) { - query = { - op: "pref-filters", method: "edit", - feed: Feeds.getActive(), is_cat: Feeds.activeIsCat() - }; - } else { - query = {op: "pref-filters", method: "edit", id: id}; - } - - console.log('Filters.edit', query); - - xhrPost("backend.php", query, function (transport) { - try { - const dialog = new fox.SingleUseDialog({ - id: "filterEditDlg", - title: __("Create Filter"), - test: function () { - Filters.test(this.attr('value')); - }, - selectRules: function (select) { - Lists.select("filterDlg_Matches", select); - }, - selectActions: function (select) { - Lists.select("filterDlg_Actions", select); - }, - editRule: function (e) { - const li = e.closest('li'); - const rule = li.querySelector('input[name="rule[]"]').value - - Filters.addFilterRule(li, rule); - }, - editAction: function (e) { - const li = e.closest('li'); - const action = li.querySelector('input[name="action[]"]').value - - Filters.addFilterAction(li, action); - }, - removeFilter: function () { - const msg = __("Remove filter?"); - - if (confirm(msg)) { - this.hide(); - - Notify.progress("Removing filter..."); - - const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id}; - - xhrPost("backend.php", query, () => { - const tree = dijit.byId("filterTree"); - - if (tree) tree.reload(); - }); - } - }, - addAction: function () { - Filters.addFilterAction(); - }, - addRule: function () { - Filters.addFilterRule(); - }, - deleteAction: function () { - $$("#filterDlg_Actions li[class*=Selected]").each(function (e) { - e.parentNode.removeChild(e) - }); - }, - deleteRule: function () { - $$("#filterDlg_Matches li[class*=Selected]").each(function (e) { - e.parentNode.removeChild(e) - }); - }, - execute: function () { - if (this.validate()) { - - Notify.progress("Saving data...", true); - - xhrPost("backend.php", this.attr('value'), () => { - dialog.hide(); - - const tree = dijit.byId("filterTree"); - if (tree) tree.reload(); - }); - } - }, - content: transport.responseText - }); - - if (!App.isPrefs()) { - /* global getSelectionText */ - const selectedText = getSelectionText(); - - const lh = dojo.connect(dialog, "onShow", function () { - dojo.disconnect(lh); - - if (selectedText != "") { - - const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) : - Feeds.getActive(); - - const rule = {reg_exp: selectedText, feed_id: [feed_id], filter_type: 1}; - - Filters.addFilterRule(null, dojo.toJson(rule)); - - } else { - - const query = {op: "rpc", method: "getlinktitlebyid", id: Article.getActive()}; - - xhrPost("backend.php", query, (transport) => { - const reply = JSON.parse(transport.responseText); - - let title = false; - - if (reply && reply.title) title = reply.title; - - if (title || Feeds.getActive() || Feeds.activeIsCat()) { - - console.log(title + " " + Feeds.getActive()); - - const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) : - Feeds.getActive(); - - const rule = {reg_exp: title, feed_id: [feed_id], filter_type: 1}; - - Filters.addFilterRule(null, dojo.toJson(rule)); - } - }); - } - }); - } - dialog.show(); - - } catch (e) { - App.Error.report(e); - } - }); - }, }; diff --git a/js/FeedTree.js b/js/FeedTree.js index 26c1c916c..17cd3deea 100755 --- a/js/FeedTree.js +++ b/js/FeedTree.js @@ -102,7 +102,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co label: __("Debug feed"), onClick: function() { /* 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}); }})); } @@ -286,7 +286,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co // focus headlines to route key events there setTimeout(() => { - $("headlines-frame").focus(); + App.byId("headlines-frame").focus(); if (treeNode) { const node = treeNode.rowNode; @@ -295,7 +295,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co if (node && tree) { // scroll tree to selection if needed if (node.offsetTop < tree.scrollTop || node.offsetTop > tree.scrollTop + tree.clientHeight) { - $("feedTree").scrollTop = node.offsetTop; + App.byId("feedTree").scrollTop = node.offsetTop; } } } diff --git a/js/Feeds.js b/js/Feeds.js index 986936285..5a2dee5cf 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -1,8 +1,9 @@ '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 = { + _default_feed_id: -3, counters_last_request: 0, _active_feed_id: undefined, _active_feed_is_cat: false, @@ -12,6 +13,19 @@ const Feeds = { _search_query: false, last_search_query: [], _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: [], // NOTE: this implementation is incomplete // for general objects but good enough for counters @@ -109,6 +123,9 @@ const Feeds = { } return false; // block unneeded form submits }, + openDefaultFeed: function() { + this.open({feed: this._default_feed_id}); + }, openNextUnread: function() { const is_cat = this.activeIsCat(); const nuf = this.getNextUnread(this.getActive(), is_cat); @@ -116,23 +133,20 @@ const Feeds = { }, toggle: function() { 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() { this._search_query = ""; this.reloadCurrent(); }, - requestCounters: function() { - xhrPost("backend.php", {op: "rpc", method: "getAllCounters", seq: App.next_seq()}, (transport) => { - App.handleRpcJson(transport); - }); + // null = get all data, [] would give empty response for specific type + requestCounters: function(feed_ids = null, label_ids = null) { + 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() { try { @@ -180,7 +194,7 @@ const Feeds = { dojo.disconnect(tmph); }); - $("feeds-holder").appendChild(tree.domNode); + App.byId("feeds-holder").appendChild(tree.domNode); const tmph2 = dojo.connect(tree, 'onLoad', function () { dojo.disconnect(tmph2); @@ -199,9 +213,23 @@ const Feeds = { 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() { console.log("in feedlist init"); + this._feeds_holder_observer.observe(App.byId("feeds-holder")); + App.setLoadingProgress(50); //document.onkeydown = (event) => { return App.hotkeyHandler(event) }; @@ -215,7 +243,7 @@ const Feeds = { if (hash_feed_id != undefined) { this.open({feed: hash_feed_id, is_cat: hash_feed_is_cat}); } else { - this.open({feed: -3}); + this.openDefaultFeed(); } this.hideOrShowFeeds(App.getInitParam("hide_read_feeds")); @@ -260,10 +288,10 @@ const Feeds = { // bw_limit disables timeout() so we request initial counters separately if (App.getInitParam("bw_limit")) { - this.requestCounters(true); + this.requestCounters(); } else { setTimeout(() => { - this.requestCounters(true); + this.requestCounters(); setInterval(() => { this.requestCounters(); }, 60 * 1000) }, 250); } @@ -284,8 +312,8 @@ const Feeds = { this._active_feed_id = id; this._active_feed_is_cat = is_cat; - $("headlines-frame").setAttribute("feed-id", id); - $("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0); + App.byId("headlines-frame").setAttribute("feed-id", id); + App.byId("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0); this.select(id, is_cat); @@ -299,7 +327,7 @@ const Feeds = { toggleUnread: function() { 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); App.setInitParam("hide_read_feeds", hide); }); @@ -310,14 +338,13 @@ const Feeds = { if (tree) return tree.hideRead(hide, App.getInitParam("hide_read_shows_special"));*/ - $$("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-feeds", !!hide); + App.findAll("body")[0].setAttribute("hide-read-shows-special", !!App.getInitParam("hide_read_shows_special")); }, open: function(params) { const feed = params.feed; const is_cat = !!params.is_cat || false; const offset = params.offset || 0; - const viewfeed_debug = params.viewfeed_debug; const append = params.append || false; const method = params.method; // this is used to quickly switch between feeds, sets active but xhr is on a timeout @@ -339,7 +366,7 @@ const Feeds = { }, 10 * 1000); } - Form.enable("toolbar-main"); + //Form.enable("toolbar-main"); let query = Object.assign({op: "feeds", method: "view", feed: feed}, dojo.formToObject("toolbar-main")); @@ -362,8 +389,6 @@ const Feeds = { query.m = "ForceUpdate"; } - Form.enable("toolbar-main"); - if (!delayed) if (!this.setExpando(feed, is_cat, (is_cat) ? 'images/indicator_tiny.gif' : 'images/indicator_white.gif')) @@ -373,20 +398,13 @@ const Feeds = { 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); this._viewfeed_wait_timeout = window.setTimeout(() => { - xhrPost("backend.php", query, (transport) => { + xhr.json("backend.php", query, (reply) => { try { window.clearTimeout(this._infscroll_timeout); 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]); } catch (e) { App.Error.report(e); @@ -401,8 +419,7 @@ const Feeds = { Notify.progress("Marking all feeds as read..."); - xhrPost("backend.php", {op: "feeds", method: "catchupAll"}, () => { - this.requestCounters(true); + xhr.json("backend.php", {op: "feeds", method: "catchupAll"}, () => { this.reloadCurrent(); }); @@ -447,9 +464,7 @@ const Feeds = { Notify.progress("Loading, please wait...", true); - xhrPost("backend.php", catchup_query, (transport) => { - App.handleRpcJson(transport); - + xhr.json("backend.php", catchup_query, () => { 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) @@ -476,9 +491,9 @@ const Feeds = { 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"); }) } @@ -501,7 +516,7 @@ const Feeds = { const tree = dijit.byId("feedTree"); if (tree && tree.model) - return tree.getFeedCategory(feed); + return tree._cat_of_feed(feed); } catch (e) { // @@ -566,14 +581,42 @@ const Feeds = { return tree.model.store.getValue(nuf, 'bare_id'); }, search: function() { - xhrPost("backend.php", - {op: "feeds", method: "search", - param: Feeds.getActive() + ":" + Feeds.activeIsCat()}, - (transport) => { + xhr.json("backend.php", + {op: "feeds", method: "search"}, + (reply) => { try { const dialog = new fox.SingleUseDialog({ - id: "searchDlg", - content: transport.responseText, + content: ` +
      +
      +
      + +
      + + ${reply.show_language ? + ` +
      + + ${App.FormFields.select_tag("search_language", reply.default_language, reply.all_languages, + {title: __('Used for word stemming')}, "search_language")} +
      + ` : ''} +
      + +
      + ${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'))} +
      +
      + `, title: __("Search"), execute: function () { if (this.validate()) { @@ -613,8 +656,13 @@ const Feeds = { updateRandom: function() { console.log("in update_random_feed"); - xhrPost("backend.php", {op: "rpc", method: "updaterandomfeed"}, (transport) => { - App.handleRpcJson(transport, true); + xhr.json("backend.php", {op: "rpc", method: "updaterandomfeed"}, () => { + // }); }, + renderIcon: function(feed_id, exists) { + return feed_id && exists ? + `` : + `rss_feed`; + } }; diff --git a/js/Headlines.js b/js/Headlines.js index ea4c81a6a..6dbe24918 100755 --- a/js/Headlines.js +++ b/js/Headlines.js @@ -1,13 +1,13 @@ 'use strict'; /* global __, ngettext, Article, App */ -/* global xhrPost, dojo, dijit, PluginHost, Notify, $$, Feeds */ +/* global dojo, dijit, PluginHost, Notify, xhr, Feeds */ /* global CommonDialogs */ const Headlines = { vgroup_last_feed: undefined, _headlines_scroll_timeout: 0, - _observer_counters_timeout: 0, + //_observer_counters_timeout: 0, headlines: [], current_first_id: 0, _scroll_reset_timeout: false, @@ -44,7 +44,7 @@ const Headlines = { row_observer: new MutationObserver((mutations) => { const modified = []; - mutations.each((m) => { + mutations.forEach((m) => { if (m.type == 'attributes' && ['class', 'data-score'].indexOf(m.attributeName) != -1) { const row = m.target; @@ -54,7 +54,7 @@ const Headlines = { const hl = Headlines.headlines[id]; if (hl) { - const hl_old = Object.extend({}, hl); + const hl_old = {...{}, ...hl}; hl.unread = row.hasClassName("Unread"); hl.marked = row.hasClassName("marked"); @@ -94,7 +94,7 @@ const Headlines = { rescore: {}, }; - modified.each(function (m) { + modified.forEach(function (m) { if (m.old.marked != m.new.marked) ops.tmark.push(m.id); @@ -118,29 +118,29 @@ const Headlines = { } }); - ops.select.each((row) => { - const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]); + ops.select.forEach((row) => { + const cb = dijit.getEnclosingWidget(row.querySelector(".rchk")); if (cb) cb.attr('checked', true); }); - ops.deselect.each((row) => { - const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]); + ops.deselect.forEach((row) => { + const cb = dijit.getEnclosingWidget(row.querySelector(".rchk")); if (cb && !row.hasClassName("active")) cb.attr('checked', false); }); - ops.activate.each((row) => { - const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]); + ops.activate.forEach((row) => { + const cb = dijit.getEnclosingWidget(row.querySelector(".rchk")); if (cb) cb.attr('checked', true); }); - ops.deactivate.each((row) => { - const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]); + ops.deactivate.forEach((row) => { + const cb = dijit.getEnclosingWidget(row.querySelector(".rchk")); if (cb && !row.hasClassName("Selected")) cb.attr('checked', false); @@ -149,39 +149,56 @@ const Headlines = { const promises = []; if (ops.tmark.length != 0) - promises.push(xhrPost("backend.php", - {op: "rpc", method: "markSelected", ids: ops.tmark.toString(), cmode: 2})); + promises.push(xhr.post("backend.php", + {op: "rpc", method: "markSelected", "ids[]": ops.tmark, cmode: 2})); if (ops.tpub.length != 0) - promises.push(xhrPost("backend.php", - {op: "rpc", method: "publishSelected", ids: ops.tpub.toString(), cmode: 2})); + promises.push(xhr.post("backend.php", + {op: "rpc", method: "publishSelected", "ids[]": ops.tpub, cmode: 2})); if (ops.read.length != 0) - promises.push(xhrPost("backend.php", - {op: "rpc", method: "catchupSelected", ids: ops.read.toString(), cmode: 0})); + promises.push(xhr.post("backend.php", + {op: "rpc", method: "catchupSelected", "ids[]": ops.read, cmode: 0})); if (ops.unread.length != 0) - promises.push(xhrPost("backend.php", - {op: "rpc", method: "catchupSelected", ids: ops.unread.toString(), cmode: 1})); + promises.push(xhr.post("backend.php", + {op: "rpc", method: "catchupSelected", "ids[]": ops.unread, cmode: 1})); const scores = Object.keys(ops.rescore); if (scores.length != 0) { - scores.each((score) => { - promises.push(xhrPost("backend.php", - {op: "article", method: "setScore", id: ops.rescore[score].toString(), score: score})); + scores.forEach((score) => { + promises.push(xhr.post("backend.php", + {op: "article", method: "setScore", "ids[]": ops.rescore[score].toString(), score: score})); }); } - if (promises.length > 0) - Promise.all([promises]).then(() => { - window.clearTimeout(this._observer_counters_timeout); + Promise.all(promises).then((results) => { + let feeds = []; + let labels = []; - this._observer_counters_timeout = setTimeout(() => { - Feeds.requestCounters(true); - }, 1000); + results.forEach((res) => { + if (res) { + 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) { in_body = in_body || false; @@ -211,7 +228,7 @@ const Headlines = { 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); @@ -222,10 +239,10 @@ const Headlines = { Headlines.toggleUnread(id, 0); } 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 - $("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B; + App.byId("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B; Article.cdmMoveToId(id); } @@ -252,7 +269,7 @@ const Headlines = { return false; }, initScrollHandler: function () { - $("headlines-frame").onscroll = (event) => { + App.byId("headlines-frame").onscroll = (event) => { clearTimeout(this._headlines_scroll_timeout); this._headlines_scroll_timeout = window.setTimeout(function () { //console.log('done scrolling', event); @@ -262,8 +279,8 @@ const Headlines = { }, loadMore: function () { const view_mode = document.forms["toolbar-main"].view_mode.value; - const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length; - const num_all = $$("#headlines-frame > div[id*=RROW]").length; + const unread_in_buffer = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread]").length; + const num_all = App.findAll("#headlines-frame > div[id*=RROW]").length; const num_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat()); // TODO implement marked & published @@ -289,10 +306,10 @@ const Headlines = { Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), offset: offset, append: true}); }, isChildVisible: function (elem) { - return App.Scrollable.isChildVisible(elem, $("headlines-frame")); + return App.Scrollable.isChildVisible(elem, App.byId("headlines-frame")); }, 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++) { const row = rows[i]; @@ -303,7 +320,7 @@ const Headlines = { } }, 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++) { if (App.Scrollable.isChildVisible(rows[i], container)) { @@ -315,8 +332,8 @@ const Headlines = { scrollHandler: function (/*event*/) { try { if (!Feeds.infscroll_disabled && !Feeds.infscroll_in_progress) { - const hsp = $("headlines-spacer"); - const container = $("headlines-frame"); + const hsp = App.byId("headlines-spacer"); + const container = App.byId("headlines-frame"); if (hsp && hsp.previousSibling) { const last_row = hsp.previousSibling; @@ -333,7 +350,7 @@ const Headlines = { } 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 */ if (container.scrollTop > 0) @@ -342,12 +359,12 @@ const Headlines = { 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++) { 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"); } else { break; @@ -362,23 +379,23 @@ const Headlines = { return this.headlines[id]; }, setCommonClasses: function () { - $("headlines-frame").removeClassName("cdm"); - $("headlines-frame").removeClassName("normal"); + App.byId("headlines-frame").removeClassName("cdm"); + 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 - $("main").removeClassName("expandable"); - $("main").removeClassName("expanded"); + App.byId("main").removeClassName("expandable"); + App.byId("main").removeClassName("expanded"); if (App.isCombinedMode()) - $("main").addClassName(App.getInitParam("cdm_expanded") ? " expanded" : " expandable"); + App.byId("main").addClassName(App.getInitParam("cdm_expanded") ? "expanded" : "expandable"); }, renderAgain: function () { // TODO: wrap headline elements into a knockoutjs model to prevent all this stuff 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 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) }); 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) }); @@ -423,7 +440,7 @@ const Headlines = { if (headlines.vfeed_group_enabled && hl.feed_title && this.vgroup_last_feed != hl.feed_id) { const vgrhdr = `
      -
      ${hl.feed_icon}
      +
      ${Feeds.renderIcon(hl.feed_id, hl.has_icon)}
      ${hl.feed_title} done_all
      ` @@ -431,7 +448,7 @@ const Headlines = { const tmp = document.createElement("div"); tmp.innerHTML = vgrhdr; - $("headlines-frame").appendChild(tmp.firstChild); + App.byId("headlines-frame").appendChild(tmp.firstChild); this.vgroup_last_feed = hl.feed_id; } @@ -462,7 +479,7 @@ const Headlines = { ${hl.title} ${hl.author} - ${hl.labels} + ${Article.renderLabels(hl.id, hl.labels)} ${hl.cdm_excerpt ? hl.cdm_excerpt : ""} @@ -477,25 +494,26 @@ const Headlines = { ${Article.getScorePic(hl.score)} - ${hl.feed_icon} + ${Feeds.renderIcon(hl.feed_id, hl.has_icon)} +
      -
      ${hl.note}
      + ${Article.renderNote(hl.id, hl.note)}
      - ${hl.enclosures} + ${Article.renderEnclosures(hl.enclosures)}
      ${Article.getScorePic(hl.score)} - ${hl.feed_icon} + ${Feeds.renderIcon(hl.feed_id, hl.has_icon)}
      `; @@ -555,20 +573,74 @@ const Headlines = { return tmp.firstChild; }, updateCurrentUnread: function () { - if ($("feed_current_unread")) { + if (App.byId("feed_current_unread")) { const feed_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat()); 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"); } else { Element.hide("feed_current_unread"); } } }, - onLoaded: function (transport, offset, append) { - const reply = App.handleRpcJson(transport); + renderToolbar: function(headlines) { + 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', + ` + + + rss_feed + + ${tb.site_url ? + `${tb.title}` : + `${tb.title}`} + ${search_query ? + ` + (${__("Cancel search")}) + ` : ''} + ${tb.error ? `error` : ''} + + + + +
      + ${__("Select...")} +
      +
      ${__('All')}
      +
      ${__('Unread')}
      +
      ${__('Invert')}
      +
      ${__('None')}
      +
      +
      ${__('Toggle unread')}
      +
      ${__('Toggle starred')}
      +
      ${__('Toggle published')}
      +
      +
      ${__('Mark as read')}
      +
      ${__('Set score')}
      + ${tb.plugin_menu_items} + ${headlines.id === 0 && !headlines.is_cat ? + ` +
      +
      ${__('Delete permanently')}
      + ` : ''} +
      + ${tb.plugin_buttons} + + `); + } else { + target.attr('innerHTML', ''); + } + + dojo.parser.parse(target.domNode); + }, + onLoaded: function (reply, offset, append) { console.log("Headlines.onLoaded: offset=", offset, "append=", append); let is_cat = false; @@ -597,15 +669,15 @@ const Headlines = { // also called in renderAgain() after view mode switch Headlines.setCommonClasses(); - $("headlines-frame").setAttribute("is-vfeed", + App.byId("headlines-frame").setAttribute("is-vfeed", reply['headlines']['is_vfeed'] ? 1 : 0); Article.setActive(0); try { - $("headlines-frame").removeClassName("smooth-scroll"); - $("headlines-frame").scrollTop = 0; - $("headlines-frame").addClassName("smooth-scroll"); + App.byId("headlines-frame").removeClassName("smooth-scroll"); + App.byId("headlines-frame").scrollTop = 0; + App.byId("headlines-frame").addClassName("smooth-scroll"); } catch (e) { console.warn(e); } @@ -613,25 +685,27 @@ const Headlines = { this.headlines = []; this.vgroup_last_feed = undefined; - dojo.html.set($("toolbar-headlines"), + /*dojo.html.set(App.byId("toolbar-headlines"), reply['headlines']['toolbar'], - {parseContent: true}); + {parseContent: true});*/ + + Headlines.renderToolbar(reply['headlines']); if (typeof reply['headlines']['content'] == 'string') { - $("headlines-frame").innerHTML = reply['headlines']['content']; + App.byId("headlines-frame").innerHTML = reply['headlines']['content']; } else { - $("headlines-frame").innerHTML = ''; + App.byId("headlines-frame").innerHTML = ''; for (let i = 0; i < reply['headlines']['content'].length; 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; } } - let hsp = $("headlines-spacer"); + let hsp = App.byId("headlines-spacer"); if (!hsp) { hsp = document.createElement("div"); @@ -646,18 +720,19 @@ const Headlines = { hsp.innerHTML = "" + __("Click to open next unread feed.") + ""; + /* if (Feeds._search_query) { - $("feed_title").innerHTML += "" + + App.byId("feed_title").innerHTML += "" + " (" + __("Cancel search") + ")" + ""; - } + } */ Headlines.updateCurrentUnread(); } else if (headlines_count > 0 && feed_id == Feeds.getActive() && is_cat == Feeds.activeIsCat()) { const c = dijit.byId("headlines-frame"); - let hsp = $("headlines-spacer"); + let hsp = App.byId("headlines-spacer"); if (hsp) c.domNode.removeChild(hsp); @@ -665,13 +740,13 @@ const Headlines = { let headlines_appended = 0; if (typeof reply['headlines']['content'] == 'string') { - $("headlines-frame").innerHTML = reply['headlines']['content']; + App.byId("headlines-frame").innerHTML = reply['headlines']['content']; } else { for (let i = 0; i < reply['headlines']['content'].length; i++) { const hl = reply['headlines']['content'][i]; 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; ++headlines_appended; @@ -703,7 +778,7 @@ const Headlines = { 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 (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) }); 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) }); } else { - console.error("Invalid object received: " + transport.responseText); dijit.byId("headlines-frame").attr('content', "
      " + __('Could not update headlines (invalid object received - see error console for details)') + "
      "); @@ -755,9 +829,7 @@ const Headlines = { Feeds.reloadCurrent(); }, - selectionToggleUnread: function (params) { - params = params || {}; - + selectionToggleUnread: function (params = {}) { const cmode = params.cmode != undefined ? params.cmode : 2; const no_error = params.no_error || false; const ids = params.ids || Headlines.getSelected(); @@ -769,8 +841,8 @@ const Headlines = { return; } - ids.each((id) => { - const row = $("RROW-" + id); + ids.forEach((id) => { + const row = App.byId(`RROW-${id}`); if (row) { switch (cmode) { @@ -794,7 +866,7 @@ const Headlines = { return; } - ids.each((id) => { + ids.forEach((id) => { this.toggleMark(id); }); }, @@ -806,26 +878,24 @@ const Headlines = { return; } - ids.each((id) => { + ids.forEach((id) => { this.togglePub(id); }); }, toggleMark: function (id) { - const row = $("RROW-" + id); + const row = App.byId(`RROW-${id}`); if (row) row.toggleClassName("marked"); }, togglePub: function (id) { - const row = $("RROW-" + id); + const row = App.byId(`RROW-${id}`); if (row) row.toggleClassName("published"); }, - move: function (mode, params) { - params = params || {}; - + move: function (mode, params = {}) { const no_expand = params.no_expand || false; const force_previous = params.force_previous || this.default_force_previous; const force_to_top = params.force_to_top || this.default_force_to_top; @@ -834,7 +904,7 @@ const Headlines = { let next_id = false; 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...'); current_id = Headlines.firstVisible(); prev_id = current_id; @@ -871,13 +941,30 @@ const Headlines = { } else { 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") { if (prev_id || current_id) { if (App.isCombinedMode()) { window.requestAnimationFrame(() => { - const row = $("RROW-" + current_id); - const ctr = $("headlines-frame"); + const row = App.byId(`RROW-${current_id}`); + const ctr = App.byId("headlines-frame"); const delta_px = Math.round(row.offsetTop) - Math.round(ctr.scrollTop); console.log('moving back, delta_px', delta_px); @@ -898,7 +985,7 @@ const Headlines = { }, updateSelectedPrompt: function () { const count = Headlines.getSelected().length; - const elem = $("selected_prompt"); + const elem = App.byId("selected_prompt"); if (elem) { elem.innerHTML = ngettext("%d article selected", @@ -908,7 +995,7 @@ const Headlines = { } }, toggleUnread: function (id, cmode) { - const row = $("RROW-" + id); + const row = App.byId(`RROW-${id}`); if (row) { if (typeof cmode == "undefined") cmode = 2; @@ -939,9 +1026,8 @@ const Headlines = { ids: ids.toString(), lid: id }; - xhrPost("backend.php", query, (transport) => { - App.handleRpcJson(transport); - this.onLabelsUpdated(transport); + xhr.json("backend.php", query, (reply) => { + this.onLabelsUpdated(reply); }); }, selectionAssignLabel: function (id, ids) { @@ -957,9 +1043,8 @@ const Headlines = { ids: ids.toString(), lid: id }; - xhrPost("backend.php", query, (transport) => { - App.handleRpcJson(transport); - this.onLabelsUpdated(transport); + xhr.json("backend.php", query, (reply) => { + this.onLabelsUpdated(reply); }); }, deleteSelection: function () { @@ -988,15 +1073,14 @@ const Headlines = { const query = {op: "rpc", method: "delete", ids: rows.toString()}; - xhrPost("backend.php", query, (transport) => { - App.handleRpcJson(transport); + xhr.json("backend.php", query, () => { Feeds.reloadCurrent(); }); }, getSelected: function () { const rv = []; - $$("#headlines-frame > div[id*=RROW][class*=Selected]").each( + App.findAll("#headlines-frame > div[id*=RROW][class*=Selected]").forEach( function (child) { rv.push(child.getAttribute("data-article-id")); }); @@ -1010,9 +1094,9 @@ const Headlines = { getLoaded: function () { 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)) { rv.push(child.getAttribute("data-article-id")); } @@ -1021,7 +1105,7 @@ const Headlines = { return rv; }, 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 if (row.hasClassName("active")) { @@ -1039,7 +1123,7 @@ const Headlines = { if (start == stop) return [start]; - const rows = $$("#headlines-frame > div[id*=RROW]"); + const rows = App.findAll("#headlines-frame > div[id*=RROW]"); const results = []; let collecting = false; @@ -1066,7 +1150,7 @@ const Headlines = { // mode = all,none,unread,invert,marked,published let query = "#headlines-frame > div[id*=RROW]"; - if (articleId) query += "[data-article-id=" + articleId + "]"; + if (articleId) query += `[data-article-id="${articleId}"]`; switch (mode) { case "none": @@ -1086,10 +1170,7 @@ const Headlines = { console.warn("select: unknown mode", mode); } - const rows = $$(query); - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; + App.findAll(query).forEach((row) => { switch (mode) { case "none": @@ -1101,7 +1182,7 @@ const Headlines = { default: row.addClassName("Selected"); } - } + }); }, catchupSelection: function () { const rows = Headlines.getSelected(); @@ -1140,7 +1221,7 @@ const Headlines = { if (!below) { for (let i = 0; i < visible_ids.length; i++) { if (visible_ids[i] != id) { - const e = $("RROW-" + visible_ids[i]); + const e = App.byId(`RROW-${visible_ids[i]}`); if (e && e.hasClassName("Unread")) { ids_to_mark.push(visible_ids[i]); @@ -1152,7 +1233,7 @@ const Headlines = { } else { for (let i = visible_ids.length - 1; i >= 0; i--) { if (visible_ids[i] != id) { - const e = $("RROW-" + visible_ids[i]); + const e = App.byId(`RROW-${visible_ids[i]}`); if (e && e.hasClassName("Unread")) { ids_to_mark.push(visible_ids[i]); @@ -1171,26 +1252,40 @@ const Headlines = { if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) { 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"); } } } }, - onLabelsUpdated: function (transport) { - const data = JSON.parse(transport.responseText); - + onTagsUpdated: function (data) { if (data) { - data['info-for-headlines'].each(function (elem) { - $$(".HLLCTR-" + elem.id).each(function (ctr) { - ctr.innerHTML = elem.labels; + if (this.headlines[data.id]) { + this.headlines[data.id].tags = data.tags; + } + + 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) { - const container = $("headlines-frame"); - const row = $("RROW-" + id); + const container = App.byId("headlines-frame"); + const row = App.byId(`RROW-${id}`); if (!container || !row) return; @@ -1289,7 +1384,7 @@ const Headlines = { const labelAddMenu = 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 name = label.caption; @@ -1337,10 +1432,10 @@ const Headlines = { } }, scrollByPages: function (page_offset) { - App.Scrollable.scrollByPages($("headlines-frame"), page_offset); + App.Scrollable.scrollByPages(App.byId("headlines-frame"), page_offset); }, scroll: function (offset) { - App.Scrollable.scroll($("headlines-frame"), offset); + App.Scrollable.scroll(App.byId("headlines-frame"), offset); }, initHeadlinesMenu: function () { if (!dijit.byId("headlinesMenu")) { diff --git a/js/PrefFeedTree.js b/js/PrefFeedTree.js index 89195e616..bb5d25e67 100644 --- a/js/PrefFeedTree.js +++ b/js/PrefFeedTree.js @@ -1,9 +1,44 @@ /* 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, { + // 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) { 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"; }, reload: function() { - const searchElem = $("feed_search"); + const searchElem = App.byId("feed_search"); const search = (searchElem) ? searchElem.value : ""; - xhrPost("backend.php", { op: "pref-feeds", search: search }, (transport) => { - dijit.byId('feedsTab').attr('content', transport.responseText); + xhr.post("backend.php", { op: "pref-feeds", search: search }, (reply) => { + dijit.byId('feedsTab').attr('content', reply); Notify.close(); }); }, @@ -129,14 +164,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio resetFeedOrder: function() { Notify.progress("Loading, please wait..."); - xhrPost("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => { + xhr.post("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => { this.reload(); }); }, resetCatOrder: function() { Notify.progress("Loading, please wait..."); - xhrPost("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => { + xhr.post("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => { 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))) { 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(); this.reload(); }); @@ -163,7 +198,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio ids: sel_rows.toString() }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { this.reload(); }); } @@ -174,9 +209,16 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio 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() { - xhrPost("backend.php", {op: "pref-feeds", method: "getinactivefeeds"}, (transport) => { - if (parseInt(transport.responseText) > 0) { + xhr.json("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (reply) => { + if (reply.length > 0) { 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 rv = []; - items.each(function (item) { + items.forEach(function (item) { if (item.id[0].match("CAT:")) 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() }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { this.reload(); }); } @@ -220,7 +262,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio const items = tree.model.getCheckedItems(); const rv = []; - items.each(function (item) { + items.forEach(function (item) { if (item.id[0].match("FEED:")) 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..."); - 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(); try { const dialog = new fox.SingleUseDialog({ - id: "feedEditDlg", title: __("Edit Multiple Feeds"), - getChildByName: function (name) { + /*getChildByName: function (name) { let rv = null; - this.getChildren().each( + this.getChildren().forEach( function (child) { if (child.name == name) { rv = child; @@ -270,16 +311,22 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio } }); return rv; - }, - toggleField: function (checkbox, elem, label) { - this.getChildByName(elem).attr('disabled', !checkbox.checked); + },*/ + toggleField: function (checkbox) { + const name = checkbox.attr("data-control-for"); + const target = dijit.getEnclosingWidget(dialog.domNode.querySelector(`input[name="${name}"]`)); - if ($(label)) - if (checkbox.checked) - $(label).removeClassName('text-muted'); + target.attr('disabled', !checkbox.attr('checked')); + console.log(target, target.attr('type')); + + if (target.attr('type') == "checkbox") { + const label = checkbox.domNode.closest("label"); + + if (checkbox.attr('checked')) + label.removeClassName('text-muted'); else - $(label).addClassName('text-muted'); - + label.addClassName('text-muted'); + } }, execute: function () { 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 */ - Object.keys(query).each((key) => { + Object.keys(query).forEach((key) => { const val = query[key]; 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); - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { dialog.hide(); 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(); @@ -325,7 +372,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio 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(); }); } @@ -336,63 +383,22 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio if (title) { 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(); this.reload(); }); } }, batchSubscribe: function() { - const dialog = new fox.SingleUseDialog({ - id: "batchSubDlg", - title: __("Batch subscribe"), - execute: function () { - if (this.validate()) { - Notify.progress(__("Subscribing to feeds..."), true); + xhr.json("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (reply) => { + const dialog = new fox.SingleUseDialog({ + id: "batchSubDlg", + title: __("Batch subscribe"), + execute: function () { + if (this.validate()) { + Notify.progress(__("Subscribing to feeds..."), true); - xhrPost("backend.php", this.attr('value'), () => { - Notify.close(); - - const tree = dijit.byId("feedTree"); - if (tree) tree.reload(); - - dialog.hide(); - }); - } - }, - content: __("Loading, please wait...") - }); - - const tmph = dojo.connect(dialog, 'onShow', function () { - dojo.disconnect(tmph); - - xhrPost("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (transport) => { - dialog.attr('content', transport.responseText); - }) - }); - - dialog.show(); - }, - showInactiveFeeds: function() { - const dialog = new fox.SingleUseDialog({ - id: "inactiveFeedsDlg", - title: __("Feeds without recent updates"), - getSelectedFeeds: function () { - return Tables.getSelected("inactive-feeds-list"); - }, - removeSelected: function () { - const sel_rows = this.getSelectedFeeds(); - - if (sel_rows.length > 0) { - if (confirm(__("Remove selected feeds?"))) { - Notify.progress("Removing selected feeds...", true); - - const query = { - op: "pref-feeds", method: "remove", - ids: sel_rows.toString() - }; - - xhrPost("backend.php", query, () => { + xhr.post("backend.php", this.attr('value'), () => { Notify.close(); const tree = dijit.byId("feedTree"); @@ -401,23 +407,143 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio dialog.hide(); }); } + }, + content: ` +
      + ${App.FormFields.hidden_tag("op", "pref-feeds")} + ${App.FormFields.hidden_tag("method", "batchaddfeeds")} + +
      + ${__("One valid feed per line (no detection is done)")} +
      + +
      + + + ${reply.enable_cats ? + `
      + + ${reply.cat_select} +
      + ` : '' + } +
      + + + +
      + +
      + +
      + +
      + + +
      + + ` + }); + + dialog.show(); + + }); + }, + showInactiveFeeds: function() { + xhr.json("backend.php", {op: 'pref-feeds', method: 'inactivefeeds'}, function (reply) { + + const dialog = new fox.SingleUseDialog({ + id: "inactiveFeedsDlg", + title: __("Feeds without recent updates"), + getSelectedFeeds: function () { + return Tables.getSelected("inactive-feeds-list"); + }, + removeSelected: function () { + const sel_rows = this.getSelectedFeeds(); + + if (sel_rows.length > 0) { + if (confirm(__("Remove selected feeds?"))) { + Notify.progress("Removing selected feeds...", true); + + const query = { + op: "pref-feeds", method: "remove", + ids: sel_rows.toString() + }; + + xhr.post("backend.php", query, () => { + Notify.close(); + + const tree = dijit.byId("feedTree"); + if (tree) tree.reload(); + + dialog.hide(); + }); + } + + } else { + alert(__("No feeds selected.")); + } + }, + content: ` +
      +
      + ${__('Select')} +
      +
      ${__('All')}
      +
      ${__('None')}
      +
      +
      +
      + +
      + + ${reply.map((row) => ` + + + + + `).join("")} +
      + + + + ${App.escapeHtml(row.title)} + + + ${row.last_article} +
      +
      + +
      + + +
      + ` + }); + + dialog.show(); - } else { - alert(__("No feeds selected.")); - } - }, - content: __("Loading, please wait...") }); - const tmph = dojo.connect(dialog, 'onShow', function () { - dojo.disconnect(tmph); - - xhrPost("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (transport) => { - dialog.attr('content', transport.responseText); - }) - }); - - dialog.show(); } }); }); diff --git a/js/PrefFilterTree.js b/js/PrefFilterTree.js index abfdbb3b0..fff58ff1a 100644 --- a/js/PrefFilterTree.js +++ b/js/PrefFilterTree.js @@ -1,5 +1,5 @@ /* 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) { @@ -80,26 +80,26 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio const items = tree.model.getCheckedItems(); const rv = []; - items.each(function (item) { + items.forEach(function (item) { rv.push(tree.model.store.getValue(item, 'bare_id')); }); return rv; }, reload: function() { - const user_search = $("filter_search"); + const user_search = App.byId("filter_search"); let search = ""; if (user_search) { search = user_search.value; } - xhrPost("backend.php", { op: "pref-filters", search: search }, (transport) => { - dijit.byId('filtersTab').attr('content', transport.responseText); + xhr.post("backend.php", { op: "pref-filters", search: search }, (reply) => { + dijit.byId('filtersTab').attr('content', reply); Notify.close(); }); }, resetFilterOrder: function() { Notify.progress("Loading, please wait..."); - xhrPost("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => { + xhr.post("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => { this.reload(); }); }, @@ -114,28 +114,11 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio if (confirm(__("Combine selected 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(); }); } }, - 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() { const sel_rows = this.getSelectedFilters(); @@ -148,7 +131,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio ids: sel_rows.toString() }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { this.reload(); }); } diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js index 5bb76d179..62f6d91b1 100644 --- a/js/PrefHelpers.js +++ b/js/PrefHelpers.js @@ -1,7 +1,7 @@ 'use strict'; /* 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 = { AppPasswords: { @@ -9,7 +9,7 @@ const Helpers = { return Tables.getSelected("app-password-list"); }, updateContent: function(data) { - $("app_passwords_holder").innerHTML = data; + App.byId("app_passwords_holder").innerHTML = data; dojo.parser.parse("app_passwords_holder"); }, removeSelected: function() { @@ -19,8 +19,8 @@ const Helpers = { alert("No passwords selected."); } else if (confirm(__("Remove selected app passwords?"))) { - xhrPost("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (transport) => { - this.updateContent(transport.responseText); + xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (reply) => { + this.updateContent(reply); Notify.close(); }); @@ -31,8 +31,8 @@ const Helpers = { const title = prompt("Password description:") if (title) { - xhrPost("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (transport) => { - this.updateContent(transport.responseText); + xhr.post("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (reply) => { + this.updateContent(reply); Notify.close(); }); @@ -40,16 +40,21 @@ const Helpers = { } }, }, - clearFeedAccessKeys: function() { - if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) { - Notify.progress("Clearing URLs..."); + Feeds: { + clearFeedAccessKeys: function() { + if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) { + Notify.progress("Clearing URLs..."); - xhrPost("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => { - Notify.info("Generated URLs cleared."); - }); - } + xhr.post("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => { + Notify.info("Generated URLs cleared."); + }); + } - return false; + return false; + }, + }, + System: { + // }, EventLog: { log_page: 0, @@ -58,8 +63,13 @@ const Helpers = { this.update(); }, update: function() { - xhrPost("backend.php", { op: "pref-system", severity: dijit.byId("severity").attr('value'), page: Helpers.EventLog.log_page }, (transport) => { - dijit.byId('systemTab').attr('content', transport.responseText); + xhr.post("backend.php", { + op: "pref-system", + severity: dijit.byId("severity").attr('value'), + page: Helpers.EventLog.log_page + }, (reply) => { + + dijit.byId('systemTab').attr('content', reply); Notify.close(); }); }, @@ -77,161 +87,216 @@ const Helpers = { 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(); }); } }, }, - editProfiles: function() { - const dialog = new fox.SingleUseDialog({ - id: "profileEditDlg", - title: __("Settings Profiles"), - getSelectedProfiles: function () { - return Tables.getSelected("pref-profiles-list"); - }, - removeSelected: function () { - const sel_rows = this.getSelectedProfiles(); + Profiles: { + edit: function() { + const dialog = new fox.SingleUseDialog({ + id: "profileEditDlg", + title: __("Settings Profiles"), + getSelectedProfiles: function () { + return Tables.getSelected("pref-profiles-list"); + }, + removeSelected: function () { + const sel_rows = this.getSelectedProfiles(); - if (sel_rows.length > 0) { - if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) { - Notify.progress("Removing selected profiles...", true); + if (sel_rows.length > 0) { + if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) { + Notify.progress("Removing selected profiles...", true); - const query = { - op: "rpc", method: "remprofiles", - ids: sel_rows.toString() - }; + const query = { + op: "pref-prefs", method: "remprofiles", + ids: sel_rows.toString() + }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { + Notify.close(); + dialog.refresh(); + }); + } + + } else { + alert(__("No profiles selected.")); + } + }, + addProfile: function () { + if (this.validate()) { + Notify.progress("Creating profile...", true); + + const query = {op: "pref-prefs", method: "addprofile", title: dialog.attr('value').newprofile}; + + xhr.post("backend.php", query, () => { Notify.close(); dialog.refresh(); }); + } + }, + refresh: function() { + xhr.json("backend.php", {op: 'pref-prefs', method: 'getprofiles'}, (reply) => { + dialog.attr('content', ` +
      +
      + ${__('Select')} +
      +
      ${__('All')}
      +
      ${__('None')}
      +
      +
      - } else { - alert(__("No profiles selected.")); - } - }, - addProfile: function () { - if (this.validate()) { - Notify.progress("Creating profile...", true); +
      + + ${App.FormFields.button_tag(__('Create profile'), "", {onclick: 'App.dialogOf(this).addProfile()'})} +
      +
      - const query = {op: "rpc", method: "addprofile", title: dialog.attr('value').newprofile}; +
      +
      + + ${reply.map((profile) => ` + + + + + `).join("")} +
      + ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Tables.onRowChecked(this)'})} + + ${profile.id > 0 ? + `${profile.title} + + ` : `${profile.title}`} + ${profile.active ? __("(active)") : ""} +
      +
      - xhrPost("backend.php", query, () => { - Notify.close(); - dialog.refresh(); - }); - - } - }, - refresh: function() { - xhrPost("backend.php", {op: 'pref-prefs', method: 'editPrefProfiles'}, (transport) => { - dialog.attr('content', transport.responseText); - }); - }, - execute: function () { - const sel_rows = this.getSelectedProfiles(); - - if (sel_rows.length == 1) { - if (confirm(__("Activate selected profile?"))) { - Notify.progress("Loading, please wait..."); - - xhrPost("backend.php", {op: "rpc", method: "setprofile", id: sel_rows.toString()}, () => { - window.location.reload(); - }); - } - - } else { - alert(__("Please choose a profile to activate.")); - } - }, - content: "" - }); - - dialog.refresh(); - dialog.show(); - }, - customizeCSS: function() { - xhrJson("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => { - - const dialog = new fox.SingleUseDialog({ - title: __("Customize stylesheet"), - apply: function() { - xhrPost("backend.php", this.attr('value'), () => { - new Effect.Appear("css_edit_apply_msg"); - $("user_css_style").innerText = this.attr('value'); +
      + ${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'))} +
      +
      + `); }); }, execute: function () { - Notify.progress('Saving data...', true); + const sel_rows = this.getSelectedProfiles(); - xhrPost("backend.php", this.attr('value'), () => { - window.location.reload(); - }); + if (sel_rows.length == 1) { + if (confirm(__("Activate selected profile?"))) { + Notify.progress("Loading, please wait..."); + + xhr.post("backend.php", {op: "pref-prefs", method: "activateprofile", id: sel_rows.toString()}, () => { + window.location.reload(); + }); + } + + } else { + alert(__("Please choose a profile to activate.")); + } }, - content: ` -
      - ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")} -
      - - ${App.FormFields.hidden('op', 'rpc')} - ${App.FormFields.hidden('method', 'setpref')} - ${App.FormFields.hidden('key', 'USER_STYLESHEET')} - - - - - -
      - - - -
      - ` + content: "" }); + dialog.refresh(); dialog.show(); + }, + }, + Prefs: { + customizeCSS: function() { + xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => { - }); - }, - confirmReset: function() { - if (confirm(__("Reset to defaults?"))) { - xhrPost("backend.php", {op: "pref-prefs", method: "resetconfig"}, (transport) => { - Helpers.refresh(); - Notify.info(transport.responseText); - }); - } - }, - clearPluginData: function(name) { - if (confirm(__("Clear stored data for this plugin?"))) { - Notify.progress("Loading, please wait..."); + const dialog = new fox.SingleUseDialog({ + title: __("Customize stylesheet"), + apply: function() { + xhr.post("backend.php", this.attr('value'), () => { + Element.show("css_edit_apply_msg"); + App.byId("user_css_style").innerText = this.attr('value'); + }); + }, + execute: function () { + Notify.progress('Saving data...', true); + + xhr.post("backend.php", this.attr('value'), () => { + window.location.reload(); + }); + }, + content: ` +
      + ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")} +
      + + ${App.FormFields.hidden_tag('op', 'rpc')} + ${App.FormFields.hidden_tag('method', 'setpref')} + ${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')} + + + + + +
      + + + +
      + ` + }); + + dialog.show(); - xhrPost("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => { - Helpers.refresh(); }); - } - }, - refresh: function() { - xhrPost("backend.php", { op: "pref-prefs" }, (transport) => { - dijit.byId('prefsTab').attr('content', transport.responseText); - Notify.close(); - }); + }, + confirmReset: function() { + if (confirm(__("Reset to defaults?"))) { + xhr.post("backend.php", {op: "pref-prefs", method: "resetconfig"}, (reply) => { + Helpers.Prefs.refresh(); + Notify.info(reply); + }); + } + }, + clearPluginData: function(name) { + if (confirm(__("Clear stored data for this plugin?"))) { + Notify.progress("Loading, please wait..."); + + xhr.post("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => { + Helpers.Prefs.refresh(); + }); + } + }, + refresh: function() { + xhr.post("backend.php", { op: "pref-prefs" }, (reply) => { + dijit.byId('prefsTab').attr('content', reply); + Notify.close(); + }); + }, }, OPML: { import: function() { - const opml_file = $("opml_file"); + const opml_file = App.byId("opml_file"); if (opml_file.value.length == 0) { alert(__("Please choose an OPML file first.")); @@ -273,7 +338,7 @@ const Helpers = { dialog.show(); }; - xhr.send(new FormData($("opml_import_form"))); + xhr.send(new FormData(App.byId("opml_import_form"))); return false; } @@ -282,30 +347,62 @@ const Helpers = { console.log("export"); window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm")); }, - changeKey: function() { - if (confirm(__("Replace current OPML publishing address with a new one?"))) { - Notify.progress("Trying to change address...", true); + publish: function() { + Notify.progress("Loading, please wait...", true); - xhrJson("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => { - if (reply) { - const new_link = reply.link; - const e = $('pub_opml_url'); + 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?"))) { + Notify.progress("Trying to change address...", true); - if (new_link) { - e.href = new_link; - e.innerHTML = new_link; + xhr.json("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => { + if (reply) { + const new_link = reply.link; + const target = this.domNode.querySelector('.generated_url'); - new Effect.Highlight(e); + if (new_link && target) { + target.href = new_link; + target.innerHTML = new_link; - Notify.close(); + Notify.close(); - } else { - Notify.error("Could not change feed URL."); - } - } - }); - } - return false; + } else { + Notify.error("Could not change feed URL."); + } + } + }); + } + return false; + }, + content: ` +
      ${__("Your Public OPML URL is:")}
      +
      + +
      +
      + + +
      + ` + }); + + dialog.show(); + + Notify.close(); + + } catch (e) { + App.Error.report(e); + } + }); }, } }; diff --git a/js/PrefLabelTree.js b/js/PrefLabelTree.js index 73f375f2d..2b78927c2 100644 --- a/js/PrefLabelTree.js +++ b/js/PrefLabelTree.js @@ -1,5 +1,5 @@ /* 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) { @@ -48,83 +48,140 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f const items = tree.model.getCheckedItems(); const rv = []; - items.each(function(item) { + items.forEach(function(item) { rv.push(tree.model.store.getValue(item, 'bare_id')); }); return rv; }, reload: function() { - xhrPost("backend.php", { op: "pref-labels" }, (transport) => { - dijit.byId('labelsTab').attr('content', transport.responseText); + xhr.post("backend.php", { op: "pref-labels" }, (reply) => { + dijit.byId('labelsTab').attr('content', reply); Notify.close(); }); }, editLabel: function(id) { - const dialog = new fox.SingleUseDialog({ - id: "labelEditDlg", - title: __("Label Editor"), - style: "width: 650px", - setLabelColor: function (id, fg, bg) { + xhr.json("backend.php", {op: "pref-labels", method: "edit", id: id}, (reply) => { - let kind = ''; - let color = ''; + const fg_color = reply['fg_color']; + const bg_color = reply['bg_color'] ? reply['bg_color'] : '#fff7d5'; - if (fg && bg) { - kind = 'both'; - } else if (fg) { - kind = 'fg'; - color = fg; - } else if (bg) { - kind = 'bg'; - color = bg; - } + const dialog = new fox.SingleUseDialog({ + id: "labelEditDlg", + title: __("Label Editor"), + style: "width: 650px", + setLabelColor: function (id, fg, bg) { - const e = $("icon-label-" + id); + let kind = ''; + let color = ''; - if (e) { - if (bg) e.style.color = bg; - } + if (fg && bg) { + kind = 'both'; + } else if (fg) { + kind = 'fg'; + color = fg; + } else if (bg) { + kind = 'bg'; + color = bg; + } - const query = { - op: "pref-labels", method: "colorset", kind: kind, - ids: id, fg: fg, bg: bg, color: color - }; + const e = App.byId(`icon-label-${id}`); - xhrPost("backend.php", query, () => { - const tree = dijit.byId("filterTree"); - if (tree) tree.reload(); // maybe there's labels in there - }); + if (e) { + if (bg) e.style.color = bg; + } - }, - execute: function () { - if (this.validate()) { - const caption = this.attr('value').caption; - const fg_color = this.attr('value').fg_color; - const bg_color = this.attr('value').bg_color; + const query = { + op: "pref-labels", method: "colorset", kind: kind, + ids: id, fg: fg, bg: bg, color: color + }; - dijit.byId('labelTree').setNameById(id, caption); - this.setLabelColor(id, fg_color, bg_color); - this.hide(); - - xhrPost("backend.php", this.attr('value'), () => { + xhr.post("backend.php", query, () => { const tree = dijit.byId("filterTree"); if (tree) tree.reload(); // maybe there's labels in there }); - } - }, - content: __("Loading, please wait...") + + }, + execute: function () { + if (this.validate()) { + const caption = this.attr('value').caption; + const fg_color = this.attr('value').fg_color; + const bg_color = this.attr('value').bg_color; + + dijit.byId('labelTree').setNameById(id, caption); + this.setLabelColor(id, fg_color, bg_color); + this.hide(); + + xhr.post("backend.php", this.attr('value'), () => { + const tree = dijit.byId("filterTree"); + if (tree) tree.reload(); // maybe there's labels in there + }); + } + }, + content: ` +
      + +
      ${__("Caption")}
      +
      + +
      + + ${App.FormFields.hidden_tag('id', id)} + ${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')} + +
      ${__("Colors")}
      +
      + + + + + + + + + +
      ${__("Foreground:")}${__("Background:")}
      +
      + +
      +
      +
      + +
      +
      +
      + +
      + + +
      + +
      + ` + }); + + dialog.show(); + }); - - const tmph = dojo.connect(dialog, 'onShow', function () { - dojo.disconnect(tmph); - - xhrPost("backend.php", {op: "pref-labels", method: "edit", id: id}, (transport) => { - dialog.attr('content', transport.responseText); - }) - }); - - dialog.show(); }, resetColors: function() { const labels = this.getSelectedLabels(); @@ -137,7 +194,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f ids: labels.toString() }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { this.reload(); }); } @@ -158,7 +215,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f ids: sel_rows.toString() }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { this.reload(); }); } diff --git a/js/PrefUsers.js b/js/PrefUsers.js index 0a7e635fe..3eb83b02a 100644 --- a/js/PrefUsers.js +++ b/js/PrefUsers.js @@ -1,15 +1,15 @@ 'use strict' /* global __ */ -/* global xhrPost, dojo, dijit, Notify, Tables, fox */ +/* global xhrPost, xhr, dijit, Notify, Tables, App, fox */ const Users = { reload: function(sort) { - const user_search = $("user_search"); + const user_search = App.byId("user_search"); const search = user_search ? user_search.value : ""; - xhrPost("backend.php", { op: "pref-users", sort: sort, search: search }, (transport) => { - dijit.byId('usersTab').attr('content', transport.responseText); + xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => { + dijit.byId('usersTab').attr('content', reply); Notify.close(); }); }, @@ -19,15 +19,18 @@ const Users = { if (login) { Notify.progress("Adding user..."); - xhrPost("backend.php", {op: "pref-users", method: "add", login: login}, (transport) => { - alert(transport.responseText); + xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => { + alert(reply); Users.reload(); }); } }, 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({ id: "userEditDlg", title: __("User Editor"), @@ -35,13 +38,86 @@ const Users = { if (this.validate()) { Notify.progress("Saving data...", true); - xhrPost("backend.php", dojo.formToObject("user_edit_form"), (/* transport */) => { + xhr.post("backend.php", this.attr('value'), () => { dialog.hide(); Users.reload(); }); } }, - content: transport.responseText + content: ` +
      + + ${App.FormFields.hidden_tag('id', user.id.toString())} + ${App.FormFields.hidden_tag('op', 'pref-users')} + ${App.FormFields.hidden_tag('method', 'editSave')} + +
      +
      + +
      ${__("User")}
      + +
      +
      + + + + ${admin_disabled ? App.FormFields.hidden_tag("login", user.login) : ''} +
      +
      + +
      ${__("Authentication")}
      + +
      +
      + + ${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()) : ''} +
      +
      + + +
      +
      + +
      ${__("Options")}
      + +
      +
      + + +
      +
      +
      +
      + + ${__("Loading, please wait...")} +
      +
      + +
      + + +
      +
      + ` }); dialog.show(); @@ -65,9 +141,9 @@ const Users = { 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.info(transport.responseText, true); + Notify.info(reply, true); }); } @@ -84,7 +160,7 @@ const Users = { ids: sel_rows.toString() }; - xhrPost("backend.php", query, () => { + xhr.post("backend.php", query, () => { this.reload(); }); } @@ -93,21 +169,6 @@ const Users = { 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() { return Tables.getSelected("users-list"); } diff --git a/js/SingleUseDialog.js b/js/SingleUseDialog.js index 944f24c6f..2de6f83ff 100644 --- a/js/SingleUseDialog.js +++ b/js/SingleUseDialog.js @@ -1,6 +1,17 @@ +/* eslint-disable prefer-rest-params */ /* global dijit, define */ define(["dojo/_base/declare", "dijit/Dialog"], function (declare) { 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() { this.destroyRecursive(); } diff --git a/js/common.js b/js/common.js index fb5cc6531..670ee1b30 100755 --- a/js/common.js +++ b/js/common.js @@ -1,60 +1,225 @@ 'use strict'; -/* global dijit, __, App, Ajax */ +/* global dijit, __, App, dojo, __csrf_token */ /* eslint-disable no-new */ -/* error reporting shim */ -// TODO: deprecated; remove -/* function exception_error(e, e_compat, filename, lineno, colno) { - if (typeof e == "string") - e = e_compat; +/* exported $ */ +function $(id) { + console.warn("FIXME: please use App.byId() or document.getElementById() instead of $():", id); + return document.getElementById(id); +} - App.Error.report(e, {filename: filename, lineno: lineno, colno: colno}); -} */ +/* exported $$ */ +function $$(query) { + console.warn("FIXME: please use App.findAll() or document.querySelectorAll() instead of $$():", query); + return document.querySelectorAll(query); +} -/* xhr shorthand helpers */ -/* exported xhrPost */ -function xhrPost(url, params, complete) { - console.log("xhrPost:", params); +Element.prototype.hasClassName = function(className) { + return this.classList.contains(className); +}; - return new Promise((resolve, reject) => { - new Ajax.Request(url, { - parameters: params, - onComplete: function(reply) { - if (complete != undefined) complete(reply); +Element.prototype.addClassName = function(className) { + return this.classList.add(className); +}; - resolve(reply); - } - }); +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 */ -function xhrJson(url, params, complete) { - return new Promise((resolve, reject) => - xhrPost(url, params).then((reply) => { - let obj = null; +Element.visible = function(elem) { + if (typeof elem == "string") + elem = document.getElementById(elem); - try { - obj = JSON.parse(reply.responseText); - } catch (e) { - console.error("xhrJson", e, reply); - } - - if (complete != undefined) complete(obj); - - resolve(obj); - })); + return elem.visible(); +} + +Element.show = function(elem) { + if (typeof elem == "string") + elem = document.getElementById(elem); + + 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) { for (let i=0; i < this.length; i++) { 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 */ /* exported Lists */ @@ -64,14 +229,14 @@ const Lists = { // account for dojo checkboxes elem = elem.domNode || elem; - const row = elem.up("li"); + const row = elem.closest("li"); if (row) checked ? row.addClassName("Selected") : row.removeClassName("Selected"); }, select: function(elemId, selected) { - $(elemId).select("li").each((row) => { - const checkNode = row.select(".dijitCheckBox,input[type=checkbox]")[0]; + $(elemId).querySelectorAll("li").forEach((row) => { + const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]"); if (checkNode) { const widget = dijit.getEnclosingWidget(checkNode); @@ -94,15 +259,15 @@ const Tables = { const checked = elem.domNode ? elem.attr("checked") : elem.checked; elem = elem.domNode || elem; - const row = elem.up("tr"); + const row = elem.closest("tr"); if (row) checked ? row.addClassName("Selected") : row.removeClassName("Selected"); }, select: function(elemId, selected) { - $(elemId).select("tr").each((row) => { - const checkNode = row.select(".dijitCheckBox,input[type=checkbox]")[0]; + $(elemId).querySelectorAll("tr").forEach((row) => { + const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]"); if (checkNode) { const widget = dijit.getEnclosingWidget(checkNode); @@ -119,7 +284,7 @@ const Tables = { getSelected: function(elemId) { const rv = []; - $(elemId).select("tr").each((row) => { + $(elemId).querySelectorAll("tr").forEach((row) => { if (row.hasClassName("Selected")) { // either older prefix-XXX notation or separate attribute const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, ""); @@ -173,7 +338,7 @@ const Notify = { kind = kind || this.KIND_GENERIC; keep = keep || false; - const notify = $("notify"); + const notify = App.byId("notify"); 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(); -} diff --git a/js/form/ValidationMultiSelect.js b/js/form/ValidationMultiSelect.js new file mode 100644 index 000000000..4e7263c61 --- /dev/null +++ b/js/form/ValidationMultiSelect.js @@ -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; + }, + }) + }); diff --git a/js/prefs.js b/js/prefs.js index 803a7edf3..8f4f45700 100755 --- a/js/prefs.js +++ b/js/prefs.js @@ -41,6 +41,7 @@ require(["dojo/_base/kernel", "dojo/data/ItemFileWriteStore", "lib/CheckBoxStoreModel", "lib/CheckBoxTree", + "fox/PluginHost", "fox/CommonDialogs", "fox/CommonFilters", "fox/PrefUsers", @@ -52,6 +53,7 @@ require(["dojo/_base/kernel", "fox/PrefLabelTree", "fox/Toolbar", "fox/SingleUseDialog", + "fox/form/ValidationMultiSelect", "fox/form/ValidationTextArea", "fox/form/Select", "fox/form/ComboButton", diff --git a/js/tt-rss.js b/js/tt-rss.js index 764667a0d..4a7f2e643 100644 --- a/js/tt-rss.js +++ b/js/tt-rss.js @@ -1,6 +1,6 @@ 'use strict' -/* global require, App, $H */ +/* global require, App, dojo */ /* exported Plugins */ const Plugins = {}; @@ -51,6 +51,7 @@ require(["dojo/_base/kernel", "fox/FeedTree", "fox/Toolbar", "fox/SingleUseDialog", + "fox/form/ValidationMultiSelect", "fox/form/ValidationTextArea", "fox/form/Select", "fox/form/ComboButton", @@ -70,13 +71,13 @@ require(["dojo/_base/kernel", /* exported hash_get */ function hash_get(key) { - const kv = window.location.hash.substring(1).toQueryParams(); - return kv[key]; + const obj = dojo.queryToObject(window.location.hash.substring(1)); + return obj[key]; } /* exported hash_set */ function hash_set(key, value) { - const kv = window.location.hash.substring(1).toQueryParams(); - kv[key] = value; - window.location.hash = $H(kv).toQueryString(); + const obj = dojo.queryToObject(window.location.hash.substring(1)); + obj[key] = value; + window.location.hash = dojo.objectToQuery(obj); } diff --git a/js/utility.js b/js/utility.js index eef1c6b61..43ad5644e 100644 --- a/js/utility.js +++ b/js/utility.js @@ -2,7 +2,7 @@ /* 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 = { apply_night_mode: function (is_night, link) { console.log("night mode changed to", is_night); @@ -16,10 +16,10 @@ Event.observe(window, "load", function() { setup_night_mode: function() { const mql = window.matchMedia('(prefers-color-scheme: dark)'); - const link = new Element("link", { - rel: "stylesheet", - id: "theme_auto_css" - }); + const link = document.createElement("link"); + + link.rel = "stylesheet"; + link.id = "theme_auto_css"; link.onload = function() { document.querySelector("body").removeClassName("css_loading"); diff --git a/lib/accept-to-gettext.php b/lib/accept-to-gettext.php deleted file mode 100644 index c86a62b2e..000000000 --- a/lib/accept-to-gettext.php +++ /dev/null @@ -1,186 +0,0 @@ - - * - * 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(, - * ); - * 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 - * - * 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; -} - -?> diff --git a/lib/floIcon.php b/lib/floIcon.php index 11a102b50..c3dcbe52e 100644 --- a/lib/floIcon.php +++ b/lib/floIcon.php @@ -10,6 +10,8 @@ class floIconIcon { } class floIcon { + public $images = array(); + function readICO($file) { $jim = new jimIcon(); $icon = new floIconIcon(); diff --git a/lib/jimIcon.php b/lib/jimIcon.php index f8e533f90..eaa768272 100644 --- a/lib/jimIcon.php +++ b/lib/jimIcon.php @@ -104,11 +104,11 @@ class jimIcon { } // See if we can parse it (might be PNG format here) - $i = @imagecreatefromstring($data); - - if ($i) { - imagesavealpha($i, true); - return $i; + if (self::has_parsable_image_type($data)) { + if ($i = @imagecreatefromstring($data)) { + imagesavealpha($i, true); + return $i; + } } // Must be a BMP. Parse it ourselves. @@ -267,5 +267,12 @@ class jimIcon { } 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]); + } } ?> diff --git a/lib/prototype.js b/lib/prototype.js deleted file mode 100644 index d9bbc6e80..000000000 --- a/lib/prototype.js +++ /dev/null @@ -1,7590 +0,0 @@ -// Modified document.on() to modified.p_on() to fix compatibility with Dojo -fox - -/* Prototype JavaScript framework, version 1.7.3 - * (c) 2005-2010 Sam Stephenson - * - * Prototype is freely distributable under the terms of an MIT-style license. - * For details, see the Prototype web site: http://www.prototypejs.org/ - * - *--------------------------------------------------------------------------*/ - -var Prototype = { - - Version: '1.7.3', - - Browser: (function(){ - var ua = navigator.userAgent; - var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; - return { - IE: !!window.attachEvent && !isOpera, - Opera: isOpera, - WebKit: ua.indexOf('AppleWebKit/') > -1, - Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, - MobileSafari: /Apple.*Mobile/.test(ua) - } - })(), - - BrowserFeatures: { - XPath: !!document.evaluate, - - SelectorsAPI: !!document.querySelector, - - ElementExtensions: (function() { - var constructor = window.Element || window.HTMLElement; - return !!(constructor && constructor.prototype); - })(), - SpecificElementExtensions: (function() { - if (typeof window.HTMLDivElement !== 'undefined') - return true; - - var div = document.createElement('div'), - form = document.createElement('form'), - isSupported = false; - - if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { - isSupported = true; - } - - div = form = null; - - return isSupported; - })() - }, - - ScriptFragment: ']*>([\\S\\s]*?)<\/script\\s*>', - JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, - - emptyFunction: function() { }, - - K: function(x) { return x } -}; - -if (Prototype.Browser.MobileSafari) - Prototype.BrowserFeatures.SpecificElementExtensions = false; -/* Based on Alex Arnell's inheritance implementation. */ - -var Class = (function() { - - var IS_DONTENUM_BUGGY = (function(){ - for (var p in { toString: 1 }) { - if (p === 'toString') return false; - } - return true; - })(); - - function subclass() {}; - function create() { - var parent = null, properties = $A(arguments); - if (Object.isFunction(properties[0])) - parent = properties.shift(); - - function klass() { - this.initialize.apply(this, arguments); - } - - Object.extend(klass, Class.Methods); - klass.superclass = parent; - klass.subclasses = []; - - if (parent) { - subclass.prototype = parent.prototype; - klass.prototype = new subclass; - parent.subclasses.push(klass); - } - - for (var i = 0, length = properties.length; i < length; i++) - klass.addMethods(properties[i]); - - if (!klass.prototype.initialize) - klass.prototype.initialize = Prototype.emptyFunction; - - klass.prototype.constructor = klass; - return klass; - } - - function addMethods(source) { - var ancestor = this.superclass && this.superclass.prototype, - properties = Object.keys(source); - - if (IS_DONTENUM_BUGGY) { - if (source.toString != Object.prototype.toString) - properties.push("toString"); - if (source.valueOf != Object.prototype.valueOf) - properties.push("valueOf"); - } - - for (var i = 0, length = properties.length; i < length; i++) { - var property = properties[i], value = source[property]; - if (ancestor && Object.isFunction(value) && - value.argumentNames()[0] == "$super") { - var method = value; - value = (function(m) { - return function() { return ancestor[m].apply(this, arguments); }; - })(property).wrap(method); - - value.valueOf = (function(method) { - return function() { return method.valueOf.call(method); }; - })(method); - - value.toString = (function(method) { - return function() { return method.toString.call(method); }; - })(method); - } - this.prototype[property] = value; - } - - return this; - } - - return { - create: create, - Methods: { - addMethods: addMethods - } - }; -})(); -(function() { - - var _toString = Object.prototype.toString, - _hasOwnProperty = Object.prototype.hasOwnProperty, - NULL_TYPE = 'Null', - UNDEFINED_TYPE = 'Undefined', - BOOLEAN_TYPE = 'Boolean', - NUMBER_TYPE = 'Number', - STRING_TYPE = 'String', - OBJECT_TYPE = 'Object', - FUNCTION_CLASS = '[object Function]', - BOOLEAN_CLASS = '[object Boolean]', - NUMBER_CLASS = '[object Number]', - STRING_CLASS = '[object String]', - ARRAY_CLASS = '[object Array]', - DATE_CLASS = '[object Date]', - NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON && - typeof JSON.stringify === 'function' && - JSON.stringify(0) === '0' && - typeof JSON.stringify(Prototype.K) === 'undefined'; - - - - var DONT_ENUMS = ['toString', 'toLocaleString', 'valueOf', - 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor']; - - var IS_DONTENUM_BUGGY = (function(){ - for (var p in { toString: 1 }) { - if (p === 'toString') return false; - } - return true; - })(); - - function Type(o) { - switch(o) { - case null: return NULL_TYPE; - case (void 0): return UNDEFINED_TYPE; - } - var type = typeof o; - switch(type) { - case 'boolean': return BOOLEAN_TYPE; - case 'number': return NUMBER_TYPE; - case 'string': return STRING_TYPE; - } - return OBJECT_TYPE; - } - - function extend(destination, source) { - for (var property in source) - destination[property] = source[property]; - return destination; - } - - function inspect(object) { - try { - if (isUndefined(object)) return 'undefined'; - if (object === null) return 'null'; - return object.inspect ? object.inspect() : String(object); - } catch (e) { - if (e instanceof RangeError) return '...'; - throw e; - } - } - - function toJSON(value) { - return Str('', { '': value }, []); - } - - function Str(key, holder, stack) { - var value = holder[key]; - if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - - var _class = _toString.call(value); - - switch (_class) { - case NUMBER_CLASS: - case BOOLEAN_CLASS: - case STRING_CLASS: - value = value.valueOf(); - } - - switch (value) { - case null: return 'null'; - case true: return 'true'; - case false: return 'false'; - } - - var type = typeof value; - switch (type) { - case 'string': - return value.inspect(true); - case 'number': - return isFinite(value) ? String(value) : 'null'; - case 'object': - - for (var i = 0, length = stack.length; i < length; i++) { - if (stack[i] === value) { - throw new TypeError("Cyclic reference to '" + value + "' in object"); - } - } - stack.push(value); - - var partial = []; - if (_class === ARRAY_CLASS) { - for (var i = 0, length = value.length; i < length; i++) { - var str = Str(i, value, stack); - partial.push(typeof str === 'undefined' ? 'null' : str); - } - partial = '[' + partial.join(',') + ']'; - } else { - var keys = Object.keys(value); - for (var i = 0, length = keys.length; i < length; i++) { - var key = keys[i], str = Str(key, value, stack); - if (typeof str !== "undefined") { - partial.push(key.inspect(true)+ ':' + str); - } - } - partial = '{' + partial.join(',') + '}'; - } - stack.pop(); - return partial; - } - } - - function stringify(object) { - return JSON.stringify(object); - } - - function toQueryString(object) { - return $H(object).toQueryString(); - } - - function toHTML(object) { - return object && object.toHTML ? object.toHTML() : String.interpret(object); - } - - function keys(object) { - if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } - var results = []; - for (var property in object) { - if (_hasOwnProperty.call(object, property)) - results.push(property); - } - - if (IS_DONTENUM_BUGGY) { - for (var i = 0; property = DONT_ENUMS[i]; i++) { - if (_hasOwnProperty.call(object, property)) - results.push(property); - } - } - - return results; - } - - function values(object) { - var results = []; - for (var property in object) - results.push(object[property]); - return results; - } - - function clone(object) { - return extend({ }, object); - } - - function isElement(object) { - return !!(object && object.nodeType == 1); - } - - function isArray(object) { - return _toString.call(object) === ARRAY_CLASS; - } - - var hasNativeIsArray = (typeof Array.isArray == 'function') - && Array.isArray([]) && !Array.isArray({}); - - if (hasNativeIsArray) { - isArray = Array.isArray; - } - - function isHash(object) { - return object instanceof Hash; - } - - function isFunction(object) { - return _toString.call(object) === FUNCTION_CLASS; - } - - function isString(object) { - return _toString.call(object) === STRING_CLASS; - } - - function isNumber(object) { - return _toString.call(object) === NUMBER_CLASS; - } - - function isDate(object) { - return _toString.call(object) === DATE_CLASS; - } - - function isUndefined(object) { - return typeof object === "undefined"; - } - - extend(Object, { - extend: extend, - inspect: inspect, - toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON, - toQueryString: toQueryString, - toHTML: toHTML, - keys: Object.keys || keys, - values: values, - clone: clone, - isElement: isElement, - isArray: isArray, - isHash: isHash, - isFunction: isFunction, - isString: isString, - isNumber: isNumber, - isDate: isDate, - isUndefined: isUndefined - }); -})(); -Object.extend(Function.prototype, (function() { - var slice = Array.prototype.slice; - - function update(array, args) { - var arrayLength = array.length, length = args.length; - while (length--) array[arrayLength + length] = args[length]; - return array; - } - - function merge(array, args) { - array = slice.call(array, 0); - return update(array, args); - } - - function argumentNames() { - var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] - .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') - .replace(/\s+/g, '').split(','); - return names.length == 1 && !names[0] ? [] : names; - } - - - function bind(context) { - if (arguments.length < 2 && Object.isUndefined(arguments[0])) - return this; - - if (!Object.isFunction(this)) - throw new TypeError("The object is not callable."); - - var nop = function() {}; - var __method = this, args = slice.call(arguments, 1); - - var bound = function() { - var a = merge(args, arguments); - var c = this instanceof bound ? this : context; - return __method.apply(c, a); - }; - - nop.prototype = this.prototype; - bound.prototype = new nop(); - - return bound; - } - - function bindAsEventListener(context) { - var __method = this, args = slice.call(arguments, 1); - return function(event) { - var a = update([event || window.event], args); - return __method.apply(context, a); - } - } - - function curry() { - if (!arguments.length) return this; - var __method = this, args = slice.call(arguments, 0); - return function() { - var a = merge(args, arguments); - return __method.apply(this, a); - } - } - - function delay(timeout) { - var __method = this, args = slice.call(arguments, 1); - timeout = timeout * 1000; - return window.setTimeout(function() { - return __method.apply(__method, args); - }, timeout); - } - - function defer() { - var args = update([0.01], arguments); - return this.delay.apply(this, args); - } - - function wrap(wrapper) { - var __method = this; - return function() { - var a = update([__method.bind(this)], arguments); - return wrapper.apply(this, a); - } - } - - function methodize() { - if (this._methodized) return this._methodized; - var __method = this; - return this._methodized = function() { - var a = update([this], arguments); - return __method.apply(null, a); - }; - } - - var extensions = { - argumentNames: argumentNames, - bindAsEventListener: bindAsEventListener, - curry: curry, - delay: delay, - defer: defer, - wrap: wrap, - methodize: methodize - }; - - if (!Function.prototype.bind) - extensions.bind = bind; - - return extensions; -})()); - - - -(function(proto) { - - - function toISOString() { - return this.getUTCFullYear() + '-' + - (this.getUTCMonth() + 1).toPaddedString(2) + '-' + - this.getUTCDate().toPaddedString(2) + 'T' + - this.getUTCHours().toPaddedString(2) + ':' + - this.getUTCMinutes().toPaddedString(2) + ':' + - this.getUTCSeconds().toPaddedString(2) + 'Z'; - } - - - function toJSON() { - return this.toISOString(); - } - - if (!proto.toISOString) proto.toISOString = toISOString; - if (!proto.toJSON) proto.toJSON = toJSON; - -})(Date.prototype); - - -RegExp.prototype.match = RegExp.prototype.test; - -RegExp.escape = function(str) { - return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); -}; -var PeriodicalExecuter = Class.create({ - initialize: function(callback, frequency) { - this.callback = callback; - this.frequency = frequency; - this.currentlyExecuting = false; - - this.registerCallback(); - }, - - registerCallback: function() { - this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); - }, - - execute: function() { - this.callback(this); - }, - - stop: function() { - if (!this.timer) return; - clearInterval(this.timer); - this.timer = null; - }, - - onTimerEvent: function() { - if (!this.currentlyExecuting) { - try { - this.currentlyExecuting = true; - this.execute(); - this.currentlyExecuting = false; - } catch(e) { - this.currentlyExecuting = false; - throw e; - } - } - } -}); -Object.extend(String, { - interpret: function(value) { - return value == null ? '' : String(value); - }, - specialChar: { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '\\': '\\\\' - } -}); - -Object.extend(String.prototype, (function() { - var NATIVE_JSON_PARSE_SUPPORT = window.JSON && - typeof JSON.parse === 'function' && - JSON.parse('{"test": true}').test; - - function prepareReplacement(replacement) { - if (Object.isFunction(replacement)) return replacement; - var template = new Template(replacement); - return function(match) { return template.evaluate(match) }; - } - - function isNonEmptyRegExp(regexp) { - return regexp.source && regexp.source !== '(?:)'; - } - - - function gsub(pattern, replacement) { - var result = '', source = this, match; - replacement = prepareReplacement(replacement); - - if (Object.isString(pattern)) - pattern = RegExp.escape(pattern); - - if (!(pattern.length || isNonEmptyRegExp(pattern))) { - replacement = replacement(''); - return replacement + source.split('').join(replacement) + replacement; - } - - while (source.length > 0) { - match = source.match(pattern) - if (match && match[0].length > 0) { - result += source.slice(0, match.index); - result += String.interpret(replacement(match)); - source = source.slice(match.index + match[0].length); - } else { - result += source, source = ''; - } - } - return result; - } - - function sub(pattern, replacement, count) { - replacement = prepareReplacement(replacement); - count = Object.isUndefined(count) ? 1 : count; - - return this.gsub(pattern, function(match) { - if (--count < 0) return match[0]; - return replacement(match); - }); - } - - function scan(pattern, iterator) { - this.gsub(pattern, iterator); - return String(this); - } - - function truncate(length, truncation) { - length = length || 30; - truncation = Object.isUndefined(truncation) ? '...' : truncation; - return this.length > length ? - this.slice(0, length - truncation.length) + truncation : String(this); - } - - function strip() { - return this.replace(/^\s+/, '').replace(/\s+$/, ''); - } - - function stripTags() { - return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?(\/)?>|<\/\w+>/gi, ''); - } - - function stripScripts() { - return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); - } - - function extractScripts() { - var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), - matchOne = new RegExp(Prototype.ScriptFragment, 'im'); - return (this.match(matchAll) || []).map(function(scriptTag) { - return (scriptTag.match(matchOne) || ['', ''])[1]; - }); - } - - function evalScripts() { - return this.extractScripts().map(function(script) { return eval(script); }); - } - - function escapeHTML() { - return this.replace(/&/g,'&').replace(//g,'>'); - } - - function unescapeHTML() { - return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&'); - } - - - function toQueryParams(separator) { - var match = this.strip().match(/([^?#]*)(#.*)?$/); - if (!match) return { }; - - return match[1].split(separator || '&').inject({ }, function(hash, pair) { - if ((pair = pair.split('='))[0]) { - var key = decodeURIComponent(pair.shift()), - value = pair.length > 1 ? pair.join('=') : pair[0]; - - if (value != undefined) { - value = value.gsub('+', ' '); - value = decodeURIComponent(value); - } - - if (key in hash) { - if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; - hash[key].push(value); - } - else hash[key] = value; - } - return hash; - }); - } - - function toArray() { - return this.split(''); - } - - function succ() { - return this.slice(0, this.length - 1) + - String.fromCharCode(this.charCodeAt(this.length - 1) + 1); - } - - function times(count) { - return count < 1 ? '' : new Array(count + 1).join(this); - } - - function camelize() { - return this.replace(/-+(.)?/g, function(match, chr) { - return chr ? chr.toUpperCase() : ''; - }); - } - - function capitalize() { - return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); - } - - function underscore() { - return this.replace(/::/g, '/') - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') - .replace(/([a-z\d])([A-Z])/g, '$1_$2') - .replace(/-/g, '_') - .toLowerCase(); - } - - function dasherize() { - return this.replace(/_/g, '-'); - } - - function inspect(useDoubleQuotes) { - var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) { - if (character in String.specialChar) { - return String.specialChar[character]; - } - return '\\u00' + character.charCodeAt().toPaddedString(2, 16); - }); - if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; - return "'" + escapedString.replace(/'/g, '\\\'') + "'"; - } - - function unfilterJSON(filter) { - return this.replace(filter || Prototype.JSONFilter, '$1'); - } - - function isJSON() { - var str = this; - if (str.blank()) return false; - str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); - str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); - str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); - return (/^[\],:{}\s]*$/).test(str); - } - - function evalJSON(sanitize) { - var json = this.unfilterJSON(), - cx = /[\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff\u0000]/g; - if (cx.test(json)) { - json = json.replace(cx, function (a) { - return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - try { - if (!sanitize || json.isJSON()) return eval('(' + json + ')'); - } catch (e) { } - throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); - } - - function parseJSON() { - var json = this.unfilterJSON(); - return JSON.parse(json); - } - - function include(pattern) { - return this.indexOf(pattern) > -1; - } - - function startsWith(pattern, position) { - position = Object.isNumber(position) ? position : 0; - return this.lastIndexOf(pattern, position) === position; - } - - function endsWith(pattern, position) { - pattern = String(pattern); - position = Object.isNumber(position) ? position : this.length; - if (position < 0) position = 0; - if (position > this.length) position = this.length; - var d = position - pattern.length; - return d >= 0 && this.indexOf(pattern, d) === d; - } - - function empty() { - return this == ''; - } - - function blank() { - return /^\s*$/.test(this); - } - - function interpolate(object, pattern) { - return new Template(this, pattern).evaluate(object); - } - - return { - gsub: gsub, - sub: sub, - scan: scan, - truncate: truncate, - strip: String.prototype.trim || strip, - stripTags: stripTags, - stripScripts: stripScripts, - extractScripts: extractScripts, - evalScripts: evalScripts, - escapeHTML: escapeHTML, - unescapeHTML: unescapeHTML, - toQueryParams: toQueryParams, - parseQuery: toQueryParams, - toArray: toArray, - succ: succ, - times: times, - camelize: camelize, - capitalize: capitalize, - underscore: underscore, - dasherize: dasherize, - inspect: inspect, - unfilterJSON: unfilterJSON, - isJSON: isJSON, - evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON, - include: include, - startsWith: String.prototype.startsWith || startsWith, - endsWith: String.prototype.endsWith || endsWith, - empty: empty, - blank: blank, - interpolate: interpolate - }; -})()); - -var Template = Class.create({ - initialize: function(template, pattern) { - this.template = template.toString(); - this.pattern = pattern || Template.Pattern; - }, - - evaluate: function(object) { - if (object && Object.isFunction(object.toTemplateReplacements)) - object = object.toTemplateReplacements(); - - return this.template.gsub(this.pattern, function(match) { - if (object == null) return (match[1] + ''); - - var before = match[1] || ''; - if (before == '\\') return match[2]; - - var ctx = object, expr = match[3], - pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; - - match = pattern.exec(expr); - if (match == null) return before; - - while (match != null) { - var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1]; - ctx = ctx[comp]; - if (null == ctx || '' == match[3]) break; - expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); - match = pattern.exec(expr); - } - - return before + String.interpret(ctx); - }); - } -}); -Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; - -var $break = { }; - -var Enumerable = (function() { - function each(iterator, context) { - try { - this._each(iterator, context); - } catch (e) { - if (e != $break) throw e; - } - return this; - } - - function eachSlice(number, iterator, context) { - var index = -number, slices = [], array = this.toArray(); - if (number < 1) return array; - while ((index += number) < array.length) - slices.push(array.slice(index, index+number)); - return slices.collect(iterator, context); - } - - function all(iterator, context) { - iterator = iterator || Prototype.K; - var result = true; - this.each(function(value, index) { - result = result && !!iterator.call(context, value, index, this); - if (!result) throw $break; - }, this); - return result; - } - - function any(iterator, context) { - iterator = iterator || Prototype.K; - var result = false; - this.each(function(value, index) { - if (result = !!iterator.call(context, value, index, this)) - throw $break; - }, this); - return result; - } - - function collect(iterator, context) { - iterator = iterator || Prototype.K; - var results = []; - this.each(function(value, index) { - results.push(iterator.call(context, value, index, this)); - }, this); - return results; - } - - function detect(iterator, context) { - var result; - this.each(function(value, index) { - if (iterator.call(context, value, index, this)) { - result = value; - throw $break; - } - }, this); - return result; - } - - function findAll(iterator, context) { - var results = []; - this.each(function(value, index) { - if (iterator.call(context, value, index, this)) - results.push(value); - }, this); - return results; - } - - function grep(filter, iterator, context) { - iterator = iterator || Prototype.K; - var results = []; - - if (Object.isString(filter)) - filter = new RegExp(RegExp.escape(filter)); - - this.each(function(value, index) { - if (filter.match(value)) - results.push(iterator.call(context, value, index, this)); - }, this); - return results; - } - - function include(object) { - if (Object.isFunction(this.indexOf) && this.indexOf(object) != -1) - return true; - - var found = false; - this.each(function(value) { - if (value == object) { - found = true; - throw $break; - } - }); - return found; - } - - function inGroupsOf(number, fillWith) { - fillWith = Object.isUndefined(fillWith) ? null : fillWith; - return this.eachSlice(number, function(slice) { - while(slice.length < number) slice.push(fillWith); - return slice; - }); - } - - function inject(memo, iterator, context) { - this.each(function(value, index) { - memo = iterator.call(context, memo, value, index, this); - }, this); - return memo; - } - - function invoke(method) { - var args = $A(arguments).slice(1); - return this.map(function(value) { - return value[method].apply(value, args); - }); - } - - function max(iterator, context) { - iterator = iterator || Prototype.K; - var result; - this.each(function(value, index) { - value = iterator.call(context, value, index, this); - if (result == null || value >= result) - result = value; - }, this); - return result; - } - - function min(iterator, context) { - iterator = iterator || Prototype.K; - var result; - this.each(function(value, index) { - value = iterator.call(context, value, index, this); - if (result == null || value < result) - result = value; - }, this); - return result; - } - - function partition(iterator, context) { - iterator = iterator || Prototype.K; - var trues = [], falses = []; - this.each(function(value, index) { - (iterator.call(context, value, index, this) ? - trues : falses).push(value); - }, this); - return [trues, falses]; - } - - function pluck(property) { - var results = []; - this.each(function(value) { - results.push(value[property]); - }); - return results; - } - - function reject(iterator, context) { - var results = []; - this.each(function(value, index) { - if (!iterator.call(context, value, index, this)) - results.push(value); - }, this); - return results; - } - - function sortBy(iterator, context) { - return this.map(function(value, index) { - return { - value: value, - criteria: iterator.call(context, value, index, this) - }; - }, this).sort(function(left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; - }).pluck('value'); - } - - function toArray() { - return this.map(); - } - - function zip() { - var iterator = Prototype.K, args = $A(arguments); - if (Object.isFunction(args.last())) - iterator = args.pop(); - - var collections = [this].concat(args).map($A); - return this.map(function(value, index) { - return iterator(collections.pluck(index)); - }); - } - - function size() { - return this.toArray().length; - } - - function inspect() { - return '#'; - } - - - - - - - - - - return { - each: each, - eachSlice: eachSlice, - all: all, - every: all, - any: any, - some: any, - collect: collect, - map: collect, - detect: detect, - findAll: findAll, - select: findAll, - filter: findAll, - grep: grep, - include: include, - member: include, - inGroupsOf: inGroupsOf, - inject: inject, - invoke: invoke, - max: max, - min: min, - partition: partition, - pluck: pluck, - reject: reject, - sortBy: sortBy, - toArray: toArray, - entries: toArray, - zip: zip, - size: size, - inspect: inspect, - find: detect - }; -})(); - -function $A(iterable) { - if (!iterable) return []; - if ('toArray' in Object(iterable)) return iterable.toArray(); - var length = iterable.length || 0, results = new Array(length); - while (length--) results[length] = iterable[length]; - return results; -} - - -function $w(string) { - if (!Object.isString(string)) return []; - string = string.strip(); - return string ? string.split(/\s+/) : []; -} - -Array.from = $A; - - -(function() { - var arrayProto = Array.prototype, - slice = arrayProto.slice, - _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available - - function each(iterator, context) { - for (var i = 0, length = this.length >>> 0; i < length; i++) { - if (i in this) iterator.call(context, this[i], i, this); - } - } - if (!_each) _each = each; - - function clear() { - this.length = 0; - return this; - } - - function first() { - return this[0]; - } - - function last() { - return this[this.length - 1]; - } - - function compact() { - return this.select(function(value) { - return value != null; - }); - } - - function flatten() { - return this.inject([], function(array, value) { - if (Object.isArray(value)) - return array.concat(value.flatten()); - array.push(value); - return array; - }); - } - - function without() { - var values = slice.call(arguments, 0); - return this.select(function(value) { - return !values.include(value); - }); - } - - function reverse(inline) { - return (inline === false ? this.toArray() : this)._reverse(); - } - - function uniq(sorted) { - return this.inject([], function(array, value, index) { - if (0 == index || (sorted ? array.last() != value : !array.include(value))) - array.push(value); - return array; - }); - } - - function intersect(array) { - return this.uniq().findAll(function(item) { - return array.indexOf(item) !== -1; - }); - } - - - function clone() { - return slice.call(this, 0); - } - - function size() { - return this.length; - } - - function inspect() { - return '[' + this.map(Object.inspect).join(', ') + ']'; - } - - function indexOf(item, i) { - if (this == null) throw new TypeError(); - - var array = Object(this), length = array.length >>> 0; - if (length === 0) return -1; - - i = Number(i); - if (isNaN(i)) { - i = 0; - } else if (i !== 0 && isFinite(i)) { - i = (i > 0 ? 1 : -1) * Math.floor(Math.abs(i)); - } - - if (i > length) return -1; - - var k = i >= 0 ? i : Math.max(length - Math.abs(i), 0); - for (; k < length; k++) - if (k in array && array[k] === item) return k; - return -1; - } - - - function lastIndexOf(item, i) { - if (this == null) throw new TypeError(); - - var array = Object(this), length = array.length >>> 0; - if (length === 0) return -1; - - if (!Object.isUndefined(i)) { - i = Number(i); - if (isNaN(i)) { - i = 0; - } else if (i !== 0 && isFinite(i)) { - i = (i > 0 ? 1 : -1) * Math.floor(Math.abs(i)); - } - } else { - i = length; - } - - var k = i >= 0 ? Math.min(i, length - 1) : - length - Math.abs(i); - - for (; k >= 0; k--) - if (k in array && array[k] === item) return k; - return -1; - } - - function concat(_) { - var array = [], items = slice.call(arguments, 0), item, n = 0; - items.unshift(this); - for (var i = 0, length = items.length; i < length; i++) { - item = items[i]; - if (Object.isArray(item) && !('callee' in item)) { - for (var j = 0, arrayLength = item.length; j < arrayLength; j++) { - if (j in item) array[n] = item[j]; - n++; - } - } else { - array[n++] = item; - } - } - array.length = n; - return array; - } - - - function wrapNative(method) { - return function() { - if (arguments.length === 0) { - return method.call(this, Prototype.K); - } else if (arguments[0] === undefined) { - var args = slice.call(arguments, 1); - args.unshift(Prototype.K); - return method.apply(this, args); - } else { - return method.apply(this, arguments); - } - }; - } - - - function map(iterator) { - if (this == null) throw new TypeError(); - iterator = iterator || Prototype.K; - - var object = Object(this); - var results = [], context = arguments[1], n = 0; - - for (var i = 0, length = object.length >>> 0; i < length; i++) { - if (i in object) { - results[n] = iterator.call(context, object[i], i, object); - } - n++; - } - results.length = n; - return results; - } - - if (arrayProto.map) { - map = wrapNative(Array.prototype.map); - } - - function filter(iterator) { - if (this == null || !Object.isFunction(iterator)) - throw new TypeError(); - - var object = Object(this); - var results = [], context = arguments[1], value; - - for (var i = 0, length = object.length >>> 0; i < length; i++) { - if (i in object) { - value = object[i]; - if (iterator.call(context, value, i, object)) { - results.push(value); - } - } - } - return results; - } - - if (arrayProto.filter) { - filter = Array.prototype.filter; - } - - function some(iterator) { - if (this == null) throw new TypeError(); - iterator = iterator || Prototype.K; - var context = arguments[1]; - - var object = Object(this); - for (var i = 0, length = object.length >>> 0; i < length; i++) { - if (i in object && iterator.call(context, object[i], i, object)) { - return true; - } - } - - return false; - } - - if (arrayProto.some) { - some = wrapNative(Array.prototype.some); - } - - function every(iterator) { - if (this == null) throw new TypeError(); - iterator = iterator || Prototype.K; - var context = arguments[1]; - - var object = Object(this); - for (var i = 0, length = object.length >>> 0; i < length; i++) { - if (i in object && !iterator.call(context, object[i], i, object)) { - return false; - } - } - - return true; - } - - if (arrayProto.every) { - every = wrapNative(Array.prototype.every); - } - - - Object.extend(arrayProto, Enumerable); - - if (arrayProto.entries === Enumerable.entries) { - delete arrayProto.entries; - } - - if (!arrayProto._reverse) - arrayProto._reverse = arrayProto.reverse; - - Object.extend(arrayProto, { - _each: _each, - - map: map, - collect: map, - select: filter, - filter: filter, - findAll: filter, - some: some, - any: some, - every: every, - all: every, - - clear: clear, - first: first, - last: last, - compact: compact, - flatten: flatten, - without: without, - reverse: reverse, - uniq: uniq, - intersect: intersect, - clone: clone, - toArray: clone, - size: size, - inspect: inspect - }); - - var CONCAT_ARGUMENTS_BUGGY = (function() { - return [].concat(arguments)[0][0] !== 1; - })(1,2); - - if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat; - - if (!arrayProto.indexOf) arrayProto.indexOf = indexOf; - if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf; -})(); -function $H(object) { - return new Hash(object); -}; - -var Hash = Class.create(Enumerable, (function() { - function initialize(object) { - this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); - } - - - function _each(iterator, context) { - var i = 0; - for (var key in this._object) { - var value = this._object[key], pair = [key, value]; - pair.key = key; - pair.value = value; - iterator.call(context, pair, i); - i++; - } - } - - function set(key, value) { - return this._object[key] = value; - } - - function get(key) { - if (this._object[key] !== Object.prototype[key]) - return this._object[key]; - } - - function unset(key) { - var value = this._object[key]; - delete this._object[key]; - return value; - } - - function toObject() { - return Object.clone(this._object); - } - - - - function keys() { - return this.pluck('key'); - } - - function values() { - return this.pluck('value'); - } - - function index(value) { - var match = this.detect(function(pair) { - return pair.value === value; - }); - return match && match.key; - } - - function merge(object) { - return this.clone().update(object); - } - - function update(object) { - return new Hash(object).inject(this, function(result, pair) { - result.set(pair.key, pair.value); - return result; - }); - } - - function toQueryPair(key, value) { - if (Object.isUndefined(value)) return key; - - value = String.interpret(value); - - value = value.gsub(/(\r)?\n/, '\r\n'); - value = encodeURIComponent(value); - value = value.gsub(/%20/, '+'); - return key + '=' + value; - } - - function toQueryString() { - return this.inject([], function(results, pair) { - var key = encodeURIComponent(pair.key), values = pair.value; - - if (values && typeof values == 'object') { - if (Object.isArray(values)) { - var queryValues = []; - for (var i = 0, len = values.length, value; i < len; i++) { - value = values[i]; - queryValues.push(toQueryPair(key, value)); - } - return results.concat(queryValues); - } - } else results.push(toQueryPair(key, values)); - return results; - }).join('&'); - } - - function inspect() { - return '#'; - } - - function clone() { - return new Hash(this); - } - - return { - initialize: initialize, - _each: _each, - set: set, - get: get, - unset: unset, - toObject: toObject, - toTemplateReplacements: toObject, - keys: keys, - values: values, - index: index, - merge: merge, - update: update, - toQueryString: toQueryString, - inspect: inspect, - toJSON: toObject, - clone: clone - }; -})()); - -Hash.from = $H; -Object.extend(Number.prototype, (function() { - function toColorPart() { - return this.toPaddedString(2, 16); - } - - function succ() { - return this + 1; - } - - function times(iterator, context) { - $R(0, this, true).each(iterator, context); - return this; - } - - function toPaddedString(length, radix) { - var string = this.toString(radix || 10); - return '0'.times(length - string.length) + string; - } - - function abs() { - return Math.abs(this); - } - - function round() { - return Math.round(this); - } - - function ceil() { - return Math.ceil(this); - } - - function floor() { - return Math.floor(this); - } - - return { - toColorPart: toColorPart, - succ: succ, - times: times, - toPaddedString: toPaddedString, - abs: abs, - round: round, - ceil: ceil, - floor: floor - }; -})()); - -function $R(start, end, exclusive) { - return new ObjectRange(start, end, exclusive); -} - -var ObjectRange = Class.create(Enumerable, (function() { - function initialize(start, end, exclusive) { - this.start = start; - this.end = end; - this.exclusive = exclusive; - } - - function _each(iterator, context) { - var value = this.start, i; - for (i = 0; this.include(value); i++) { - iterator.call(context, value, i); - value = value.succ(); - } - } - - function include(value) { - if (value < this.start) - return false; - if (this.exclusive) - return value < this.end; - return value <= this.end; - } - - return { - initialize: initialize, - _each: _each, - include: include - }; -})()); - - - -var Abstract = { }; - - -var Try = { - these: function() { - var returnValue; - - for (var i = 0, length = arguments.length; i < length; i++) { - var lambda = arguments[i]; - try { - returnValue = lambda(); - break; - } catch (e) { } - } - - return returnValue; - } -}; - -var Ajax = { - getTransport: function() { - return Try.these( - function() {return new XMLHttpRequest()}, - function() {return new ActiveXObject('Msxml2.XMLHTTP')}, - function() {return new ActiveXObject('Microsoft.XMLHTTP')} - ) || false; - }, - - activeRequestCount: 0 -}; - -Ajax.Responders = { - responders: [], - - _each: function(iterator, context) { - this.responders._each(iterator, context); - }, - - register: function(responder) { - if (!this.include(responder)) - this.responders.push(responder); - }, - - unregister: function(responder) { - this.responders = this.responders.without(responder); - }, - - dispatch: function(callback, request, transport, json) { - this.each(function(responder) { - if (Object.isFunction(responder[callback])) { - try { - responder[callback].apply(responder, [request, transport, json]); - } catch (e) { } - } - }); - } -}; - -Object.extend(Ajax.Responders, Enumerable); - -Ajax.Responders.register({ - onCreate: function() { Ajax.activeRequestCount++ }, - onComplete: function() { Ajax.activeRequestCount-- } -}); -Ajax.Base = Class.create({ - initialize: function(options) { - this.options = { - method: 'post', - asynchronous: true, - contentType: 'application/x-www-form-urlencoded', - encoding: 'UTF-8', - parameters: '', - evalJSON: true, - evalJS: true - }; - Object.extend(this.options, options || { }); - - this.options.method = this.options.method.toLowerCase(); - - if (Object.isHash(this.options.parameters)) - this.options.parameters = this.options.parameters.toObject(); - } -}); -Ajax.Request = Class.create(Ajax.Base, { - _complete: false, - - initialize: function($super, url, options) { - $super(options); - this.transport = Ajax.getTransport(); - this.request(url); - }, - - request: function(url) { - this.url = url; - this.method = this.options.method; - var params = Object.isString(this.options.parameters) ? - this.options.parameters : - Object.toQueryString(this.options.parameters); - - if (!['get', 'post'].include(this.method)) { - params += (params ? '&' : '') + "_method=" + this.method; - this.method = 'post'; - } - - if (params && this.method === 'get') { - this.url += (this.url.include('?') ? '&' : '?') + params; - } - - this.parameters = params.toQueryParams(); - - try { - var response = new Ajax.Response(this); - if (this.options.onCreate) this.options.onCreate(response); - Ajax.Responders.dispatch('onCreate', this, response); - - this.transport.open(this.method.toUpperCase(), this.url, - this.options.asynchronous); - - if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); - - this.transport.onreadystatechange = this.onStateChange.bind(this); - this.setRequestHeaders(); - - this.body = this.method == 'post' ? (this.options.postBody || params) : null; - this.transport.send(this.body); - - /* Force Firefox to handle ready state 4 for synchronous requests */ - if (!this.options.asynchronous && this.transport.overrideMimeType) - this.onStateChange(); - - } - catch (e) { - this.dispatchException(e); - } - }, - - onStateChange: function() { - var readyState = this.transport.readyState; - if (readyState > 1 && !((readyState == 4) && this._complete)) - this.respondToReadyState(this.transport.readyState); - }, - - setRequestHeaders: function() { - var headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'X-Prototype-Version': Prototype.Version, - 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' - }; - - if (this.method == 'post') { - headers['Content-type'] = this.options.contentType + - (this.options.encoding ? '; charset=' + this.options.encoding : ''); - - /* Force "Connection: close" for older Mozilla browsers to work - * around a bug where XMLHttpRequest sends an incorrect - * Content-length header. See Mozilla Bugzilla #246651. - */ - if (this.transport.overrideMimeType && - (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) - headers['Connection'] = 'close'; - } - - if (typeof this.options.requestHeaders == 'object') { - var extras = this.options.requestHeaders; - - if (Object.isFunction(extras.push)) - for (var i = 0, length = extras.length; i < length; i += 2) - headers[extras[i]] = extras[i+1]; - else - $H(extras).each(function(pair) { headers[pair.key] = pair.value }); - } - - for (var name in headers) - if (headers[name] != null) - this.transport.setRequestHeader(name, headers[name]); - }, - - success: function() { - var status = this.getStatus(); - return !status || (status >= 200 && status < 300) || status == 304; - }, - - getStatus: function() { - try { - if (this.transport.status === 1223) return 204; - return this.transport.status || 0; - } catch (e) { return 0 } - }, - - respondToReadyState: function(readyState) { - var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); - - if (state == 'Complete') { - try { - this._complete = true; - (this.options['on' + response.status] - || this.options['on' + (this.success() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(response, response.headerJSON); - } catch (e) { - this.dispatchException(e); - } - - var contentType = response.getHeader('Content-type'); - if (this.options.evalJS == 'force' - || (this.options.evalJS && this.isSameOrigin() && contentType - && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) - this.evalResponse(); - } - - try { - (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); - Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); - } catch (e) { - this.dispatchException(e); - } - - if (state == 'Complete') { - this.transport.onreadystatechange = Prototype.emptyFunction; - } - }, - - isSameOrigin: function() { - var m = this.url.match(/^\s*https?:\/\/[^\/]*/); - return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ - protocol: location.protocol, - domain: document.domain, - port: location.port ? ':' + location.port : '' - })); - }, - - getHeader: function(name) { - try { - return this.transport.getResponseHeader(name) || null; - } catch (e) { return null; } - }, - - evalResponse: function() { - try { - return eval((this.transport.responseText || '').unfilterJSON()); - } catch (e) { - this.dispatchException(e); - } - }, - - dispatchException: function(exception) { - (this.options.onException || Prototype.emptyFunction)(this, exception); - Ajax.Responders.dispatch('onException', this, exception); - } -}); - -Ajax.Request.Events = - ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; - - - - - - - - -Ajax.Response = Class.create({ - initialize: function(request){ - this.request = request; - var transport = this.transport = request.transport, - readyState = this.readyState = transport.readyState; - - if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { - this.status = this.getStatus(); - this.statusText = this.getStatusText(); - this.responseText = String.interpret(transport.responseText); - this.headerJSON = this._getHeaderJSON(); - } - - if (readyState == 4) { - var xml = transport.responseXML; - this.responseXML = Object.isUndefined(xml) ? null : xml; - this.responseJSON = this._getResponseJSON(); - } - }, - - status: 0, - - statusText: '', - - getStatus: Ajax.Request.prototype.getStatus, - - getStatusText: function() { - try { - return this.transport.statusText || ''; - } catch (e) { return '' } - }, - - getHeader: Ajax.Request.prototype.getHeader, - - getAllHeaders: function() { - try { - return this.getAllResponseHeaders(); - } catch (e) { return null } - }, - - getResponseHeader: function(name) { - return this.transport.getResponseHeader(name); - }, - - getAllResponseHeaders: function() { - return this.transport.getAllResponseHeaders(); - }, - - _getHeaderJSON: function() { - var json = this.getHeader('X-JSON'); - if (!json) return null; - - try { - json = decodeURIComponent(escape(json)); - } catch(e) { - } - - try { - return json.evalJSON(this.request.options.sanitizeJSON || - !this.request.isSameOrigin()); - } catch (e) { - this.request.dispatchException(e); - } - }, - - _getResponseJSON: function() { - var options = this.request.options; - if (!options.evalJSON || (options.evalJSON != 'force' && - !(this.getHeader('Content-type') || '').include('application/json')) || - this.responseText.blank()) - return null; - try { - return this.responseText.evalJSON(options.sanitizeJSON || - !this.request.isSameOrigin()); - } catch (e) { - this.request.dispatchException(e); - } - } -}); - -Ajax.Updater = Class.create(Ajax.Request, { - initialize: function($super, container, url, options) { - this.container = { - success: (container.success || container), - failure: (container.failure || (container.success ? null : container)) - }; - - options = Object.clone(options); - var onComplete = options.onComplete; - options.onComplete = (function(response, json) { - this.updateContent(response.responseText); - if (Object.isFunction(onComplete)) onComplete(response, json); - }).bind(this); - - $super(url, options); - }, - - updateContent: function(responseText) { - var receiver = this.container[this.success() ? 'success' : 'failure'], - options = this.options; - - if (!options.evalScripts) responseText = responseText.stripScripts(); - - if (receiver = $(receiver)) { - if (options.insertion) { - if (Object.isString(options.insertion)) { - var insertion = { }; insertion[options.insertion] = responseText; - receiver.insert(insertion); - } - else options.insertion(receiver, responseText); - } - else receiver.update(responseText); - } - } -}); - -Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { - initialize: function($super, container, url, options) { - $super(options); - this.onComplete = this.options.onComplete; - - this.frequency = (this.options.frequency || 2); - this.decay = (this.options.decay || 1); - - this.updater = { }; - this.container = container; - this.url = url; - - this.start(); - }, - - start: function() { - this.options.onComplete = this.updateComplete.bind(this); - this.onTimerEvent(); - }, - - stop: function() { - this.updater.options.onComplete = undefined; - clearTimeout(this.timer); - (this.onComplete || Prototype.emptyFunction).apply(this, arguments); - }, - - updateComplete: function(response) { - if (this.options.decay) { - this.decay = (response.responseText == this.lastText ? - this.decay * this.options.decay : 1); - - this.lastText = response.responseText; - } - this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); - }, - - onTimerEvent: function() { - this.updater = new Ajax.Updater(this.container, this.url, this.options); - } -}); - -(function(GLOBAL) { - - var UNDEFINED; - var SLICE = Array.prototype.slice; - - var DIV = document.createElement('div'); - - - function $(element) { - if (arguments.length > 1) { - for (var i = 0, elements = [], length = arguments.length; i < length; i++) - elements.push($(arguments[i])); - return elements; - } - - if (Object.isString(element)) - element = document.getElementById(element); - return Element.extend(element); - } - - GLOBAL.$ = $; - - - if (!GLOBAL.Node) GLOBAL.Node = {}; - - if (!GLOBAL.Node.ELEMENT_NODE) { - Object.extend(GLOBAL.Node, { - ELEMENT_NODE: 1, - ATTRIBUTE_NODE: 2, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - ENTITY_REFERENCE_NODE: 5, - ENTITY_NODE: 6, - PROCESSING_INSTRUCTION_NODE: 7, - COMMENT_NODE: 8, - DOCUMENT_NODE: 9, - DOCUMENT_TYPE_NODE: 10, - DOCUMENT_FRAGMENT_NODE: 11, - NOTATION_NODE: 12 - }); - } - - var ELEMENT_CACHE = {}; - - function shouldUseCreationCache(tagName, attributes) { - if (tagName === 'select') return false; - if ('type' in attributes) return false; - return true; - } - - var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){ - try { - var el = document.createElement(''); - return el.tagName.toLowerCase() === 'input' && el.name === 'x'; - } - catch(err) { - return false; - } - })(); - - - var oldElement = GLOBAL.Element; - function Element(tagName, attributes) { - attributes = attributes || {}; - tagName = tagName.toLowerCase(); - - if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) { - tagName = '<' + tagName + ' name="' + attributes.name + '">'; - delete attributes.name; - return Element.writeAttribute(document.createElement(tagName), attributes); - } - - if (!ELEMENT_CACHE[tagName]) - ELEMENT_CACHE[tagName] = Element.extend(document.createElement(tagName)); - - var node = shouldUseCreationCache(tagName, attributes) ? - ELEMENT_CACHE[tagName].cloneNode(false) : document.createElement(tagName); - - return Element.writeAttribute(node, attributes); - } - - GLOBAL.Element = Element; - - Object.extend(GLOBAL.Element, oldElement || {}); - if (oldElement) GLOBAL.Element.prototype = oldElement.prototype; - - Element.Methods = { ByTag: {}, Simulated: {} }; - - var methods = {}; - - var INSPECT_ATTRIBUTES = { id: 'id', className: 'class' }; - function inspect(element) { - element = $(element); - var result = '<' + element.tagName.toLowerCase(); - - var attribute, value; - for (var property in INSPECT_ATTRIBUTES) { - attribute = INSPECT_ATTRIBUTES[property]; - value = (element[property] || '').toString(); - if (value) result += ' ' + attribute + '=' + value.inspect(true); - } - - return result + '>'; - } - - methods.inspect = inspect; - - - function visible(element) { - return $(element).getStyle('display') !== 'none'; - } - - function toggle(element, bool) { - element = $(element); - if (typeof bool !== 'boolean') - bool = !Element.visible(element); - Element[bool ? 'show' : 'hide'](element); - - return element; - } - - function hide(element) { - element = $(element); - element.style.display = 'none'; - return element; - } - - function show(element) { - element = $(element); - element.style.display = ''; - return element; - } - - - Object.extend(methods, { - visible: visible, - toggle: toggle, - hide: hide, - show: show - }); - - - function remove(element) { - element = $(element); - element.parentNode.removeChild(element); - return element; - } - - var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){ - var el = document.createElement("select"), - isBuggy = true; - el.innerHTML = ""; - if (el.options && el.options[0]) { - isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION"; - } - el = null; - return isBuggy; - })(); - - var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){ - try { - var el = document.createElement("table"); - if (el && el.tBodies) { - el.innerHTML = "test"; - var isBuggy = typeof el.tBodies[0] == "undefined"; - el = null; - return isBuggy; - } - } catch (e) { - return true; - } - })(); - - var LINK_ELEMENT_INNERHTML_BUGGY = (function() { - try { - var el = document.createElement('div'); - el.innerHTML = ""; - var isBuggy = (el.childNodes.length === 0); - el = null; - return isBuggy; - } catch(e) { - return true; - } - })(); - - var ANY_INNERHTML_BUGGY = SELECT_ELEMENT_INNERHTML_BUGGY || - TABLE_ELEMENT_INNERHTML_BUGGY || LINK_ELEMENT_INNERHTML_BUGGY; - - var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { - var s = document.createElement("script"), - isBuggy = false; - try { - s.appendChild(document.createTextNode("")); - isBuggy = !s.firstChild || - s.firstChild && s.firstChild.nodeType !== 3; - } catch (e) { - isBuggy = true; - } - s = null; - return isBuggy; - })(); - - function update(element, content) { - element = $(element); - - var descendants = element.getElementsByTagName('*'), - i = descendants.length; - while (i--) purgeElement(descendants[i]); - - if (content && content.toElement) - content = content.toElement(); - - if (Object.isElement(content)) - return element.update().insert(content); - - - content = Object.toHTML(content); - var tagName = element.tagName.toUpperCase(); - - if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) { - element.text = content; - return element; - } - - if (ANY_INNERHTML_BUGGY) { - if (tagName in INSERTION_TRANSLATIONS.tags) { - while (element.firstChild) - element.removeChild(element.firstChild); - - var nodes = getContentFromAnonymousElement(tagName, content.stripScripts()); - for (var i = 0, node; node = nodes[i]; i++) - element.appendChild(node); - - } else if (LINK_ELEMENT_INNERHTML_BUGGY && Object.isString(content) && content.indexOf(' -1) { - while (element.firstChild) - element.removeChild(element.firstChild); - - var nodes = getContentFromAnonymousElement(tagName, - content.stripScripts(), true); - - for (var i = 0, node; node = nodes[i]; i++) - element.appendChild(node); - } else { - element.innerHTML = content.stripScripts(); - } - } else { - element.innerHTML = content.stripScripts(); - } - - content.evalScripts.bind(content).defer(); - return element; - } - - function replace(element, content) { - element = $(element); - - if (content && content.toElement) { - content = content.toElement(); - } else if (!Object.isElement(content)) { - content = Object.toHTML(content); - var range = element.ownerDocument.createRange(); - range.selectNode(element); - content.evalScripts.bind(content).defer(); - content = range.createContextualFragment(content.stripScripts()); - } - - element.parentNode.replaceChild(content, element); - return element; - } - - var INSERTION_TRANSLATIONS = { - before: function(element, node) { - element.parentNode.insertBefore(node, element); - }, - top: function(element, node) { - element.insertBefore(node, element.firstChild); - }, - bottom: function(element, node) { - element.appendChild(node); - }, - after: function(element, node) { - element.parentNode.insertBefore(node, element.nextSibling); - }, - - tags: { - TABLE: ['', '
      ', 1], - TBODY: ['', '
      ', 2], - TR: ['', '
      ', 3], - TD: ['
      ', '
      ', 4], - SELECT: ['', 1] - } - }; - - var tags = INSERTION_TRANSLATIONS.tags; - - Object.extend(tags, { - THEAD: tags.TBODY, - TFOOT: tags.TBODY, - TH: tags.TD - }); - - function replace_IE(element, content) { - element = $(element); - if (content && content.toElement) - content = content.toElement(); - if (Object.isElement(content)) { - element.parentNode.replaceChild(content, element); - return element; - } - - content = Object.toHTML(content); - var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); - - if (tagName in INSERTION_TRANSLATIONS.tags) { - var nextSibling = Element.next(element); - var fragments = getContentFromAnonymousElement( - tagName, content.stripScripts()); - - parent.removeChild(element); - - var iterator; - if (nextSibling) - iterator = function(node) { parent.insertBefore(node, nextSibling) }; - else - iterator = function(node) { parent.appendChild(node); } - - fragments.each(iterator); - } else { - element.outerHTML = content.stripScripts(); - } - - content.evalScripts.bind(content).defer(); - return element; - } - - if ('outerHTML' in document.documentElement) - replace = replace_IE; - - function isContent(content) { - if (Object.isUndefined(content) || content === null) return false; - - if (Object.isString(content) || Object.isNumber(content)) return true; - if (Object.isElement(content)) return true; - if (content.toElement || content.toHTML) return true; - - return false; - } - - function insertContentAt(element, content, position) { - position = position.toLowerCase(); - var method = INSERTION_TRANSLATIONS[position]; - - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) { - method(element, content); - return element; - } - - content = Object.toHTML(content); - var tagName = ((position === 'before' || position === 'after') ? - element.parentNode : element).tagName.toUpperCase(); - - var childNodes = getContentFromAnonymousElement(tagName, content.stripScripts()); - - if (position === 'top' || position === 'after') childNodes.reverse(); - - for (var i = 0, node; node = childNodes[i]; i++) - method(element, node); - - content.evalScripts.bind(content).defer(); - } - - function insert(element, insertions) { - element = $(element); - - if (isContent(insertions)) - insertions = { bottom: insertions }; - - for (var position in insertions) - insertContentAt(element, insertions[position], position); - - return element; - } - - function wrap(element, wrapper, attributes) { - element = $(element); - - if (Object.isElement(wrapper)) { - $(wrapper).writeAttribute(attributes || {}); - } else if (Object.isString(wrapper)) { - wrapper = new Element(wrapper, attributes); - } else { - wrapper = new Element('div', wrapper); - } - - if (element.parentNode) - element.parentNode.replaceChild(wrapper, element); - - wrapper.appendChild(element); - - return wrapper; - } - - function cleanWhitespace(element) { - element = $(element); - var node = element.firstChild; - - while (node) { - var nextNode = node.nextSibling; - if (node.nodeType === Node.TEXT_NODE && !/\S/.test(node.nodeValue)) - element.removeChild(node); - node = nextNode; - } - return element; - } - - function empty(element) { - return $(element).innerHTML.blank(); - } - - function getContentFromAnonymousElement(tagName, html, force) { - var t = INSERTION_TRANSLATIONS.tags[tagName], div = DIV; - - var workaround = !!t; - if (!workaround && force) { - workaround = true; - t = ['', '', 0]; - } - - if (workaround) { - div.innerHTML = ' ' + t[0] + html + t[1]; - div.removeChild(div.firstChild); - for (var i = t[2]; i--; ) - div = div.firstChild; - } else { - div.innerHTML = html; - } - - return $A(div.childNodes); - } - - function clone(element, deep) { - if (!(element = $(element))) return; - var clone = element.cloneNode(deep); - if (!HAS_UNIQUE_ID_PROPERTY) { - clone._prototypeUID = UNDEFINED; - if (deep) { - var descendants = Element.select(clone, '*'), - i = descendants.length; - while (i--) - descendants[i]._prototypeUID = UNDEFINED; - } - } - return Element.extend(clone); - } - - function purgeElement(element) { - var uid = getUniqueElementID(element); - if (uid) { - Element.stopObserving(element); - if (!HAS_UNIQUE_ID_PROPERTY) - element._prototypeUID = UNDEFINED; - delete Element.Storage[uid]; - } - } - - function purgeCollection(elements) { - var i = elements.length; - while (i--) - purgeElement(elements[i]); - } - - function purgeCollection_IE(elements) { - var i = elements.length, element, uid; - while (i--) { - element = elements[i]; - uid = getUniqueElementID(element); - delete Element.Storage[uid]; - delete Event.cache[uid]; - } - } - - if (HAS_UNIQUE_ID_PROPERTY) { - purgeCollection = purgeCollection_IE; - } - - - function purge(element) { - if (!(element = $(element))) return; - purgeElement(element); - - var descendants = element.getElementsByTagName('*'), - i = descendants.length; - - while (i--) purgeElement(descendants[i]); - - return null; - } - - Object.extend(methods, { - remove: remove, - update: update, - replace: replace, - insert: insert, - wrap: wrap, - cleanWhitespace: cleanWhitespace, - empty: empty, - clone: clone, - purge: purge - }); - - - - function recursivelyCollect(element, property, maximumLength) { - element = $(element); - maximumLength = maximumLength || -1; - var elements = []; - - while (element = element[property]) { - if (element.nodeType === Node.ELEMENT_NODE) - elements.push(Element.extend(element)); - - if (elements.length === maximumLength) break; - } - - return elements; - } - - - function ancestors(element) { - return recursivelyCollect(element, 'parentNode'); - } - - function descendants(element) { - return Element.select(element, '*'); - } - - function firstDescendant(element) { - element = $(element).firstChild; - while (element && element.nodeType !== Node.ELEMENT_NODE) - element = element.nextSibling; - - return $(element); - } - - function immediateDescendants(element) { - var results = [], child = $(element).firstChild; - - while (child) { - if (child.nodeType === Node.ELEMENT_NODE) - results.push(Element.extend(child)); - - child = child.nextSibling; - } - - return results; - } - - function previousSiblings(element) { - return recursivelyCollect(element, 'previousSibling'); - } - - function nextSiblings(element) { - return recursivelyCollect(element, 'nextSibling'); - } - - function siblings(element) { - element = $(element); - var previous = previousSiblings(element), - next = nextSiblings(element); - return previous.reverse().concat(next); - } - - function match(element, selector) { - element = $(element); - - if (Object.isString(selector)) - return Prototype.Selector.match(element, selector); - - return selector.match(element); - } - - - function _recursivelyFind(element, property, expression, index) { - element = $(element), expression = expression || 0, index = index || 0; - if (Object.isNumber(expression)) { - index = expression, expression = null; - } - - while (element = element[property]) { - if (element.nodeType !== 1) continue; - if (expression && !Prototype.Selector.match(element, expression)) - continue; - if (--index >= 0) continue; - - return Element.extend(element); - } - } - - - function up(element, expression, index) { - element = $(element); - - if (arguments.length === 1) return $(element.parentNode); - return _recursivelyFind(element, 'parentNode', expression, index); - } - - function down(element, expression, index) { - if (arguments.length === 1) return firstDescendant(element); - element = $(element), expression = expression || 0, index = index || 0; - - if (Object.isNumber(expression)) - index = expression, expression = '*'; - - var node = Prototype.Selector.select(expression, element)[index]; - return Element.extend(node); - } - - function previous(element, expression, index) { - return _recursivelyFind(element, 'previousSibling', expression, index); - } - - function next(element, expression, index) { - return _recursivelyFind(element, 'nextSibling', expression, index); - } - - function select(element) { - element = $(element); - var expressions = SLICE.call(arguments, 1).join(', '); - return Prototype.Selector.select(expressions, element); - } - - function adjacent(element) { - element = $(element); - var expressions = SLICE.call(arguments, 1).join(', '); - var siblings = Element.siblings(element), results = []; - for (var i = 0, sibling; sibling = siblings[i]; i++) { - if (Prototype.Selector.match(sibling, expressions)) - results.push(sibling); - } - - return results; - } - - function descendantOf_DOM(element, ancestor) { - element = $(element), ancestor = $(ancestor); - if (!element || !ancestor) return false; - while (element = element.parentNode) - if (element === ancestor) return true; - return false; - } - - function descendantOf_contains(element, ancestor) { - element = $(element), ancestor = $(ancestor); - if (!element || !ancestor) return false; - if (!ancestor.contains) return descendantOf_DOM(element, ancestor); - return ancestor.contains(element) && ancestor !== element; - } - - function descendantOf_compareDocumentPosition(element, ancestor) { - element = $(element), ancestor = $(ancestor); - if (!element || !ancestor) return false; - return (element.compareDocumentPosition(ancestor) & 8) === 8; - } - - var descendantOf; - if (DIV.compareDocumentPosition) { - descendantOf = descendantOf_compareDocumentPosition; - } else if (DIV.contains) { - descendantOf = descendantOf_contains; - } else { - descendantOf = descendantOf_DOM; - } - - - Object.extend(methods, { - recursivelyCollect: recursivelyCollect, - ancestors: ancestors, - descendants: descendants, - firstDescendant: firstDescendant, - immediateDescendants: immediateDescendants, - previousSiblings: previousSiblings, - nextSiblings: nextSiblings, - siblings: siblings, - match: match, - up: up, - down: down, - previous: previous, - next: next, - select: select, - adjacent: adjacent, - descendantOf: descendantOf, - - getElementsBySelector: select, - - childElements: immediateDescendants - }); - - - var idCounter = 1; - function identify(element) { - element = $(element); - var id = Element.readAttribute(element, 'id'); - if (id) return id; - - do { id = 'anonymous_element_' + idCounter++ } while ($(id)); - - Element.writeAttribute(element, 'id', id); - return id; - } - - - function readAttribute(element, name) { - return $(element).getAttribute(name); - } - - function readAttribute_IE(element, name) { - element = $(element); - - var table = ATTRIBUTE_TRANSLATIONS.read; - if (table.values[name]) - return table.values[name](element, name); - - if (table.names[name]) name = table.names[name]; - - if (name.include(':')) { - if (!element.attributes || !element.attributes[name]) return null; - return element.attributes[name].value; - } - - return element.getAttribute(name); - } - - function readAttribute_Opera(element, name) { - if (name === 'title') return element.title; - return element.getAttribute(name); - } - - var PROBLEMATIC_ATTRIBUTE_READING = (function() { - DIV.setAttribute('onclick', []); - var value = DIV.getAttribute('onclick'); - var isFunction = Object.isArray(value); - DIV.removeAttribute('onclick'); - return isFunction; - })(); - - if (PROBLEMATIC_ATTRIBUTE_READING) { - readAttribute = readAttribute_IE; - } else if (Prototype.Browser.Opera) { - readAttribute = readAttribute_Opera; - } - - - function writeAttribute(element, name, value) { - element = $(element); - var attributes = {}, table = ATTRIBUTE_TRANSLATIONS.write; - - if (typeof name === 'object') { - attributes = name; - } else { - attributes[name] = Object.isUndefined(value) ? true : value; - } - - for (var attr in attributes) { - name = table.names[attr] || attr; - value = attributes[attr]; - if (table.values[attr]) { - value = table.values[attr](element, value); - if (Object.isUndefined(value)) continue; - } - if (value === false || value === null) - element.removeAttribute(name); - else if (value === true) - element.setAttribute(name, name); - else element.setAttribute(name, value); - } - - return element; - } - - var PROBLEMATIC_HAS_ATTRIBUTE_WITH_CHECKBOXES = (function () { - if (!HAS_EXTENDED_CREATE_ELEMENT_SYNTAX) { - return false; - } - var checkbox = document.createElement(''); - checkbox.checked = true; - var node = checkbox.getAttributeNode('checked'); - return !node || !node.specified; - })(); - - function hasAttribute(element, attribute) { - attribute = ATTRIBUTE_TRANSLATIONS.has[attribute] || attribute; - var node = $(element).getAttributeNode(attribute); - return !!(node && node.specified); - } - - function hasAttribute_IE(element, attribute) { - if (attribute === 'checked') { - return element.checked; - } - return hasAttribute(element, attribute); - } - - GLOBAL.Element.Methods.Simulated.hasAttribute = - PROBLEMATIC_HAS_ATTRIBUTE_WITH_CHECKBOXES ? - hasAttribute_IE : hasAttribute; - - function classNames(element) { - return new Element.ClassNames(element); - } - - var regExpCache = {}; - function getRegExpForClassName(className) { - if (regExpCache[className]) return regExpCache[className]; - - var re = new RegExp("(^|\\s+)" + className + "(\\s+|$)"); - regExpCache[className] = re; - return re; - } - - function hasClassName(element, className) { - if (!(element = $(element))) return; - - var elementClassName = element.className; - - if (elementClassName.length === 0) return false; - if (elementClassName === className) return true; - - return getRegExpForClassName(className).test(elementClassName); - } - - function addClassName(element, className) { - if (!(element = $(element))) return; - - if (!hasClassName(element, className)) - element.className += (element.className ? ' ' : '') + className; - - return element; - } - - function removeClassName(element, className) { - if (!(element = $(element))) return; - - element.className = element.className.replace( - getRegExpForClassName(className), ' ').strip(); - - return element; - } - - function toggleClassName(element, className, bool) { - if (!(element = $(element))) return; - - if (Object.isUndefined(bool)) - bool = !hasClassName(element, className); - - var method = Element[bool ? 'addClassName' : 'removeClassName']; - return method(element, className); - } - - var ATTRIBUTE_TRANSLATIONS = {}; - - var classProp = 'className', forProp = 'for'; - - DIV.setAttribute(classProp, 'x'); - if (DIV.className !== 'x') { - DIV.setAttribute('class', 'x'); - if (DIV.className === 'x') - classProp = 'class'; - } - - var LABEL = document.createElement('label'); - LABEL.setAttribute(forProp, 'x'); - if (LABEL.htmlFor !== 'x') { - LABEL.setAttribute('htmlFor', 'x'); - if (LABEL.htmlFor === 'x') - forProp = 'htmlFor'; - } - LABEL = null; - - function _getAttr(element, attribute) { - return element.getAttribute(attribute); - } - - function _getAttr2(element, attribute) { - return element.getAttribute(attribute, 2); - } - - function _getAttrNode(element, attribute) { - var node = element.getAttributeNode(attribute); - return node ? node.value : ''; - } - - function _getFlag(element, attribute) { - return $(element).hasAttribute(attribute) ? attribute : null; - } - - DIV.onclick = Prototype.emptyFunction; - var onclickValue = DIV.getAttribute('onclick'); - - var _getEv; - - if (String(onclickValue).indexOf('{') > -1) { - _getEv = function(element, attribute) { - var value = element.getAttribute(attribute); - if (!value) return null; - value = value.toString(); - value = value.split('{')[1]; - value = value.split('}')[0]; - return value.strip(); - }; - } - else if (onclickValue === '') { - _getEv = function(element, attribute) { - var value = element.getAttribute(attribute); - if (!value) return null; - return value.strip(); - }; - } - - ATTRIBUTE_TRANSLATIONS.read = { - names: { - 'class': classProp, - 'className': classProp, - 'for': forProp, - 'htmlFor': forProp - }, - - values: { - style: function(element) { - return element.style.cssText.toLowerCase(); - }, - title: function(element) { - return element.title; - } - } - }; - - ATTRIBUTE_TRANSLATIONS.write = { - names: { - className: 'class', - htmlFor: 'for', - cellpadding: 'cellPadding', - cellspacing: 'cellSpacing' - }, - - values: { - checked: function(element, value) { - value = !!value; - element.checked = value; - return value ? 'checked' : null; - }, - - style: function(element, value) { - element.style.cssText = value ? value : ''; - } - } - }; - - ATTRIBUTE_TRANSLATIONS.has = { names: {} }; - - Object.extend(ATTRIBUTE_TRANSLATIONS.write.names, - ATTRIBUTE_TRANSLATIONS.read.names); - - var CAMEL_CASED_ATTRIBUTE_NAMES = $w('colSpan rowSpan vAlign dateTime ' + - 'accessKey tabIndex encType maxLength readOnly longDesc frameBorder'); - - for (var i = 0, attr; attr = CAMEL_CASED_ATTRIBUTE_NAMES[i]; i++) { - ATTRIBUTE_TRANSLATIONS.write.names[attr.toLowerCase()] = attr; - ATTRIBUTE_TRANSLATIONS.has.names[attr.toLowerCase()] = attr; - } - - Object.extend(ATTRIBUTE_TRANSLATIONS.read.values, { - href: _getAttr2, - src: _getAttr2, - type: _getAttr, - action: _getAttrNode, - disabled: _getFlag, - checked: _getFlag, - readonly: _getFlag, - multiple: _getFlag, - onload: _getEv, - onunload: _getEv, - onclick: _getEv, - ondblclick: _getEv, - onmousedown: _getEv, - onmouseup: _getEv, - onmouseover: _getEv, - onmousemove: _getEv, - onmouseout: _getEv, - onfocus: _getEv, - onblur: _getEv, - onkeypress: _getEv, - onkeydown: _getEv, - onkeyup: _getEv, - onsubmit: _getEv, - onreset: _getEv, - onselect: _getEv, - onchange: _getEv - }); - - - Object.extend(methods, { - identify: identify, - readAttribute: readAttribute, - writeAttribute: writeAttribute, - classNames: classNames, - hasClassName: hasClassName, - addClassName: addClassName, - removeClassName: removeClassName, - toggleClassName: toggleClassName - }); - - - function normalizeStyleName(style) { - if (style === 'float' || style === 'styleFloat') - return 'cssFloat'; - return style.camelize(); - } - - function normalizeStyleName_IE(style) { - if (style === 'float' || style === 'cssFloat') - return 'styleFloat'; - return style.camelize(); - } - - function setStyle(element, styles) { - element = $(element); - var elementStyle = element.style, match; - - if (Object.isString(styles)) { - elementStyle.cssText += ';' + styles; - if (styles.include('opacity')) { - var opacity = styles.match(/opacity:\s*(\d?\.?\d*)/)[1]; - Element.setOpacity(element, opacity); - } - return element; - } - - for (var property in styles) { - if (property === 'opacity') { - Element.setOpacity(element, styles[property]); - } else { - var value = styles[property]; - if (property === 'float' || property === 'cssFloat') { - property = Object.isUndefined(elementStyle.styleFloat) ? - 'cssFloat' : 'styleFloat'; - } - elementStyle[property] = value; - } - } - - return element; - } - - - function getStyle(element, style) { - element = $(element); - style = normalizeStyleName(style); - - var value = element.style[style]; - if (!value || value === 'auto') { - var css = document.defaultView.getComputedStyle(element, null); - value = css ? css[style] : null; - } - - if (style === 'opacity') return value ? parseFloat(value) : 1.0; - return value === 'auto' ? null : value; - } - - function getStyle_Opera(element, style) { - switch (style) { - case 'height': case 'width': - if (!Element.visible(element)) return null; - - var dim = parseInt(getStyle(element, style), 10); - - if (dim !== element['offset' + style.capitalize()]) - return dim + 'px'; - - return Element.measure(element, style); - - default: return getStyle(element, style); - } - } - - function getStyle_IE(element, style) { - element = $(element); - style = normalizeStyleName_IE(style); - - var value = element.style[style]; - if (!value && element.currentStyle) { - value = element.currentStyle[style]; - } - - if (style === 'opacity') { - if (!STANDARD_CSS_OPACITY_SUPPORTED) - return getOpacity_IE(element); - else return value ? parseFloat(value) : 1.0; - } - - if (value === 'auto') { - if ((style === 'width' || style === 'height') && Element.visible(element)) - return Element.measure(element, style) + 'px'; - return null; - } - - return value; - } - - function stripAlphaFromFilter_IE(filter) { - return (filter || '').replace(/alpha\([^\)]*\)/gi, ''); - } - - function hasLayout_IE(element) { - if (!element.currentStyle || !element.currentStyle.hasLayout) - element.style.zoom = 1; - return element; - } - - var STANDARD_CSS_OPACITY_SUPPORTED = (function() { - DIV.style.cssText = "opacity:.55"; - return /^0.55/.test(DIV.style.opacity); - })(); - - function setOpacity(element, value) { - element = $(element); - if (value == 1 || value === '') value = ''; - else if (value < 0.00001) value = 0; - element.style.opacity = value; - return element; - } - - function setOpacity_IE(element, value) { - if (STANDARD_CSS_OPACITY_SUPPORTED) - return setOpacity(element, value); - - element = hasLayout_IE($(element)); - var filter = Element.getStyle(element, 'filter'), - style = element.style; - - if (value == 1 || value === '') { - filter = stripAlphaFromFilter_IE(filter); - if (filter) style.filter = filter; - else style.removeAttribute('filter'); - return element; - } - - if (value < 0.00001) value = 0; - - style.filter = stripAlphaFromFilter_IE(filter) + - ' alpha(opacity=' + (value * 100) + ')'; - - return element; - } - - - function getOpacity(element) { - return Element.getStyle(element, 'opacity'); - } - - function getOpacity_IE(element) { - if (STANDARD_CSS_OPACITY_SUPPORTED) - return getOpacity(element); - - var filter = Element.getStyle(element, 'filter'); - if (filter.length === 0) return 1.0; - var match = (filter || '').match(/alpha\(opacity=(.*)\)/i); - if (match && match[1]) return parseFloat(match[1]) / 100; - return 1.0; - } - - - Object.extend(methods, { - setStyle: setStyle, - getStyle: getStyle, - setOpacity: setOpacity, - getOpacity: getOpacity - }); - - if ('styleFloat' in DIV.style) { - methods.getStyle = getStyle_IE; - methods.setOpacity = setOpacity_IE; - methods.getOpacity = getOpacity_IE; - } - - var UID = 0; - - GLOBAL.Element.Storage = { UID: 1 }; - - function getUniqueElementID(element) { - if (element === window) return 0; - - if (typeof element._prototypeUID === 'undefined') - element._prototypeUID = Element.Storage.UID++; - return element._prototypeUID; - } - - function getUniqueElementID_IE(element) { - if (element === window) return 0; - if (element == document) return 1; - return element.uniqueID; - } - - var HAS_UNIQUE_ID_PROPERTY = ('uniqueID' in DIV); - if (HAS_UNIQUE_ID_PROPERTY) - getUniqueElementID = getUniqueElementID_IE; - - function getStorage(element) { - if (!(element = $(element))) return; - - var uid = getUniqueElementID(element); - - if (!Element.Storage[uid]) - Element.Storage[uid] = $H(); - - return Element.Storage[uid]; - } - - function store(element, key, value) { - if (!(element = $(element))) return; - var storage = getStorage(element); - if (arguments.length === 2) { - storage.update(key); - } else { - storage.set(key, value); - } - return element; - } - - function retrieve(element, key, defaultValue) { - if (!(element = $(element))) return; - var storage = getStorage(element), value = storage.get(key); - - if (Object.isUndefined(value)) { - storage.set(key, defaultValue); - value = defaultValue; - } - - return value; - } - - - Object.extend(methods, { - getStorage: getStorage, - store: store, - retrieve: retrieve - }); - - - var Methods = {}, ByTag = Element.Methods.ByTag, - F = Prototype.BrowserFeatures; - - if (!F.ElementExtensions && ('__proto__' in DIV)) { - GLOBAL.HTMLElement = {}; - GLOBAL.HTMLElement.prototype = DIV['__proto__']; - F.ElementExtensions = true; - } - - function checkElementPrototypeDeficiency(tagName) { - if (typeof window.Element === 'undefined') return false; - if (!HAS_EXTENDED_CREATE_ELEMENT_SYNTAX) return false; - var proto = window.Element.prototype; - if (proto) { - var id = '_' + (Math.random() + '').slice(2), - el = document.createElement(tagName); - proto[id] = 'x'; - var isBuggy = (el[id] !== 'x'); - delete proto[id]; - el = null; - return isBuggy; - } - - return false; - } - - var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = - checkElementPrototypeDeficiency('object'); - - function extendElementWith(element, methods) { - for (var property in methods) { - var value = methods[property]; - if (Object.isFunction(value) && !(property in element)) - element[property] = value.methodize(); - } - } - - var EXTENDED = {}; - function elementIsExtended(element) { - var uid = getUniqueElementID(element); - return (uid in EXTENDED); - } - - function extend(element) { - if (!element || elementIsExtended(element)) return element; - if (element.nodeType !== Node.ELEMENT_NODE || element == window) - return element; - - var methods = Object.clone(Methods), - tagName = element.tagName.toUpperCase(); - - if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); - - extendElementWith(element, methods); - EXTENDED[getUniqueElementID(element)] = true; - return element; - } - - function extend_IE8(element) { - if (!element || elementIsExtended(element)) return element; - - var t = element.tagName; - if (t && (/^(?:object|applet|embed)$/i.test(t))) { - extendElementWith(element, Element.Methods); - extendElementWith(element, Element.Methods.Simulated); - extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]); - } - - return element; - } - - if (F.SpecificElementExtensions) { - extend = HTMLOBJECTELEMENT_PROTOTYPE_BUGGY ? extend_IE8 : Prototype.K; - } - - function addMethodsToTagName(tagName, methods) { - tagName = tagName.toUpperCase(); - if (!ByTag[tagName]) ByTag[tagName] = {}; - Object.extend(ByTag[tagName], methods); - } - - function mergeMethods(destination, methods, onlyIfAbsent) { - if (Object.isUndefined(onlyIfAbsent)) onlyIfAbsent = false; - for (var property in methods) { - var value = methods[property]; - if (!Object.isFunction(value)) continue; - if (!onlyIfAbsent || !(property in destination)) - destination[property] = value.methodize(); - } - } - - function findDOMClass(tagName) { - var klass; - var trans = { - "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", - "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", - "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", - "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", - "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": - "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": - "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": - "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": - "FrameSet", "IFRAME": "IFrame" - }; - if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; - if (window[klass]) return window[klass]; - klass = 'HTML' + tagName + 'Element'; - if (window[klass]) return window[klass]; - klass = 'HTML' + tagName.capitalize() + 'Element'; - if (window[klass]) return window[klass]; - - var element = document.createElement(tagName), - proto = element['__proto__'] || element.constructor.prototype; - - element = null; - return proto; - } - - function addMethods(methods) { - if (arguments.length === 0) addFormMethods(); - - if (arguments.length === 2) { - var tagName = methods; - methods = arguments[1]; - } - - if (!tagName) { - Object.extend(Element.Methods, methods || {}); - } else { - if (Object.isArray(tagName)) { - for (var i = 0, tag; tag = tagName[i]; i++) - addMethodsToTagName(tag, methods); - } else { - addMethodsToTagName(tagName, methods); - } - } - - var ELEMENT_PROTOTYPE = window.HTMLElement ? HTMLElement.prototype : - Element.prototype; - - if (F.ElementExtensions) { - mergeMethods(ELEMENT_PROTOTYPE, Element.Methods); - mergeMethods(ELEMENT_PROTOTYPE, Element.Methods.Simulated, true); - } - - if (F.SpecificElementExtensions) { - for (var tag in Element.Methods.ByTag) { - var klass = findDOMClass(tag); - if (Object.isUndefined(klass)) continue; - mergeMethods(klass.prototype, ByTag[tag]); - } - } - - Object.extend(Element, Element.Methods); - Object.extend(Element, Element.Methods.Simulated); - delete Element.ByTag; - delete Element.Simulated; - - Element.extend.refresh(); - - ELEMENT_CACHE = {}; - } - - Object.extend(GLOBAL.Element, { - extend: extend, - addMethods: addMethods - }); - - if (extend === Prototype.K) { - GLOBAL.Element.extend.refresh = Prototype.emptyFunction; - } else { - GLOBAL.Element.extend.refresh = function() { - if (Prototype.BrowserFeatures.ElementExtensions) return; - Object.extend(Methods, Element.Methods); - Object.extend(Methods, Element.Methods.Simulated); - - EXTENDED = {}; - }; - } - - function addFormMethods() { - Object.extend(Form, Form.Methods); - Object.extend(Form.Element, Form.Element.Methods); - Object.extend(Element.Methods.ByTag, { - "FORM": Object.clone(Form.Methods), - "INPUT": Object.clone(Form.Element.Methods), - "SELECT": Object.clone(Form.Element.Methods), - "TEXTAREA": Object.clone(Form.Element.Methods), - "BUTTON": Object.clone(Form.Element.Methods) - }); - } - - Element.addMethods(methods); - - function destroyCache_IE() { - DIV = null; - ELEMENT_CACHE = null; - } - - if (window.attachEvent) - window.attachEvent('onunload', destroyCache_IE); - -})(this); -(function() { - - function toDecimal(pctString) { - var match = pctString.match(/^(\d+)%?$/i); - if (!match) return null; - return (Number(match[1]) / 100); - } - - function getRawStyle(element, style) { - element = $(element); - - var value = element.style[style]; - if (!value || value === 'auto') { - var css = document.defaultView.getComputedStyle(element, null); - value = css ? css[style] : null; - } - - if (style === 'opacity') return value ? parseFloat(value) : 1.0; - return value === 'auto' ? null : value; - } - - function getRawStyle_IE(element, style) { - var value = element.style[style]; - if (!value && element.currentStyle) { - value = element.currentStyle[style]; - } - return value; - } - - function getContentWidth(element, context) { - var boxWidth = element.offsetWidth; - - var bl = getPixelValue(element, 'borderLeftWidth', context) || 0; - var br = getPixelValue(element, 'borderRightWidth', context) || 0; - var pl = getPixelValue(element, 'paddingLeft', context) || 0; - var pr = getPixelValue(element, 'paddingRight', context) || 0; - - return boxWidth - bl - br - pl - pr; - } - - if (!Object.isUndefined(document.documentElement.currentStyle) && !Prototype.Browser.Opera) { - getRawStyle = getRawStyle_IE; - } - - - function getPixelValue(value, property, context) { - var element = null; - if (Object.isElement(value)) { - element = value; - value = getRawStyle(element, property); - } - - if (value === null || Object.isUndefined(value)) { - return null; - } - - if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) { - return window.parseFloat(value); - } - - var isPercentage = value.include('%'), isViewport = (context === document.viewport); - - if (/\d/.test(value) && element && element.runtimeStyle && !(isPercentage && isViewport)) { - var style = element.style.left, rStyle = element.runtimeStyle.left; - element.runtimeStyle.left = element.currentStyle.left; - element.style.left = value || 0; - value = element.style.pixelLeft; - element.style.left = style; - element.runtimeStyle.left = rStyle; - - return value; - } - - if (element && isPercentage) { - context = context || element.parentNode; - var decimal = toDecimal(value), whole = null; - - var isHorizontal = property.include('left') || property.include('right') || - property.include('width'); - - var isVertical = property.include('top') || property.include('bottom') || - property.include('height'); - - if (context === document.viewport) { - if (isHorizontal) { - whole = document.viewport.getWidth(); - } else if (isVertical) { - whole = document.viewport.getHeight(); - } - } else { - if (isHorizontal) { - whole = $(context).measure('width'); - } else if (isVertical) { - whole = $(context).measure('height'); - } - } - - return (whole === null) ? 0 : whole * decimal; - } - - return 0; - } - - function toCSSPixels(number) { - if (Object.isString(number) && number.endsWith('px')) - return number; - return number + 'px'; - } - - function isDisplayed(element) { - while (element && element.parentNode) { - var display = element.getStyle('display'); - if (display === 'none') { - return false; - } - element = $(element.parentNode); - } - return true; - } - - var hasLayout = Prototype.K; - if ('currentStyle' in document.documentElement) { - hasLayout = function(element) { - if (!element.currentStyle.hasLayout) { - element.style.zoom = 1; - } - return element; - }; - } - - function cssNameFor(key) { - if (key.include('border')) key = key + '-width'; - return key.camelize(); - } - - Element.Layout = Class.create(Hash, { - initialize: function($super, element, preCompute) { - $super(); - this.element = $(element); - - Element.Layout.PROPERTIES.each( function(property) { - this._set(property, null); - }, this); - - if (preCompute) { - this._preComputing = true; - this._begin(); - Element.Layout.PROPERTIES.each( this._compute, this ); - this._end(); - this._preComputing = false; - } - }, - - _set: function(property, value) { - return Hash.prototype.set.call(this, property, value); - }, - - set: function(property, value) { - throw "Properties of Element.Layout are read-only."; - }, - - get: function($super, property) { - var value = $super(property); - return value === null ? this._compute(property) : value; - }, - - _begin: function() { - if (this._isPrepared()) return; - - var element = this.element; - if (isDisplayed(element)) { - this._setPrepared(true); - return; - } - - - var originalStyles = { - position: element.style.position || '', - width: element.style.width || '', - visibility: element.style.visibility || '', - display: element.style.display || '' - }; - - element.store('prototype_original_styles', originalStyles); - - var position = getRawStyle(element, 'position'), width = element.offsetWidth; - - if (width === 0 || width === null) { - element.style.display = 'block'; - width = element.offsetWidth; - } - - var context = (position === 'fixed') ? document.viewport : - element.parentNode; - - var tempStyles = { - visibility: 'hidden', - display: 'block' - }; - - if (position !== 'fixed') tempStyles.position = 'absolute'; - - element.setStyle(tempStyles); - - var positionedWidth = element.offsetWidth, newWidth; - if (width && (positionedWidth === width)) { - newWidth = getContentWidth(element, context); - } else if (position === 'absolute' || position === 'fixed') { - newWidth = getContentWidth(element, context); - } else { - var parent = element.parentNode, pLayout = $(parent).getLayout(); - - newWidth = pLayout.get('width') - - this.get('margin-left') - - this.get('border-left') - - this.get('padding-left') - - this.get('padding-right') - - this.get('border-right') - - this.get('margin-right'); - } - - element.setStyle({ width: newWidth + 'px' }); - - this._setPrepared(true); - }, - - _end: function() { - var element = this.element; - var originalStyles = element.retrieve('prototype_original_styles'); - element.store('prototype_original_styles', null); - element.setStyle(originalStyles); - this._setPrepared(false); - }, - - _compute: function(property) { - var COMPUTATIONS = Element.Layout.COMPUTATIONS; - if (!(property in COMPUTATIONS)) { - throw "Property not found."; - } - - return this._set(property, COMPUTATIONS[property].call(this, this.element)); - }, - - _isPrepared: function() { - return this.element.retrieve('prototype_element_layout_prepared', false); - }, - - _setPrepared: function(bool) { - return this.element.store('prototype_element_layout_prepared', bool); - }, - - toObject: function() { - var args = $A(arguments); - var keys = (args.length === 0) ? Element.Layout.PROPERTIES : - args.join(' ').split(' '); - var obj = {}; - keys.each( function(key) { - if (!Element.Layout.PROPERTIES.include(key)) return; - var value = this.get(key); - if (value != null) obj[key] = value; - }, this); - return obj; - }, - - toHash: function() { - var obj = this.toObject.apply(this, arguments); - return new Hash(obj); - }, - - toCSS: function() { - var args = $A(arguments); - var keys = (args.length === 0) ? Element.Layout.PROPERTIES : - args.join(' ').split(' '); - var css = {}; - - keys.each( function(key) { - if (!Element.Layout.PROPERTIES.include(key)) return; - if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; - - var value = this.get(key); - if (value != null) css[cssNameFor(key)] = value + 'px'; - }, this); - return css; - }, - - inspect: function() { - return "#"; - } - }); - - Object.extend(Element.Layout, { - PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), - - COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), - - COMPUTATIONS: { - 'height': function(element) { - if (!this._preComputing) this._begin(); - - var bHeight = this.get('border-box-height'); - if (bHeight <= 0) { - if (!this._preComputing) this._end(); - return 0; - } - - var bTop = this.get('border-top'), - bBottom = this.get('border-bottom'); - - var pTop = this.get('padding-top'), - pBottom = this.get('padding-bottom'); - - if (!this._preComputing) this._end(); - - return bHeight - bTop - bBottom - pTop - pBottom; - }, - - 'width': function(element) { - if (!this._preComputing) this._begin(); - - var bWidth = this.get('border-box-width'); - if (bWidth <= 0) { - if (!this._preComputing) this._end(); - return 0; - } - - var bLeft = this.get('border-left'), - bRight = this.get('border-right'); - - var pLeft = this.get('padding-left'), - pRight = this.get('padding-right'); - - if (!this._preComputing) this._end(); - return bWidth - bLeft - bRight - pLeft - pRight; - }, - - 'padding-box-height': function(element) { - var height = this.get('height'), - pTop = this.get('padding-top'), - pBottom = this.get('padding-bottom'); - - return height + pTop + pBottom; - }, - - 'padding-box-width': function(element) { - var width = this.get('width'), - pLeft = this.get('padding-left'), - pRight = this.get('padding-right'); - - return width + pLeft + pRight; - }, - - 'border-box-height': function(element) { - if (!this._preComputing) this._begin(); - var height = element.offsetHeight; - if (!this._preComputing) this._end(); - return height; - }, - - 'border-box-width': function(element) { - if (!this._preComputing) this._begin(); - var width = element.offsetWidth; - if (!this._preComputing) this._end(); - return width; - }, - - 'margin-box-height': function(element) { - var bHeight = this.get('border-box-height'), - mTop = this.get('margin-top'), - mBottom = this.get('margin-bottom'); - - if (bHeight <= 0) return 0; - - return bHeight + mTop + mBottom; - }, - - 'margin-box-width': function(element) { - var bWidth = this.get('border-box-width'), - mLeft = this.get('margin-left'), - mRight = this.get('margin-right'); - - if (bWidth <= 0) return 0; - - return bWidth + mLeft + mRight; - }, - - 'top': function(element) { - var offset = element.positionedOffset(); - return offset.top; - }, - - 'bottom': function(element) { - var offset = element.positionedOffset(), - parent = element.getOffsetParent(), - pHeight = parent.measure('height'); - - var mHeight = this.get('border-box-height'); - - return pHeight - mHeight - offset.top; - }, - - 'left': function(element) { - var offset = element.positionedOffset(); - return offset.left; - }, - - 'right': function(element) { - var offset = element.positionedOffset(), - parent = element.getOffsetParent(), - pWidth = parent.measure('width'); - - var mWidth = this.get('border-box-width'); - - return pWidth - mWidth - offset.left; - }, - - 'padding-top': function(element) { - return getPixelValue(element, 'paddingTop'); - }, - - 'padding-bottom': function(element) { - return getPixelValue(element, 'paddingBottom'); - }, - - 'padding-left': function(element) { - return getPixelValue(element, 'paddingLeft'); - }, - - 'padding-right': function(element) { - return getPixelValue(element, 'paddingRight'); - }, - - 'border-top': function(element) { - return getPixelValue(element, 'borderTopWidth'); - }, - - 'border-bottom': function(element) { - return getPixelValue(element, 'borderBottomWidth'); - }, - - 'border-left': function(element) { - return getPixelValue(element, 'borderLeftWidth'); - }, - - 'border-right': function(element) { - return getPixelValue(element, 'borderRightWidth'); - }, - - 'margin-top': function(element) { - return getPixelValue(element, 'marginTop'); - }, - - 'margin-bottom': function(element) { - return getPixelValue(element, 'marginBottom'); - }, - - 'margin-left': function(element) { - return getPixelValue(element, 'marginLeft'); - }, - - 'margin-right': function(element) { - return getPixelValue(element, 'marginRight'); - } - } - }); - - if ('getBoundingClientRect' in document.documentElement) { - Object.extend(Element.Layout.COMPUTATIONS, { - 'right': function(element) { - var parent = hasLayout(element.getOffsetParent()); - var rect = element.getBoundingClientRect(), - pRect = parent.getBoundingClientRect(); - - return (pRect.right - rect.right).round(); - }, - - 'bottom': function(element) { - var parent = hasLayout(element.getOffsetParent()); - var rect = element.getBoundingClientRect(), - pRect = parent.getBoundingClientRect(); - - return (pRect.bottom - rect.bottom).round(); - } - }); - } - - Element.Offset = Class.create({ - initialize: function(left, top) { - this.left = left.round(); - this.top = top.round(); - - this[0] = this.left; - this[1] = this.top; - }, - - relativeTo: function(offset) { - return new Element.Offset( - this.left - offset.left, - this.top - offset.top - ); - }, - - inspect: function() { - return "#".interpolate(this); - }, - - toString: function() { - return "[#{left}, #{top}]".interpolate(this); - }, - - toArray: function() { - return [this.left, this.top]; - } - }); - - function getLayout(element, preCompute) { - return new Element.Layout(element, preCompute); - } - - function measure(element, property) { - return $(element).getLayout().get(property); - } - - function getHeight(element) { - return Element.getDimensions(element).height; - } - - function getWidth(element) { - return Element.getDimensions(element).width; - } - - function getDimensions(element) { - element = $(element); - var display = Element.getStyle(element, 'display'); - - if (display && display !== 'none') { - return { width: element.offsetWidth, height: element.offsetHeight }; - } - - var style = element.style; - var originalStyles = { - visibility: style.visibility, - position: style.position, - display: style.display - }; - - var newStyles = { - visibility: 'hidden', - display: 'block' - }; - - if (originalStyles.position !== 'fixed') - newStyles.position = 'absolute'; - - Element.setStyle(element, newStyles); - - var dimensions = { - width: element.offsetWidth, - height: element.offsetHeight - }; - - Element.setStyle(element, originalStyles); - - return dimensions; - } - - function getOffsetParent(element) { - element = $(element); - - function selfOrBody(element) { - return isHtml(element) ? $(document.body) : $(element); - } - - if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) - return $(document.body); - - var isInline = (Element.getStyle(element, 'display') === 'inline'); - if (!isInline && element.offsetParent) return selfOrBody(element.offsetParent); - - while ((element = element.parentNode) && element !== document.body) { - if (Element.getStyle(element, 'position') !== 'static') { - return selfOrBody(element); - } - } - - return $(document.body); - } - - - function cumulativeOffset(element) { - element = $(element); - var valueT = 0, valueL = 0; - if (element.parentNode) { - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - } - return new Element.Offset(valueL, valueT); - } - - function positionedOffset(element) { - element = $(element); - - var layout = element.getLayout(); - - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if (isBody(element)) break; - var p = Element.getStyle(element, 'position'); - if (p !== 'static') break; - } - } while (element); - - valueL -= layout.get('margin-left'); - valueT -= layout.get('margin-top'); - - return new Element.Offset(valueL, valueT); - } - - function cumulativeScrollOffset(element) { - var valueT = 0, valueL = 0; - do { - if (element === document.body) { - var bodyScrollNode = document.documentElement || document.body.parentNode || document.body; - valueT += !Object.isUndefined(window.pageYOffset) ? window.pageYOffset : bodyScrollNode.scrollTop || 0; - valueL += !Object.isUndefined(window.pageXOffset) ? window.pageXOffset : bodyScrollNode.scrollLeft || 0; - break; - } else { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } - } while (element); - return new Element.Offset(valueL, valueT); - } - - function viewportOffset(forElement) { - var valueT = 0, valueL = 0, docBody = document.body; - - forElement = $(forElement); - var element = forElement; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == docBody && - Element.getStyle(element, 'position') == 'absolute') break; - } while (element = element.offsetParent); - - element = forElement; - do { - if (element != docBody) { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); - return new Element.Offset(valueL, valueT); - } - - function absolutize(element) { - element = $(element); - - if (Element.getStyle(element, 'position') === 'absolute') { - return element; - } - - var offsetParent = getOffsetParent(element); - var eOffset = element.viewportOffset(), - pOffset = offsetParent.viewportOffset(); - - var offset = eOffset.relativeTo(pOffset); - var layout = element.getLayout(); - - element.store('prototype_absolutize_original_styles', { - position: element.getStyle('position'), - left: element.getStyle('left'), - top: element.getStyle('top'), - width: element.getStyle('width'), - height: element.getStyle('height') - }); - - element.setStyle({ - position: 'absolute', - top: offset.top + 'px', - left: offset.left + 'px', - width: layout.get('width') + 'px', - height: layout.get('height') + 'px' - }); - - return element; - } - - function relativize(element) { - element = $(element); - if (Element.getStyle(element, 'position') === 'relative') { - return element; - } - - var originalStyles = - element.retrieve('prototype_absolutize_original_styles'); - - if (originalStyles) element.setStyle(originalStyles); - return element; - } - - - function scrollTo(element) { - element = $(element); - var pos = Element.cumulativeOffset(element); - window.scrollTo(pos.left, pos.top); - return element; - } - - - function makePositioned(element) { - element = $(element); - var position = Element.getStyle(element, 'position'), styles = {}; - if (position === 'static' || !position) { - styles.position = 'relative'; - if (Prototype.Browser.Opera) { - styles.top = 0; - styles.left = 0; - } - Element.setStyle(element, styles); - Element.store(element, 'prototype_made_positioned', true); - } - return element; - } - - function undoPositioned(element) { - element = $(element); - var storage = Element.getStorage(element), - madePositioned = storage.get('prototype_made_positioned'); - - if (madePositioned) { - storage.unset('prototype_made_positioned'); - Element.setStyle(element, { - position: '', - top: '', - bottom: '', - left: '', - right: '' - }); - } - return element; - } - - function makeClipping(element) { - element = $(element); - - var storage = Element.getStorage(element), - madeClipping = storage.get('prototype_made_clipping'); - - if (Object.isUndefined(madeClipping)) { - var overflow = Element.getStyle(element, 'overflow'); - storage.set('prototype_made_clipping', overflow); - if (overflow !== 'hidden') - element.style.overflow = 'hidden'; - } - - return element; - } - - function undoClipping(element) { - element = $(element); - var storage = Element.getStorage(element), - overflow = storage.get('prototype_made_clipping'); - - if (!Object.isUndefined(overflow)) { - storage.unset('prototype_made_clipping'); - element.style.overflow = overflow || ''; - } - - return element; - } - - function clonePosition(element, source, options) { - options = Object.extend({ - setLeft: true, - setTop: true, - setWidth: true, - setHeight: true, - offsetTop: 0, - offsetLeft: 0 - }, options || {}); - - var docEl = document.documentElement; - - source = $(source); - element = $(element); - var p, delta, layout, styles = {}; - - if (options.setLeft || options.setTop) { - p = Element.viewportOffset(source); - delta = [0, 0]; - if (Element.getStyle(element, 'position') === 'absolute') { - var parent = Element.getOffsetParent(element); - if (parent !== document.body) delta = Element.viewportOffset(parent); - } - } - - function pageScrollXY() { - var x = 0, y = 0; - if (Object.isNumber(window.pageXOffset)) { - x = window.pageXOffset; - y = window.pageYOffset; - } else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { - x = document.body.scrollLeft; - y = document.body.scrollTop; - } else if (docEl && (docEl.scrollLeft || docEl.scrollTop)) { - x = docEl.scrollLeft; - y = docEl.scrollTop; - } - return { x: x, y: y }; - } - - var pageXY = pageScrollXY(); - - - if (options.setWidth || options.setHeight) { - layout = Element.getLayout(source); - } - - if (options.setLeft) - styles.left = (p[0] + pageXY.x - delta[0] + options.offsetLeft) + 'px'; - if (options.setTop) - styles.top = (p[1] + pageXY.y - delta[1] + options.offsetTop) + 'px'; - - var currentLayout = element.getLayout(); - - if (options.setWidth) { - styles.width = layout.get('width') + 'px'; - } - if (options.setHeight) { - styles.height = layout.get('height') + 'px'; - } - - return Element.setStyle(element, styles); - } - - - if (Prototype.Browser.IE) { - getOffsetParent = getOffsetParent.wrap( - function(proceed, element) { - element = $(element); - - if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) - return $(document.body); - - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - - positionedOffset = positionedOffset.wrap(function(proceed, element) { - element = $(element); - if (!element.parentNode) return new Element.Offset(0, 0); - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - - var offsetParent = element.getOffsetParent(); - if (offsetParent && offsetParent.getStyle('position') === 'fixed') - hasLayout(offsetParent); - - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - }); - } else if (Prototype.Browser.Webkit) { - cumulativeOffset = function(element) { - element = $(element); - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == document.body) { - if (Element.getStyle(element, 'position') == 'absolute') break; - } - - element = element.offsetParent; - } while (element); - - return new Element.Offset(valueL, valueT); - }; - } - - - Element.addMethods({ - getLayout: getLayout, - measure: measure, - getWidth: getWidth, - getHeight: getHeight, - getDimensions: getDimensions, - getOffsetParent: getOffsetParent, - cumulativeOffset: cumulativeOffset, - positionedOffset: positionedOffset, - cumulativeScrollOffset: cumulativeScrollOffset, - viewportOffset: viewportOffset, - absolutize: absolutize, - relativize: relativize, - scrollTo: scrollTo, - makePositioned: makePositioned, - undoPositioned: undoPositioned, - makeClipping: makeClipping, - undoClipping: undoClipping, - clonePosition: clonePosition - }); - - function isBody(element) { - return element.nodeName.toUpperCase() === 'BODY'; - } - - function isHtml(element) { - return element.nodeName.toUpperCase() === 'HTML'; - } - - function isDocument(element) { - return element.nodeType === Node.DOCUMENT_NODE; - } - - function isDetached(element) { - return element !== document.body && - !Element.descendantOf(element, document.body); - } - - if ('getBoundingClientRect' in document.documentElement) { - Element.addMethods({ - viewportOffset: function(element) { - element = $(element); - if (isDetached(element)) return new Element.Offset(0, 0); - - var rect = element.getBoundingClientRect(), - docEl = document.documentElement; - return new Element.Offset(rect.left - docEl.clientLeft, - rect.top - docEl.clientTop); - } - }); - } - - -})(); - -(function() { - - var IS_OLD_OPERA = Prototype.Browser.Opera && - (window.parseFloat(window.opera.version()) < 9.5); - var ROOT = null; - function getRootElement() { - if (ROOT) return ROOT; - ROOT = IS_OLD_OPERA ? document.body : document.documentElement; - return ROOT; - } - - function getDimensions() { - return { width: this.getWidth(), height: this.getHeight() }; - } - - function getWidth() { - return getRootElement().clientWidth; - } - - function getHeight() { - return getRootElement().clientHeight; - } - - function getScrollOffsets() { - var x = window.pageXOffset || document.documentElement.scrollLeft || - document.body.scrollLeft; - var y = window.pageYOffset || document.documentElement.scrollTop || - document.body.scrollTop; - - return new Element.Offset(x, y); - } - - document.viewport = { - getDimensions: getDimensions, - getWidth: getWidth, - getHeight: getHeight, - getScrollOffsets: getScrollOffsets - }; - -})(); -window.$$ = function() { - var expression = $A(arguments).join(', '); - return Prototype.Selector.select(expression, document); -}; - -Prototype.Selector = (function() { - - function select() { - throw new Error('Method "Prototype.Selector.select" must be defined.'); - } - - function match() { - throw new Error('Method "Prototype.Selector.match" must be defined.'); - } - - function find(elements, expression, index) { - index = index || 0; - var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; - - for (i = 0; i < length; i++) { - if (match(elements[i], expression) && index == matchIndex++) { - return Element.extend(elements[i]); - } - } - } - - function extendElements(elements) { - for (var i = 0, length = elements.length; i < length; i++) { - Element.extend(elements[i]); - } - return elements; - } - - - var K = Prototype.K; - - return { - select: select, - match: match, - find: find, - extendElements: (Element.extend === K) ? K : extendElements, - extendElement: Element.extend - }; -})(); -Prototype._original_property = window.Sizzle; - -;(function () { - function fakeDefine(fn) { - Prototype._actual_sizzle = fn(); - } - fakeDefine.amd = true; - - if (typeof define !== 'undefined' && define.amd) { - Prototype._original_define = define; - Prototype._actual_sizzle = null; - window.define = fakeDefine; - } -})(); - -/*! - * Sizzle CSS Selector Engine v1.10.18 - * http://sizzlejs.com/ - * - * Copyright 2013 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-02-05 - */ -(function( window ) { - -var i, - support, - Expr, - getText, - isXML, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, - - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - indexOf = arr.indexOf || function( elem ) { - var i = 0, - len = this.length; - for ( ; i < len; i++ ) { - if ( this[i] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - - whitespace = "[\\x20\\t\\r\\n\\f]", - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - identifier = characterEncoding.replace( "w", "w#" ), - - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", - - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - rescape = /'|\\/g, - - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - return high !== high || escapedWhitespace ? - escaped : - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; - -try { - push.apply( - (arr = slice.call( preferredDoc.childNodes )), - preferredDoc.childNodes - ); - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - function( target, els ) { - push_native.apply( target, slice.call(els) ); - } : - - function( target, els ) { - var j = target.length, - i = 0; - while ( (target[j++] = els[i++]) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } - - if ( documentIsHTML && !seed ) { - - if ( (match = rquickExpr.exec( selector )) ) { - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - if ( elem && elem.parentNode ) { - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - if ( keys.push( key + " " ) > Expr.cacheLength ) { - delete cache[ keys.shift() ]; - } - return (cache[ key + " " ] = value); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return !!fn( div ); - } catch (e) { - return false; - } finally { - if ( div.parentNode ) { - div.parentNode.removeChild( div ); - } - div = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split("|"), - i = attrs.length; - - while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - ( ~b.sourceIndex || MAX_NEGATIVE ) - - ( ~a.sourceIndex || MAX_NEGATIVE ); - - if ( diff ) { - return diff; - } - - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== strundefined && context; -} - -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, - doc = node ? node.ownerDocument || node : preferredDoc, - parent = doc.defaultView; - - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - document = doc; - docElem = doc.documentElement; - - documentIsHTML = !isXML( doc ); - - if ( parent && parent !== parent.top ) { - if ( parent.addEventListener ) { - parent.addEventListener( "unload", function() { - setDocument(); - }, false ); - } else if ( parent.attachEvent ) { - parent.attachEvent( "onunload", function() { - setDocument(); - }); - } - } - - /* Attributes - ---------------------------------------------------------------------- */ - - support.attributes = assert(function( div ) { - div.className = "i"; - return !div.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { - div.innerHTML = "
      "; - - div.firstChild.className = "i"; - return div.getElementsByClassName("i").length === 2; - }); - - support.getById = assert(function( div ) { - docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; - }); - - if ( support.getById ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && documentIsHTML ) { - var m = context.getElementById( id ); - return m && m.parentNode ? [m] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - delete Expr.find["ID"]; - - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - Expr.find["TAG"] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { - return context.getElementsByTagName( tag ); - } - } : - function( tag, context ) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName( tag ); - - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - - rbuggyMatches = []; - - rbuggyQSA = []; - - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { - assert(function( div ) { - div.innerHTML = ""; - - if ( div.querySelectorAll("[t^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - }); - - assert(function( div ) { - var input = doc.createElement("input"); - input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "name", "D" ); - - if ( div.querySelectorAll("[name=d]").length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - support.disconnectedMatch = matches.call( div, "div" ); - - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - sortOrder = hasCompare ? - function( a, b ) { - - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - 1; - - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { - return 1; - } - - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - siblingCheck( ap[i], bp[i] ) : - - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return doc; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - expr = expr.replace( rattributeQuotes, "='$1']" ); - - if ( support.matchesSelector && documentIsHTML && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - if ( ret || support.disconnectedMatch || - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch(e) {} - } - - return Sizzle( expr, document, null, [elem] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - while ( (node = elem[i++]) ) { - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - - return ret; -}; - -Expr = Sizzle.selectors = { - - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[5] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - if ( match[3] && match[4] !== undefined ) { - match[2] = match[4]; - - } else if ( unquoted && rpseudo.test( unquoted ) && - (excess = tokenize( unquoted, true )) && - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { return true; } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - if ( forward && useCache ) { - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - } else { - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - if ( fn[ expando ] ) { - return fn( argument ); - } - - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - "not": markFunction(function( selector ) { - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - "lang": markFunction( function( lang ) { - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - "empty": function( elem ) { - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); - }, - - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( (tokens = []) ); - } - - matched = false; - - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: match[0].replace( rtrim, " " ) - }); - soFar = soFar.slice( matched.length ); - } - - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (oldCache = outerCache[ dir ]) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - return (newCache[ 2 ] = oldCache[ 2 ]); - } else { - outerCache[ dir ] = newCache; - - if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - [] : - - results : - matcherIn; - - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - if ( matcher[ expando ] ) { - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), - len = elems.length; - - if ( outermost ) { - outermostContext = context !== document && context; - } - - for ( ; i !== len && (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - if ( bySet ) { - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - if ( seed ) { - unmatched.push( elem ); - } - } - } - - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - setMatched = condense( setMatched ); - } - - push.apply( results, setMatched ); - - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( (selector = compiled.selector || selector) ); - - results = results || []; - - if ( match.length === 1 ) { - - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { - - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context - )) ) { - - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - - -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; - -support.detectDuplicates = !!hasDuplicate; - -setDocument(); - -support.sortDetached = assert(function( div1 ) { - return div1.compareDocumentPosition( document.createElement("div") ) & 1; -}); - -if ( !assert(function( div ) { - div.innerHTML = ""; - return div.firstChild.getAttribute("href") === "#" ; -}) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - }); -} - -if ( !support.attributes || !assert(function( div ) { - div.innerHTML = ""; - div.firstChild.setAttribute( "value", "" ); - return div.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - }); -} - -if ( !assert(function( div ) { - return div.getAttribute("disabled") == null; -}) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - null; - } - }); -} - -if ( typeof define === "function" && define.amd ) { - define(function() { return Sizzle; }); -} else if ( typeof module !== "undefined" && module.exports ) { - module.exports = Sizzle; -} else { - window.Sizzle = Sizzle; -} - -})( window ); - -;(function() { - if (typeof Sizzle !== 'undefined') { - return; - } - - if (typeof define !== 'undefined' && define.amd) { - window.Sizzle = Prototype._actual_sizzle; - window.define = Prototype._original_define; - delete Prototype._actual_sizzle; - delete Prototype._original_define; - } else if (typeof module !== 'undefined' && module.exports) { - window.Sizzle = module.exports; - module.exports = {}; - } -})(); - -;(function(engine) { - var extendElements = Prototype.Selector.extendElements; - - function select(selector, scope) { - return extendElements(engine(selector, scope || document)); - } - - function match(element, selector) { - return engine.matches(selector, [element]).length == 1; - } - - Prototype.Selector.engine = engine; - Prototype.Selector.select = select; - Prototype.Selector.match = match; -})(Sizzle); - -window.Sizzle = Prototype._original_property; -delete Prototype._original_property; - -var Form = { - reset: function(form) { - form = $(form); - form.reset(); - return form; - }, - - serializeElements: function(elements, options) { - if (typeof options != 'object') options = { hash: !!options }; - else if (Object.isUndefined(options.hash)) options.hash = true; - var key, value, submitted = false, submit = options.submit, accumulator, initial; - - if (options.hash) { - initial = {}; - accumulator = function(result, key, value) { - if (key in result) { - if (!Object.isArray(result[key])) result[key] = [result[key]]; - result[key] = result[key].concat(value); - } else result[key] = value; - return result; - }; - } else { - initial = ''; - accumulator = function(result, key, values) { - if (!Object.isArray(values)) {values = [values];} - if (!values.length) {return result;} - var encodedKey = encodeURIComponent(key).gsub(/%20/, '+'); - return result + (result ? "&" : "") + values.map(function (value) { - value = value.gsub(/(\r)?\n/, '\r\n'); - value = encodeURIComponent(value); - value = value.gsub(/%20/, '+'); - return encodedKey + "=" + value; - }).join("&"); - }; - } - - return elements.inject(initial, function(result, element) { - if (!element.disabled && element.name) { - key = element.name; value = $(element).getValue(); - if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && - submit !== false && (!submit || key == submit) && (submitted = true)))) { - result = accumulator(result, key, value); - } - } - return result; - }); - } -}; - -Form.Methods = { - serialize: function(form, options) { - return Form.serializeElements(Form.getElements(form), options); - }, - - - getElements: function(form) { - var elements = $(form).getElementsByTagName('*'); - var element, results = [], serializers = Form.Element.Serializers; - - for (var i = 0; element = elements[i]; i++) { - if (serializers[element.tagName.toLowerCase()]) - results.push(Element.extend(element)); - } - return results; - }, - - getInputs: function(form, typeName, name) { - form = $(form); - var inputs = form.getElementsByTagName('input'); - - if (!typeName && !name) return $A(inputs).map(Element.extend); - - for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { - var input = inputs[i]; - if ((typeName && input.type != typeName) || (name && input.name != name)) - continue; - matchingInputs.push(Element.extend(input)); - } - - return matchingInputs; - }, - - disable: function(form) { - form = $(form); - Form.getElements(form).invoke('disable'); - return form; - }, - - enable: function(form) { - form = $(form); - Form.getElements(form).invoke('enable'); - return form; - }, - - findFirstElement: function(form) { - var elements = $(form).getElements().findAll(function(element) { - return 'hidden' != element.type && !element.disabled; - }); - var firstByIndex = elements.findAll(function(element) { - return element.hasAttribute('tabIndex') && element.tabIndex >= 0; - }).sortBy(function(element) { return element.tabIndex }).first(); - - return firstByIndex ? firstByIndex : elements.find(function(element) { - return /^(?:input|select|textarea)$/i.test(element.tagName); - }); - }, - - focusFirstElement: function(form) { - form = $(form); - var element = form.findFirstElement(); - if (element) element.activate(); - return form; - }, - - request: function(form, options) { - form = $(form), options = Object.clone(options || { }); - - var params = options.parameters, action = form.readAttribute('action') || ''; - if (action.blank()) action = window.location.href; - options.parameters = form.serialize(true); - - if (params) { - if (Object.isString(params)) params = params.toQueryParams(); - Object.extend(options.parameters, params); - } - - if (form.hasAttribute('method') && !options.method) - options.method = form.method; - - return new Ajax.Request(action, options); - } -}; - -/*--------------------------------------------------------------------------*/ - - -Form.Element = { - focus: function(element) { - $(element).focus(); - return element; - }, - - select: function(element) { - $(element).select(); - return element; - } -}; - -Form.Element.Methods = { - - serialize: function(element) { - element = $(element); - if (!element.disabled && element.name) { - var value = element.getValue(); - if (value != undefined) { - var pair = { }; - pair[element.name] = value; - return Object.toQueryString(pair); - } - } - return ''; - }, - - getValue: function(element) { - element = $(element); - var method = element.tagName.toLowerCase(); - return Form.Element.Serializers[method](element); - }, - - setValue: function(element, value) { - element = $(element); - var method = element.tagName.toLowerCase(); - Form.Element.Serializers[method](element, value); - return element; - }, - - clear: function(element) { - $(element).value = ''; - return element; - }, - - present: function(element) { - return $(element).value != ''; - }, - - activate: function(element) { - element = $(element); - try { - element.focus(); - if (element.select && (element.tagName.toLowerCase() != 'input' || - !(/^(?:button|reset|submit)$/i.test(element.type)))) - element.select(); - } catch (e) { } - return element; - }, - - disable: function(element) { - element = $(element); - element.disabled = true; - return element; - }, - - enable: function(element) { - element = $(element); - element.disabled = false; - return element; - } -}; - -/*--------------------------------------------------------------------------*/ - -var Field = Form.Element; - -var $F = Form.Element.Methods.getValue; - -/*--------------------------------------------------------------------------*/ - -Form.Element.Serializers = (function() { - function input(element, value) { - switch (element.type.toLowerCase()) { - case 'checkbox': - case 'radio': - return inputSelector(element, value); - default: - return valueSelector(element, value); - } - } - - function inputSelector(element, value) { - if (Object.isUndefined(value)) - return element.checked ? element.value : null; - else element.checked = !!value; - } - - function valueSelector(element, value) { - if (Object.isUndefined(value)) return element.value; - else element.value = value; - } - - function select(element, value) { - if (Object.isUndefined(value)) - return (element.type === 'select-one' ? selectOne : selectMany)(element); - - var opt, currentValue, single = !Object.isArray(value); - for (var i = 0, length = element.length; i < length; i++) { - opt = element.options[i]; - currentValue = this.optionValue(opt); - if (single) { - if (currentValue == value) { - opt.selected = true; - return; - } - } - else opt.selected = value.include(currentValue); - } - } - - function selectOne(element) { - var index = element.selectedIndex; - return index >= 0 ? optionValue(element.options[index]) : null; - } - - function selectMany(element) { - var values, length = element.length; - if (!length) return null; - - for (var i = 0, values = []; i < length; i++) { - var opt = element.options[i]; - if (opt.selected) values.push(optionValue(opt)); - } - return values; - } - - function optionValue(opt) { - return Element.hasAttribute(opt, 'value') ? opt.value : opt.text; - } - - return { - input: input, - inputSelector: inputSelector, - textarea: valueSelector, - select: select, - selectOne: selectOne, - selectMany: selectMany, - optionValue: optionValue, - button: valueSelector - }; -})(); - -/*--------------------------------------------------------------------------*/ - - -Abstract.TimedObserver = Class.create(PeriodicalExecuter, { - initialize: function($super, element, frequency, callback) { - $super(callback, frequency); - this.element = $(element); - this.lastValue = this.getValue(); - }, - - execute: function() { - var value = this.getValue(); - if (Object.isString(this.lastValue) && Object.isString(value) ? - this.lastValue != value : String(this.lastValue) != String(value)) { - this.callback(this.element, value); - this.lastValue = value; - } - } -}); - -Form.Element.Observer = Class.create(Abstract.TimedObserver, { - getValue: function() { - return Form.Element.getValue(this.element); - } -}); - -Form.Observer = Class.create(Abstract.TimedObserver, { - getValue: function() { - return Form.serialize(this.element); - } -}); - -/*--------------------------------------------------------------------------*/ - -Abstract.EventObserver = Class.create({ - initialize: function(element, callback) { - this.element = $(element); - this.callback = callback; - - this.lastValue = this.getValue(); - if (this.element.tagName.toLowerCase() == 'form') - this.registerFormCallbacks(); - else - this.registerCallback(this.element); - }, - - onElementEvent: function() { - var value = this.getValue(); - if (this.lastValue != value) { - this.callback(this.element, value); - this.lastValue = value; - } - }, - - registerFormCallbacks: function() { - Form.getElements(this.element).each(this.registerCallback, this); - }, - - registerCallback: function(element) { - if (element.type) { - switch (element.type.toLowerCase()) { - case 'checkbox': - case 'radio': - Event.observe(element, 'click', this.onElementEvent.bind(this)); - break; - default: - Event.observe(element, 'change', this.onElementEvent.bind(this)); - break; - } - } - } -}); - -Form.Element.EventObserver = Class.create(Abstract.EventObserver, { - getValue: function() { - return Form.Element.getValue(this.element); - } -}); - -Form.EventObserver = Class.create(Abstract.EventObserver, { - getValue: function() { - return Form.serialize(this.element); - } -}); -(function(GLOBAL) { - var DIV = document.createElement('div'); - var docEl = document.documentElement; - var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl - && 'onmouseleave' in docEl; - - var Event = { - KEY_BACKSPACE: 8, - KEY_TAB: 9, - KEY_RETURN: 13, - KEY_ESC: 27, - KEY_LEFT: 37, - KEY_UP: 38, - KEY_RIGHT: 39, - KEY_DOWN: 40, - KEY_DELETE: 46, - KEY_HOME: 36, - KEY_END: 35, - KEY_PAGEUP: 33, - KEY_PAGEDOWN: 34, - KEY_INSERT: 45 - }; - - - var isIELegacyEvent = function(event) { return false; }; - - if (window.attachEvent) { - if (window.addEventListener) { - isIELegacyEvent = function(event) { - return !(event instanceof window.Event); - }; - } else { - isIELegacyEvent = function(event) { return true; }; - } - } - - var _isButton; - - function _isButtonForDOMEvents(event, code) { - return event.which ? (event.which === code + 1) : (event.button === code); - } - - var legacyButtonMap = { 0: 1, 1: 4, 2: 2 }; - function _isButtonForLegacyEvents(event, code) { - return event.button === legacyButtonMap[code]; - } - - function _isButtonForWebKit(event, code) { - switch (code) { - case 0: return event.which == 1 && !event.metaKey; - case 1: return event.which == 2 || (event.which == 1 && event.metaKey); - case 2: return event.which == 3; - default: return false; - } - } - - if (window.attachEvent) { - if (!window.addEventListener) { - _isButton = _isButtonForLegacyEvents; - } else { - _isButton = function(event, code) { - return isIELegacyEvent(event) ? _isButtonForLegacyEvents(event, code) : - _isButtonForDOMEvents(event, code); - } - } - } else if (Prototype.Browser.WebKit) { - _isButton = _isButtonForWebKit; - } else { - _isButton = _isButtonForDOMEvents; - } - - function isLeftClick(event) { return _isButton(event, 0) } - - function isMiddleClick(event) { return _isButton(event, 1) } - - function isRightClick(event) { return _isButton(event, 2) } - - function element(event) { - return Element.extend(_element(event)); - } - - function _element(event) { - event = Event.extend(event); - - var node = event.target, type = event.type, - currentTarget = event.currentTarget; - - if (currentTarget && currentTarget.tagName) { - if (type === 'load' || type === 'error' || - (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' - && currentTarget.type === 'radio')) - node = currentTarget; - } - - return node.nodeType == Node.TEXT_NODE ? node.parentNode : node; - } - - function findElement(event, expression) { - var element = _element(event), selector = Prototype.Selector; - if (!expression) return Element.extend(element); - while (element) { - if (Object.isElement(element) && selector.match(element, expression)) - return Element.extend(element); - element = element.parentNode; - } - } - - function pointer(event) { - return { x: pointerX(event), y: pointerY(event) }; - } - - function pointerX(event) { - var docElement = document.documentElement, - body = document.body || { scrollLeft: 0 }; - - return event.pageX || (event.clientX + - (docElement.scrollLeft || body.scrollLeft) - - (docElement.clientLeft || 0)); - } - - function pointerY(event) { - var docElement = document.documentElement, - body = document.body || { scrollTop: 0 }; - - return event.pageY || (event.clientY + - (docElement.scrollTop || body.scrollTop) - - (docElement.clientTop || 0)); - } - - - function stop(event) { - Event.extend(event); - event.preventDefault(); - event.stopPropagation(); - - event.stopped = true; - } - - - Event.Methods = { - isLeftClick: isLeftClick, - isMiddleClick: isMiddleClick, - isRightClick: isRightClick, - - element: element, - findElement: findElement, - - pointer: pointer, - pointerX: pointerX, - pointerY: pointerY, - - stop: stop - }; - - var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { - m[name] = Event.Methods[name].methodize(); - return m; - }); - - if (window.attachEvent) { - function _relatedTarget(event) { - var element; - switch (event.type) { - case 'mouseover': - case 'mouseenter': - element = event.fromElement; - break; - case 'mouseout': - case 'mouseleave': - element = event.toElement; - break; - default: - return null; - } - return Element.extend(element); - } - - var additionalMethods = { - stopPropagation: function() { this.cancelBubble = true }, - preventDefault: function() { this.returnValue = false }, - inspect: function() { return '[object Event]' } - }; - - Event.extend = function(event, element) { - if (!event) return false; - - if (!isIELegacyEvent(event)) return event; - - if (event._extendedByPrototype) return event; - event._extendedByPrototype = Prototype.emptyFunction; - - var pointer = Event.pointer(event); - - Object.extend(event, { - target: event.srcElement || element, - relatedTarget: _relatedTarget(event), - pageX: pointer.x, - pageY: pointer.y - }); - - Object.extend(event, methods); - Object.extend(event, additionalMethods); - - return event; - }; - } else { - Event.extend = Prototype.K; - } - - if (window.addEventListener) { - Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; - Object.extend(Event.prototype, methods); - } - - var EVENT_TRANSLATIONS = { - mouseenter: 'mouseover', - mouseleave: 'mouseout' - }; - - function getDOMEventName(eventName) { - return EVENT_TRANSLATIONS[eventName] || eventName; - } - - if (MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) - getDOMEventName = Prototype.K; - - function getUniqueElementID(element) { - if (element === window) return 0; - - if (typeof element._prototypeUID === 'undefined') - element._prototypeUID = Element.Storage.UID++; - return element._prototypeUID; - } - - function getUniqueElementID_IE(element) { - if (element === window) return 0; - if (element == document) return 1; - return element.uniqueID; - } - - if ('uniqueID' in DIV) - getUniqueElementID = getUniqueElementID_IE; - - function isCustomEvent(eventName) { - return eventName.include(':'); - } - - Event._isCustomEvent = isCustomEvent; - - function getOrCreateRegistryFor(element, uid) { - var CACHE = GLOBAL.Event.cache; - if (Object.isUndefined(uid)) - uid = getUniqueElementID(element); - if (!CACHE[uid]) CACHE[uid] = { element: element }; - return CACHE[uid]; - } - - function destroyRegistryForElement(element, uid) { - if (Object.isUndefined(uid)) - uid = getUniqueElementID(element); - delete GLOBAL.Event.cache[uid]; - } - - - function register(element, eventName, handler) { - var registry = getOrCreateRegistryFor(element); - if (!registry[eventName]) registry[eventName] = []; - var entries = registry[eventName]; - - var i = entries.length; - while (i--) - if (entries[i].handler === handler) return null; - - var uid = getUniqueElementID(element); - var responder = GLOBAL.Event._createResponder(uid, eventName, handler); - var entry = { - responder: responder, - handler: handler - }; - - entries.push(entry); - return entry; - } - - function unregister(element, eventName, handler) { - var registry = getOrCreateRegistryFor(element); - var entries = registry[eventName] || []; - - var i = entries.length, entry; - while (i--) { - if (entries[i].handler === handler) { - entry = entries[i]; - break; - } - } - - if (entry) { - var index = entries.indexOf(entry); - entries.splice(index, 1); - } - - if (entries.length === 0) { - delete registry[eventName]; - if (Object.keys(registry).length === 1 && ('element' in registry)) - destroyRegistryForElement(element); - } - - return entry; - } - - - function observe(element, eventName, handler) { - element = $(element); - var entry = register(element, eventName, handler); - - if (entry === null) return element; - - var responder = entry.responder; - if (isCustomEvent(eventName)) - observeCustomEvent(element, eventName, responder); - else - observeStandardEvent(element, eventName, responder); - - return element; - } - - function observeStandardEvent(element, eventName, responder) { - var actualEventName = getDOMEventName(eventName); - if (element.addEventListener) { - element.addEventListener(actualEventName, responder, false); - } else { - element.attachEvent('on' + actualEventName, responder); - } - } - - function observeCustomEvent(element, eventName, responder) { - if (element.addEventListener) { - element.addEventListener('dataavailable', responder, false); - } else { - element.attachEvent('ondataavailable', responder); - element.attachEvent('onlosecapture', responder); - } - } - - function stopObserving(element, eventName, handler) { - element = $(element); - var handlerGiven = !Object.isUndefined(handler), - eventNameGiven = !Object.isUndefined(eventName); - - if (!eventNameGiven && !handlerGiven) { - stopObservingElement(element); - return element; - } - - if (!handlerGiven) { - stopObservingEventName(element, eventName); - return element; - } - - var entry = unregister(element, eventName, handler); - - if (!entry) return element; - removeEvent(element, eventName, entry.responder); - return element; - } - - function stopObservingStandardEvent(element, eventName, responder) { - var actualEventName = getDOMEventName(eventName); - if (element.removeEventListener) { - element.removeEventListener(actualEventName, responder, false); - } else { - element.detachEvent('on' + actualEventName, responder); - } - } - - function stopObservingCustomEvent(element, eventName, responder) { - if (element.removeEventListener) { - element.removeEventListener('dataavailable', responder, false); - } else { - element.detachEvent('ondataavailable', responder); - element.detachEvent('onlosecapture', responder); - } - } - - - - function stopObservingElement(element) { - var uid = getUniqueElementID(element), registry = GLOBAL.Event.cache[uid]; - if (!registry) return; - - destroyRegistryForElement(element, uid); - - var entries, i; - for (var eventName in registry) { - if (eventName === 'element') continue; - - entries = registry[eventName]; - i = entries.length; - while (i--) - removeEvent(element, eventName, entries[i].responder); - } - } - - function stopObservingEventName(element, eventName) { - var registry = getOrCreateRegistryFor(element); - var entries = registry[eventName]; - if (entries) { - delete registry[eventName]; - } - - entries = entries || []; - - var i = entries.length; - while (i--) - removeEvent(element, eventName, entries[i].responder); - - for (var name in registry) { - if (name === 'element') continue; - return; // There is another registered event - } - - destroyRegistryForElement(element); - } - - - function removeEvent(element, eventName, handler) { - if (isCustomEvent(eventName)) - stopObservingCustomEvent(element, eventName, handler); - else - stopObservingStandardEvent(element, eventName, handler); - } - - - - function getFireTarget(element) { - if (element !== document) return element; - if (document.createEvent && !element.dispatchEvent) - return document.documentElement; - return element; - } - - function fire(element, eventName, memo, bubble) { - element = getFireTarget($(element)); - if (Object.isUndefined(bubble)) bubble = true; - memo = memo || {}; - - var event = fireEvent(element, eventName, memo, bubble); - return Event.extend(event); - } - - function fireEvent_DOM(element, eventName, memo, bubble) { - var event = document.createEvent('HTMLEvents'); - event.initEvent('dataavailable', bubble, true); - - event.eventName = eventName; - event.memo = memo; - - element.dispatchEvent(event); - return event; - } - - function fireEvent_IE(element, eventName, memo, bubble) { - var event = document.createEventObject(); - event.eventType = bubble ? 'ondataavailable' : 'onlosecapture'; - - event.eventName = eventName; - event.memo = memo; - - element.fireEvent(event.eventType, event); - return event; - } - - var fireEvent = document.createEvent ? fireEvent_DOM : fireEvent_IE; - - - - Event.Handler = Class.create({ - initialize: function(element, eventName, selector, callback) { - this.element = $(element); - this.eventName = eventName; - this.selector = selector; - this.callback = callback; - this.handler = this.handleEvent.bind(this); - }, - - - start: function() { - Event.observe(this.element, this.eventName, this.handler); - return this; - }, - - stop: function() { - Event.stopObserving(this.element, this.eventName, this.handler); - return this; - }, - - handleEvent: function(event) { - var element = Event.findElement(event, this.selector); - if (element) this.callback.call(this.element, event, element); - } - }); - - function on(element, eventName, selector, callback) { - element = $(element); - if (Object.isFunction(selector) && Object.isUndefined(callback)) { - callback = selector, selector = null; - } - - return new Event.Handler(element, eventName, selector, callback).start(); - } - - Object.extend(Event, Event.Methods); - - Object.extend(Event, { - fire: fire, - observe: observe, - stopObserving: stopObserving, - p_on: on - }); - - Element.addMethods({ - fire: fire, - - observe: observe, - - stopObserving: stopObserving, - - p_on: on - }); - - Object.extend(document, { - fire: fire.methodize(), - - observe: observe.methodize(), - - stopObserving: stopObserving.methodize(), - - p_on: on.methodize(), - - loaded: false - }); - - if (GLOBAL.Event) Object.extend(window.Event, Event); - else GLOBAL.Event = Event; - - GLOBAL.Event.cache = {}; - - function destroyCache_IE() { - GLOBAL.Event.cache = null; - } - - if (window.attachEvent) - window.attachEvent('onunload', destroyCache_IE); - - DIV = null; - docEl = null; -})(this); - -(function(GLOBAL) { - /* Code for creating leak-free event responders is based on work by - John-David Dalton. */ - - var docEl = document.documentElement; - var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl - && 'onmouseleave' in docEl; - - function isSimulatedMouseEnterLeaveEvent(eventName) { - return !MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED && - (eventName === 'mouseenter' || eventName === 'mouseleave'); - } - - function createResponder(uid, eventName, handler) { - if (Event._isCustomEvent(eventName)) - return createResponderForCustomEvent(uid, eventName, handler); - if (isSimulatedMouseEnterLeaveEvent(eventName)) - return createMouseEnterLeaveResponder(uid, eventName, handler); - - return function(event) { - if (!Event.cache) return; - - var element = Event.cache[uid].element; - Event.extend(event, element); - handler.call(element, event); - }; - } - - function createResponderForCustomEvent(uid, eventName, handler) { - return function(event) { - var cache = Event.cache[uid]; - var element = cache && cache.element; - - if (Object.isUndefined(event.eventName)) - return false; - - if (event.eventName !== eventName) - return false; - - Event.extend(event, element); - handler.call(element, event); - }; - } - - function createMouseEnterLeaveResponder(uid, eventName, handler) { - return function(event) { - var element = Event.cache[uid].element; - - Event.extend(event, element); - var parent = event.relatedTarget; - - while (parent && parent !== element) { - try { parent = parent.parentNode; } - catch(e) { parent = element; } - } - - if (parent === element) return; - handler.call(element, event); - } - } - - GLOBAL.Event._createResponder = createResponder; - docEl = null; -})(this); - -(function(GLOBAL) { - /* Support for the DOMContentLoaded event is based on work by Dan Webb, - Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ - - var TIMER; - - function fireContentLoadedEvent() { - if (document.loaded) return; - if (TIMER) window.clearTimeout(TIMER); - document.loaded = true; - document.fire('dom:loaded'); - } - - function checkReadyState() { - if (document.readyState === 'complete') { - document.detachEvent('onreadystatechange', checkReadyState); - fireContentLoadedEvent(); - } - } - - function pollDoScroll() { - try { - document.documentElement.doScroll('left'); - } catch (e) { - TIMER = pollDoScroll.defer(); - return; - } - - fireContentLoadedEvent(); - } - - - if (document.readyState === 'complete') { - fireContentLoadedEvent(); - return; - } - - if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); - } else { - document.attachEvent('onreadystatechange', checkReadyState); - if (window == top) TIMER = pollDoScroll.defer(); - } - - Event.observe(window, 'load', fireContentLoadedEvent); -})(this); - - -Element.addMethods(); -/*------------------------------- DEPRECATED -------------------------------*/ - -Hash.toQueryString = Object.toQueryString; - -var Toggle = { display: Element.toggle }; - -Element.addMethods({ - childOf: Element.Methods.descendantOf -}); - -var Insertion = { - Before: function(element, content) { - return Element.insert(element, {before:content}); - }, - - Top: function(element, content) { - return Element.insert(element, {top:content}); - }, - - Bottom: function(element, content) { - return Element.insert(element, {bottom:content}); - }, - - After: function(element, content) { - return Element.insert(element, {after:content}); - } -}; - -var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); - -var Position = { - includeScrollOffsets: false, - - prepare: function() { - this.deltaX = window.pageXOffset - || document.documentElement.scrollLeft - || document.body.scrollLeft - || 0; - this.deltaY = window.pageYOffset - || document.documentElement.scrollTop - || document.body.scrollTop - || 0; - }, - - within: function(element, x, y) { - if (this.includeScrollOffsets) - return this.withinIncludingScrolloffsets(element, x, y); - this.xcomp = x; - this.ycomp = y; - this.offset = Element.cumulativeOffset(element); - - return (y >= this.offset[1] && - y < this.offset[1] + element.offsetHeight && - x >= this.offset[0] && - x < this.offset[0] + element.offsetWidth); - }, - - withinIncludingScrolloffsets: function(element, x, y) { - var offsetcache = Element.cumulativeScrollOffset(element); - - this.xcomp = x + offsetcache[0] - this.deltaX; - this.ycomp = y + offsetcache[1] - this.deltaY; - this.offset = Element.cumulativeOffset(element); - - return (this.ycomp >= this.offset[1] && - this.ycomp < this.offset[1] + element.offsetHeight && - this.xcomp >= this.offset[0] && - this.xcomp < this.offset[0] + element.offsetWidth); - }, - - overlap: function(mode, element) { - if (!mode) return 0; - if (mode == 'vertical') - return ((this.offset[1] + element.offsetHeight) - this.ycomp) / - element.offsetHeight; - if (mode == 'horizontal') - return ((this.offset[0] + element.offsetWidth) - this.xcomp) / - element.offsetWidth; - }, - - - cumulativeOffset: Element.Methods.cumulativeOffset, - - positionedOffset: Element.Methods.positionedOffset, - - absolutize: function(element) { - Position.prepare(); - return Element.absolutize(element); - }, - - relativize: function(element) { - Position.prepare(); - return Element.relativize(element); - }, - - realOffset: Element.Methods.cumulativeScrollOffset, - - offsetParent: Element.Methods.getOffsetParent, - - page: Element.Methods.viewportOffset, - - clone: function(source, target, options) { - options = options || { }; - return Element.clonePosition(target, source, options); - } -}; - -/*--------------------------------------------------------------------------*/ - -if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ - function iter(name) { - return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; - } - - instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? - function(element, className) { - className = className.toString().strip(); - var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); - return cond ? document._getElementsByXPath('.//*' + cond, element) : []; - } : function(element, className) { - className = className.toString().strip(); - var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); - if (!classNames && !className) return elements; - - var nodes = $(element).getElementsByTagName('*'); - className = ' ' + className + ' '; - - for (var i = 0, child, cn; child = nodes[i]; i++) { - if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || - (classNames && classNames.all(function(name) { - return !name.toString().blank() && cn.include(' ' + name + ' '); - })))) - elements.push(Element.extend(child)); - } - return elements; - }; - - return function(className, parentElement) { - return $(parentElement || document.body).getElementsByClassName(className); - }; -}(Element.Methods); - -/*--------------------------------------------------------------------------*/ - -Element.ClassNames = Class.create(); -Element.ClassNames.prototype = { - initialize: function(element) { - this.element = $(element); - }, - - _each: function(iterator, context) { - this.element.className.split(/\s+/).select(function(name) { - return name.length > 0; - })._each(iterator, context); - }, - - set: function(className) { - this.element.className = className; - }, - - add: function(classNameToAdd) { - if (this.include(classNameToAdd)) return; - this.set($A(this).concat(classNameToAdd).join(' ')); - }, - - remove: function(classNameToRemove) { - if (!this.include(classNameToRemove)) return; - this.set($A(this).without(classNameToRemove).join(' ')); - }, - - toString: function() { - return $A(this).join(' '); - } -}; - -Object.extend(Element.ClassNames.prototype, Enumerable); - -/*--------------------------------------------------------------------------*/ - -(function() { - window.Selector = Class.create({ - initialize: function(expression) { - this.expression = expression.strip(); - }, - - findElements: function(rootElement) { - return Prototype.Selector.select(this.expression, rootElement); - }, - - match: function(element) { - return Prototype.Selector.match(element, this.expression); - }, - - toString: function() { - return this.expression; - }, - - inspect: function() { - return "#"; - } - }); - - Object.extend(Selector, { - matchElements: function(elements, expression) { - var match = Prototype.Selector.match, - results = []; - - for (var i = 0, length = elements.length; i < length; i++) { - var element = elements[i]; - if (match(element, expression)) { - results.push(Element.extend(element)); - } - } - return results; - }, - - findElement: function(elements, expression, index) { - index = index || 0; - var matchIndex = 0, element; - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - if (Prototype.Selector.match(element, expression) && index === matchIndex++) { - return Element.extend(element); - } - } - }, - - findChildElements: function(element, expressions) { - var selector = expressions.toArray().join(', '); - return Prototype.Selector.select(selector, element || document); - } - }); -})(); diff --git a/lib/scriptaculous/controls.js b/lib/scriptaculous/controls.js deleted file mode 100644 index 5137ab510..000000000 --- a/lib/scriptaculous/controls.js +++ /dev/null @@ -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, - ''); - 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("
    • " + elem.substr(0, entry.length) + "" + - elem.substr(entry.length) + "
    • "); - 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("
    • " + elem.substr(0, foundPos) + "" + - elem.substr(foundPos, entry.length) + "" + elem.substr( - foundPos + entry.length) + "
    • "); - 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 "
        " + ret.join('') + "
      "; - } - }, options || { }); - } -}); - -// AJAX in-place editor and collection editor -// Full rewrite by Christophe Porteneuve (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)); - } -}); \ No newline at end of file diff --git a/lib/scriptaculous/effects.js b/lib/scriptaculous/effects.js deleted file mode 100644 index 860ddc093..000000000 --- a/lib/scriptaculous/effects.js +++ /dev/null @@ -1,1123 +0,0 @@ -// script.aculo.us effects.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) -// Contributors: -// Justin Palmer (http://encytemedia.com/) -// Mark Pilgrim (http://diveintomark.org/) -// Martin Bialasinki -// -// 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/ - -// converts rgb() and #xxx to #xxxxxx format, -// returns self (or first argument) if not convertable -String.prototype.parseColor = function() { - var color = '#'; - if (this.slice(0,4) == 'rgb(') { - var cols = this.slice(4,this.length-1).split(','); - var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); - } else { - if (this.slice(0,1) == '#') { - if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); - if (this.length==7) color = this.toLowerCase(); - } - } - return (color.length==7 ? color : (arguments[0] || this)); -}; - -/*--------------------------------------------------------------------------*/ - -Element.collectTextNodes = function(element) { - return $A($(element).childNodes).collect( function(node) { - return (node.nodeType==3 ? node.nodeValue : - (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); - }).flatten().join(''); -}; - -Element.collectTextNodesIgnoreClass = function(element, className) { - return $A($(element).childNodes).collect( function(node) { - return (node.nodeType==3 ? node.nodeValue : - ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? - Element.collectTextNodesIgnoreClass(node, className) : '')); - }).flatten().join(''); -}; - -Element.setContentZoom = function(element, percent) { - element = $(element); - element.setStyle({fontSize: (percent/100) + 'em'}); - if (Prototype.Browser.WebKit) window.scrollBy(0,0); - return element; -}; - -Element.getInlineOpacity = function(element){ - return $(element).style.opacity || ''; -}; - -Element.forceRerendering = function(element) { - try { - element = $(element); - var n = document.createTextNode(' '); - element.appendChild(n); - element.removeChild(n); - } catch(e) { } -}; - -/*--------------------------------------------------------------------------*/ - -var Effect = { - _elementDoesNotExistError: { - name: 'ElementDoesNotExistError', - message: 'The specified DOM element does not exist, but is required for this effect to operate' - }, - Transitions: { - linear: Prototype.K, - sinoidal: function(pos) { - return (-Math.cos(pos*Math.PI)/2) + .5; - }, - reverse: function(pos) { - return 1-pos; - }, - flicker: function(pos) { - var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; - return pos > 1 ? 1 : pos; - }, - wobble: function(pos) { - return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; - }, - pulse: function(pos, pulses) { - return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; - }, - spring: function(pos) { - return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); - }, - none: function(pos) { - return 0; - }, - full: function(pos) { - return 1; - } - }, - DefaultOptions: { - duration: 1.0, // seconds - fps: 100, // 100= assume 66fps max. - sync: false, // true for combining - from: 0.0, - to: 1.0, - delay: 0.0, - queue: 'parallel' - }, - tagifyText: function(element) { - var tagifyStyle = 'position:relative'; - if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; - - element = $(element); - $A(element.childNodes).each( function(child) { - if (child.nodeType==3) { - child.nodeValue.toArray().each( function(character) { - element.insertBefore( - new Element('span', {style: tagifyStyle}).update( - character == ' ' ? String.fromCharCode(160) : character), - child); - }); - Element.remove(child); - } - }); - }, - multiple: function(element, effect) { - var elements; - if (((typeof element == 'object') || - Object.isFunction(element)) && - (element.length)) - elements = element; - else - elements = $(element).childNodes; - - var options = Object.extend({ - speed: 0.1, - delay: 0.0 - }, arguments[2] || { }); - var masterDelay = options.delay; - - $A(elements).each( function(element, index) { - new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); - }); - }, - PAIRS: { - 'slide': ['SlideDown','SlideUp'], - 'blind': ['BlindDown','BlindUp'], - 'appear': ['Appear','Fade'] - }, - toggle: function(element, effect, options) { - element = $(element); - effect = (effect || 'appear').toLowerCase(); - - return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({ - queue: { position:'end', scope:(element.id || 'global'), limit: 1 } - }, options || {})); - } -}; - -Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; - -/* ------------- core effects ------------- */ - -Effect.ScopedQueue = Class.create(Enumerable, { - initialize: function() { - this.effects = []; - this.interval = null; - }, - _each: function(iterator) { - this.effects._each(iterator); - }, - add: function(effect) { - var timestamp = new Date().getTime(); - - var position = Object.isString(effect.options.queue) ? - effect.options.queue : effect.options.queue.position; - - switch(position) { - case 'front': - // move unstarted effects after this effect - this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { - e.startOn += effect.finishOn; - e.finishOn += effect.finishOn; - }); - break; - case 'with-last': - timestamp = this.effects.pluck('startOn').max() || timestamp; - break; - case 'end': - // start effect after last queued effect has finished - timestamp = this.effects.pluck('finishOn').max() || timestamp; - break; - } - - effect.startOn += timestamp; - effect.finishOn += timestamp; - - if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) - this.effects.push(effect); - - if (!this.interval) - this.interval = setInterval(this.loop.bind(this), 15); - }, - remove: function(effect) { - this.effects = this.effects.reject(function(e) { return e==effect }); - if (this.effects.length == 0) { - clearInterval(this.interval); - this.interval = null; - } - }, - loop: function() { - var timePos = new Date().getTime(); - for(var i=0, len=this.effects.length;i= this.startOn) { - if (timePos >= this.finishOn) { - this.render(1.0); - this.cancel(); - this.event('beforeFinish'); - if (this.finish) this.finish(); - this.event('afterFinish'); - return; - } - var pos = (timePos - this.startOn) / this.totalTime, - frame = (pos * this.totalFrames).round(); - if (frame > this.currentFrame) { - this.render(pos); - this.currentFrame = frame; - } - } - }, - cancel: function() { - if (!this.options.sync) - Effect.Queues.get(Object.isString(this.options.queue) ? - 'global' : this.options.queue.scope).remove(this); - this.state = 'finished'; - }, - event: function(eventName) { - if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); - if (this.options[eventName]) this.options[eventName](this); - }, - inspect: function() { - var data = $H(); - for(property in this) - if (!Object.isFunction(this[property])) data.set(property, this[property]); - return '#'; - } -}); - -Effect.Parallel = Class.create(Effect.Base, { - initialize: function(effects) { - this.effects = effects || []; - this.start(arguments[1]); - }, - update: function(position) { - this.effects.invoke('render', position); - }, - finish: function(position) { - this.effects.each( function(effect) { - effect.render(1.0); - effect.cancel(); - effect.event('beforeFinish'); - if (effect.finish) effect.finish(position); - effect.event('afterFinish'); - }); - } -}); - -Effect.Tween = Class.create(Effect.Base, { - initialize: function(object, from, to) { - object = Object.isString(object) ? $(object) : object; - var args = $A(arguments), method = args.last(), - options = args.length == 5 ? args[3] : null; - this.method = Object.isFunction(method) ? method.bind(object) : - Object.isFunction(object[method]) ? object[method].bind(object) : - function(value) { object[method] = value }; - this.start(Object.extend({ from: from, to: to }, options || { })); - }, - update: function(position) { - this.method(position); - } -}); - -Effect.Event = Class.create(Effect.Base, { - initialize: function() { - this.start(Object.extend({ duration: 0 }, arguments[0] || { })); - }, - update: Prototype.emptyFunction -}); - -Effect.Opacity = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - // make this work on IE on elements without 'layout' - if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) - this.element.setStyle({zoom: 1}); - var options = Object.extend({ - from: this.element.getOpacity() || 0.0, - to: 1.0 - }, arguments[1] || { }); - this.start(options); - }, - update: function(position) { - this.element.setOpacity(position); - } -}); - -Effect.Move = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ - x: 0, - y: 0, - mode: 'relative' - }, arguments[1] || { }); - this.start(options); - }, - setup: function() { - this.element.makePositioned(); - this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); - this.originalTop = parseFloat(this.element.getStyle('top') || '0'); - if (this.options.mode == 'absolute') { - this.options.x = this.options.x - this.originalLeft; - this.options.y = this.options.y - this.originalTop; - } - }, - update: function(position) { - this.element.setStyle({ - left: (this.options.x * position + this.originalLeft).round() + 'px', - top: (this.options.y * position + this.originalTop).round() + 'px' - }); - } -}); - -// for backwards compatibility -Effect.MoveBy = function(element, toTop, toLeft) { - return new Effect.Move(element, - Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); -}; - -Effect.Scale = Class.create(Effect.Base, { - initialize: function(element, percent) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ - scaleX: true, - scaleY: true, - scaleContent: true, - scaleFromCenter: false, - scaleMode: 'box', // 'box' or 'contents' or { } with provided values - scaleFrom: 100.0, - scaleTo: percent - }, arguments[2] || { }); - this.start(options); - }, - setup: function() { - this.restoreAfterFinish = this.options.restoreAfterFinish || false; - this.elementPositioning = this.element.getStyle('position'); - - this.originalStyle = { }; - ['top','left','width','height','fontSize'].each( function(k) { - this.originalStyle[k] = this.element.style[k]; - }.bind(this)); - - this.originalTop = this.element.offsetTop; - this.originalLeft = this.element.offsetLeft; - - var fontSize = this.element.getStyle('font-size') || '100%'; - ['em','px','%','pt'].each( function(fontSizeType) { - if (fontSize.indexOf(fontSizeType)>0) { - this.fontSize = parseFloat(fontSize); - this.fontSizeType = fontSizeType; - } - }.bind(this)); - - this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; - - this.dims = null; - if (this.options.scaleMode=='box') - this.dims = [this.element.offsetHeight, this.element.offsetWidth]; - if (/^content/.test(this.options.scaleMode)) - this.dims = [this.element.scrollHeight, this.element.scrollWidth]; - if (!this.dims) - this.dims = [this.options.scaleMode.originalHeight, - this.options.scaleMode.originalWidth]; - }, - update: function(position) { - var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); - if (this.options.scaleContent && this.fontSize) - this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); - this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); - }, - finish: function(position) { - if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); - }, - setDimensions: function(height, width) { - var d = { }; - if (this.options.scaleX) d.width = width.round() + 'px'; - if (this.options.scaleY) d.height = height.round() + 'px'; - if (this.options.scaleFromCenter) { - var topd = (height - this.dims[0])/2; - var leftd = (width - this.dims[1])/2; - if (this.elementPositioning == 'absolute') { - if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; - if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; - } else { - if (this.options.scaleY) d.top = -topd + 'px'; - if (this.options.scaleX) d.left = -leftd + 'px'; - } - } - this.element.setStyle(d); - } -}); - -Effect.Highlight = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); - this.start(options); - }, - setup: function() { - // Prevent executing on elements not in the layout flow - if (this.element.getStyle('display')=='none') { this.cancel(); return; } - // Disable background image during the effect - this.oldStyle = { }; - if (!this.options.keepBackgroundImage) { - this.oldStyle.backgroundImage = this.element.getStyle('background-image'); - this.element.setStyle({backgroundImage: 'none'}); - } - if (!this.options.endcolor) - this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); - if (!this.options.restorecolor) - this.options.restorecolor = this.element.getStyle('background-color'); - // init color calculations - this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); - this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); - }, - update: function(position) { - this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ - return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); - }, - finish: function() { - this.element.setStyle(Object.extend(this.oldStyle, { - backgroundColor: this.options.restorecolor - })); - } -}); - -Effect.ScrollTo = function(element) { - var options = arguments[1] || { }, - scrollOffsets = document.viewport.getScrollOffsets(), - elementOffsets = $(element).cumulativeOffset(); - - if (options.offset) elementOffsets[1] += options.offset; - - return new Effect.Tween(null, - scrollOffsets.top, - elementOffsets[1], - options, - function(p){ scrollTo(scrollOffsets.left, p.round()); } - ); -}; - -/* ------------- combination effects ------------- */ - -Effect.Fade = function(element) { - element = $(element); - var oldOpacity = element.getInlineOpacity(); - var options = Object.extend({ - from: element.getOpacity() || 1.0, - to: 0.0, - afterFinishInternal: function(effect) { - if (effect.options.to!=0) return; - effect.element.hide().setStyle({opacity: oldOpacity}); - } - }, arguments[1] || { }); - return new Effect.Opacity(element,options); -}; - -Effect.Appear = function(element) { - element = $(element); - var options = Object.extend({ - from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), - to: 1.0, - // force Safari to render floated elements properly - afterFinishInternal: function(effect) { - effect.element.forceRerendering(); - }, - beforeSetup: function(effect) { - effect.element.setOpacity(effect.options.from).show(); - }}, arguments[1] || { }); - return new Effect.Opacity(element,options); -}; - -Effect.Puff = function(element) { - element = $(element); - var oldStyle = { - opacity: element.getInlineOpacity(), - position: element.getStyle('position'), - top: element.style.top, - left: element.style.left, - width: element.style.width, - height: element.style.height - }; - return new Effect.Parallel( - [ new Effect.Scale(element, 200, - { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), - new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], - Object.extend({ duration: 1.0, - beforeSetupInternal: function(effect) { - Position.absolutize(effect.effects[0].element); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.hide().setStyle(oldStyle); } - }, arguments[1] || { }) - ); -}; - -Effect.BlindUp = function(element) { - element = $(element); - element.makeClipping(); - return new Effect.Scale(element, 0, - Object.extend({ scaleContent: false, - scaleX: false, - restoreAfterFinish: true, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping(); - } - }, arguments[1] || { }) - ); -}; - -Effect.BlindDown = function(element) { - element = $(element); - var elementDimensions = element.getDimensions(); - return new Effect.Scale(element, 100, Object.extend({ - scaleContent: false, - scaleX: false, - scaleFrom: 0, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - effect.element.makeClipping().setStyle({height: '0px'}).show(); - }, - afterFinishInternal: function(effect) { - effect.element.undoClipping(); - } - }, arguments[1] || { })); -}; - -Effect.SwitchOff = function(element) { - element = $(element); - var oldOpacity = element.getInlineOpacity(); - return new Effect.Appear(element, Object.extend({ - duration: 0.4, - from: 0, - transition: Effect.Transitions.flicker, - afterFinishInternal: function(effect) { - new Effect.Scale(effect.element, 1, { - duration: 0.3, scaleFromCenter: true, - scaleX: false, scaleContent: false, restoreAfterFinish: true, - beforeSetup: function(effect) { - effect.element.makePositioned().makeClipping(); - }, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); - } - }); - } - }, arguments[1] || { })); -}; - -Effect.DropOut = function(element) { - element = $(element); - var oldStyle = { - top: element.getStyle('top'), - left: element.getStyle('left'), - opacity: element.getInlineOpacity() }; - return new Effect.Parallel( - [ new Effect.Move(element, {x: 0, y: 100, sync: true }), - new Effect.Opacity(element, { sync: true, to: 0.0 }) ], - Object.extend( - { duration: 0.5, - beforeSetup: function(effect) { - effect.effects[0].element.makePositioned(); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); - } - }, arguments[1] || { })); -}; - -Effect.Shake = function(element) { - element = $(element); - var options = Object.extend({ - distance: 20, - duration: 0.5 - }, arguments[1] || {}); - var distance = parseFloat(options.distance); - var split = parseFloat(options.duration) / 10.0; - var oldStyle = { - top: element.getStyle('top'), - left: element.getStyle('left') }; - return new Effect.Move(element, - { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { - effect.element.undoPositioned().setStyle(oldStyle); - }}); }}); }}); }}); }}); }}); -}; - -Effect.SlideDown = function(element) { - element = $(element).cleanWhitespace(); - // SlideDown need to have the content of the element wrapped in a container element with fixed height! - var oldInnerBottom = element.down().getStyle('bottom'); - var elementDimensions = element.getDimensions(); - return new Effect.Scale(element, 100, Object.extend({ - scaleContent: false, - scaleX: false, - scaleFrom: window.opera ? 0 : 1, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - effect.element.makePositioned(); - effect.element.down().makePositioned(); - if (window.opera) effect.element.setStyle({top: ''}); - effect.element.makeClipping().setStyle({height: '0px'}).show(); - }, - afterUpdateInternal: function(effect) { - effect.element.down().setStyle({bottom: - (effect.dims[0] - effect.element.clientHeight) + 'px' }); - }, - afterFinishInternal: function(effect) { - effect.element.undoClipping().undoPositioned(); - effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } - }, arguments[1] || { }) - ); -}; - -Effect.SlideUp = function(element) { - element = $(element).cleanWhitespace(); - var oldInnerBottom = element.down().getStyle('bottom'); - var elementDimensions = element.getDimensions(); - return new Effect.Scale(element, window.opera ? 0 : 1, - Object.extend({ scaleContent: false, - scaleX: false, - scaleMode: 'box', - scaleFrom: 100, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - effect.element.makePositioned(); - effect.element.down().makePositioned(); - if (window.opera) effect.element.setStyle({top: ''}); - effect.element.makeClipping().show(); - }, - afterUpdateInternal: function(effect) { - effect.element.down().setStyle({bottom: - (effect.dims[0] - effect.element.clientHeight) + 'px' }); - }, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().undoPositioned(); - effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); - } - }, arguments[1] || { }) - ); -}; - -// Bug in opera makes the TD containing this element expand for a instance after finish -Effect.Squish = function(element) { - return new Effect.Scale(element, window.opera ? 1 : 0, { - restoreAfterFinish: true, - beforeSetup: function(effect) { - effect.element.makeClipping(); - }, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping(); - } - }); -}; - -Effect.Grow = function(element) { - element = $(element); - var options = Object.extend({ - direction: 'center', - moveTransition: Effect.Transitions.sinoidal, - scaleTransition: Effect.Transitions.sinoidal, - opacityTransition: Effect.Transitions.full - }, arguments[1] || { }); - var oldStyle = { - top: element.style.top, - left: element.style.left, - height: element.style.height, - width: element.style.width, - opacity: element.getInlineOpacity() }; - - var dims = element.getDimensions(); - var initialMoveX, initialMoveY; - var moveX, moveY; - - switch (options.direction) { - case 'top-left': - initialMoveX = initialMoveY = moveX = moveY = 0; - break; - case 'top-right': - initialMoveX = dims.width; - initialMoveY = moveY = 0; - moveX = -dims.width; - break; - case 'bottom-left': - initialMoveX = moveX = 0; - initialMoveY = dims.height; - moveY = -dims.height; - break; - case 'bottom-right': - initialMoveX = dims.width; - initialMoveY = dims.height; - moveX = -dims.width; - moveY = -dims.height; - break; - case 'center': - initialMoveX = dims.width / 2; - initialMoveY = dims.height / 2; - moveX = -dims.width / 2; - moveY = -dims.height / 2; - break; - } - - return new Effect.Move(element, { - x: initialMoveX, - y: initialMoveY, - duration: 0.01, - beforeSetup: function(effect) { - effect.element.hide().makeClipping().makePositioned(); - }, - afterFinishInternal: function(effect) { - new Effect.Parallel( - [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), - new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), - new Effect.Scale(effect.element, 100, { - scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, - sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) - ], Object.extend({ - beforeSetup: function(effect) { - effect.effects[0].element.setStyle({height: '0px'}).show(); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); - } - }, options) - ); - } - }); -}; - -Effect.Shrink = function(element) { - element = $(element); - var options = Object.extend({ - direction: 'center', - moveTransition: Effect.Transitions.sinoidal, - scaleTransition: Effect.Transitions.sinoidal, - opacityTransition: Effect.Transitions.none - }, arguments[1] || { }); - var oldStyle = { - top: element.style.top, - left: element.style.left, - height: element.style.height, - width: element.style.width, - opacity: element.getInlineOpacity() }; - - var dims = element.getDimensions(); - var moveX, moveY; - - switch (options.direction) { - case 'top-left': - moveX = moveY = 0; - break; - case 'top-right': - moveX = dims.width; - moveY = 0; - break; - case 'bottom-left': - moveX = 0; - moveY = dims.height; - break; - case 'bottom-right': - moveX = dims.width; - moveY = dims.height; - break; - case 'center': - moveX = dims.width / 2; - moveY = dims.height / 2; - break; - } - - return new Effect.Parallel( - [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), - new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), - new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) - ], Object.extend({ - beforeStartInternal: function(effect) { - effect.effects[0].element.makePositioned().makeClipping(); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } - }, options) - ); -}; - -Effect.Pulsate = function(element) { - element = $(element); - var options = arguments[1] || { }, - oldOpacity = element.getInlineOpacity(), - transition = options.transition || Effect.Transitions.linear, - reverser = function(pos){ - return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); - }; - - return new Effect.Opacity(element, - Object.extend(Object.extend({ duration: 2.0, from: 0, - afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } - }, options), {transition: reverser})); -}; - -Effect.Fold = function(element) { - element = $(element); - var oldStyle = { - top: element.style.top, - left: element.style.left, - width: element.style.width, - height: element.style.height }; - element.makeClipping(); - return new Effect.Scale(element, 5, Object.extend({ - scaleContent: false, - scaleX: false, - afterFinishInternal: function(effect) { - new Effect.Scale(element, 1, { - scaleContent: false, - scaleY: false, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().setStyle(oldStyle); - } }); - }}, arguments[1] || { })); -}; - -Effect.Morph = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ - style: { } - }, arguments[1] || { }); - - if (!Object.isString(options.style)) this.style = $H(options.style); - else { - if (options.style.include(':')) - this.style = options.style.parseStyle(); - else { - this.element.addClassName(options.style); - this.style = $H(this.element.getStyles()); - this.element.removeClassName(options.style); - var css = this.element.getStyles(); - this.style = this.style.reject(function(style) { - return style.value == css[style.key]; - }); - options.afterFinishInternal = function(effect) { - effect.element.addClassName(effect.options.style); - effect.transforms.each(function(transform) { - effect.element.style[transform.style] = ''; - }); - }; - } - } - this.start(options); - }, - - setup: function(){ - function parseColor(color){ - if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; - color = color.parseColor(); - return $R(0,2).map(function(i){ - return parseInt( color.slice(i*2+1,i*2+3), 16 ); - }); - } - this.transforms = this.style.map(function(pair){ - var property = pair[0], value = pair[1], unit = null; - - if (value.parseColor('#zzzzzz') != '#zzzzzz') { - value = value.parseColor(); - unit = 'color'; - } else if (property == 'opacity') { - value = parseFloat(value); - if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) - this.element.setStyle({zoom: 1}); - } else if (Element.CSS_LENGTH.test(value)) { - var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); - value = parseFloat(components[1]); - unit = (components.length == 3) ? components[2] : null; - } - - var originalValue = this.element.getStyle(property); - return { - style: property.camelize(), - originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), - targetValue: unit=='color' ? parseColor(value) : value, - unit: unit - }; - }.bind(this)).reject(function(transform){ - return ( - (transform.originalValue == transform.targetValue) || - ( - transform.unit != 'color' && - (isNaN(transform.originalValue) || isNaN(transform.targetValue)) - ) - ); - }); - }, - update: function(position) { - var style = { }, transform, i = this.transforms.length; - while(i--) - style[(transform = this.transforms[i]).style] = - transform.unit=='color' ? '#'+ - (Math.round(transform.originalValue[0]+ - (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + - (Math.round(transform.originalValue[1]+ - (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + - (Math.round(transform.originalValue[2]+ - (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : - (transform.originalValue + - (transform.targetValue - transform.originalValue) * position).toFixed(3) + - (transform.unit === null ? '' : transform.unit); - this.element.setStyle(style, true); - } -}); - -Effect.Transform = Class.create({ - initialize: function(tracks){ - this.tracks = []; - this.options = arguments[1] || { }; - this.addTracks(tracks); - }, - addTracks: function(tracks){ - tracks.each(function(track){ - track = $H(track); - var data = track.values().first(); - this.tracks.push($H({ - ids: track.keys().first(), - effect: Effect.Morph, - options: { style: data } - })); - }.bind(this)); - return this; - }, - play: function(){ - return new Effect.Parallel( - this.tracks.map(function(track){ - var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); - var elements = [$(ids) || $$(ids)].flatten(); - return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); - }).flatten(), - this.options - ); - } -}); - -Element.CSS_PROPERTIES = $w( - 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + - 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + - 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + - 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + - 'fontSize fontWeight height left letterSpacing lineHeight ' + - 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ - 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + - 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + - 'right textIndent top width wordSpacing zIndex'); - -Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; - -String.__parseStyleElement = document.createElement('div'); -String.prototype.parseStyle = function(){ - var style, styleRules = $H(); - if (Prototype.Browser.WebKit) - style = new Element('div',{style:this}).style; - else { - String.__parseStyleElement.innerHTML = '
      '; - style = String.__parseStyleElement.childNodes[0].style; - } - - Element.CSS_PROPERTIES.each(function(property){ - if (style[property]) styleRules.set(property, style[property]); - }); - - if (Prototype.Browser.IE && this.include('opacity')) - styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); - - return styleRules; -}; - -if (document.defaultView && document.defaultView.getComputedStyle) { - Element.getStyles = function(element) { - var css = document.defaultView.getComputedStyle($(element), null); - return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { - styles[property] = css[property]; - return styles; - }); - }; -} else { - Element.getStyles = function(element) { - element = $(element); - var css = element.currentStyle, styles; - styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { - results[property] = css[property]; - return results; - }); - if (!styles.opacity) styles.opacity = element.getOpacity(); - return styles; - }; -} - -Effect.Methods = { - morph: function(element, style) { - element = $(element); - new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); - return element; - }, - visualEffect: function(element, effect, options) { - element = $(element); - var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); - new Effect[klass](element, options); - return element; - }, - highlight: function(element, options) { - element = $(element); - new Effect.Highlight(element, options); - return element; - } -}; - -$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ - 'pulsate shake puff squish switchOff dropOut').each( - function(effect) { - Effect.Methods[effect] = function(element, options){ - element = $(element); - Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); - return element; - }; - } -); - -$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( - function(f) { Effect.Methods[f] = Element[f]; } -); - -Element.addMethods(Effect.Methods); \ No newline at end of file diff --git a/lib/scriptaculous/scriptaculous.js b/lib/scriptaculous/scriptaculous.js deleted file mode 100644 index 0ea5c4457..000000000 --- a/lib/scriptaculous/scriptaculous.js +++ /dev/null @@ -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('"; + - print_hidden("op", "pluginhandler"); - print_hidden("method", "save"); - print_hidden("plugin", "af_proxy_http"); +
      + +
      - $proxy_all = $this->host->get($this, "proxy_all"); - print_checkbox("proxy_all", $proxy_all); - print " 
      "; +
      - print "

      "; print_button("submit", __("Save")); - - print ""; - - print "

      "; + + +
      + host->set($this, "proxy_all", $proxy_all); diff --git a/plugins/af_psql_trgm/init.js b/plugins/af_psql_trgm/init.js index a22e673f6..921272c4b 100644 --- a/plugins/af_psql_trgm/init.js +++ b/plugins/af_psql_trgm/init.js @@ -1,15 +1,18 @@ -/* global dijit, Plugins, __ */ +/* global dijit, dojo, Plugins, xhr, __ */ Plugins.Psql_Trgm = { showRelated: function (id) { - const query = "backend.php?op=pluginhandler&plugin=af_psql_trgm&method=showrelated¶m=" + encodeURIComponent(id); - const dialog = new dijit.Dialog({ title: __("Related articles"), - execute: function () { - // - }, - href: query, + content: __("Loading, please wait...") + }); + + const tmph = dojo.connect(dialog, "onShow", null, function (/* e */) { + dojo.disconnect(tmph); + + xhr.post("backend.php", {op: 'pluginhandler', plugin: 'af_psql_trgm', method: 'showrelated', id: id}, (reply) => { + dialog.attr('content', reply); + }); }); dialog.show(); diff --git a/plugins/af_psql_trgm/init.php b/plugins/af_psql_trgm/init.php index 163b0ec38..5611d8998 100644 --- a/plugins/af_psql_trgm/init.php +++ b/plugins/af_psql_trgm/init.php @@ -15,7 +15,7 @@ class Af_Psql_Trgm extends Plugin { function save() { $similarity = (float) $_POST["similarity"]; $min_title_length = (int) $_POST["min_title_length"]; - $enable_globally = checkbox_to_sql_bool($_POST["enable_globally"]); + $enable_globally = checkbox_to_sql_bool($_POST["enable_globally"] ?? ""); if ($similarity < 0) $similarity = 0; if ($similarity > 1) $similarity = 1; @@ -46,7 +46,7 @@ class Af_Psql_Trgm extends Plugin { } function showrelated() { - $id = (int) $_REQUEST['param']; + $id = (int) $_REQUEST['id']; $owner_uid = $_SESSION["uid"]; $sth = $this->pdo->prepare("SELECT title FROM ttrss_entries, ttrss_user_entries @@ -124,115 +124,117 @@ class Af_Psql_Trgm extends Plugin { function hook_prefs_tab($args) { if ($args != "prefFeeds") return; - print "
      extension ".__('Mark similar articles as read (af_psql_trgm)')."\">"; + $similarity = $this->host->get($this, "similarity", $this->default_similarity); + $min_title_length = $this->host->get($this, "min_title_length", $this->default_min_length); + $enable_globally = sql_bool_to_bool($this->host->get($this, "enable_globally")); - if (DB_TYPE != "pgsql") { - print_error("Database type not supported."); - } else { + ?> - $res = $this->pdo->query("select 'similarity'::regproc"); +
      - if (!$res || !$res->fetch()) { - print_error("pg_trgm extension not found."); - } + pdo->query("select 'similarity'::regproc"); - $similarity = $this->host->get($this, "similarity", $this->default_similarity); - $min_title_length = $this->host->get($this, "min_title_length", $this->default_min_length); - $enable_globally = $this->host->get($this, "enable_globally"); - - print "
      "; - - print ""; + } ?> - print_hidden("op", "pluginhandler"); - print_hidden("method", "save"); - print_hidden("plugin", "af_psql_trgm"); + - print "

      " . __("Global settings") . "

      "; + - print_notice("Enable for specific feeds in the feed editor."); + - print "
      "; + - print " "; - print ""; +
      + + - print "
      " . - __("PostgreSQL trigram extension returns string similarity as a floating point number (0-1). Setting it too low might produce false positives, zero disables checking.") . - "
      "; +
      + +
      +
      - print "
      "; +
      + + +
      - print " "; - print ""; +
      + +
      - print "
      "; +
      - print ""; + + - print "
      "; + filter_unknown_feeds( + $this->get_stored_array("enabled_feeds")); - print_button("submit", __("Save"), "class='alt-primary'"); - print ""; + $this->host->set($this, "enabled_feeds", $enabled_feeds); + ?> - /* cleanup */ - $enabled_feeds = $this->filter_unknown_feeds( - $this->get_stored_array("enabled_feeds")); + 0) { ?> +
      +

      - $this->host->set($this, "enabled_feeds", $enabled_feeds); - - if (count($enabled_feeds) > 0) { - print "

      " . __("Currently enabled for (click to edit):") . "

      "; - - print ""; - } - } - - print "
      "; +
        + +
      • + rss_feed + + + +
      • + +
      + +
      + ".__("Similarity (af_psql_trgm)").""; - print "
      "; + $enabled_feeds = $this->get_stored_array("enabled_feeds"); + ?> +
      - $enabled_feeds = $this->get_stored_array("enabled_feeds"); - $checked = in_array($feed_id, $enabled_feeds) ? "checked" : ""; - - print "
      "; - - print ""; - - print "
      "; - - print "
      "; +
      +
      + +
      +
      + + pdo->query("select 'similarity'::regproc"); if (!$res || !$res->fetch()) return $article; diff --git a/plugins/af_readability/init.js b/plugins/af_readability/init.js index 3155475cc..0232ed32d 100644 --- a/plugins/af_readability/init.js +++ b/plugins/af_readability/init.js @@ -1,9 +1,11 @@ +/* global xhr, App, Plugins, Article, Notify */ + Plugins.Af_Readability = { orig_attr_name: 'data-readability-orig-content', self: this, embed: function(id) { - const content = $$(App.isCombinedMode() ? ".cdm[data-article-id=" + id + "] .content-inner" : - ".post[data-article-id=" + id + "] .content")[0]; + const content = App.find(App.isCombinedMode() ? `.cdm[data-article-id="${id}"] .content-inner` : + `.post[data-article-id="${id}"] .content`); if (content.hasAttribute(self.orig_attr_name)) { content.innerHTML = content.getAttribute(self.orig_attr_name); @@ -16,7 +18,7 @@ Plugins.Af_Readability = { Notify.progress("Loading, please wait..."); - xhrJson("backend.php",{ op: "pluginhandler", plugin: "af_readability", method: "embed", param: id }, (reply) => { + xhr.json("backend.php", App.getPhArgs("af_readability", "embed", {id: id}), (reply) => { if (content && reply.content) { content.setAttribute(self.orig_attr_name, content.innerHTML); diff --git a/plugins/af_readability/init.php b/plugins/af_readability/init.php index a76c98380..be9220cda 100755 --- a/plugins/af_readability/init.php +++ b/plugins/af_readability/init.php @@ -18,7 +18,7 @@ class Af_Readability extends Plugin { } function save() { - $enable_share_anything = checkbox_to_sql_bool($_POST["enable_share_anything"]); + $enable_share_anything = checkbox_to_sql_bool($_POST["enable_share_anything"] ?? ""); $this->host->set($this, "enable_share_anything", $enable_share_anything); @@ -29,11 +29,6 @@ class Af_Readability extends Plugin { { $this->host = $host; - if (version_compare(PHP_VERSION, '7.0.0', '<')) { - user_error("af_readability requires PHP 7.0", E_USER_WARNING); - return; - } - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); $host->add_hook($host::HOOK_PREFS_TAB, $this); $host->add_hook($host::HOOK_PREFS_EDIT_FEED, $this); @@ -60,99 +55,92 @@ class Af_Readability extends Plugin { function hook_prefs_tab($args) { if ($args != "prefFeeds") return; - print "
      extension ".__('Readability settings (af_readability)')."\">"; + $enable_share_anything = sql_bool_to_bool($this->host->get($this, "enable_share_anything")); - if (version_compare(PHP_VERSION, '7.0.0', '<')) { - print_error("This plugin requires PHP 7.0."); - } else { + ?> +
      - print "

      " . __("Global settings") . "

      "; + - print_notice("Enable for specific feeds in the feed editor."); +
      - print ""; + - print ""; + - print_hidden("op", "pluginhandler"); - print_hidden("method", "save"); - print_hidden("plugin", "af_readability"); +
      + +
      - $enable_share_anything = $this->host->get($this, "enable_share_anything"); +
      - print "
      "; - print ""; - print "
      "; + +
      - print_button("submit", __("Save"), "class='alt-primary'"); - print ""; + filter_unknown_feeds( + $this->get_stored_array("enabled_feeds")); - /* cleanup */ - $enabled_feeds = $this->filter_unknown_feeds( - $this->get_stored_array("enabled_feeds")); + $append_feeds = $this->filter_unknown_feeds( + $this->get_stored_array("append_feeds")); - $append_feeds = $this->filter_unknown_feeds( - $this->get_stored_array("append_feeds")); + $this->host->set($this, "enabled_feeds", $enabled_feeds); + $this->host->set($this, "append_feeds", $append_feeds); + ?> - $this->host->set($this, "enabled_feeds", $enabled_feeds); - $this->host->set($this, "append_feeds", $append_feeds); + 0) { ?> +
      +

      - if (count($enabled_feeds) > 0) { - print "

      " . __("Currently enabled for (click to edit):") . "

      "; - - print ""; - } - - } - - print "
      "; +
        + +
      • + rss_feed + + + +
      • + +
      + +
      + ".__("Readability").""; - print "
      "; - $enabled_feeds = $this->get_stored_array("enabled_feeds"); $append_feeds = $this->get_stored_array("append_feeds"); + ?> - $enable_checked = in_array($feed_id, $enabled_feeds) ? "checked" : ""; - $append_checked = in_array($feed_id, $append_feeds) ? "checked" : ""; - - print "
      "; - - print ""; - - print "
      "; - - print ""; - - print "
      "; +
      +
      +
      + +
      +
      + +
      +
      + pdo->prepare("SELECT link FROM ttrss_entries WHERE id = ?"); $sth->execute([$article_id]); diff --git a/plugins/af_redditimgur/init.php b/plugins/af_redditimgur/init.php index 2e89fcdff..1aa4793ea 100755 --- a/plugins/af_redditimgur/init.php +++ b/plugins/af_redditimgur/init.php @@ -31,65 +31,62 @@ class Af_RedditImgur extends Plugin { function hook_prefs_tab($args) { if ($args != "prefFeeds") return; - print "
      extension ".__('Reddit content settings (af_redditimgur)')."\">"; + $enable_readability = $this->host->get($this, "enable_readability"); + $enable_content_dupcheck = $this->host->get($this, "enable_content_dupcheck"); + $reddit_to_teddit = $this->host->get($this, "reddit_to_teddit"); + ?> - $enable_readability = $this->host->get($this, "enable_readability"); - $enable_content_dupcheck = $this->host->get($this, "enable_content_dupcheck"); - $reddit_to_teddit = $this->host->get($this, "reddit_to_teddit"); +
      - if (version_compare(PHP_VERSION, '5.6.0', '<')) { - print_error("Readability requires PHP version 5.6."); - } +
      - print ""; + - print ""; + - print_hidden("op", "pluginhandler"); - print_hidden("method", "save"); - print_hidden("plugin", "af_redditimgur"); +
      + +
      - print "
      "; - print ""; - print "
      "; +
      + +
      - print "
      "; - print ""; - print "
      "; +
      + +
      - print "
      "; - print ""; +
      + + +
      - print_button("submit", __("Save"), 'class="alt-primary"'); - print ""; - - print "
      "; + host->set($this, "enable_readability", $enable_readability, false); $this->host->set($this, "reddit_to_teddit", $reddit_to_teddit, false); @@ -220,6 +217,7 @@ class Af_RedditImgur extends Plugin { $this->fallback_preview_urls = []; + // @phpstan-ignore-next-line if ($tmp && $anchor) { $json = json_decode($tmp, true); @@ -349,6 +347,8 @@ class Af_RedditImgur extends Plugin { if (strpos($source_stream, "imgur.com") !== false) $poster_url = str_replace(".mp4", "h.jpg", $source_stream); + else + $poster_url = false; $this->handle_as_video($doc, $entry, $source_stream, $poster_url); @@ -530,7 +530,7 @@ class Af_RedditImgur extends Plugin { $entry_guid = $article["guid_hashed"]; $owner_uid = $article["owner_uid"]; - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $interval_qpart = "date_entered < NOW() - INTERVAL '1 day'"; } else { $interval_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 DAY)"; @@ -631,6 +631,10 @@ class Af_RedditImgur extends Plugin { $entry->parentNode->insertBefore($img, $entry);*/ } + function csrf_ignore($method) { + return $method === "testurl"; + } + function testurl() { $url = clean($_POST["url"]); @@ -645,14 +649,17 @@ class Af_RedditImgur extends Plugin { fieldset { border : 0; } label { display : inline-block; min-width : 120px; } -
      + + + +
      - +
      - +
      @@ -694,7 +701,7 @@ class Af_RedditImgur extends Plugin { private function get_header($url, $header, $useragent = SELF_USER_AGENT) { $ret = false; - if (function_exists("curl_init") && !defined("NO_CURL")) { + if (function_exists("curl_init")) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_TIMEOUT, 5); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); @@ -720,7 +727,7 @@ class Af_RedditImgur extends Plugin { private function readability($article, $url, $doc, $xpath, $debug = false) { - if (!defined('NO_CURL') && function_exists("curl_init") && $this->host->get($this, "enable_readability") && + if (function_exists("curl_init") && $this->host->get($this, "enable_readability") && mb_strlen(strip_tags($article["content"])) <= 150) { // do not try to embed posts linking back to other reddit posts diff --git a/plugins/af_tumblr_1280/init.php b/plugins/af_tumblr_1280/init.php deleted file mode 100755 index 5d7f366a4..000000000 --- a/plugins/af_tumblr_1280/init.php +++ /dev/null @@ -1,91 +0,0 @@ - true); - } - - function init($host) { - $this->host = $host; - - if (function_exists("curl_init")) { - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); - } - } - - function hook_article_filter($article) { - - if (!function_exists("curl_init") || ini_get("open_basedir")) - return $article; - - $doc = new DOMDocument(); - $doc->loadHTML('' . $article["content"]); - - $found = false; - - if ($doc) { - $xpath = new DOMXpath($doc); - - $images = $xpath->query('(//img[contains(@src, \'media.tumblr.com\')])'); - - foreach ($images as $img) { - $src = $img->getAttribute("src"); - - $test_src = preg_replace("/_\d{3}.(jpg|gif|png)/", "_1280.$1", $src); - - if ($src != $test_src) { - - $ch = curl_init($test_src); - curl_setopt($ch, CURLOPT_TIMEOUT, 5); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_NOBODY, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_USERAGENT, SELF_USER_AGENT); - - @$result = curl_exec($ch); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - if ($result && $http_code == 200) { - $img->setAttribute("src", $test_src); - $found = true; - } - } - } - - $video_sources = $xpath->query('//video/source[contains(@src, \'.tumblr.com/video_file\')]'); - - foreach ($video_sources as $source) { - $src = $source->getAttribute("src"); - - $new_src = preg_replace("/\/\d{3}$/", "", $src); - - if ($src != $new_src) { - $source->setAttribute("src", $new_src); - $found = true; - } - } - - if ($found) { - $doc->removeChild($doc->firstChild); //remove doctype - $article["content"] = $doc->saveHTML(); - } - } - - return $article; - - } - - - function api_version() { - return 2; - } - -} diff --git a/plugins/af_unburn/init.php b/plugins/af_unburn/init.php index 4d0c56740..386b6387f 100755 --- a/plugins/af_unburn/init.php +++ b/plugins/af_unburn/init.php @@ -21,7 +21,7 @@ class Af_Unburn extends Plugin { function hook_article_filter($article) { $owner_uid = $article["owner_uid"]; - if (defined('NO_CURL') || !function_exists("curl_init") || ini_get("open_basedir")) + if (!function_exists("curl_init") || ini_get("open_basedir")) return $article; if ((strpos($article["link"], "feedproxy.google.com") !== false || @@ -37,8 +37,8 @@ class Af_Unburn extends Plugin { curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_USERAGENT, SELF_USER_AGENT); - if (defined('_CURL_HTTP_PROXY')) { - curl_setopt($ch, CURLOPT_PROXY, _CURL_HTTP_PROXY); + if (Config::get(Config::HTTP_PROXY)) { + curl_setopt($ch, CURLOPT_PROXY, Config::get(Config::HTTP_PROXY)); } @curl_exec($ch); @@ -80,4 +80,4 @@ class Af_Unburn extends Plugin { return 2; } -} \ No newline at end of file +} diff --git a/plugins/af_youtube_embed/init.php b/plugins/af_youtube_embed/init.php index db82dc9f5..6309aac02 100644 --- a/plugins/af_youtube_embed/init.php +++ b/plugins/af_youtube_embed/init.php @@ -23,9 +23,9 @@ class Af_Youtube_Embed extends Plugin { $matches = array(); - if (preg_match("/\/\/www\.youtube\.com\/v\/([\w-]+)/", $entry["url"], $matches) || - preg_match("/\/\/www\.youtube\.com\/watch?v=([\w-]+)/", $entry["url"], $matches) || - preg_match("/\/\/youtu.be\/([\w-]+)/", $entry["url"], $matches)) { + if (preg_match("/\/\/www\.youtube\.com\/v\/([\w-]+)/", $entry["content_url"], $matches) || + preg_match("/\/\/www\.youtube\.com\/watch?v=([\w-]+)/", $entry["content_url"], $matches) || + preg_match("/\/\/youtu.be\/([\w-]+)/", $entry["content_url"], $matches)) { $vid_id = $matches[1]; diff --git a/plugins/af_zz_vidmute/init.js b/plugins/af_zz_vidmute/init.js index fab9b99e6..b8be8cecd 100644 --- a/plugins/af_zz_vidmute/init.js +++ b/plugins/af_zz_vidmute/init.js @@ -3,7 +3,7 @@ require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) { PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED_CDM, function (row) { if (row) { - row.select("video").each(function (v) { + row.querySelectorAll("video").forEach(function (v) { v.muted = true; }); } @@ -14,7 +14,7 @@ require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) { PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED, function (row) { if (row) { - row.select("video").each(function (v) { + row.querySelectorAll("video").forEach(function (v) { v.muted = true; }); } diff --git a/plugins/auth_internal/init.php b/plugins/auth_internal/init.php index a69ea444c..13a7bc969 100644 --- a/plugins/auth_internal/init.php +++ b/plugins/auth_internal/init.php @@ -63,21 +63,21 @@ class Auth_Internal extends Auth_Base { Tiny Tiny RSS - + -

      +

      - - - - "> - "> - "> + + + "> + "> + ">
      - +
      @@ -244,7 +244,7 @@ class Auth_Internal extends Auth_Base { $tpl->readTemplateFromFile("password_change_template.txt"); $tpl->setVariable('LOGIN', $row["login"]); - $tpl->setVariable('TTRSS_HOST', SELF_URL_PATH); + $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH)); $tpl->addBlock('message'); diff --git a/plugins/auth_remote/init.php b/plugins/auth_remote/init.php index 85be67d05..f2dcfb318 100644 --- a/plugins/auth_remote/init.php +++ b/plugins/auth_remote/init.php @@ -56,7 +56,7 @@ class Auth_Remote extends Auth_Base { $_SESSION["hide_logout"] = true; // LemonLDAP can send user informations via HTTP HEADER - if (defined('AUTH_AUTO_CREATE') && AUTH_AUTO_CREATE){ + if (Config::get(Config::AUTH_AUTO_CREATE)) { // update user name $fullname = isset($_SERVER['HTTP_USER_NAME']) ? $_SERVER['HTTP_USER_NAME'] : ($_SERVER['AUTHENTICATE_CN'] ?? ""); if ($fullname){ diff --git a/plugins/auto_assign_labels/init.php b/plugins/auto_assign_labels/init.php index 3fa4ad8c0..341895cef 100755 --- a/plugins/auto_assign_labels/init.php +++ b/plugins/auto_assign_labels/init.php @@ -19,6 +19,7 @@ class Auto_Assign_Labels extends Plugin { function get_all_labels_filter_format($owner_uid) { $rv = array(); + // TODO: use Labels::get_all() $sth = $this->pdo->prepare("SELECT id, fg_color, bg_color, caption FROM ttrss_labels2 WHERE owner_uid = ?"); $sth->execute([$owner_uid]); diff --git a/plugins/bookmarklets/init.php b/plugins/bookmarklets/init.php index fa1bb8cf6..967918823 100644 --- a/plugins/bookmarklets/init.php +++ b/plugins/bookmarklets/init.php @@ -1,53 +1,371 @@ host = $host; + function init($host) { + $this->host = $host; - $host->add_hook($host::HOOK_PREFS_TAB, $this); - } + $host->add_hook($host::HOOK_PREFS_TAB, $this); + } - function hook_prefs_tab($args) { - if ($args == "prefFeeds") { + function is_public_method($method) { + return in_array($method, ["subscribe", "sharepopup"]); + } - print "
      bookmark ".__('Bookmarklets')."\">"; + function subscribe() { + if (Config::get(Config::SINGLE_USER_MODE)) { + UserHelper::login_sequence(); + } - print "

      " . __("Drag the link below to your browser toolbar, open the feed you're interested in in your browser and click on the link to subscribe to it.") . "

      "; + if (!empty($_SESSION["uid"])) { - $bm_subscribe_url = str_replace('%s', '', Pref_Feeds::subscribe_to_feed_url()); + $feed_url = clean($_REQUEST["feed_url"] ?? ""); + $csrf_token = clean($_POST["csrf_token"] ?? ""); - $confirm_str = str_replace("'", "\'", __('Subscribe to %s in Tiny Tiny RSS?')); + header('Content-Type: text/html; charset=utf-8'); + ?> + + + + <?= __("Subscribe to feed...") ?> + + + + + + + + + + + +
      +

      +
      +

      "; + if (!$feed_url || !validate_csrf($csrf_token)) { + ?> + + + - print "

      " . __("Use this bookmarklet to publish arbitrary pages using Tiny Tiny RSS") . "

      "; +
      + + +
      - print ""; + - print ""; + + + "; #pane + $rc = Feeds::_subscribe($feed_url); + $feed_urls = false; - } - } + switch ($rc['code']) { + case 0: + print_warning(T_sprintf("Already subscribed to %s.", $feed_url)); + break; + case 1: + print_notice(T_sprintf("Subscribed to %s.", $feed_url)); + break; + case 2: + print_error(T_sprintf("Could not subscribe to %s.", $feed_url)); + break; + case 3: + print_error(T_sprintf("No feeds found in %s.", $feed_url)); + break; + case 4: + $feed_urls = $rc["feeds"]; + break; + case 5: + print_error(T_sprintf("Could not subscribe to %s.
      Can't download the Feed URL.", $feed_url)); + break; + } + + if ($feed_urls) { + ?> +
      + + + +
      + + +
      + + + +
      + +
      "> + + + + + +
      + + + +
      +
      + + + + + + + <?= __("Share with Tiny Tiny RSS") ?> + + + + + + + + + + + +
      + + + + +
      + + + + + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + + +
      + +
      + +
      + + "window.close()"]) ?> + +
      + +
      + + +
      + + + +
      + + " /> +
      + +
      + + + "/> +
      + +
      + +
      + + + +
      + +
      + +
      + + + host->get_public_method_url($this, "subscribe"); + $bm_share_url = $this->host->get_public_method_url($this, "sharepopup"); + + $confirm_str = str_replace("'", "\'", __('Subscribe to %s in Tiny Tiny RSS?')); + + $bm_subscribe_url = htmlspecialchars("javascript:{if(confirm('$confirm_str'.replace('%s',window.location.href)))window.location.href='$bm_subscribe_url&feed_url='+encodeURIComponent(window.location.href)}"); + $bm_share_url = htmlspecialchars("javascript:(function(){var d=document,w=window,e=w.getSelection,k=d.getSelection,x=d.selection,s=(e?e():(k)?k():(x?x.createRange().text:0)),f='$bm_share_url',l=d.location,e=encodeURIComponent,g=f+'&title='+((e(s))?e(s):e(document.title))+'&url='+e(l.href);function a(){if(!w.open(g,'t','toolbar=0,resizable=0,scrollbars=1,status=1,width=500,height=250')){l.href=g;}}a();})()"); + ?> + +
      + +

      + + + +

      + + + + 'alt-info', "onclick" => "window.open('https://tt-rss.org/wiki/ShareAnything')"]) ?> + +
      + + host = $host; $this->cache = new DiskCache("starred-images"); - if ($this->cache->makeDir()) - chmod($this->cache->getDir(), 0777); + if ($this->cache->make_dir()) + chmod($this->cache->get_dir(), 0777); if (!$this->cache->exists(".no-auto-expiry")) $this->cache->touch(".no-auto-expiry"); - if ($this->cache->isWritable()) { + if ($this->cache->is_writable()) { $host->add_hook($host::HOOK_HOUSE_KEEPING, $this); $host->add_hook($host::HOOK_ENCLOSURE_ENTRY, $this); $host->add_hook($host::HOOK_SANITIZE, $this); } else { - user_error("Starred cache directory ".$this->cache->getDir()." is not writable.", E_USER_WARNING); + user_error("Starred cache directory ".$this->cache->get_dir()." is not writable.", E_USER_WARNING); } } @@ -38,13 +38,13 @@ class Cache_Starred_Images extends Plugin { Debug::log("caching media of starred articles for user " . $this->host->get_owner_uid() . "..."); $sth = $this->pdo->prepare("SELECT content, ttrss_entries.title, - ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data + ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON (ttrss_user_entries.feed_id = ttrss_feeds.id) WHERE ref_id = ttrss_entries.id AND marked = true AND site_url != '' AND - ttrss_user_entries.owner_uid = ? AND + ttrss_user_entries.owner_uid = ? AND plugin_data NOT LIKE '%starred_cache_images%' ORDER BY ".Db::sql_random_function()." LIMIT 100"); @@ -59,7 +59,7 @@ class Cache_Starred_Images extends Plugin { $success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]); if ($success) { - $plugin_data = "starred_cache_images,${line['owner_uid']}:" . $line["plugin_data"]; + $plugin_data = "starred_cache_images," . $line["owner_uid"] . ":" . $line["plugin_data"]; $usth->execute([$plugin_data, $line['id']]); } @@ -69,9 +69,12 @@ class Cache_Starred_Images extends Plugin { /* actual housekeeping */ - Debug::log("expiring " . $this->cache->getDir() . "..."); + Debug::log("expiring " . $this->cache->get_dir() . "..."); - $files = glob($this->cache->getDir() . "/*.{png,mp4,status}", GLOB_BRACE); + $files = array_merge( + glob($this->cache->get_dir() . "/*.png"), + glob($this->cache->get_dir() . "/*.mp4"), + glob($this->cache->get_dir() . "/*.status")); $last_article_id = 0; $article_exists = 1; @@ -98,14 +101,14 @@ class Cache_Starred_Images extends Plugin { $local_filename = $article_id . "-" . sha1($enc["content_url"]); if ($this->cache->exists($local_filename)) { - $enc["content_url"] = $this->cache->getUrl($local_filename); + $enc["content_url"] = $this->cache->get_url($local_filename); } return $enc; } function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) { - $xpath = new DOMXpath($doc); + $xpath = new DOMXPath($doc); if ($article_id) { $entries = $xpath->query('(//img[@src])|(//video/source[@src])'); @@ -117,7 +120,7 @@ class Cache_Starred_Images extends Plugin { $local_filename = $article_id . "-" . sha1($src); if ($this->cache->exists($local_filename)) { - $entry->setAttribute("src", $this->cache->getUrl($local_filename)); + $entry->setAttribute("src", $this->cache->get_url($local_filename)); $entry->removeAttribute("srcset"); } } @@ -133,7 +136,7 @@ class Cache_Starred_Images extends Plugin { if (!$this->cache->exists($local_filename)) { Debug::log("cache_images: downloading: $url to $local_filename", Debug::$LOG_VERBOSE); - $data = UrlHelper::fetch(["url" => $url, "max_size" => MAX_CACHE_FILE_SIZE]); + $data = UrlHelper::fetch(["url" => $url, "max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)]); if ($data) return $this->cache->put($local_filename, $data);; @@ -151,37 +154,37 @@ class Cache_Starred_Images extends Plugin { $status_filename = $article_id . "-" . sha1($site_url) . ".status"; /* housekeeping might run as a separate user, in this case status/media might not be writable */ - if (!$this->cache->isWritable($status_filename)) { + if (!$this->cache->is_writable($status_filename)) { Debug::log("status not writable: $status_filename", Debug::$LOG_VERBOSE); return false; } Debug::log("status: $status_filename", Debug::$LOG_VERBOSE); - if ($this->cache->exists($status_filename)) - $status = json_decode($this->cache->get($status_filename), true); - else - $status = []; + if ($this->cache->exists($status_filename)) + $status = json_decode($this->cache->get($status_filename), true); + else + $status = ["attempt" => 0]; - $status["attempt"] += 1; + $status["attempt"] += 1; - // only allow several download attempts for article - if ($status["attempt"] > $this->max_cache_attempts) { - Debug::log("too many attempts for $site_url", Debug::$LOG_VERBOSE); - return false; - } + // only allow several download attempts for article + if ($status["attempt"] > $this->max_cache_attempts) { + Debug::log("too many attempts for $site_url", Debug::$LOG_VERBOSE); + return false; + } - if (!$this->cache->put($status_filename, json_encode($status))) { - user_error("unable to write status file: $status_filename", E_USER_WARNING); - return false; - } + if (!$this->cache->put($status_filename, json_encode($status))) { + user_error("unable to write status file: $status_filename", E_USER_WARNING); + return false; + } $doc = new DOMDocument(); $has_images = false; $success = false; - if (@$doc->loadHTML('' . $content)) { + if (@$doc->loadHTML('' . $content)) { $xpath = new DOMXPath($doc); $entries = $xpath->query('(//img[@src])|(//video/source[@src])'); @@ -203,11 +206,11 @@ class Cache_Starred_Images extends Plugin { $esth = $this->pdo->prepare("SELECT content_url FROM ttrss_enclosures WHERE post_id = ? AND (content_type LIKE '%image%' OR content_type LIKE '%video%')"); - if ($esth->execute([$article_id])) { - while ($enc = $esth->fetch()) { + if ($esth->execute([$article_id])) { + while ($enc = $esth->fetch()) { - $has_images = true; - $url = rewrite_relative_url($site_url, $enc["content_url"]); + $has_images = true; + $url = rewrite_relative_url($site_url, $enc["content_url"]); if ($this->cache_url($article_id, $url)) { $success = true; diff --git a/plugins/close_button/init.php b/plugins/close_button/init.php index a2ba89478..4f33d1af0 100644 --- a/plugins/close_button/init.php +++ b/plugins/close_button/init.php @@ -15,7 +15,7 @@ class Close_Button extends Plugin { } function get_css() { - return "i.icon-close-article { color : red; }"; + return ".post .header .buttons i.material-icons.icon-close-article { color : red; }"; } function hook_article_button($line) { diff --git a/plugins/mail/init.php b/plugins/mail/init.php index 40d147fc9..467f8294a 100644 --- a/plugins/mail/init.php +++ b/plugins/mail/init.php @@ -15,10 +15,15 @@ class Mail extends Plugin { $host->add_hook($host::HOOK_ARTICLE_BUTTON, $this); $host->add_hook($host::HOOK_PREFS_TAB, $this); + $host->add_hook($host::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, $this); } function get_js() { - return file_get_contents(dirname(__FILE__) . "/mail.js"); + return file_get_contents(__DIR__ . "/mail.js"); + } + + function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) { + return "
      ".__('Forward by email')."
      "; } function save() { @@ -32,42 +37,38 @@ class Mail extends Plugin { function hook_prefs_tab($args) { if ($args != "prefPrefs") return; - print "
      mail ".__('Mail plugin')."\">"; + $addresslist = $this->host->get($this, "addresslist"); - print "

      " . __("You can set predefined email addressed here (comma-separated list):") . "

      "; + ?> - print "
      "; +
      - print ""; + - print_hidden("op", "pluginhandler"); - print_hidden("method", "save"); - print_hidden("plugin", "mail"); +
      - $addresslist = $this->host->get($this, "addresslist"); + - print ""; +
      - print "

      "; + - print "

      "; - - print "
      "; + +
      + pdo->prepare("SELECT email, full_name FROM ttrss_users WHERE id = ?"); @@ -100,9 +98,6 @@ class Mail extends Plugin { if (!$user_name) $user_name = $_SESSION['name']; - print_hidden("from_email", "$user_email"); - print_hidden("from_name", "$user_name"); - $tpl = new Templator(); $tpl->readTemplateFromFile("email_article_template.txt"); @@ -143,46 +138,56 @@ class Mail extends Plugin { $content = ""; $tpl->generateOutputToString($content); - print ""; - - print "
      "; - $addresslist = explode(",", $this->host->get($this, "addresslist")); - print __('To:'); + ?> - print ""; +
      -/* print ""; */ + - print_select("destination", "", $addresslist, 'style="width: 30em" dojoType="dijit.form.ComboBox"'); + + -/* print "
      "; */ + - print "
      "; +
      +
      + + "width: 380px", "required" => 1, "dojoType" => "dijit.form.ComboBox"]) ?> +
      +
      - print __('Subject:'); +
      +
      + + +
      +
      - print "
      "; + - print ""; +
      + + +
      - print "
      "; - - print "
      "; - - print "
      "; - print " "; - print ""; - print "
      "; - - //return; + + "; - - foreach ($_SESSION['stored_emails'] as $email) { - if (strpos($email, $search) !== false) { - print "
    • $email
    • "; - } - } - - print ""; - } */ - function api_version() { return 2; } diff --git a/plugins/mail/mail.js b/plugins/mail/mail.js index 5ddc0dc41..d2bafe0e9 100644 --- a/plugins/mail/mail.js +++ b/plugins/mail/mail.js @@ -1,4 +1,4 @@ -/* global Plugins, Headlines, xhrJson, Notify, fox, __ */ +/* global Plugins, Headlines, dojo, App, xhr, Notify, fox, __ */ Plugins.Mail = { send: function(id) { @@ -13,14 +13,11 @@ Plugins.Mail = { id = ids.toString(); } - const query = "backend.php?op=pluginhandler&plugin=mail&method=emailArticle¶m=" + encodeURIComponent(id); - const dialog = new fox.SingleUseDialog({ - id: "emailArticleDlg", title: __("Forward article by email"), execute: function () { if (this.validate()) { - xhrJson("backend.php", this.attr('value'), (reply) => { + xhr.json("backend.php", this.attr('value'), (reply) => { if (reply) { const error = reply['error']; @@ -35,16 +32,16 @@ Plugins.Mail = { }); } }, - href: query + content: __("Loading, please wait...") }); - /* var tmph = dojo.connect(dialog, 'onLoad', function() { - dojo.disconnect(tmph); + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); - new Ajax.Autocompleter('emailArticleDlg_destination', 'emailArticleDlg_dst_choices', - "backend.php?op=pluginhandler&plugin=mail&method=completeEmails", - { tokens: '', paramName: "search" }); - }); */ + xhr.post("backend.php", App.getPhArgs("mail", "emailArticle", {ids: id}), (reply) => { + dialog.attr('content', reply); + }); + }); dialog.show(); }, diff --git a/plugins/mail/mail.png b/plugins/mail/mail.png deleted file mode 100644 index 7348aed77..000000000 Binary files a/plugins/mail/mail.png and /dev/null differ diff --git a/plugins/mailto/init.js b/plugins/mailto/init.js index ae68bf49b..4a9557249 100644 --- a/plugins/mailto/init.js +++ b/plugins/mailto/init.js @@ -1,4 +1,4 @@ -/* global Plugins, Headlines, fox, __ */ +/* global Plugins, Headlines, xhr, dojo, fox, __ */ Plugins.Mailto = { send: function (id) { @@ -13,12 +13,19 @@ Plugins.Mailto = { id = ids.toString(); } - const query = "backend.php?op=pluginhandler&plugin=mailto&method=emailArticle¶m=" + encodeURIComponent(id); - const dialog = new fox.SingleUseDialog({ - id: "emailArticleDlg", - title: __("Forward article by email"), - href: query}); + title: __("Forward article by email (mailto:)"), + content: __("Loading, please wait...") + }); + + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + xhr.post("backend.php", App.getPhArgs("mailto", "emailArticle", {ids: id}), (reply) => { + dialog.attr('content', reply); + }); + }); + dialog.show(); } diff --git a/plugins/mailto/init.php b/plugins/mailto/init.php index 390984b71..c34b400ce 100644 --- a/plugins/mailto/init.php +++ b/plugins/mailto/init.php @@ -12,21 +12,26 @@ class MailTo extends Plugin { $this->host = $host; $host->add_hook($host::HOOK_ARTICLE_BUTTON, $this); + $host->add_hook($host::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, $this); + } + + function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) { + return "
      ".__('Forward by email (mailto:)')."
      "; } function get_js() { - return file_get_contents(dirname(__FILE__) . "/init.js"); + return file_get_contents(__DIR__ . "/init.js"); } function hook_article_button($line) { return "mail_outline"; + title='".__('Forward by email (mailto:)')."'>mail_outline"; } function emailArticle() { - $ids = explode(",", $_REQUEST['param']); + $ids = explode(",", clean($_REQUEST['ids'])); $ids_qmarks = arr_qmarks($ids); $tpl = new Templator(); @@ -37,7 +42,6 @@ class MailTo extends Plugin { //$tpl->setVariable('USER_EMAIL', $user_email, true); $tpl->setVariable('TTRSS_HOST', $_SERVER["HTTP_HOST"], true); - $sth = $this->pdo->prepare("SELECT DISTINCT link, content, title FROM ttrss_user_entries, ttrss_entries WHERE id = ref_id AND id IN ($ids_qmarks) AND owner_uid = ?"); @@ -65,25 +69,23 @@ class MailTo extends Plugin { $content = ""; $tpl->generateOutputToString($content); - $mailto_link = htmlspecialchars("mailto:?subject=".rawurlencode($subject). - "&body=".rawurlencode($content)); + $mailto_link = "mailto:?subject=".rawurlencode($subject)."&body=".rawurlencode($content); - print __("Clicking the following link to invoke your mail client:"); + ?> - print ""; +
      +
      + + + +
      +
      - print __("You should be able to edit the message before sending in your mail client."); +
      + +
      - print "

      "; - - print "

      "; - print ""; - print "
      "; - - //return; + pdo->prepare("SELECT note FROM ttrss_user_entries WHERE ref_id = ? AND owner_uid = ?"); - $sth->execute([$param, $_SESSION['uid']]); + $sth->execute([$id, $_SESSION['uid']]); if ($row = $sth->fetch()) { $note = $row['note']; - print_hidden("id", "$param"); - print_hidden("op", "pluginhandler"); - print_hidden("method", "setNote"); - print_hidden("plugin", "note"); + print \Controls\hidden_tag("id", $id); + print \Controls\pluginhandler_tags($this, "setnote"); - print ""; - + name='note'> + "; - print " "; - print ""; - print ""; - + ?> +
      + + +
      + pdo->prepare("UPDATE ttrss_user_entries SET note = ? WHERE ref_id = ? AND owner_uid = ?"); $sth->execute([$note, $id, $_SESSION['uid']]); - $formatted_note = Article::format_article_note($id, $note); - - print json_encode(array("note" => $formatted_note, - "raw_length" => mb_strlen($note))); + print json_encode(["id" => $id, "note" => $note]); } function api_version() { diff --git a/plugins/note/note.js b/plugins/note/note.js index ab2ed9208..a46acb355 100644 --- a/plugins/note/note.js +++ b/plugins/note/note.js @@ -1,36 +1,39 @@ -/* global Plugins, xhrJson, Notify, fox, __ */ +/* global dojo, Plugins, xhr, App, Notify, fox, __ */ Plugins.Note = { edit: function(id) { - const query = "backend.php?op=pluginhandler&plugin=note&method=edit¶m=" + encodeURIComponent(id); - const dialog = new fox.SingleUseDialog({ - id: "editNoteDlg", title: __("Edit article note"), execute: function () { if (this.validate()) { Notify.progress("Saving article note...", true); - xhrJson("backend.php", this.attr('value'), (reply) => { + xhr.json("backend.php", this.attr('value'), (reply) => { Notify.close(); dialog.hide(); if (reply) { - const elem = $("POSTNOTE-" + id); + App.findAll(`div[data-note-for="${reply.id}"]`).forEach((elem) => { + elem.querySelector(".body").innerHTML = reply.note; - if (elem) { - elem.innerHTML = reply.note; - - if (reply.raw_length != 0) - Element.show(elem); + if (reply.note) + elem.show(); else - Element.hide(elem); - } + elem.hide(); + }); } }); } }, - href: query, + content: __("Loading, please wait...") + }); + + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + xhr.post("backend.php", App.getPhArgs("note", "edit", {id: id}), (reply) => { + dialog.attr('content', reply); + }); }); dialog.show(); diff --git a/plugins/nsfw/init.js b/plugins/nsfw/init.js index adb6d43c0..4bc2443e8 100644 --- a/plugins/nsfw/init.js +++ b/plugins/nsfw/init.js @@ -1,7 +1,12 @@ -function nsfwShow(elem) { - let content = elem.parentNode.getElementsBySelector("div.nswf.content")[0]; +/* global Plugins */ - if (content) { - Element.toggle(content); +Plugins.NSFW = { + toggle: function(elem) { + const content = elem.domNode.parentNode.querySelector(".nswf.content"); + + if (content) { + Element.toggle(content); + } } } + diff --git a/plugins/nsfw/init.php b/plugins/nsfw/init.php index 02344eb14..7c5b8d00f 100644 --- a/plugins/nsfw/init.php +++ b/plugins/nsfw/init.php @@ -19,7 +19,7 @@ class NSFW extends Plugin { } function get_js() { - return file_get_contents(dirname(__FILE__) . "/init.js"); + return file_get_contents(__DIR__ . "/init.js"); } function hook_render_article($article) { @@ -27,74 +27,60 @@ class NSFW extends Plugin { $a_tags = array_map("trim", explode(",", $article["tag_cache"])); if (count(array_intersect($tags, $a_tags)) > 0) { - $article["content"] = "
      -
      "; + $article["content"] = "
      ". + \Controls\button_tag(__("Not work safe (click to toggle)"), '', ['onclick' => 'Plugins.NSFW.toggle(this)']). + " +
      "; } return $article; } function hook_render_article_cdm($article) { - $tags = array_map("trim", explode(",", $this->host->get($this, "tags"))); - $a_tags = array_map("trim", explode(",", $article["tag_cache"])); - - if (count(array_intersect($tags, $a_tags)) > 0) { - $article["content"] = "
      -
      "; - } - - return $article; + return $this->hook_render_article($article); } function hook_prefs_tab($args) { if ($args != "prefPrefs") return; - print "
      extension ".__("NSFW Plugin")."\">"; - - print "
      "; - $tags = $this->host->get($this, "tags"); - print "
      "; + ?> +
      "> + - print ""; + - print_hidden("op", "pluginhandler"); - print_hidden("method", "save"); - print_hidden("plugin", "nsfw"); +
      - print ""; +
      + +
      - print ""; - print ""; +
      - print "
      ".__("Tags to consider NSFW (comma-separated)")."
      "; - - print "

      "; - - print "

      "; - - print "
      "; #pane + + +
      + host->set($this, "tags", $tags); diff --git a/plugins/share/init.php b/plugins/share/init.php index 0794f5125..37799fba6 100644 --- a/plugins/share/init.php +++ b/plugins/share/init.php @@ -16,19 +16,22 @@ class Share extends Plugin { $host->add_hook($host::HOOK_PREFS_TAB_SECTION, $this); } + function is_public_method($method) { + return $method == "get"; + } + function get_js() { - return file_get_contents(dirname(__FILE__) . "/share.js"); + return file_get_contents(__DIR__ . "/share.js"); } function get_css() { - return file_get_contents(dirname(__FILE__) . "/share.css"); + return file_get_contents(__DIR__ . "/share.css"); } function get_prefs_js() { - return file_get_contents(dirname(__FILE__) . "/share_prefs.js"); + return file_get_contents(__DIR__ . "/share_prefs.js"); } - function unshare() { $id = $_REQUEST['id']; @@ -36,32 +39,30 @@ class Share extends Plugin { AND owner_uid = ?"); $sth->execute([$id, $_SESSION['uid']]); - print "OK"; + print __("Article unshared"); } function hook_prefs_tab_section($id) { if ($id == "prefFeedsPublishedGenerated") { + ?> +
      - print "

      " . __("You can disable all articles shared by unique URLs here.") . "

      "; - - print " "; - - print "

      "; +

      + + pdo->prepare("UPDATE ttrss_user_entries SET uuid = '' WHERE owner_uid = ?"); $sth->execute([$_SESSION['uid']]); - return; + print __("Shared URLs cleared."); } - function newkey() { $id = $_REQUEST['id']; $uuid = uniqid_short(); @@ -70,26 +71,169 @@ class Share extends Plugin { AND owner_uid = ?"); $sth->execute([$uuid, $id, $_SESSION['uid']]); - print json_encode(array("link" => $uuid)); + print json_encode(["link" => $uuid]); } function hook_article_button($line) { - $img_class = $line['uuid'] ? "shared" : ""; + $icon_class = !empty($line['uuid']) ? "is-shared" : ""; - return "link"; } - function shareArticle() { - $param = $_REQUEST['param']; + function get() { + $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"]; + + $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]); + + if ($line = $sth->fetch()) { + + $line["tags"] = Article::_get_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); + + $enclosures = Article::_get_enclosures($line["id"]); + list ($og_image, $og_stream) = Article::_get_image($enclosures, $line['content'], $line["site_url"]); + + $content_decoded = html_entity_decode($line["title"], ENT_NOQUOTES | ENT_HTML401); + $parsed_updated = TimeHelper::make_local_datetime($line["updated"], true, $owner_uid, true); + + $line['content'] = DiskCache::rewrite_urls($line['content']); + + ob_start(); + + ?> + + + + + <?= $line["title"] ?> + + + + + + + + "> + + + + + + + +
      + + +

      + "> +

      + +

      + + +
      +
      +
      +
      +
      +
      +
      + +
      "> + +
      +
      + + + chain_hooks_callback(PluginHost::HOOK_FORMAT_ARTICLE, + function ($result) use (&$rv) { + $rv = $result; + }, + $rv, $line); + + print $rv; + } + } + + function shareDialog() { + $id = (int)clean($_REQUEST['id'] ?? 0); $sth = $this->pdo->prepare("SELECT uuid FROM ttrss_user_entries WHERE int_id = ? AND owner_uid = ?"); - $sth->execute([$param, $_SESSION['uid']]); + $sth->execute([$id, $_SESSION['uid']]); if ($row = $sth->fetch()) { - $uuid = $row['uuid']; if (!$uuid) { @@ -97,42 +241,34 @@ class Share extends Plugin { $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET uuid = ? WHERE int_id = ? AND owner_uid = ?"); - $sth->execute([$uuid, $param, $_SESSION['uid']]); + $sth->execute([$uuid, $id, $_SESSION['uid']]); } - print "
      " . __("You can share this article by the following unique URL:") . "
      "; + $url_path = $this->host->get_public_method_url($this, "get", ["key" => $uuid]); + ?> - $url_path = get_self_url_prefix(); - $url_path .= "/public.php?op=share&key=$uuid"; +
      - print "
      +
      - $url_path +
      -
      "; - - /* if (!label_find_id(__('Shared'), $_SESSION["uid"])) - label_create(__('Shared'), $_SESSION["uid"]); - - label_add_article($ref_id, __('Shared'), $_SESSION['uid']); */ +
      + "; - - print ""; - - print ""; - - print ""; - - print ""; + ?> +
      + 'alt-danger', 'onclick' => "App.dialogOf(this).unshare()"]) ?> + "App.dialogOf(this).newurl()"]) ?> + +
      + { + xhr.json("backend.php", App.getPhArgs("share", "newkey", {id: id}), (reply) => { if (reply) { const new_link = reply.link; - const e = $('gen_article_url'); + const target = dialog.domNode.querySelector(".target-url"); - if (new_link) { + if (new_link && target) { - e.innerHTML = e.innerHTML.replace(/\&key=.*$/, + target.innerHTML = target.innerHTML.replace(/&key=.*$/, "&key=" + new_link); - e.href = e.href.replace(/\&key=.*$/, + target.href = target.href.replace(/&key=.*$/, "&key=" + new_link); - new Effect.Highlight(e); + const icon = document.querySelector(".share-icon-" + id); - const img = $("SHARE-IMG-" + id); - img.addClassName("shared"); + if (icon) + icon.addClassName("is-shared"); Notify.close(); @@ -44,32 +40,35 @@ Plugins.Share = { }, unshare: function () { if (confirm(__("Remove sharing for this article?"))) { + xhr.post("backend.php", App.getPhArgs("share", "unshare", {id: id}), (reply) => { + Notify.info(reply); - const query = {op: "pluginhandler", plugin: "share", method: "unshare", id: id}; + const icon = document.querySelector(".share-icon-" + id); - xhrPost("backend.php", query, () => { - try { - const img = $("SHARE-IMG-" + id); + if (icon) + icon.removeClassName("is-shared"); - if (img) { - img.removeClassName("shared"); - img.up("div[id*=RROW]").removeClassName("shared"); - } - - dialog.hide(); - } catch (e) { - console.error(e); - } + dialog.hide(); }); } }, - href: query + content: __("Loading, please wait...") + }); + + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + xhr.post("backend.php", App.getPhArgs("share", "shareDialog", {id: id}), (reply) => { + dialog.attr('content', reply) + + const icon = document.querySelector(".share-icon-" + id); + + if (icon) + icon.addClassName("is-shared"); + }); }); dialog.show(); - - const img = $("SHARE-IMG-" + id); - img.addClassName("shared"); } } diff --git a/plugins/share/share_prefs.js b/plugins/share/share_prefs.js index 071a6667c..d974af618 100644 --- a/plugins/share/share_prefs.js +++ b/plugins/share/share_prefs.js @@ -1,12 +1,12 @@ +/* global Plugins, Notify, xhr, App */ + Plugins.Share = { clearKeys: function() { if (confirm(__("This will invalidate all previously shared article URLs. Continue?"))) { Notify.progress("Clearing URLs..."); - const query = {op: "pluginhandler", plugin: "share", method: "clearArticleKeys"}; - - xhrPost("backend.php", query, () => { - Notify.info("Shared URLs cleared."); + xhr.post("backend.php", App.getPhArgs("share", "clearArticleKeys"), (reply) => { + Notify.info(reply); }); } diff --git a/plugins/shorten_expanded/init.js b/plugins/shorten_expanded/init.js index 30bfac6ba..0abc8c129 100644 --- a/plugins/shorten_expanded/init.js +++ b/plugins/shorten_expanded/init.js @@ -1,3 +1,5 @@ +/* global Plugins, __, require, PluginHost */ + const _shorten_expanded_threshold = 1.5; //window heights Plugins.Shorten_Expanded = { @@ -5,8 +7,8 @@ Plugins.Shorten_Expanded = { const row = $(id); if (row) { - const content = row.select(".content-shrink-wrap")[0]; - const link = row.select(".expand-prompt")[0]; + const content = row.querySelector(".content-shrink-wrap"); + const link = row.querySelector(".expand-prompt"); if (content) content.removeClassName("content-shrink-wrap"); if (link) Element.hide(link); @@ -22,26 +24,26 @@ require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) { window.setTimeout(function() { if (row) { - const c_inner = row.select(".content-inner")[0]; - const c_inter = row.select(".intermediate")[0]; + const content = row.querySelector(".content-inner"); - if (c_inner && c_inter && - row.offsetHeight >= _shorten_expanded_threshold * window.innerHeight) { + //console.log('shorten', row.offsetHeight, 'vs', _shorten_expanded_threshold * window.innerHeight); - let tmp = document.createElement("div"); + if (content && row.offsetHeight >= _shorten_expanded_threshold * window.innerHeight) { - c_inter.select("> *:not([class*='attachments'])").each(function(p) { - p.parentNode.removeChild(p); - tmp.appendChild(p); - }); + const attachments = row.querySelector(".attachments-inline"); // optional - c_inner.innerHTML = `
      - ${c_inner.innerHTML} - ${tmp.innerHTML}
      + content.innerHTML = ` +
      + ${content.innerHTML} + ${attachments ? attachments.innerHTML : ''} +
      `; - dojo.parser.parse(c_inner); + if (attachments) + attachments.innerHTML = ""; + + dojo.parser.parse(content); } } }, 150); diff --git a/plugins/toggle_sidebar/init.php b/plugins/toggle_sidebar/init.php index f8ec35a91..19ca960e2 100644 --- a/plugins/toggle_sidebar/init.php +++ b/plugins/toggle_sidebar/init.php @@ -24,7 +24,7 @@ class Toggle_Sidebar extends Plugin { "ttrss_feeds.title AS feed_title," ); - $qfh_ret = Feeds::queryFeedHeadlines($params); + $qfh_ret = Feeds::_get_headlines($params); $qfh_ret[1] = __("Shared articles"); return $qfh_ret; diff --git a/prefs.php b/prefs.php index 61e1145b7..57ddbba7c 100644 --- a/prefs.php +++ b/prefs.php @@ -1,19 +1,11 @@ Fatal Error: You forgot to copy - config.php-dist to config.php and edit it.\n"; - exit; - } - require_once "autoload.php"; require_once "sessions.php"; require_once "functions.php"; require_once "sanity_check.php"; - require_once "config.php"; - require_once "db-prefs.php"; if (!init_plugins()) return; @@ -24,7 +16,7 @@ - Tiny Tiny RSS : <?php echo __("Preferences") ?> + Tiny Tiny RSS : <?= __("Preferences") ?> - @@ -50,7 +42,7 @@
      - +
      @@ -126,34 +115,37 @@
      + title="settings ">
      + title="rss_feed ">
      + title="filter_list1 ">
      + title="label_outline1 ">
      = 10) { ?>
      + title="person ">
      + title="info_outline ">
      run_hooks(PluginHost::HOOK_PREFS_TABS); @@ -162,8 +154,8 @@ diff --git a/public.php b/public.php index 36308e25e..28f95d0a9 100644 --- a/public.php +++ b/public.php @@ -1,14 +1,11 @@ lookup_handler("public", $method); @@ -30,6 +34,13 @@ $handler = new Handler_Public($_REQUEST); } + if (strpos($method, "_") === 0) { + user_error("Refusing to invoke method $method which starts with underscore.", E_USER_WARNING); + header("Content-Type: text/json"); + print Errors::to_json(Errors::E_UNAUTHORIZED); + return; + } + if (implements_interface($handler, "IHandler") && $handler->before($method)) { if ($method && method_exists($handler, $method)) { $reflection = new ReflectionMethod($handler, $method); @@ -37,8 +48,9 @@ if ($reflection->getNumberOfRequiredParameters() == 0) { $handler->$method(); } else { + user_error("Refusing to invoke method $method which has required parameters.", E_USER_WARNING); header("Content-Type: text/json"); - print error_json(6); + print Errors::to_json(Errors::E_UNAUTHORIZED); } } else if (method_exists($handler, 'index')) { $handler->index(); @@ -48,5 +60,5 @@ } header("Content-Type: text/plain"); - print error_json(13); + print Errors::to_json(Errors::E_UNKNOWN_METHOD); ?> diff --git a/themes/compact.css b/themes/compact.css index 080f82961..16058309f 100644 --- a/themes/compact.css +++ b/themes/compact.css @@ -456,6 +456,7 @@ body.ttrss_main .dijitDialog .dlgSecCont { margin: 10px 20px; } body.ttrss_main .dijitDialog header.horizontal + section, +body.ttrss_main .dijitDialog section.horizontal, body.ttrss_main .dijitDialog .dlgSecHoriz + .dlgSecCont { margin: 10px 0; } @@ -506,6 +507,11 @@ body.ttrss_main .dijitDialog .dlgButtons { body.ttrss_main .dijitDialog footer.text-center { text-align: center; } +body.ttrss_main .dijitDialog textarea#tags_str { + height: 100px; + font-size: 12px; + width: 98%; +} body.ttrss_main i.icon-label { color: #fff7d5; } @@ -526,22 +532,13 @@ body.ttrss_main #feed_browser_spinner { height: 18px; width: 18px; } -body.ttrss_main #exceptionDlg .dijitDialogTitleBar { - background: red; - color: white; -} -body.ttrss_main #exceptionDlg .dijitDialogPaneContent { - background: #fcc; -} -body.ttrss_main #exceptionDlg .error-contents .message { +body.ttrss_main .exception-contents h3 { color: red; } -body.ttrss_main #exceptionDlg .error-contents textarea { +body.ttrss_main .exception-contents textarea { width: 99%; height: 200px; -} -body.ttrss_main #exceptionDlg .error-contents .dlgButtons { - text-align: center; + font-size: 11px; } body.ttrss_main #content-wrap { padding: 0px; @@ -713,6 +710,8 @@ body.ttrss_main #toolbar-frame #toolbar i { margin: 0 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines { + font-size: 12px; + background: transparent; padding-right: 4px; flex-grow: 2; display: flex; @@ -722,7 +721,8 @@ body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left { display: flex; align-items: center; } -body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left #feed_title { +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .feed_title, +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .cancel_search { margin-left: 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .right { @@ -755,6 +755,17 @@ body.ttrss_main #header { top: 0px; z-index: 5; } +body.ttrss_main #header i.net-alert, +body.ttrss_main #header .left i.icon-error { + color: red; +} +body.ttrss_main #header i.log-alert { + color: #ddba1c; + cursor: pointer; +} +body.ttrss_main #header i { + margin: 0 4px; +} body.ttrss_main #content-insert { padding: 0px; border-color: #ddd; @@ -812,16 +823,18 @@ body.ttrss_main #headlines-spacer a:hover { } body.ttrss_main ul#filterDlg_Matches, body.ttrss_main ul#filterDlg_Actions { - max-height: 100px; - overflow: auto; list-style-type: none; - border-style: solid; - border-color: #ddd; - border-width: 1px 1px 1px 1px; - background-color: white; - margin: 0px 0px 5px 0px; - padding: 4px; - min-height: 16px; + margin: 0; + padding: 0; + /*max-height : 100px; + overflow : auto; + border-style : solid; + border-color : @border-default; + border-width : 1px 1px 1px 1px; + background-color : @default-bg; + margin : 0px 0px 5px 0px; + padding : 4px; + min-height : 16px;*/ } body.ttrss_main ul#filterDlg_Matches li, body.ttrss_main ul#filterDlg_Actions li { @@ -860,9 +873,6 @@ body.ttrss_main span.highlight { body.ttrss_main #headlines-frame .dijitCheckBox { margin-right: 4px; } -body.ttrss_main #editTagsDlg { - overflow: visible; -} body.ttrss_main #feedEditDlg img.feedIcon { border: 1px solid #ccc; padding: 5px; @@ -888,12 +898,14 @@ body.ttrss_main .dijitTooltipBelow .dijitTooltipConnector { body.ttrss_main .dijitTooltipAbove .dijitTooltipConnector { border-top-color: #1c5c7d; } -body.ttrss_main .dijitDialog h1:first-of-type, -body.ttrss_main .dijitDialog h2:first-of-type, -body.ttrss_main .dijitDialog h3:first-of-type, -body.ttrss_main .dijitDialog h4:first-of-type { - margin-top: 0px; -} +/*body.ttrss_main .dijitDialog { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type, + h4:first-of-type { + margin-top: 0px; + } +}*/ body.ttrss_main[view-mode="marked"] #feeds-holder #feedTree .dijitTreeRow.Has_Marked .dijitTreeLabel { color: #257aa7; } @@ -1403,6 +1415,13 @@ body.ttrss_prefs { background-color: #f5f5f5; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + /*.dijitContentPane { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type { + margin-top: 0px; + } + }*/ } body.ttrss_prefs h1, body.ttrss_prefs h2, @@ -1412,11 +1431,6 @@ body.ttrss_prefs h4 { font-weight: 600; color: #555; } -body.ttrss_prefs .dijitContentPane h1:first-of-type, -body.ttrss_prefs .dijitContentPane h2:first-of-type, -body.ttrss_prefs .dijitContentPane h3:first-of-type { - margin-top: 0px; -} body.ttrss_prefs #footer, body.ttrss_prefs #header { padding: 8px; @@ -1450,22 +1464,13 @@ body.ttrss_prefs .dijitAccordionTitle i.material-icons { body.ttrss_prefs .dijitAccordionTitleSelected i.material-icons { color: white; } +body.ttrss_prefs #feedsTab { + background: #f5f5f5; +} body.ttrss_prefs .dijitDialog #pref-profiles-list .dijitInlineEditBoxDisplayMode { padding: 0px; } -body.ttrss_prefs div#feedlistLoading, -body.ttrss_prefs div#filterlistLoading, -body.ttrss_prefs div#labellistLoading { - text-align: center; - padding: 5px; - color: #555; -} -body.ttrss_prefs div#feedlistLoading img, -body.ttrss_prefs div#filterlistLoading img, -body.ttrss_prefs div#labellistLoading { - margin-right: 5px; -} -body.ttrss_prefs #errorButton { +body.ttrss_prefs #pref_feeds_errors_btn { color: red; } body.ttrss_prefs .user-css-editor { @@ -1702,6 +1707,13 @@ body.ttrss_utility.share_popup .content { font-size: 13px; padding: 0px; } +.flat .dijitToolbar .dijitTextBox .dijitInputInner { + line-height: 10px; +} +.flat .dijitToolbar label { + position: relative; + top: 2px; +} .flat .dijitAccordionContainer { box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1); } diff --git a/themes/compact_night.css b/themes/compact_night.css index be6a25a2e..f1894f4de 100644 --- a/themes/compact_night.css +++ b/themes/compact_night.css @@ -456,6 +456,7 @@ body.ttrss_main .dijitDialog .dlgSecCont { margin: 10px 20px; } body.ttrss_main .dijitDialog header.horizontal + section, +body.ttrss_main .dijitDialog section.horizontal, body.ttrss_main .dijitDialog .dlgSecHoriz + .dlgSecCont { margin: 10px 0; } @@ -506,6 +507,11 @@ body.ttrss_main .dijitDialog .dlgButtons { body.ttrss_main .dijitDialog footer.text-center { text-align: center; } +body.ttrss_main .dijitDialog textarea#tags_str { + height: 100px; + font-size: 12px; + width: 98%; +} body.ttrss_main i.icon-label { color: #fff7d5; } @@ -526,22 +532,13 @@ body.ttrss_main #feed_browser_spinner { height: 18px; width: 18px; } -body.ttrss_main #exceptionDlg .dijitDialogTitleBar { - background: red; - color: white; -} -body.ttrss_main #exceptionDlg .dijitDialogPaneContent { - background: #fcc; -} -body.ttrss_main #exceptionDlg .error-contents .message { +body.ttrss_main .exception-contents h3 { color: red; } -body.ttrss_main #exceptionDlg .error-contents textarea { +body.ttrss_main .exception-contents textarea { width: 99%; height: 200px; -} -body.ttrss_main #exceptionDlg .error-contents .dlgButtons { - text-align: center; + font-size: 11px; } body.ttrss_main #content-wrap { padding: 0px; @@ -713,6 +710,8 @@ body.ttrss_main #toolbar-frame #toolbar i { margin: 0 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines { + font-size: 12px; + background: transparent; padding-right: 4px; flex-grow: 2; display: flex; @@ -722,7 +721,8 @@ body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left { display: flex; align-items: center; } -body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left #feed_title { +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .feed_title, +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .cancel_search { margin-left: 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .right { @@ -755,6 +755,17 @@ body.ttrss_main #header { top: 0px; z-index: 5; } +body.ttrss_main #header i.net-alert, +body.ttrss_main #header .left i.icon-error { + color: red; +} +body.ttrss_main #header i.log-alert { + color: #ddba1c; + cursor: pointer; +} +body.ttrss_main #header i { + margin: 0 4px; +} body.ttrss_main #content-insert { padding: 0px; border-color: #222; @@ -812,16 +823,18 @@ body.ttrss_main #headlines-spacer a:hover { } body.ttrss_main ul#filterDlg_Matches, body.ttrss_main ul#filterDlg_Actions { - max-height: 100px; - overflow: auto; list-style-type: none; - border-style: solid; - border-color: #222; - border-width: 1px 1px 1px 1px; - background-color: #333; - margin: 0px 0px 5px 0px; - padding: 4px; - min-height: 16px; + margin: 0; + padding: 0; + /*max-height : 100px; + overflow : auto; + border-style : solid; + border-color : @border-default; + border-width : 1px 1px 1px 1px; + background-color : @default-bg; + margin : 0px 0px 5px 0px; + padding : 4px; + min-height : 16px;*/ } body.ttrss_main ul#filterDlg_Matches li, body.ttrss_main ul#filterDlg_Actions li { @@ -860,9 +873,6 @@ body.ttrss_main span.highlight { body.ttrss_main #headlines-frame .dijitCheckBox { margin-right: 4px; } -body.ttrss_main #editTagsDlg { - overflow: visible; -} body.ttrss_main #feedEditDlg img.feedIcon { border: 1px solid #ccc; padding: 5px; @@ -888,12 +898,14 @@ body.ttrss_main .dijitTooltipBelow .dijitTooltipConnector { body.ttrss_main .dijitTooltipAbove .dijitTooltipConnector { border-top-color: #d29745; } -body.ttrss_main .dijitDialog h1:first-of-type, -body.ttrss_main .dijitDialog h2:first-of-type, -body.ttrss_main .dijitDialog h3:first-of-type, -body.ttrss_main .dijitDialog h4:first-of-type { - margin-top: 0px; -} +/*body.ttrss_main .dijitDialog { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type, + h4:first-of-type { + margin-top: 0px; + } +}*/ body.ttrss_main[view-mode="marked"] #feeds-holder #feedTree .dijitTreeRow.Has_Marked .dijitTreeLabel { color: #b87d2c; } @@ -1403,6 +1415,13 @@ body.ttrss_prefs { background-color: #222; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + /*.dijitContentPane { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type { + margin-top: 0px; + } + }*/ } body.ttrss_prefs h1, body.ttrss_prefs h2, @@ -1412,11 +1431,6 @@ body.ttrss_prefs h4 { font-weight: 600; color: #ccc; } -body.ttrss_prefs .dijitContentPane h1:first-of-type, -body.ttrss_prefs .dijitContentPane h2:first-of-type, -body.ttrss_prefs .dijitContentPane h3:first-of-type { - margin-top: 0px; -} body.ttrss_prefs #footer, body.ttrss_prefs #header { padding: 8px; @@ -1450,22 +1464,13 @@ body.ttrss_prefs .dijitAccordionTitle i.material-icons { body.ttrss_prefs .dijitAccordionTitleSelected i.material-icons { color: white; } +body.ttrss_prefs #feedsTab { + background: #222; +} body.ttrss_prefs .dijitDialog #pref-profiles-list .dijitInlineEditBoxDisplayMode { padding: 0px; } -body.ttrss_prefs div#feedlistLoading, -body.ttrss_prefs div#filterlistLoading, -body.ttrss_prefs div#labellistLoading { - text-align: center; - padding: 5px; - color: #ccc; -} -body.ttrss_prefs div#feedlistLoading img, -body.ttrss_prefs div#filterlistLoading img, -body.ttrss_prefs div#labellistLoading { - margin-right: 5px; -} -body.ttrss_prefs #errorButton { +body.ttrss_prefs #pref_feeds_errors_btn { color: red; } body.ttrss_prefs .user-css-editor { @@ -1604,6 +1609,13 @@ body.ttrss_utility fieldset > label.checkbox { font-size: 13px; padding: 0px; } +.flat .dijitToolbar .dijitTextBox .dijitInputInner { + line-height: 10px; +} +.flat .dijitToolbar label { + position: relative; + top: 2px; +} .flat .dijitAccordionContainer { box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1); } diff --git a/themes/light.css b/themes/light.css index e16ff83dd..c014858ef 100644 --- a/themes/light.css +++ b/themes/light.css @@ -456,6 +456,7 @@ body.ttrss_main .dijitDialog .dlgSecCont { margin: 10px 20px; } body.ttrss_main .dijitDialog header.horizontal + section, +body.ttrss_main .dijitDialog section.horizontal, body.ttrss_main .dijitDialog .dlgSecHoriz + .dlgSecCont { margin: 10px 0; } @@ -506,6 +507,11 @@ body.ttrss_main .dijitDialog .dlgButtons { body.ttrss_main .dijitDialog footer.text-center { text-align: center; } +body.ttrss_main .dijitDialog textarea#tags_str { + height: 100px; + font-size: 12px; + width: 98%; +} body.ttrss_main i.icon-label { color: #fff7d5; } @@ -526,22 +532,13 @@ body.ttrss_main #feed_browser_spinner { height: 18px; width: 18px; } -body.ttrss_main #exceptionDlg .dijitDialogTitleBar { - background: red; - color: white; -} -body.ttrss_main #exceptionDlg .dijitDialogPaneContent { - background: #fcc; -} -body.ttrss_main #exceptionDlg .error-contents .message { +body.ttrss_main .exception-contents h3 { color: red; } -body.ttrss_main #exceptionDlg .error-contents textarea { +body.ttrss_main .exception-contents textarea { width: 99%; height: 200px; -} -body.ttrss_main #exceptionDlg .error-contents .dlgButtons { - text-align: center; + font-size: 11px; } body.ttrss_main #content-wrap { padding: 0px; @@ -713,6 +710,8 @@ body.ttrss_main #toolbar-frame #toolbar i { margin: 0 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines { + font-size: 12px; + background: transparent; padding-right: 4px; flex-grow: 2; display: flex; @@ -722,7 +721,8 @@ body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left { display: flex; align-items: center; } -body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left #feed_title { +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .feed_title, +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .cancel_search { margin-left: 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .right { @@ -755,6 +755,17 @@ body.ttrss_main #header { top: 0px; z-index: 5; } +body.ttrss_main #header i.net-alert, +body.ttrss_main #header .left i.icon-error { + color: red; +} +body.ttrss_main #header i.log-alert { + color: #ddba1c; + cursor: pointer; +} +body.ttrss_main #header i { + margin: 0 4px; +} body.ttrss_main #content-insert { padding: 0px; border-color: #ddd; @@ -812,16 +823,18 @@ body.ttrss_main #headlines-spacer a:hover { } body.ttrss_main ul#filterDlg_Matches, body.ttrss_main ul#filterDlg_Actions { - max-height: 100px; - overflow: auto; list-style-type: none; - border-style: solid; - border-color: #ddd; - border-width: 1px 1px 1px 1px; - background-color: white; - margin: 0px 0px 5px 0px; - padding: 4px; - min-height: 16px; + margin: 0; + padding: 0; + /*max-height : 100px; + overflow : auto; + border-style : solid; + border-color : @border-default; + border-width : 1px 1px 1px 1px; + background-color : @default-bg; + margin : 0px 0px 5px 0px; + padding : 4px; + min-height : 16px;*/ } body.ttrss_main ul#filterDlg_Matches li, body.ttrss_main ul#filterDlg_Actions li { @@ -860,9 +873,6 @@ body.ttrss_main span.highlight { body.ttrss_main #headlines-frame .dijitCheckBox { margin-right: 4px; } -body.ttrss_main #editTagsDlg { - overflow: visible; -} body.ttrss_main #feedEditDlg img.feedIcon { border: 1px solid #ccc; padding: 5px; @@ -888,12 +898,14 @@ body.ttrss_main .dijitTooltipBelow .dijitTooltipConnector { body.ttrss_main .dijitTooltipAbove .dijitTooltipConnector { border-top-color: #1c5c7d; } -body.ttrss_main .dijitDialog h1:first-of-type, -body.ttrss_main .dijitDialog h2:first-of-type, -body.ttrss_main .dijitDialog h3:first-of-type, -body.ttrss_main .dijitDialog h4:first-of-type { - margin-top: 0px; -} +/*body.ttrss_main .dijitDialog { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type, + h4:first-of-type { + margin-top: 0px; + } +}*/ body.ttrss_main[view-mode="marked"] #feeds-holder #feedTree .dijitTreeRow.Has_Marked .dijitTreeLabel { color: #257aa7; } @@ -1403,6 +1415,13 @@ body.ttrss_prefs { background-color: #f5f5f5; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + /*.dijitContentPane { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type { + margin-top: 0px; + } + }*/ } body.ttrss_prefs h1, body.ttrss_prefs h2, @@ -1412,11 +1431,6 @@ body.ttrss_prefs h4 { font-weight: 600; color: #555; } -body.ttrss_prefs .dijitContentPane h1:first-of-type, -body.ttrss_prefs .dijitContentPane h2:first-of-type, -body.ttrss_prefs .dijitContentPane h3:first-of-type { - margin-top: 0px; -} body.ttrss_prefs #footer, body.ttrss_prefs #header { padding: 8px; @@ -1450,22 +1464,13 @@ body.ttrss_prefs .dijitAccordionTitle i.material-icons { body.ttrss_prefs .dijitAccordionTitleSelected i.material-icons { color: white; } +body.ttrss_prefs #feedsTab { + background: #f5f5f5; +} body.ttrss_prefs .dijitDialog #pref-profiles-list .dijitInlineEditBoxDisplayMode { padding: 0px; } -body.ttrss_prefs div#feedlistLoading, -body.ttrss_prefs div#filterlistLoading, -body.ttrss_prefs div#labellistLoading { - text-align: center; - padding: 5px; - color: #555; -} -body.ttrss_prefs div#feedlistLoading img, -body.ttrss_prefs div#filterlistLoading img, -body.ttrss_prefs div#labellistLoading { - margin-right: 5px; -} -body.ttrss_prefs #errorButton { +body.ttrss_prefs #pref_feeds_errors_btn { color: red; } body.ttrss_prefs .user-css-editor { @@ -1702,6 +1707,13 @@ body.ttrss_utility.share_popup .content { font-size: 13px; padding: 0px; } +.flat .dijitToolbar .dijitTextBox .dijitInputInner { + line-height: 10px; +} +.flat .dijitToolbar label { + position: relative; + top: 2px; +} .flat .dijitAccordionContainer { box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1); } diff --git a/themes/light/dijit_basic.less b/themes/light/dijit_basic.less index 2854c77d0..0582ddc5c 100644 --- a/themes/light/dijit_basic.less +++ b/themes/light/dijit_basic.less @@ -40,6 +40,15 @@ .dijitToolbar { font-size: 13px; padding: 0px; + + .dijitTextBox .dijitInputInner { + line-height : 10px; + } + + label { + position : relative; + top : 2px; + } } .dijitAccordionContainer { diff --git a/themes/light/prefs.less b/themes/light/prefs.less index 95ddefc34..510388391 100644 --- a/themes/light/prefs.less +++ b/themes/light/prefs.less @@ -9,13 +9,13 @@ body.ttrss_prefs { color : @default-text; } - .dijitContentPane { + /*.dijitContentPane { h1:first-of-type, h2:first-of-type, h3:first-of-type { margin-top: 0px; } - } + }*/ #footer, #header { padding : 8px; @@ -57,21 +57,15 @@ body.ttrss_prefs { color : white; } + #feedsTab { + background : @color-panel-bg; + } + .dijitDialog #pref-profiles-list .dijitInlineEditBoxDisplayMode { padding : 0px; } - div#feedlistLoading, div#filterlistLoading, div#labellistLoading { - text-align : center; - padding : 5px; - color : @default-text; - } - - div#feedlistLoading img, div#filterlistLoading img, div#labellistLoading { - margin-right : 5px; - } - - #errorButton { + #pref_feeds_errors_btn { color : red; } diff --git a/themes/light/tt-rss.less b/themes/light/tt-rss.less index 65ec33bc3..35eec3e48 100644 --- a/themes/light/tt-rss.less +++ b/themes/light/tt-rss.less @@ -516,7 +516,6 @@ body.ttrss_main { } .dijitDialog { - header, .dlgSec, .dlgSecHoriz { font-size : 16px; @@ -531,6 +530,7 @@ body.ttrss_main { } header.horizontal + section, + section.horizontal, .dlgSecHoriz + .dlgSecCont { margin : 10px 0; } @@ -584,6 +584,12 @@ body.ttrss_main { footer.text-center { text-align: center; } + + textarea#tags_str { + height : 100px; + font-size : 12px; + width : 98%; + } } i.icon-label { @@ -609,28 +615,14 @@ body.ttrss_main { width : 18px; } - #exceptionDlg { - .dijitDialogTitleBar { - background : red; - color : white; + .exception-contents { + h3 { + color : red; } - - .dijitDialogPaneContent { - background : #fcc; - } - - .error-contents { - .message { - color : red; - } - - textarea { - width : 99%; - height : 200px; - } - .dlgButtons { - text-align : center; - } + textarea { + width : 99%; + height : 200px; + font-size : 11px; } } @@ -834,6 +826,8 @@ body.ttrss_main { } #toolbar-headlines { + font-size : 12px; + background: transparent; padding-right : 4px; flex-grow : 2; display : flex; @@ -843,7 +837,7 @@ body.ttrss_main { display : flex; align-items : center; - #feed_title { + .feed_title, .cancel_search { margin-left : 4px; } } @@ -884,6 +878,19 @@ body.ttrss_main { right : 0px; top : 0px; z-index : 5; + + i.net-alert, .left i.icon-error { + color : red; + } + + i.log-alert { + color : #ddba1c; + cursor : pointer; + } + + i { + margin : 0 4px; + } } #content-insert { @@ -949,16 +956,18 @@ body.ttrss_main { } ul#filterDlg_Matches, ul#filterDlg_Actions { - max-height : 100px; - overflow : auto; list-style-type : none; + margin : 0; + padding: 0; + /*max-height : 100px; + overflow : auto; border-style : solid; border-color : @border-default; border-width : 1px 1px 1px 1px; background-color : @default-bg; margin : 0px 0px 5px 0px; padding : 4px; - min-height : 16px; + min-height : 16px;*/ } ul#filterDlg_Matches li, ul#filterDlg_Actions li { @@ -1004,18 +1013,10 @@ body.ttrss_main { color : #cc90cc; } - div.enclosure_title { - - } - #headlines-frame .dijitCheckBox { margin-right : 4px; } - #editTagsDlg{ - overflow: visible; - } - #feedEditDlg img.feedIcon { border : 1px solid #ccc; padding : 5px; @@ -1048,14 +1049,14 @@ body.ttrss_main { } } -body.ttrss_main .dijitDialog { +/*body.ttrss_main .dijitDialog { h1:first-of-type, h2:first-of-type, h3:first-of-type, h4:first-of-type { margin-top: 0px; } -} +}*/ body.ttrss_main[view-mode="marked"] #feeds-holder #feedTree { .dijitTreeRow.Has_Marked .dijitTreeLabel { diff --git a/themes/night.css b/themes/night.css index 6090890e2..1e397ee74 100644 --- a/themes/night.css +++ b/themes/night.css @@ -457,6 +457,7 @@ body.ttrss_main .dijitDialog .dlgSecCont { margin: 10px 20px; } body.ttrss_main .dijitDialog header.horizontal + section, +body.ttrss_main .dijitDialog section.horizontal, body.ttrss_main .dijitDialog .dlgSecHoriz + .dlgSecCont { margin: 10px 0; } @@ -507,6 +508,11 @@ body.ttrss_main .dijitDialog .dlgButtons { body.ttrss_main .dijitDialog footer.text-center { text-align: center; } +body.ttrss_main .dijitDialog textarea#tags_str { + height: 100px; + font-size: 12px; + width: 98%; +} body.ttrss_main i.icon-label { color: #fff7d5; } @@ -527,22 +533,13 @@ body.ttrss_main #feed_browser_spinner { height: 18px; width: 18px; } -body.ttrss_main #exceptionDlg .dijitDialogTitleBar { - background: red; - color: white; -} -body.ttrss_main #exceptionDlg .dijitDialogPaneContent { - background: #fcc; -} -body.ttrss_main #exceptionDlg .error-contents .message { +body.ttrss_main .exception-contents h3 { color: red; } -body.ttrss_main #exceptionDlg .error-contents textarea { +body.ttrss_main .exception-contents textarea { width: 99%; height: 200px; -} -body.ttrss_main #exceptionDlg .error-contents .dlgButtons { - text-align: center; + font-size: 11px; } body.ttrss_main #content-wrap { padding: 0px; @@ -714,6 +711,8 @@ body.ttrss_main #toolbar-frame #toolbar i { margin: 0 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines { + font-size: 12px; + background: transparent; padding-right: 4px; flex-grow: 2; display: flex; @@ -723,7 +722,8 @@ body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left { display: flex; align-items: center; } -body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left #feed_title { +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .feed_title, +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .cancel_search { margin-left: 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .right { @@ -756,6 +756,17 @@ body.ttrss_main #header { top: 0px; z-index: 5; } +body.ttrss_main #header i.net-alert, +body.ttrss_main #header .left i.icon-error { + color: red; +} +body.ttrss_main #header i.log-alert { + color: #ddba1c; + cursor: pointer; +} +body.ttrss_main #header i { + margin: 0 4px; +} body.ttrss_main #content-insert { padding: 0px; border-color: #222; @@ -813,16 +824,18 @@ body.ttrss_main #headlines-spacer a:hover { } body.ttrss_main ul#filterDlg_Matches, body.ttrss_main ul#filterDlg_Actions { - max-height: 100px; - overflow: auto; list-style-type: none; - border-style: solid; - border-color: #222; - border-width: 1px 1px 1px 1px; - background-color: #333; - margin: 0px 0px 5px 0px; - padding: 4px; - min-height: 16px; + margin: 0; + padding: 0; + /*max-height : 100px; + overflow : auto; + border-style : solid; + border-color : @border-default; + border-width : 1px 1px 1px 1px; + background-color : @default-bg; + margin : 0px 0px 5px 0px; + padding : 4px; + min-height : 16px;*/ } body.ttrss_main ul#filterDlg_Matches li, body.ttrss_main ul#filterDlg_Actions li { @@ -861,9 +874,6 @@ body.ttrss_main span.highlight { body.ttrss_main #headlines-frame .dijitCheckBox { margin-right: 4px; } -body.ttrss_main #editTagsDlg { - overflow: visible; -} body.ttrss_main #feedEditDlg img.feedIcon { border: 1px solid #ccc; padding: 5px; @@ -889,12 +899,14 @@ body.ttrss_main .dijitTooltipBelow .dijitTooltipConnector { body.ttrss_main .dijitTooltipAbove .dijitTooltipConnector { border-top-color: #d29745; } -body.ttrss_main .dijitDialog h1:first-of-type, -body.ttrss_main .dijitDialog h2:first-of-type, -body.ttrss_main .dijitDialog h3:first-of-type, -body.ttrss_main .dijitDialog h4:first-of-type { - margin-top: 0px; -} +/*body.ttrss_main .dijitDialog { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type, + h4:first-of-type { + margin-top: 0px; + } +}*/ body.ttrss_main[view-mode="marked"] #feeds-holder #feedTree .dijitTreeRow.Has_Marked .dijitTreeLabel { color: #b87d2c; } @@ -1404,6 +1416,13 @@ body.ttrss_prefs { background-color: #222; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + /*.dijitContentPane { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type { + margin-top: 0px; + } + }*/ } body.ttrss_prefs h1, body.ttrss_prefs h2, @@ -1413,11 +1432,6 @@ body.ttrss_prefs h4 { font-weight: 600; color: #ccc; } -body.ttrss_prefs .dijitContentPane h1:first-of-type, -body.ttrss_prefs .dijitContentPane h2:first-of-type, -body.ttrss_prefs .dijitContentPane h3:first-of-type { - margin-top: 0px; -} body.ttrss_prefs #footer, body.ttrss_prefs #header { padding: 8px; @@ -1451,22 +1465,13 @@ body.ttrss_prefs .dijitAccordionTitle i.material-icons { body.ttrss_prefs .dijitAccordionTitleSelected i.material-icons { color: white; } +body.ttrss_prefs #feedsTab { + background: #222; +} body.ttrss_prefs .dijitDialog #pref-profiles-list .dijitInlineEditBoxDisplayMode { padding: 0px; } -body.ttrss_prefs div#feedlistLoading, -body.ttrss_prefs div#filterlistLoading, -body.ttrss_prefs div#labellistLoading { - text-align: center; - padding: 5px; - color: #ccc; -} -body.ttrss_prefs div#feedlistLoading img, -body.ttrss_prefs div#filterlistLoading img, -body.ttrss_prefs div#labellistLoading { - margin-right: 5px; -} -body.ttrss_prefs #errorButton { +body.ttrss_prefs #pref_feeds_errors_btn { color: red; } body.ttrss_prefs .user-css-editor { @@ -1605,6 +1610,13 @@ body.ttrss_utility fieldset > label.checkbox { font-size: 13px; padding: 0px; } +.flat .dijitToolbar .dijitTextBox .dijitInputInner { + line-height: 10px; +} +.flat .dijitToolbar label { + position: relative; + top: 2px; +} .flat .dijitAccordionContainer { box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1); } diff --git a/themes/night_blue.css b/themes/night_blue.css index 4bea2256f..361cd4fbf 100644 --- a/themes/night_blue.css +++ b/themes/night_blue.css @@ -457,6 +457,7 @@ body.ttrss_main .dijitDialog .dlgSecCont { margin: 10px 20px; } body.ttrss_main .dijitDialog header.horizontal + section, +body.ttrss_main .dijitDialog section.horizontal, body.ttrss_main .dijitDialog .dlgSecHoriz + .dlgSecCont { margin: 10px 0; } @@ -507,6 +508,11 @@ body.ttrss_main .dijitDialog .dlgButtons { body.ttrss_main .dijitDialog footer.text-center { text-align: center; } +body.ttrss_main .dijitDialog textarea#tags_str { + height: 100px; + font-size: 12px; + width: 98%; +} body.ttrss_main i.icon-label { color: #fff7d5; } @@ -527,22 +533,13 @@ body.ttrss_main #feed_browser_spinner { height: 18px; width: 18px; } -body.ttrss_main #exceptionDlg .dijitDialogTitleBar { - background: red; - color: white; -} -body.ttrss_main #exceptionDlg .dijitDialogPaneContent { - background: #fcc; -} -body.ttrss_main #exceptionDlg .error-contents .message { +body.ttrss_main .exception-contents h3 { color: red; } -body.ttrss_main #exceptionDlg .error-contents textarea { +body.ttrss_main .exception-contents textarea { width: 99%; height: 200px; -} -body.ttrss_main #exceptionDlg .error-contents .dlgButtons { - text-align: center; + font-size: 11px; } body.ttrss_main #content-wrap { padding: 0px; @@ -714,6 +711,8 @@ body.ttrss_main #toolbar-frame #toolbar i { margin: 0 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines { + font-size: 12px; + background: transparent; padding-right: 4px; flex-grow: 2; display: flex; @@ -723,7 +722,8 @@ body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left { display: flex; align-items: center; } -body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left #feed_title { +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .feed_title, +body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .left .cancel_search { margin-left: 4px; } body.ttrss_main #toolbar-frame #toolbar #toolbar-headlines .right { @@ -756,6 +756,17 @@ body.ttrss_main #header { top: 0px; z-index: 5; } +body.ttrss_main #header i.net-alert, +body.ttrss_main #header .left i.icon-error { + color: red; +} +body.ttrss_main #header i.log-alert { + color: #ddba1c; + cursor: pointer; +} +body.ttrss_main #header i { + margin: 0 4px; +} body.ttrss_main #content-insert { padding: 0px; border-color: #222; @@ -813,16 +824,18 @@ body.ttrss_main #headlines-spacer a:hover { } body.ttrss_main ul#filterDlg_Matches, body.ttrss_main ul#filterDlg_Actions { - max-height: 100px; - overflow: auto; list-style-type: none; - border-style: solid; - border-color: #222; - border-width: 1px 1px 1px 1px; - background-color: #333; - margin: 0px 0px 5px 0px; - padding: 4px; - min-height: 16px; + margin: 0; + padding: 0; + /*max-height : 100px; + overflow : auto; + border-style : solid; + border-color : @border-default; + border-width : 1px 1px 1px 1px; + background-color : @default-bg; + margin : 0px 0px 5px 0px; + padding : 4px; + min-height : 16px;*/ } body.ttrss_main ul#filterDlg_Matches li, body.ttrss_main ul#filterDlg_Actions li { @@ -861,9 +874,6 @@ body.ttrss_main span.highlight { body.ttrss_main #headlines-frame .dijitCheckBox { margin-right: 4px; } -body.ttrss_main #editTagsDlg { - overflow: visible; -} body.ttrss_main #feedEditDlg img.feedIcon { border: 1px solid #ccc; padding: 5px; @@ -889,12 +899,14 @@ body.ttrss_main .dijitTooltipBelow .dijitTooltipConnector { body.ttrss_main .dijitTooltipAbove .dijitTooltipConnector { border-top-color: #2e99d1; } -body.ttrss_main .dijitDialog h1:first-of-type, -body.ttrss_main .dijitDialog h2:first-of-type, -body.ttrss_main .dijitDialog h3:first-of-type, -body.ttrss_main .dijitDialog h4:first-of-type { - margin-top: 0px; -} +/*body.ttrss_main .dijitDialog { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type, + h4:first-of-type { + margin-top: 0px; + } +}*/ body.ttrss_main[view-mode="marked"] #feeds-holder #feedTree .dijitTreeRow.Has_Marked .dijitTreeLabel { color: #257aa7; } @@ -1404,6 +1416,13 @@ body.ttrss_prefs { background-color: #222; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + /*.dijitContentPane { + h1:first-of-type, + h2:first-of-type, + h3:first-of-type { + margin-top: 0px; + } + }*/ } body.ttrss_prefs h1, body.ttrss_prefs h2, @@ -1413,11 +1432,6 @@ body.ttrss_prefs h4 { font-weight: 600; color: #ccc; } -body.ttrss_prefs .dijitContentPane h1:first-of-type, -body.ttrss_prefs .dijitContentPane h2:first-of-type, -body.ttrss_prefs .dijitContentPane h3:first-of-type { - margin-top: 0px; -} body.ttrss_prefs #footer, body.ttrss_prefs #header { padding: 8px; @@ -1451,22 +1465,13 @@ body.ttrss_prefs .dijitAccordionTitle i.material-icons { body.ttrss_prefs .dijitAccordionTitleSelected i.material-icons { color: white; } +body.ttrss_prefs #feedsTab { + background: #222; +} body.ttrss_prefs .dijitDialog #pref-profiles-list .dijitInlineEditBoxDisplayMode { padding: 0px; } -body.ttrss_prefs div#feedlistLoading, -body.ttrss_prefs div#filterlistLoading, -body.ttrss_prefs div#labellistLoading { - text-align: center; - padding: 5px; - color: #ccc; -} -body.ttrss_prefs div#feedlistLoading img, -body.ttrss_prefs div#filterlistLoading img, -body.ttrss_prefs div#labellistLoading { - margin-right: 5px; -} -body.ttrss_prefs #errorButton { +body.ttrss_prefs #pref_feeds_errors_btn { color: red; } body.ttrss_prefs .user-css-editor { @@ -1605,6 +1610,13 @@ body.ttrss_utility fieldset > label.checkbox { font-size: 13px; padding: 0px; } +.flat .dijitToolbar .dijitTextBox .dijitInputInner { + line-height: 10px; +} +.flat .dijitToolbar label { + position: relative; + top: 2px; +} .flat .dijitAccordionContainer { box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1); } diff --git a/update.php b/update.php index 56158ca48..8d8566db7 100755 --- a/update.php +++ b/update.php @@ -1,21 +1,18 @@ #!/usr/bin/env php run_hooks(PluginHost::HOOK_UPDATE_TASK, $options); @@ -225,10 +222,10 @@ $log = isset($options['log']) ? '--log '.$options['log'] : ''; $log_level = isset($options['log-level']) ? '--log-level '.$options['log-level'] : ''; - passthru(PHP_EXECUTABLE . " " . $argv[0] ." --daemon-loop $quiet $log $log_level"); + passthru(Config::get(Config::PHP_EXECUTABLE) . " " . $argv[0] ." --daemon-loop $quiet $log $log_level"); // let's enforce a minimum spawn interval as to not forkbomb the host - $spawn_interval = max(60, DAEMON_SLEEP_INTERVAL); + $spawn_interval = max(60, Config::get(Config::DAEMON_SLEEP_INTERVAL)); Debug::log("Sleeping for $spawn_interval seconds..."); sleep($spawn_interval); @@ -256,7 +253,7 @@ Debug::log("warning: unable to create stampfile\n"); } - RSSUtils::update_daemon_common(isset($options["pidlock"]) ? 50 : DAEMON_FEED_LIMIT, $options); + RSSUtils::update_daemon_common(isset($options["pidlock"]) ? 50 : Config::get(Config::DAEMON_FEED_LIMIT), $options); if (!isset($options["pidlock"]) || $options["task"] == 0) RSSUtils::housekeeping_common(); @@ -278,7 +275,7 @@ Debug::log("clearing existing indexes..."); - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $sth = $pdo->query( "SELECT relname FROM pg_catalog.pg_class WHERE relname LIKE 'ttrss_%' AND relname NOT LIKE '%_pkey' @@ -289,7 +286,7 @@ } while ($line = $sth->fetch()) { - if (DB_TYPE == "pgsql") { + if (Config::get(Config::DB_TYPE) == "pgsql") { $statement = "DROP INDEX " . $line["relname"]; Debug::log($statement); } else { @@ -300,9 +297,9 @@ $pdo->query($statement); } - Debug::log("reading indexes from schema for: " . DB_TYPE); + Debug::log("reading indexes from schema for: " . Config::get(Config::DB_TYPE)); - $fp = fopen("schema/ttrss_schema_" . DB_TYPE . ".sql", "r"); + $fp = fopen("schema/ttrss_schema_" . Config::get(Config::DB_TYPE) . ".sql", "r"); if ($fp) { while ($line = fgets($fp)) { $matches = array(); @@ -376,14 +373,14 @@ } if (isset($options["update-schema"])) { - Debug::log("Checking for updates (" . DB_TYPE . ")..."); + Debug::log("Checking for updates (" . Config::get(Config::DB_TYPE) . ")..."); - $updater = new DbUpdater(Db::pdo(), DB_TYPE, SCHEMA_VERSION); + $updater = new DbUpdater(Db::pdo(), Config::get(Config::DB_TYPE), SCHEMA_VERSION); - if ($updater->isUpdateRequired()) { - Debug::log("Schema update required, version " . $updater->getSchemaVersion() . " to " . SCHEMA_VERSION); + if ($updater->is_update_required()) { + Debug::log("Schema update required, version " . $updater->get_schema_version() . " to " . SCHEMA_VERSION); - if (DB_TYPE == "mysql") + if (Config::get(Config::DB_TYPE) == "mysql") Debug::Log("READ THIS: Due to MySQL limitations, your database is not completely protected while updating.\n". "Errors may put it in an inconsistent state requiring manual rollback.\nBACKUP YOUR DATABASE BEFORE CONTINUING."); else @@ -400,10 +397,10 @@ Debug::log("Performing updates to version " . SCHEMA_VERSION . "..."); - for ($i = $updater->getSchemaVersion() + 1; $i <= SCHEMA_VERSION; $i++) { + for ($i = $updater->get_schema_version() + 1; $i <= SCHEMA_VERSION; $i++) { Debug::log("* Updating to version $i..."); - $result = $updater->performUpdateTo($i, false); + $result = $updater->update_to($i, false); if ($result) { Debug::log("* Completed."); @@ -460,8 +457,8 @@ if (isset($options["list-plugins"])) { $tmppluginhost = new PluginHost(); - $tmppluginhost->load_all($tmppluginhost::KIND_ALL, false); - $enabled = array_map("trim", explode(",", PLUGINS)); + $tmppluginhost->load_all($tmppluginhost::KIND_ALL); + $enabled = array_map("trim", explode(",", Config::get(Config::PLUGINS))); echo "List of all available plugins:\n"; @@ -516,8 +513,8 @@ PluginHost::getInstance()->run_commands($options); - if (file_exists(LOCK_DIRECTORY . "/$lock_filename")) + if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/$lock_filename")) if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') fclose($lock_handle); - unlink(LOCK_DIRECTORY . "/$lock_filename"); + unlink(Config::get(Config::LOCK_DIRECTORY) . "/$lock_filename"); ?> diff --git a/update_daemon2.php b/update_daemon2.php index 61cc85617..b75f06ae5 100755 --- a/update_daemon2.php +++ b/update_daemon2.php @@ -1,26 +1,16 @@ #!/usr/bin/env php MAX_CHILD_RUNTIME) { + if (time() - $started > Config::get(Config::DAEMON_MAX_CHILD_RUNTIME)) { Debug::log("Child process with PID $pid seems to be stuck, aborting..."); posix_kill($pid, SIGKILL); } @@ -99,9 +89,9 @@ function shutdown($caller_pid) { if ($caller_pid == posix_getpid()) { - if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) { + if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) { Debug::log("Removing lockfile (master)..."); - unlink(LOCK_DIRECTORY . "/update_daemon.lock"); + unlink(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock"); } } } @@ -109,9 +99,9 @@ function task_shutdown() { $pid = posix_getpid(); - if (file_exists(LOCK_DIRECTORY . "/update_daemon-$pid.lock")) { + if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon-$pid.lock")) { Debug::log("Removing task lockfile for PID $pid..."); - unlink(LOCK_DIRECTORY . "/update_daemon-$pid.lock"); + unlink(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon-$pid.lock"); } } @@ -144,9 +134,9 @@ print " --log FILE - log messages to FILE\n"; print " --log-level N - log verbosity level\n"; print " --tasks N - amount of update tasks to spawn\n"; - print " default: " . MAX_JOBS . "\n"; + print " default: " . Config::get(Config::DAEMON_MAX_JOBS) . "\n"; print " --interval N - task spawn interval\n"; - print " default: " . SPAWN_INTERVAL . " seconds.\n"; + print " default: " . Config::get(Config::DAEMON_SLEEP_INTERVAL) . " seconds.\n"; print " --quiet - don't output messages to stdout\n"; return; } @@ -171,14 +161,14 @@ Debug::log("Set to spawn " . $options["tasks"] . " children."); $max_jobs = $options["tasks"]; } else { - $max_jobs = MAX_JOBS; + $max_jobs = Config::get(Config::DAEMON_MAX_JOBS); } if (isset($options["interval"])) { Debug::log("Spawn interval: " . $options["interval"] . " seconds."); $spawn_interval = $options["interval"]; } else { - $spawn_interval = SPAWN_INTERVAL; + $spawn_interval = Config::get(Config::DAEMON_SLEEP_INTERVAL); } // let's enforce a minimum spawn interval as to not forkbomb the host @@ -250,7 +240,7 @@ $my_pid = posix_getpid(); - passthru(PHP_EXECUTABLE . " update.php --daemon-loop $quiet $log --task $j --pidlock $my_pid"); + passthru(Config::get(Config::PHP_EXECUTABLE) . " update.php --daemon-loop $quiet $log --task $j --pidlock $my_pid"); sleep(1); diff --git a/utils/phpstan_tunables.php b/utils/phpstan_tunables.php deleted file mode 100644 index 7d5d8f03a..000000000 --- a/utils/phpstan_tunables.php +++ /dev/null @@ -1,44 +0,0 @@ - $DESTINATION - -echo -n "define('GENERATED_CONFIG_CHECK', " >> $DESTINATION -grep CONFIG_VERSION config.php-dist | awk -F ' |\)' '{ print $2 }' | xargs echo -n >> $DESTINATION -echo ");" >> $DESTINATION - -echo -n "\$required_defines = array( " >> $DESTINATION - -grep define\( config.php-dist | awk -F\' '{ print "*" $2 "*," }' | grep -v DB_PORT | xargs echo -n | sed -e s/,$// -e s/*/\'/g >> $DESTINATION - -echo "); ?>" >> $DESTINATION - -