feeds.php 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802
  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'] = -1;
  54. $cat['child_unread'] = -1;
  55. $cat['auxcounter'] = -1;
  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'] = -1;
  76. $feed['name'] = $feed_line['title'];
  77. $feed['checkbox'] = false;
  78. $feed['unread'] = -1;
  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'] = -1;
  120. $item['name'] = $feed['title'];
  121. $item['checkbox'] = false;
  122. $item['error'] = '';
  123. $item['icon'] = $feed['icon'];
  124. $item['param'] = '';
  125. $item['unread'] = -1;
  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'] = -1;
  171. $cat['name'] = $line['title'];
  172. $cat['items'] = array();
  173. $cat['checkbox'] = false;
  174. $cat['type'] = 'category';
  175. $cat['unread'] = -1;
  176. $cat['child_unread'] = -1;
  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'] = -1;
  189. $cat['name'] = __("Uncategorized");
  190. $cat['items'] = array();
  191. $cat['type'] = 'category';
  192. $cat['checkbox'] = false;
  193. $cat['unread'] = -1;
  194. $cat['child_unread'] = -1;
  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'] = -1;
  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'] = -1;
  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'] = -1;
  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'] = -1;
  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. $rc = 2; // failed
  369. if (is_file($icon_file) && $feed_id) {
  370. if (filesize($icon_file) < 65535) {
  371. $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
  372. WHERE id = ? AND owner_uid = ?");
  373. $sth->execute([$feed_id, $_SESSION['uid']]);
  374. if ($row = $sth->fetch()) {
  375. @unlink(ICONS_DIR . "/$feed_id.ico");
  376. if (rename($icon_file, ICONS_DIR . "/$feed_id.ico")) {
  377. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET
  378. favicon_avg_color = ''
  379. WHERE id = ?");
  380. $sth->execute([$feed_id]);
  381. $rc = 0;
  382. }
  383. }
  384. } else {
  385. $rc = 1;
  386. }
  387. }
  388. if (is_file($icon_file)) @unlink($icon_file);
  389. print $rc;
  390. return;
  391. }
  392. function editfeed() {
  393. global $purge_intervals;
  394. global $update_intervals;
  395. $feed_id = clean($_REQUEST["id"]);
  396. $sth = $this->pdo->prepare("SELECT * FROM ttrss_feeds WHERE id = ? AND
  397. owner_uid = ?");
  398. $sth->execute([$feed_id, $_SESSION['uid']]);
  399. if ($row = $sth->fetch()) {
  400. print '<div dojoType="dijit.layout.TabContainer" style="height : 450px">
  401. <div dojoType="dijit.layout.ContentPane" title="'.__('General').'">';
  402. $title = htmlspecialchars($row["title"]);
  403. print_hidden("id", "$feed_id");
  404. print_hidden("op", "pref-feeds");
  405. print_hidden("method", "editSave");
  406. print "<header>".__("Feed")."</header>";
  407. print "<section>";
  408. /* Title */
  409. print "<fieldset>";
  410. print "<input dojoType='dijit.form.ValidationTextBox' required='1'
  411. placeHolder=\"".__("Feed Title")."\"
  412. style='font-size : 16px; width: 500px' name='title' value=\"$title\">";
  413. print "</fieldset>";
  414. /* Feed URL */
  415. $feed_url = htmlspecialchars($row["feed_url"]);
  416. print "<fieldset>";
  417. print "<label>" . __('URL:') . "</label> ";
  418. print "<input dojoType='dijit.form.ValidationTextBox' required='1'
  419. placeHolder=\"".__("Feed URL")."\"
  420. regExp='^(http|https)://.*' style='width : 300px'
  421. name='feed_url' value=\"$feed_url\">";
  422. $last_error = $row["last_error"];
  423. if ($last_error) {
  424. print "&nbsp;<i class=\"material-icons\"
  425. title=\"".htmlspecialchars($last_error)."\">error</i>";
  426. }
  427. print "</fieldset>";
  428. /* Category */
  429. if (get_pref('ENABLE_FEED_CATS')) {
  430. $cat_id = $row["cat_id"];
  431. print "<fieldset>";
  432. print "<label>" . __('Place in category:') . "</label> ";
  433. print_feed_cat_select("cat_id", $cat_id,
  434. 'dojoType="fox.form.Select"');
  435. print "</fieldset>";
  436. }
  437. /* Site URL */
  438. $site_url = htmlspecialchars($row["site_url"]);
  439. print "<fieldset>";
  440. print "<label>" . __('Site URL:') . "</label> ";
  441. print "<input dojoType='dijit.form.ValidationTextBox' required='1'
  442. placeHolder=\"".__("Site URL")."\"
  443. regExp='^(http|https)://.*' style='width : 300px'
  444. name='site_url' value=\"$site_url\">";
  445. print "</fieldset>";
  446. /* FTS Stemming Language */
  447. if (DB_TYPE == "pgsql") {
  448. $feed_language = $row["feed_language"];
  449. if (!$feed_language)
  450. $feed_language = get_pref('DEFAULT_SEARCH_LANGUAGE');
  451. print "<fieldset>";
  452. print "<label>" . __('Language:') . "</label> ";
  453. print_select("feed_language", $feed_language, $this::get_ts_languages(),
  454. 'dojoType="fox.form.Select"');
  455. print "</fieldset>";
  456. }
  457. print "</section>";
  458. print "<header>".__("Update")."</header>";
  459. print "<section>";
  460. /* Update Interval */
  461. $update_interval = $row["update_interval"];
  462. print "<fieldset>";
  463. print "<label>".__("Interval:")."</label> ";
  464. print_select_hash("update_interval", $update_interval, $update_intervals,
  465. 'dojoType="fox.form.Select"');
  466. print "</fieldset>";
  467. /* Purge intl */
  468. $purge_interval = $row["purge_interval"];
  469. print "<fieldset>";
  470. print "<label>" . __('Article purging:') . "</label> ";
  471. print_select_hash("purge_interval", $purge_interval, $purge_intervals,
  472. 'dojoType="fox.form.Select" ' .
  473. ((FORCE_ARTICLE_PURGE == 0) ? "" : 'disabled="1"'));
  474. print "</fieldset>";
  475. print "</section>";
  476. $auth_login = htmlspecialchars($row["auth_login"]);
  477. $auth_pass = htmlspecialchars($row["auth_pass"]);
  478. $auth_enabled = $auth_login !== '' || $auth_pass !== '';
  479. $auth_style = $auth_enabled ? '' : 'display: none';
  480. print "<div id='feedEditDlg_loginContainer' style='$auth_style'>";
  481. print "<header>".__("Authentication")."</header>";
  482. print "<section>";
  483. print "<fieldset>";
  484. print "<input dojoType='dijit.form.TextBox' id='feedEditDlg_login'
  485. placeHolder='".__("Login")."'
  486. autocomplete='new-password'
  487. name='auth_login' value=\"$auth_login\">";
  488. print "</fieldset><fieldset>";
  489. print "<input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
  490. autocomplete='new-password'
  491. placeHolder='".__("Password")."'
  492. value=\"$auth_pass\">";
  493. print "<div dojoType='dijit.Tooltip' connectId='feedEditDlg_login' position='below'>
  494. ".__('<b>Hint:</b> you need to fill in your login information if your feed requires authentication, except for Twitter feeds.')."
  495. </div>";
  496. print "</fieldset>";
  497. print "</section></div>";
  498. $auth_checked = $auth_enabled ? 'checked' : '';
  499. print "<label class='checkbox'>
  500. <input type='checkbox' $auth_checked name='need_auth' dojoType='dijit.form.CheckBox' id='feedEditDlg_loginCheck'
  501. onclick='displayIfChecked(this, \"feedEditDlg_loginContainer\")'>
  502. ".__('This feed requires authentication.')."</label>";
  503. print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Options').'">';
  504. print "<section class='narrow'>";
  505. $include_in_digest = $row["include_in_digest"];
  506. if ($include_in_digest) {
  507. $checked = "checked=\"1\"";
  508. } else {
  509. $checked = "";
  510. }
  511. print "<fieldset class='narrow'>";
  512. print "<label class='checkbox'><input dojoType=\"dijit.form.CheckBox\" type=\"checkbox\" id=\"include_in_digest\"
  513. name=\"include_in_digest\"
  514. $checked> ".__('Include in e-mail digest')."</label>";
  515. print "</fieldset>";
  516. $always_display_enclosures = $row["always_display_enclosures"];
  517. if ($always_display_enclosures) {
  518. $checked = "checked";
  519. } else {
  520. $checked = "";
  521. }
  522. print "<fieldset class='narrow'>";
  523. print "<label class='checkbox'><input dojoType=\"dijit.form.CheckBox\" type=\"checkbox\" id=\"always_display_enclosures\"
  524. name=\"always_display_enclosures\"
  525. $checked> ".__('Always display image attachments')."</label>";
  526. print "</fieldset>";
  527. $hide_images = $row["hide_images"];
  528. if ($hide_images) {
  529. $checked = "checked=\"1\"";
  530. } else {
  531. $checked = "";
  532. }
  533. print "<fieldset class='narrow'>";
  534. print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='hide_images'
  535. name='hide_images' $checked> ".__('Do not embed media')."</label>";
  536. print "</fieldset>";
  537. $cache_images = $row["cache_images"];
  538. if ($cache_images) {
  539. $checked = "checked=\"1\"";
  540. } else {
  541. $checked = "";
  542. }
  543. print "<fieldset class='narrow'>";
  544. print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='cache_images'
  545. name='cache_images' $checked> ". __('Cache media')."</label>";
  546. print "</fieldset>";
  547. $mark_unread_on_update = $row["mark_unread_on_update"];
  548. if ($mark_unread_on_update) {
  549. $checked = "checked";
  550. } else {
  551. $checked = "";
  552. }
  553. print "<fieldset class='narrow'>";
  554. print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='mark_unread_on_update'
  555. name='mark_unread_on_update' $checked> ".__('Mark updated articles as unread')."</label>";
  556. print "</fieldset>";
  557. print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Icon').'">';
  558. /* Icon */
  559. print "<img class='feedIcon feed-editor-icon' src=\"".Feeds::getFeedIcon($feed_id)."\">";
  560. print "<form onsubmit='return false;' id='feed_icon_upload_form'
  561. enctype='multipart/form-data' method='POST'>
  562. <label class='dijitButton'>".__("Choose file...")."
  563. <input style='display: none' id='icon_file' size='10' name='icon_file' type='file'>
  564. </label>
  565. <input type='hidden' name='op' value='pref-feeds'>
  566. <input type='hidden' name='feed_id' value='$feed_id'>
  567. <input type='hidden' name='method' value='uploadicon'>
  568. <button dojoType='dijit.form.Button' onclick=\"return CommonDialogs.uploadFeedIcon();\"
  569. type='submit'>".__('Replace')."</button>
  570. <button class='alt-danger' dojoType='dijit.form.Button' onclick=\"return CommonDialogs.removeFeedIcon($feed_id);\"
  571. type='submit'>".__('Remove')."</button>
  572. </form>";
  573. print "</section>";
  574. print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Plugins').'">';
  575. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_EDIT_FEED,
  576. "hook_prefs_edit_feed", $feed_id);
  577. print "</div></div>";
  578. $title = htmlspecialchars($title, ENT_QUOTES);
  579. print "<footer>
  580. <button style='float : left' class='alt-danger' dojoType='dijit.form.Button' onclick='return CommonDialogs.unsubscribeFeed($feed_id, \"$title\")'>".
  581. __('Unsubscribe')."</button>
  582. <button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return dijit.byId('feedEditDlg').execute()\">".__('Save')."</button>
  583. <button dojoType='dijit.form.Button' onclick=\"return dijit.byId('feedEditDlg').hide()\">".__('Cancel')."</button>
  584. </footer>";
  585. }
  586. }
  587. function editfeeds() {
  588. global $purge_intervals;
  589. global $update_intervals;
  590. $feed_ids = clean($_REQUEST["ids"]);
  591. print_notice("Enable the options you wish to apply using checkboxes on the right:");
  592. print "<p>";
  593. print_hidden("ids", "$feed_ids");
  594. print_hidden("op", "pref-feeds");
  595. print_hidden("method", "batchEditSave");
  596. print "<header>".__("Feed")."</header>";
  597. print "<section>";
  598. /* Category */
  599. if (get_pref('ENABLE_FEED_CATS')) {
  600. print "<fieldset>";
  601. print "<label>" . __('Place in category:') . "</label> ";
  602. print_feed_cat_select("cat_id", false,
  603. 'disabled="1" dojoType="fox.form.Select"');
  604. $this->batch_edit_cbox("cat_id");
  605. print "</fieldset>";
  606. }
  607. /* FTS Stemming Language */
  608. if (DB_TYPE == "pgsql") {
  609. print "<fieldset>";
  610. print "<label>" . __('Language:') . "</label> ";
  611. print_select("feed_language", "", $this::get_ts_languages(),
  612. 'disabled="1" dojoType="fox.form.Select"');
  613. $this->batch_edit_cbox("feed_language");
  614. print "</fieldset>";
  615. }
  616. print "</section>";
  617. print "<header>".__("Update")."</header>";
  618. print "<section>";
  619. /* Update Interval */
  620. print "<fieldset>";
  621. print "<label>".__("Interval:")."</label> ";
  622. print_select_hash("update_interval", "", $update_intervals,
  623. 'disabled="1" dojoType="fox.form.Select"');
  624. $this->batch_edit_cbox("update_interval");
  625. print "</fieldset>";
  626. /* Purge intl */
  627. if (FORCE_ARTICLE_PURGE == 0) {
  628. print "<fieldset>";
  629. print "<label>" . __('Article purging:') . "</label> ";
  630. print_select_hash("purge_interval", "", $purge_intervals,
  631. 'disabled="1" dojoType="fox.form.Select"');
  632. $this->batch_edit_cbox("purge_interval");
  633. print "</fieldset>";
  634. }
  635. print "</section>";
  636. print "<header>".__("Authentication")."</header>";
  637. print "<section>";
  638. print "<fieldset>";
  639. print "<input dojoType='dijit.form.TextBox'
  640. placeHolder=\"".__("Login")."\" disabled='1'
  641. autocomplete='new-password'
  642. name='auth_login' value=''>";
  643. $this->batch_edit_cbox("auth_login");
  644. print "<input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
  645. autocomplete='new-password'
  646. placeHolder=\"".__("Password")."\" disabled='1'
  647. value=''>";
  648. $this->batch_edit_cbox("auth_pass");
  649. print "</fieldset>";
  650. print "</section>";
  651. print "<header>".__("Options")."</header>";
  652. print "<section>";
  653. print "<fieldset class='narrow'>";
  654. print "<label class='checkbox'><input disabled='1' type='checkbox' id='include_in_digest'
  655. name='include_in_digest' dojoType='dijit.form.CheckBox'>&nbsp;".__('Include in e-mail digest')."</label>";
  656. print "&nbsp;"; $this->batch_edit_cbox("include_in_digest", "include_in_digest_l");
  657. print "</fieldset><fieldset class='narrow'>";
  658. print "<label class='checkbox'><input disabled='1' type='checkbox' id='always_display_enclosures'
  659. name='always_display_enclosures' dojoType='dijit.form.CheckBox'>&nbsp;".__('Always display image attachments')."</label>";
  660. print "&nbsp;"; $this->batch_edit_cbox("always_display_enclosures", "always_display_enclosures_l");
  661. print "</fieldset><fieldset class='narrow'>";
  662. print "<label class='checkbox'><input disabled='1' type='checkbox' id='hide_images'
  663. name='hide_images' dojoType='dijit.form.CheckBox'>&nbsp;". __('Do not embed media')."</label>";
  664. print "&nbsp;"; $this->batch_edit_cbox("hide_images", "hide_images_l");
  665. print "</fieldset><fieldset class='narrow'>";
  666. print "<label class='checkbox'><input disabled='1' type='checkbox' id='cache_images'
  667. name='cache_images' dojoType='dijit.form.CheckBox'>&nbsp;".__('Cache media')."</label>";
  668. print "&nbsp;"; $this->batch_edit_cbox("cache_images", "cache_images_l");
  669. print "</fieldset><fieldset class='narrow'>";
  670. print "<label class='checkbox'><input disabled='1' type='checkbox' id='mark_unread_on_update'
  671. name='mark_unread_on_update' dojoType='dijit.form.CheckBox'>&nbsp;".__('Mark updated articles as unread')."</label>";
  672. print "&nbsp;"; $this->batch_edit_cbox("mark_unread_on_update", "mark_unread_on_update_l");
  673. print "</fieldset>";
  674. print "</section>";
  675. print "<footer>
  676. <button dojoType='dijit.form.Button' type='submit' class='alt-primary'
  677. onclick=\"return dijit.byId('feedEditDlg').execute()\">".
  678. __('Save')."</button>
  679. <button dojoType='dijit.form.Button'
  680. onclick=\"return dijit.byId('feedEditDlg').hide()\">".
  681. __('Cancel')."</button>
  682. </footer>";
  683. return;
  684. }
  685. function batchEditSave() {
  686. return $this->editsaveops(true);
  687. }
  688. function editSave() {
  689. return $this->editsaveops(false);
  690. }
  691. function editsaveops($batch) {
  692. $feed_title = trim(clean($_POST["title"]));
  693. $feed_url = trim(clean($_POST["feed_url"]));
  694. $site_url = trim(clean($_POST["site_url"]));
  695. $upd_intl = (int) clean($_POST["update_interval"]);
  696. $purge_intl = (int) clean($_POST["purge_interval"]);
  697. $feed_id = (int) clean($_POST["id"]); /* editSave */
  698. $feed_ids = explode(",", clean($_POST["ids"])); /* batchEditSave */
  699. $cat_id = (int) clean($_POST["cat_id"]);
  700. $auth_login = trim(clean($_POST["auth_login"]));
  701. $auth_pass = trim(clean($_POST["auth_pass"]));
  702. $private = checkbox_to_sql_bool(clean($_POST["private"]));
  703. $include_in_digest = checkbox_to_sql_bool(
  704. clean($_POST["include_in_digest"]));
  705. $cache_images = checkbox_to_sql_bool(
  706. clean($_POST["cache_images"]));
  707. $hide_images = checkbox_to_sql_bool(
  708. clean($_POST["hide_images"]));
  709. $always_display_enclosures = checkbox_to_sql_bool(
  710. clean($_POST["always_display_enclosures"]));
  711. $mark_unread_on_update = checkbox_to_sql_bool(
  712. clean($_POST["mark_unread_on_update"]));
  713. $feed_language = trim(clean($_POST["feed_language"]));
  714. if (!$batch) {
  715. if (clean($_POST["need_auth"]) !== 'on') {
  716. $auth_login = '';
  717. $auth_pass = '';
  718. }
  719. /* $sth = $this->pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?");
  720. $sth->execute([$feed_id]);
  721. $row = $sth->fetch();$orig_feed_url = $row["feed_url"];
  722. $reset_basic_info = $orig_feed_url != $feed_url; */
  723. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET
  724. cat_id = :cat_id,
  725. title = :title,
  726. feed_url = :feed_url,
  727. site_url = :site_url,
  728. update_interval = :upd_intl,
  729. purge_interval = :purge_intl,
  730. auth_login = :auth_login,
  731. auth_pass = :auth_pass,
  732. auth_pass_encrypted = false,
  733. private = :private,
  734. cache_images = :cache_images,
  735. hide_images = :hide_images,
  736. include_in_digest = :include_in_digest,
  737. always_display_enclosures = :always_display_enclosures,
  738. mark_unread_on_update = :mark_unread_on_update,
  739. feed_language = :feed_language
  740. WHERE id = :id AND owner_uid = :uid");
  741. $sth->execute([":title" => $feed_title,
  742. ":cat_id" => $cat_id ? $cat_id : null,
  743. ":feed_url" => $feed_url,
  744. ":site_url" => $site_url,
  745. ":upd_intl" => $upd_intl,
  746. ":purge_intl" => $purge_intl,
  747. ":auth_login" => $auth_login,
  748. ":auth_pass" => $auth_pass,
  749. ":private" => (int)$private,
  750. ":cache_images" => (int)$cache_images,
  751. ":hide_images" => (int)$hide_images,
  752. ":include_in_digest" => (int)$include_in_digest,
  753. ":always_display_enclosures" => (int)$always_display_enclosures,
  754. ":mark_unread_on_update" => (int)$mark_unread_on_update,
  755. ":feed_language" => $feed_language,
  756. ":id" => $feed_id,
  757. ":uid" => $_SESSION['uid']]);
  758. /* if ($reset_basic_info) {
  759. RSSUtils::set_basic_feed_info($feed_id);
  760. } */
  761. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_SAVE_FEED,
  762. "hook_prefs_save_feed", $feed_id);
  763. } else {
  764. $feed_data = array();
  765. foreach (array_keys($_POST) as $k) {
  766. if ($k != "op" && $k != "method" && $k != "ids") {
  767. $feed_data[$k] = clean($_POST[$k]);
  768. }
  769. }
  770. $this->pdo->beginTransaction();
  771. $feed_ids_qmarks = arr_qmarks($feed_ids);
  772. foreach (array_keys($feed_data) as $k) {
  773. $qpart = "";
  774. switch ($k) {
  775. case "title":
  776. $qpart = "title = " . $this->pdo->quote($feed_title);
  777. break;
  778. case "feed_url":
  779. $qpart = "feed_url = " . $this->pdo->quote($feed_url);
  780. break;
  781. case "update_interval":
  782. $qpart = "update_interval = " . $this->pdo->quote($upd_intl);
  783. break;
  784. case "purge_interval":
  785. $qpart = "purge_interval =" . $this->pdo->quote($purge_intl);
  786. break;
  787. case "auth_login":
  788. $qpart = "auth_login = " . $this->pdo->quote($auth_login);
  789. break;
  790. case "auth_pass":
  791. $qpart = "auth_pass =" . $this->pdo->quote($auth_pass). ", auth_pass_encrypted = false";
  792. break;
  793. case "private":
  794. $qpart = "private = " . $this->pdo->quote($private);
  795. break;
  796. case "include_in_digest":
  797. $qpart = "include_in_digest = " . $this->pdo->quote($include_in_digest);
  798. break;
  799. case "always_display_enclosures":
  800. $qpart = "always_display_enclosures = " . $this->pdo->quote($always_display_enclosures);
  801. break;
  802. case "mark_unread_on_update":
  803. $qpart = "mark_unread_on_update = " . $this->pdo->quote($mark_unread_on_update);
  804. break;
  805. case "cache_images":
  806. $qpart = "cache_images = " . $this->pdo->quote($cache_images);
  807. break;
  808. case "hide_images":
  809. $qpart = "hide_images = " . $this->pdo->quote($hide_images);
  810. break;
  811. case "cat_id":
  812. if (get_pref('ENABLE_FEED_CATS')) {
  813. if ($cat_id) {
  814. $qpart = "cat_id = " . $this->pdo->quote($cat_id);
  815. } else {
  816. $qpart = 'cat_id = NULL';
  817. }
  818. } else {
  819. $qpart = "";
  820. }
  821. break;
  822. case "feed_language":
  823. $qpart = "feed_language = " . $this->pdo->quote($feed_language);
  824. break;
  825. }
  826. if ($qpart) {
  827. $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET $qpart WHERE id IN ($feed_ids_qmarks)
  828. AND owner_uid = ?");
  829. $sth->execute(array_merge($feed_ids, [$_SESSION['uid']]));
  830. }
  831. }
  832. $this->pdo->commit();
  833. }
  834. return;
  835. }
  836. function remove() {
  837. $ids = explode(",", clean($_REQUEST["ids"]));
  838. foreach ($ids as $id) {
  839. Pref_Feeds::remove_feed($id, $_SESSION["uid"]);
  840. }
  841. return;
  842. }
  843. function removeCat() {
  844. $ids = explode(",", clean($_REQUEST["ids"]));
  845. foreach ($ids as $id) {
  846. $this->remove_feed_category($id, $_SESSION["uid"]);
  847. }
  848. }
  849. function addCat() {
  850. $feed_cat = trim(clean($_REQUEST["cat"]));
  851. Feeds::add_feed_category($feed_cat);
  852. }
  853. function index() {
  854. print "<div dojoType='dijit.layout.AccordionContainer' region='center'>";
  855. print "<div style='padding : 0px' dojoType='dijit.layout.AccordionPane'
  856. title=\"<i class='material-icons'>rss_feed</i> ".__('Feeds')."\">";
  857. $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors
  858. FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
  859. $sth->execute([$_SESSION['uid']]);
  860. if ($row = $sth->fetch()) {
  861. $num_errors = $row["num_errors"];
  862. } else {
  863. $num_errors = 0;
  864. }
  865. if ($num_errors > 0) {
  866. $error_button = "<button dojoType=\"dijit.form.Button\"
  867. onclick=\"CommonDialogs.showFeedsWithErrors()\" id=\"errorButton\">" .
  868. __("Feeds with errors") . "</button>";
  869. }
  870. $inactive_button = "<button dojoType=\"dijit.form.Button\"
  871. id=\"pref_feeds_inactive_btn\"
  872. style=\"display : none\"
  873. onclick=\"dijit.byId('feedTree').showInactiveFeeds()\">" .
  874. __("Inactive feeds") . "</button>";
  875. $feed_search = clean($_REQUEST["search"]);
  876. if (array_key_exists("search", $_REQUEST)) {
  877. $_SESSION["prefs_feed_search"] = $feed_search;
  878. } else {
  879. $feed_search = $_SESSION["prefs_feed_search"];
  880. }
  881. print '<div dojoType="dijit.layout.BorderContainer" gutters="false">';
  882. print "<div region='top' dojoType=\"fox.Toolbar\">"; #toolbar
  883. print "<div style='float : right; padding-right : 4px;'>
  884. <input dojoType=\"dijit.form.TextBox\" id=\"feed_search\" size=\"20\" type=\"search\"
  885. value=\"$feed_search\">
  886. <button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('feedTree').reload()\">".
  887. __('Search')."</button>
  888. </div>";
  889. print "<div dojoType=\"fox.form.DropDownButton\">".
  890. "<span>" . __('Select')."</span>";
  891. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  892. print "<div onclick=\"dijit.byId('feedTree').model.setAllChecked(true)\"
  893. dojoType=\"dijit.MenuItem\">".__('All')."</div>";
  894. print "<div onclick=\"dijit.byId('feedTree').model.setAllChecked(false)\"
  895. dojoType=\"dijit.MenuItem\">".__('None')."</div>";
  896. print "</div></div>";
  897. print "<div dojoType=\"fox.form.DropDownButton\">".
  898. "<span>" . __('Feeds')."</span>";
  899. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  900. print "<div onclick=\"CommonDialogs.quickAddFeed()\"
  901. dojoType=\"dijit.MenuItem\">".__('Subscribe to feed')."</div>";
  902. print "<div onclick=\"dijit.byId('feedTree').editSelectedFeed()\"
  903. dojoType=\"dijit.MenuItem\">".__('Edit selected feeds')."</div>";
  904. print "<div onclick=\"dijit.byId('feedTree').resetFeedOrder()\"
  905. dojoType=\"dijit.MenuItem\">".__('Reset sort order')."</div>";
  906. print "<div onclick=\"dijit.byId('feedTree').batchSubscribe()\"
  907. dojoType=\"dijit.MenuItem\">".__('Batch subscribe')."</div>";
  908. print "<div dojoType=\"dijit.MenuItem\" onclick=\"dijit.byId('feedTree').removeSelectedFeeds()\">"
  909. .__('Unsubscribe')."</div> ";
  910. print "</div></div>";
  911. if (get_pref('ENABLE_FEED_CATS')) {
  912. print "<div dojoType=\"fox.form.DropDownButton\">".
  913. "<span>" . __('Categories')."</span>";
  914. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  915. print "<div onclick=\"dijit.byId('feedTree').createCategory()\"
  916. dojoType=\"dijit.MenuItem\">".__('Add category')."</div>";
  917. print "<div onclick=\"dijit.byId('feedTree').resetCatOrder()\"
  918. dojoType=\"dijit.MenuItem\">".__('Reset sort order')."</div>";
  919. print "<div onclick=\"dijit.byId('feedTree').removeSelectedCategories()\"
  920. dojoType=\"dijit.MenuItem\">".__('Remove selected')."</div>";
  921. print "</div></div>";
  922. }
  923. print $error_button;
  924. print $inactive_button;
  925. print "</div>"; # toolbar
  926. //print '</div>';
  927. print '<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">';
  928. print "<div id=\"feedlistLoading\">
  929. <img src='images/indicator_tiny.gif'>".
  930. __("Loading, please wait...")."</div>";
  931. $auto_expand = $feed_search != "" ? "true" : "false";
  932. print "<div dojoType=\"fox.PrefFeedStore\" jsId=\"feedStore\"
  933. url=\"backend.php?op=pref-feeds&method=getfeedtree\">
  934. </div>
  935. <div dojoType=\"lib.CheckBoxStoreModel\" jsId=\"feedModel\" store=\"feedStore\"
  936. query=\"{id:'root'}\" rootId=\"root\" rootLabel=\"Feeds\"
  937. childrenAttrs=\"items\" checkboxStrict=\"false\" checkboxAll=\"false\">
  938. </div>
  939. <div dojoType=\"fox.PrefFeedTree\" id=\"feedTree\"
  940. dndController=\"dijit.tree.dndSource\"
  941. betweenThreshold=\"5\"
  942. autoExpand='$auto_expand'
  943. model=\"feedModel\" openOnClick=\"false\">
  944. <script type=\"dojo/method\" event=\"onClick\" args=\"item\">
  945. var id = String(item.id);
  946. var bare_id = id.substr(id.indexOf(':')+1);
  947. if (id.match('FEED:')) {
  948. CommonDialogs.editFeed(bare_id);
  949. } else if (id.match('CAT:')) {
  950. dijit.byId('feedTree').editCategory(bare_id, item);
  951. }
  952. </script>
  953. <script type=\"dojo/method\" event=\"onLoad\" args=\"item\">
  954. Element.hide(\"feedlistLoading\");
  955. dijit.byId('feedTree').checkInactiveFeeds();
  956. </script>
  957. </div>";
  958. # print "<div dojoType=\"dijit.Tooltip\" connectId=\"feedTree\" position=\"below\">
  959. # ".__('<b>Hint:</b> you can drag feeds and categories around.')."
  960. # </div>";
  961. print '</div>';
  962. print '</div>';
  963. print "</div>"; # feeds pane
  964. print "<div dojoType='dijit.layout.AccordionPane'
  965. title='<i class=\"material-icons\">import_export</i> ".__('OPML')."'>";
  966. print "<h3>" . __("Using OPML you can export and import your feeds, filters, labels and Tiny Tiny RSS settings.") . "</h3>";
  967. print_notice("Only main settings profile can be migrated using OPML.");
  968. print "<iframe id=\"upload_iframe\"
  969. name=\"upload_iframe\" onload=\"Helpers.OPML.onImportComplete(this)\"
  970. style=\"width: 400px; height: 100px; display: none;\"></iframe>";
  971. print "<form name='opml_form' style='display : inline-block' target='upload_iframe'
  972. enctype='multipart/form-data' method='POST'
  973. action='backend.php'>
  974. <label class='dijitButton'>".__("Choose file...")."
  975. <input style='display : none' id='opml_file' name='opml_file' type='file'>&nbsp;
  976. </label>
  977. <input type='hidden' name='op' value='dlg'>
  978. <input type='hidden' name='method' value='importOpml'>
  979. <button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return Helpers.OPML.import();\" type=\"submit\">" .
  980. __('Import OPML') . "</button>";
  981. print "</form>";
  982. print "<form dojoType='dijit.form.Form' id='opmlExportForm' style='display : inline-block'>";
  983. print "<button dojoType='dijit.form.Button'
  984. onclick='Helpers.OPML.export()' >" .
  985. __('Export OPML') . "</button>";
  986. print " <label class='checkbox'>";
  987. print_checkbox("include_settings", true, "1", "");
  988. print " " . __("Include settings");
  989. print "</label>";
  990. print "</form>";
  991. print "<p/>";
  992. print "<h2>" . __("Published OPML") . "</h2>";
  993. print "<p>" . __('Your OPML can be published publicly and can be subscribed by anyone who knows the URL below.') .
  994. " " .
  995. __("Published OPML does not include your Tiny Tiny RSS settings, feeds that require authentication or feeds hidden from Popular feeds.") . "</p>";
  996. print "<button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return App.displayDlg('".__("Public OPML URL")."','pubOPMLUrl')\">".
  997. __('Display published OPML URL')."</button> ";
  998. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  999. "hook_prefs_tab_section", "prefFeedsOPML");
  1000. print "</div>"; # pane
  1001. print "<div dojoType=\"dijit.layout.AccordionPane\"
  1002. title=\"<i class='material-icons'>share</i> ".__('Published & shared articles / Generated feeds')."\">";
  1003. print "<h3>" . __('Published articles can be subscribed by anyone who knows the following URL:') . "</h3>";
  1004. $rss_url = '-2::' . htmlspecialchars(get_self_url_prefix() .
  1005. "/public.php?op=rss&id=-2&view-mode=all_articles");;
  1006. print "<button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return App.displayDlg('".__("Show as feed")."','generatedFeed', '$rss_url')\">".
  1007. __('Display URL')."</button> ";
  1008. print "<button class=\"alt-danger\" dojoType=\"dijit.form.Button\" onclick=\"return Helpers.clearFeedAccessKeys()\">".
  1009. __('Clear all generated URLs')."</button> ";
  1010. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  1011. "hook_prefs_tab_section", "prefFeedsPublishedGenerated");
  1012. print "</div>"; #pane
  1013. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB,
  1014. "hook_prefs_tab", "prefFeeds");
  1015. print "</div>"; #container
  1016. }
  1017. private function feedlist_init_cat($cat_id) {
  1018. $obj = array();
  1019. $cat_id = (int) $cat_id;
  1020. if ($cat_id > 0) {
  1021. $cat_unread = CCache::find($cat_id, $_SESSION["uid"], true);
  1022. } else if ($cat_id == 0 || $cat_id == -2) {
  1023. $cat_unread = Feeds::getCategoryUnread($cat_id);
  1024. }
  1025. $obj['id'] = 'CAT:' . $cat_id;
  1026. $obj['items'] = array();
  1027. $obj['name'] = Feeds::getCategoryTitle($cat_id);
  1028. $obj['type'] = 'category';
  1029. $obj['unread'] = (int) $cat_unread;
  1030. $obj['bare_id'] = $cat_id;
  1031. return $obj;
  1032. }
  1033. private function feedlist_init_feed($feed_id, $title = false, $unread = false, $error = '', $updated = '') {
  1034. $obj = array();
  1035. $feed_id = (int) $feed_id;
  1036. if (!$title)
  1037. $title = Feeds::getFeedTitle($feed_id, false);
  1038. if ($unread === false)
  1039. $unread = getFeedUnread($feed_id, false);
  1040. $obj['id'] = 'FEED:' . $feed_id;
  1041. $obj['name'] = $title;
  1042. $obj['unread'] = (int) $unread;
  1043. $obj['type'] = 'feed';
  1044. $obj['error'] = $error;
  1045. $obj['updated'] = $updated;
  1046. $obj['icon'] = Feeds::getFeedIcon($feed_id);
  1047. $obj['bare_id'] = $feed_id;
  1048. $obj['auxcounter'] = 0;
  1049. return $obj;
  1050. }
  1051. function inactiveFeeds() {
  1052. if (DB_TYPE == "pgsql") {
  1053. $interval_qpart = "NOW() - INTERVAL '3 months'";
  1054. } else {
  1055. $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
  1056. }
  1057. $sth = $this->pdo->prepare("SELECT ttrss_feeds.title, ttrss_feeds.site_url,
  1058. ttrss_feeds.feed_url, ttrss_feeds.id, MAX(updated) AS last_article
  1059. FROM ttrss_feeds, ttrss_entries, ttrss_user_entries WHERE
  1060. (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE
  1061. ttrss_entries.id = ref_id AND
  1062. ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart
  1063. AND ttrss_feeds.owner_uid = ? AND
  1064. ttrss_user_entries.feed_id = ttrss_feeds.id AND
  1065. ttrss_entries.id = ref_id
  1066. GROUP BY ttrss_feeds.title, ttrss_feeds.id, ttrss_feeds.site_url, ttrss_feeds.feed_url
  1067. ORDER BY last_article");
  1068. $sth->execute([$_SESSION['uid']]);
  1069. print "<div dojoType='fox.Toolbar'>";
  1070. print "<div dojoType='fox.form.DropDownButton'>".
  1071. "<span>" . __('Select')."</span>";
  1072. print "<div dojoType='dijit.Menu' style='display: none'>";
  1073. print "<div onclick=\"Tables.select('inactive-feeds-list', true)\"
  1074. dojoType='dijit.MenuItem'>".__('All')."</div>";
  1075. print "<div onclick=\"Tables.select('inactive-feeds-list', false)\"
  1076. dojoType='dijit.MenuItem'>".__('None')."</div>";
  1077. print "</div></div>";
  1078. print "</div>"; #toolbar
  1079. print "<div class='panel panel-scrollable'>";
  1080. print "<table width='100%' id='inactive-feeds-list'>";
  1081. $lnum = 1;
  1082. while ($line = $sth->fetch()) {
  1083. $feed_id = $line["id"];
  1084. print "<tr data-row-id='$feed_id'>";
  1085. print "<td width='5%' align='center'><input
  1086. onclick='Tables.onRowChecked(this);' dojoType='dijit.form.CheckBox'
  1087. type='checkbox'></td>";
  1088. print "<td>";
  1089. print "<a href='#' ".
  1090. "title=\"".__("Click to edit feed")."\" ".
  1091. "onclick=\"CommonDialogs.editFeed(".$line["id"].")\">".
  1092. htmlspecialchars($line["title"])."</a>";
  1093. print "</td><td class='text-muted' align='right'>";
  1094. print make_local_datetime($line['last_article'], false);
  1095. print "</td>";
  1096. print "</tr>";
  1097. ++$lnum;
  1098. }
  1099. print "</table>";
  1100. print "</div>";
  1101. print "<footer>
  1102. <button style='float : left' class=\"alt-danger\" dojoType='dijit.form.Button' onclick=\"dijit.byId('inactiveFeedsDlg').removeSelected()\">"
  1103. .__('Unsubscribe from selected feeds')."</button>
  1104. <button dojoType='dijit.form.Button' onclick=\"dijit.byId('inactiveFeedsDlg').hide()\">"
  1105. .__('Close this window')."</button>
  1106. </footer>";
  1107. }
  1108. function feedsWithErrors() {
  1109. $sth = $this->pdo->prepare("SELECT id,title,feed_url,last_error,site_url
  1110. FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
  1111. $sth->execute([$_SESSION['uid']]);
  1112. print "<div dojoType=\"fox.Toolbar\">";
  1113. print "<div dojoType=\"fox.form.DropDownButton\">".
  1114. "<span>" . __('Select')."</span>";
  1115. print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
  1116. print "<div onclick=\"Tables.select('error-feeds-list', true)\"
  1117. dojoType=\"dijit.MenuItem\">".__('All')."</div>";
  1118. print "<div onclick=\"Tables.select('error-feeds-list', false)\"
  1119. dojoType=\"dijit.MenuItem\">".__('None')."</div>";
  1120. print "</div></div>";
  1121. print "</div>"; #toolbar
  1122. print "<div class='panel panel-scrollable'>";
  1123. print "<table width='100%' id='error-feeds-list'>";
  1124. $lnum = 1;
  1125. while ($line = $sth->fetch()) {
  1126. $feed_id = $line["id"];
  1127. print "<tr data-row-id='$feed_id'>";
  1128. print "<td width='5%' align='center'><input
  1129. onclick='Tables.onRowChecked(this);' dojoType=\"dijit.form.CheckBox\"
  1130. type=\"checkbox\"></td>";
  1131. print "<td>";
  1132. print "<a class=\"visibleLink\" href=\"#\" ".
  1133. "title=\"".__("Click to edit feed")."\" ".
  1134. "onclick=\"CommonDialogs.editFeed(".$line["id"].")\">".
  1135. htmlspecialchars($line["title"])."</a>: ";
  1136. print "<span class=\"text-muted\">";
  1137. print htmlspecialchars($line["last_error"]);
  1138. print "</span>";
  1139. print "</td>";
  1140. print "</tr>";
  1141. ++$lnum;
  1142. }
  1143. print "</table>";
  1144. print "</div>";
  1145. print "<footer>";
  1146. print "<button style='float : left' class=\"alt-danger\" dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('errorFeedsDlg').removeSelected()\">"
  1147. .__('Unsubscribe from selected feeds')."</button> ";
  1148. print "<button dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('errorFeedsDlg').hide()\">".
  1149. __('Close this window')."</button>";
  1150. print "</footer>";
  1151. }
  1152. private function remove_feed_category($id, $owner_uid) {
  1153. $sth = $this->pdo->prepare("DELETE FROM ttrss_feed_categories
  1154. WHERE id = ? AND owner_uid = ?");
  1155. $sth->execute([$id, $owner_uid]);
  1156. CCache::remove($id, $owner_uid, true);
  1157. }
  1158. static function remove_feed($id, $owner_uid) {
  1159. foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_UNSUBSCRIBE_FEED) as $p) {
  1160. if (! $p->hook_unsubscribe_feed($id, $owner_uid)) {
  1161. user_error("Feed $id (owner: $owner_uid) not removed due to plugin error (HOOK_UNSUBSCRIBE_FEED).", E_USER_WARNING);
  1162. return;
  1163. }
  1164. }
  1165. $pdo = Db::pdo();
  1166. if ($id > 0) {
  1167. $pdo->beginTransaction();
  1168. /* save starred articles in Archived feed */
  1169. /* prepare feed if necessary */
  1170. $sth = $pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?
  1171. AND owner_uid = ?");
  1172. $sth->execute([$id, $owner_uid]);
  1173. if ($row = $sth->fetch()) {
  1174. $feed_url = $row["feed_url"];
  1175. $sth = $pdo->prepare("SELECT id FROM ttrss_archived_feeds
  1176. WHERE feed_url = ? AND owner_uid = ?");
  1177. $sth->execute([$feed_url, $owner_uid]);
  1178. if ($row = $sth->fetch()) {
  1179. $archive_id = $row["id"];
  1180. } else {
  1181. $res = $pdo->query("SELECT MAX(id) AS id FROM ttrss_archived_feeds");
  1182. $row = $res->fetch();
  1183. $new_feed_id = (int)$row['id'] + 1;
  1184. $sth = $pdo->prepare("INSERT INTO ttrss_archived_feeds
  1185. (id, owner_uid, title, feed_url, site_url, created)
  1186. SELECT ?, owner_uid, title, feed_url, site_url, NOW() from ttrss_feeds
  1187. WHERE id = ?");
  1188. $sth->execute([$new_feed_id, $id]);
  1189. $archive_id = $new_feed_id;
  1190. }
  1191. $sth = $pdo->prepare("UPDATE ttrss_user_entries SET feed_id = NULL,
  1192. orig_feed_id = ? WHERE feed_id = ? AND
  1193. marked = true AND owner_uid = ?");
  1194. $sth->execute([$archive_id, $id, $owner_uid]);
  1195. /* Remove access key for the feed */
  1196. $sth = $pdo->prepare("DELETE FROM ttrss_access_keys WHERE
  1197. feed_id = ? AND owner_uid = ?");
  1198. $sth->execute([$id, $owner_uid]);
  1199. /* remove the feed */
  1200. $sth = $pdo->prepare("DELETE FROM ttrss_feeds
  1201. WHERE id = ? AND owner_uid = ?");
  1202. $sth->execute([$id, $owner_uid]);
  1203. }
  1204. $pdo->commit();
  1205. if (file_exists(ICONS_DIR . "/$id.ico")) {
  1206. unlink(ICONS_DIR . "/$id.ico");
  1207. }
  1208. CCache::remove($id, $owner_uid);
  1209. } else {
  1210. Labels::remove(Labels::feed_to_label_id($id), $owner_uid);
  1211. //CCache::remove($id, $owner_uid); don't think labels are cached
  1212. }
  1213. }
  1214. function batchSubscribe() {
  1215. print_hidden("op", "pref-feeds");
  1216. print_hidden("method", "batchaddfeeds");
  1217. print "<header class='horizontal'>".__("One valid feed per line (no detection is done)")."</header>";
  1218. print "<section>";
  1219. print "<textarea
  1220. style='font-size : 12px; width : 98%; height: 200px;'
  1221. dojoType='dijit.form.SimpleTextarea' name='feeds'></textarea>";
  1222. if (get_pref('ENABLE_FEED_CATS')) {
  1223. print "<fieldset>";
  1224. print "<label>" . __('Place in category:') . "</label> ";
  1225. print_feed_cat_select("cat", false, 'dojoType="fox.form.Select"');
  1226. print "</fieldset>";
  1227. }
  1228. print "</section>";
  1229. print "<div id='feedDlg_loginContainer' style='display : none'>";
  1230. print "<header>" . __("Authentication") . "</header>";
  1231. print "<section>";
  1232. print "<input dojoType='dijit.form.TextBox' name='login' placeHolder=\"".__("Login")."\">
  1233. <input placeHolder=\"".__("Password")."\" dojoType=\"dijit.form.TextBox\" type='password'
  1234. autocomplete='new-password' name='pass''></div>";
  1235. print "</section>";
  1236. print "</div>";
  1237. print "<fieldset class='narrow'>
  1238. <label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox'
  1239. onclick='displayIfChecked(this, \"feedDlg_loginContainer\")'> ".
  1240. __('Feeds require authentication.')."</label></div>";
  1241. print "</fieldset>";
  1242. print "<footer>
  1243. <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".__('Subscribe')."</button>
  1244. <button dojoType='dijit.form.Button' onclick=\"return dijit.byId('batchSubDlg').hide()\">".__('Cancel')."</button>
  1245. </footer>";
  1246. }
  1247. function batchAddFeeds() {
  1248. $cat_id = clean($_REQUEST['cat']);
  1249. $feeds = explode("\n", clean($_REQUEST['feeds']));
  1250. $login = clean($_REQUEST['login']);
  1251. $pass = trim(clean($_REQUEST['pass']));
  1252. $csth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
  1253. WHERE feed_url = ? AND owner_uid = ?");
  1254. $isth = $this->pdo->prepare("INSERT INTO ttrss_feeds
  1255. (owner_uid,feed_url,title,cat_id,auth_login,auth_pass,update_method,auth_pass_encrypted)
  1256. VALUES (?, ?, '[Unknown]', ?, ?, ?, 0, false)");
  1257. foreach ($feeds as $feed) {
  1258. $feed = trim($feed);
  1259. if (Feeds::validate_feed_url($feed)) {
  1260. $this->pdo->beginTransaction();
  1261. $csth->execute([$feed, $_SESSION['uid']]);
  1262. if (!$csth->fetch()) {
  1263. $isth->execute([$_SESSION['uid'], $feed, $cat_id ? $cat_id : null, $login, $pass]);
  1264. }
  1265. $this->pdo->commit();
  1266. }
  1267. }
  1268. }
  1269. function regenOPMLKey() {
  1270. $this->update_feed_access_key('OPML:Publish',
  1271. false, $_SESSION["uid"]);
  1272. $new_link = Opml::opml_publish_url();
  1273. print json_encode(array("link" => $new_link));
  1274. }
  1275. function regenFeedKey() {
  1276. $feed_id = clean($_REQUEST['id']);
  1277. $is_cat = clean($_REQUEST['is_cat']);
  1278. $new_key = $this->update_feed_access_key($feed_id, $is_cat);
  1279. print json_encode(["link" => $new_key]);
  1280. }
  1281. private function update_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
  1282. if (!$owner_uid) $owner_uid = $_SESSION["uid"];
  1283. // clear old value and generate new one
  1284. $sth = $this->pdo->prepare("DELETE FROM ttrss_access_keys
  1285. WHERE feed_id = ? AND is_cat = ? AND owner_uid = ?");
  1286. $sth->execute([$feed_id, bool_to_sql_bool($is_cat), $owner_uid]);
  1287. return Feeds::get_feed_access_key($feed_id, $is_cat, $owner_uid);
  1288. }
  1289. // Silent
  1290. function clearKeys() {
  1291. $sth = $this->pdo->prepare("DELETE FROM ttrss_access_keys WHERE
  1292. owner_uid = ?");
  1293. $sth->execute([$_SESSION['uid']]);
  1294. }
  1295. private function calculate_children_count($cat) {
  1296. $c = 0;
  1297. foreach ($cat['items'] as $child) {
  1298. if ($child['type'] == 'category') {
  1299. $c += $this->calculate_children_count($child);
  1300. } else {
  1301. $c += 1;
  1302. }
  1303. }
  1304. return $c;
  1305. }
  1306. function getinactivefeeds() {
  1307. if (DB_TYPE == "pgsql") {
  1308. $interval_qpart = "NOW() - INTERVAL '3 months'";
  1309. } else {
  1310. $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
  1311. }
  1312. $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_inactive FROM ttrss_feeds WHERE
  1313. (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE
  1314. ttrss_entries.id = ref_id AND
  1315. ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart AND
  1316. ttrss_feeds.owner_uid = ?");
  1317. $sth->execute([$_SESSION['uid']]);
  1318. if ($row = $sth->fetch()) {
  1319. print (int)$row["num_inactive"];
  1320. }
  1321. }
  1322. static function subscribe_to_feed_url() {
  1323. $url_path = get_self_url_prefix() .
  1324. "/public.php?op=subscribe&feed_url=%s";
  1325. return $url_path;
  1326. }
  1327. }