feeds.php 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807
  1. <?php
  2. class Pref_Feeds extends Handler_Protected {
  3. function csrf_ignore($method) {
  4. $csrf_ignored = array("index", "getfeedtree", "add", "editcats", "editfeed",
  5. "savefeedorder", "uploadicon", "feedswitherrors", "inactivefeeds",
  6. "batchsubscribe");
  7. return array_search($method, $csrf_ignored) !== false;
  8. }
  9. public static function get_ts_languages() {
  10. $rv = [];
  11. if (DB_TYPE == "pgsql") {
  12. $dbh = Db::pdo();
  13. $res = $dbh->query("SELECT cfgname FROM pg_ts_config");
  14. while ($row = $res->fetch()) {
  15. array_push($rv, ucfirst($row['cfgname']));
  16. }
  17. }
  18. return $rv;
  19. }
  20. function batch_edit_cbox($elem, $label = false) {
  21. print "<input type=\"checkbox\" title=\"".__("Check to enable field")."\"
  22. onchange=\"dijit.byId('feedEditDlg').toggleField(this, '$elem', '$label')\">";
  23. }
  24. function renamecat() {
  25. $title = clean($_REQUEST['title']);
  26. $id = clean($_REQUEST['id']);
  27. if ($title) {
  28. $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories SET
  29. title = ? WHERE id = ? AND owner_uid = ?");
  30. $sth->execute([$title, $id, $_SESSION['uid']]);
  31. }
  32. }
  33. private function get_category_items($cat_id) {
  34. if (clean($_REQUEST['mode']) != 2)
  35. $search = $_SESSION["prefs_feed_search"];
  36. else
  37. $search = "";
  38. // first one is set by API
  39. $show_empty_cats = clean($_REQUEST['force_show_empty']) ||
  40. (clean($_REQUEST['mode']) != 2 && !$search);
  41. $items = array();
  42. $sth = $this->pdo->prepare("SELECT id, title FROM ttrss_feed_categories
  43. WHERE owner_uid = ? AND parent_cat = ? ORDER BY order_id, title");
  44. $sth->execute([$_SESSION['uid'], $cat_id]);
  45. while ($line = $sth->fetch()) {
  46. $cat = array();
  47. $cat['id'] = 'CAT:' . $line['id'];
  48. $cat['bare_id'] = (int)$line['id'];
  49. $cat['name'] = $line['title'];
  50. $cat['items'] = array();
  51. $cat['checkbox'] = false;
  52. $cat['type'] = 'category';
  53. $cat['unread'] = 0;
  54. $cat['child_unread'] = 0;
  55. $cat['auxcounter'] = 0;
  56. $cat['parent_id'] = $cat_id;
  57. $cat['items'] = $this->get_category_items($line['id']);
  58. $num_children = $this->calculate_children_count($cat);
  59. $cat['param'] = vsprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
  60. if ($num_children > 0 || $show_empty_cats)
  61. array_push($items, $cat);
  62. }
  63. $fsth = $this->pdo->prepare("SELECT id, title, last_error,
  64. ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval
  65. FROM ttrss_feeds
  66. WHERE cat_id = :cat AND
  67. owner_uid = :uid AND
  68. (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search))
  69. ORDER BY order_id, title");
  70. $fsth->execute([":cat" => $cat_id, ":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]);
  71. while ($feed_line = $fsth->fetch()) {
  72. $feed = array();
  73. $feed['id'] = 'FEED:' . $feed_line['id'];
  74. $feed['bare_id'] = (int)$feed_line['id'];
  75. $feed['auxcounter'] = 0;
  76. $feed['name'] = $feed_line['title'];
  77. $feed['checkbox'] = false;
  78. $feed['unread'] = 0;
  79. $feed['error'] = $feed_line['last_error'];
  80. $feed['icon'] = Feeds::getFeedIcon($feed_line['id']);
  81. $feed['param'] = make_local_datetime(
  82. $feed_line['last_updated'], true);
  83. $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
  84. array_push($items, $feed);
  85. }
  86. return $items;
  87. }
  88. function getfeedtree() {
  89. print json_encode($this->makefeedtree());
  90. }
  91. function makefeedtree() {
  92. if (clean($_REQUEST['mode']) != 2)
  93. $search = $_SESSION["prefs_feed_search"];
  94. else
  95. $search = "";
  96. $root = array();
  97. $root['id'] = 'root';
  98. $root['name'] = __('Feeds');
  99. $root['items'] = array();
  100. $root['type'] = 'category';
  101. $enable_cats = get_pref('ENABLE_FEED_CATS');
  102. if (clean($_REQUEST['mode']) == 2) {
  103. if ($enable_cats) {
  104. $cat = $this->feedlist_init_cat(-1);
  105. } else {
  106. $cat['items'] = array();
  107. }
  108. foreach (array(-4, -3, -1, -2, 0, -6) as $i) {
  109. array_push($cat['items'], $this->feedlist_init_feed($i));
  110. }
  111. /* Plugin feeds for -1 */
  112. $feeds = PluginHost::getInstance()->get_feeds(-1);
  113. if ($feeds) {
  114. foreach ($feeds as $feed) {
  115. $feed_id = PluginHost::pfeed_to_feed_id($feed['id']);
  116. $item = array();
  117. $item['id'] = 'FEED:' . $feed_id;
  118. $item['bare_id'] = (int)$feed_id;
  119. $item['auxcounter'] = 0;
  120. $item['name'] = $feed['title'];
  121. $item['checkbox'] = false;
  122. $item['error'] = '';
  123. $item['icon'] = $feed['icon'];
  124. $item['param'] = '';
  125. $item['unread'] = 0; //$feed['sender']->get_unread($feed['id']);
  126. $item['type'] = 'feed';
  127. array_push($cat['items'], $item);
  128. }
  129. }
  130. if ($enable_cats) {
  131. array_push($root['items'], $cat);
  132. } else {
  133. $root['items'] = array_merge($root['items'], $cat['items']);
  134. }
  135. $sth = $this->pdo->prepare("SELECT * FROM
  136. ttrss_labels2 WHERE owner_uid = ? ORDER by caption");
  137. $sth->execute([$_SESSION['uid']]);
  138. if (get_pref('ENABLE_FEED_CATS')) {
  139. $cat = $this->feedlist_init_cat(-2);
  140. } else {
  141. $cat['items'] = array();
  142. }
  143. $num_labels = 0;
  144. while ($line = $sth->fetch()) {
  145. ++$num_labels;
  146. $label_id = Labels::label_to_feed_id($line['id']);
  147. $feed = $this->feedlist_init_feed($label_id, false, 0);
  148. $feed['fg_color'] = $line['fg_color'];
  149. $feed['bg_color'] = $line['bg_color'];
  150. array_push($cat['items'], $feed);
  151. }
  152. if ($num_labels) {
  153. if ($enable_cats) {
  154. array_push($root['items'], $cat);
  155. } else {
  156. $root['items'] = array_merge($root['items'], $cat['items']);
  157. }
  158. }
  159. }
  160. if ($enable_cats) {
  161. $show_empty_cats = clean($_REQUEST['force_show_empty']) ||
  162. (clean($_REQUEST['mode']) != 2 && !$search);
  163. $sth = $this->pdo->prepare("SELECT id, title FROM ttrss_feed_categories
  164. WHERE owner_uid = ? AND parent_cat IS NULL ORDER BY order_id, title");
  165. $sth->execute([$_SESSION['uid']]);
  166. while ($line = $sth->fetch()) {
  167. $cat = array();
  168. $cat['id'] = 'CAT:' . $line['id'];
  169. $cat['bare_id'] = (int)$line['id'];
  170. $cat['auxcounter'] = 0;
  171. $cat['name'] = $line['title'];
  172. $cat['items'] = array();
  173. $cat['checkbox'] = false;
  174. $cat['type'] = 'category';
  175. $cat['unread'] = 0;
  176. $cat['child_unread'] = 0;
  177. $cat['items'] = $this->get_category_items($line['id']);
  178. $num_children = $this->calculate_children_count($cat);
  179. $cat['param'] = vsprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
  180. if ($num_children > 0 || $show_empty_cats)
  181. array_push($root['items'], $cat);
  182. $root['param'] += count($cat['items']);
  183. }
  184. /* Uncategorized is a special case */
  185. $cat = array();
  186. $cat['id'] = 'CAT:0';
  187. $cat['bare_id'] = 0;
  188. $cat['auxcounter'] = 0;
  189. $cat['name'] = __("Uncategorized");
  190. $cat['items'] = array();
  191. $cat['type'] = 'category';
  192. $cat['checkbox'] = false;
  193. $cat['unread'] = 0;
  194. $cat['child_unread'] = 0;
  195. $fsth = $this->pdo->prepare("SELECT id, title,last_error,
  196. ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval
  197. FROM ttrss_feeds
  198. WHERE cat_id IS NULL AND
  199. owner_uid = :uid AND
  200. (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search))
  201. ORDER BY order_id, title");
  202. $fsth->execute([":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]);
  203. while ($feed_line = $fsth->fetch()) {
  204. $feed = array();
  205. $feed['id'] = 'FEED:' . $feed_line['id'];
  206. $feed['bare_id'] = (int)$feed_line['id'];
  207. $feed['auxcounter'] = 0;
  208. $feed['name'] = $feed_line['title'];
  209. $feed['checkbox'] = false;
  210. $feed['error'] = $feed_line['last_error'];
  211. $feed['icon'] = Feeds::getFeedIcon($feed_line['id']);
  212. $feed['param'] = make_local_datetime(
  213. $feed_line['last_updated'], true);
  214. $feed['unread'] = 0;
  215. $feed['type'] = 'feed';
  216. $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
  217. array_push($cat['items'], $feed);
  218. }
  219. $cat['param'] = vsprintf(_ngettext('(%d feed)', '(%d feeds)', count($cat['items'])), count($cat['items']));
  220. if (count($cat['items']) > 0 || $show_empty_cats)
  221. array_push($root['items'], $cat);
  222. $num_children = $this->calculate_children_count($root);
  223. $root['param'] = vsprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
  224. } else {
  225. $fsth = $this->pdo->prepare("SELECT id, title, last_error,
  226. ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval
  227. FROM ttrss_feeds
  228. WHERE owner_uid = :uid AND
  229. (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search))
  230. ORDER BY order_id, title");
  231. $fsth->execute([":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]);
  232. while ($feed_line = $fsth->fetch()) {
  233. $feed = array();
  234. $feed['id'] = 'FEED:' . $feed_line['id'];
  235. $feed['bare_id'] = (int)$feed_line['id'];
  236. $feed['auxcounter'] = 0;
  237. $feed['name'] = $feed_line['title'];
  238. $feed['checkbox'] = false;
  239. $feed['error'] = $feed_line['last_error'];
  240. $feed['icon'] = Feeds::getFeedIcon($feed_line['id']);
  241. $feed['param'] = make_local_datetime(
  242. $feed_line['last_updated'], true);
  243. $feed['unread'] = 0;
  244. $feed['type'] = 'feed';
  245. $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
  246. array_push($root['items'], $feed);
  247. }
  248. $root['param'] = vsprintf(_ngettext('(%d feed)', '(%d feeds)', count($cat['items'])), count($cat['items']));
  249. }
  250. $fl = array();
  251. $fl['identifier'] = 'id';
  252. $fl['label'] = 'name';
  253. if (clean($_REQUEST['mode']) != 2) {
  254. $fl['items'] = array($root);
  255. } else {
  256. $fl['items'] = $root['items'];
  257. }
  258. return $fl;
  259. }
  260. function catsortreset() {
  261. $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
  262. SET order_id = 0 WHERE owner_uid = ?");
  263. $sth->execute([$_SESSION['uid']]);
  264. }
  265. function feedsortreset() {
  266. $sth = $this->pdo->prepare("UPDATE ttrss_feeds
  267. SET order_id = 0 WHERE owner_uid = ?");
  268. $sth->execute([$_SESSION['uid']]);
  269. }
  270. private function process_category_order(&$data_map, $item_id, $parent_id = false, $nest_level = 0) {
  271. $prefix = "";
  272. for ($i = 0; $i < $nest_level; $i++)
  273. $prefix .= " ";
  274. Debug::log("$prefix C: $item_id P: $parent_id");
  275. $bare_item_id = substr($item_id, strpos($item_id, ':')+1);
  276. if ($item_id != 'root') {
  277. if ($parent_id && $parent_id != 'root') {
  278. $parent_bare_id = substr($parent_id, strpos($parent_id, ':')+1);
  279. $parent_qpart = $parent_bare_id;
  280. } else {
  281. $parent_qpart = null;
  282. }
  283. $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
  284. SET parent_cat = ? WHERE id = ? AND
  285. owner_uid = ?");
  286. $sth->execute([$parent_qpart, $bare_item_id, $_SESSION['uid']]);
  287. }
  288. $order_id = 1;
  289. $cat = $data_map[$item_id];
  290. if ($cat && is_array($cat)) {
  291. foreach ($cat as $item) {
  292. $id = $item['_reference'];
  293. $bare_id = substr($id, strpos($id, ':')+1);
  294. Debug::log("$prefix [$order_id] $id/$bare_id");
  295. if ($item['_reference']) {
  296. if (strpos($id, "FEED") === 0) {
  297. $cat_id = ($item_id != "root") ? $bare_item_id : null;
  298. $sth = $this->pdo->prepare("UPDATE ttrss_feeds
  299. SET order_id = ?, cat_id = ?
  300. WHERE id = ? AND owner_uid = ?");
  301. $sth->execute([$order_id, $cat_id ? $cat_id : null, $bare_id, $_SESSION['uid']]);
  302. } else if (strpos($id, "CAT:") === 0) {
  303. $this->process_category_order($data_map, $item['_reference'], $item_id,
  304. $nest_level+1);
  305. $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
  306. SET order_id = ? WHERE id = ? AND
  307. owner_uid = ?");
  308. $sth->execute([$order_id, $bare_id, $_SESSION['uid']]);
  309. }
  310. }
  311. ++$order_id;
  312. }
  313. }
  314. }
  315. function savefeedorder() {
  316. $data = json_decode($_POST['payload'], true);
  317. #file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
  318. #$data = json_decode(file_get_contents("/tmp/saveorder.json"), true);
  319. if (!is_array($data['items']))
  320. $data['items'] = json_decode($data['items'], true);
  321. # print_r($data['items']);
  322. if (is_array($data) && is_array($data['items'])) {
  323. # $cat_order_id = 0;
  324. $data_map = array();
  325. $root_item = false;
  326. foreach ($data['items'] as $item) {
  327. # if ($item['id'] != 'root') {
  328. if (is_array($item['items'])) {
  329. if (isset($item['items']['_reference'])) {
  330. $data_map[$item['id']] = array($item['items']);
  331. } else {
  332. $data_map[$item['id']] = $item['items'];
  333. }
  334. }
  335. if ($item['id'] == 'root') {
  336. $root_item = $item['id'];
  337. }
  338. }
  339. $this->process_category_order($data_map, $root_item);
  340. }
  341. }
  342. function removeicon() {
  343. $feed_id = clean($_REQUEST["feed_id"]);
  344. $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
  345. WHERE id = ? AND owner_uid = ?");
  346. $sth->execute([$feed_id, $_SESSION['uid']]);
  347. if ($row = $sth->fetch()) {
  348. @unlink(ICONS_DIR . "/$feed_id.ico");
  349. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = NULL
  350. where id = ?");
  351. $sth->execute([$feed_id]);
  352. }
  353. }
  354. function uploadicon() {
  355. header("Content-type: text/html");
  356. if (is_uploaded_file($_FILES['icon_file']['tmp_name'])) {
  357. $tmp_file = tempnam(CACHE_DIR . '/upload', 'icon');
  358. $result = move_uploaded_file($_FILES['icon_file']['tmp_name'],
  359. $tmp_file);
  360. if (!$result) {
  361. return;
  362. }
  363. } else {
  364. return;
  365. }
  366. $icon_file = $tmp_file;
  367. $feed_id = clean($_REQUEST["feed_id"]);
  368. if (is_file($icon_file) && $feed_id) {
  369. if (filesize($icon_file) < 65535) {
  370. $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
  371. WHERE id = ? AND owner_uid = ?");
  372. $sth->execute([$feed_id, $_SESSION['uid']]);
  373. if ($row = $sth->fetch()) {
  374. @unlink(ICONS_DIR . "/$feed_id.ico");
  375. if (rename($icon_file, ICONS_DIR . "/$feed_id.ico")) {
  376. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET
  377. favicon_avg_color = ''
  378. WHERE id = ?");
  379. $sth->execute([$feed_id]);
  380. $rc = 0;
  381. }
  382. } else {
  383. $rc = 2;
  384. }
  385. } else {
  386. $rc = 1;
  387. }
  388. } else {
  389. $rc = 2;
  390. }
  391. if (is_file($icon_file)) @unlink($icon_file);
  392. print "<script type=\"text/javascript\">";
  393. print "parent.CommonDialogs.uploadIconHandler($rc);";
  394. print "</script>";
  395. return;
  396. }
  397. function editfeed() {
  398. global $purge_intervals;
  399. global $update_intervals;
  400. $feed_id = clean($_REQUEST["id"]);
  401. $sth = $this->pdo->prepare("SELECT * FROM ttrss_feeds WHERE id = ? AND
  402. owner_uid = ?");
  403. $sth->execute([$feed_id, $_SESSION['uid']]);
  404. if ($row = $sth->fetch()) {
  405. print '<div dojoType="dijit.layout.TabContainer" style="height : 450px">
  406. <div dojoType="dijit.layout.ContentPane" title="'.__('General').'">';
  407. $title = htmlspecialchars($row["title"]);
  408. print_hidden("id", "$feed_id");
  409. print_hidden("op", "pref-feeds");
  410. print_hidden("method", "editSave");
  411. print "<header>".__("Feed")."</header>";
  412. print "<section>";
  413. /* Title */
  414. print "<fieldset>";
  415. print "<input dojoType='dijit.form.ValidationTextBox' required='1'
  416. placeHolder=\"".__("Feed Title")."\"
  417. style='font-size : 16px; width: 500px' name='title' value=\"$title\">";
  418. print "</fieldset>";
  419. /* Feed URL */
  420. $feed_url = htmlspecialchars($row["feed_url"]);
  421. print "<fieldset>";
  422. print "<label>" . __('URL:') . "</label> ";
  423. print "<input dojoType='dijit.form.ValidationTextBox' required='1'
  424. placeHolder=\"".__("Feed URL")."\"
  425. regExp='^(http|https)://.*' style='width : 300px'
  426. name='feed_url' value=\"$feed_url\">";
  427. $last_error = $row["last_error"];
  428. if ($last_error) {
  429. print "&nbsp;<i class=\"material-icons\"
  430. title=\"".htmlspecialchars($last_error)."\">error</i>";
  431. }
  432. print "</fieldset>";
  433. /* Category */
  434. if (get_pref('ENABLE_FEED_CATS')) {
  435. $cat_id = $row["cat_id"];
  436. print "<fieldset>";
  437. print "<label>" . __('Place in category:') . "</label> ";
  438. print_feed_cat_select("cat_id", $cat_id,
  439. 'dojoType="dijit.form.Select"');
  440. print "</fieldset>";
  441. }
  442. /* Site URL */
  443. $site_url = htmlspecialchars($row["site_url"]);
  444. print "<fieldset>";
  445. print "<label>" . __('Site URL:') . "</label> ";
  446. print "<input dojoType='dijit.form.ValidationTextBox' required='1'
  447. placeHolder=\"".__("Site URL")."\"
  448. regExp='^(http|https)://.*' style='width : 300px'
  449. name='site_url' value=\"$site_url\">";
  450. print "</fieldset>";
  451. /* FTS Stemming Language */
  452. if (DB_TYPE == "pgsql") {
  453. $feed_language = $row["feed_language"];
  454. print "<fieldset>";
  455. print "<label>" . __('Language:') . "</label> ";
  456. print_select("feed_language", $feed_language, $this::get_ts_languages(),
  457. 'dojoType="dijit.form.Select"');
  458. print "</fieldset>";
  459. }
  460. print "</section>";
  461. print "<header>".__("Update")."</header>";
  462. print "<section>";
  463. /* Update Interval */
  464. $update_interval = $row["update_interval"];
  465. print "<fieldset>";
  466. print "<label>".__("Interval:")."</label> ";
  467. print_select_hash("update_interval", $update_interval, $update_intervals,
  468. 'dojoType="dijit.form.Select"');
  469. print "</fieldset>";
  470. /* Purge intl */
  471. $purge_interval = $row["purge_interval"];
  472. print "<fieldset>";
  473. print "<label>" . __('Article purging:') . "</label> ";
  474. print_select_hash("purge_interval", $purge_interval, $purge_intervals,
  475. 'dojoType="dijit.form.Select" ' .
  476. ((FORCE_ARTICLE_PURGE == 0) ? "" : 'disabled="1"'));
  477. print "</fieldset>";
  478. print "</section>";
  479. $auth_login = htmlspecialchars($row["auth_login"]);
  480. $auth_pass = htmlspecialchars($row["auth_pass"]);
  481. $auth_enabled = $auth_login !== '' || $auth_pass !== '';
  482. $auth_style = $auth_enabled ? '' : 'display: none';
  483. print "<div id='feedEditDlg_loginContainer' style='$auth_style'>";
  484. print "<header>".__("Authentication")."</header>";
  485. print "<section>";
  486. print "<fieldset>";
  487. print "<input dojoType='dijit.form.TextBox' id='feedEditDlg_login'
  488. placeHolder='".__("Login")."'
  489. autocomplete='new-password'
  490. name='auth_login' value=\"$auth_login\">";
  491. print "</fieldset><fieldset>";
  492. print "<input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
  493. autocomplete='new-password'
  494. placeHolder='".__("Password")."'
  495. value=\"$auth_pass\">";
  496. print "<div dojoType='dijit.Tooltip' connectId='feedEditDlg_login' position='below'>
  497. ".__('<b>Hint:</b> you need to fill in your login information if your feed requires authentication, except for Twitter feeds.')."
  498. </div>";
  499. print "</fieldset>";
  500. print "</section></div>";
  501. $auth_checked = $auth_enabled ? 'checked' : '';
  502. print "<label class='checkbox'>
  503. <input type='checkbox' $auth_checked name='need_auth' dojoType='dijit.form.CheckBox' id='feedEditDlg_loginCheck'
  504. onclick='displayIfChecked(this, \"feedEditDlg_loginContainer\")'>
  505. ".__('This feed requires authentication.')."</label>";
  506. print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Options').'">';
  507. print "<section class='narrow'>";
  508. $include_in_digest = $row["include_in_digest"];
  509. if ($include_in_digest) {
  510. $checked = "checked=\"1\"";
  511. } else {
  512. $checked = "";
  513. }
  514. print "<fieldset class='narrow'>";
  515. print "<label class='checkbox'><input dojoType=\"dijit.form.CheckBox\" type=\"checkbox\" id=\"include_in_digest\"
  516. name=\"include_in_digest\"
  517. $checked> ".__('Include in e-mail digest')."</label>";
  518. print "</fieldset>";
  519. $always_display_enclosures = $row["always_display_enclosures"];
  520. if ($always_display_enclosures) {
  521. $checked = "checked";
  522. } else {
  523. $checked = "";
  524. }
  525. print "<fieldset class='narrow'>";
  526. print "<label class='checkbox'><input dojoType=\"dijit.form.CheckBox\" type=\"checkbox\" id=\"always_display_enclosures\"
  527. name=\"always_display_enclosures\"
  528. $checked> ".__('Always display image attachments')."</label>";
  529. print "</fieldset>";
  530. $hide_images = $row["hide_images"];
  531. if ($hide_images) {
  532. $checked = "checked=\"1\"";
  533. } else {
  534. $checked = "";
  535. }
  536. print "<fieldset class='narrow'>";
  537. print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='hide_images'
  538. name='hide_images' $checked> ".__('Do not embed media')."</label>";
  539. print "</fieldset>";
  540. $cache_images = $row["cache_images"];
  541. if ($cache_images) {
  542. $checked = "checked=\"1\"";
  543. } else {
  544. $checked = "";
  545. }
  546. print "<fieldset class='narrow'>";
  547. print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='cache_images'
  548. name='cache_images' $checked> ". __('Cache media')."</label>";
  549. print "</fieldset>";
  550. $mark_unread_on_update = $row["mark_unread_on_update"];
  551. if ($mark_unread_on_update) {
  552. $checked = "checked";
  553. } else {
  554. $checked = "";
  555. }
  556. print "<fieldset class='narrow'>";
  557. print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='mark_unread_on_update'
  558. name='mark_unread_on_update' $checked> ".__('Mark updated articles as unread')."</label>";
  559. print "</fieldset>";
  560. print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Icon').'">';
  561. /* Icon */
  562. print "<img class='feedIcon' src=\"".Feeds::getFeedIcon($feed_id)."\">";
  563. print "<iframe name='icon_upload_iframe'
  564. style='width: 400px; height: 100px; display: none;'></iframe>";
  565. print "<form style='display : block' target='icon_upload_iframe'
  566. enctype='multipart/form-data' method='POST'
  567. action='backend.php'>
  568. <label class='dijitButton'>".__("Choose file...")."
  569. <input style='display: none' id='icon_file' size='10' name='icon_file' type='file'>
  570. </label>
  571. <input type='hidden' name='op' value='pref-feeds'>
  572. <input type='hidden' name='feed_id' value=\"$feed_id\">
  573. <input type='hidden' name='method' value=\"uploadicon\">
  574. <button dojoType='dijit.form.Button' onclick=\"return CommonDialogs.uploadFeedIcon();\"
  575. type='submit'>".__('Replace')."</button>
  576. <button class='alt-danger' dojoType='dijit.form.Button' onclick=\"return CommonDialogs.removeFeedIcon($feed_id);\"
  577. type='submit'>".__('Remove')."</button>
  578. </form>";
  579. print "</section>";
  580. print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Plugins').'">';
  581. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_EDIT_FEED,
  582. "hook_prefs_edit_feed", $feed_id);
  583. print "</div></div>";
  584. $title = htmlspecialchars($title, ENT_QUOTES);
  585. print "<footer>
  586. <button style='float : left' class='alt-danger' dojoType='dijit.form.Button' onclick='return CommonDialogs.unsubscribeFeed($feed_id, \"$title\")'>".
  587. __('Unsubscribe')."</button>
  588. <button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return dijit.byId('feedEditDlg').execute()\">".__('Save')."</button>
  589. <button dojoType='dijit.form.Button' onclick=\"return dijit.byId('feedEditDlg').hide()\">".__('Cancel')."</button>
  590. </footer>";
  591. }
  592. }
  593. function editfeeds() {
  594. global $purge_intervals;
  595. global $update_intervals;
  596. $feed_ids = clean($_REQUEST["ids"]);
  597. print_notice("Enable the options you wish to apply using checkboxes on the right:");
  598. print "<p>";
  599. print_hidden("ids", "$feed_ids");
  600. print_hidden("op", "pref-feeds");
  601. print_hidden("method", "batchEditSave");
  602. print "<header>".__("Feed")."</header>";
  603. print "<section>";
  604. /* Category */
  605. if (get_pref('ENABLE_FEED_CATS')) {
  606. print "<fieldset>";
  607. print "<label>" . __('Place in category:') . "</label> ";
  608. print_feed_cat_select("cat_id", false,
  609. 'disabled="1" dojoType="dijit.form.Select"');
  610. $this->batch_edit_cbox("cat_id");
  611. print "</fieldset>";
  612. }
  613. /* FTS Stemming Language */
  614. if (DB_TYPE == "pgsql") {
  615. print "<fieldset>";
  616. print "<label>" . __('Language:') . "</label> ";
  617. print_select("feed_language", "", $this::get_ts_languages(),
  618. 'disabled="1" dojoType="dijit.form.Select"');
  619. $this->batch_edit_cbox("feed_language");
  620. print "</fieldset>";
  621. }
  622. print "</section>";
  623. print "<header>".__("Update")."</header>";
  624. print "<section>";
  625. /* Update Interval */
  626. print "<fieldset>";
  627. print "<label>".__("Interval:")."</label> ";
  628. print_select_hash("update_interval", "", $update_intervals,
  629. 'disabled="1" dojoType="dijit.form.Select"');
  630. $this->batch_edit_cbox("update_interval");
  631. print "</fieldset>";
  632. /* Purge intl */
  633. if (FORCE_ARTICLE_PURGE == 0) {
  634. print "<fieldset>";
  635. print "<label>" . __('Article purging:') . "</label> ";
  636. print_select_hash("purge_interval", "", $purge_intervals,
  637. 'disabled="1" dojoType="dijit.form.Select"');
  638. $this->batch_edit_cbox("purge_interval");
  639. print "</fieldset>";
  640. }
  641. print "</section>";
  642. print "<header>".__("Authentication")."</header>";
  643. print "<section>";
  644. print "<fieldset>";
  645. print "<input dojoType='dijit.form.TextBox'
  646. placeHolder=\"".__("Login")."\" disabled='1'
  647. autocomplete='new-password'
  648. name='auth_login' value=''>";
  649. $this->batch_edit_cbox("auth_login");
  650. print "<input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
  651. autocomplete='new-password'
  652. placeHolder=\"".__("Password")."\" disabled='1'
  653. value=''>";
  654. $this->batch_edit_cbox("auth_pass");
  655. print "</fieldset>";
  656. print "</section>";
  657. print "<header>".__("Options")."</header>";
  658. print "<section>";
  659. print "<fieldset class='narrow'>";
  660. print "<label class='checkbox'><input disabled='1' type='checkbox' id='include_in_digest'
  661. name='include_in_digest' dojoType='dijit.form.CheckBox'>&nbsp;".__('Include in e-mail digest')."</label>";
  662. print "&nbsp;"; $this->batch_edit_cbox("include_in_digest", "include_in_digest_l");
  663. print "</fieldset><fieldset class='narrow'>";
  664. print "<label class='checkbox'><input disabled='1' type='checkbox' id='always_display_enclosures'
  665. name='always_display_enclosures' dojoType='dijit.form.CheckBox'>&nbsp;".__('Always display image attachments')."</label>";
  666. print "&nbsp;"; $this->batch_edit_cbox("always_display_enclosures", "always_display_enclosures_l");
  667. print "</fieldset><fieldset class='narrow'>";
  668. print "<label class='checkbox'><input disabled='1' type='checkbox' id='hide_images'
  669. name='hide_images' dojoType='dijit.form.CheckBox'>&nbsp;". __('Do not embed media')."</label>";
  670. print "&nbsp;"; $this->batch_edit_cbox("hide_images", "hide_images_l");
  671. print "</fieldset><fieldset class='narrow'>";
  672. print "<label class='checkbox'><input disabled='1' type='checkbox' id='cache_images'
  673. name='cache_images' dojoType='dijit.form.CheckBox'>&nbsp;".__('Cache media')."</label>";
  674. print "&nbsp;"; $this->batch_edit_cbox("cache_images", "cache_images_l");
  675. print "</fieldset><fieldset class='narrow'>";
  676. print "<label class='checkbox'><input disabled='1' type='checkbox' id='mark_unread_on_update'
  677. name='mark_unread_on_update' dojoType='dijit.form.CheckBox'>&nbsp;".__('Mark updated articles as unread')."</label>";
  678. print "&nbsp;"; $this->batch_edit_cbox("mark_unread_on_update", "mark_unread_on_update_l");
  679. print "</fieldset>";
  680. print "</section>";
  681. print "<footer>
  682. <button dojoType='dijit.form.Button' type='submit' class='alt-primary'
  683. onclick=\"return dijit.byId('feedEditDlg').execute()\">".
  684. __('Save')."</button>
  685. <button dojoType='dijit.form.Button'
  686. onclick=\"return dijit.byId('feedEditDlg').hide()\">".
  687. __('Cancel')."</button>
  688. </footer>";
  689. return;
  690. }
  691. function batchEditSave() {
  692. return $this->editsaveops(true);
  693. }
  694. function editSave() {
  695. return $this->editsaveops(false);
  696. }
  697. function editsaveops($batch) {
  698. $feed_title = trim(clean($_POST["title"]));
  699. $feed_url = trim(clean($_POST["feed_url"]));
  700. $site_url = trim(clean($_POST["site_url"]));
  701. $upd_intl = (int) clean($_POST["update_interval"]);
  702. $purge_intl = (int) clean($_POST["purge_interval"]);
  703. $feed_id = (int) clean($_POST["id"]); /* editSave */
  704. $feed_ids = explode(",", clean($_POST["ids"])); /* batchEditSave */
  705. $cat_id = (int) clean($_POST["cat_id"]);
  706. $auth_login = trim(clean($_POST["auth_login"]));
  707. $auth_pass = trim(clean($_POST["auth_pass"]));
  708. $private = checkbox_to_sql_bool(clean($_POST["private"]));
  709. $include_in_digest = checkbox_to_sql_bool(
  710. clean($_POST["include_in_digest"]));
  711. $cache_images = checkbox_to_sql_bool(
  712. clean($_POST["cache_images"]));
  713. $hide_images = checkbox_to_sql_bool(
  714. clean($_POST["hide_images"]));
  715. $always_display_enclosures = checkbox_to_sql_bool(
  716. clean($_POST["always_display_enclosures"]));
  717. $mark_unread_on_update = checkbox_to_sql_bool(
  718. clean($_POST["mark_unread_on_update"]));
  719. $feed_language = trim(clean($_POST["feed_language"]));
  720. if (!$batch) {
  721. if (clean($_POST["need_auth"]) !== 'on') {
  722. $auth_login = '';
  723. $auth_pass = '';
  724. }
  725. /* $sth = $this->pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?");
  726. $sth->execute([$feed_id]);
  727. $row = $sth->fetch();$orig_feed_url = $row["feed_url"];
  728. $reset_basic_info = $orig_feed_url != $feed_url; */
  729. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET
  730. cat_id = :cat_id,
  731. title = :title,
  732. feed_url = :feed_url,
  733. site_url = :site_url,
  734. update_interval = :upd_intl,
  735. purge_interval = :purge_intl,
  736. auth_login = :auth_login,
  737. auth_pass = :auth_pass,
  738. auth_pass_encrypted = false,
  739. private = :private,
  740. cache_images = :cache_images,
  741. hide_images = :hide_images,
  742. include_in_digest = :include_in_digest,
  743. always_display_enclosures = :always_display_enclosures,
  744. mark_unread_on_update = :mark_unread_on_update,
  745. feed_language = :feed_language
  746. WHERE id = :id AND owner_uid = :uid");
  747. $sth->execute([":title" => $feed_title,
  748. ":cat_id" => $cat_id ? $cat_id : null,
  749. ":feed_url" => $feed_url,
  750. ":site_url" => $site_url,
  751. ":upd_intl" => $upd_intl,
  752. ":purge_intl" => $purge_intl,
  753. ":auth_login" => $auth_login,
  754. ":auth_pass" => $auth_pass,
  755. ":private" => (int)$private,
  756. ":cache_images" => (int)$cache_images,
  757. ":hide_images" => (int)$hide_images,
  758. ":include_in_digest" => (int)$include_in_digest,
  759. ":always_display_enclosures" => (int)$always_display_enclosures,
  760. ":mark_unread_on_update" => (int)$mark_unread_on_update,
  761. ":feed_language" => $feed_language,
  762. ":id" => $feed_id,
  763. ":uid" => $_SESSION['uid']]);
  764. /* if ($reset_basic_info) {
  765. RSSUtils::set_basic_feed_info($feed_id);
  766. } */
  767. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_SAVE_FEED,
  768. "hook_prefs_save_feed", $feed_id);
  769. } else {
  770. $feed_data = array();
  771. foreach (array_keys($_POST) as $k) {
  772. if ($k != "op" && $k != "method" && $k != "ids") {
  773. $feed_data[$k] = clean($_POST[$k]);
  774. }
  775. }
  776. $this->pdo->beginTransaction();
  777. $feed_ids_qmarks = arr_qmarks($feed_ids);
  778. foreach (array_keys($feed_data) as $k) {
  779. $qpart = "";
  780. switch ($k) {
  781. case "title":
  782. $qpart = "title = " . $this->pdo->quote($feed_title);
  783. break;
  784. case "feed_url":
  785. $qpart = "feed_url = " . $this->pdo->quote($feed_url);
  786. break;
  787. case "update_interval":
  788. $qpart = "update_interval = " . $this->pdo->quote($upd_intl);
  789. break;
  790. case "purge_interval":
  791. $qpart = "purge_interval =" . $this->pdo->quote($purge_intl);
  792. break;
  793. case "auth_login":
  794. $qpart = "auth_login = " . $this->pdo->quote($auth_login);
  795. break;
  796. case "auth_pass":
  797. $qpart = "auth_pass =" . $this->pdo->quote($auth_pass). ", auth_pass_encrypted = false";
  798. break;
  799. case "private":
  800. $qpart = "private = " . $this->pdo->quote($private);
  801. break;
  802. case "include_in_digest":
  803. $qpart = "include_in_digest = " . $this->pdo->quote($include_in_digest);
  804. break;
  805. case "always_display_enclosures":
  806. $qpart = "always_display_enclosures = " . $this->pdo->quote($always_display_enclosures);
  807. break;
  808. case "mark_unread_on_update":
  809. $qpart = "mark_unread_on_update = " . $this->pdo->quote($mark_unread_on_update);
  810. break;
  811. case "cache_images":
  812. $qpart = "cache_images = " . $this->pdo->quote($cache_images);
  813. break;
  814. case "hide_images":
  815. $qpart = "hide_images = " . $this->pdo->quote($hide_images);
  816. break;
  817. case "cat_id":
  818. if (get_pref('ENABLE_FEED_CATS')) {
  819. if ($cat_id) {
  820. $qpart = "cat_id = " . $this->pdo->quote($cat_id);
  821. } else {
  822. $qpart = 'cat_id = NULL';
  823. }
  824. } else {
  825. $qpart = "";
  826. }
  827. break;
  828. case "feed_language":
  829. $qpart = "feed_language = " . $this->pdo->quote($feed_language);
  830. break;
  831. }
  832. if ($qpart) {
  833. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET $qpart WHERE id IN ($feed_ids_qmarks)
  834. AND owner_uid = ?");
  835. $sth->execute(array_merge($feed_ids, [$_SESSION['uid']]));
  836. }
  837. }
  838. $this->pdo->commit();
  839. }
  840. return;
  841. }
  842. function remove() {
  843. $ids = explode(",", clean($_REQUEST["ids"]));
  844. foreach ($ids as $id) {
  845. Pref_Feeds::remove_feed($id, $_SESSION["uid"]);
  846. }
  847. return;
  848. }
  849. function removeCat() {
  850. $ids = explode(",", clean($_REQUEST["ids"]));
  851. foreach ($ids as $id) {
  852. $this->remove_feed_category($id, $_SESSION["uid"]);
  853. }
  854. }
  855. function addCat() {
  856. $feed_cat = trim(clean($_REQUEST["cat"]));
  857. add_feed_category($feed_cat);
  858. }
  859. function index() {
  860. print "<div dojoType='dijit.layout.AccordionContainer' region='center'>";
  861. print "<div style='padding : 0px' dojoType='dijit.layout.AccordionPane'
  862. title=\"<i class='material-icons'>rss_feed</i> ".__('Feeds')."\">";
  863. $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors
  864. FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
  865. $sth->execute([$_SESSION['uid']]);
  866. if ($row = $sth->fetch()) {
  867. $num_errors = $row["num_errors"];
  868. } else {
  869. $num_errors = 0;
  870. }
  871. if ($num_errors > 0) {
  872. $error_button = "<button dojoType=\"dijit.form.Button\"
  873. onclick=\"CommonDialogs.showFeedsWithErrors()\" id=\"errorButton\">" .
  874. __("Feeds with errors") . "</button>";
  875. }
  876. $inactive_button = "<button dojoType=\"dijit.form.Button\"
  877. id=\"pref_feeds_inactive_btn\"
  878. style=\"display : none\"
  879. onclick=\"dijit.byId('feedTree').showInactiveFeeds()\">" .
  880. __("Inactive feeds") . "</button>";
  881. $feed_search = clean($_REQUEST["search"]);
  882. if (array_key_exists("search", $_REQUEST)) {
  883. $_SESSION["prefs_feed_search"] = $feed_search;
  884. } else {
  885. $feed_search = $_SESSION["prefs_feed_search"];
  886. }
  887. print '<div dojoType="dijit.layout.BorderContainer" gutters="false">';
  888. print "<div region='top' dojoType=\"dijit.Toolbar\">"; #toolbar
  889. print "<div style='float : right; padding-right : 4px;'>
  890. <input dojoType=\"dijit.form.TextBox\" id=\"feed_search\" size=\"20\" type=\"search\"
  891. value=\"$feed_search\">
  892. <button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('feedTree').reload()\">".
  893. __('Search')."</button>
  894. </div>";
  895. print "<div dojoType=\"dijit.form.DropDownButton\">".
  896. "<span>" . __('Select')."</span>";
  897. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  898. print "<div onclick=\"dijit.byId('feedTree').model.setAllChecked(true)\"
  899. dojoType=\"dijit.MenuItem\">".__('All')."</div>";
  900. print "<div onclick=\"dijit.byId('feedTree').model.setAllChecked(false)\"
  901. dojoType=\"dijit.MenuItem\">".__('None')."</div>";
  902. print "</div></div>";
  903. print "<div dojoType=\"dijit.form.DropDownButton\">".
  904. "<span>" . __('Feeds')."</span>";
  905. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  906. print "<div onclick=\"CommonDialogs.quickAddFeed()\"
  907. dojoType=\"dijit.MenuItem\">".__('Subscribe to feed')."</div>";
  908. print "<div onclick=\"dijit.byId('feedTree').editSelectedFeed()\"
  909. dojoType=\"dijit.MenuItem\">".__('Edit selected feeds')."</div>";
  910. print "<div onclick=\"dijit.byId('feedTree').resetFeedOrder()\"
  911. dojoType=\"dijit.MenuItem\">".__('Reset sort order')."</div>";
  912. print "<div onclick=\"dijit.byId('feedTree').batchSubscribe()\"
  913. dojoType=\"dijit.MenuItem\">".__('Batch subscribe')."</div>";
  914. print "<div dojoType=\"dijit.MenuItem\" onclick=\"dijit.byId('feedTree').removeSelectedFeeds()\">"
  915. .__('Unsubscribe')."</div> ";
  916. print "</div></div>";
  917. if (get_pref('ENABLE_FEED_CATS')) {
  918. print "<div dojoType=\"dijit.form.DropDownButton\">".
  919. "<span>" . __('Categories')."</span>";
  920. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  921. print "<div onclick=\"dijit.byId('feedTree').createCategory()\"
  922. dojoType=\"dijit.MenuItem\">".__('Add category')."</div>";
  923. print "<div onclick=\"dijit.byId('feedTree').resetCatOrder()\"
  924. dojoType=\"dijit.MenuItem\">".__('Reset sort order')."</div>";
  925. print "<div onclick=\"dijit.byId('feedTree').removeSelectedCategories()\"
  926. dojoType=\"dijit.MenuItem\">".__('Remove selected')."</div>";
  927. print "</div></div>";
  928. }
  929. print $error_button;
  930. print $inactive_button;
  931. print "</div>"; # toolbar
  932. //print '</div>';
  933. print '<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">';
  934. print "<div id=\"feedlistLoading\">
  935. <img src='images/indicator_tiny.gif'>".
  936. __("Loading, please wait...")."</div>";
  937. $auto_expand = $feed_search != "" ? "true" : "false";
  938. print "<div dojoType=\"fox.PrefFeedStore\" jsId=\"feedStore\"
  939. url=\"backend.php?op=pref-feeds&method=getfeedtree\">
  940. </div>
  941. <div dojoType=\"lib.CheckBoxStoreModel\" jsId=\"feedModel\" store=\"feedStore\"
  942. query=\"{id:'root'}\" rootId=\"root\" rootLabel=\"Feeds\"
  943. childrenAttrs=\"items\" checkboxStrict=\"false\" checkboxAll=\"false\">
  944. </div>
  945. <div dojoType=\"fox.PrefFeedTree\" id=\"feedTree\"
  946. dndController=\"dijit.tree.dndSource\"
  947. betweenThreshold=\"5\"
  948. autoExpand='$auto_expand'
  949. model=\"feedModel\" openOnClick=\"false\">
  950. <script type=\"dojo/method\" event=\"onClick\" args=\"item\">
  951. var id = String(item.id);
  952. var bare_id = id.substr(id.indexOf(':')+1);
  953. if (id.match('FEED:')) {
  954. CommonDialogs.editFeed(bare_id);
  955. } else if (id.match('CAT:')) {
  956. dijit.byId('feedTree').editCategory(bare_id, item);
  957. }
  958. </script>
  959. <script type=\"dojo/method\" event=\"onLoad\" args=\"item\">
  960. Element.hide(\"feedlistLoading\");
  961. dijit.byId('feedTree').checkInactiveFeeds();
  962. </script>
  963. </div>";
  964. # print "<div dojoType=\"dijit.Tooltip\" connectId=\"feedTree\" position=\"below\">
  965. # ".__('<b>Hint:</b> you can drag feeds and categories around.')."
  966. # </div>";
  967. print '</div>';
  968. print '</div>';
  969. print "</div>"; # feeds pane
  970. print "<div dojoType='dijit.layout.AccordionPane'
  971. title='<i class=\"material-icons\">import_export</i> ".__('OPML')."'>";
  972. print "<h3>" . __("Using OPML you can export and import your feeds, filters, labels and Tiny Tiny RSS settings.") . "</h3>";
  973. print_notice("Only main settings profile can be migrated using OPML.");
  974. print "<iframe id=\"upload_iframe\"
  975. name=\"upload_iframe\" onload=\"Helpers.OPML.onImportComplete(this)\"
  976. style=\"width: 400px; height: 100px; display: none;\"></iframe>";
  977. print "<form name='opml_form' style='display : inline-block' target='upload_iframe'
  978. enctype='multipart/form-data' method='POST'
  979. action='backend.php'>
  980. <label class='dijitButton'>".__("Choose file...")."
  981. <input style='display : none' id='opml_file' name='opml_file' type='file'>&nbsp;
  982. </label>
  983. <input type='hidden' name='op' value='dlg'>
  984. <input type='hidden' name='method' value='importOpml'>
  985. <button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return Helpers.OPML.import();\" type=\"submit\">" .
  986. __('Import OPML') . "</button>";
  987. print "</form>";
  988. print "<form dojoType='dijit.form.Form' id='opmlExportForm' style='display : inline-block'>";
  989. print "<button dojoType='dijit.form.Button'
  990. onclick='Helpers.OPML.export()' >" .
  991. __('Export OPML') . "</button>";
  992. print " <label class='checkbox'>";
  993. print_checkbox("include_settings", true, "1", "");
  994. print " " . __("Include settings");
  995. print "</label>";
  996. print "</form>";
  997. print "<p/>";
  998. print "<h2>" . __("Published OPML") . "</h2>";
  999. print "<p>" . __('Your OPML can be published publicly and can be subscribed by anyone who knows the URL below.') .
  1000. " " .
  1001. __("Published OPML does not include your Tiny Tiny RSS settings, feeds that require authentication or feeds hidden from Popular feeds.") . "</p>";
  1002. print "<button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return App.displayDlg('".__("Public OPML URL")."','pubOPMLUrl')\">".
  1003. __('Display published OPML URL')."</button> ";
  1004. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  1005. "hook_prefs_tab_section", "prefFeedsOPML");
  1006. print "</div>"; # pane
  1007. print "<div dojoType=\"dijit.layout.AccordionPane\"
  1008. title=\"<i class='material-icons'>share</i> ".__('Published & shared articles / Generated feeds')."\">";
  1009. print "<h3>" . __('Published articles can be subscribed by anyone who knows the following URL:') . "</h3>";
  1010. $rss_url = '-2::' . htmlspecialchars(get_self_url_prefix() .
  1011. "/public.php?op=rss&id=-2&view-mode=all_articles");;
  1012. print "<button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return App.displayDlg('".__("Show as feed")."','generatedFeed', '$rss_url')\">".
  1013. __('Display URL')."</button> ";
  1014. print "<button class=\"alt-danger\" dojoType=\"dijit.form.Button\" onclick=\"return Helpers.clearFeedAccessKeys()\">".
  1015. __('Clear all generated URLs')."</button> ";
  1016. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  1017. "hook_prefs_tab_section", "prefFeedsPublishedGenerated");
  1018. print "</div>"; #pane
  1019. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB,
  1020. "hook_prefs_tab", "prefFeeds");
  1021. print "</div>"; #container
  1022. }
  1023. private function feedlist_init_cat($cat_id) {
  1024. $obj = array();
  1025. $cat_id = (int) $cat_id;
  1026. if ($cat_id > 0) {
  1027. $cat_unread = CCache::find($cat_id, $_SESSION["uid"], true);
  1028. } else if ($cat_id == 0 || $cat_id == -2) {
  1029. $cat_unread = Feeds::getCategoryUnread($cat_id);
  1030. }
  1031. $obj['id'] = 'CAT:' . $cat_id;
  1032. $obj['items'] = array();
  1033. $obj['name'] = Feeds::getCategoryTitle($cat_id);
  1034. $obj['type'] = 'category';
  1035. $obj['unread'] = (int) $cat_unread;
  1036. $obj['bare_id'] = $cat_id;
  1037. return $obj;
  1038. }
  1039. private function feedlist_init_feed($feed_id, $title = false, $unread = false, $error = '', $updated = '') {
  1040. $obj = array();
  1041. $feed_id = (int) $feed_id;
  1042. if (!$title)
  1043. $title = Feeds::getFeedTitle($feed_id, false);
  1044. if ($unread === false)
  1045. $unread = getFeedUnread($feed_id, false);
  1046. $obj['id'] = 'FEED:' . $feed_id;
  1047. $obj['name'] = $title;
  1048. $obj['unread'] = (int) $unread;
  1049. $obj['type'] = 'feed';
  1050. $obj['error'] = $error;
  1051. $obj['updated'] = $updated;
  1052. $obj['icon'] = Feeds::getFeedIcon($feed_id);
  1053. $obj['bare_id'] = $feed_id;
  1054. $obj['auxcounter'] = 0;
  1055. return $obj;
  1056. }
  1057. function inactiveFeeds() {
  1058. if (DB_TYPE == "pgsql") {
  1059. $interval_qpart = "NOW() - INTERVAL '3 months'";
  1060. } else {
  1061. $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
  1062. }
  1063. $sth = $this->pdo->prepare("SELECT ttrss_feeds.title, ttrss_feeds.site_url,
  1064. ttrss_feeds.feed_url, ttrss_feeds.id, MAX(updated) AS last_article
  1065. FROM ttrss_feeds, ttrss_entries, ttrss_user_entries WHERE
  1066. (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE
  1067. ttrss_entries.id = ref_id AND
  1068. ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart
  1069. AND ttrss_feeds.owner_uid = ? AND
  1070. ttrss_user_entries.feed_id = ttrss_feeds.id AND
  1071. ttrss_entries.id = ref_id
  1072. GROUP BY ttrss_feeds.title, ttrss_feeds.id, ttrss_feeds.site_url, ttrss_feeds.feed_url
  1073. ORDER BY last_article");
  1074. $sth->execute([$_SESSION['uid']]);
  1075. print "<div dojoType='dijit.Toolbar'>";
  1076. print "<div dojoType='dijit.form.DropDownButton'>".
  1077. "<span>" . __('Select')."</span>";
  1078. print "<div dojoType='dijit.Menu' style='display: none'>";
  1079. print "<div onclick=\"Tables.select('inactive-feeds-list', true)\"
  1080. dojoType='dijit.MenuItem'>".__('All')."</div>";
  1081. print "<div onclick=\"Tables.select('inactive-feeds-list', false)\"
  1082. dojoType='dijit.MenuItem'>".__('None')."</div>";
  1083. print "</div></div>";
  1084. print "</div>"; #toolbar
  1085. print "<div class='panel panel-scrollable'>";
  1086. print "<table width='100%' id='inactive-feeds-list'>";
  1087. $lnum = 1;
  1088. while ($line = $sth->fetch()) {
  1089. $feed_id = $line["id"];
  1090. print "<tr data-row-id='$feed_id'>";
  1091. print "<td width='5%' align='center'><input
  1092. onclick='Tables.onRowChecked(this);' dojoType='dijit.form.CheckBox'
  1093. type='checkbox'></td>";
  1094. print "<td>";
  1095. print "<a href='#' ".
  1096. "title=\"".__("Click to edit feed")."\" ".
  1097. "onclick=\"CommonDialogs.editFeed(".$line["id"].")\">".
  1098. htmlspecialchars($line["title"])."</a>";
  1099. print "</td><td class='text-muted' align='right'>";
  1100. print make_local_datetime($line['last_article'], false);
  1101. print "</td>";
  1102. print "</tr>";
  1103. ++$lnum;
  1104. }
  1105. print "</table>";
  1106. print "</div>";
  1107. print "<footer>
  1108. <button style='float : left' class=\"alt-danger\" dojoType='dijit.form.Button' onclick=\"dijit.byId('inactiveFeedsDlg').removeSelected()\">"
  1109. .__('Unsubscribe from selected feeds')."</button>
  1110. <button dojoType='dijit.form.Button' onclick=\"dijit.byId('inactiveFeedsDlg').hide()\">"
  1111. .__('Close this window')."</button>
  1112. </footer>";
  1113. }
  1114. function feedsWithErrors() {
  1115. $sth = $this->pdo->prepare("SELECT id,title,feed_url,last_error,site_url
  1116. FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
  1117. $sth->execute([$_SESSION['uid']]);
  1118. print "<div dojoType=\"dijit.Toolbar\">";
  1119. print "<div dojoType=\"dijit.form.DropDownButton\">".
  1120. "<span>" . __('Select')."</span>";
  1121. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  1122. print "<div onclick=\"Tables.select('error-feeds-list', true)\"
  1123. dojoType=\"dijit.MenuItem\">".__('All')."</div>";
  1124. print "<div onclick=\"Tables.select('error-feeds-list', false)\"
  1125. dojoType=\"dijit.MenuItem\">".__('None')."</div>";
  1126. print "</div></div>";
  1127. print "</div>"; #toolbar
  1128. print "<div class='panel panel-scrollable'>";
  1129. print "<table width='100%' id='error-feeds-list'>";
  1130. $lnum = 1;
  1131. while ($line = $sth->fetch()) {
  1132. $feed_id = $line["id"];
  1133. print "<tr data-row-id='$feed_id'>";
  1134. print "<td width='5%' align='center'><input
  1135. onclick='Tables.onRowChecked(this);' dojoType=\"dijit.form.CheckBox\"
  1136. type=\"checkbox\"></td>";
  1137. print "<td>";
  1138. print "<a class=\"visibleLink\" href=\"#\" ".
  1139. "title=\"".__("Click to edit feed")."\" ".
  1140. "onclick=\"CommonDialogs.editFeed(".$line["id"].")\">".
  1141. htmlspecialchars($line["title"])."</a>: ";
  1142. print "<span class=\"text-muted\">";
  1143. print htmlspecialchars($line["last_error"]);
  1144. print "</span>";
  1145. print "</td>";
  1146. print "</tr>";
  1147. ++$lnum;
  1148. }
  1149. print "</table>";
  1150. print "</div>";
  1151. print "<footer>";
  1152. print "<button style='float : left' class=\"alt-danger\" dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('errorFeedsDlg').removeSelected()\">"
  1153. .__('Unsubscribe from selected feeds')."</button> ";
  1154. print "<button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('errorFeedsDlg').hide()\">".
  1155. __('Close this window')."</button>";
  1156. print "</footer>";
  1157. }
  1158. private function remove_feed_category($id, $owner_uid) {
  1159. $sth = $this->pdo->prepare("DELETE FROM ttrss_feed_categories
  1160. WHERE id = ? AND owner_uid = ?");
  1161. $sth->execute([$id, $owner_uid]);
  1162. CCache::remove($id, $owner_uid, true);
  1163. }
  1164. static function remove_feed($id, $owner_uid) {
  1165. foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_UNSUBSCRIBE_FEED) as $p) {
  1166. if (! $p->hook_unsubscribe_feed($id, $owner_uid)) {
  1167. user_error("Feed $id (owner: $owner_uid) not removed due to plugin error (HOOK_UNSUBSCRIBE_FEED).", E_USER_WARNING);
  1168. return;
  1169. }
  1170. }
  1171. $pdo = Db::pdo();
  1172. if ($id > 0) {
  1173. $pdo->beginTransaction();
  1174. /* save starred articles in Archived feed */
  1175. /* prepare feed if necessary */
  1176. $sth = $pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?
  1177. AND owner_uid = ?");
  1178. $sth->execute([$id, $owner_uid]);
  1179. if ($row = $sth->fetch()) {
  1180. $feed_url = $row["feed_url"];
  1181. $sth = $pdo->prepare("SELECT id FROM ttrss_archived_feeds
  1182. WHERE feed_url = ? AND owner_uid = ?");
  1183. $sth->execute([$feed_url, $owner_uid]);
  1184. if ($row = $sth->fetch()) {
  1185. $archive_id = $row["id"];
  1186. } else {
  1187. $res = $pdo->query("SELECT MAX(id) AS id FROM ttrss_archived_feeds");
  1188. $row = $res->fetch();
  1189. $new_feed_id = (int)$row['id'] + 1;
  1190. $sth = $pdo->prepare("INSERT INTO ttrss_archived_feeds
  1191. (id, owner_uid, title, feed_url, site_url, created)
  1192. SELECT ?, owner_uid, title, feed_url, site_url, NOW() from ttrss_feeds
  1193. WHERE id = ?");
  1194. $sth->execute([$new_feed_id, $id]);
  1195. $archive_id = $new_feed_id;
  1196. }
  1197. $sth = $pdo->prepare("UPDATE ttrss_user_entries SET feed_id = NULL,
  1198. orig_feed_id = ? WHERE feed_id = ? AND
  1199. marked = true AND owner_uid = ?");
  1200. $sth->execute([$archive_id, $id, $owner_uid]);
  1201. /* Remove access key for the feed */
  1202. $sth = $pdo->prepare("DELETE FROM ttrss_access_keys WHERE
  1203. feed_id = ? AND owner_uid = ?");
  1204. $sth->execute([$id, $owner_uid]);
  1205. /* remove the feed */
  1206. $sth = $pdo->prepare("DELETE FROM ttrss_feeds
  1207. WHERE id = ? AND owner_uid = ?");
  1208. $sth->execute([$id, $owner_uid]);
  1209. }
  1210. $pdo->commit();
  1211. if (file_exists(ICONS_DIR . "/$id.ico")) {
  1212. unlink(ICONS_DIR . "/$id.ico");
  1213. }
  1214. CCache::remove($id, $owner_uid);
  1215. } else {
  1216. Labels::remove(Labels::feed_to_label_id($id), $owner_uid);
  1217. //CCache::remove($id, $owner_uid); don't think labels are cached
  1218. }
  1219. }
  1220. function batchSubscribe() {
  1221. print_hidden("op", "pref-feeds");
  1222. print_hidden("method", "batchaddfeeds");
  1223. print "<header class='horizontal'>".__("One valid feed per line (no detection is done)")."</header>";
  1224. print "<section>";
  1225. print "<textarea
  1226. style='font-size : 12px; width : 98%; height: 200px;'
  1227. dojoType='dijit.form.SimpleTextarea' name='feeds'></textarea>";
  1228. if (get_pref('ENABLE_FEED_CATS')) {
  1229. print "<fieldset>";
  1230. print "<label>" . __('Place in category:') . "</label> ";
  1231. print_feed_cat_select("cat", false, 'dojoType="dijit.form.Select"');
  1232. print "</fieldset>";
  1233. }
  1234. print "</section>";
  1235. print "<div id='feedDlg_loginContainer' style='display : none'>";
  1236. print "<header>" . __("Authentication") . "</header>";
  1237. print "<section>";
  1238. print "<input dojoType='dijit.form.TextBox' name='login' placeHolder=\"".__("Login")."\">
  1239. <input placeHolder=\"".__("Password")."\" dojoType=\"dijit.form.TextBox\" type='password'
  1240. autocomplete='new-password' name='pass''></div>";
  1241. print "</section>";
  1242. print "</div>";
  1243. print "<fieldset class='narrow'>
  1244. <label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox'
  1245. onclick='displayIfChecked(this, \"feedDlg_loginContainer\")'> ".
  1246. __('Feeds require authentication.')."</label></div>";
  1247. print "</fieldset>";
  1248. print "<footer>
  1249. <button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick=\"return dijit.byId('batchSubDlg').execute()\">".__('Subscribe')."</button>
  1250. <button dojoType='dijit.form.Button' onclick=\"return dijit.byId('batchSubDlg').hide()\">".__('Cancel')."</button>
  1251. </footer>";
  1252. }
  1253. function batchAddFeeds() {
  1254. $cat_id = clean($_REQUEST['cat']);
  1255. $feeds = explode("\n", clean($_REQUEST['feeds']));
  1256. $login = clean($_REQUEST['login']);
  1257. $pass = trim(clean($_REQUEST['pass']));
  1258. foreach ($feeds as $feed) {
  1259. $feed = trim($feed);
  1260. if (validate_feed_url($feed)) {
  1261. $this->pdo->beginTransaction();
  1262. $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
  1263. WHERE feed_url = ? AND owner_uid = ?");
  1264. $sth->execute([$feed, $_SESSION['uid']]);
  1265. if (!$sth->fetch()) {
  1266. $sth = $this->pdo->prepare("INSERT INTO ttrss_feeds
  1267. (owner_uid,feed_url,title,cat_id,auth_login,auth_pass,update_method,auth_pass_encrypted)
  1268. VALUES (?, ?, '[Unknown]', ?, ?, ?, 0, false)");
  1269. $sth->execute([$_SESSION['uid'], $feed, $cat_id ? $cat_id : null, $login, $pass]);
  1270. }
  1271. $this->pdo->commit();
  1272. }
  1273. }
  1274. }
  1275. function regenOPMLKey() {
  1276. $this->update_feed_access_key('OPML:Publish',
  1277. false, $_SESSION["uid"]);
  1278. $new_link = Opml::opml_publish_url();
  1279. print json_encode(array("link" => $new_link));
  1280. }
  1281. function regenFeedKey() {
  1282. $feed_id = clean($_REQUEST['id']);
  1283. $is_cat = clean($_REQUEST['is_cat']);
  1284. $new_key = $this->update_feed_access_key($feed_id, $is_cat);
  1285. print json_encode(["link" => $new_key]);
  1286. }
  1287. private function update_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
  1288. if (!$owner_uid) $owner_uid = $_SESSION["uid"];
  1289. // clear old value and generate new one
  1290. $sth = $this->pdo->prepare("DELETE FROM ttrss_access_keys
  1291. WHERE feed_id = ? AND is_cat = ? AND owner_uid = ?");
  1292. $sth->execute([$feed_id, bool_to_sql_bool($is_cat), $owner_uid]);
  1293. return get_feed_access_key($feed_id, $is_cat, $owner_uid);
  1294. }
  1295. // Silent
  1296. function clearKeys() {
  1297. $sth = $this->pdo->prepare("DELETE FROM ttrss_access_keys WHERE
  1298. owner_uid = ?");
  1299. $sth->execute([$_SESSION['uid']]);
  1300. }
  1301. private function calculate_children_count($cat) {
  1302. $c = 0;
  1303. foreach ($cat['items'] as $child) {
  1304. if ($child['type'] == 'category') {
  1305. $c += $this->calculate_children_count($child);
  1306. } else {
  1307. $c += 1;
  1308. }
  1309. }
  1310. return $c;
  1311. }
  1312. function getinactivefeeds() {
  1313. if (DB_TYPE == "pgsql") {
  1314. $interval_qpart = "NOW() - INTERVAL '3 months'";
  1315. } else {
  1316. $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
  1317. }
  1318. $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_inactive FROM ttrss_feeds WHERE
  1319. (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE
  1320. ttrss_entries.id = ref_id AND
  1321. ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart AND
  1322. ttrss_feeds.owner_uid = ?");
  1323. $sth->execute([$_SESSION['uid']]);
  1324. if ($row = $sth->fetch()) {
  1325. print (int)$row["num_inactive"];
  1326. }
  1327. }
  1328. static function subscribe_to_feed_url() {
  1329. $url_path = get_self_url_prefix() .
  1330. "/public.php?op=subscribe&feed_url=%s";
  1331. return $url_path;
  1332. }
  1333. }