summaryrefslogtreecommitdiff
path: root/classes/pref
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2023-10-25 12:55:09 +0300
committerAndrew Dolgov <[email protected]>2023-10-25 12:55:09 +0300
commit865ecc87963dc3b26e66296616eef2a1cc41ac3f (patch)
treebf2ecd8a391103bdb2c8b70cd33c47467310754b /classes/pref
parent0a5507d3bd79d04c860455664f919bf8e7274fda (diff)
move to psr-4 autoloader
Diffstat (limited to 'classes/pref')
-rwxr-xr-xclasses/pref/feeds.php1301
-rwxr-xr-xclasses/pref/filters.php966
-rw-r--r--classes/pref/labels.php225
-rw-r--r--classes/pref/prefs.php1603
-rw-r--r--classes/pref/system.php225
-rw-r--r--classes/pref/users.php294
6 files changed, 0 insertions, 4614 deletions
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
deleted file mode 100755
index fa56f6d1a..000000000
--- a/classes/pref/feeds.php
+++ /dev/null
@@ -1,1301 +0,0 @@
-<?php
-class Pref_Feeds extends Handler_Protected {
- const E_ICON_FILE_TOO_LARGE = 'E_ICON_FILE_TOO_LARGE';
- const E_ICON_RENAME_FAILED = 'E_ICON_RENAME_FAILED';
- const E_ICON_UPLOAD_FAILED = 'E_ICON_UPLOAD_FAILED';
- const E_ICON_UPLOAD_SUCCESS = 'E_ICON_UPLOAD_SUCCESS';
-
- function csrf_ignore(string $method): bool {
- $csrf_ignored = array("index", "getfeedtree", "savefeedorder");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
- /**
- * @return array<int, string>
- */
- public static function get_ts_languages(): array {
- if (Config::get(Config::DB_TYPE) == 'pgsql') {
- return array_map('ucfirst',
- array_column(ORM::for_table('pg_ts_config')->select('cfgname')->find_array(), 'cfgname'));
- }
-
- return [];
- }
-
- function renameCat(): void {
- $cat = ORM::for_table("ttrss_feed_categories")
- ->where("owner_uid", $_SESSION["uid"])
- ->find_one($_REQUEST['id']);
-
- $title = clean($_REQUEST['title']);
-
- if ($cat && $title) {
- $cat->title = $title;
- $cat->save();
- }
- }
-
- /**
- * @return array<int, array<string, bool|int|string>>
- */
- private function get_category_items(int $cat_id): array {
-
- if (clean($_REQUEST['mode'] ?? 0) != 2)
- $search = $_SESSION["prefs_feed_search"] ?? "";
- else
- $search = "";
-
- // first one is set by API
- $show_empty_cats = self::_param_to_bool($_REQUEST['force_show_empty'] ?? false) ||
- (clean($_REQUEST['mode'] ?? 0) != 2 && !$search);
-
- $items = [];
-
- $feed_categories = ORM::for_table('ttrss_feed_categories')
- ->select_many('id', 'title')
- ->where(['owner_uid' => $_SESSION['uid'], 'parent_cat' => $cat_id])
- ->order_by_asc('order_id')
- ->order_by_asc('title')
- ->find_many();
-
- foreach ($feed_categories as $feed_category) {
- $cat = [
- 'id' => 'CAT:' . $feed_category->id,
- 'bare_id' => (int)$feed_category->id,
- 'name' => $feed_category->title,
- 'items' => $this->get_category_items($feed_category->id),
- 'checkbox' => false,
- 'type' => 'category',
- 'unread' => -1,
- 'child_unread' => -1,
- 'auxcounter' => -1,
- 'parent_id' => $cat_id,
- ];
-
- $num_children = $this->calculate_children_count($cat);
- $cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
-
- if ($num_children > 0 || $show_empty_cats)
- array_push($items, $cat);
- }
-
- $feeds_obj = ORM::for_table('ttrss_feeds')
- ->select_many('id', 'title', 'last_error', 'update_interval')
- ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
- ->where(['cat_id' => $cat_id, 'owner_uid' => $_SESSION['uid']])
- ->order_by_asc('order_id')
- ->order_by_asc('title');
-
- if ($search) {
- $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
- }
-
- foreach ($feeds_obj->find_many() as $feed) {
- array_push($items, [
- 'id' => 'FEED:' . $feed->id,
- 'bare_id' => (int) $feed->id,
- 'auxcounter' => -1,
- 'name' => $feed->title,
- 'checkbox' => false,
- 'unread' => -1,
- 'error' => $feed->last_error,
- 'icon' => Feeds::_get_icon($feed->id),
- 'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
- 'updates_disabled' => (int)($feed->update_interval < 0),
- ]);
- }
-
- return $items;
- }
-
- function getfeedtree(): void {
- print json_encode($this->_makefeedtree());
- }
-
- /**
- * @return array<string, array<int|string, mixed>|string>
- */
- function _makefeedtree(): array {
-
- if (clean($_REQUEST['mode'] ?? 0) != 2)
- $search = $_SESSION["prefs_feed_search"] ?? "";
- else
- $search = "";
-
- $root = array();
- $root['id'] = 'root';
- $root['name'] = __('Feeds');
- $root['items'] = array();
- $root['param'] = 0;
- $root['type'] = 'category';
-
- $enable_cats = get_pref(Prefs::ENABLE_FEED_CATS);
-
- if (clean($_REQUEST['mode'] ?? 0) == 2) {
-
- if ($enable_cats) {
- $cat = $this->feedlist_init_cat(Feeds::CATEGORY_SPECIAL);
- } else {
- $cat['items'] = array();
- }
-
- foreach ([Feeds::FEED_ALL, Feeds::FEED_FRESH, Feeds::FEED_STARRED, Feeds::FEED_PUBLISHED,
- Feeds::FEED_ARCHIVED, Feeds::FEED_RECENTLY_READ] as $feed_id) {
- array_push($cat['items'], $this->feedlist_init_feed($feed_id));
- }
-
- /* Plugin feeds for -1 (Feeds::CATEGORY_SPECIAL) */
-
- $feeds = PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL);
-
- if ($feeds) {
- foreach ($feeds as $feed) {
- $feed_id = PluginHost::pfeed_to_feed_id($feed['id']);
-
- $item = array();
- $item['id'] = 'FEED:' . $feed_id;
- $item['bare_id'] = (int)$feed_id;
- $item['auxcounter'] = -1;
- $item['name'] = $feed['title'];
- $item['checkbox'] = false;
- $item['error'] = '';
- $item['icon'] = $feed['icon'];
-
- $item['param'] = '';
- $item['unread'] = -1;
- $item['type'] = 'feed';
-
- array_push($cat['items'], $item);
- }
- }
-
- if ($enable_cats) {
- array_push($root['items'], $cat);
- } else {
- array_push($root['items'], ...$cat['items']);
- }
-
- $sth = $this->pdo->prepare("SELECT * FROM
- ttrss_labels2 WHERE owner_uid = ? ORDER by caption");
- $sth->execute([$_SESSION['uid']]);
-
- if (get_pref(Prefs::ENABLE_FEED_CATS)) {
- $cat = $this->feedlist_init_cat(Feeds::CATEGORY_LABELS);
- } else {
- $cat['items'] = [];
- }
-
- $labels = ORM::for_table('ttrss_labels2')
- ->where('owner_uid', $_SESSION['uid'])
- ->order_by_asc('caption')
- ->find_many();
-
- if (count($labels)) {
- foreach ($labels as $label) {
- $label_id = Labels::label_to_feed_id($label->id);
- $feed = $this->feedlist_init_feed($label_id, null, false);
- $feed['fg_color'] = $label->fg_color;
- $feed['bg_color'] = $label->bg_color;
- array_push($cat['items'], $feed);
- }
-
- if ($enable_cats) {
- array_push($root['items'], $cat);
- } else {
- array_push($root['items'], ...$cat['items']);
- }
- }
- }
-
- if ($enable_cats) {
- $show_empty_cats = self::_param_to_bool($_REQUEST['force_show_empty'] ?? false) ||
- (clean($_REQUEST['mode'] ?? 0) != 2 && !$search);
-
- $feed_categories = ORM::for_table('ttrss_feed_categories')
- ->select_many('id', 'title')
- ->where('owner_uid', $_SESSION['uid'])
- ->where_null('parent_cat')
- ->order_by_asc('order_id')
- ->order_by_asc('title')
- ->find_many();
-
- foreach ($feed_categories as $feed_category) {
- $cat = [
- 'id' => 'CAT:' . $feed_category->id,
- 'bare_id' => (int) $feed_category->id,
- 'auxcounter' => -1,
- 'name' => $feed_category->title,
- 'items' => $this->get_category_items($feed_category->id),
- 'checkbox' => false,
- 'type' => 'category',
- 'unread' => -1,
- 'child_unread' => -1,
- ];
-
- $num_children = $this->calculate_children_count($cat);
- $cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
-
- if ($num_children > 0 || $show_empty_cats)
- array_push($root['items'], $cat);
-
- //$root['param'] += count($cat['items']);
- }
-
- /**
- * Uncategorized is a special case.
- *
- * Define a minimal array shape to help PHPStan with the type of $cat['items']
- * @var array{items: array<int, array<string, mixed>>} $cat
- */
- $cat = [
- 'id' => 'CAT:0',
- 'bare_id' => 0,
- 'auxcounter' => -1,
- 'name' => __('Uncategorized'),
- 'items' => [],
- 'type' => 'category',
- 'checkbox' => false,
- 'unread' => -1,
- 'child_unread' => -1,
- ];
-
- $feeds_obj = ORM::for_table('ttrss_feeds')
- ->select_many('id', 'title', 'last_error', 'update_interval')
- ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
- ->where('owner_uid', $_SESSION['uid'])
- ->where_null('cat_id')
- ->order_by_asc('order_id')
- ->order_by_asc('title');
-
- if ($search) {
- $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
- }
-
- foreach ($feeds_obj->find_many() as $feed) {
- array_push($cat['items'], [
- 'id' => 'FEED:' . $feed->id,
- 'bare_id' => (int) $feed->id,
- 'auxcounter' => -1,
- 'name' => $feed->title,
- 'checkbox' => false,
- 'error' => $feed->last_error,
- 'icon' => Feeds::_get_icon($feed->id),
- 'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
- 'unread' => -1,
- 'type' => 'feed',
- 'updates_disabled' => (int)($feed->update_interval < 0),
- ]);
- }
-
- $cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($cat['items'])), count($cat['items']));
-
- if (count($cat['items']) > 0 || $show_empty_cats)
- array_push($root['items'], $cat);
-
- $num_children = $this->calculate_children_count($root);
- $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
-
- } else {
- $feeds_obj = ORM::for_table('ttrss_feeds')
- ->select_many('id', 'title', 'last_error', 'update_interval')
- ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
- ->where('owner_uid', $_SESSION['uid'])
- ->order_by_asc('order_id')
- ->order_by_asc('title');
-
- if ($search) {
- $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
- }
-
- foreach ($feeds_obj->find_many() as $feed) {
- array_push($root['items'], [
- 'id' => 'FEED:' . $feed->id,
- 'bare_id' => (int) $feed->id,
- 'auxcounter' => -1,
- 'name' => $feed->title,
- 'checkbox' => false,
- 'error' => $feed->last_error,
- 'icon' => Feeds::_get_icon($feed->id),
- 'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
- 'unread' => -1,
- 'type' => 'feed',
- 'updates_disabled' => (int)($feed->update_interval < 0),
- ]);
- }
-
- $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($root['items'])), count($root['items']));
- }
-
- return [
- 'identifier' => 'id',
- 'label' => 'name',
- 'items' => clean($_REQUEST['mode'] ?? 0) != 2 ? [$root] : $root['items'],
- ];
- }
-
- function catsortreset(): void {
- $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
- SET order_id = 0 WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- }
-
- function feedsortreset(): void {
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds
- SET order_id = 0 WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- }
-
- /**
- * @param array<string, mixed> $data_map
- */
- private function process_category_order(array &$data_map, string $item_id = '', string $parent_id = '', int $nest_level = 0): void {
-
- $prefix = "";
- for ($i = 0; $i < $nest_level; $i++)
- $prefix .= " ";
-
- Debug::log("$prefix C: $item_id P: $parent_id");
-
- $bare_item_id = substr($item_id, strpos($item_id, ':')+1);
-
- if ($item_id != 'root') {
- if ($parent_id && $parent_id != 'root') {
- $parent_bare_id = substr($parent_id, strpos($parent_id, ':')+1);
- $parent_qpart = $parent_bare_id;
- } else {
- $parent_qpart = null;
- }
-
- $feed_category = ORM::for_table('ttrss_feed_categories')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($bare_item_id);
-
- if ($feed_category) {
- $feed_category->parent_cat = $parent_qpart;
- $feed_category->save();
- }
- }
-
- $order_id = 1;
-
- $cat = ($data_map[$item_id] ?? false);
-
- if ($cat && is_array($cat)) {
- foreach ($cat as $item) {
- $id = $item['_reference'];
- $bare_id = substr($id, strpos($id, ':')+1);
-
- Debug::log("$prefix [$order_id] $id/$bare_id");
-
- if ($item['_reference']) {
-
- if (strpos($id, "FEED") === 0) {
-
- $feed = ORM::for_table('ttrss_feeds')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($bare_id);
-
- if ($feed) {
- $feed->order_id = $order_id;
- $feed->cat_id = ($item_id != "root" && $bare_item_id) ? $bare_item_id : null;
- $feed->save();
- }
- } else if (strpos($id, "CAT:") === 0) {
- $this->process_category_order($data_map, $item['_reference'], $item_id,
- $nest_level+1);
-
- $feed_category = ORM::for_table('ttrss_feed_categories')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($bare_id);
-
- if ($feed_category) {
- $feed_category->order_id = $order_id;
- $feed_category->save();
- }
- }
- }
-
- ++$order_id;
- }
- }
- }
-
- function savefeedorder(): void {
- $data = json_decode($_POST['payload'], true);
-
- #file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
- #$data = json_decode(file_get_contents("/tmp/saveorder.json"), true);
-
- if (!is_array($data['items']))
- $data['items'] = json_decode($data['items'], true);
-
-# print_r($data['items']);
-
- if (is_array($data) && is_array($data['items'])) {
-# $cat_order_id = 0;
-
- /** @var array<int, mixed> */
- $data_map = array();
- $root_item = '';
-
- foreach ($data['items'] as $item) {
-
-# if ($item['id'] != 'root') {
- if (is_array($item['items'] ?? false)) {
- if (isset($item['items']['_reference'])) {
- $data_map[$item['id']] = array($item['items']);
- } else {
- $data_map[$item['id']] = $item['items'];
- }
- }
- if ($item['id'] == 'root') {
- $root_item = $item['id'];
- }
- }
-
- $this->process_category_order($data_map, $root_item);
- }
- }
-
- function removeIcon(): void {
- $feed_id = (int) $_REQUEST["feed_id"];
-
- $cache = DiskCache::instance('feed-icons');
-
- $feed = ORM::for_table('ttrss_feeds')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($feed_id);
-
- if ($feed && $cache->exists((string)$feed_id)) {
- if ($cache->remove((string)$feed_id)) {
- $feed->set([
- 'favicon_avg_color' => null,
- 'favicon_last_checked' => '1970-01-01',
- 'favicon_is_custom' => false,
- ]);
- $feed->save();
- }
- }
- }
-
- function uploadIcon(): void {
- $feed_id = (int) $_REQUEST['feed_id'];
- $tmp_file = tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'icon');
-
- // default value
- $rc = self::E_ICON_UPLOAD_FAILED;
-
- $feed = ORM::for_table('ttrss_feeds')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($feed_id);
-
- if ($feed && $tmp_file && move_uploaded_file($_FILES['icon_file']['tmp_name'], $tmp_file)) {
- if (filesize($tmp_file) < Config::get(Config::MAX_FAVICON_FILE_SIZE)) {
-
- $cache = DiskCache::instance('feed-icons');
-
- if ($cache->put((string)$feed_id, file_get_contents($tmp_file))) {
-
- $feed->set([
- 'favicon_avg_color' => null,
- 'favicon_is_custom' => true,
- ]);
-
- if ($feed->save()) {
- $rc = self::E_ICON_UPLOAD_SUCCESS;
- }
-
- } else {
- $rc = self::E_ICON_RENAME_FAILED;
- }
-
- @unlink($tmp_file);
-
- } else {
- $rc = self::E_ICON_FILE_TOO_LARGE;
- }
- }
-
- if (file_exists($tmp_file))
- unlink($tmp_file);
-
- print json_encode(['rc' => $rc, 'icon_url' =>
- Feeds::_get_icon($feed_id) . "?ts=" . time() ]);
- }
-
- function editfeed(): void {
- global $purge_intervals;
- global $update_intervals;
-
- $feed_id = (int)clean($_REQUEST["id"]);
-
- $row = ORM::for_table('ttrss_feeds')
- ->where("owner_uid", $_SESSION["uid"])
- ->find_one($feed_id)->as_array();
-
- if ($row) {
-
- ob_start();
- PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_EDIT_FEED, $feed_id);
- $plugin_data = trim((string)ob_get_contents());
- ob_end_clean();
-
- $row["icon"] = Feeds::_get_icon($feed_id);
-
- $local_update_intervals = $update_intervals;
- $local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[get_pref(Prefs::DEFAULT_UPDATE_INTERVAL)]);
-
- if (Config::get(Config::FORCE_ARTICLE_PURGE) == 0) {
- $local_purge_intervals = $purge_intervals;
- $default_purge_interval = get_pref(Prefs::PURGE_OLD_DAYS);
-
- if ($default_purge_interval > 0)
- $local_purge_intervals[0] .= " " . T_nsprintf('(%d day)', '(%d days)', $default_purge_interval, $default_purge_interval);
- else
- $local_purge_intervals[0] .= " " . sprintf("(%s)", __("Disabled"));
-
- } else {
- $purge_interval = Config::get(Config::FORCE_ARTICLE_PURGE);
- $local_purge_intervals = [ T_nsprintf('%d day', '%d days', $purge_interval, $purge_interval) ];
- }
-
- $user = ORM::for_table("ttrss_users")->find_one($_SESSION["uid"]);
-
- print json_encode([
- "feed" => $row,
- "cats" => [
- "enabled" => get_pref(Prefs::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,
- ],
- "user" => [
- "access_level" => $user->access_level
- ],
- "lang" => [
- "enabled" => Config::get(Config::DB_TYPE) == "pgsql",
- "default" => get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE),
- "all" => $this::get_ts_languages(),
- ]
- ]);
- }
- }
-
- private function _batch_toggle_checkbox(string $name): string {
- return \Controls\checkbox_tag("", false, "",
- ["data-control-for" => $name, "title" => __("Check to enable field"), "onchange" => "App.dialogOf(this).toggleField(this)"]);
- }
-
- function editfeeds(): void {
- global $purge_intervals;
- global $update_intervals;
-
- $feed_ids = clean($_REQUEST["ids"]);
-
- $local_update_intervals = $update_intervals;
- $local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[get_pref(Prefs::DEFAULT_UPDATE_INTERVAL)]);
-
- $local_purge_intervals = $purge_intervals;
- $default_purge_interval = get_pref(Prefs::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"));
-
- $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')
- ];
-
- print_notice("Enable the options you wish to apply using checkboxes on the right.");
- ?>
-
- <?= \Controls\hidden_tag("ids", $feed_ids) ?>
- <?= \Controls\hidden_tag("op", "pref-feeds") ?>
- <?= \Controls\hidden_tag("method", "batchEditSave") ?>
-
- <div dojoType="dijit.layout.TabContainer" style="height : 450px">
- <div dojoType="dijit.layout.ContentPane" title="<?= __('General') ?>">
- <section>
- <?php if (get_pref(Prefs::ENABLE_FEED_CATS)) { ?>
- <fieldset>
- <label><?= __('Place in category:') ?></label>
- <?= \Controls\select_feeds_cats("cat_id", null, ['disabled' => '1']) ?>
- <?= $this->_batch_toggle_checkbox("cat_id") ?>
- </fieldset>
- <?php } ?>
-
- <?php if (Config::get(Config::DB_TYPE) == "pgsql") { ?>
- <fieldset>
- <label><?= __('Language:') ?></label>
- <?= \Controls\select_tag("feed_language", "", $this::get_ts_languages(), ["disabled"=> 1]) ?>
- <?= $this->_batch_toggle_checkbox("feed_language") ?>
- </fieldset>
- <?php } ?>
- </section>
-
- <hr/>
-
- <section>
- <fieldset>
- <label><?= __("Update interval:") ?></label>
- <?= \Controls\select_hash("update_interval", "", $local_update_intervals, ["disabled" => 1]) ?>
- <?= $this->_batch_toggle_checkbox("update_interval") ?>
- </fieldset>
-
- <?php if (Config::get(Config::FORCE_ARTICLE_PURGE) == 0) { ?>
- <fieldset>
- <label><?= __('Article purging:') ?></label>
- <?= \Controls\select_hash("purge_interval", "", $local_purge_intervals, ["disabled" => 1]) ?>
- <?= $this->_batch_toggle_checkbox("purge_interval") ?>
- </fieldset>
- <?php } ?>
- </section>
- </div>
- <div dojoType="dijit.layout.ContentPane" title="<?= __('Authentication') ?>">
- <section>
- <fieldset>
- <label><?= __("Login:") ?></label>
- <input dojoType='dijit.form.TextBox'
- disabled='1' autocomplete='new-password' name='auth_login' value=''>
- <?= $this->_batch_toggle_checkbox("auth_login") ?>
- </fieldset>
- <fieldset>
- <label><?= __("Password:") ?></label>
- <input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
- autocomplete='new-password' disabled='1' value=''>
- <?= $this->_batch_toggle_checkbox("auth_pass") ?>
- </fieldset>
- </section>
- </div>
- <div dojoType="dijit.layout.ContentPane" title="<?= __('Options') ?>">
- <?php
- foreach ($options as $name => $caption) {
- ?>
- <fieldset class='narrow'>
- <label class="checkbox text-muted">
- <?= \Controls\checkbox_tag($name, false, "", ["disabled" => "1"]) ?>
- <?= $caption ?>
- <?= $this->_batch_toggle_checkbox($name) ?>
- </label>
- </fieldset>
- <?php } ?>
- </div>
- </div>
-
- <footer>
- <?= \Controls\submit_tag(__("Save")) ?>
- <?= \Controls\cancel_dialog_tag(__("Cancel")) ?>
- </footer>
- <?php
- }
-
- function batchEditSave(): void {
- $this->editsaveops(true);
- }
-
- function editSave(): void {
- $this->editsaveops(false);
- }
-
- private function editsaveops(bool $batch): void {
-
- $feed_title = clean($_POST["title"] ?? "");
- $feed_url = clean($_POST["feed_url"] ?? "");
- $site_url = clean($_POST["site_url"] ?? "");
- $upd_intl = (int) clean($_POST["update_interval"] ?? 0);
- $purge_intl = (int) clean($_POST["purge_interval"] ?? 0);
- $feed_id = (int) clean($_POST["id"] ?? 0); /* editSave */
- $feed_ids = explode(",", clean($_POST["ids"] ?? "")); /* batchEditSave */
- $cat_id = (int) clean($_POST["cat_id"] ?? 0);
- $auth_login = clean($_POST["auth_login"] ?? "");
- $auth_pass = clean($_POST["auth_pass"] ?? "");
- $private = checkbox_to_sql_bool($_POST["private"] ?? "");
- $include_in_digest = checkbox_to_sql_bool($_POST["include_in_digest"] ?? "");
- $cache_images = checkbox_to_sql_bool($_POST["cache_images"] ?? "");
- $hide_images = checkbox_to_sql_bool($_POST["hide_images"] ?? "");
- $always_display_enclosures = checkbox_to_sql_bool($_POST["always_display_enclosures"] ?? "");
- $mark_unread_on_update = checkbox_to_sql_bool($_POST["mark_unread_on_update"] ?? "");
-
- $feed_language = clean($_POST["feed_language"] ?? "");
-
- if (!$batch) {
-
- /* $sth = $this->pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed_id]);
- $row = $sth->fetch();$orig_feed_url = $row["feed_url"];
-
- $reset_basic_info = $orig_feed_url != $feed_url; */
-
- $feed = ORM::for_table('ttrss_feeds')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($feed_id);
-
- if ($feed) {
-
- $feed->title = $feed_title;
- $feed->cat_id = $cat_id ? $cat_id : null;
- $feed->feed_url = $feed_url;
- $feed->site_url = $site_url;
- $feed->update_interval = $upd_intl;
- $feed->purge_interval = $purge_intl;
- $feed->auth_login = $auth_login;
- $feed->auth_pass = $auth_pass;
- $feed->private = (int)$private;
- $feed->cache_images = (int)$cache_images;
- $feed->hide_images = (int)$hide_images;
- $feed->feed_language = $feed_language;
- $feed->include_in_digest = (int)$include_in_digest;
- $feed->always_display_enclosures = (int)$always_display_enclosures;
- $feed->mark_unread_on_update = (int)$mark_unread_on_update;
-
- $feed->save();
-
- PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_SAVE_FEED, $feed_id);
- }
-
- } else {
- $feed_data = array();
-
- foreach (array_keys($_POST) as $k) {
- if ($k != "op" && $k != "method" && $k != "ids") {
- $feed_data[$k] = clean($_POST[$k]);
- }
- }
-
- $this->pdo->beginTransaction();
-
- $feed_ids_qmarks = arr_qmarks($feed_ids);
-
- foreach (array_keys($feed_data) as $k) {
-
- $qpart = "";
- $qparams = [];
-
- switch ($k) {
- case "update_interval":
- $qpart = "update_interval = ?";
- $qparams = [$upd_intl];
- break;
-
- case "purge_interval":
- $qpart = "purge_interval = ?";
- $qparams = [$purge_intl];
- break;
-
- case "auth_login":
- $qpart = "auth_login = ?";
- $qparams = [$auth_login];
- break;
-
- case "auth_pass":
- $qpart = "auth_pass = ?, auth_pass_encrypted = false";
- $qparams = [$auth_pass];
- break;
-
- case "private":
- $qpart = "private = ?";
- $qparams = [$private];
- break;
-
- case "include_in_digest":
- $qpart = "include_in_digest = ?";
- $qparams = [$include_in_digest];
- break;
-
- case "always_display_enclosures":
- $qpart = "always_display_enclosures = ?";
- $qparams = [$always_display_enclosures];
- break;
-
- case "mark_unread_on_update":
- $qpart = "mark_unread_on_update = ?";
- $qparams = [$mark_unread_on_update];
- break;
-
- case "cache_images":
- $qpart = "cache_images = ?";
- $qparams = [$cache_images];
- break;
-
- case "hide_images":
- $qpart = "hide_images = ?";
- $qparams = [$hide_images];
- break;
-
- case "cat_id":
- if (get_pref(Prefs::ENABLE_FEED_CATS)) {
- $qpart = "cat_id = ?";
- $qparams = $cat_id ? [$cat_id] : [null];
- }
- break;
-
- case "feed_language":
- $qpart = "feed_language = ?";
- $qparams = [$feed_language];
- break;
-
- }
-
- if ($qpart) {
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET $qpart WHERE id IN ($feed_ids_qmarks)
- AND owner_uid = ?");
- $sth->execute([...$qparams, ...$feed_ids, $_SESSION['uid']]);
- }
- }
-
- $this->pdo->commit();
- }
- }
-
- function remove(): void {
- /** @var array<int, int> */
- $ids = array_map('intval', explode(",", clean($_REQUEST["ids"])));
-
- foreach ($ids as $id) {
- self::remove_feed($id, $_SESSION["uid"]);
- }
- }
-
- function removeCat(): void {
- $ids = explode(",", clean($_REQUEST["ids"]));
- foreach ($ids as $id) {
- Feeds::_remove_cat((int)$id, $_SESSION["uid"]);
- }
- }
-
- function addCat(): void {
- $feed_cat = clean($_REQUEST["cat"]);
-
- Feeds::_add_cat($feed_cat, $_SESSION['uid']);
- }
-
- function importOpml(): void {
- $opml = new OPML($_REQUEST);
- $opml->opml_import($_SESSION["uid"]);
- }
-
- private function index_feeds(): void {
- $error_button = "<button dojoType='dijit.form.Button'
- id='pref_feeds_errors_btn' style='display : none'
- onclick='CommonDialogs.showFeedsWithErrors()'>".
- __("Feeds with errors")."</button>";
-
- $inactive_button = "<button dojoType='dijit.form.Button'
- id='pref_feeds_inactive_btn'
- style='display : none'
- onclick=\"dijit.byId('feedTree').showInactiveFeeds()\">" .
- __("Inactive feeds") . "</button>";
-
- $feed_search = clean($_REQUEST["search"] ?? "");
-
- if (array_key_exists("search", $_REQUEST)) {
- $_SESSION["prefs_feed_search"] = $feed_search;
- } else {
- $feed_search = $_SESSION["prefs_feed_search"] ?? "";
- }
-
- ?>
-
- <div dojoType="dijit.layout.BorderContainer" gutters="false">
- <div region='top' dojoType="fox.Toolbar">
- <div style='float : right'>
- <input dojoType="dijit.form.TextBox" id="feed_search" size="20" type="search"
- value="<?= htmlspecialchars($feed_search) ?>">
- <button dojoType="dijit.form.Button" onclick="dijit.byId('feedTree').reload()">
- <?= __('Search') ?></button>
- </div>
-
- <div dojoType="fox.form.DropDownButton">
- <span><?= __('Select') ?></span>
- <div dojoType="dijit.Menu" style="display: none;">
- <div onclick="dijit.byId('feedTree').model.setAllChecked(true)"
- dojoType="dijit.MenuItem"><?= __('All') ?></div>
- <div onclick="dijit.byId('feedTree').model.setAllChecked(false)"
- dojoType="dijit.MenuItem"><?= __('None') ?></div>
- </div>
- </div>
-
- <div dojoType="fox.form.DropDownButton">
- <span><?= __('Feeds') ?></span>
- <div dojoType="dijit.Menu" style="display: none">
- <div onclick="CommonDialogs.subscribeToFeed()"
- dojoType="dijit.MenuItem"><?= __('Subscribe to feed') ?></div>
- <div onclick="dijit.byId('feedTree').editSelectedFeed()"
- dojoType="dijit.MenuItem"><?= __('Edit selected feeds') ?></div>
- <div onclick="dijit.byId('feedTree').resetFeedOrder()"
- dojoType="dijit.MenuItem"><?= __('Reset sort order') ?></div>
- <div onclick="dijit.byId('feedTree').batchSubscribe()"
- dojoType="dijit.MenuItem"><?= __('Batch subscribe') ?></div>
- <div dojoType="dijit.MenuItem" onclick="dijit.byId('feedTree').removeSelectedFeeds()">
- <?= __('Unsubscribe') ?></div>
- </div>
- </div>
-
- <?php if (get_pref(Prefs::ENABLE_FEED_CATS)) { ?>
- <div dojoType="fox.form.DropDownButton">
- <span><?= __('Categories') ?></span>
- <div dojoType="dijit.Menu" style="display: none">
- <div onclick="dijit.byId('feedTree').createCategory()"
- dojoType="dijit.MenuItem"><?= __('Add category') ?></div>
- <div onclick="dijit.byId('feedTree').resetCatOrder()"
- dojoType="dijit.MenuItem"><?= __('Reset sort order') ?></div>
- <div onclick="dijit.byId('feedTree').removeSelectedCategories()"
- dojoType="dijit.MenuItem"><?= __('Remove selected') ?></div>
- </div>
- </div>
- <?php } ?>
- <?= $error_button ?>
- <?= $inactive_button ?>
- </div>
- <div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">
- <div dojoType="fox.PrefFeedStore" jsId="feedStore"
- url="backend.php?op=pref-feeds&method=getfeedtree">
- </div>
-
- <div dojoType="lib.CheckBoxStoreModel" jsId="feedModel" store="feedStore"
- query="{id:'root'}" rootId="root" rootLabel="Feeds" childrenAttrs="items"
- checkboxStrict="false" checkboxAll="false">
- </div>
-
- <div dojoType="fox.PrefFeedTree" id="feedTree"
- dndController="dijit.tree.dndSource"
- betweenThreshold="5"
- autoExpand="<?= (!empty($feed_search) ? "true" : "false") ?>"
- persist="true"
- model="feedModel"
- openOnClick="false">
- </div>
- </div>
- </div>
- <?php
-
- }
-
- private function index_opml(): void {
- ?>
-
- <form id='opml_import_form' method='post' enctype='multipart/form-data'>
- <label class='dijitButton'><?= __("Choose file...") ?>
- <input style='display : none' id='opml_file' name='opml_file' type='file'>
- </label>
- <input type='hidden' name='op' value='pref-feeds'>
- <input type='hidden' name='csrf_token' value="<?= $_SESSION['csrf_token'] ?>">
- <input type='hidden' name='method' value='importOpml'>
- <button dojoType='dijit.form.Button' class='alt-primary' onclick="return Helpers.OPML.import()" type="submit">
- <?= \Controls\icon("file_upload") ?>
- <?= __('Import OPML') ?>
- </button>
- </form>
-
- <hr/>
-
- <?php print_notice("Only main settings profile can be migrated using OPML.") ?>
-
- <form dojoType='dijit.form.Form' id='opmlExportForm' style='display : inline-block'>
- <button dojoType='dijit.form.Button' onclick='Helpers.OPML.export()'>
- <?= \Controls\icon("file_download") ?>
- <?= __('Export OPML') ?>
- </button>
-
- <label class='checkbox'>
- <?= \Controls\checkbox_tag("include_settings", true, "1") ?>
- <?= __("Include tt-rss settings") ?>
- </label>
- </form>
-
- <?php
- PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsOPML");
- }
-
- private function index_shared(): void {
- ?>
-
- <?= format_notice('Published articles can be subscribed by anyone who knows the following URL:') ?></h3>
-
- <button dojoType='dijit.form.Button' class='alt-primary'
- onclick="CommonDialogs.generatedFeed(<?= Feeds::FEED_PUBLISHED ?>, false)">
- <?= \Controls\icon('share') ?>
- <?= __('Display URL') ?>
- </button>
-
- <button class='alt-danger' dojoType='dijit.form.Button' onclick='return Helpers.Feeds.clearFeedAccessKeys()'>
- <?= \Controls\icon('delete') ?>
- <?= __('Clear all generated URLs') ?>
- </button>
-
- <?php
- PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsPublishedGenerated");
- }
-
- function index(): void {
- ?>
-
- <div dojoType='dijit.layout.TabContainer' tabPosition='left-h'>
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane'
- title="<i class='material-icons'>rss_feed</i> <?= __('My feeds') ?>">
- <?php $this->index_feeds() ?>
- </div>
-
- <div dojoType='dijit.layout.ContentPane'
- title="<i class='material-icons'>import_export</i> <?= __('OPML') ?>">
- <?php $this->index_opml() ?>
- </div>
-
- <div dojoType="dijit.layout.ContentPane"
- title="<i class='material-icons'>share</i> <?= __('Sharing') ?>">
- <?php $this->index_shared() ?>
- </div>
-
- <?php
- ob_start();
- PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFeeds");
- $plugin_data = trim((string)ob_get_contents());
- ob_end_clean();
- ?>
-
- <?php if ($plugin_data) { ?>
- <div dojoType='dijit.layout.ContentPane'
- title="<i class='material-icons'>extension</i> <?= __('Plugins') ?>">
-
- <div dojoType='dijit.layout.AccordionContainer' region='center'>
- <?= $plugin_data ?>
- </div>
- </div>
- <?php } ?>
- </div>
- <?php
- }
-
- /**
- * @return array<string, mixed>
- */
- private function feedlist_init_cat(int $cat_id): array {
- $span = OpenTelemetry\API\Trace\Span::getCurrent();
- $span->addEvent(__METHOD__ . ": $cat_id");
-
- return [
- 'id' => 'CAT:' . $cat_id,
- 'items' => array(),
- 'name' => Feeds::_get_cat_title($cat_id),
- 'type' => 'category',
- 'unread' => -1, //(int) Feeds::_get_cat_unread($cat_id);
- 'bare_id' => $cat_id,
- ];
- }
-
- /**
- * @return array<string, mixed>
- */
- private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
- $span = OpenTelemetry\API\Trace\Span::getCurrent();
- $span->addEvent(__METHOD__ . ": $feed_id");
-
- if (!$title)
- $title = Feeds::_get_title($feed_id, false);
-
- if ($unread === false)
- $unread = Feeds::_get_counters($feed_id, false, true);
-
- return [
- 'id' => 'FEED:' . $feed_id,
- 'name' => $title,
- 'unread' => (int) $unread,
- 'type' => 'feed',
- 'error' => $error,
- 'updated' => $updated,
- 'icon' => Feeds::_get_icon($feed_id),
- 'bare_id' => $feed_id,
- 'auxcounter' => 0,
- ];
- }
-
- function inactiveFeeds(): void {
-
- if (Config::get(Config::DB_TYPE) == "pgsql") {
- $interval_qpart = "NOW() - INTERVAL '3 months'";
- } else {
- $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
- }
-
- $inactive_feeds = ORM::for_table('ttrss_feeds')
- ->table_alias('f')
- ->select_many('f.id', 'f.title', 'f.site_url', 'f.feed_url')
- ->select_expr('MAX(e.updated)', 'last_article')
- ->join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue')
- ->join('ttrss_entries', ['e.id', '=', 'ue.ref_id'], 'e')
- ->where('f.owner_uid', $_SESSION['uid'])
- ->where_raw(
- "(SELECT MAX(ttrss_entries.updated)
- FROM ttrss_entries
- JOIN ttrss_user_entries ON ttrss_entries.id = ttrss_user_entries.ref_id
- WHERE ttrss_user_entries.feed_id = f.id) < $interval_qpart")
- ->group_by('f.title')
- ->group_by('f.id')
- ->group_by('f.site_url')
- ->group_by('f.feed_url')
- ->order_by_asc('last_article')
- ->find_array();
-
- foreach ($inactive_feeds as $inactive_feed) {
- $inactive_feed['last_article'] = TimeHelper::make_local_datetime($inactive_feed['last_article'], false);
- }
-
- print json_encode($inactive_feeds);
- }
-
- function feedsWithErrors(): void {
- print json_encode(ORM::for_table('ttrss_feeds')
- ->select_many('id', 'title', 'feed_url', 'last_error', 'site_url')
- ->where_not_equal('last_error', '')
- ->where('owner_uid', $_SESSION['uid'])
- ->where_gte('update_interval', 0)
- ->find_array());
- }
-
- static function remove_feed(int $id, int $owner_uid): void {
-
- if (PluginHost::getInstance()->run_hooks_until(PluginHost::HOOK_UNSUBSCRIBE_FEED, true, $id, $owner_uid))
- return;
-
- $pdo = Db::pdo();
-
- if ($id > 0) {
- $pdo->beginTransaction();
-
- /* save starred articles in Archived feed */
-
- $sth = $pdo->prepare("UPDATE ttrss_user_entries SET
- feed_id = NULL, orig_feed_id = NULL
- WHERE feed_id = ? AND marked = true AND owner_uid = ?");
-
- $sth->execute([$id, $owner_uid]);
-
- /* Remove access key for the feed */
-
- $sth = $pdo->prepare("DELETE FROM ttrss_access_keys WHERE
- feed_id = ? AND owner_uid = ?");
- $sth->execute([$id, $owner_uid]);
-
- /* remove the feed */
-
- $sth = $pdo->prepare("DELETE FROM ttrss_feeds
- WHERE id = ? AND owner_uid = ?");
- $sth->execute([$id, $owner_uid]);
-
- $pdo->commit();
-
- $favicon_cache = DiskCache::instance('feed-icons');
-
- if ($favicon_cache->exists((string)$id))
- $favicon_cache->remove((string)$id);
-
- } else {
- Labels::remove(Labels::feed_to_label_id($id), $owner_uid);
- }
- }
-
- function batchSubscribe(): void {
- print json_encode([
- "enable_cats" => (int)get_pref(Prefs::ENABLE_FEED_CATS),
- "cat_select" => \Controls\select_feeds_cats("cat")
- ]);
- }
-
- function batchAddFeeds(): void {
- $cat_id = clean($_REQUEST['cat']);
- $feeds = explode("\n", clean($_REQUEST['feeds']));
- $login = clean($_REQUEST['login']);
- $pass = clean($_REQUEST['pass']);
-
- $user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
-
- // TODO: we should return some kind of error code to frontend here
- if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
- return;
- }
-
- $csth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
- WHERE feed_url = ? AND owner_uid = ?");
-
- $isth = $this->pdo->prepare("INSERT INTO ttrss_feeds
- (owner_uid,feed_url,title,cat_id,auth_login,auth_pass,update_method,auth_pass_encrypted)
- VALUES (?, ?, '[Unknown]', ?, ?, ?, 0, false)");
-
- foreach ($feeds as $feed) {
- $feed = trim($feed);
-
- if (UrlHelper::validate($feed)) {
-
- $this->pdo->beginTransaction();
-
- $csth->execute([$feed, $_SESSION['uid']]);
-
- if (!$csth->fetch()) {
- $isth->execute([$_SESSION['uid'], $feed, $cat_id ? $cat_id : null, $login, $pass]);
- }
-
- $this->pdo->commit();
- }
- }
- }
-
- function clearKeys(): void {
- Feeds::_clear_access_keys($_SESSION['uid']);
- }
-
- function regenFeedKey(): void {
- $feed_id = clean($_REQUEST['id']);
- $is_cat = self::_param_to_bool($_REQUEST['is_cat'] ?? false);
-
- $new_key = Feeds::_update_access_key($feed_id, $is_cat, $_SESSION["uid"]);
-
- print json_encode(["link" => $new_key]);
- }
-
- function getSharedURL(): void {
- $feed_id = clean($_REQUEST['id']);
- $is_cat = self::_param_to_bool($_REQUEST['is_cat'] ?? false);
- $search = clean($_REQUEST['search']);
-
- $link = Config::get_self_url() . "/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
- ]);
- }
-
- /**
- * @param array<string, mixed> $cat
- */
- private function calculate_children_count(array $cat): int {
- $c = 0;
-
- foreach ($cat['items'] ?? [] as $child) {
- if (($child['type'] ?? '') == 'category') {
- $c += $this->calculate_children_count($child);
- } else {
- $c += 1;
- }
- }
-
- return $c;
- }
-
-}
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
deleted file mode 100755
index 2656c0370..000000000
--- a/classes/pref/filters.php
+++ /dev/null
@@ -1,966 +0,0 @@
-<?php
-class Pref_Filters extends Handler_Protected {
-
- const ACTION_TAG = 4;
- const ACTION_SCORE = 6;
- const ACTION_LABEL = 7;
- const ACTION_PLUGIN = 9;
- const ACTION_REMOVE_TAG = 10;
-
- const PARAM_ACTIONS = [self::ACTION_TAG, self::ACTION_SCORE,
- self::ACTION_LABEL, self::ACTION_PLUGIN, self::ACTION_REMOVE_TAG];
-
- function csrf_ignore(string $method): bool {
- $csrf_ignored = array("index", "getfiltertree", "savefilterorder");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
- function filtersortreset(): void {
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2
- SET order_id = 0 WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- }
-
- function savefilterorder(): void {
- $data = json_decode($_POST['payload'], true);
-
- #file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
- #$data = json_decode(file_get_contents("/tmp/saveorder.json"), true);
-
- if (!is_array($data['items']))
- $data['items'] = json_decode($data['items'], true);
-
- $index = 0;
-
- if (is_array($data) && is_array($data['items'])) {
-
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2 SET
- order_id = ? WHERE id = ? AND
- owner_uid = ?");
-
- foreach ($data['items'][0]['items'] as $item) {
- $filter_id = (int) str_replace("FILTER:", "", $item['_reference']);
-
- if ($filter_id > 0) {
- $sth->execute([$index, $filter_id, $_SESSION['uid']]);
- ++$index;
- }
- }
- }
- }
-
- function testFilterDo(): void {
- $offset = (int) clean($_REQUEST["offset"]);
- $limit = (int) clean($_REQUEST["limit"]);
-
- $filter = array();
-
- $filter["enabled"] = true;
- $filter["match_any_rule"] = checkbox_to_sql_bool($_REQUEST["match_any_rule"] ?? false);
- $filter["inverse"] = checkbox_to_sql_bool($_REQUEST["inverse"] ?? false);
-
- $filter["rules"] = array();
- $filter["actions"] = array("dummy-action");
-
- $res = $this->pdo->query("SELECT id,name FROM ttrss_filter_types");
-
- /** @var array<int, string> */
- $filter_types = [];
-
- while ($line = $res->fetch()) {
- $filter_types[$line["id"]] = $line["name"];
- }
-
- $scope_qparts = array();
-
- $rctr = 0;
-
- /** @var string $r */
- foreach (clean($_REQUEST["rule"]) AS $r) {
- /** @var array{'reg_exp': string, 'filter_type': int, 'feed_id': array<int, int|string>, 'name': string}|null */
- $rule = json_decode($r, true);
-
- if ($rule && $rctr < 5) {
- $rule["type"] = $filter_types[$rule["filter_type"]];
- unset($rule["filter_type"]);
-
- $scope_inner_qparts = [];
-
- /** @var int|string $feed_id may be a category string (e.g. 'CAT:7') or feed ID int */
- foreach ($rule["feed_id"] as $feed_id) {
-
- if (strpos("$feed_id", "CAT:") === 0) {
- $cat_id = (int) substr("$feed_id", 4);
- array_push($scope_inner_qparts, "cat_id = " . $cat_id);
- } else if (is_numeric($feed_id) && $feed_id > 0) {
- array_push($scope_inner_qparts, "feed_id = " . (int)$feed_id);
- }
- }
-
- if (count($scope_inner_qparts) > 0) {
- array_push($scope_qparts, "(" . implode(" OR ", $scope_inner_qparts) . ")");
- }
-
- array_push($filter["rules"], $rule);
-
- ++$rctr;
- } else {
- break;
- }
- }
-
- if (count($scope_qparts) == 0) $scope_qparts = ["true"];
-
- $glue = $filter['match_any_rule'] ? " OR " : " AND ";
- $scope_qpart = join($glue, $scope_qparts);
-
- /** @phpstan-ignore-next-line */
- if (!$scope_qpart) $scope_qpart = "true";
-
- $rv = array();
-
- //while ($found < $limit && $offset < $limit * 1000 && time() - $started < ini_get("max_execution_time") * 0.7) {
-
- $sth = $this->pdo->prepare("SELECT ttrss_entries.id,
- ttrss_entries.title,
- ttrss_feeds.id AS feed_id,
- ttrss_feeds.title AS feed_title,
- ttrss_feed_categories.id AS cat_id,
- content,
- date_entered,
- link,
- author,
- tag_cache
- FROM
- ttrss_entries, ttrss_user_entries
- LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)
- LEFT JOIN ttrss_feed_categories ON (ttrss_feeds.cat_id = ttrss_feed_categories.id)
- WHERE
- ref_id = ttrss_entries.id AND
- ($scope_qpart) AND
- ttrss_user_entries.owner_uid = ?
- ORDER BY date_entered DESC LIMIT $limit OFFSET $offset");
-
- $sth->execute([$_SESSION['uid']]);
-
- while ($line = $sth->fetch()) {
-
- $rc = RSSUtils::get_article_filters(array($filter), $line['title'], $line['content'], $line['link'],
- $line['author'], explode(",", $line['tag_cache']));
-
- if (count($rc) > 0) {
-
- $line["content_preview"] = truncate_string(strip_tags($line["content"]), 200, '&hellip;');
-
- $excerpt_length = 100;
-
- PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
- function ($result) use (&$line) {
- $line = $result;
- },
- $line, $excerpt_length);
-
- $content_preview = $line["content_preview"];
-
- $tmp = "<li><span class='title'>" . $line["title"] . "</span><br/>" .
- "<span class='feed'>" . $line['feed_title'] . "</span>, <span class='date'>" . mb_substr($line["date_entered"], 0, 16) . "</span>" .
- "<div class='preview text-muted'>" . $content_preview . "</div>" .
- "</li>";
-
- array_push($rv, $tmp);
-
- }
- }
-
- print json_encode($rv);
- }
-
- private function _get_rules_list(int $filter_id): string {
- $sth = $this->pdo->prepare("SELECT reg_exp,
- inverse,
- match_on,
- feed_id,
- cat_id,
- cat_filter,
- ttrss_filter_types.description AS field
- FROM
- ttrss_filters2_rules, ttrss_filter_types
- WHERE
- filter_id = ? AND filter_type = ttrss_filter_types.id
- ORDER BY reg_exp");
- $sth->execute([$filter_id]);
-
- $rv = "";
-
- while ($line = $sth->fetch()) {
-
- if ($line["match_on"]) {
- $feeds = json_decode($line["match_on"], true);
- $feeds_fmt = [];
-
- foreach ($feeds as $feed_id) {
-
- if (strpos($feed_id, "CAT:") === 0) {
- $feed_id = (int)substr($feed_id, 4);
- array_push($feeds_fmt, Feeds::_get_cat_title($feed_id));
- } else {
- if ($feed_id)
- array_push($feeds_fmt, Feeds::_get_title((int)$feed_id));
- else
- array_push($feeds_fmt, __("All feeds"));
- }
- }
-
- $where = implode(", ", $feeds_fmt);
-
- } else {
-
- $where = $line["cat_filter"] ?
- Feeds::_get_cat_title($line["cat_id"] ?? 0) :
- ($line["feed_id"] ?
- Feeds::_get_title($line["feed_id"]) : __("All feeds"));
- }
-
-# $where = $line["cat_id"] . "/" . $line["feed_id"];
-
- $inverse = $line["inverse"] ? "inverse" : "";
-
- $rv .= "<li class='$inverse'>" . T_sprintf("%s on %s in %s %s",
- htmlspecialchars($line["reg_exp"]),
- $line["field"],
- $where,
- $line["inverse"] ? __("(inverse)") : "") . "</li>";
- }
-
- return $rv;
- }
-
- function getfiltertree(): void {
- $root = array();
- $root['id'] = 'root';
- $root['name'] = __('Filters');
- $root['enabled'] = true;
- $root['items'] = array();
-
- $filter_search = ($_SESSION["prefs_filter_search"] ?? "");
-
- $sth = $this->pdo->prepare("SELECT *,
- (SELECT action_param FROM ttrss_filters2_actions
- WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1) AS action_param,
- (SELECT action_id FROM ttrss_filters2_actions
- WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1) AS action_id,
- (SELECT description FROM ttrss_filter_actions
- WHERE id = (SELECT action_id FROM ttrss_filters2_actions
- WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1)) AS action_name,
- (SELECT reg_exp FROM ttrss_filters2_rules
- WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1) AS reg_exp
- FROM ttrss_filters2 WHERE
- owner_uid = ? ORDER BY order_id, title");
- $sth->execute([$_SESSION['uid']]);
-
- $folder = array();
- $folder['items'] = array();
-
- while ($line = $sth->fetch()) {
-
- $name = $this->_get_name($line["id"]);
-
- $match_ok = false;
- if ($filter_search) {
- if (mb_strpos($line['title'], $filter_search) !== false) {
- $match_ok = true;
- }
-
- $rules_sth = $this->pdo->prepare("SELECT reg_exp
- FROM ttrss_filters2_rules WHERE filter_id = ?");
- $rules_sth->execute([$line['id']]);
-
- while ($rule_line = $rules_sth->fetch()) {
- if (mb_strpos($rule_line['reg_exp'], $filter_search) !== false) {
- $match_ok = true;
- break;
- }
- }
- }
-
- if ($line['action_id'] == self::ACTION_LABEL) {
- $label_sth = $this->pdo->prepare("SELECT fg_color, bg_color
- FROM ttrss_labels2 WHERE caption = ? AND
- owner_uid = ?");
- $label_sth->execute([$line['action_param'], $_SESSION['uid']]);
-
- if ($label_row = $label_sth->fetch()) {
- //$fg_color = $label_row["fg_color"];
- $bg_color = $label_row["bg_color"];
-
- $name[1] = "<i class=\"material-icons\" style='color : $bg_color; margin-right : 4px'>label</i>" . $name[1];
- }
- }
-
- $filter = array();
- $filter['id'] = 'FILTER:' . $line['id'];
- $filter['bare_id'] = $line['id'];
- $filter['name'] = $name[0];
- $filter['param'] = $name[1];
- $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->_get_rules_list($line['id']);
-
- if (!$filter_search || $match_ok) {
- array_push($folder['items'], $filter);
- }
- }
-
- $root['items'] = $folder['items'];
-
- $fl = array();
- $fl['identifier'] = 'id';
- $fl['label'] = 'name';
- $fl['items'] = array($root);
-
- print json_encode($fl);
- }
-
- function edit(): void {
-
- $filter_id = (int) clean($_REQUEST["id"] ?? 0);
-
- $sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2
- WHERE id = ? AND owner_uid = ?");
- $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"])
- ];
-
- $res = $this->pdo->query("SELECT id,description
- FROM ttrss_filter_types WHERE id != 5 ORDER BY description");
-
- while ($line = $res->fetch()) {
- $rv["filter_types"][$line["id"]] = __($line["description"]);
- }
-
- $res = $this->pdo->query("SELECT id,description FROM ttrss_filter_actions
- ORDER BY name");
-
- while ($line = $res->fetch()) {
- $rv["action_types"][$line["id"]] = __($line["description"]);
- }
-
- $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]);
-
- while ($rrow = $rules_sth->fetch(PDO::FETCH_ASSOC)) {
- if ($rrow["match_on"]) {
- $rrow["feed_id"] = json_decode($rrow["match_on"], true);
- } else {
- if ($rrow["cat_filter"]) {
- $feed_id = "CAT:" . (int)$rrow["cat_id"];
- } else {
- $feed_id = (int)$rrow["feed_id"];
- }
-
- $rrow["feed_id"] = ["" . $feed_id]; // set item type to string for in_array()
- }
-
- 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"]);
-
- $rrow["name"] = $this->_get_rule_name($rrow);
-
- array_push($rv["rules"], $rrow);
- }
-
- $actions_sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
- WHERE filter_id = ? ORDER BY id");
- $actions_sth->execute([$filter_id]);
-
- while ($arow = $actions_sth->fetch(PDO::FETCH_ASSOC)) {
- $arow["action_param_label"] = $arow["action_param"];
-
- unset($arow["filter_id"]);
- unset($arow["id"]);
-
- $arow["name"] = $this->_get_action_name($arow);
-
- array_push($rv["actions"], $arow);
- }
- }
- print json_encode($rv);
- }
- }
-
- /**
- * @param array<string, mixed>|null $rule
- */
- private function _get_rule_name(?array $rule = null): string {
- if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
-
- $feeds = $rule["feed_id"];
- $feeds_fmt = [];
-
- if (!is_array($feeds)) $feeds = [$feeds];
-
- foreach ($feeds as $feed_id) {
-
- if (strpos($feed_id, "CAT:") === 0) {
- $feed_id = (int)substr($feed_id, 4);
- array_push($feeds_fmt, Feeds::_get_cat_title($feed_id));
- } else {
- if ($feed_id)
- array_push($feeds_fmt, Feeds::_get_title((int)$feed_id));
- else
- array_push($feeds_fmt, __("All feeds"));
- }
- }
-
- $feed = implode(", ", $feeds_fmt);
-
- $sth = $this->pdo->prepare("SELECT description FROM ttrss_filter_types
- WHERE id = ?");
- $sth->execute([(int)$rule["filter_type"]]);
-
- if ($row = $sth->fetch()) {
- $filter_type = $row["description"];
- } else {
- $filter_type = "?UNKNOWN?";
- }
-
- $inverse = isset($rule["inverse"]) ? "inverse" : "";
-
- return "<span class='filterRule $inverse'>" .
- T_sprintf("%s on %s in %s %s", htmlspecialchars($rule["reg_exp"]),
- "<span class='field'>$filter_type</span>", "<span class='feed'>$feed</span>", isset($rule["inverse"]) ? __("(inverse)") : "") . "</span>";
- }
-
- function printRuleName(): void {
- print $this->_get_rule_name(json_decode(clean($_REQUEST["rule"]), true));
- }
-
- /**
- * @param array<string, mixed>|null $action
- */
- private function _get_action_name(?array $action = null): string {
- if (!$action) {
- return "";
- }
-
- $sth = $this->pdo->prepare("SELECT description FROM
- ttrss_filter_actions WHERE id = ?");
- $sth->execute([(int)$action["action_id"]]);
-
- $title = "";
-
- if ($row = $sth->fetch()) {
-
- $title = __($row["description"]);
-
- if ($action["action_id"] == self::ACTION_PLUGIN) {
- list ($pfclass, $pfaction) = explode(":", $action["action_param"]);
-
- $filter_actions = PluginHost::getInstance()->get_filter_actions();
-
- foreach ($filter_actions as $fclass => $factions) {
- foreach ($factions as $faction) {
- if ($pfaction == $faction["action"] && $pfclass == $fclass) {
- $title .= ": " . $fclass . ": " . $faction["description"];
- break;
- }
- }
- }
- } else if (in_array($action["action_id"], self::PARAM_ACTIONS)) {
- $title .= ": " . $action["action_param"];
- }
- }
-
- return $title;
- }
-
- function printActionName(): void {
- print $this->_get_action_name(json_decode(clean($_REQUEST["action"] ?? ""), true));
- }
-
- function editSave(): void {
- $filter_id = (int) clean($_REQUEST["id"]);
- $enabled = checkbox_to_sql_bool($_REQUEST["enabled"] ?? false);
- $match_any_rule = checkbox_to_sql_bool($_REQUEST["match_any_rule"] ?? false);
- $inverse = checkbox_to_sql_bool($_REQUEST["inverse"] ?? false);
- $title = clean($_REQUEST["title"]);
-
- $this->pdo->beginTransaction();
-
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2 SET enabled = ?,
- match_any_rule = ?,
- inverse = ?,
- title = ?
- WHERE id = ? AND owner_uid = ?");
-
- $sth->execute([$enabled, $match_any_rule, $inverse, $title, $filter_id, $_SESSION['uid']]);
-
- $this->_save_rules_and_actions($filter_id);
-
- $this->pdo->commit();
- }
-
- function remove(): void {
-
- $ids = explode(",", clean($_REQUEST["ids"]));
- $ids_qmarks = arr_qmarks($ids);
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_filters2 WHERE id IN ($ids_qmarks)
- AND owner_uid = ?");
- $sth->execute([...$ids, $_SESSION['uid']]);
- }
-
- private function _save_rules_and_actions(int $filter_id): void {
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
- $sth->execute([$filter_id]);
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_actions WHERE filter_id = ?");
- $sth->execute([$filter_id]);
-
- if (!is_array(clean($_REQUEST["rule"] ?? ""))) $_REQUEST["rule"] = [];
- if (!is_array(clean($_REQUEST["action"] ?? ""))) $_REQUEST["action"] = [];
-
- if ($filter_id) {
- /* create rules */
-
- $rules = array();
- $actions = array();
-
- foreach (clean($_REQUEST["rule"]) as $rule) {
- $rule = json_decode($rule, true);
- unset($rule["id"]);
-
- if (array_search($rule, $rules) === false) {
- array_push($rules, $rule);
- }
- }
-
- foreach (clean($_REQUEST["action"]) as $action) {
- $action = json_decode($action, true);
- unset($action["id"]);
-
- if (array_search($action, $actions) === false) {
- array_push($actions, $action);
- }
- }
-
- $rsth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
- (filter_id, reg_exp,filter_type,feed_id,cat_id,match_on,inverse) VALUES
- (?, ?, ?, NULL, NULL, ?, ?)");
-
- foreach ($rules as $rule) {
- if ($rule) {
-
- $reg_exp = trim($rule["reg_exp"]);
- $inverse = isset($rule["inverse"]) ? 1 : 0;
-
- $filter_type = (int)trim($rule["filter_type"]);
- $match_on = json_encode($rule["feed_id"]);
-
- $rsth->execute([$filter_id, $reg_exp, $filter_type, $match_on, $inverse]);
- }
- }
-
- $asth = $this->pdo->prepare("INSERT INTO ttrss_filters2_actions
- (filter_id, action_id, action_param) VALUES
- (?, ?, ?)");
-
- foreach ($actions as $action) {
- if ($action) {
-
- $action_id = (int)$action["action_id"];
- $action_param = $action["action_param"];
- $action_param_label = $action["action_param_label"];
-
- if ($action_id == self::ACTION_LABEL) {
- $action_param = $action_param_label;
- }
-
- if ($action_id == self::ACTION_SCORE) {
- $action_param = (int)str_replace("+", "", $action_param);
- }
-
- if (in_array($action_id, [self::ACTION_TAG, self::ACTION_REMOVE_TAG])) {
- $action_param = implode(", ", FeedItem_Common::normalize_categories(
- explode(",", $action_param)));
- }
-
- $asth->execute([$filter_id, $action_id, $action_param]);
- }
- }
- }
- }
-
- function add(): void {
- $enabled = checkbox_to_sql_bool($_REQUEST["enabled"] ?? false);
- $match_any_rule = checkbox_to_sql_bool($_REQUEST["match_any_rule"] ?? false);
- $title = clean($_REQUEST["title"]);
- $inverse = checkbox_to_sql_bool($_REQUEST["inverse"] ?? false);
-
- $this->pdo->beginTransaction();
-
- /* create base filter */
-
- $sth = $this->pdo->prepare("INSERT INTO ttrss_filters2
- (owner_uid, match_any_rule, enabled, title, inverse) VALUES
- (?, ?, ?, ?, ?)");
-
- $sth->execute([$_SESSION['uid'], $match_any_rule, $enabled, $title, $inverse]);
-
- $sth = $this->pdo->prepare("SELECT MAX(id) AS id FROM ttrss_filters2
- WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
-
- if ($row = $sth->fetch()) {
- $filter_id = $row['id'];
- $this->_save_rules_and_actions($filter_id);
- }
-
- $this->pdo->commit();
- }
-
- function index(): void {
- if (array_key_exists("search", $_REQUEST)) {
- $filter_search = clean($_REQUEST["search"]);
- $_SESSION["prefs_filter_search"] = $filter_search;
- } else {
- $filter_search = ($_SESSION["prefs_filter_search"] ?? "");
- }
-
- ?>
- <div dojoType='dijit.layout.BorderContainer' gutters='false'>
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
- <div dojoType='fox.Toolbar'>
-
- <div style='float : right; padding-right : 4px;'>
- <input dojoType="dijit.form.TextBox" id="filter_search" size="20" type="search"
- value="<?= htmlspecialchars($filter_search) ?>">
- <button dojoType="dijit.form.Button" onclick="dijit.byId('filterTree').reload()">
- <?= __('Search') ?></button>
- </div>
-
- <div dojoType="fox.form.DropDownButton">
- <span><?= __('Select') ?></span>
- <div dojoType="dijit.Menu" style="display: none;">
- <div onclick="dijit.byId('filterTree').model.setAllChecked(true)"
- dojoType="dijit.MenuItem"><?= __('All') ?></div>
- <div onclick="dijit.byId('filterTree').model.setAllChecked(false)"
- dojoType="dijit.MenuItem"><?= __('None') ?></div>
- </div>
- </div>
-
- <button dojoType="dijit.form.Button" onclick="return Filters.edit()">
- <?= __('Create filter') ?></button>
- <button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').joinSelectedFilters()">
- <?= __('Combine') ?></button>
- <button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').removeSelectedFilters()">
- <?= __('Remove') ?></button>
- <button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').resetFilterOrder()">
- <?= __('Reset sort order') ?></button>
- <button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').toggleRules()">
- <?= __('Toggle rule display') ?></button>
-
- </div>
- </div>
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
- <div dojoType="fox.PrefFilterStore" jsId="filterStore"
- url="backend.php?op=pref-filters&method=getfiltertree">
- </div>
- <div dojoType="lib.CheckBoxStoreModel" jsId="filterModel" store="filterStore"
- query="{id:'root'}" rootId="root" rootLabel="Filters"
- childrenAttrs="items" checkboxStrict="false" checkboxAll="false">
- </div>
- <div dojoType="fox.PrefFilterTree" id="filterTree" dndController="dijit.tree.dndSource"
- betweenThreshold="5" model="filterModel" openOnClick="true">
- </div>
- </div>
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters") ?>
- </div>
- <?php
- }
-
- function editrule(): void {
- /** @var array<int, int|string> */
- $feed_ids = explode(",", clean($_REQUEST["ids"]));
-
- print json_encode([
- "multiselect" => $this->_feed_multi_select("feed_id", $feed_ids, 'required="1" style="width : 100%; height : 300px" dojoType="fox.form.ValidationMultiSelect"')
- ]);
- }
-
- /**
- * @return array<int, string>
- */
- private function _get_name(int $id): array {
-
- $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
- FROM ttrss_filters2 AS f LEFT JOIN ttrss_filters2_rules AS r
- ON (r.filter_id = f.id)
- LEFT JOIN ttrss_filters2_actions AS a
- ON (a.filter_id = f.id) WHERE f.id = ? GROUP BY f.title, f.match_any_rule, f.inverse");
- $sth->execute([$id]);
-
- if ($row = $sth->fetch()) {
-
- $title = $row["title"];
- $num_rules = $row["num_rules"];
- $num_actions = $row["num_actions"];
- $match_any_rule = $row["match_any_rule"];
- $inverse = $row["inverse"];
-
- if (!$title) $title = __("[No caption]");
-
- $title = sprintf(_ngettext("%s (%d rule)", "%s (%d rules)", (int) $num_rules), $title, $num_rules);
-
- $sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
- WHERE filter_id = ? ORDER BY id LIMIT 1");
- $sth->execute([$id]);
-
- $actions = "";
-
- if ($line = $sth->fetch()) {
- $actions = $this->_get_action_name($line);
-
- $num_actions -= 1;
- }
-
- if ($match_any_rule) $title .= " (" . __("matches any rule") . ")";
- if ($inverse) $title .= " (" . __("inverse") . ")";
-
- if ($num_actions > 0)
- $actions = sprintf(_ngettext("%s (+%d action)", "%s (+%d actions)", (int) $num_actions), $actions, $num_actions);
-
- return [$title, $actions];
- }
-
- return [];
- }
-
- function join(): void {
- /** @var array<int, int> */
- $ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
-
- if (count($ids) > 1) {
- $base_id = array_shift($ids);
- $ids_qmarks = arr_qmarks($ids);
-
- $this->pdo->beginTransaction();
-
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2_rules
- SET filter_id = ? WHERE filter_id IN ($ids_qmarks)");
- $sth->execute([$base_id, ...$ids]);
-
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2_actions
- SET filter_id = ? WHERE filter_id IN ($ids_qmarks)");
- $sth->execute([$base_id, ...$ids]);
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_filters2 WHERE id IN ($ids_qmarks)");
- $sth->execute($ids);
-
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2 SET match_any_rule = true WHERE id = ?");
- $sth->execute([$base_id]);
-
- $this->pdo->commit();
-
- $this->_optimize($base_id);
-
- }
- }
-
- private function _optimize(int $id): void {
-
- $this->pdo->beginTransaction();
-
- $sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
- WHERE filter_id = ?");
- $sth->execute([$id]);
-
- $tmp = array();
- $dupe_ids = array();
-
- while ($line = $sth->fetch()) {
- $id = $line["id"];
- unset($line["id"]);
-
- if (array_search($line, $tmp) === false) {
- array_push($tmp, $line);
- } else {
- array_push($dupe_ids, $id);
- }
- }
-
- if (count($dupe_ids) > 0) {
- $ids_str = join(",", $dupe_ids);
-
- $this->pdo->query("DELETE FROM ttrss_filters2_actions WHERE id IN ($ids_str)");
- }
-
- $sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_rules
- WHERE filter_id = ?");
- $sth->execute([$id]);
-
- $tmp = array();
- $dupe_ids = array();
-
- while ($line = $sth->fetch()) {
- $id = $line["id"];
- unset($line["id"]);
-
- if (array_search($line, $tmp) === false) {
- array_push($tmp, $line);
- } else {
- array_push($dupe_ids, $id);
- }
- }
-
- if (count($dupe_ids) > 0) {
- $ids_str = join(",", $dupe_ids);
-
- $this->pdo->query("DELETE FROM ttrss_filters2_rules WHERE id IN ($ids_str)");
- }
-
- $this->pdo->commit();
- }
-
- /**
- * @param array<int, int|string> $default_ids
- */
- private function _feed_multi_select(string $id, array $default_ids = [], string $attributes = "",
- bool $include_all_feeds = true, ?int $root_id = null, int $nest_level = 0): string {
-
- $pdo = Db::pdo();
-
- $rv = "";
-
- // print_r(in_array("CAT:6",$default_ids));
-
- if (!$root_id) {
- $rv .= "<select multiple=\true\" id=\"$id\" name=\"$id\" $attributes>";
- if ($include_all_feeds) {
- $is_selected = (in_array("0", $default_ids)) ? "selected=\"1\"" : "";
- $rv .= "<option $is_selected value=\"0\">".__('All feeds')."</option>";
- }
- }
-
- if (get_pref(Prefs::ENABLE_FEED_CATS)) {
-
- if (!$root_id) $root_id = null;
-
- $sth = $pdo->prepare("SELECT id,title,
- (SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE
- c2.parent_cat = ttrss_feed_categories.id) AS num_children
- FROM ttrss_feed_categories
- WHERE owner_uid = :uid AND
- (parent_cat = :root_id OR (:root_id IS NULL AND parent_cat IS NULL)) ORDER BY title");
-
- $sth->execute([":uid" => $_SESSION['uid'], ":root_id" => $root_id]);
-
- while ($line = $sth->fetch()) {
-
- for ($i = 0; $i < $nest_level; $i++)
- $line["title"] = " " . $line["title"];
-
- $is_selected = in_array("CAT:".$line["id"], $default_ids) ? "selected=\"1\"" : "";
-
- $rv .= sprintf("<option $is_selected value='CAT:%d'>%s</option>",
- $line["id"], htmlspecialchars($line["title"]));
-
- if ($line["num_children"] > 0)
- $rv .= $this->_feed_multi_select($id, $default_ids, $attributes,
- $include_all_feeds, $line["id"], $nest_level+1);
-
- $f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
- WHERE cat_id = ? AND owner_uid = ? ORDER BY title");
-
- $f_sth->execute([$line['id'], $_SESSION['uid']]);
-
- while ($fline = $f_sth->fetch()) {
- $is_selected = (in_array($fline["id"], $default_ids)) ? "selected=\"1\"" : "";
-
- $fline["title"] = " " . $fline["title"];
-
- for ($i = 0; $i < $nest_level; $i++)
- $fline["title"] = " " . $fline["title"];
-
- $rv .= sprintf("<option $is_selected value='%d'>%s</option>",
- $fline["id"], htmlspecialchars($fline["title"]));
- }
- }
-
- if (!$root_id) {
- $is_selected = in_array("CAT:0", $default_ids) ? "selected=\"1\"" : "";
-
- $rv .= sprintf("<option $is_selected value='CAT:0'>%s</option>",
- __("Uncategorized"));
-
- $f_sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
- WHERE cat_id IS NULL AND owner_uid = ? ORDER BY title");
- $f_sth->execute([$_SESSION['uid']]);
-
- while ($fline = $f_sth->fetch()) {
- $is_selected = in_array($fline["id"], $default_ids) ? "selected=\"1\"" : "";
-
- $fline["title"] = " " . $fline["title"];
-
- for ($i = 0; $i < $nest_level; $i++)
- $fline["title"] = " " . $fline["title"];
-
- $rv .= sprintf("<option $is_selected value='%d'>%s</option>",
- $fline["id"], htmlspecialchars($fline["title"]));
- }
- }
-
- } else {
- $sth = $pdo->prepare("SELECT id,title FROM ttrss_feeds
- WHERE owner_uid = ? ORDER BY title");
- $sth->execute([$_SESSION['uid']]);
-
- while ($line = $sth->fetch()) {
-
- $is_selected = (in_array($line["id"], $default_ids)) ? "selected=\"1\"" : "";
-
- $rv .= sprintf("<option $is_selected value='%d'>%s</option>",
- $line["id"], htmlspecialchars($line["title"]));
- }
- }
-
- if (!$root_id) {
- $rv .= "</select>";
- }
-
- return $rv;
- }
-}
diff --git a/classes/pref/labels.php b/classes/pref/labels.php
deleted file mode 100644
index 2e128691e..000000000
--- a/classes/pref/labels.php
+++ /dev/null
@@ -1,225 +0,0 @@
-<?php
-class Pref_Labels extends Handler_Protected {
-
- function csrf_ignore(string $method): bool {
- $csrf_ignored = array("index", "getlabeltree");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
- function edit(): void {
- $label = ORM::for_table('ttrss_labels2')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($_REQUEST['id']);
-
- if ($label) {
- print json_encode($label->as_array());
- }
- }
-
- function getlabeltree(): void {
- $root = array();
- $root['id'] = 'root';
- $root['name'] = __('Labels');
- $root['items'] = array();
-
- $sth = $this->pdo->prepare("SELECT *
- FROM ttrss_labels2
- WHERE owner_uid = ?
- ORDER BY caption");
- $sth->execute([$_SESSION['uid']]);
-
- while ($line = $sth->fetch()) {
- $label = array();
- $label['id'] = 'LABEL:' . $line['id'];
- $label['bare_id'] = $line['id'];
- $label['name'] = $line['caption'];
- $label['fg_color'] = $line['fg_color'];
- $label['bg_color'] = $line['bg_color'];
- $label['type'] = 'label';
- $label['checkbox'] = false;
-
- array_push($root['items'], $label);
- }
-
- $fl = array();
- $fl['identifier'] = 'id';
- $fl['label'] = 'name';
- $fl['items'] = array($root);
-
- print json_encode($fl);
- }
-
- function colorset(): void {
- $kind = clean($_REQUEST["kind"]);
- $ids = explode(',', clean($_REQUEST["ids"]));
- $color = clean($_REQUEST["color"]);
- $fg = clean($_REQUEST["fg"]);
- $bg = clean($_REQUEST["bg"]);
-
- foreach ($ids as $id) {
-
- if ($kind == "fg" || $kind == "bg") {
- $sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
- {$kind}_color = ? WHERE id = ?
- AND owner_uid = ?");
-
- $sth->execute([$color, $id, $_SESSION['uid']]);
-
- } else {
-
- $sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
- fg_color = ?, bg_color = ? WHERE id = ?
- AND owner_uid = ?");
-
- $sth->execute([$fg, $bg, $id, $_SESSION['uid']]);
- }
-
- /* Remove cached data */
-
- $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET label_cache = ''
- WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- }
- }
-
- function colorreset(): void {
- $ids = explode(',', clean($_REQUEST["ids"]));
-
- foreach ($ids as $id) {
- $sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
- fg_color = '', bg_color = '' WHERE id = ?
- AND owner_uid = ?");
- $sth->execute([$id, $_SESSION['uid']]);
-
- /* Remove cached data */
-
- $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET label_cache = ''
- WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- }
- }
-
- function save(): void {
-
- $id = clean($_REQUEST["id"]);
- $caption = clean($_REQUEST["caption"]);
-
- $this->pdo->beginTransaction();
-
- $sth = $this->pdo->prepare("SELECT caption FROM ttrss_labels2
- WHERE id = ? AND owner_uid = ?");
- $sth->execute([$id, $_SESSION['uid']]);
-
- if ($row = $sth->fetch()) {
- $old_caption = $row["caption"];
-
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_labels2
- WHERE caption = ? AND owner_uid = ?");
- $sth->execute([$caption, $_SESSION['uid']]);
-
- if (!$sth->fetch()) {
- if ($caption) {
- $sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
- caption = ? WHERE id = ? AND
- owner_uid = ?");
- $sth->execute([$caption, $id, $_SESSION['uid']]);
-
- /* Update filters that reference label being renamed */
-
- $sth = $this->pdo->prepare("UPDATE ttrss_filters2_actions SET
- action_param = ? WHERE action_param = ?
- AND action_id = 7
- AND filter_id IN (SELECT id FROM ttrss_filters2 WHERE owner_uid = ?)");
-
- $sth->execute([$caption, $old_caption, $_SESSION['uid']]);
-
- print clean($_REQUEST["caption"]);
- } else {
- print $old_caption;
- }
- } else {
- print $old_caption;
- }
- }
-
- $this->pdo->commit();
-
- }
-
- function remove(): void {
- /** @var array<int, int> */
- $ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
-
- foreach ($ids as $id) {
- Labels::remove($id, $_SESSION["uid"]);
- }
-
- }
-
- function add(): void {
- $caption = clean($_REQUEST["caption"]);
- $output = clean($_REQUEST["output"] ?? false);
-
- if ($caption) {
- if (Labels::create($caption)) {
- if (!$output) {
- print T_sprintf("Created label <b>%s</b>", htmlspecialchars($caption));
- }
- }
- }
- }
-
- function index(): void {
- ?>
- <div dojoType='dijit.layout.BorderContainer' gutters='false'>
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
- <div dojoType='fox.Toolbar'>
- <div dojoType='fox.form.DropDownButton'>
- <span><?= __('Select') ?></span>
- <div dojoType='dijit.Menu' style='display: none'>
- <div onclick="dijit.byId('labelTree').model.setAllChecked(true)"
- dojoType='dijit.MenuItem'><?=('All') ?></div>
- <div onclick="dijit.byId('labelTree').model.setAllChecked(false)"
- dojoType='dijit.MenuItem'><?=('None') ?></div>
- </div>
- </div>
-
- <button dojoType='dijit.form.Button' onclick='CommonDialogs.addLabel()'>
- <?=('Create label') ?></button dojoType='dijit.form.Button'>
-
- <button dojoType='dijit.form.Button' onclick="dijit.byId('labelTree').removeSelected()">
- <?=('Remove') ?></button dojoType='dijit.form.Button'>
-
- <button dojoType='dijit.form.Button' onclick="dijit.byId('labelTree').resetColors()">
- <?=('Clear colors') ?></button dojoType='dijit.form.Button'>
-
- </div>
- </div>
-
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
- <div dojoType='dojo.data.ItemFileWriteStore' jsId='labelStore'
- url='backend.php?op=pref-labels&method=getlabeltree'>
- </div>
-
- <div dojoType='lib.CheckBoxStoreModel' jsId='labelModel' store='labelStore'
- query="{id:'root'}" rootId='root'
- childrenAttrs='items' checkboxStrict='false' checkboxAll='false'>
- </div>
-
- <div dojoType='fox.PrefLabelTree' id='labelTree' model='labelModel' openOnClick='true'>
- <script type='dojo/method' event='onClick' args='item'>
- var id = String(item.id);
- var bare_id = id.substr(id.indexOf(':')+1);
-
- if (id.match('LABEL:')) {
- dijit.byId('labelTree').editLabel(bare_id);
- }
- </script>
- </div>
- </div>
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefLabels") ?>
- </div>
- <?php
- }
-}
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
deleted file mode 100644
index 2ce5f061b..000000000
--- a/classes/pref/prefs.php
+++ /dev/null
@@ -1,1603 +0,0 @@
-<?php
-use chillerlan\QRCode;
-
-class Pref_Prefs extends Handler_Protected {
- /** @var array<Prefs::*, array<int, string>> */
- private array $pref_help = [];
-
- /** @var array<string, array<int, string>> pref items are Prefs::*|Pref_Prefs::BLOCK_SEPARATOR (PHPStan was complaining) */
- private array $pref_item_map = [];
-
- /** @var array<string, string> */
- private array $pref_help_bottom = [];
-
- /** @var array<int, string> */
- private array $pref_blacklist = [];
-
- private const BLOCK_SEPARATOR = 'BLOCK_SEPARATOR';
-
- const PI_RES_ALREADY_INSTALLED = "PI_RES_ALREADY_INSTALLED";
- const PI_RES_SUCCESS = "PI_RES_SUCCESS";
- const PI_ERR_NO_CLASS = "PI_ERR_NO_CLASS";
- const PI_ERR_NO_INIT_PHP = "PI_ERR_NO_INIT_PHP";
- const PI_ERR_EXEC_FAILED = "PI_ERR_EXEC_FAILED";
- const PI_ERR_NO_TEMPDIR = "PI_ERR_NO_TEMPDIR";
- const PI_ERR_PLUGIN_NOT_FOUND = "PI_ERR_PLUGIN_NOT_FOUND";
- const PI_ERR_NO_WORKDIR = "PI_ERR_NO_WORKDIR";
-
- function csrf_ignore(string $method) : bool {
- $csrf_ignored = array("index", "updateself", "otpqrcode");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
- function __construct($args) {
- parent::__construct($args);
-
- $this->pref_item_map = [
- __('General') => [
- Prefs::USER_LANGUAGE,
- Prefs::USER_TIMEZONE,
- self::BLOCK_SEPARATOR,
- Prefs::USER_CSS_THEME,
- self::BLOCK_SEPARATOR,
- Prefs::ENABLE_API_ACCESS,
- ],
- __('Feeds') => [
- Prefs::DEFAULT_UPDATE_INTERVAL,
- Prefs::FRESH_ARTICLE_MAX_AGE,
- Prefs::DEFAULT_SEARCH_LANGUAGE,
- self::BLOCK_SEPARATOR,
- Prefs::ENABLE_FEED_CATS,
- self::BLOCK_SEPARATOR,
- Prefs::CONFIRM_FEED_CATCHUP,
- Prefs::ON_CATCHUP_SHOW_NEXT_FEED,
- self::BLOCK_SEPARATOR,
- Prefs::HIDE_READ_FEEDS,
- Prefs::HIDE_READ_SHOWS_SPECIAL,
- ],
- __('Articles') => [
- Prefs::PURGE_OLD_DAYS,
- Prefs::PURGE_UNREAD_ARTICLES,
- self::BLOCK_SEPARATOR,
- Prefs::COMBINED_DISPLAY_MODE,
- Prefs::CDM_EXPANDED,
- Prefs::CDM_ENABLE_GRID,
- self::BLOCK_SEPARATOR,
- Prefs::CDM_AUTO_CATCHUP,
- Prefs::VFEED_GROUP_BY_FEED,
- self::BLOCK_SEPARATOR,
- Prefs::SHOW_CONTENT_PREVIEW,
- Prefs::STRIP_IMAGES,
- ],
- __('Digest') => [
- Prefs::DIGEST_ENABLE,
- Prefs::DIGEST_CATCHUP,
- Prefs::DIGEST_PREFERRED_TIME,
- ],
- __('Advanced') => [
- Prefs::BLACKLISTED_TAGS,
- self::BLOCK_SEPARATOR,
- Prefs::LONG_DATE_FORMAT,
- Prefs::SHORT_DATE_FORMAT,
- self::BLOCK_SEPARATOR,
- Prefs::SSL_CERT_SERIAL,
- self::BLOCK_SEPARATOR,
- Prefs::DISABLE_CONDITIONAL_COUNTERS,
- Prefs::HEADLINES_NO_DISTINCT,
- ],
- __('Debugging') => [
- Prefs::DEBUG_HEADLINE_IDS,
- ],
- ];
-
- $this->pref_help_bottom = [
- Prefs::BLACKLISTED_TAGS => __("Never apply these tags automatically (comma-separated list)."),
- ];
-
- $this->pref_help = [
- Prefs::BLACKLISTED_TAGS => array(__("Blacklisted tags"), ""),
- Prefs::DEFAULT_SEARCH_LANGUAGE => array(__("Default language"), __("Used for full-text search")),
- Prefs::CDM_AUTO_CATCHUP => array(__("Mark read on scroll"), __("Mark articles as read as you scroll past them")),
- Prefs::CDM_EXPANDED => array(__("Always expand articles")),
- Prefs::COMBINED_DISPLAY_MODE => array(__("Combined mode"), __("Show flat list of articles instead of separate panels")),
- Prefs::CONFIRM_FEED_CATCHUP => array(__("Confirm marking feeds as read")),
- Prefs::DEFAULT_UPDATE_INTERVAL => array(__("Default update interval")),
- Prefs::DIGEST_CATCHUP => array(__("Mark sent articles as read")),
- Prefs::DIGEST_ENABLE => array(__("Enable digest"), __("Send daily digest of new (and unread) headlines to your e-mail address")),
- Prefs::DIGEST_PREFERRED_TIME => array(__("Try to send around this time"), __("Time in UTC")),
- Prefs::ENABLE_API_ACCESS => array(__("Enable API"), __("Allows accessing this account through the API")),
- Prefs::ENABLE_FEED_CATS => array(__("Enable categories")),
- Prefs::FRESH_ARTICLE_MAX_AGE => array(__("Maximum age of fresh articles"), "<strong>" . __("hours") . "</strong>"),
- Prefs::HIDE_READ_FEEDS => array(__("Hide read feeds")),
- Prefs::HIDE_READ_SHOWS_SPECIAL => array(__("Always show special feeds"), __("While hiding read feeds")),
- Prefs::LONG_DATE_FORMAT => array(__("Long date format"), __("Syntax is identical to PHP <a href='http://php.net/manual/function.date.php'>date()</a> function.")),
- Prefs::ON_CATCHUP_SHOW_NEXT_FEED => array(__("Automatically show next feed"), __("After marking one as read")),
- Prefs::PURGE_OLD_DAYS => array(__("Purge articles older than"), __("<strong>days</strong> (0 disables)")),
- Prefs::PURGE_UNREAD_ARTICLES => array(__("Purge unread articles")),
- Prefs::SHORT_DATE_FORMAT => array(__("Short date format")),
- Prefs::SHOW_CONTENT_PREVIEW => array(__("Show content preview in headlines")),
- Prefs::SSL_CERT_SERIAL => array(__("SSL client certificate")),
- Prefs::STRIP_IMAGES => array(__("Do not embed media")),
- Prefs::USER_TIMEZONE => array(__("Time zone")),
- Prefs::VFEED_GROUP_BY_FEED => array(__("Group by feed"), __("Group multiple-feed output by originating feed")),
- Prefs::USER_LANGUAGE => array(__("Language")),
- Prefs::USER_CSS_THEME => array(__("Theme")),
- Prefs::HEADLINES_NO_DISTINCT => array(__("Don't enforce DISTINCT headlines"), __("May produce duplicate entries")),
- Prefs::DEBUG_HEADLINE_IDS => array(__("Show article and feed IDs"), __("In the headlines buffer")),
- Prefs::DISABLE_CONDITIONAL_COUNTERS => array(__("Disable conditional counter updates"), __("May increase server load")),
- Prefs::CDM_ENABLE_GRID => array(__("Grid view"), __("On wider screens, if always expanded")),
- ];
-
- // hidden in the main prefs UI (use to hide things that have description set above)
- $this->pref_blacklist = [
- //
- ];
- }
-
- function changepassword(): void {
-
- if (Config::get(Config::FORBID_PASSWORD_CHANGES)) {
- print "ERROR: ".format_error("Access forbidden.");
- return;
- }
-
- $old_pw = clean($_POST["old_password"]);
- $new_pw = clean($_POST["new_password"]);
- $new_unclean_pw = $_POST["new_password"];
- $con_pw = clean($_POST["confirm_password"]);
-
- if ($new_unclean_pw != $new_pw) {
- print "ERROR: ".format_error("New password contains disallowed characters.");
- return;
- }
-
- if ($old_pw == $new_pw) {
- print "ERROR: ".format_error("New password must be different from the old one.");
- return;
- }
-
- if ($old_pw == "") {
- print "ERROR: ".format_error("Old password cannot be blank.");
- return;
- }
-
- if ($new_pw == "") {
- print "ERROR: ".format_error("New password cannot be blank.");
- return;
- }
-
- if ($new_pw != $con_pw) {
- print "ERROR: ".format_error("Entered passwords do not match.");
- return;
- }
-
- $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
-
- if (method_exists($authenticator, "change_password")) {
- print format_notice($authenticator->change_password($_SESSION["uid"], $old_pw, $new_pw));
- } else {
- print "ERROR: ".format_error("Function not supported by authentication module.");
- }
- }
-
- function saveconfig(): void {
- $boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
-
- foreach ($boolean_prefs as $pref) {
- $_POST[$pref] ??= 'false';
- }
-
- $need_reload = false;
-
- foreach (array_keys($_POST) as $pref_name) {
-
- $value = $_POST[$pref_name];
-
- switch ($pref_name) {
- case Prefs::DIGEST_PREFERRED_TIME:
- if (get_pref(Prefs::DIGEST_PREFERRED_TIME) != $value) {
-
- $sth = $this->pdo->prepare("UPDATE ttrss_users SET
- last_digest_sent = NULL WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
-
- }
- break;
- case Prefs::USER_LANGUAGE:
- if (!$need_reload) $need_reload = $_SESSION["language"] != $value;
- break;
-
- case Prefs::USER_CSS_THEME:
- if (!$need_reload) $need_reload = get_pref($pref_name) != $value;
- break;
-
- case Prefs::BLACKLISTED_TAGS:
- $cats = FeedItem_Common::normalize_categories(explode(",", $value));
- asort($cats);
- $value = implode(", ", $cats);
- break;
- }
-
- if (Prefs::is_valid($pref_name)) {
- Prefs::set($pref_name, $value, $_SESSION["uid"], $_SESSION["profile"] ?? null);
- }
- }
-
- if ($need_reload) {
- print "PREFS_NEED_RELOAD";
- } else {
- print __("The configuration was saved.");
- }
- }
-
- function changePersonalData(): void {
-
- $user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
- $new_email = clean($_POST['email']);
-
- if ($user) {
- $user->full_name = clean($_POST['full_name']);
-
- if ($user->email != $new_email) {
- Logger::log(E_USER_NOTICE, "Email address of user {$user->login} has been changed to {$new_email}.");
-
- if ($user->email) {
- $mailer = new Mailer();
-
- $tpl = new Templator();
-
- $tpl->readTemplateFromFile("mail_change_template.txt");
-
- $tpl->setVariable('LOGIN', $user->login);
- $tpl->setVariable('NEWMAIL', $new_email);
- $tpl->setVariable('TTRSS_HOST', Config::get_self_url());
-
- $tpl->addBlock('message');
-
- $tpl->generateOutputToString($message);
-
- $mailer->mail(["to_name" => $user->login,
- "to_address" => $user->email,
- "subject" => "[tt-rss] Email address change notification",
- "message" => $message]);
- }
-
- $user->email = $new_email;
- }
-
- $user->save();
- }
-
- print __("Your personal data has been saved.");
- }
-
- function resetconfig(): void {
- Prefs::reset($_SESSION["uid"], $_SESSION["profile"] ?? null);
-
- print "PREFS_NEED_RELOAD";
- }
-
- private function index_auth_personal(): void {
-
- $user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
-
- ?>
- <form dojoType='dijit.form.Form'>
-
- <?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "changePersonalData") ?>
-
- <script type="dojo/method" event="onSubmit" args="evt">
- evt.preventDefault();
- if (this.validate()) {
- Notify.progress('Saving data...', true);
- xhr.post("backend.php", this.getValues(), (reply) => {
- Notify.info(reply);
- })
- }
- </script>
-
- <fieldset>
- <label><?= __('Full name:') ?></label>
- <input dojoType='dijit.form.ValidationTextBox' name='full_name' required='1' value="<?= htmlspecialchars($user->full_name) ?>">
- </fieldset>
-
- <fieldset>
- <label><?= __('E-mail:') ?></label>
- <input dojoType='dijit.form.ValidationTextBox' name='email' required='1' value="<?= htmlspecialchars($user->email) ?>">
- </fieldset>
-
- <hr/>
-
- <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
- <?= \Controls\icon("save") ?>
- <?= __("Save") ?>
- </button>
- </form>
- <?php
- }
-
- private function index_auth_password(): void {
- if ($_SESSION["auth_module"]) {
- $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
- } else {
- $authenticator = false;
- }
-
- $otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
-
- if ($authenticator && method_exists($authenticator, "change_password")) {
- ?>
-
- <div style='display : none' id='pwd_change_infobox'></div>
-
- <form dojoType='dijit.form.Form'>
-
- <?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "changepassword") ?>
-
- <!-- TODO: return JSON the backend call -->
- <script type="dojo/method" event="onSubmit" args="evt">
- evt.preventDefault();
- if (this.validate()) {
- Notify.progress('Saving data...', true);
- xhr.post("backend.php", this.getValues(), (reply) => {
- Notify.close();
- if (reply.indexOf('ERROR: ') == 0) {
-
- App.byId('pwd_change_infobox').innerHTML =
- reply.replace('ERROR: ', '');
-
- } else {
- App.byId('pwd_change_infobox').innerHTML =
- reply.replace('ERROR: ', '');
-
- const warn = App.byId('default_pass_warning');
- if (warn) Element.hide(warn);
- }
-
- Element.show('pwd_change_infobox');
- })
- }
- </script>
-
- <fieldset>
- <label><?= __("Old password:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='old_password'>
- </fieldset>
-
- <fieldset>
- <label><?= __("New password:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' type='password' regexp='^[^<>]+' required='1' name='new_password'>
- </fieldset>
-
- <fieldset>
- <label><?= __("Confirm password:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' type='password' regexp='^[^<>]+' required='1' name='confirm_password'>
- </fieldset>
-
- <hr/>
-
- <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
- <?= \Controls\icon("security") ?>
- <?= __("Change password") ?>
- </button>
- </form>
-
- <?php
-
- } else {
- print_notice(T_sprintf("Authentication module used for this session (<b>%s</b>) does not provide an ability to set passwords.",
- $_SESSION["auth_module"]));
- }
- }
-
- private function index_auth_app_passwords(): void {
- print_notice("Separate passwords used for API clients. Required if you enable OTP.");
- ?>
-
- <div id='app_passwords_holder'>
- <?php $this->appPasswordList() ?>
- </div>
-
- <hr>
-
- <button style='float : left' class='alt-primary' dojoType='dijit.form.Button' onclick="Helpers.AppPasswords.generate()">
- <?= \Controls\icon("add") ?>
- <?= __('Generate password') ?>
- </button>
-
- <button style='float : left' class='alt-danger' dojoType='dijit.form.Button'
- onclick="Helpers.AppPasswords.removeSelected()">
- <?= \Controls\icon("delete") ?>
- <?= __('Remove selected') ?>
- </button>
-
- <?php
- }
-
- private function index_auth_2fa(): void {
- $otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
-
- if ($_SESSION["auth_module"] == "auth_internal") {
- if ($otp_enabled) {
- print_warning("One time passwords are currently enabled. Enter your current password below to disable.");
- ?>
-
- <form dojoType='dijit.form.Form'>
- <?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "otpdisable") ?>
-
- <!-- TODO: return JSON from the backend call -->
- <script type="dojo/method" event="onSubmit" args="evt">
- evt.preventDefault();
- if (this.validate()) {
- Notify.progress('Saving data...', true);
- xhr.post("backend.php", this.getValues(), (reply) => {
- Notify.close();
-
- if (reply.indexOf('ERROR: ') == 0) {
- Notify.error(reply.replace('ERROR: ', ''));
- } else {
- window.location.reload();
- }
- })
- }
- </script>
-
- <fieldset>
- <label><?= __("Your password:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='password'>
- </fieldset>
-
- <hr/>
-
- <button dojoType='dijit.form.Button' type='submit' class='alt-danger'>
- <?= \Controls\icon("lock_open") ?>
- <?= __("Disable OTP") ?>
- </button>
-
- </form>
-
- <?php
-
- } else {
-
- print "<img src=".($this->_get_otp_qrcode_img()).">";
-
- print_notice("You will need to generate app passwords for API clients if you enable OTP.");
-
- $otp_secret = UserHelper::get_otp_secret($_SESSION["uid"]);
- ?>
-
- <form dojoType='dijit.form.Form'>
-
- <?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "otpenable") ?>
-
- <fieldset>
- <label><?= __("OTP secret:") ?></label>
- <code><?= $this->format_otp_secret($otp_secret) ?></code>
- </fieldset>
-
- <!-- TODO: return JSON from the backend call -->
- <script type="dojo/method" event="onSubmit" args="evt">
- evt.preventDefault();
- if (this.validate()) {
- Notify.progress('Saving data...', true);
- xhr.post("backend.php", this.getValues(), (reply) => {
- Notify.close();
-
- if (reply.indexOf('ERROR:') == 0) {
- Notify.error(reply.replace('ERROR:', ''));
- } else {
- window.location.reload();
- }
- })
- }
- </script>
-
- <fieldset>
- <label><?= __("Your password:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='password'>
- </fieldset>
-
- <fieldset>
- <label><?= __("Verification code:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' autocomplete='off' required='1' name='otp'>
- </fieldset>
-
- <hr/>
-
- <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
- <?= \Controls\icon("lock") ?>
- <?= __("Enable OTP") ?>
- </button>
-
- </form>
- <?php
- }
- } else {
- print_notice("OTP is only available when using <b>auth_internal</b> authentication module.");
- }
- }
-
- function index_auth(): void {
- ?>
- <div dojoType='dijit.layout.TabContainer'>
- <div dojoType='dijit.layout.ContentPane' title="<?= __('Personal data') ?>">
- <?php $this->index_auth_personal() ?>
- </div>
- <div dojoType='dijit.layout.ContentPane' title="<?= __('Password') ?>">
- <?php $this->index_auth_password() ?>
- </div>
- <div dojoType='dijit.layout.ContentPane' title="<?= __('App passwords') ?>">
- <?php if (PluginHost::getInstance()->get_plugin('auth_internal')) { ?>
- <?php $this->index_auth_app_passwords() ?>
- <?php } else { ?>
- <?= format_warning("App passwords are only available if <b>auth_internal<b> plugin is enabled."); ?>
- <?php } ?>
- </div>
- <div dojoType='dijit.layout.ContentPane' title="<?= __('Authenticator (OTP)') ?>">
- <?php $this->index_auth_2fa() ?>
- </div>
- </div>
- <?php
- }
-
- private function index_prefs_list(): void {
- $profile = $_SESSION["profile"] ?? null;
-
- if ($profile) {
- print_notice(__("Some preferences are only available in default profile."));
- }
-
- /** @var array<string, array{'type_hint': Config::T_*, 'value': bool|int|string, 'help_text': string, 'short_desc': string}> */
- $prefs_available = [];
-
- /** @var array<int, string> */
- $listed_boolean_prefs = [];
-
- foreach (Prefs::get_all($_SESSION["uid"], $profile) as $pref) {
-
- if (in_array($pref["pref_name"], $this->pref_blacklist)) {
- continue;
- }
-
- if ($profile && in_array($pref["pref_name"], Prefs::_PROFILE_BLACKLIST)) {
- continue;
- }
-
- $pref_name = $pref["pref_name"];
- $short_desc = $this->_get_short_desc($pref_name);
-
- if (!$short_desc)
- continue;
-
- $prefs_available[$pref_name] = [
- 'type_hint' => $pref['type_hint'],
- 'value' => $pref['value'],
- 'help_text' => $this->_get_help_text($pref_name),
- 'short_desc' => $short_desc
- ];
- }
-
- foreach (array_keys($this->pref_item_map) as $section) {
-
- print "<h2>$section</h2>";
-
- foreach ($this->pref_item_map[$section] as $pref_name) {
-
- if ($pref_name == self::BLOCK_SEPARATOR && !$profile) {
- print "<hr/>";
- continue;
- }
-
- if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE && Config::get(Config::DB_TYPE) != "pgsql") {
- continue;
- }
-
- if (isset($prefs_available[$pref_name])) {
-
- $item = $prefs_available[$pref_name];
-
- print "<fieldset class='prefs'>";
-
- print "<label for='CB_$pref_name'>";
- print $item['short_desc'] . ":";
- print "</label>";
-
- $value = $item['value'];
- $type_hint = $item['type_hint'];
-
- if ($pref_name == Prefs::USER_LANGUAGE) {
- print \Controls\select_hash($pref_name, $value, get_translations(),
- ["style" => 'width : 220px; margin : 0px']);
-
- } else if ($pref_name == Prefs::USER_TIMEZONE) {
-
- $timezones = explode("\n", file_get_contents("lib/timezones.txt"));
-
- print \Controls\select_tag($pref_name, $value, $timezones, ["dojoType" => "dijit.form.FilteringSelect"]);
-
- } else if ($pref_name == Prefs::BLACKLISTED_TAGS) { # TODO: other possible <textarea> prefs go here
-
- print "<div>";
-
- print "<textarea dojoType='dijit.form.SimpleTextarea' rows='4'
- style='width: 500px; font-size : 12px;'
- name='$pref_name'>$value</textarea><br/>";
-
- print "<div class='help-text-bottom text-muted'>" . $this->pref_help_bottom[$pref_name] . "</div>";
-
- print "</div>";
-
- } else if ($pref_name == Prefs::USER_CSS_THEME) {
-
- $theme_files = array_map("basename", [
- ...glob("themes/*.php") ?: [],
- ...glob("themes/*.css") ?: [],
- ...glob("themes.local/*.css") ?: [],
- ]);
-
- asort($theme_files);
-
- $themes = [ "" => __("default") ];
-
- foreach ($theme_files as $file) {
- $themes[$file] = basename($file, ".css");
- }
- ?>
-
- <?= \Controls\select_hash($pref_name, $value, $themes) ?>
- <?= \Controls\button_tag(\Controls\icon("palette") . " " . __("Customize"), "",
- ["onclick" => "Helpers.Prefs.customizeCSS()"]) ?>
- <?= \Controls\button_tag(\Controls\icon("open_in_new") . " " . __("More themes..."), "",
- ["class" => "alt-info", "onclick" => "window.open(\"https://tt-rss.org/wiki/Themes\")"]) ?>
-
- <?php
-
- } else if ($pref_name == Prefs::DEFAULT_UPDATE_INTERVAL) {
-
- global $update_intervals_nodefault;
-
- print \Controls\select_hash($pref_name, $value, $update_intervals_nodefault);
-
- } else if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE) {
-
- print \Controls\select_tag($pref_name, $value, Pref_Feeds::get_ts_languages());
-
- } else if ($type_hint == Config::T_BOOL) {
-
- array_push($listed_boolean_prefs, $pref_name);
-
- if ($pref_name == Prefs::PURGE_UNREAD_ARTICLES && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
- $is_disabled = true;
- $is_checked = true;
- } else {
- $is_disabled = false;
- $is_checked = ($value == "true");
- }
-
- print \Controls\checkbox_tag($pref_name, $is_checked, "true",
- ["disabled" => $is_disabled], "CB_$pref_name");
-
- if ($pref_name == Prefs::DIGEST_ENABLE) {
- print \Controls\button_tag(\Controls\icon("info") . " " . __('Preview'), '',
- ['onclick' => 'Helpers.Digest.preview()', 'style' => 'margin-left : 10px']);
- }
-
- } else if (in_array($pref_name, [Prefs::FRESH_ARTICLE_MAX_AGE,
- Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT])) {
-
- if ($pref_name == Prefs::PURGE_OLD_DAYS && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
- $attributes = ["disabled" => true, "required" => true];
- $value = Config::get(Config::FORCE_ARTICLE_PURGE);
- } else {
- $attributes = ["required" => true];
- }
-
- if ($type_hint == Config::T_INT)
- print \Controls\number_spinner_tag($pref_name, $value, $attributes);
- else
- print \Controls\input_tag($pref_name, $value, "text", $attributes);
-
- } else if ($pref_name == Prefs::SSL_CERT_SERIAL) {
-
- print \Controls\input_tag($pref_name, $value, "text", ["readonly" => true], "SSL_CERT_SERIAL");
-
- $cert_serial = htmlspecialchars(self::_get_ssl_certificate_id());
- $has_serial = ($cert_serial) ? true : false;
-
- print \Controls\button_tag(\Controls\icon("security") . " " . __('Register'), "", [
- "disabled" => !$has_serial,
- "onclick" => "dijit.byId('SSL_CERT_SERIAL').attr('value', '$cert_serial')"]);
-
- print \Controls\button_tag(\Controls\icon("clear") . " " . __('Clear'), "", [
- "class" => "alt-danger",
- "onclick" => "dijit.byId('SSL_CERT_SERIAL').attr('value', '')"]);
-
- print \Controls\button_tag(\Controls\icon("help") . " " . __("More info..."), "", [
- "class" => "alt-info",
- "onclick" => "window.open('https://tt-rss.org/wiki/SSL%20Certificate%20Authentication')"]);
-
- } else if ($pref_name == Prefs::DIGEST_PREFERRED_TIME) {
- print "<input dojoType=\"dijit.form.ValidationTextBox\"
- id=\"$pref_name\" regexp=\"[012]?\d:\d\d\" placeHolder=\"12:00\"
- name=\"$pref_name\" value=\"$value\">";
- $item['help_text'] .= ". " . T_sprintf("Current server time: %s", date("H:i"));
- } else {
- $regexp = ($type_hint == Config::T_INT) ? 'regexp="^\d*$"' : '';
-
- print "<input dojoType=\"dijit.form.ValidationTextBox\" $regexp name=\"$pref_name\" value=\"$value\">";
- }
-
- if ($item['help_text'])
- print "<div class='help-text text-muted'><label for='CB_$pref_name'>".$item['help_text']."</label></div>";
-
- print "</fieldset>";
- }
- }
- }
- print \Controls\hidden_tag("boolean_prefs", htmlspecialchars(join(",", $listed_boolean_prefs)));
- }
-
- private function index_prefs(): void {
- ?>
- <form dojoType='dijit.form.Form' id='changeSettingsForm'>
- <?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "saveconfig") ?>
-
- <script type="dojo/method" event="onSubmit" args="evt, quit">
- if (evt) evt.preventDefault();
- if (this.validate()) {
- xhr.post("backend.php", this.getValues(), (reply) => {
- if (quit) {
- document.location.href = 'index.php';
- } else {
- if (reply == 'PREFS_NEED_RELOAD') {
- window.location.reload();
- } else {
- Notify.info(reply);
- }
- }
- })
- }
- </script>
-
- <div dojoType="dijit.layout.BorderContainer" gutters="false">
- <div dojoType="dijit.layout.ContentPane" region="center" style="overflow-y : auto">
- <?php $this->index_prefs_list() ?>
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefPrefsPrefsInside") ?>
- </div>
- <div dojoType="dijit.layout.ContentPane" region="bottom">
-
- <div dojoType="fox.form.ComboButton" type="submit" class="alt-primary">
- <span> <?= __('Save configuration') ?></span>
- <div dojoType="dijit.DropDownMenu">
- <div dojoType="dijit.MenuItem" onclick="dijit.byId('changeSettingsForm').onSubmit(null, true)">
- <?= __("Save and exit") ?>
- </div>
- </div>
- </div>
-
- <button dojoType="dijit.form.Button" onclick="return Helpers.Profiles.edit()">
- <?= \Controls\icon("settings") ?>
- <?= __('Manage profiles') ?>
- </button>
-
- <button dojoType="dijit.form.Button" class="alt-danger" onclick="return Helpers.Prefs.confirmReset()">
- <?= \Controls\icon("clear") ?>
- <?= __('Reset to defaults') ?>
- </button>
-
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefPrefsPrefsOutside") ?>
- </div>
- </div>
- </form>
- <?php
- }
-
- function getPluginsList(): void {
- $system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
- $user_enabled = array_map("trim", explode(",", get_pref(Prefs::_ENABLED_PLUGINS)));
-
- $tmppluginhost = new PluginHost();
- $tmppluginhost->load_all($tmppluginhost::KIND_ALL, $_SESSION["uid"], true);
-
- $rv = [];
-
- foreach ($tmppluginhost->get_plugins() as $name => $plugin) {
- $about = $plugin->about();
- $is_local = $tmppluginhost->is_local($plugin);
- $version = htmlspecialchars($this->_get_plugin_version($plugin));
-
- array_push($rv, [
- "name" => $name,
- "is_local" => $is_local,
- "system_enabled" => in_array($name, $system_enabled),
- "user_enabled" => in_array($name, $user_enabled),
- "has_data" => count($tmppluginhost->get_all($plugin)) > 0,
- "is_system" => (bool)($about[3] ?? false),
- "version" => $version,
- "author" => $about[2] ?? "",
- "description" => $about[1] ?? "",
- "more_info" => $about[4] ?? "",
- ]);
- }
-
- usort($rv, function($a, $b) { return strcmp($a["name"], $b["name"]); });
-
- print json_encode(['plugins' => $rv, 'is_admin' => $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN]);
- }
-
- function index_plugins(): void {
- ?>
- <form dojoType="dijit.form.Form" id="changePluginsForm">
-
- <?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "setplugins") ?>
-
- <div dojoType="dijit.layout.BorderContainer" gutters="false">
- <div region="top" dojoType='fox.Toolbar'>
- <div class='pull-right'>
- <input name="search" type="search" onkeyup='Helpers.Plugins.search()' dojoType="dijit.form.TextBox">
- <button dojoType='dijit.form.Button' onclick='Helpers.Plugins.search()'>
- <?= __('Search') ?>
- </button>
- </div>
-
- <div dojoType='fox.form.DropDownButton'>
- <span><?= __('Select') ?></span>
- <div dojoType='dijit.Menu' style='display: none'>
- <div onclick="Lists.select('prefs-plugin-list', true)"
- dojoType='dijit.MenuItem'><?= __('All') ?></div>
- <div onclick="Lists.select('prefs-plugin-list', false)"
- dojoType='dijit.MenuItem'><?= __('None') ?></div>
- </div>
- </div>
- </div>
-
- <div dojoType="dijit.layout.ContentPane" region="center" style="overflow-y : auto">
-
- <script type="dojo/method" event="onShow">
- Helpers.Plugins.reload();
- </script>
-
- <!-- <?php
- if (!empty($_SESSION["safe_mode"])) {
- print_error("You have logged in using safe mode, no user plugins will be actually enabled until you login again.");
- }
-
- $feed_handler_whitelist = [ "Af_Comics" ];
-
- $feed_handlers = [
- ...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_FETCHED),
- ...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_PARSED),
- ...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FETCH_FEED),
- ];
-
- $feed_handlers = array_filter($feed_handlers,
- fn($plugin) => in_array(get_class($plugin), $feed_handler_whitelist) === false);
-
- if (count($feed_handlers) > 0) {
- print_error(
- T_sprintf("The following plugins use per-feed content hooks. This may cause excessive data usage and origin server load resulting in a ban of your instance: <b>%s</b>" ,
- implode(", ", array_map(fn($plugin) => get_class($plugin), $feed_handlers))
- ) . " (<a href='https://tt-rss.org/wiki/FeedHandlerPlugins' target='_blank'>".__("More info...")."</a>)"
- );
- }
- ?> -->
-
- <ul id="prefs-plugin-list" class="prefs-plugin-list list-unstyled">
- <li class='text-center'><?= __("Loading, please wait...") ?></li>
- </ul>
-
- </div>
- <div dojoType="dijit.layout.ContentPane" region="bottom">
-
- <button dojoType='dijit.form.Button' class="alt-info pull-right" onclick='window.open("https://tt-rss.org/wiki/Plugins")'>
- <i class='material-icons'>help</i>
- <?= __("More info") ?>
- </button>
-
- <?= \Controls\button_tag(\Controls\icon("check") . " " .__("Enable selected"), "", ["class" => "alt-primary",
- "onclick" => "Helpers.Plugins.enableSelected()"]) ?>
-
- <?= \Controls\button_tag(\Controls\icon("refresh"), "", ["title" => __("Reload"), "onclick" => "Helpers.Plugins.reload()"]) ?>
-
- <?php if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) { ?>
- <?php if (Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) { ?>
-
- <button class='alt-warning' dojoType='dijit.form.Button' onclick="Helpers.Plugins.update()">
- <?= \Controls\icon("update") ?>
- <?= __("Check for updates") ?>
- </button>
- <?php } ?>
-
- <?php if (Config::get(Config::ENABLE_PLUGIN_INSTALLER)) { ?>
- <button dojoType='dijit.form.Button' onclick="Helpers.Plugins.install()">
- <?= \Controls\icon("add") ?>
- <?= __("Install plugin") ?>
- </button>
- <?php } ?>
- <?php } ?>
- </div>
- </div>
- </form>
- <?php
- }
-
- function index(): void {
- ?>
- <div dojoType='dijit.layout.AccordionContainer' region='center'>
- <div dojoType='dijit.layout.AccordionPane' title="<i class='material-icons'>person</i> <?= __('Personal data / Authentication')?>">
- <script type='dojo/method' event='onSelected' args='evt'>
- if (this.domNode.querySelector('.loading'))
- window.setTimeout(() => {
- xhr.post("backend.php", {op: 'pref-prefs', method: 'index_auth'}, (reply) => {
- this.attr('content', reply);
- });
- }, 100);
- </script>
- <span class='loading'><?= __("Loading, please wait...") ?></span>
- </div>
- <div dojoType='dijit.layout.AccordionPane' selected='true' title="<i class='material-icons'>settings</i> <?= __('Preferences') ?>">
- <?php $this->index_prefs() ?>
- </div>
- <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title="<i class='material-icons'>extension</i> <?= __('Plugins') ?>">
- <?php $this->index_plugins() ?>
- </div>
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefPrefs") ?>
- </div>
- <?php
- }
-
- function _get_otp_qrcode_img(): ?string {
- $secret = UserHelper::get_otp_secret($_SESSION["uid"]);
- $login = UserHelper::get_login_by_id($_SESSION["uid"]);
-
- if ($secret && $login) {
- $qrcode = new \chillerlan\QRCode\QRCode();
-
- $otpurl = "otpauth://totp/".urlencode($login)."?secret=$secret&issuer=".urlencode("Tiny Tiny RSS");
-
- return $qrcode->render($otpurl);
- }
-
- return null;
- }
-
- function otpenable(): void {
- $password = clean($_REQUEST["password"]);
- $otp_check = clean($_REQUEST["otp"]);
-
- $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
-
- /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
- if ($authenticator->check_password($_SESSION["uid"], $password)) {
- if (UserHelper::enable_otp($_SESSION["uid"], $otp_check)) {
- print "OK";
- } else {
- print "ERROR:".__("Incorrect one time password");
- }
- } else {
- print "ERROR:".__("Incorrect password");
- }
- }
-
- function otpdisable(): void {
- $password = clean($_REQUEST["password"]);
-
- /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
- $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
-
- if ($authenticator->check_password($_SESSION["uid"], $password)) {
-
- $sth = $this->pdo->prepare("SELECT email, login FROM ttrss_users WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
-
- if ($row = $sth->fetch()) {
- $mailer = new Mailer();
-
- $tpl = new Templator();
-
- $tpl->readTemplateFromFile("otp_disabled_template.txt");
-
- $tpl->setVariable('LOGIN', $row["login"]);
- $tpl->setVariable('TTRSS_HOST', Config::get_self_url());
-
- $tpl->addBlock('message');
-
- $tpl->generateOutputToString($message);
-
- $mailer->mail(["to_name" => $row["login"],
- "to_address" => $row["email"],
- "subject" => "[tt-rss] OTP change notification",
- "message" => $message]);
- }
-
- UserHelper::disable_otp($_SESSION["uid"]);
-
- print "OK";
- } else {
- print "ERROR: ".__("Incorrect password");
- }
-
- }
-
- function setplugins(): void {
- $plugins = array_filter($_REQUEST["plugins"] ?? [], 'clean');
-
- set_pref(Prefs::_ENABLED_PLUGINS, implode(",", $plugins));
- }
-
- function _get_plugin_version(Plugin $plugin): string {
- $about = $plugin->about();
-
- if (!empty($about[0])) {
- return T_sprintf("v%.2f, by %s", $about[0], $about[2]);
- }
-
- $ref = new ReflectionClass(get_class($plugin));
-
- $plugin_dir = dirname($ref->getFileName());
-
- if (basename($plugin_dir) == "plugins") {
- return "";
- }
-
- if (is_dir("$plugin_dir/.git")) {
- $ver = Config::get_version_from_git($plugin_dir);
-
- return $ver["status"] == 0 ? T_sprintf("v%s, by %s", $ver["version"], $about[2]) : $ver["version"];
- }
-
- return "";
- }
-
- /**
- * @return array<int, array{'plugin': string, 'rv': array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null}>
- */
- static function _get_updated_plugins(): array {
- $root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
- $plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
- $rv = [];
-
- foreach ($plugin_dirs as $dir) {
- if (is_dir("$dir/.git")) {
- $plugin_name = basename($dir);
-
- array_push($rv, ["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)]);
- }
- }
-
- $rv = array_values(array_filter($rv, fn($item) => $item["rv"]["need_update"]));
-
- return $rv;
- }
-
- /**
- * @return array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null
- */
- private static function _plugin_needs_update(string $root_dir, string $plugin_name): ?array {
- $plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
- $rv = null;
-
- if (is_dir($plugin_dir) && is_dir("$plugin_dir/.git")) {
- $pipes = [];
-
- $descriptorspec = [
- //0 => ["pipe", "r"], // STDIN
- 1 => ["pipe", "w"], // STDOUT
- 2 => ["pipe", "w"], // STDERR
- ];
-
- $proc = proc_open("git fetch -q origin -a && git log HEAD..origin/master --oneline", $descriptorspec, $pipes, $plugin_dir);
-
- if (is_resource($proc)) {
- $rv = [
- "stdout" => stream_get_contents($pipes[1]),
- "stderr" => stream_get_contents($pipes[2]),
- "git_status" => proc_close($proc),
- ];
- $rv["need_update"] = !empty($rv["stdout"]);
- }
- }
-
- return $rv;
- }
-
-
- /**
- * @return array{'stdout': false|string, 'stderr': false|string, 'git_status': int}
- */
- private function _update_plugin(string $root_dir, string $plugin_name): array {
- $plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
- $rv = [];
-
- if (is_dir($plugin_dir) && is_dir("$plugin_dir/.git")) {
- $pipes = [];
-
- $descriptorspec = [
- //0 => ["pipe", "r"], // STDIN
- 1 => ["pipe", "w"], // STDOUT
- 2 => ["pipe", "w"], // STDERR
- ];
-
- $proc = proc_open("git fetch origin -a && git log HEAD..origin/master --oneline && git pull --ff-only origin master", $descriptorspec, $pipes, $plugin_dir);
-
- if (is_resource($proc)) {
- $rv["stdout"] = stream_get_contents($pipes[1]);
- $rv["stderr"] = stream_get_contents($pipes[2]);
- $rv["git_status"] = proc_close($proc);
- }
- }
-
- return $rv;
- }
-
- // https://gist.github.com/mindplay-dk/a4aad91f5a4f1283a5e2#gistcomment-2036828
- private function _recursive_rmdir(string $dir, bool $keep_root = false): bool {
- // Handle bad arguments.
- if (empty($dir) || !file_exists($dir)) {
- return true; // No such file/dir$dir exists.
- } elseif (is_file($dir) || is_link($dir)) {
- return unlink($dir); // Delete file/link.
- }
-
- // Delete all children.
- $files = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
- \RecursiveIteratorIterator::CHILD_FIRST
- );
-
- foreach ($files as $fileinfo) {
- $action = $fileinfo->isDir() ? 'rmdir' : 'unlink';
- if (!$action($fileinfo->getRealPath())) {
- return false; // Abort due to the failure.
- }
- }
-
- return $keep_root ? true : rmdir($dir);
- }
-
- // https://stackoverflow.com/questions/7153000/get-class-name-from-file
- private function _get_class_name_from_file(string $file): string {
- $tokens = token_get_all(file_get_contents($file));
-
- for ($i = 0; $i < count($tokens); $i++) {
- if (isset($tokens[$i][0]) && $tokens[$i][0] == T_CLASS) {
- for ($j = $i+1; $j < count($tokens); $j++) {
- if (isset($tokens[$j][1]) && $tokens[$j][1] != " ") {
- return $tokens[$j][1];
- }
- }
- }
- }
-
- return "";
- }
-
- function uninstallPlugin(): void {
- if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
- $plugin_name = basename(clean($_REQUEST['plugin']));
- $status = 0;
-
- $plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local/$plugin_name";
-
- if (is_dir($plugin_dir)) {
- $status = $this->_recursive_rmdir($plugin_dir);
- }
-
- print json_encode(['status' => $status]);
- }
- }
-
- function installPlugin(): void {
- if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
- $plugin_name = basename(clean($_REQUEST['plugin']));
- $all_plugins = $this->_get_available_plugins();
- $plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local";
-
- $work_dir = "$plugin_dir/plugin-installer";
-
- $rv = [ ];
-
- if (is_dir($work_dir) || mkdir($work_dir)) {
- foreach ($all_plugins as $plugin) {
- if ($plugin['name'] == $plugin_name) {
-
- $tmp_dir = tempnam($work_dir, $plugin_name);
-
- if (file_exists($tmp_dir)) {
- unlink($tmp_dir);
-
- $pipes = [];
-
- $descriptorspec = [
- 1 => ["pipe", "w"], // STDOUT
- 2 => ["pipe", "w"], // STDERR
- ];
-
- $proc = proc_open("git clone " . escapeshellarg($plugin['clone_url']) . " " . $tmp_dir,
- $descriptorspec, $pipes, sys_get_temp_dir());
-
- $status = 0;
-
- if (is_resource($proc)) {
- $rv["stdout"] = stream_get_contents($pipes[1]);
- $rv["stderr"] = stream_get_contents($pipes[2]);
- $status = proc_close($proc);
- $rv["git_status"] = $status;
-
- // yeah I know about mysterious RC = -1
- if (file_exists("$tmp_dir/init.php")) {
- $class_name = strtolower(basename($this->_get_class_name_from_file("$tmp_dir/init.php")));
-
- if ($class_name) {
- $dst_dir = "$plugin_dir/$class_name";
-
- if (is_dir($dst_dir)) {
- $rv['result'] = self::PI_RES_ALREADY_INSTALLED;
- } else {
- if (rename($tmp_dir, "$plugin_dir/$class_name")) {
- $rv['result'] = self::PI_RES_SUCCESS;
- }
- }
- } else {
- $rv['result'] = self::PI_ERR_NO_CLASS;
- }
- } else {
- $rv['result'] = self::PI_ERR_NO_INIT_PHP;
- }
-
- } else {
- $rv['result'] = self::PI_ERR_EXEC_FAILED;
- }
- } else {
- $rv['result'] = self::PI_ERR_NO_TEMPDIR;
- }
-
- // cleanup after failure
- if ($tmp_dir && is_dir($tmp_dir)) {
- $this->_recursive_rmdir($tmp_dir);
- }
-
- break;
- }
- }
-
- if (empty($rv['result']))
- $rv['result'] = self::PI_ERR_PLUGIN_NOT_FOUND;
-
- } else {
- $rv["result"] = self::PI_ERR_NO_WORKDIR;
- }
-
- print json_encode($rv);
- }
- }
-
- /**
- * @return array<int, array{'name': string, 'description': string, 'topics': array<int, string>, 'html_url': string, 'clone_url': string, 'last_update': string}>
- */
- private function _get_available_plugins(): array {
- if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
- $content = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
-
- if ($content) {
- return $content;
- }
- }
-
- return [];
- }
-
- function getAvailablePlugins(): void {
- if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
- print json_encode($this->_get_available_plugins());
- } else {
- print "[]";
- }
- }
-
- function checkForPluginUpdates(): void {
- if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
- $plugin_name = $_REQUEST["name"] ?? "";
- $root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
-
- $rv = empty($plugin_name) ? self::_get_updated_plugins() : [
- ["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)],
- ];
-
- print json_encode($rv);
- }
- }
-
- function updateLocalPlugins(): void {
- if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
- $plugins = explode(",", $_REQUEST["plugins"] ?? "");
-
- if ($plugins !== false) {
- $plugins = array_filter($plugins, 'strlen');
- }
-
- # we're in classes/pref/
- $root_dir = dirname(dirname(__DIR__));
-
- $rv = [];
-
- if ($plugins) {
- foreach ($plugins as $plugin_name) {
- array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
- }
- } else {
- $plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
-
- foreach ($plugin_dirs as $dir) {
- if (is_dir("$dir/.git")) {
- $plugin_name = basename($dir);
-
- $test = self::_plugin_needs_update($root_dir, $plugin_name);
-
- if (!empty($test["stdout"]))
- array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
- }
- }
- }
-
- print json_encode($rv);
- }
- }
-
- function clearplugindata(): void {
- $name = clean($_REQUEST["name"]);
-
- PluginHost::getInstance()->clear_data(PluginHost::getInstance()->get_plugin($name));
- }
-
- function customizeCSS(): void {
- $value = get_pref(Prefs::USER_STYLESHEET);
- $value = str_replace("<br/>", "\n", $value);
-
- print json_encode(["value" => $value]);
- }
-
- function activateprofile(): void {
- $id = (int) ($_REQUEST['id'] ?? 0);
-
- $profile = ORM::for_table('ttrss_settings_profiles')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($id);
-
- if ($profile) {
- $_SESSION["profile"] = $id;
- } else {
- $_SESSION["profile"] = null;
- }
- }
-
- function cloneprofile(): void {
- $old_profile = $_REQUEST["old_profile"] ?? 0;
- $new_title = clean($_REQUEST["new_title"]);
-
- if ($old_profile && $new_title) {
- $new_profile = ORM::for_table('ttrss_settings_profiles')->create();
- $new_profile->title = $new_title;
- $new_profile->owner_uid = $_SESSION['uid'];
-
- if ($new_profile->save()) {
- $sth = $this->pdo->prepare("INSERT INTO ttrss_user_prefs
- (owner_uid, pref_name, profile, value)
- SELECT
- :uid,
- pref_name,
- :new_profile,
- value
- FROM ttrss_user_prefs
- WHERE owner_uid = :uid AND profile = :old_profile");
-
- $sth->execute([
- "uid" => $_SESSION["uid"],
- "new_profile" => $new_profile->id,
- "old_profile" => $old_profile,
- ]);
- }
- }
- }
-
- function remprofiles(): void {
- $ids = $_REQUEST["ids"] ?? [];
-
- ORM::for_table('ttrss_settings_profiles')
- ->where('owner_uid', $_SESSION['uid'])
- ->where_in('id', $ids)
- ->where_not_equal('id', $_SESSION['profile'] ?? 0)
- ->delete_many();
- }
-
- function addprofile(): void {
- $title = clean($_REQUEST["title"]);
-
- if ($title) {
- $profile = ORM::for_table('ttrss_settings_profiles')
- ->where('owner_uid', $_SESSION['uid'])
- ->where('title', $title)
- ->find_one();
-
- if (!$profile) {
- $profile = ORM::for_table('ttrss_settings_profiles')->create();
-
- $profile->title = $title;
- $profile->owner_uid = $_SESSION['uid'];
- $profile->save();
- }
- }
- }
-
- function saveprofile(): void {
- $id = (int)$_REQUEST["id"];
- $title = clean($_REQUEST["value"]);
-
- if ($title && $id) {
- $profile = ORM::for_table('ttrss_settings_profiles')
- ->where('owner_uid', $_SESSION['uid'])
- ->find_one($id);
-
- if ($profile) {
- $profile->title = $title;
- $profile->save();
- }
- }
- }
-
- // TODO: this maybe needs to be unified with Public::getProfiles()
- function getProfiles(): void {
- $rv = [];
-
- $profiles = ORM::for_table('ttrss_settings_profiles')
- ->where('owner_uid', $_SESSION['uid'])
- ->order_by_expr('title')
- ->find_many();
-
- array_push($rv, ["title" => __("Default profile"),
- "id" => 0,
- "initialized" => true,
- "active" => empty($_SESSION["profile"])
- ]);
-
- foreach ($profiles as $profile) {
- $profile['active'] = ($_SESSION["profile"] ?? 0) == $profile->id;
-
- $num_settings = ORM::for_table('ttrss_user_prefs')
- ->where('profile', $profile->id)
- ->count();
-
- $profile['initialized'] = $num_settings > 0;
-
- array_push($rv, $profile->as_array());
- };
-
- print json_encode($rv);
- }
-
- private function _get_short_desc(string $pref_name): string {
- if (isset($this->pref_help[$pref_name][0])) {
- return $this->pref_help[$pref_name][0];
- }
- return "";
- }
-
- private function _get_help_text(string $pref_name): string {
- if (isset($this->pref_help[$pref_name][1])) {
- return $this->pref_help[$pref_name][1];
- }
- return "";
- }
-
- private function appPasswordList(): void {
- ?>
- <div dojoType='fox.Toolbar'>
- <div dojoType='fox.form.DropDownButton'>
- <span><?= __('Select') ?></span>
- <div dojoType='dijit.Menu' style='display: none'>
- <div onclick="Tables.select('app-password-list', true)"
- dojoType="dijit.MenuItem"><?= __('All') ?></div>
- <div onclick="Tables.select('app-password-list', false)"
- dojoType="dijit.MenuItem"><?= __('None') ?></div>
- </div>
- </div>
- </div>
-
- <div class='panel panel-scrollable'>
- <table width='100%' id='app-password-list'>
- <tr>
- <th class="checkbox"> </th>
- <th width='50%'><?= __("Description") ?></th>
- <th><?= __("Created") ?></th>
- <th><?= __("Last used") ?></th>
- </tr>
- <?php
-
- $passwords = ORM::for_table('ttrss_app_passwords')
- ->where('owner_uid', $_SESSION['uid'])
- ->order_by_asc('title')
- ->find_many();
-
- foreach ($passwords as $pass) { ?>
- <tr data-row-id='<?= $pass['id'] ?>'>
- <td class="checkbox">
- <input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'>
- </td>
- <td>
- <?= htmlspecialchars($pass["title"]) ?>
- </td>
- <td class='text-muted'>
- <?= TimeHelper::make_local_datetime($pass['created'], false) ?>
- </td>
- <td class='text-muted'>
- <?= TimeHelper::make_local_datetime($pass['last_used'], false) ?>
- </td>
- </tr>
- <?php } ?>
- </table>
- </div>
- <?php
- }
-
- function deleteAppPasswords(): void {
- $passwords = ORM::for_table('ttrss_app_passwords')
- ->where('owner_uid', $_SESSION['uid'])
- ->where_in('id', $_REQUEST['ids'] ?? [])
- ->delete_many();
-
- $this->appPasswordList();
- }
-
- function generateAppPassword(): void {
- $title = clean($_REQUEST['title']);
- $new_password = make_password(16);
- $new_salt = UserHelper::get_salt();
- $new_password_hash = UserHelper::hash_password($new_password, $new_salt, UserHelper::HASH_ALGOS[0]);
-
- print_warning(T_sprintf("Generated password <strong>%s</strong> for %s. Please remember it for future reference.", $new_password, $title));
-
- $password = ORM::for_table('ttrss_app_passwords')->create();
-
- $password->title = $title;
- $password->owner_uid = $_SESSION['uid'];
- $password->pwd_hash = "$new_password_hash:$new_salt";
- $password->service = Auth_Base::AUTH_SERVICE_API;
- $password->created = Db::NOW();
-
- $password->save();
-
- $this->appPasswordList();
- }
-
- function previewDigest(): void {
- print json_encode(Digest::prepare_headlines_digest($_SESSION["uid"], 1, 16));
- }
-
- static function _get_ssl_certificate_id(): string {
- if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] ?? false) {
- return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
- $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
- $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
- $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
- }
- if ($_SERVER["SSL_CLIENT_M_SERIAL"] ?? false) {
- return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
- $_SERVER["SSL_CLIENT_V_START"] .
- $_SERVER["SSL_CLIENT_V_END"] .
- $_SERVER["SSL_CLIENT_S_DN"]);
- }
- return "";
- }
-
- private function format_otp_secret(string $secret): string {
- return implode(" ", str_split($secret, 4));
- }
-}
diff --git a/classes/pref/system.php b/classes/pref/system.php
deleted file mode 100644
index 806291c72..000000000
--- a/classes/pref/system.php
+++ /dev/null
@@ -1,225 +0,0 @@
-<?php
-
-class Pref_System extends Handler_Administrative {
-
- private const LOG_PAGE_LIMIT = 15;
-
- function csrf_ignore(string $method): bool {
- $csrf_ignored = array("index");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
- function clearLog(): void {
- $this->pdo->query("DELETE FROM ttrss_error_log");
- }
-
- function sendTestEmail(): void {
- $mail_address = clean($_REQUEST["mail_address"]);
-
- $mailer = new Mailer();
-
- $rc = $mailer->mail(["to_name" => "",
- "to_address" => $mail_address,
- "subject" => __("Test message from tt-rss"),
- "message" => ("This message confirms that tt-rss can send outgoing mail.")
- ]);
-
- print json_encode(['rc' => $rc, 'error' => $mailer->error()]);
- }
-
- function getphpinfo(): void {
- ob_start();
- phpinfo();
- $info = ob_get_contents();
- ob_end_clean();
-
- print preg_replace( '%^.*<body>(.*)</body>.*$%ms','$1', (string)$info);
- }
-
- private function _log_viewer(int $page, int $severity): void {
- $errno_values = [];
-
- switch ($severity) {
- case E_USER_ERROR:
- $errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR ];
- break;
- case E_USER_WARNING:
- $errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED ];
- break;
- }
-
- if (count($errno_values) > 0) {
- $errno_qmarks = arr_qmarks($errno_values);
- $errno_filter_qpart = "errno IN ($errno_qmarks)";
- } else {
- $errno_filter_qpart = "true";
- }
-
- $offset = self::LOG_PAGE_LIMIT * $page;
-
- $sth = $this->pdo->prepare("SELECT
- COUNT(id) AS total_pages
- FROM
- ttrss_error_log
- WHERE
- $errno_filter_qpart");
-
- $sth->execute($errno_values);
-
- if ($res = $sth->fetch()) {
- $total_pages = (int)($res["total_pages"] / self::LOG_PAGE_LIMIT);
- } else {
- $total_pages = 0;
- }
-
- ?>
- <div dojoType='dijit.layout.BorderContainer' gutters='false'>
- <div region='top' dojoType='fox.Toolbar'>
-
- <button dojoType='dijit.form.Button' onclick='Helpers.EventLog.refresh()'>
- <?= __('Refresh') ?>
- </button>
-
- <button dojoType='dijit.form.Button' <?= ($page <= 0 ? "disabled" : "") ?>
- onclick='Helpers.EventLog.prevPage()'>
- <?= __('&lt;&lt;') ?>
- </button>
-
- <button dojoType='dijit.form.Button' disabled>
- <?= T_sprintf('Page %d of %d', $page+1, $total_pages+1) ?>
- </button>
-
- <button dojoType='dijit.form.Button' <?= ($page >= $total_pages ? "disabled" : "") ?>
- onclick='Helpers.EventLog.nextPage()'>
- <?= __('&gt;&gt;') ?>
- </button>
-
- <button dojoType='dijit.form.Button'
- onclick='Helpers.EventLog.clear()'>
- <?= __('Clear') ?>
- </button>
-
- <div class='pull-right'>
- <label><?= __("Severity:") ?></label>
-
- <?= \Controls\select_hash("severity", $severity,
- [
- E_USER_ERROR => __("Errors"),
- E_USER_WARNING => __("Warnings"),
- E_USER_NOTICE => __("Everything")
- ], ["onchange"=> "Helpers.EventLog.refresh()"], "severity") ?>
- </div>
- </div>
-
- <div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">
-
- <table width='100%' class='event-log'>
-
- <tr>
- <th width='5%'><?= __("Error") ?></th>
- <th><?= __("Filename") ?></th>
- <th><?= __("Message") ?></th>
- <th width='5%'><?= __("User") ?></th>
- <th width='5%'><?= __("Date") ?></th>
- </tr>
-
- <?php
- $sth = $this->pdo->prepare("SELECT
- errno, errstr, filename, lineno, created_at, login, context
- FROM
- ttrss_error_log LEFT JOIN ttrss_users ON (owner_uid = ttrss_users.id)
- WHERE
- $errno_filter_qpart
- ORDER BY
- ttrss_error_log.id DESC
- LIMIT ". self::LOG_PAGE_LIMIT ." OFFSET $offset");
-
- $sth->execute($errno_values);
-
- while ($line = $sth->fetch()) {
- foreach ($line as $k => $v) { $line[$k] = htmlspecialchars($v ?? ''); }
- ?>
- <tr>
- <td class='errno'>
- <?= Logger::ERROR_NAMES[$line["errno"]] . " (" . $line["errno"] . ")" ?>
- </td>
- <td class='filename'><?= $line["filename"] . ":" . $line["lineno"] ?></td>
- <td class='errstr'><?= $line["errstr"] . "\n" . $line["context"] ?></td>
- <td class='login'><?= $line["login"] ?></td>
- <td class='timestamp'>
- <?= TimeHelper::make_local_datetime($line["created_at"], false) ?>
- </td>
- </tr>
- <?php } ?>
- </table>
- </div>
- </div>
- <?php
- }
-
- function index(): void {
-
- $severity = (int) ($_REQUEST["severity"] ?? E_USER_WARNING);
- $page = (int) ($_REQUEST["page"] ?? 0);
- ?>
- <div dojoType='dijit.layout.AccordionContainer' region='center'>
- <?php if (Config::get(Config::LOG_DESTINATION) == Logger::LOG_DEST_SQL) { ?>
- <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">report</i> <?= __('Event log') ?>'>
- <?php
- $this->_log_viewer($page, $severity);
- ?>
- </div>
- <?php } ?>
- <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">mail</i> <?= __('Mail configuration') ?>'>
- <div dojoType="dijit.layout.ContentPane">
-
- <form dojoType="dijit.form.Form">
- <script type="dojo/method" event="onSubmit" args="evt">
- evt.preventDefault();
- if (this.validate()) {
- xhr.json("backend.php", this.getValues(), (reply) => {
- const msg = App.byId("mail-test-result");
-
- if (reply.rc) {
- msg.innerHTML = __("Mail sent.");
- msg.className = 'alert alert-success';
- } else {
- msg.innerHTML = reply.error;
- msg.className = 'alert alert-danger';
- }
-
- msg.show();
- })
- }
- </script>
-
- <?= \Controls\hidden_tag("op", "pref-system") ?>
- <?= \Controls\hidden_tag("method", "sendTestEmail") ?>
-
- <fieldset>
- <label><?= __("To:") ?></label>
- <?= \Controls\input_tag("mail_address", "", "text", ['required' => 1]) ?>
- <?= \Controls\submit_tag(__("Send test email")) ?>
- <span style="display: none; margin-left : 10px" class="alert alert-error" id="mail-test-result">...</span>
- </fieldset>
- </form>
- </div>
- </div>
- <div dojoType='dijit.layout.AccordionPane' title='<i class="material-icons">info</i> <?= __('PHP Information') ?>'>
- <script type='dojo/method' event='onSelected' args='evt'>
- if (this.domNode.querySelector('.loading'))
- window.setTimeout(() => {
- xhr.post("backend.php", {op: 'pref-system', method: 'getphpinfo'}, (reply) => {
- this.attr('content', `<div class='phpinfo'>${reply}</div>`);
- });
- }, 200);
- </script>
- <span class='loading'><?= __("Loading, please wait...") ?></span>
- </div>
-
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefSystem") ?>
- </div>
- <?php
- }
-}
diff --git a/classes/pref/users.php b/classes/pref/users.php
deleted file mode 100644
index 78291592d..000000000
--- a/classes/pref/users.php
+++ /dev/null
@@ -1,294 +0,0 @@
-<?php
-class Pref_Users extends Handler_Administrative {
- function csrf_ignore(string $method): bool {
- return $method == "index";
- }
-
- function edit(): void {
- $user = ORM::for_table('ttrss_users')
- ->select_expr("id,login,access_level,email,full_name,otp_enabled")
- ->find_one((int)$_REQUEST["id"])
- ->as_array();
-
- global $access_level_names;
-
- if ($user) {
- print json_encode([
- "user" => $user,
- "access_level_names" => $access_level_names
- ]);
- }
- }
-
- function userdetails(): void {
- $id = (int) clean($_REQUEST["id"]);
-
- $sth = $this->pdo->prepare("SELECT login,
- ".SUBSTRING_FOR_DATE."(last_login,1,16) AS last_login,
- access_level,
- (SELECT COUNT(int_id) FROM ttrss_user_entries
- WHERE owner_uid = id) AS stored_articles,
- ".SUBSTRING_FOR_DATE."(created,1,16) AS created
- FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$id]);
-
- if ($row = $sth->fetch()) {
-
- $last_login = TimeHelper::make_local_datetime(
- $row["last_login"], true);
-
- $created = TimeHelper::make_local_datetime(
- $row["created"], true);
-
- $stored_articles = $row["stored_articles"];
-
- $sth = $this->pdo->prepare("SELECT COUNT(id) as num_feeds FROM ttrss_feeds
- WHERE owner_uid = ?");
- $sth->execute([$id]);
- $row = $sth->fetch();
-
- $num_feeds = $row["num_feeds"];
-
- ?>
-
- <fieldset>
- <label><?= __('Registered') ?>:</label>
- <?= $created ?>
- </fieldset>
-
- <fieldset>
- <label><?= __('Last logged in') ?>:</label>
- <?= $last_login ?>
- </fieldset>
-
- <fieldset>
- <label><?= __('Subscribed feeds') ?>:</label>
- <?= $num_feeds ?>
- </fieldset>
-
- <fieldset>
- <label><?= __('Stored articles') ?>:</label>
- <?= $stored_articles ?>
- </fieldset>
-
- <?php
- $sth = $this->pdo->prepare("SELECT id,title,site_url FROM ttrss_feeds
- WHERE owner_uid = ? ORDER BY title");
- $sth->execute([$id]);
- ?>
-
- <ul class="panel panel-scrollable list list-unstyled">
- <?php while ($row = $sth->fetch()) { ?>
- <li>
- <?php
- $icon_url = Feeds::_get_icon_url($row['id'], 'images/blank_icon.gif');
- ?>
-
- <img class="icon" src="<?= htmlspecialchars($icon_url) ?>">
-
- <a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>">
- <?= htmlspecialchars($row["title"]) ?>
- </a>
- </li>
- <?php } ?>
- </ul>
-
- <?php
-
- } else {
- print_error(__('User not found'));
- }
-
- }
-
- function editSave(): void {
- $id = (int)$_REQUEST['id'];
- $password = clean($_REQUEST["password"]);
- $user = ORM::for_table('ttrss_users')->find_one($id);
-
- if ($user) {
- $login = clean($_REQUEST["login"]);
-
- if ($id == 1) $login = "admin";
- if (!$login) return;
-
- $user->login = mb_strtolower($login);
- $user->access_level = (int) clean($_REQUEST["access_level"]);
- $user->email = clean($_REQUEST["email"]);
- $user->otp_enabled = checkbox_to_sql_bool($_REQUEST["otp_enabled"] ?? "");
-
- // force new OTP secret when next enabled
- if (Config::get_schema_version() >= 143 && !$user->otp_enabled) {
- $user->otp_secret = null;
- }
-
- $user->save();
- }
-
- if ($password) {
- UserHelper::reset_password($id, false, $password);
- }
- }
-
- function remove(): void {
- $ids = explode(",", clean($_REQUEST["ids"]));
-
- foreach ($ids as $id) {
- if ($id != $_SESSION["uid"] && $id != 1) {
- $sth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE owner_uid = ?");
- $sth->execute([$id]);
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_feeds WHERE owner_uid = ?");
- $sth->execute([$id]);
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_users WHERE id = ?");
- $sth->execute([$id]);
- }
- }
- }
-
- function add(): void {
- $login = clean($_REQUEST["login"]);
-
- if (!$login) return; // no blank usernames
-
- if (!UserHelper::find_user_by_login($login)) {
-
- $new_password = make_password();
-
- $user = ORM::for_table('ttrss_users')->create();
-
- $user->salt = UserHelper::get_salt();
- $user->login = mb_strtolower($login);
- $user->pwd_hash = UserHelper::hash_password($new_password, $user->salt);
- $user->access_level = 0;
- $user->created = Db::NOW();
- $user->save();
-
- if (!is_null(UserHelper::find_user_by_login($login))) {
- print T_sprintf("Added user %s with password %s",
- $login, $new_password);
- } else {
- print T_sprintf("Could not create user %s", $login);
- }
- } else {
- print T_sprintf("User %s already exists.", $login);
- }
- }
-
- function resetPass(): void {
- UserHelper::reset_password(clean($_REQUEST["id"]));
- }
-
- function index(): void {
-
- global $access_level_names;
-
- $user_search = clean($_REQUEST["search"] ?? "");
-
- if (array_key_exists("search", $_REQUEST)) {
- $_SESSION["prefs_user_search"] = $user_search;
- } else {
- $user_search = ($_SESSION["prefs_user_search"] ?? "");
- }
-
- $sort = clean($_REQUEST["sort"] ?? "");
-
- if (!$sort || $sort == "undefined") {
- $sort = "login";
- }
-
- if (!in_array($sort, ["login", "access_level", "created", "num_feeds", "created", "last_login"]))
- $sort = "login";
-
- if ($sort != "login") $sort = "$sort DESC";
- ?>
-
- <div dojoType='dijit.layout.BorderContainer' gutters='false'>
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
- <div dojoType='fox.Toolbar'>
-
- <div style='float : right'>
- <input dojoType='dijit.form.TextBox' id='user_search' size='20' type='search'
- value="<?= htmlspecialchars($user_search) ?>">
- <button dojoType='dijit.form.Button' onclick='Users.reload()'>
- <?= __('Search') ?>
- </button>
- </div>
-
- <div dojoType='fox.form.DropDownButton'>
- <span><?= __('Select') ?></span>
- <div dojoType='dijit.Menu' style='display: none'>
- <div onclick="Tables.select('users-list', true)"
- dojoType='dijit.MenuItem'><?= __('All') ?></div>
- <div onclick="Tables.select('users-list', false)"
- dojoType='dijit.MenuItem'><?= __('None') ?></div>
- </div>
- </div>
-
- <button dojoType='dijit.form.Button' onclick='Users.add()'>
- <?= __('Create user') ?>
- </button>
-
- <button dojoType='dijit.form.Button' onclick='Users.removeSelected()'>
- <?= __('Remove') ?>
- </button>
-
- <button dojoType='dijit.form.Button' onclick='Users.resetSelected()'>
- <?= __('Reset password') ?>
- </button>
-
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefUsersToolbar") ?>
-
- </div>
- </div>
- <div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
-
- <table width='100%' class='users-list' id='users-list'>
-
- <tr>
- <th></th>
- <th><a href='#' onclick="Users.reload('login')"><?= ('Login') ?></a></th>
- <th><a href='#' onclick="Users.reload('access_level')"><?= ('Access Level') ?></a></th>
- <th><a href='#' onclick="Users.reload('num_feeds')"><?= ('Subscribed feeds') ?></a></th>
- <th><a href='#' onclick="Users.reload('created')"><?= ('Registered') ?></a></th>
- <th><a href='#' onclick="Users.reload('last_login')"><?= ('Last login') ?></a></th>
- </tr>
-
- <?php
- $users = ORM::for_table('ttrss_users')
- ->table_alias('u')
- ->left_outer_join("ttrss_feeds", ["owner_uid", "=", "u.id"], 'f')
- ->select_expr('u.*,COUNT(f.id) AS num_feeds')
- ->where_like("login", $user_search ? "%$user_search%" : "%")
- ->order_by_expr($sort)
- ->group_by_expr('u.id')
- ->find_many();
-
- foreach ($users as $user) { ?>
-
- <tr data-row-id='<?= $user["id"] ?>' onclick='Users.edit(<?= $user["id"] ?>)' title="<?= __('Click to edit') ?>">
- <td class='checkbox'>
- <input onclick='Tables.onRowChecked(this); event.stopPropagation();'
- dojoType='dijit.form.CheckBox' type='checkbox'>
- </td>
-
- <td width='30%'>
- <i class='material-icons'>person</i>
- <strong><?= htmlspecialchars($user["login"]) ?></strong>
- </td>
- <td><?= $access_level_names[$user["access_level"]] ?></td>
- <td><?= $user["num_feeds"] ?></td>
- <td class='text-muted'><?= TimeHelper::make_local_datetime($user["created"], false) ?></td>
- <td class='text-muted'><?= TimeHelper::make_local_datetime($user["last_login"], false) ?></td>
- </tr>
- <?php } ?>
- </table>
- </div>
- <?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefUsers") ?>
- </div>
- <?php
- }
-
-}