prefs.php 36 KB

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