functions.php 69 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546
  1. <?php
  2. define('EXPECTED_CONFIG_VERSION', 26);
  3. define('SCHEMA_VERSION', 132);
  4. define('LABEL_BASE_INDEX', -1024);
  5. define('PLUGIN_FEED_BASE_INDEX', -128);
  6. define('COOKIE_LIFETIME_LONG', 86400*365);
  7. $fetch_last_error = false;
  8. $fetch_last_error_code = false;
  9. $fetch_last_content_type = false;
  10. $fetch_last_error_content = false; // curl only for the time being
  11. $fetch_curl_used = false;
  12. $suppress_debugging = false;
  13. libxml_disable_entity_loader(true);
  14. // separate test because this is included before sanity checks
  15. if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
  16. date_default_timezone_set('UTC');
  17. if (defined('E_DEPRECATED')) {
  18. error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
  19. } else {
  20. error_reporting(E_ALL & ~E_NOTICE);
  21. }
  22. require_once 'config.php';
  23. /**
  24. * Define a constant if not already defined
  25. */
  26. function define_default($name, $value) {
  27. defined($name) or define($name, $value);
  28. }
  29. /* Some tunables you can override in config.php using define(): */
  30. define_default('FEED_FETCH_TIMEOUT', 45);
  31. // How may seconds to wait for response when requesting feed from a site
  32. define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
  33. // How may seconds to wait for response when requesting feed from a
  34. // site when that feed wasn't cached before
  35. define_default('FILE_FETCH_TIMEOUT', 45);
  36. // Default timeout when fetching files from remote sites
  37. define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
  38. // How many seconds to wait for initial response from website when
  39. // fetching files from remote sites
  40. define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
  41. // stop updating feeds if users haven't logged in for X days
  42. define_default('DAEMON_FEED_LIMIT', 500);
  43. // feed limit for one update batch
  44. define_default('DAEMON_SLEEP_INTERVAL', 120);
  45. // default sleep interval between feed updates (sec)
  46. define_default('MIN_CACHE_FILE_SIZE', 1024);
  47. // do not cache files smaller than that (bytes)
  48. define_default('CACHE_MAX_DAYS', 7);
  49. // max age in days for various automatically cached (temporary) files
  50. /* tunables end here */
  51. if (DB_TYPE == "pgsql") {
  52. define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
  53. } else {
  54. define('SUBSTRING_FOR_DATE', 'SUBSTRING');
  55. }
  56. /**
  57. * Return available translations names.
  58. *
  59. * @access public
  60. * @return array A array of available translations.
  61. */
  62. function get_translations() {
  63. $tr = array(
  64. "auto" => "Detect automatically",
  65. "ar_SA" => "العربيّة (Arabic)",
  66. "bg_BG" => "Bulgarian",
  67. "da_DA" => "Dansk",
  68. "ca_CA" => "Català",
  69. "cs_CZ" => "Česky",
  70. "en_US" => "English",
  71. "el_GR" => "Ελληνικά",
  72. "es_ES" => "Español (España)",
  73. "es_LA" => "Español",
  74. "de_DE" => "Deutsch",
  75. "fr_FR" => "Français",
  76. "hu_HU" => "Magyar (Hungarian)",
  77. "it_IT" => "Italiano",
  78. "ja_JP" => "日本語 (Japanese)",
  79. "lv_LV" => "Latviešu",
  80. "nb_NO" => "Norwegian bokmål",
  81. "nl_NL" => "Dutch",
  82. "pl_PL" => "Polski",
  83. "ru_RU" => "Русский",
  84. "pt_BR" => "Portuguese/Brazil",
  85. "pt_PT" => "Portuguese/Portugal",
  86. "zh_CN" => "Simplified Chinese",
  87. "zh_TW" => "Traditional Chinese",
  88. "sv_SE" => "Svenska",
  89. "fi_FI" => "Suomi",
  90. "tr_TR" => "Türkçe");
  91. return $tr;
  92. }
  93. require_once "lib/accept-to-gettext.php";
  94. require_once "lib/gettext/gettext.inc";
  95. function startup_gettext() {
  96. # Get locale from Accept-Language header
  97. $lang = al2gt(array_keys(get_translations()), "text/html");
  98. if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
  99. $lang = _TRANSLATION_OVERRIDE_DEFAULT;
  100. }
  101. if ($_SESSION["uid"] && get_schema_version() >= 120) {
  102. $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
  103. if ($pref_lang && $pref_lang != 'auto') {
  104. $lang = $pref_lang;
  105. }
  106. }
  107. if ($lang) {
  108. if (defined('LC_MESSAGES')) {
  109. _setlocale(LC_MESSAGES, $lang);
  110. } else if (defined('LC_ALL')) {
  111. _setlocale(LC_ALL, $lang);
  112. }
  113. _bindtextdomain("messages", "locale");
  114. _textdomain("messages");
  115. _bind_textdomain_codeset("messages", "UTF-8");
  116. }
  117. }
  118. require_once 'db-prefs.php';
  119. require_once 'version.php';
  120. require_once 'controls.php';
  121. define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION . ' (http://tt-rss.org/)');
  122. ini_set('user_agent', SELF_USER_AGENT);
  123. $schema_version = false;
  124. function _debug_suppress($suppress) {
  125. global $suppress_debugging;
  126. $suppress_debugging = $suppress;
  127. }
  128. /**
  129. * Print a timestamped debug message.
  130. *
  131. * @param string $msg The debug message.
  132. * @return void
  133. */
  134. function _debug($msg, $show = true) {
  135. global $suppress_debugging;
  136. //echo "[$suppress_debugging] $msg $show\n";
  137. if ($suppress_debugging) return false;
  138. $ts = strftime("%H:%M:%S", time());
  139. if (function_exists('posix_getpid')) {
  140. $ts = "$ts/" . posix_getpid();
  141. }
  142. if ($show && !(defined('QUIET') && QUIET)) {
  143. print "[$ts] $msg\n";
  144. }
  145. if (defined('LOGFILE')) {
  146. $fp = fopen(LOGFILE, 'a+');
  147. if ($fp) {
  148. $locked = false;
  149. if (function_exists("flock")) {
  150. $tries = 0;
  151. // try to lock logfile for writing
  152. while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB)) {
  153. sleep(1);
  154. ++$tries;
  155. }
  156. if (!$locked) {
  157. fclose($fp);
  158. return;
  159. }
  160. }
  161. fputs($fp, "[$ts] $msg\n");
  162. if (function_exists("flock")) {
  163. flock($fp, LOCK_UN);
  164. }
  165. fclose($fp);
  166. }
  167. }
  168. } // function _debug
  169. /**
  170. * Purge a feed old posts.
  171. *
  172. * @param mixed $link A database connection.
  173. * @param mixed $feed_id The id of the purged feed.
  174. * @param mixed $purge_interval Olderness of purged posts.
  175. * @param boolean $debug Set to True to enable the debug. False by default.
  176. * @access public
  177. * @return void
  178. */
  179. function purge_feed($feed_id, $purge_interval, $debug = false) {
  180. if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
  181. $rows = -1;
  182. $result = db_query(
  183. "SELECT owner_uid FROM ttrss_feeds WHERE id = '$feed_id'");
  184. $owner_uid = false;
  185. if (db_num_rows($result) == 1) {
  186. $owner_uid = db_fetch_result($result, 0, "owner_uid");
  187. }
  188. if ($purge_interval == -1 || !$purge_interval) {
  189. if ($owner_uid) {
  190. CCache::update($feed_id, $owner_uid);
  191. }
  192. return;
  193. }
  194. if (!$owner_uid) return;
  195. if (FORCE_ARTICLE_PURGE == 0) {
  196. $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
  197. $owner_uid, false);
  198. } else {
  199. $purge_unread = true;
  200. $purge_interval = FORCE_ARTICLE_PURGE;
  201. }
  202. if (!$purge_unread) $query_limit = " unread = false AND ";
  203. if (DB_TYPE == "pgsql") {
  204. $result = db_query("DELETE FROM ttrss_user_entries
  205. USING ttrss_entries
  206. WHERE ttrss_entries.id = ref_id AND
  207. marked = false AND
  208. feed_id = '$feed_id' AND
  209. $query_limit
  210. ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
  211. } else {
  212. /* $result = db_query("DELETE FROM ttrss_user_entries WHERE
  213. marked = false AND feed_id = '$feed_id' AND
  214. (SELECT date_updated FROM ttrss_entries WHERE
  215. id = ref_id) < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)"); */
  216. $result = db_query("DELETE FROM ttrss_user_entries
  217. USING ttrss_user_entries, ttrss_entries
  218. WHERE ttrss_entries.id = ref_id AND
  219. marked = false AND
  220. feed_id = '$feed_id' AND
  221. $query_limit
  222. ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
  223. }
  224. $rows = db_affected_rows($result);
  225. CCache::update($feed_id, $owner_uid);
  226. if ($debug) {
  227. _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
  228. }
  229. return $rows;
  230. } // function purge_feed
  231. function feed_purge_interval($feed_id) {
  232. $result = db_query("SELECT purge_interval, owner_uid FROM ttrss_feeds
  233. WHERE id = '$feed_id'");
  234. if (db_num_rows($result) == 1) {
  235. $purge_interval = db_fetch_result($result, 0, "purge_interval");
  236. $owner_uid = db_fetch_result($result, 0, "owner_uid");
  237. if ($purge_interval == 0) $purge_interval = get_pref(
  238. 'PURGE_OLD_DAYS', $owner_uid);
  239. return $purge_interval;
  240. } else {
  241. return -1;
  242. }
  243. }
  244. /*function get_feed_update_interval($feed_id) {
  245. $result = db_query("SELECT owner_uid, update_interval FROM
  246. ttrss_feeds WHERE id = '$feed_id'");
  247. if (db_num_rows($result) == 1) {
  248. $update_interval = db_fetch_result($result, 0, "update_interval");
  249. $owner_uid = db_fetch_result($result, 0, "owner_uid");
  250. if ($update_interval != 0) {
  251. return $update_interval;
  252. } else {
  253. return get_pref('DEFAULT_UPDATE_INTERVAL', $owner_uid, false);
  254. }
  255. } else {
  256. return -1;
  257. }
  258. }*/
  259. // TODO: multiple-argument way is deprecated, first parameter is a hash now
  260. function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
  261. 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
  262. global $fetch_last_error;
  263. global $fetch_last_error_code;
  264. global $fetch_last_error_content;
  265. global $fetch_last_content_type;
  266. global $fetch_last_modified;
  267. global $fetch_curl_used;
  268. $fetch_last_error = false;
  269. $fetch_last_error_code = -1;
  270. $fetch_last_error_content = "";
  271. $fetch_last_content_type = "";
  272. $fetch_curl_used = false;
  273. $fetch_last_modified = "";
  274. if (!is_array($options)) {
  275. // falling back on compatibility shim
  276. $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
  277. $tmp = [];
  278. for ($i = 0; $i < func_num_args(); $i++) {
  279. $tmp[$option_names[$i]] = func_get_arg($i);
  280. }
  281. $options = $tmp;
  282. /*$options = array(
  283. "url" => func_get_arg(0),
  284. "type" => @func_get_arg(1),
  285. "login" => @func_get_arg(2),
  286. "pass" => @func_get_arg(3),
  287. "post_query" => @func_get_arg(4),
  288. "timeout" => @func_get_arg(5),
  289. "timestamp" => @func_get_arg(6),
  290. "useragent" => @func_get_arg(7)
  291. ); */
  292. }
  293. $url = $options["url"];
  294. $type = isset($options["type"]) ? $options["type"] : false;
  295. $login = isset($options["login"]) ? $options["login"] : false;
  296. $pass = isset($options["pass"]) ? $options["pass"] : false;
  297. $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
  298. $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
  299. $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
  300. $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
  301. $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
  302. $url = ltrim($url, ' ');
  303. $url = str_replace(' ', '%20', $url);
  304. if (strpos($url, "//") === 0)
  305. $url = 'http:' . $url;
  306. if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
  307. $fetch_curl_used = true;
  308. $ch = curl_init($url);
  309. if ($last_modified && !$post_query) {
  310. curl_setopt($ch, CURLOPT_HTTPHEADER,
  311. array("If-Modified-Since: $last_modified"));
  312. }
  313. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
  314. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
  315. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
  316. curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
  317. curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
  318. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  319. curl_setopt($ch, CURLOPT_HEADER, true);
  320. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
  321. curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
  322. SELF_USER_AGENT);
  323. curl_setopt($ch, CURLOPT_ENCODING, "");
  324. //curl_setopt($ch, CURLOPT_REFERER, $url);
  325. if (!ini_get("open_basedir")) {
  326. curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
  327. }
  328. if (defined('_CURL_HTTP_PROXY')) {
  329. curl_setopt($ch, CURLOPT_PROXY, _CURL_HTTP_PROXY);
  330. }
  331. if ($post_query) {
  332. curl_setopt($ch, CURLOPT_POST, true);
  333. curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
  334. }
  335. if ($login && $pass)
  336. curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
  337. $ret = @curl_exec($ch);
  338. $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
  339. $headers = explode("\r\n", substr($ret, 0, $headers_length));
  340. $contents = substr($ret, $headers_length);
  341. foreach ($headers as $header) {
  342. list ($key, $value) = explode(": ", $header);
  343. if (strtolower($key) == "last-modified") {
  344. $fetch_last_modified = $value;
  345. }
  346. }
  347. if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
  348. curl_setopt($ch, CURLOPT_ENCODING, 'none');
  349. $contents = @curl_exec($ch);
  350. }
  351. $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  352. $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  353. $fetch_last_error_code = $http_code;
  354. if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
  355. if (curl_errno($ch) != 0) {
  356. $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
  357. } else {
  358. $fetch_last_error = "HTTP Code: $http_code";
  359. }
  360. $fetch_last_error_content = $contents;
  361. curl_close($ch);
  362. return false;
  363. }
  364. if (!$contents) {
  365. $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
  366. curl_close($ch);
  367. return false;
  368. }
  369. /*$fetch_last_modified = curl_getinfo($ch, CURLINFO_FILETIME);
  370. if ($fetch_last_modified != -1) {
  371. echo date("Y-m-d H:i:s", $fetch_last_modified); die;
  372. }*/
  373. curl_close($ch);
  374. return $contents;
  375. } else {
  376. $fetch_curl_used = false;
  377. if ($login && $pass){
  378. $url_parts = array();
  379. preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
  380. $pass = urlencode($pass);
  381. if ($url_parts[1] && $url_parts[2]) {
  382. $url = $url_parts[1] . "://$login:[email protected]" . $url_parts[2];
  383. }
  384. }
  385. // TODO: should this support POST requests or not? idk
  386. if (!$post_query && $last_modified) {
  387. $context = stream_context_create(array(
  388. 'http' => array(
  389. 'method' => 'GET',
  390. 'ignore_errors' => true,
  391. 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
  392. 'protocol_version'=> 1.1,
  393. 'header' => "If-Modified-Since: $last_modified\r\n")
  394. ));
  395. } else {
  396. $context = stream_context_create(array(
  397. 'http' => array(
  398. 'method' => 'GET',
  399. 'ignore_errors' => true,
  400. 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
  401. 'protocol_version'=> 1.1
  402. )));
  403. }
  404. $old_error = error_get_last();
  405. $data = @file_get_contents($url, false, $context);
  406. if (isset($http_response_header) && is_array($http_response_header)) {
  407. foreach ($http_response_header as $h) {
  408. list ($key, $value) = explode(": ", $h);
  409. $key = strtolower($key);
  410. if ($key == 'content-type') {
  411. $fetch_last_content_type = $value;
  412. // don't abort here b/c there might be more than one
  413. // e.g. if we were being redirected -- last one is the right one
  414. } else if ($key == 'last-modified') {
  415. $fetch_last_modified = $value;
  416. }
  417. if (substr(strtolower($h), 0, 7) == 'http/1.') {
  418. $fetch_last_error_code = (int) substr($h, 9, 3);
  419. }
  420. }
  421. }
  422. if ($fetch_last_error_code != 200) {
  423. $error = error_get_last();
  424. if ($error['message'] != $old_error['message']) {
  425. $fetch_last_error = $error["message"];
  426. } else {
  427. $fetch_last_error = "HTTP Code: $fetch_last_error_code";
  428. }
  429. $fetch_last_error_content = $data;
  430. return false;
  431. }
  432. return $data;
  433. }
  434. }
  435. /**
  436. * Try to determine the favicon URL for a feed.
  437. * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
  438. * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
  439. *
  440. * @param string $url A feed or page URL
  441. * @access public
  442. * @return mixed The favicon URL, or false if none was found.
  443. */
  444. function get_favicon_url($url) {
  445. $favicon_url = false;
  446. if ($html = @fetch_file_contents($url)) {
  447. libxml_use_internal_errors(true);
  448. $doc = new DOMDocument();
  449. $doc->loadHTML($html);
  450. $xpath = new DOMXPath($doc);
  451. $base = $xpath->query('/html/head/base[@href]');
  452. foreach ($base as $b) {
  453. $url = rewrite_relative_url($url, $b->getAttribute("href"));
  454. break;
  455. }
  456. $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
  457. if (count($entries) > 0) {
  458. foreach ($entries as $entry) {
  459. $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
  460. break;
  461. }
  462. }
  463. }
  464. if (!$favicon_url)
  465. $favicon_url = rewrite_relative_url($url, "/favicon.ico");
  466. return $favicon_url;
  467. } // function get_favicon_url
  468. function initialize_user_prefs($uid, $profile = false) {
  469. $uid = db_escape_string($uid);
  470. if (!$profile) {
  471. $profile = "NULL";
  472. $profile_qpart = "AND profile IS NULL";
  473. } else {
  474. $profile_qpart = "AND profile = '$profile'";
  475. }
  476. if (get_schema_version() < 63) $profile_qpart = "";
  477. db_query("BEGIN");
  478. $result = db_query("SELECT pref_name,def_value FROM ttrss_prefs");
  479. $u_result = db_query("SELECT pref_name
  480. FROM ttrss_user_prefs WHERE owner_uid = '$uid' $profile_qpart");
  481. $active_prefs = array();
  482. while ($line = db_fetch_assoc($u_result)) {
  483. array_push($active_prefs, $line["pref_name"]);
  484. }
  485. while ($line = db_fetch_assoc($result)) {
  486. if (array_search($line["pref_name"], $active_prefs) === FALSE) {
  487. // print "adding " . $line["pref_name"] . "<br>";
  488. $line["def_value"] = db_escape_string($line["def_value"]);
  489. $line["pref_name"] = db_escape_string($line["pref_name"]);
  490. if (get_schema_version() < 63) {
  491. db_query("INSERT INTO ttrss_user_prefs
  492. (owner_uid,pref_name,value) VALUES
  493. ('$uid', '".$line["pref_name"]."','".$line["def_value"]."')");
  494. } else {
  495. db_query("INSERT INTO ttrss_user_prefs
  496. (owner_uid,pref_name,value, profile) VALUES
  497. ('$uid', '".$line["pref_name"]."','".$line["def_value"]."', $profile)");
  498. }
  499. }
  500. }
  501. db_query("COMMIT");
  502. }
  503. function get_ssl_certificate_id() {
  504. if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
  505. return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
  506. $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
  507. $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
  508. $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
  509. }
  510. if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
  511. return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
  512. $_SERVER["SSL_CLIENT_V_START"] .
  513. $_SERVER["SSL_CLIENT_V_END"] .
  514. $_SERVER["SSL_CLIENT_S_DN"]);
  515. }
  516. return "";
  517. }
  518. function authenticate_user($login, $password, $check_only = false) {
  519. if (!SINGLE_USER_MODE) {
  520. $user_id = false;
  521. foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
  522. $user_id = (int) $plugin->authenticate($login, $password);
  523. if ($user_id) {
  524. $_SESSION["auth_module"] = strtolower(get_class($plugin));
  525. break;
  526. }
  527. }
  528. if ($user_id && !$check_only) {
  529. @session_start();
  530. $_SESSION["uid"] = $user_id;
  531. $_SESSION["version"] = VERSION_STATIC;
  532. $result = db_query("SELECT login,access_level,pwd_hash FROM ttrss_users
  533. WHERE id = '$user_id'");
  534. $_SESSION["name"] = db_fetch_result($result, 0, "login");
  535. $_SESSION["access_level"] = db_fetch_result($result, 0, "access_level");
  536. $_SESSION["csrf_token"] = uniqid_short();
  537. db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
  538. $_SESSION["uid"]);
  539. $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
  540. $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
  541. $_SESSION["pwd_hash"] = db_fetch_result($result, 0, "pwd_hash");
  542. $_SESSION["last_version_check"] = time();
  543. initialize_user_prefs($_SESSION["uid"]);
  544. return true;
  545. }
  546. return false;
  547. } else {
  548. $_SESSION["uid"] = 1;
  549. $_SESSION["name"] = "admin";
  550. $_SESSION["access_level"] = 10;
  551. $_SESSION["hide_hello"] = true;
  552. $_SESSION["hide_logout"] = true;
  553. $_SESSION["auth_module"] = false;
  554. if (!$_SESSION["csrf_token"]) {
  555. $_SESSION["csrf_token"] = uniqid_short();
  556. }
  557. $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
  558. initialize_user_prefs($_SESSION["uid"]);
  559. return true;
  560. }
  561. }
  562. function make_password($length = 8) {
  563. $password = "";
  564. $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
  565. $i = 0;
  566. while ($i < $length) {
  567. $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
  568. if (!strstr($password, $char)) {
  569. $password .= $char;
  570. $i++;
  571. }
  572. }
  573. return $password;
  574. }
  575. // this is called after user is created to initialize default feeds, labels
  576. // or whatever else
  577. // user preferences are checked on every login, not here
  578. function initialize_user($uid) {
  579. db_query("insert into ttrss_feeds (owner_uid,title,feed_url)
  580. values ('$uid', 'Tiny Tiny RSS: Forum',
  581. 'http://tt-rss.org/forum/rss.php')");
  582. }
  583. function logout_user() {
  584. session_destroy();
  585. if (isset($_COOKIE[session_name()])) {
  586. setcookie(session_name(), '', time()-42000, '/');
  587. }
  588. }
  589. function validate_csrf($csrf_token) {
  590. return $csrf_token == $_SESSION['csrf_token'];
  591. }
  592. function load_user_plugins($owner_uid, $pluginhost = false) {
  593. if (!$pluginhost) $pluginhost = PluginHost::getInstance();
  594. if ($owner_uid && SCHEMA_VERSION >= 100) {
  595. $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
  596. $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
  597. if (get_schema_version() > 100) {
  598. $pluginhost->load_data();
  599. }
  600. }
  601. }
  602. function login_sequence() {
  603. if (SINGLE_USER_MODE) {
  604. @session_start();
  605. authenticate_user("admin", null);
  606. startup_gettext();
  607. load_user_plugins($_SESSION["uid"]);
  608. } else {
  609. if (!validate_session()) $_SESSION["uid"] = false;
  610. if (!$_SESSION["uid"]) {
  611. if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
  612. $_SESSION["ref_schema_version"] = get_schema_version(true);
  613. } else {
  614. authenticate_user(null, null, true);
  615. }
  616. if (!$_SESSION["uid"]) {
  617. @session_destroy();
  618. setcookie(session_name(), '', time()-42000, '/');
  619. render_login_form();
  620. exit;
  621. }
  622. } else {
  623. /* bump login timestamp */
  624. db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
  625. $_SESSION["uid"]);
  626. $_SESSION["last_login_update"] = time();
  627. }
  628. if ($_SESSION["uid"]) {
  629. startup_gettext();
  630. load_user_plugins($_SESSION["uid"]);
  631. /* cleanup ccache */
  632. db_query("DELETE FROM ttrss_counters_cache WHERE owner_uid = ".
  633. $_SESSION["uid"] . " AND
  634. (SELECT COUNT(id) FROM ttrss_feeds WHERE
  635. ttrss_feeds.id = feed_id) = 0");
  636. db_query("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ".
  637. $_SESSION["uid"] . " AND
  638. (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
  639. ttrss_feed_categories.id = feed_id) = 0");
  640. }
  641. }
  642. }
  643. function truncate_string($str, $max_len, $suffix = '&hellip;') {
  644. if (mb_strlen($str, "utf-8") > $max_len) {
  645. return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
  646. } else {
  647. return $str;
  648. }
  649. }
  650. // is not utf8 clean
  651. function truncate_middle($str, $max_len, $suffix = '&hellip;') {
  652. if (strlen($str) > $max_len) {
  653. return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
  654. } else {
  655. return $str;
  656. }
  657. }
  658. function convert_timestamp($timestamp, $source_tz, $dest_tz) {
  659. try {
  660. $source_tz = new DateTimeZone($source_tz);
  661. } catch (Exception $e) {
  662. $source_tz = new DateTimeZone('UTC');
  663. }
  664. try {
  665. $dest_tz = new DateTimeZone($dest_tz);
  666. } catch (Exception $e) {
  667. $dest_tz = new DateTimeZone('UTC');
  668. }
  669. $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
  670. return $dt->format('U') + $dest_tz->getOffset($dt);
  671. }
  672. function make_local_datetime($timestamp, $long, $owner_uid = false,
  673. $no_smart_dt = false, $eta_min = false) {
  674. if (!$owner_uid) $owner_uid = $_SESSION['uid'];
  675. if (!$timestamp) $timestamp = '1970-01-01 0:00';
  676. global $utc_tz;
  677. global $user_tz;
  678. if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
  679. $timestamp = substr($timestamp, 0, 19);
  680. # We store date in UTC internally
  681. $dt = new DateTime($timestamp, $utc_tz);
  682. $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
  683. if ($user_tz_string != 'Automatic') {
  684. try {
  685. if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
  686. } catch (Exception $e) {
  687. $user_tz = $utc_tz;
  688. }
  689. $tz_offset = $user_tz->getOffset($dt);
  690. } else {
  691. $tz_offset = (int) -$_SESSION["clientTzOffset"];
  692. }
  693. $user_timestamp = $dt->format('U') + $tz_offset;
  694. if (!$no_smart_dt) {
  695. return smart_date_time($user_timestamp,
  696. $tz_offset, $owner_uid, $eta_min);
  697. } else {
  698. if ($long)
  699. $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
  700. else
  701. $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
  702. return date($format, $user_timestamp);
  703. }
  704. }
  705. function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
  706. if (!$owner_uid) $owner_uid = $_SESSION['uid'];
  707. if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
  708. return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
  709. } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
  710. return date("G:i", $timestamp);
  711. } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
  712. $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
  713. return date($format, $timestamp);
  714. } else {
  715. $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
  716. return date($format, $timestamp);
  717. }
  718. }
  719. function sql_bool_to_bool($s) {
  720. if ($s == "t" || $s == "1" || strtolower($s) == "true") {
  721. return true;
  722. } else {
  723. return false;
  724. }
  725. }
  726. function bool_to_sql_bool($s) {
  727. if ($s) {
  728. return "true";
  729. } else {
  730. return "false";
  731. }
  732. }
  733. // Session caching removed due to causing wrong redirects to upgrade
  734. // script when get_schema_version() is called on an obsolete session
  735. // created on a previous schema version.
  736. function get_schema_version($nocache = false) {
  737. global $schema_version;
  738. if (!$schema_version && !$nocache) {
  739. $result = db_query("SELECT schema_version FROM ttrss_version");
  740. $version = db_fetch_result($result, 0, "schema_version");
  741. $schema_version = $version;
  742. return $version;
  743. } else {
  744. return $schema_version;
  745. }
  746. }
  747. function sanity_check() {
  748. require_once 'errors.php';
  749. global $ERRORS;
  750. $error_code = 0;
  751. $schema_version = get_schema_version(true);
  752. if ($schema_version != SCHEMA_VERSION) {
  753. $error_code = 5;
  754. }
  755. if (DB_TYPE == "mysql") {
  756. $result = db_query("SELECT true", false);
  757. if (db_num_rows($result) != 1) {
  758. $error_code = 10;
  759. }
  760. }
  761. if (db_escape_string("testTEST") != "testTEST") {
  762. $error_code = 12;
  763. }
  764. return array("code" => $error_code, "message" => $ERRORS[$error_code]);
  765. }
  766. function file_is_locked($filename) {
  767. if (file_exists(LOCK_DIRECTORY . "/$filename")) {
  768. if (function_exists('flock')) {
  769. $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
  770. if ($fp) {
  771. if (flock($fp, LOCK_EX | LOCK_NB)) {
  772. flock($fp, LOCK_UN);
  773. fclose($fp);
  774. return false;
  775. }
  776. fclose($fp);
  777. return true;
  778. } else {
  779. return false;
  780. }
  781. }
  782. return true; // consider the file always locked and skip the test
  783. } else {
  784. return false;
  785. }
  786. }
  787. function make_lockfile($filename) {
  788. $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
  789. if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
  790. $stat_h = fstat($fp);
  791. $stat_f = stat(LOCK_DIRECTORY . "/$filename");
  792. if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
  793. if ($stat_h["ino"] != $stat_f["ino"] ||
  794. $stat_h["dev"] != $stat_f["dev"]) {
  795. return false;
  796. }
  797. }
  798. if (function_exists('posix_getpid')) {
  799. fwrite($fp, posix_getpid() . "\n");
  800. }
  801. return $fp;
  802. } else {
  803. return false;
  804. }
  805. }
  806. function make_stampfile($filename) {
  807. $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
  808. if (flock($fp, LOCK_EX | LOCK_NB)) {
  809. fwrite($fp, time() . "\n");
  810. flock($fp, LOCK_UN);
  811. fclose($fp);
  812. return true;
  813. } else {
  814. return false;
  815. }
  816. }
  817. function sql_random_function() {
  818. if (DB_TYPE == "mysql") {
  819. return "RAND()";
  820. } else {
  821. return "RANDOM()";
  822. }
  823. }
  824. function getFeedUnread($feed, $is_cat = false) {
  825. return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
  826. }
  827. /*function get_pgsql_version() {
  828. $result = db_query("SELECT version() AS version");
  829. $version = explode(" ", db_fetch_result($result, 0, "version"));
  830. return $version[1];
  831. }*/
  832. function checkbox_to_sql_bool($val) {
  833. return ($val == "on") ? "true" : "false";
  834. }
  835. /*function getFeedCatTitle($id) {
  836. if ($id == -1) {
  837. return __("Special");
  838. } else if ($id < LABEL_BASE_INDEX) {
  839. return __("Labels");
  840. } else if ($id > 0) {
  841. $result = db_query("SELECT ttrss_feed_categories.title
  842. FROM ttrss_feeds, ttrss_feed_categories WHERE ttrss_feeds.id = '$id' AND
  843. cat_id = ttrss_feed_categories.id");
  844. if (db_num_rows($result) == 1) {
  845. return db_fetch_result($result, 0, "title");
  846. } else {
  847. return __("Uncategorized");
  848. }
  849. } else {
  850. return "getFeedCatTitle($id) failed";
  851. }
  852. }*/
  853. function uniqid_short() {
  854. return uniqid(base_convert(rand(), 10, 36));
  855. }
  856. function make_init_params() {
  857. $params = array();
  858. foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
  859. "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
  860. "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
  861. "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
  862. $params[strtolower($param)] = (int) get_pref($param);
  863. }
  864. $params["icons_url"] = ICONS_URL;
  865. $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
  866. $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
  867. $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
  868. $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
  869. $params["bw_limit"] = (int) $_SESSION["bw_limit"];
  870. $params["label_base_index"] = (int) LABEL_BASE_INDEX;
  871. $theme = get_pref( "USER_CSS_THEME", false, false);
  872. $params["theme"] = theme_valid("$theme") ? $theme : "";
  873. $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
  874. $params["php_platform"] = PHP_OS;
  875. $params["php_version"] = PHP_VERSION;
  876. $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
  877. $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
  878. ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
  879. $max_feed_id = db_fetch_result($result, 0, "mid");
  880. $num_feeds = db_fetch_result($result, 0, "nf");
  881. $params["max_feed_id"] = (int) $max_feed_id;
  882. $params["num_feeds"] = (int) $num_feeds;
  883. $params["hotkeys"] = get_hotkeys_map();
  884. $params["csrf_token"] = $_SESSION["csrf_token"];
  885. $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
  886. $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
  887. $params["icon_alert"] = base64_img("images/alert.png");
  888. $params["icon_information"] = base64_img("images/information.png");
  889. $params["icon_cross"] = base64_img("images/cross.png");
  890. $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
  891. $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
  892. return $params;
  893. }
  894. function get_hotkeys_info() {
  895. $hotkeys = array(
  896. __("Navigation") => array(
  897. "next_feed" => __("Open next feed"),
  898. "prev_feed" => __("Open previous feed"),
  899. "next_article" => __("Open next article"),
  900. "prev_article" => __("Open previous article"),
  901. "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
  902. "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
  903. "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
  904. "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
  905. "search_dialog" => __("Show search dialog")),
  906. __("Article") => array(
  907. "toggle_mark" => __("Toggle starred"),
  908. "toggle_publ" => __("Toggle published"),
  909. "toggle_unread" => __("Toggle unread"),
  910. "edit_tags" => __("Edit tags"),
  911. "open_in_new_window" => __("Open in new window"),
  912. "catchup_below" => __("Mark below as read"),
  913. "catchup_above" => __("Mark above as read"),
  914. "article_scroll_down" => __("Scroll down"),
  915. "article_scroll_up" => __("Scroll up"),
  916. "select_article_cursor" => __("Select article under cursor"),
  917. "email_article" => __("Email article"),
  918. "close_article" => __("Close/collapse article"),
  919. "toggle_expand" => __("Toggle article expansion (combined mode)"),
  920. "toggle_widescreen" => __("Toggle widescreen mode"),
  921. "toggle_embed_original" => __("Toggle embed original")),
  922. __("Article selection") => array(
  923. "select_all" => __("Select all articles"),
  924. "select_unread" => __("Select unread"),
  925. "select_marked" => __("Select starred"),
  926. "select_published" => __("Select published"),
  927. "select_invert" => __("Invert selection"),
  928. "select_none" => __("Deselect everything")),
  929. __("Feed") => array(
  930. "feed_refresh" => __("Refresh current feed"),
  931. "feed_unhide_read" => __("Un/hide read feeds"),
  932. "feed_subscribe" => __("Subscribe to feed"),
  933. "feed_edit" => __("Edit feed"),
  934. "feed_catchup" => __("Mark as read"),
  935. "feed_reverse" => __("Reverse headlines"),
  936. "feed_toggle_vgroup" => __("Toggle headline grouping"),
  937. "feed_debug_update" => __("Debug feed update"),
  938. "feed_debug_viewfeed" => __("Debug viewfeed()"),
  939. "catchup_all" => __("Mark all feeds as read"),
  940. "cat_toggle_collapse" => __("Un/collapse current category"),
  941. "toggle_combined_mode" => __("Toggle combined mode"),
  942. "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
  943. __("Go to") => array(
  944. "goto_all" => __("All articles"),
  945. "goto_fresh" => __("Fresh"),
  946. "goto_marked" => __("Starred"),
  947. "goto_published" => __("Published"),
  948. "goto_tagcloud" => __("Tag cloud"),
  949. "goto_prefs" => __("Preferences")),
  950. __("Other") => array(
  951. "create_label" => __("Create label"),
  952. "create_filter" => __("Create filter"),
  953. "collapse_sidebar" => __("Un/collapse sidebar"),
  954. "help_dialog" => __("Show help dialog"))
  955. );
  956. foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
  957. $hotkeys = $plugin->hook_hotkey_info($hotkeys);
  958. }
  959. return $hotkeys;
  960. }
  961. function get_hotkeys_map() {
  962. $hotkeys = array(
  963. // "navigation" => array(
  964. "k" => "next_feed",
  965. "j" => "prev_feed",
  966. "n" => "next_article",
  967. "p" => "prev_article",
  968. "(38)|up" => "prev_article",
  969. "(40)|down" => "next_article",
  970. // "^(38)|Ctrl-up" => "prev_article_noscroll",
  971. // "^(40)|Ctrl-down" => "next_article_noscroll",
  972. "(191)|/" => "search_dialog",
  973. // "article" => array(
  974. "s" => "toggle_mark",
  975. "*s" => "toggle_publ",
  976. "u" => "toggle_unread",
  977. "*t" => "edit_tags",
  978. "o" => "open_in_new_window",
  979. "c p" => "catchup_below",
  980. "c n" => "catchup_above",
  981. "*n" => "article_scroll_down",
  982. "*p" => "article_scroll_up",
  983. "*(38)|Shift+up" => "article_scroll_up",
  984. "*(40)|Shift+down" => "article_scroll_down",
  985. "a *w" => "toggle_widescreen",
  986. "a e" => "toggle_embed_original",
  987. "e" => "email_article",
  988. "a q" => "close_article",
  989. // "article_selection" => array(
  990. "a a" => "select_all",
  991. "a u" => "select_unread",
  992. "a *u" => "select_marked",
  993. "a p" => "select_published",
  994. "a i" => "select_invert",
  995. "a n" => "select_none",
  996. // "feed" => array(
  997. "f r" => "feed_refresh",
  998. "f a" => "feed_unhide_read",
  999. "f s" => "feed_subscribe",
  1000. "f e" => "feed_edit",
  1001. "f q" => "feed_catchup",
  1002. "f x" => "feed_reverse",
  1003. "f g" => "feed_toggle_vgroup",
  1004. "f *d" => "feed_debug_update",
  1005. "f *g" => "feed_debug_viewfeed",
  1006. "f *c" => "toggle_combined_mode",
  1007. "f c" => "toggle_cdm_expanded",
  1008. "*q" => "catchup_all",
  1009. "x" => "cat_toggle_collapse",
  1010. // "goto" => array(
  1011. "g a" => "goto_all",
  1012. "g f" => "goto_fresh",
  1013. "g s" => "goto_marked",
  1014. "g p" => "goto_published",
  1015. "g t" => "goto_tagcloud",
  1016. "g *p" => "goto_prefs",
  1017. // "other" => array(
  1018. "(9)|Tab" => "select_article_cursor", // tab
  1019. "c l" => "create_label",
  1020. "c f" => "create_filter",
  1021. "c s" => "collapse_sidebar",
  1022. "^(191)|Ctrl+/" => "help_dialog",
  1023. );
  1024. if (get_pref('COMBINED_DISPLAY_MODE')) {
  1025. $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
  1026. $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
  1027. }
  1028. foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
  1029. $hotkeys = $plugin->hook_hotkey_map($hotkeys);
  1030. }
  1031. $prefixes = array();
  1032. foreach (array_keys($hotkeys) as $hotkey) {
  1033. $pair = explode(" ", $hotkey, 2);
  1034. if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
  1035. array_push($prefixes, $pair[0]);
  1036. }
  1037. }
  1038. return array($prefixes, $hotkeys);
  1039. }
  1040. function check_for_update() {
  1041. if (defined("GIT_VERSION_TIMESTAMP")) {
  1042. $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
  1043. if ($content) {
  1044. $content = json_decode($content, true);
  1045. if ($content && isset($content["changeset"])) {
  1046. if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
  1047. GIT_VERSION_HEAD != $content["changeset"]["id"]) {
  1048. return $content["changeset"]["id"];
  1049. }
  1050. }
  1051. }
  1052. }
  1053. return "";
  1054. }
  1055. function make_runtime_info($disable_update_check = false) {
  1056. $data = array();
  1057. $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
  1058. ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
  1059. $max_feed_id = db_fetch_result($result, 0, "mid");
  1060. $num_feeds = db_fetch_result($result, 0, "nf");
  1061. $data["max_feed_id"] = (int) $max_feed_id;
  1062. $data["num_feeds"] = (int) $num_feeds;
  1063. $data['last_article_id'] = Article::getLastArticleId();
  1064. $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
  1065. $data['dep_ts'] = calculate_dep_timestamp();
  1066. $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
  1067. $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
  1068. if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
  1069. $update_result = @check_for_update();
  1070. $data["update_result"] = $update_result;
  1071. $_SESSION["last_version_check"] = time();
  1072. }
  1073. if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
  1074. $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
  1075. if (time() - $_SESSION["daemon_stamp_check"] > 30) {
  1076. $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
  1077. if ($stamp) {
  1078. $stamp_delta = time() - $stamp;
  1079. if ($stamp_delta > 1800) {
  1080. $stamp_check = 0;
  1081. } else {
  1082. $stamp_check = 1;
  1083. $_SESSION["daemon_stamp_check"] = time();
  1084. }
  1085. $data['daemon_stamp_ok'] = $stamp_check;
  1086. $stamp_fmt = date("Y.m.d, G:i", $stamp);
  1087. $data['daemon_stamp'] = $stamp_fmt;
  1088. }
  1089. }
  1090. }
  1091. return $data;
  1092. }
  1093. function search_to_sql($search, $search_language) {
  1094. $keywords = str_getcsv(trim($search), " ");
  1095. $query_keywords = array();
  1096. $search_words = array();
  1097. $search_query_leftover = array();
  1098. if ($search_language)
  1099. $search_language = db_escape_string(mb_strtolower($search_language));
  1100. else
  1101. $search_language = "english";
  1102. foreach ($keywords as $k) {
  1103. if (strpos($k, "-") === 0) {
  1104. $k = substr($k, 1);
  1105. $not = "NOT";
  1106. } else {
  1107. $not = "";
  1108. }
  1109. $commandpair = explode(":", mb_strtolower($k), 2);
  1110. switch ($commandpair[0]) {
  1111. case "title":
  1112. if ($commandpair[1]) {
  1113. array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
  1114. db_escape_string(mb_strtolower($commandpair[1]))."%'))");
  1115. } else {
  1116. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1117. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1118. array_push($search_words, $k);
  1119. }
  1120. break;
  1121. case "author":
  1122. if ($commandpair[1]) {
  1123. array_push($query_keywords, "($not (LOWER(author) LIKE '%".
  1124. db_escape_string(mb_strtolower($commandpair[1]))."%'))");
  1125. } else {
  1126. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1127. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1128. array_push($search_words, $k);
  1129. }
  1130. break;
  1131. case "note":
  1132. if ($commandpair[1]) {
  1133. if ($commandpair[1] == "true")
  1134. array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
  1135. else if ($commandpair[1] == "false")
  1136. array_push($query_keywords, "($not (note IS NULL OR note = ''))");
  1137. else
  1138. array_push($query_keywords, "($not (LOWER(note) LIKE '%".
  1139. db_escape_string(mb_strtolower($commandpair[1]))."%'))");
  1140. } else {
  1141. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1142. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1143. if (!$not) array_push($search_words, $k);
  1144. }
  1145. break;
  1146. case "star":
  1147. if ($commandpair[1]) {
  1148. if ($commandpair[1] == "true")
  1149. array_push($query_keywords, "($not (marked = true))");
  1150. else
  1151. array_push($query_keywords, "($not (marked = false))");
  1152. } else {
  1153. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1154. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1155. if (!$not) array_push($search_words, $k);
  1156. }
  1157. break;
  1158. case "pub":
  1159. if ($commandpair[1]) {
  1160. if ($commandpair[1] == "true")
  1161. array_push($query_keywords, "($not (published = true))");
  1162. else
  1163. array_push($query_keywords, "($not (published = false))");
  1164. } else {
  1165. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1166. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1167. if (!$not) array_push($search_words, $k);
  1168. }
  1169. break;
  1170. case "unread":
  1171. if ($commandpair[1]) {
  1172. if ($commandpair[1] == "true")
  1173. array_push($query_keywords, "($not (unread = true))");
  1174. else
  1175. array_push($query_keywords, "($not (unread = false))");
  1176. } else {
  1177. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1178. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1179. if (!$not) array_push($search_words, $k);
  1180. }
  1181. break;
  1182. default:
  1183. if (strpos($k, "@") === 0) {
  1184. $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
  1185. $orig_ts = strtotime(substr($k, 1));
  1186. $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
  1187. //$k = date("Y-m-d", strtotime(substr($k, 1)));
  1188. array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
  1189. } else {
  1190. if (DB_TYPE == "pgsql") {
  1191. $k = mb_strtolower($k);
  1192. array_push($search_query_leftover, $not ? "!$k" : $k);
  1193. } else {
  1194. array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
  1195. OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
  1196. }
  1197. if (!$not) array_push($search_words, $k);
  1198. }
  1199. }
  1200. }
  1201. if (count($search_query_leftover) > 0) {
  1202. $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
  1203. if (DB_TYPE == "pgsql") {
  1204. array_push($query_keywords,
  1205. "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
  1206. }
  1207. }
  1208. $search_query_part = implode("AND", $query_keywords);
  1209. return array($search_query_part, $search_words);
  1210. }
  1211. function iframe_whitelisted($entry) {
  1212. $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
  1213. @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
  1214. if ($src) {
  1215. foreach ($whitelist as $w) {
  1216. if ($src == $w || $src == "www.$w")
  1217. return true;
  1218. }
  1219. }
  1220. return false;
  1221. }
  1222. function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
  1223. if (!$owner) $owner = $_SESSION["uid"];
  1224. $res = trim($str); if (!$res) return '';
  1225. $charset_hack = '<head>
  1226. <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  1227. </head>';
  1228. $res = trim($res); if (!$res) return '';
  1229. libxml_use_internal_errors(true);
  1230. $doc = new DOMDocument();
  1231. $doc->loadHTML($charset_hack . $res);
  1232. $xpath = new DOMXPath($doc);
  1233. $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
  1234. $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
  1235. foreach ($entries as $entry) {
  1236. if ($entry->hasAttribute('href')) {
  1237. $entry->setAttribute('href',
  1238. rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
  1239. $entry->setAttribute('rel', 'noopener noreferrer');
  1240. }
  1241. if ($entry->hasAttribute('src')) {
  1242. $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
  1243. $cached_filename = CACHE_DIR . '/images/' . sha1($src);
  1244. if (file_exists($cached_filename)) {
  1245. // this is strictly cosmetic
  1246. if ($entry->tagName == 'img') {
  1247. $suffix = ".png";
  1248. } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
  1249. $suffix = ".mp4";
  1250. } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
  1251. $suffix = ".ogg";
  1252. } else {
  1253. $suffix = "";
  1254. }
  1255. $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
  1256. if ($entry->hasAttribute('srcset')) {
  1257. $entry->removeAttribute('srcset');
  1258. }
  1259. if ($entry->hasAttribute('sizes')) {
  1260. $entry->removeAttribute('sizes');
  1261. }
  1262. }
  1263. $entry->setAttribute('src', $src);
  1264. }
  1265. if ($entry->nodeName == 'img') {
  1266. if ($entry->hasAttribute('src')) {
  1267. $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
  1268. if (is_prefix_https() && !$is_https_url) {
  1269. if ($entry->hasAttribute('srcset')) {
  1270. $entry->removeAttribute('srcset');
  1271. }
  1272. if ($entry->hasAttribute('sizes')) {
  1273. $entry->removeAttribute('sizes');
  1274. }
  1275. }
  1276. }
  1277. if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
  1278. $force_remove_images || $_SESSION["bw_limit"]) {
  1279. $p = $doc->createElement('p');
  1280. $a = $doc->createElement('a');
  1281. $a->setAttribute('href', $entry->getAttribute('src'));
  1282. $a->appendChild(new DOMText($entry->getAttribute('src')));
  1283. $a->setAttribute('target', '_blank');
  1284. $a->setAttribute('rel', 'noopener noreferrer');
  1285. $p->appendChild($a);
  1286. $entry->parentNode->replaceChild($p, $entry);
  1287. }
  1288. }
  1289. if (strtolower($entry->nodeName) == "a") {
  1290. $entry->setAttribute("target", "_blank");
  1291. $entry->setAttribute("rel", "noopener noreferrer");
  1292. }
  1293. }
  1294. $entries = $xpath->query('//iframe');
  1295. foreach ($entries as $entry) {
  1296. if (!iframe_whitelisted($entry)) {
  1297. $entry->setAttribute('sandbox', 'allow-scripts');
  1298. } else {
  1299. if (is_prefix_https()) {
  1300. $entry->setAttribute("src",
  1301. str_replace("http://", "https://",
  1302. $entry->getAttribute("src")));
  1303. }
  1304. }
  1305. }
  1306. $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
  1307. 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
  1308. 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
  1309. 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
  1310. 'dt', 'em', 'footer', 'figure', 'figcaption',
  1311. 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
  1312. 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
  1313. 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
  1314. 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
  1315. 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
  1316. 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
  1317. if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
  1318. $disallowed_attributes = array('id', 'style', 'class');
  1319. foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
  1320. $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
  1321. if (is_array($retval)) {
  1322. $doc = $retval[0];
  1323. $allowed_elements = $retval[1];
  1324. $disallowed_attributes = $retval[2];
  1325. } else {
  1326. $doc = $retval;
  1327. }
  1328. }
  1329. $doc->removeChild($doc->firstChild); //remove doctype
  1330. $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
  1331. if ($highlight_words) {
  1332. foreach ($highlight_words as $word) {
  1333. // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
  1334. $elements = $xpath->query("//*/text()");
  1335. foreach ($elements as $child) {
  1336. $fragment = $doc->createDocumentFragment();
  1337. $text = $child->textContent;
  1338. while (($pos = mb_stripos($text, $word)) !== false) {
  1339. $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
  1340. $word = mb_substr($text, $pos, mb_strlen($word));
  1341. $highlight = $doc->createElement('span');
  1342. $highlight->appendChild(new DomText($word));
  1343. $highlight->setAttribute('class', 'highlight');
  1344. $fragment->appendChild($highlight);
  1345. $text = mb_substr($text, $pos + mb_strlen($word));
  1346. }
  1347. if (!empty($text)) $fragment->appendChild(new DomText($text));
  1348. $child->parentNode->replaceChild($fragment, $child);
  1349. }
  1350. }
  1351. }
  1352. $res = $doc->saveHTML();
  1353. /* strip everything outside of <body>...</body> */
  1354. $res_frag = array();
  1355. if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
  1356. return $res_frag[1];
  1357. } else {
  1358. return $res;
  1359. }
  1360. }
  1361. function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
  1362. $xpath = new DOMXPath($doc);
  1363. $entries = $xpath->query('//*');
  1364. foreach ($entries as $entry) {
  1365. if (!in_array($entry->nodeName, $allowed_elements)) {
  1366. $entry->parentNode->removeChild($entry);
  1367. }
  1368. if ($entry->hasAttributes()) {
  1369. $attrs_to_remove = array();
  1370. foreach ($entry->attributes as $attr) {
  1371. if (strpos($attr->nodeName, 'on') === 0) {
  1372. array_push($attrs_to_remove, $attr);
  1373. }
  1374. if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
  1375. array_push($attrs_to_remove, $attr);
  1376. }
  1377. if (in_array($attr->nodeName, $disallowed_attributes)) {
  1378. array_push($attrs_to_remove, $attr);
  1379. }
  1380. }
  1381. foreach ($attrs_to_remove as $attr) {
  1382. $entry->removeAttributeNode($attr);
  1383. }
  1384. }
  1385. }
  1386. return $doc;
  1387. }
  1388. function trim_array($array) {
  1389. $tmp = $array;
  1390. array_walk($tmp, 'trim');
  1391. return $tmp;
  1392. }
  1393. function tag_is_valid($tag) {
  1394. if ($tag == '') return false;
  1395. if (is_numeric($tag)) return false;
  1396. if (mb_strlen($tag) > 250) return false;
  1397. if (!$tag) return false;
  1398. return true;
  1399. }
  1400. function render_login_form() {
  1401. header('Cache-Control: public');
  1402. require_once "login_form.php";
  1403. exit;
  1404. }
  1405. function T_sprintf() {
  1406. $args = func_get_args();
  1407. return vsprintf(__(array_shift($args)), $args);
  1408. }
  1409. function print_checkpoint($n, $s) {
  1410. $ts = microtime(true);
  1411. echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
  1412. return $ts;
  1413. }
  1414. function sanitize_tag($tag) {
  1415. $tag = trim($tag);
  1416. $tag = mb_strtolower($tag, 'utf-8');
  1417. $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
  1418. if (DB_TYPE == "mysql") {
  1419. $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
  1420. }
  1421. return $tag;
  1422. }
  1423. function is_server_https() {
  1424. return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
  1425. }
  1426. function is_prefix_https() {
  1427. return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
  1428. }
  1429. // this returns SELF_URL_PATH sans ending slash
  1430. function get_self_url_prefix() {
  1431. if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
  1432. return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
  1433. } else {
  1434. return SELF_URL_PATH;
  1435. }
  1436. }
  1437. function encrypt_password($pass, $salt = '', $mode2 = false) {
  1438. if ($salt && $mode2) {
  1439. return "MODE2:" . hash('sha256', $salt . $pass);
  1440. } else if ($salt) {
  1441. return "SHA1X:" . sha1("$salt:$pass");
  1442. } else {
  1443. return "SHA1:" . sha1($pass);
  1444. }
  1445. } // function encrypt_password
  1446. function load_filters($feed_id, $owner_uid) {
  1447. $filters = array();
  1448. $cat_id = (int)Feeds::getFeedCategory($feed_id);
  1449. if ($cat_id == 0)
  1450. $null_cat_qpart = "cat_id IS NULL OR";
  1451. else
  1452. $null_cat_qpart = "";
  1453. $result = db_query("SELECT * FROM ttrss_filters2 WHERE
  1454. owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
  1455. $check_cats = array_merge(
  1456. Feeds::getParentCategories($cat_id, $owner_uid),
  1457. [$cat_id]);
  1458. $check_cats_str = join(",", $check_cats);
  1459. $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
  1460. while ($line = db_fetch_assoc($result)) {
  1461. $filter_id = $line["id"];
  1462. $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
  1463. $result2 = db_query("SELECT
  1464. r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
  1465. FROM ttrss_filters2_rules AS r,
  1466. ttrss_filter_types AS t
  1467. WHERE
  1468. (match_on IS NOT NULL OR
  1469. (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
  1470. (feed_id IS NULL OR feed_id = '$feed_id'))) AND
  1471. filter_type = t.id AND filter_id = '$filter_id'");
  1472. $rules = array();
  1473. $actions = array();
  1474. while ($rule_line = db_fetch_assoc($result2)) {
  1475. # print_r($rule_line);
  1476. if ($rule_line["match_on"]) {
  1477. $match_on = json_decode($rule_line["match_on"], true);
  1478. if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
  1479. $rule = array();
  1480. $rule["reg_exp"] = $rule_line["reg_exp"];
  1481. $rule["type"] = $rule_line["type_name"];
  1482. $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
  1483. array_push($rules, $rule);
  1484. } else if (!$match_any_rule) {
  1485. // this filter contains a rule that doesn't match to this feed/category combination
  1486. // thus filter has to be rejected
  1487. $rules = [];
  1488. break;
  1489. }
  1490. } else {
  1491. $rule = array();
  1492. $rule["reg_exp"] = $rule_line["reg_exp"];
  1493. $rule["type"] = $rule_line["type_name"];
  1494. $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
  1495. array_push($rules, $rule);
  1496. }
  1497. }
  1498. if (count($rules) > 0) {
  1499. $result2 = db_query("SELECT a.action_param,t.name AS type_name
  1500. FROM ttrss_filters2_actions AS a,
  1501. ttrss_filter_actions AS t
  1502. WHERE
  1503. action_id = t.id AND filter_id = '$filter_id'");
  1504. while ($action_line = db_fetch_assoc($result2)) {
  1505. # print_r($action_line);
  1506. $action = array();
  1507. $action["type"] = $action_line["type_name"];
  1508. $action["param"] = $action_line["action_param"];
  1509. array_push($actions, $action);
  1510. }
  1511. }
  1512. $filter = array();
  1513. $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
  1514. $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
  1515. $filter["rules"] = $rules;
  1516. $filter["actions"] = $actions;
  1517. if (count($rules) > 0 && count($actions) > 0) {
  1518. array_push($filters, $filter);
  1519. }
  1520. }
  1521. return $filters;
  1522. }
  1523. function get_score_pic($score) {
  1524. if ($score > 100) {
  1525. return "score_high.png";
  1526. } else if ($score > 0) {
  1527. return "score_half_high.png";
  1528. } else if ($score < -100) {
  1529. return "score_low.png";
  1530. } else if ($score < 0) {
  1531. return "score_half_low.png";
  1532. } else {
  1533. return "score_neutral.png";
  1534. }
  1535. }
  1536. function feed_has_icon($id) {
  1537. return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
  1538. }
  1539. function init_plugins() {
  1540. PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
  1541. return true;
  1542. }
  1543. function add_feed_category($feed_cat, $parent_cat_id = false) {
  1544. if (!$feed_cat) return false;
  1545. db_query("BEGIN");
  1546. if ($parent_cat_id) {
  1547. $parent_qpart = "parent_cat = '$parent_cat_id'";
  1548. $parent_insert = "'$parent_cat_id'";
  1549. } else {
  1550. $parent_qpart = "parent_cat IS NULL";
  1551. $parent_insert = "NULL";
  1552. }
  1553. $feed_cat = mb_substr($feed_cat, 0, 250);
  1554. $result = db_query(
  1555. "SELECT id FROM ttrss_feed_categories
  1556. WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
  1557. if (db_num_rows($result) == 0) {
  1558. $result = db_query(
  1559. "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
  1560. VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
  1561. db_query("COMMIT");
  1562. return true;
  1563. }
  1564. return false;
  1565. }
  1566. /**
  1567. * Fixes incomplete URLs by prepending "http://".
  1568. * Also replaces feed:// with http://, and
  1569. * prepends a trailing slash if the url is a domain name only.
  1570. *
  1571. * @param string $url Possibly incomplete URL
  1572. *
  1573. * @return string Fixed URL.
  1574. */
  1575. function fix_url($url) {
  1576. // support schema-less urls
  1577. if (strpos($url, '//') === 0) {
  1578. $url = 'https:' . $url;
  1579. }
  1580. if (strpos($url, '://') === false) {
  1581. $url = 'http://' . $url;
  1582. } else if (substr($url, 0, 5) == 'feed:') {
  1583. $url = 'http:' . substr($url, 5);
  1584. }
  1585. //prepend slash if the URL has no slash in it
  1586. // "http://www.example" -> "http://www.example/"
  1587. if (strpos($url, '/', strpos($url, ':') + 3) === false) {
  1588. $url .= '/';
  1589. }
  1590. //convert IDNA hostname to punycode if possible
  1591. if (function_exists("idn_to_ascii")) {
  1592. $parts = parse_url($url);
  1593. if (mb_detect_encoding($parts['host']) != 'ASCII')
  1594. {
  1595. $parts['host'] = idn_to_ascii($parts['host']);
  1596. $url = build_url($parts);
  1597. }
  1598. }
  1599. if ($url != "http:///")
  1600. return $url;
  1601. else
  1602. return '';
  1603. }
  1604. function validate_feed_url($url) {
  1605. $parts = parse_url($url);
  1606. return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
  1607. }
  1608. /* function save_email_address($email) {
  1609. // FIXME: implement persistent storage of emails
  1610. if (!$_SESSION['stored_emails'])
  1611. $_SESSION['stored_emails'] = array();
  1612. if (!in_array($email, $_SESSION['stored_emails']))
  1613. array_push($_SESSION['stored_emails'], $email);
  1614. } */
  1615. function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
  1616. if (!$owner_uid) $owner_uid = $_SESSION["uid"];
  1617. $sql_is_cat = bool_to_sql_bool($is_cat);
  1618. $result = db_query("SELECT access_key FROM ttrss_access_keys
  1619. WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
  1620. AND owner_uid = " . $owner_uid);
  1621. if (db_num_rows($result) == 1) {
  1622. return db_fetch_result($result, 0, "access_key");
  1623. } else {
  1624. $key = db_escape_string(uniqid_short());
  1625. $result = db_query("INSERT INTO ttrss_access_keys
  1626. (access_key, feed_id, is_cat, owner_uid)
  1627. VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
  1628. return $key;
  1629. }
  1630. return false;
  1631. }
  1632. function get_feeds_from_html($url, $content)
  1633. {
  1634. $url = fix_url($url);
  1635. $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
  1636. libxml_use_internal_errors(true);
  1637. $doc = new DOMDocument();
  1638. $doc->loadHTML($content);
  1639. $xpath = new DOMXPath($doc);
  1640. $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
  1641. '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
  1642. $feedUrls = array();
  1643. foreach ($entries as $entry) {
  1644. if ($entry->hasAttribute('href')) {
  1645. $title = $entry->getAttribute('title');
  1646. if ($title == '') {
  1647. $title = $entry->getAttribute('type');
  1648. }
  1649. $feedUrl = rewrite_relative_url(
  1650. $baseUrl, $entry->getAttribute('href')
  1651. );
  1652. $feedUrls[$feedUrl] = $title;
  1653. }
  1654. }
  1655. return $feedUrls;
  1656. }
  1657. function is_html($content) {
  1658. return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
  1659. }
  1660. function url_is_html($url, $login = false, $pass = false) {
  1661. return is_html(fetch_file_contents($url, false, $login, $pass));
  1662. }
  1663. function build_url($parts) {
  1664. return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
  1665. }
  1666. function cleanup_url_path($path) {
  1667. $path = str_replace("/./", "/", $path);
  1668. $path = str_replace("//", "/", $path);
  1669. return $path;
  1670. }
  1671. /**
  1672. * Converts a (possibly) relative URL to a absolute one.
  1673. *
  1674. * @param string $url Base URL (i.e. from where the document is)
  1675. * @param string $rel_url Possibly relative URL in the document
  1676. *
  1677. * @return string Absolute URL
  1678. */
  1679. function rewrite_relative_url($url, $rel_url) {
  1680. if (strpos($rel_url, "://") !== false) {
  1681. return $rel_url;
  1682. } else if (strpos($rel_url, "//") === 0) {
  1683. # protocol-relative URL (rare but they exist)
  1684. return $rel_url;
  1685. } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
  1686. # magnet:, feed:, etc
  1687. return $rel_url;
  1688. } else if (strpos($rel_url, "/") === 0) {
  1689. $parts = parse_url($url);
  1690. $parts['path'] = $rel_url;
  1691. $parts['path'] = cleanup_url_path($parts['path']);
  1692. return build_url($parts);
  1693. } else {
  1694. $parts = parse_url($url);
  1695. if (!isset($parts['path'])) {
  1696. $parts['path'] = '/';
  1697. }
  1698. $dir = $parts['path'];
  1699. if (substr($dir, -1) !== '/') {
  1700. $dir = dirname($parts['path']);
  1701. $dir !== '/' && $dir .= '/';
  1702. }
  1703. $parts['path'] = $dir . $rel_url;
  1704. $parts['path'] = cleanup_url_path($parts['path']);
  1705. return build_url($parts);
  1706. }
  1707. }
  1708. function cleanup_tags($days = 14, $limit = 1000) {
  1709. if (DB_TYPE == "pgsql") {
  1710. $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
  1711. } else if (DB_TYPE == "mysql") {
  1712. $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
  1713. }
  1714. $tags_deleted = 0;
  1715. while ($limit > 0) {
  1716. $limit_part = 500;
  1717. $query = "SELECT ttrss_tags.id AS id
  1718. FROM ttrss_tags, ttrss_user_entries, ttrss_entries
  1719. WHERE post_int_id = int_id AND $interval_query AND
  1720. ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
  1721. $result = db_query($query);
  1722. $ids = array();
  1723. while ($line = db_fetch_assoc($result)) {
  1724. array_push($ids, $line['id']);
  1725. }
  1726. if (count($ids) > 0) {
  1727. $ids = join(",", $ids);
  1728. $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
  1729. $tags_deleted += db_affected_rows($tmp_result);
  1730. } else {
  1731. break;
  1732. }
  1733. $limit -= $limit_part;
  1734. }
  1735. return $tags_deleted;
  1736. }
  1737. function print_user_stylesheet() {
  1738. $value = get_pref('USER_STYLESHEET');
  1739. if ($value) {
  1740. print "<style type=\"text/css\">";
  1741. print str_replace("<br/>", "\n", $value);
  1742. print "</style>";
  1743. }
  1744. }
  1745. function filter_to_sql($filter, $owner_uid) {
  1746. $query = array();
  1747. if (DB_TYPE == "pgsql")
  1748. $reg_qpart = "~";
  1749. else
  1750. $reg_qpart = "REGEXP";
  1751. foreach ($filter["rules"] AS $rule) {
  1752. $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
  1753. $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
  1754. $rule['reg_exp']) !== FALSE;
  1755. if ($regexp_valid) {
  1756. $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
  1757. switch ($rule["type"]) {
  1758. case "title":
  1759. $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
  1760. $rule['reg_exp'] . "')";
  1761. break;
  1762. case "content":
  1763. $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
  1764. $rule['reg_exp'] . "')";
  1765. break;
  1766. case "both":
  1767. $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
  1768. $rule['reg_exp'] . "') OR LOWER(" .
  1769. "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
  1770. break;
  1771. case "tag":
  1772. $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
  1773. $rule['reg_exp'] . "')";
  1774. break;
  1775. case "link":
  1776. $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
  1777. $rule['reg_exp'] . "')";
  1778. break;
  1779. case "author":
  1780. $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
  1781. $rule['reg_exp'] . "')";
  1782. break;
  1783. }
  1784. if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
  1785. if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
  1786. $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
  1787. }
  1788. if (isset($rule["cat_id"])) {
  1789. if ($rule["cat_id"] > 0) {
  1790. $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
  1791. array_push($children, $rule["cat_id"]);
  1792. $children = join(",", $children);
  1793. $cat_qpart = "cat_id IN ($children)";
  1794. } else {
  1795. $cat_qpart = "cat_id IS NULL";
  1796. }
  1797. $qpart .= " AND $cat_qpart";
  1798. }
  1799. $qpart .= " AND feed_id IS NOT NULL";
  1800. array_push($query, "($qpart)");
  1801. }
  1802. }
  1803. if (count($query) > 0) {
  1804. $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
  1805. } else {
  1806. $fullquery = "(false)";
  1807. }
  1808. if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
  1809. return $fullquery;
  1810. }
  1811. if (!function_exists('gzdecode')) {
  1812. function gzdecode($string) { // no support for 2nd argument
  1813. return file_get_contents('compress.zlib://data:who/cares;base64,'.
  1814. base64_encode($string));
  1815. }
  1816. }
  1817. function get_random_bytes($length) {
  1818. if (function_exists('openssl_random_pseudo_bytes')) {
  1819. return openssl_random_pseudo_bytes($length);
  1820. } else {
  1821. $output = "";
  1822. for ($i = 0; $i < $length; $i++)
  1823. $output .= chr(mt_rand(0, 255));
  1824. return $output;
  1825. }
  1826. }
  1827. function read_stdin() {
  1828. $fp = fopen("php://stdin", "r");
  1829. if ($fp) {
  1830. $line = trim(fgets($fp));
  1831. fclose($fp);
  1832. return $line;
  1833. }
  1834. return null;
  1835. }
  1836. function implements_interface($class, $interface) {
  1837. return in_array($interface, class_implements($class));
  1838. }
  1839. function get_minified_js($files) {
  1840. require_once 'lib/jshrink/Minifier.php';
  1841. $rv = '';
  1842. foreach ($files as $js) {
  1843. if (!isset($_GET['debug'])) {
  1844. $cached_file = CACHE_DIR . "/js/".basename($js).".js";
  1845. if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
  1846. list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
  1847. if ($header && $contents) {
  1848. list($htag, $hversion) = explode(":", $header);
  1849. if ($htag == "tt-rss" && $hversion == VERSION) {
  1850. $rv .= $contents;
  1851. continue;
  1852. }
  1853. }
  1854. }
  1855. $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
  1856. file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
  1857. $rv .= $minified;
  1858. } else {
  1859. $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
  1860. }
  1861. }
  1862. return $rv;
  1863. }
  1864. function calculate_dep_timestamp() {
  1865. $files = array_merge(glob("js/*.js"), glob("css/*.css"));
  1866. $max_ts = -1;
  1867. foreach ($files as $file) {
  1868. if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
  1869. }
  1870. return $max_ts;
  1871. }
  1872. function T_js_decl($s1, $s2) {
  1873. if ($s1 && $s2) {
  1874. $s1 = preg_replace("/\n/", "", $s1);
  1875. $s2 = preg_replace("/\n/", "", $s2);
  1876. $s1 = preg_replace("/\"/", "\\\"", $s1);
  1877. $s2 = preg_replace("/\"/", "\\\"", $s2);
  1878. return "T_messages[\"$s1\"] = \"$s2\";\n";
  1879. }
  1880. }
  1881. function init_js_translations() {
  1882. print 'var T_messages = new Object();
  1883. function __(msg) {
  1884. if (T_messages[msg]) {
  1885. return T_messages[msg];
  1886. } else {
  1887. return msg;
  1888. }
  1889. }
  1890. function ngettext(msg1, msg2, n) {
  1891. return __((parseInt(n) > 1) ? msg2 : msg1);
  1892. }';
  1893. $l10n = _get_reader();
  1894. for ($i = 0; $i < $l10n->total; $i++) {
  1895. $orig = $l10n->get_original_string($i);
  1896. if(strpos($orig, "\000") !== FALSE) { // Plural forms
  1897. $key = explode(chr(0), $orig);
  1898. print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
  1899. print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
  1900. } else {
  1901. $translation = __($orig);
  1902. print T_js_decl($orig, $translation);
  1903. }
  1904. }
  1905. }
  1906. function get_theme_path($theme) {
  1907. $check = "themes/$theme";
  1908. if (file_exists($check)) return $check;
  1909. $check = "themes.local/$theme";
  1910. if (file_exists($check)) return $check;
  1911. }
  1912. function theme_valid($theme) {
  1913. $bundled_themes = [ "default.php", "night.css", "compact.css" ];
  1914. if (in_array($theme, $bundled_themes)) return true;
  1915. $file = "themes/" . basename($theme);
  1916. if (!file_exists($file)) $file = "themes.local/" . basename($theme);
  1917. if (file_exists($file) && is_readable($file)) {
  1918. $fh = fopen($file, "r");
  1919. if ($fh) {
  1920. $header = fgets($fh);
  1921. fclose($fh);
  1922. return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
  1923. }
  1924. }
  1925. return false;
  1926. }
  1927. /**
  1928. * @SuppressWarnings(unused)
  1929. */
  1930. function error_json($code) {
  1931. require_once "errors.php";
  1932. @$message = $ERRORS[$code];
  1933. return json_encode(array("error" =>
  1934. array("code" => $code, "message" => $message)));
  1935. }
  1936. /*function abs_to_rel_path($dir) {
  1937. $tmp = str_replace(dirname(__DIR__), "", $dir);
  1938. if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
  1939. return $tmp;
  1940. }*/
  1941. function get_upload_error_message($code) {
  1942. $errors = array(
  1943. 0 => __('There is no error, the file uploaded with success'),
  1944. 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
  1945. 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
  1946. 3 => __('The uploaded file was only partially uploaded'),
  1947. 4 => __('No file was uploaded'),
  1948. 6 => __('Missing a temporary folder'),
  1949. 7 => __('Failed to write file to disk.'),
  1950. 8 => __('A PHP extension stopped the file upload.'),
  1951. );
  1952. return $errors[$code];
  1953. }
  1954. function base64_img($filename) {
  1955. if (file_exists($filename)) {
  1956. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  1957. return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
  1958. } else {
  1959. return "";
  1960. }
  1961. }
  1962. /* this is essentially a wrapper for readfile() which allows plugins to hook
  1963. output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
  1964. hook function should return true if request was handled (or at least attempted to)
  1965. note that this can be called without user context so the plugin to handle this
  1966. should be loaded systemwide in config.php */
  1967. function send_local_file($filename) {
  1968. if (file_exists($filename)) {
  1969. $tmppluginhost = new PluginHost();
  1970. $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
  1971. $tmppluginhost->load_data();
  1972. foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
  1973. if ($plugin->hook_send_local_file($filename)) return true;
  1974. }
  1975. $mimetype = mime_content_type($filename);
  1976. header("Content-type: $mimetype");
  1977. $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
  1978. header("Last-Modified: $stamp", true);
  1979. return readfile($filename);
  1980. } else {
  1981. return false;
  1982. }
  1983. }