prefs.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123
  1. <?php
  2. class Pref_Prefs extends Handler_Protected {
  3. private $pref_help = [];
  4. private $pref_item_map = [];
  5. private $pref_blacklist = [];
  6. private $profile_blacklist = [];
  7. function csrf_ignore($method) {
  8. $csrf_ignored = array("index", "updateself", "customizecss", "editprefprofiles");
  9. return array_search($method, $csrf_ignored) !== false;
  10. }
  11. function __construct($args) {
  12. parent::__construct($args);
  13. $this->pref_item_map = [
  14. __('General') => [
  15. 'USER_LANGUAGE',
  16. 'USER_TIMEZONE',
  17. 'BLOCK_SEPARATOR',
  18. 'USER_CSS_THEME',
  19. 'BLOCK_SEPARATOR',
  20. 'ENABLE_API_ACCESS',
  21. ],
  22. __('Feeds') => [
  23. 'DEFAULT_UPDATE_INTERVAL',
  24. 'FRESH_ARTICLE_MAX_AGE',
  25. 'BLOCK_SEPARATOR',
  26. 'ENABLE_FEED_CATS',
  27. 'BLOCK_SEPARATOR',
  28. 'CONFIRM_FEED_CATCHUP',
  29. 'ON_CATCHUP_SHOW_NEXT_FEED',
  30. 'BLOCK_SEPARATOR',
  31. 'HIDE_READ_FEEDS',
  32. 'HIDE_READ_SHOWS_SPECIAL',
  33. ],
  34. __('Articles') => [
  35. 'PURGE_OLD_DAYS',
  36. 'PURGE_UNREAD_ARTICLES',
  37. 'BLOCK_SEPARATOR',
  38. 'COMBINED_DISPLAY_MODE',
  39. 'CDM_EXPANDED',
  40. 'BLOCK_SEPARATOR',
  41. 'CDM_AUTO_CATCHUP',
  42. 'VFEED_GROUP_BY_FEED',
  43. 'BLOCK_SEPARATOR',
  44. 'SHOW_CONTENT_PREVIEW',
  45. 'STRIP_IMAGES',
  46. ],
  47. __('Digest') => [
  48. 'DIGEST_ENABLE',
  49. 'DIGEST_CATCHUP',
  50. 'DIGEST_PREFERRED_TIME',
  51. ],
  52. __('Advanced') => [
  53. 'BLACKLISTED_TAGS',
  54. 'BLOCK_SEPARATOR',
  55. 'LONG_DATE_FORMAT',
  56. 'SHORT_DATE_FORMAT',
  57. 'BLOCK_SEPARATOR',
  58. 'SSL_CERT_SERIAL',
  59. ]
  60. ];
  61. $this->pref_help = [
  62. "ALLOW_DUPLICATE_POSTS" => array(__("Allow duplicate articles"), ""),
  63. "BLACKLISTED_TAGS" => array(__("Blacklisted tags"), __("Never apply these tags automatically (comma-separated list).")),
  64. "CDM_AUTO_CATCHUP" => array(__("Mark read on scroll"), __("Mark articles as read as you scroll past them")),
  65. "CDM_EXPANDED" => array(__("Always expand articles")),
  66. "COMBINED_DISPLAY_MODE" => array(__("Combined mode"), __("Show flat list of articles instead of separate panels")),
  67. "CONFIRM_FEED_CATCHUP" => array(__("Confirm marking feeds as read")),
  68. "DEFAULT_ARTICLE_LIMIT" => array(__("Amount of articles to display at once")),
  69. "DEFAULT_UPDATE_INTERVAL" => array(__("Default update interval")),
  70. "DIGEST_CATCHUP" => array(__("Mark sent articles as read")),
  71. "DIGEST_ENABLE" => array(__("Enable digest"), __("Send daily digest of new (and unread) headlines to your e-mail address")),
  72. "DIGEST_PREFERRED_TIME" => array(__("Try to send around this time"), __("Time in UTC")),
  73. "ENABLE_API_ACCESS" => array(__("Enable API"), __("Allows accessing this account through the API")),
  74. "ENABLE_FEED_CATS" => array(__("Enable categories")),
  75. "FEEDS_SORT_BY_UNREAD" => array(__("Sort feeds by unread articles count"), ""),
  76. "FRESH_ARTICLE_MAX_AGE" => array(__("Maximum age of fresh articles"), "<strong>" . __("hours") . "</strong>"),
  77. "HIDE_READ_FEEDS" => array(__("Hide read feeds")),
  78. "HIDE_READ_SHOWS_SPECIAL" => array(__("Always show special feeds"), __("While hiding read feeds")),
  79. "LONG_DATE_FORMAT" => array(__("Long date format"), __("Syntax is identical to PHP <a href='http://php.net/manual/function.date.php'>date()</a> function.")),
  80. "ON_CATCHUP_SHOW_NEXT_FEED" => array(__("Automatically show next feed"), __("After marking one as read")),
  81. "PURGE_OLD_DAYS" => array(__("Purge articles older than"), __("<strong>days</strong> (0 disables)")),
  82. "PURGE_UNREAD_ARTICLES" => array(__("Purge unread articles")),
  83. "REVERSE_HEADLINES" => array(__("Reverse headline order (oldest first)")),
  84. "SHORT_DATE_FORMAT" => array(__("Short date format")),
  85. "SHOW_CONTENT_PREVIEW" => array(__("Show content preview in headlines")),
  86. "SORT_HEADLINES_BY_FEED_DATE" => array(__("Sort headlines by feed date"), __("Use feed-specified date to sort headlines instead of local import date.")),
  87. "SSL_CERT_SERIAL" => array(__("SSL client certificate")),
  88. "STRIP_IMAGES" => array(__("Do not embed media")),
  89. "STRIP_UNSAFE_TAGS" => array(__("Strip unsafe tags from articles"), __("Strip all but most common HTML tags when reading articles.")),
  90. "USER_STYLESHEET" => array(__("Customize stylesheet")),
  91. "USER_TIMEZONE" => array(__("Time zone")),
  92. "VFEED_GROUP_BY_FEED" => array(__("Group by feed"), __("Group multiple-feed output by originating feed")),
  93. "USER_LANGUAGE" => array(__("Language")),
  94. "USER_CSS_THEME" => array(__("Theme"))
  95. ];
  96. $this->pref_blacklist = ["ALLOW_DUPLICATE_POSTS", "STRIP_UNSAFE_TAGS", "REVERSE_HEADLINES",
  97. "SORT_HEADLINES_BY_FEED_DATE", "DEFAULT_ARTICLE_LIMIT",
  98. "FEEDS_SORT_BY_UNREAD", "USER_STYLESHEET"];
  99. /* "FEEDS_SORT_BY_UNREAD", "HIDE_READ_FEEDS", "REVERSE_HEADLINES" */
  100. $this->profile_blacklist = ["ALLOW_DUPLICATE_POSTS", "PURGE_OLD_DAYS",
  101. "PURGE_UNREAD_ARTICLES", "DIGEST_ENABLE", "DIGEST_CATCHUP",
  102. "BLACKLISTED_TAGS", "ENABLE_API_ACCESS", "UPDATE_POST_ON_CHECKSUM_CHANGE",
  103. "DEFAULT_UPDATE_INTERVAL", "USER_TIMEZONE", "SORT_HEADLINES_BY_FEED_DATE",
  104. "SSL_CERT_SERIAL", "DIGEST_PREFERRED_TIME"];
  105. }
  106. function changepassword() {
  107. $old_pw = clean($_POST["old_password"]);
  108. $new_pw = clean($_POST["new_password"]);
  109. $con_pw = clean($_POST["confirm_password"]);
  110. if ($old_pw == "") {
  111. print "ERROR: ".format_error("Old password cannot be blank.");
  112. return;
  113. }
  114. if ($new_pw == "") {
  115. print "ERROR: ".format_error("New password cannot be blank.");
  116. return;
  117. }
  118. if ($new_pw != $con_pw) {
  119. print "ERROR: ".format_error("Entered passwords do not match.");
  120. return;
  121. }
  122. $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
  123. if (method_exists($authenticator, "change_password")) {
  124. print format_notice($authenticator->change_password($_SESSION["uid"], $old_pw, $new_pw));
  125. } else {
  126. print "ERROR: ".format_error("Function not supported by authentication module.");
  127. }
  128. }
  129. function saveconfig() {
  130. $boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
  131. foreach ($boolean_prefs as $pref) {
  132. if (!isset($_POST[$pref])) $_POST[$pref] = 'false';
  133. }
  134. $need_reload = false;
  135. foreach (array_keys($_POST) as $pref_name) {
  136. $value = $_POST[$pref_name];
  137. switch ($pref_name) {
  138. case 'DIGEST_PREFERRED_TIME':
  139. if (get_pref('DIGEST_PREFERRED_TIME') != $value) {
  140. $sth = $this->pdo->prepare("UPDATE ttrss_users SET
  141. last_digest_sent = NULL WHERE id = ?");
  142. $sth->execute([$_SESSION['uid']]);
  143. }
  144. break;
  145. case 'USER_LANGUAGE':
  146. if (!$need_reload) $need_reload = $_SESSION["language"] != $value;
  147. break;
  148. case 'USER_CSS_THEME':
  149. if (!$need_reload) $need_reload = get_pref($pref_name) != $value;
  150. break;
  151. }
  152. set_pref($pref_name, $value);
  153. }
  154. if ($need_reload) {
  155. print "PREFS_NEED_RELOAD";
  156. } else {
  157. print __("The configuration was saved.");
  158. }
  159. }
  160. function changeemail() {
  161. $email = clean($_POST["email"]);
  162. $full_name = clean($_POST["full_name"]);
  163. $active_uid = $_SESSION["uid"];
  164. $sth = $this->pdo->prepare("UPDATE ttrss_users SET email = ?,
  165. full_name = ? WHERE id = ?");
  166. $sth->execute([$email, $full_name, $active_uid]);
  167. print __("Your personal data has been saved.");
  168. return;
  169. }
  170. function resetconfig() {
  171. $_SESSION["prefs_op_result"] = "reset-to-defaults";
  172. $sth = $this->pdo->prepare("DELETE FROM ttrss_user_prefs
  173. WHERE (profile = :profile OR (:profile IS NULL AND profile IS NULL))
  174. AND owner_uid = :uid");
  175. $sth->execute([":profile" => $_SESSION['profile'], ":uid" => $_SESSION['uid']]);
  176. initialize_user_prefs($_SESSION["uid"], $_SESSION["profile"]);
  177. echo __("Your preferences are now set to default values.");
  178. }
  179. function index() {
  180. global $access_level_names;
  181. $_SESSION["prefs_op_result"] = "";
  182. print "<div dojoType='dijit.layout.AccordionContainer' region='center'>";
  183. print "<div dojoType='dijit.layout.AccordionPane'
  184. title=\"<i class='material-icons'>person</i> ".__('Personal data / Authentication')."\">";
  185. print "<div dojoType='dijit.layout.TabContainer'>";
  186. print "<div dojoType='dijit.layout.ContentPane' title=\"".__('Personal data')."\">";
  187. print "<form dojoType='dijit.form.Form' id='changeUserdataForm'>";
  188. print "<script type='dojo/method' event='onSubmit' args='evt'>
  189. evt.preventDefault();
  190. if (this.validate()) {
  191. Notify.progress('Saving data...', true);
  192. new Ajax.Request('backend.php', {
  193. parameters: dojo.objectToQuery(this.getValues()),
  194. onComplete: function(transport) {
  195. notify_callback2(transport);
  196. } });
  197. }
  198. </script>";
  199. $sth = $this->pdo->prepare("SELECT email,full_name,otp_enabled,
  200. access_level FROM ttrss_users
  201. WHERE id = ?");
  202. $sth->execute([$_SESSION["uid"]]);
  203. $row = $sth->fetch();
  204. $email = htmlspecialchars($row["email"]);
  205. $full_name = htmlspecialchars($row["full_name"]);
  206. $otp_enabled = sql_bool_to_bool($row["otp_enabled"]);
  207. print "<fieldset>";
  208. print "<label>".__('Full name:')."</label>";
  209. print "<input dojoType='dijit.form.ValidationTextBox' name='full_name' required='1' value='$full_name'>";
  210. print "</fieldset>";
  211. print "<fieldset>";
  212. print "<label>".__('E-mail:')."</label>";
  213. print "<input dojoType='dijit.form.ValidationTextBox' name='email' required='1' value='$email'>";
  214. print "</fieldset>";
  215. if (!SINGLE_USER_MODE && !$_SESSION["hide_hello"]) {
  216. $access_level = $row["access_level"];
  217. print "<fieldset>";
  218. print "<label>".__('Access level:')."</label>";
  219. print $access_level_names[$access_level];
  220. print "</fieldset>";
  221. }
  222. print_hidden("op", "pref-prefs");
  223. print_hidden("method", "changeemail");
  224. print "<hr/>";
  225. print "<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".
  226. __("Save data")."</button>";
  227. print "</form>";
  228. print "</div>"; # content pane
  229. print "<div dojoType='dijit.layout.ContentPane' title=\"".__('Password')."\">";
  230. if ($_SESSION["auth_module"]) {
  231. $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
  232. } else {
  233. $authenticator = false;
  234. }
  235. if ($authenticator && method_exists($authenticator, "change_password")) {
  236. print "<div style='display : none' id='pwd_change_infobox'></div>";
  237. print "<form dojoType='dijit.form.Form'>";
  238. print "<script type='dojo/method' event='onSubmit' args='evt'>
  239. evt.preventDefault();
  240. if (this.validate()) {
  241. Notify.progress('Changing password...', true);
  242. new Ajax.Request('backend.php', {
  243. parameters: dojo.objectToQuery(this.getValues()),
  244. onComplete: function(transport) {
  245. Notify.close();
  246. if (transport.responseText.indexOf('ERROR: ') == 0) {
  247. $('pwd_change_infobox').innerHTML =
  248. transport.responseText.replace('ERROR: ', '');
  249. } else {
  250. $('pwd_change_infobox').innerHTML =
  251. transport.responseText.replace('ERROR: ', '');
  252. var warn = $('default_pass_warning');
  253. if (warn) Element.hide(warn);
  254. }
  255. new Effect.Appear('pwd_change_infobox');
  256. }});
  257. this.reset();
  258. }
  259. </script>";
  260. if ($otp_enabled) {
  261. print_notice(__("Changing your current password will disable OTP."));
  262. }
  263. print "<fieldset>";
  264. print "<label>".__("Old password:")."</label>";
  265. print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='old_password'>";
  266. print "</fieldset>";
  267. print "<fieldset>";
  268. print "<label>".__("New password:")."</label>";
  269. print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='new_password'>";
  270. print "</fieldset>";
  271. print "<fieldset>";
  272. print "<label>".__("Confirm password:")."</label>";
  273. print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='confirm_password'>";
  274. print "</fieldset>";
  275. print_hidden("op", "pref-prefs");
  276. print_hidden("method", "changepassword");
  277. print "<hr/>";
  278. print "<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".
  279. __("Change password")."</button>";
  280. print "</form>";
  281. print "</div>"; # content pane
  282. print "<div dojoType='dijit.layout.ContentPane' title=\"".__('One time passwords / Authenticator')."\">";
  283. if ($_SESSION["auth_module"] == "auth_internal") {
  284. if ($otp_enabled) {
  285. print_warning("One time passwords are currently enabled. Enter your current password below to disable.");
  286. print "<form dojoType='dijit.form.Form'>";
  287. print "<script type='dojo/method' event='onSubmit' args='evt'>
  288. evt.preventDefault();
  289. if (this.validate()) {
  290. Notify.progress('Disabling OTP', true);
  291. new Ajax.Request('backend.php', {
  292. parameters: dojo.objectToQuery(this.getValues()),
  293. onComplete: function(transport) {
  294. Notify.close();
  295. if (transport.responseText.indexOf('ERROR: ') == 0) {
  296. Notify.error(transport.responseText.replace('ERROR: ', ''));
  297. } else {
  298. window.location.reload();
  299. }
  300. }});
  301. this.reset();
  302. }
  303. </script>";
  304. print "<fieldset>";
  305. print "<label>".__("Your password:")."</label>";
  306. print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='password'>";
  307. print "</fieldset>";
  308. print_hidden("op", "pref-prefs");
  309. print_hidden("method", "otpdisable");
  310. print "<hr/>";
  311. print "<button dojoType='dijit.form.Button' type='submit'>".
  312. __("Disable OTP")."</button>";
  313. print "</form>";
  314. } else if (function_exists("imagecreatefromstring")) {
  315. print_warning("You will need a compatible Authenticator to use this. Changing your password would automatically disable OTP.");
  316. print_notice("Scan the following code by the Authenticator application:");
  317. $csrf_token = $_SESSION["csrf_token"];
  318. print "<img alt='otp qr-code' src='backend.php?op=pref-prefs&method=otpqrcode&csrf_token=$csrf_token'>";
  319. print "<form dojoType='dijit.form.Form' id='changeOtpForm'>";
  320. print_hidden("op", "pref-prefs");
  321. print_hidden("method", "otpenable");
  322. print "<script type='dojo/method' event='onSubmit' args='evt'>
  323. evt.preventDefault();
  324. if (this.validate()) {
  325. Notify.progress('Saving data...', true);
  326. new Ajax.Request('backend.php', {
  327. parameters: dojo.objectToQuery(this.getValues()),
  328. onComplete: function(transport) {
  329. Notify.close();
  330. if (transport.responseText.indexOf('ERROR:') == 0) {
  331. Notify.error(transport.responseText.replace('ERROR:', ''));
  332. } else {
  333. window.location.reload();
  334. }
  335. } });
  336. }
  337. </script>";
  338. print "<fieldset>";
  339. print "<label>".__("Your password:")."</label>";
  340. print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1'
  341. name='password'>";
  342. print "</fieldset>";
  343. print "<fieldset>";
  344. print "<label>".__("One time password:")."</label>";
  345. print "<input dojoType='dijit.form.ValidationTextBox' autocomplete='off'
  346. required='1' name='otp'>";
  347. print "</fieldset>";
  348. print "<hr/>";
  349. print "<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".
  350. __("Enable OTP")."</button>";
  351. print "</form>";
  352. } else {
  353. print_notice("PHP GD functions are required for OTP support.");
  354. }
  355. }
  356. print "</div>"; # content pane
  357. print "</div>"; # tab container
  358. }
  359. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  360. "hook_prefs_tab_section", "prefPrefsAuth");
  361. print "</div>"; #pane
  362. print "<div dojoType='dijit.layout.AccordionPane' selected='true'
  363. title=\"<i class='material-icons'>settings</i> ".__('Preferences')."\">";
  364. print "<form dojoType='dijit.form.Form' id='changeSettingsForm'>";
  365. print "<script type='dojo/method' event='onSubmit' args='evt, quit'>
  366. if (evt) evt.preventDefault();
  367. if (this.validate()) {
  368. console.log(dojo.objectToQuery(this.getValues()));
  369. new Ajax.Request('backend.php', {
  370. parameters: dojo.objectToQuery(this.getValues()),
  371. onComplete: function(transport) {
  372. var msg = transport.responseText;
  373. if (quit) {
  374. document.location.href = 'index.php';
  375. } else {
  376. if (msg == 'PREFS_NEED_RELOAD') {
  377. window.location.reload();
  378. } else {
  379. Notify.info(msg);
  380. }
  381. }
  382. } });
  383. }
  384. </script>";
  385. print '<div dojoType="dijit.layout.BorderContainer" gutters="false">';
  386. print '<div dojoType="dijit.layout.ContentPane" region="center" style="overflow-y : auto">';
  387. $profile = $_SESSION["profile"];
  388. if ($profile) {
  389. print_notice(__("Some preferences are only available in default profile."));
  390. initialize_user_prefs($_SESSION["uid"], $profile);
  391. } else {
  392. initialize_user_prefs($_SESSION["uid"]);
  393. }
  394. $prefs_available = [];
  395. $sth = $this->pdo->prepare("SELECT DISTINCT
  396. ttrss_user_prefs.pref_name,value,type_name,
  397. ttrss_prefs_sections.order_id,
  398. def_value,section_id
  399. FROM ttrss_prefs,ttrss_prefs_types,ttrss_prefs_sections,ttrss_user_prefs
  400. WHERE type_id = ttrss_prefs_types.id AND
  401. (profile = :profile OR (:profile IS NULL AND profile IS NULL)) AND
  402. section_id = ttrss_prefs_sections.id AND
  403. ttrss_user_prefs.pref_name = ttrss_prefs.pref_name AND
  404. owner_uid = :uid
  405. ORDER BY ttrss_prefs_sections.order_id,pref_name");
  406. $sth->execute([":uid" => $_SESSION['uid'], ":profile" => $profile]);
  407. $listed_boolean_prefs = [];
  408. while ($line = $sth->fetch()) {
  409. if (in_array($line["pref_name"], $this->pref_blacklist)) {
  410. continue;
  411. }
  412. if ($profile && in_array($line["pref_name"], $this->profile_blacklist)) {
  413. continue;
  414. }
  415. $pref_name = $line["pref_name"];
  416. $short_desc = $this->getShortDesc($pref_name);
  417. if (!$short_desc)
  418. continue;
  419. $prefs_available[$pref_name] = [
  420. 'type_name' => $line["type_name"],
  421. 'value' => $line['value'],
  422. 'help_text' => $this->getHelpText($pref_name),
  423. 'short_desc' => $short_desc
  424. ];
  425. }
  426. foreach (array_keys($this->pref_item_map) as $section) {
  427. print "<h2>$section</h2>";
  428. foreach ($this->pref_item_map[$section] as $pref_name) {
  429. if ($pref_name == 'BLOCK_SEPARATOR' && !$profile) {
  430. print "<hr/>";
  431. continue;
  432. }
  433. if ($item = $prefs_available[$pref_name]) {
  434. print "<fieldset class='prefs'>";
  435. print "<label for='CB_$pref_name'>";
  436. print $item['short_desc'] . ":";
  437. print "</label>";
  438. $value = $item['value'];
  439. $type_name = $item['type_name'];
  440. if ($pref_name == "USER_LANGUAGE") {
  441. print_select_hash($pref_name, $value, get_translations(),
  442. "style='width : 220px; margin : 0px' dojoType='dijit.form.Select'");
  443. } else if ($pref_name == "USER_TIMEZONE") {
  444. $timezones = explode("\n", file_get_contents("lib/timezones.txt"));
  445. print_select($pref_name, $value, $timezones, 'dojoType="dijit.form.FilteringSelect"');
  446. } else if ($pref_name == "USER_CSS_THEME") {
  447. $themes = array_merge(glob("themes/*.php"), glob("themes/*.css"), glob("themes.local/*.css"));
  448. $themes = array_map("basename", $themes);
  449. $themes = array_filter($themes, "theme_exists");
  450. asort($themes);
  451. if (!theme_exists($value)) $value = "default.php";
  452. print "<select name='$pref_name' id='$pref_name' dojoType='dijit.form.Select'>";
  453. $issel = $value == "default.php" ? "selected='selected'" : "";
  454. print "<option $issel value='default.php'>".__("default")."</option>";
  455. foreach ($themes as $theme) {
  456. $issel = $value == $theme ? "selected='selected'" : "";
  457. print "<option $issel value='$theme'>$theme</option>";
  458. }
  459. print "</select>";
  460. print " <button dojoType=\"dijit.form.Button\" class='alt-info'
  461. onclick=\"Helpers.customizeCSS()\">" . __('Customize') . "</button>";
  462. print " <button dojoType='dijit.form.Button' onclick='window.open(\"https://tt-rss.org/wiki/Themes\")'>
  463. <i class='material-icons'>open_in_new</i> ".__("More themes...")."</button>";
  464. } else if ($pref_name == "DEFAULT_UPDATE_INTERVAL") {
  465. global $update_intervals_nodefault;
  466. print_select_hash($pref_name, $value, $update_intervals_nodefault,
  467. 'dojoType="dijit.form.Select"');
  468. } else if ($type_name == "bool") {
  469. array_push($listed_boolean_prefs, $pref_name);
  470. $checked = ($value == "true") ? "checked=\"checked\"" : "";
  471. if ($pref_name == "PURGE_UNREAD_ARTICLES" && FORCE_ARTICLE_PURGE != 0) {
  472. $disabled = "disabled=\"1\"";
  473. $checked = "checked=\"checked\"";
  474. } else {
  475. $disabled = "";
  476. }
  477. print "<input type='checkbox' name='$pref_name' $checked $disabled
  478. dojoType='dijit.form.CheckBox' id='CB_$pref_name' value='1'>";
  479. } else if (array_search($pref_name, array('FRESH_ARTICLE_MAX_AGE',
  480. 'PURGE_OLD_DAYS', 'LONG_DATE_FORMAT', 'SHORT_DATE_FORMAT')) !== false) {
  481. $regexp = ($type_name == 'integer') ? 'regexp="^\d*$"' : '';
  482. if ($pref_name == "PURGE_OLD_DAYS" && FORCE_ARTICLE_PURGE != 0) {
  483. $disabled = "disabled='1'";
  484. $value = FORCE_ARTICLE_PURGE;
  485. } else {
  486. $disabled = "";
  487. }
  488. if ($type_name == 'integer')
  489. print "<input dojoType=\"dijit.form.NumberSpinner\"
  490. required='1' $disabled
  491. name=\"$pref_name\" value=\"$value\">";
  492. else
  493. print "<input dojoType=\"dijit.form.TextBox\"
  494. required='1' $regexp $disabled
  495. name=\"$pref_name\" value=\"$value\">";
  496. } else if ($pref_name == "SSL_CERT_SERIAL") {
  497. print "<input dojoType='dijit.form.ValidationTextBox'
  498. id='SSL_CERT_SERIAL' readonly='1'
  499. name=\"$pref_name\" value=\"$value\">";
  500. $cert_serial = htmlspecialchars(get_ssl_certificate_id());
  501. $has_serial = ($cert_serial) ? "false" : "true";
  502. print "<button dojoType='dijit.form.Button' disabled='$has_serial'
  503. onclick=\"dijit.byId('SSL_CERT_SERIAL').attr('value', '$cert_serial')\">" .
  504. __('Register') . "</button>";
  505. print "<button dojoType='dijit.form.Button' class='alt-danger'
  506. onclick=\"dijit.byId('SSL_CERT_SERIAL').attr('value', '')\">" .
  507. __('Clear') . "</button>";
  508. print "<button dojoType='dijit.form.Button' class='alt-info'
  509. onclick='window.open(\"https://tt-rss.org/wiki/SSL+Certificate+Authentication\")'>
  510. <i class='material-icons'>help</i> ".__("More info...")."</button>";
  511. } else if ($pref_name == 'DIGEST_PREFERRED_TIME') {
  512. print "<input dojoType=\"dijit.form.ValidationTextBox\"
  513. id=\"$pref_name\" regexp=\"[012]?\d:\d\d\" placeHolder=\"12:00\"
  514. name=\"$pref_name\" value=\"$value\">";
  515. $item['help_text'] .= ". " . T_sprintf("Current server time: %s", date("H:i"));
  516. } else {
  517. $regexp = ($type_name == 'integer') ? 'regexp="^\d*$"' : '';
  518. print "<input dojoType=\"dijit.form.ValidationTextBox\" $regexp name=\"$pref_name\" value=\"$value\">";
  519. }
  520. if ($item['help_text'])
  521. print "<div class='help-text text-muted'><label for='CB_$pref_name'>".$item['help_text']."</label></div>";
  522. print "</fieldset>";
  523. }
  524. }
  525. }
  526. $listed_boolean_prefs = htmlspecialchars(join(",", $listed_boolean_prefs));
  527. print_hidden("boolean_prefs", "$listed_boolean_prefs");
  528. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  529. "hook_prefs_tab_section", "prefPrefsPrefsInside");
  530. print '</div>'; # inside pane
  531. print '<div dojoType="dijit.layout.ContentPane" region="bottom">';
  532. print_hidden("op", "pref-prefs");
  533. print_hidden("method", "saveconfig");
  534. print "<div dojoType=\"dijit.form.ComboButton\" type=\"submit\" class=\"alt-primary\">
  535. <span>".__('Save configuration')."</span>
  536. <div dojoType=\"dijit.DropDownMenu\">
  537. <div dojoType=\"dijit.MenuItem\"
  538. onclick=\"dijit.byId('changeSettingsForm').onSubmit(null, true)\">".
  539. __("Save and exit preferences")."</div>
  540. </div>
  541. </div>";
  542. print "<button dojoType=\"dijit.form.Button\" onclick=\"return Helpers.editProfiles()\">".
  543. __('Manage profiles')."</button> ";
  544. print "<button dojoType=\"dijit.form.Button\" class=\"alt-danger\" onclick=\"return Helpers.confirmReset()\">".
  545. __('Reset to defaults')."</button>";
  546. print "&nbsp;";
  547. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION,
  548. "hook_prefs_tab_section", "prefPrefsPrefsOutside");
  549. print "</form>";
  550. print '</div>'; # inner pane
  551. print '</div>'; # border container
  552. print "</div>"; #pane
  553. print "<div dojoType=\"dijit.layout.AccordionPane\"
  554. title=\"<i class='material-icons'>extension</i> ".__('Plugins')."\">";
  555. print "<form dojoType=\"dijit.form.Form\" id=\"changePluginsForm\">";
  556. print "<script type=\"dojo/method\" event=\"onSubmit\" args=\"evt\">
  557. evt.preventDefault();
  558. if (this.validate()) {
  559. Notify.progress('Saving data...', true);
  560. new Ajax.Request('backend.php', {
  561. parameters: dojo.objectToQuery(this.getValues()),
  562. onComplete: function(transport) {
  563. Notify.close();
  564. if (confirm(__('Selected plugins have been enabled. Reload?'))) {
  565. window.location.reload();
  566. }
  567. } });
  568. }
  569. </script>";
  570. print_hidden("op", "pref-prefs");
  571. print_hidden("method", "setplugins");
  572. print '<div dojoType="dijit.layout.BorderContainer" gutters="false">';
  573. print '<div dojoType="dijit.layout.ContentPane" region="center" style="overflow-y : auto">';
  574. if (ini_get("open_basedir") && function_exists("curl_init") && !defined("NO_CURL")) {
  575. print_warning("Your PHP configuration has open_basedir restrictions enabled. Some plugins relying on CURL for functionality may not work correctly.");
  576. }
  577. print "<h2>".__("System plugins")."</h2>";
  578. print_notice("System plugins are enabled in <strong>config.php</strong> for all users.");
  579. $system_enabled = array_map("trim", explode(",", PLUGINS));
  580. $user_enabled = array_map("trim", explode(",", get_pref("_ENABLED_PLUGINS")));
  581. $tmppluginhost = new PluginHost();
  582. $tmppluginhost->load_all($tmppluginhost::KIND_ALL, $_SESSION["uid"], true);
  583. $tmppluginhost->load_data(true);
  584. foreach ($tmppluginhost->get_plugins() as $name => $plugin) {
  585. $about = $plugin->about();
  586. if ($about[3]) {
  587. if (in_array($name, $system_enabled)) {
  588. $checked = "checked='1'";
  589. } else {
  590. $checked = "";
  591. }
  592. print "<fieldset class='prefs plugin'>
  593. <label>$name:</label>
  594. <label class='checkbox description text-muted' id='PLABEL-$name'>
  595. <input disabled='1'
  596. dojoType='dijit.form.CheckBox' $checked type='checkbox'>
  597. ".htmlspecialchars($about[1]). "</label>";
  598. if (@$about[4]) {
  599. print "<button dojoType='dijit.form.Button' class='alt-info'
  600. onclick='window.open(\"".htmlspecialchars($about[4])."\")'>
  601. <i class='material-icons'>open_in_new</i> ".__("More info...")."</button>";
  602. }
  603. print "<div dojoType='dijit.Tooltip' connectId='PLABEL-$name' position='after'>".
  604. htmlspecialchars(T_sprintf("v%.2f, by %s", $about[0], $about[2])).
  605. "</div>";
  606. print "</fieldset>";
  607. }
  608. }
  609. print "<h2>".__("User plugins")."</h2>";
  610. foreach ($tmppluginhost->get_plugins() as $name => $plugin) {
  611. $about = $plugin->about();
  612. if (!$about[3]) {
  613. $checked = "";
  614. $disabled = "";
  615. if (in_array($name, $system_enabled)) {
  616. $checked = "checked='1'";
  617. $disabled = "disabled='1'";
  618. } else if (in_array($name, $user_enabled)) {
  619. $checked = "checked='1'";
  620. }
  621. print "<fieldset class='prefs plugin'>
  622. <label>$name:</label>
  623. <label class='checkbox description text-muted' id='PLABEL-$name'>
  624. <input name='plugins[]' value='$name' dojoType='dijit.form.CheckBox' $checked $disabled type='checkbox'>
  625. ".htmlspecialchars($about[1])."</label>";
  626. if (count($tmppluginhost->get_all($plugin)) > 0) {
  627. if (in_array($name, $system_enabled) || in_array($name, $user_enabled)) {
  628. print " <button dojoType='dijit.form.Button'
  629. onclick=\"Helpers.clearPluginData('$name')\">
  630. <i class='material-icons'>clear</i> ".__("Clear data")."</button>";
  631. }
  632. }
  633. if (@$about[4]) {
  634. print " <button dojoType='dijit.form.Button' class='alt-info'
  635. onclick='window.open(\"".htmlspecialchars($about[4])."\")'>
  636. <i class='material-icons'>open_in_new</i> ".__("More info...")."</button>";
  637. }
  638. print "<div dojoType='dijit.Tooltip' connectId='PLABEL-$name' position='after'>".
  639. htmlspecialchars(T_sprintf("v%.2f, by %s", $about[0], $about[2])).
  640. "</div>";
  641. print "</fieldset>";
  642. }
  643. }
  644. print "</div>"; #content-pane
  645. print '<div dojoType="dijit.layout.ContentPane" region="bottom">';
  646. print "<button dojoType='dijit.form.Button' style='float : left' class='alt-info' onclick='window.open(\"https://tt-rss.org/wiki/Plugins\")'>
  647. <i class='material-icons'>help</i> ".__("More info...")."</button>";
  648. print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>".
  649. __("Enable selected plugins")."</button>";
  650. print "</div>"; #pane
  651. print "</div>"; #pane
  652. print "</div>"; #border-container
  653. print "</form>";
  654. PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB,
  655. "hook_prefs_tab", "prefPrefs");
  656. print "</div>"; #container
  657. }
  658. function toggleAdvanced() {
  659. $_SESSION["prefs_show_advanced"] = !$_SESSION["prefs_show_advanced"];
  660. }
  661. function otpqrcode() {
  662. require_once "lib/phpqrcode/phpqrcode.php";
  663. $sth = $this->pdo->prepare("SELECT login,salt,otp_enabled
  664. FROM ttrss_users
  665. WHERE id = ?");
  666. $sth->execute([$_SESSION['uid']]);
  667. if ($row = $sth->fetch()) {
  668. $base32 = new \OTPHP\Base32();
  669. $login = $row["login"];
  670. $otp_enabled = sql_bool_to_bool($row["otp_enabled"]);
  671. if (!$otp_enabled) {
  672. $secret = $base32->encode(sha1($row["salt"]));
  673. QRcode::png("otpauth://totp/".urlencode($login).
  674. "?secret=$secret&issuer=".urlencode("Tiny Tiny RSS"));
  675. }
  676. }
  677. }
  678. function otpenable() {
  679. $password = clean($_REQUEST["password"]);
  680. $otp = clean($_REQUEST["otp"]);
  681. $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
  682. if ($authenticator->check_password($_SESSION["uid"], $password)) {
  683. $sth = $this->pdo->prepare("SELECT salt
  684. FROM ttrss_users
  685. WHERE id = ?");
  686. $sth->execute([$_SESSION['uid']]);
  687. if ($row = $sth->fetch()) {
  688. $base32 = new \OTPHP\Base32();
  689. $secret = $base32->encode(sha1($row["salt"]));
  690. $topt = new \OTPHP\TOTP($secret);
  691. $otp_check = $topt->now();
  692. if ($otp == $otp_check) {
  693. $sth = $this->pdo->prepare("UPDATE ttrss_users
  694. SET otp_enabled = true WHERE id = ?");
  695. $sth->execute([$_SESSION['uid']]);
  696. print "OK";
  697. } else {
  698. print "ERROR:".__("Incorrect one time password");
  699. }
  700. }
  701. } else {
  702. print "ERROR:".__("Incorrect password");
  703. }
  704. }
  705. static function isdefaultpassword() {
  706. $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
  707. if ($authenticator &&
  708. method_exists($authenticator, "check_password") &&
  709. $authenticator->check_password($_SESSION["uid"], "password")) {
  710. return true;
  711. }
  712. return false;
  713. }
  714. function otpdisable() {
  715. $password = clean($_REQUEST["password"]);
  716. $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
  717. if ($authenticator->check_password($_SESSION["uid"], $password)) {
  718. $sth = $this->pdo->prepare("UPDATE ttrss_users SET otp_enabled = false WHERE
  719. id = ?");
  720. $sth->execute([$_SESSION['uid']]);
  721. print "OK";
  722. } else {
  723. print "ERROR: ".__("Incorrect password");
  724. }
  725. }
  726. function setplugins() {
  727. if (is_array(clean($_REQUEST["plugins"])))
  728. $plugins = join(",", clean($_REQUEST["plugins"]));
  729. else
  730. $plugins = "";
  731. set_pref("_ENABLED_PLUGINS", $plugins);
  732. }
  733. function clearplugindata() {
  734. $name = clean($_REQUEST["name"]);
  735. PluginHost::getInstance()->clear_data(PluginHost::getInstance()->get_plugin($name));
  736. }
  737. function customizeCSS() {
  738. $value = get_pref("USER_STYLESHEET");
  739. $value = str_replace("<br/>", "\n", $value);
  740. print_notice(__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here."));
  741. print_hidden("op", "rpc");
  742. print_hidden("method", "setpref");
  743. print_hidden("key", "USER_STYLESHEET");
  744. print "<textarea class='panel user-css-editor' dojoType='dijit.form.SimpleTextarea'
  745. style='font-size : 12px;' name='value'>$value</textarea>";
  746. print "<footer>";
  747. print "<button dojoType='dijit.form.Button'
  748. onclick=\"dijit.byId('cssEditDlg').execute()\">".__('Save')."</button> ";
  749. print "<button dojoType='dijit.form.Button'
  750. onclick=\"dijit.byId('cssEditDlg').hide()\">".__('Cancel')."</button>";
  751. print "</footer>";
  752. }
  753. function editPrefProfiles() {
  754. print "<div dojoType='dijit.Toolbar'>";
  755. print "<div dojoType='dijit.form.DropDownButton'>".
  756. "<span>" . __('Select')."</span>";
  757. print "<div dojoType='dijit.Menu' style='display: none'>";
  758. print "<div onclick=\"Tables.select('pref-profiles-list', true)\"
  759. dojoType='dijit.MenuItem'>".__('All')."</div>";
  760. print "<div onclick=\"Tables.select('pref-profiles-list', false)\"
  761. dojoType='dijit.MenuItem'>".__('None')."</div>";
  762. print "</div></div>";
  763. print "<div style='float : right'>";
  764. print "<input name='newprofile' dojoType='dijit.form.ValidationTextBox'
  765. required='1'>
  766. <button dojoType='dijit.form.Button'
  767. onclick=\"dijit.byId('profileEditDlg').addProfile()\">".
  768. __('Create profile')."</button></div>";
  769. print "</div>";
  770. $sth = $this->pdo->prepare("SELECT title,id FROM ttrss_settings_profiles
  771. WHERE owner_uid = ? ORDER BY title");
  772. $sth->execute([$_SESSION['uid']]);
  773. print "<div class='panel panel-scrollable'>";
  774. print "<form id='profile_edit_form' onsubmit='return false'>";
  775. print "<table width='100%' id='pref-profiles-list'>";
  776. print "<tr>"; # data-row-id='0' <-- no point, shouldn't be removed
  777. print "<td><input onclick='Tables.onRowChecked(this);' dojoType='dijit.form.CheckBox' type='checkbox'></td>";
  778. if (!$_SESSION["profile"]) {
  779. $is_active = __("(active)");
  780. } else {
  781. $is_active = "";
  782. }
  783. print "<td width='100%'><span>" . __("Default profile") . " $is_active</span></td>";
  784. print "</tr>";
  785. while ($line = $sth->fetch()) {
  786. $profile_id = $line["id"];
  787. print "<tr data-row-id='$profile_id'>";
  788. $edit_title = htmlspecialchars($line["title"]);
  789. print "<td><input onclick='Tables.onRowChecked(this);' dojoType='dijit.form.CheckBox' type='checkbox'></td>";
  790. if ($_SESSION["profile"] == $line["id"]) {
  791. $is_active = __("(active)");
  792. } else {
  793. $is_active = "";
  794. }
  795. print "<td><span dojoType='dijit.InlineEditBox'
  796. width='300px' autoSave='false'
  797. profile-id='$profile_id'>" . $edit_title .
  798. "<script type='dojo/method' event='onChange' args='item'>
  799. var elem = this;
  800. dojo.xhrPost({
  801. url: 'backend.php',
  802. content: {op: 'rpc', method: 'saveprofile',
  803. value: this.value,
  804. id: this.srcNodeRef.getAttribute('profile-id')},
  805. load: function(response) {
  806. elem.attr('value', response);
  807. }
  808. });
  809. </script>
  810. </span> $is_active</td>";
  811. print "</tr>";
  812. }
  813. print "</table>";
  814. print "</form>";
  815. print "</div>";
  816. print "<footer>
  817. <button style='float : left' class='alt-danger' dojoType=\"dijit.form.Button\" onclick=\"dijit.byId('profileEditDlg').removeSelected()\">".
  818. __('Remove selected profiles')."</button>
  819. <button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick=\"dijit.byId('profileEditDlg').activateProfile()\">".
  820. __('Activate profile')."</button>
  821. <button dojoType='dijit.form.Button' onclick=\"dijit.byId('profileEditDlg').hide()\">".
  822. __('Cancel')."</button>";
  823. print "</footer>";
  824. }
  825. private function getShortDesc($pref_name) {
  826. if (isset($this->pref_help[$pref_name])) {
  827. return $this->pref_help[$pref_name][0];
  828. }
  829. return "";
  830. }
  831. private function getHelpText($pref_name) {
  832. if (isset($this->pref_help[$pref_name])) {
  833. return $this->pref_help[$pref_name][1];
  834. }
  835. return "";
  836. }
  837. }