prefs.php 36 KB

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