lib_rss.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <?php
  2. if (version_compare(PHP_VERSION, '5.5.0', '<')) {
  3. die('FreshRSS error: FreshRSS requires PHP 5.5.0+!');
  4. }
  5. if (!function_exists('json_decode')) { //PHP bug #63520 < PHP 7
  6. require_once(__DIR__ . '/JSON.php');
  7. function json_decode($var, $assoc = false) {
  8. $JSON = new Services_JSON($assoc ? SERVICES_JSON_LOOSE_TYPE : 0);
  9. return $JSON->decode($var);
  10. }
  11. }
  12. if (!function_exists('json_encode')) {
  13. require_once(__DIR__ . '/JSON.php');
  14. function json_encode($var) {
  15. $JSON = new Services_JSON();
  16. return $JSON->encodeUnsafe($var);
  17. }
  18. }
  19. if (!function_exists('mb_strcut')) {
  20. function mb_strcut($str, $start, $length = null, $encoding = 'UTF-8') {
  21. return substr($str, $start, $length);
  22. }
  23. }
  24. /**
  25. * Build a directory path by concatenating a list of directory names.
  26. *
  27. * @param $path_parts a list of directory names
  28. * @return a string corresponding to the final pathname
  29. */
  30. function join_path() {
  31. $path_parts = func_get_args();
  32. return join(DIRECTORY_SEPARATOR, $path_parts);
  33. }
  34. //<Auto-loading>
  35. function classAutoloader($class) {
  36. if (strpos($class, 'FreshRSS') === 0) {
  37. $components = explode('_', $class);
  38. switch (count($components)) {
  39. case 1:
  40. include(APP_PATH . '/' . $components[0] . '.php');
  41. return;
  42. case 2:
  43. include(APP_PATH . '/Models/' . $components[1] . '.php');
  44. return;
  45. case 3: //Controllers, Exceptions
  46. include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php');
  47. return;
  48. }
  49. } elseif (strpos($class, 'Minz') === 0) {
  50. include(LIB_PATH . '/' . str_replace('_', '/', $class) . '.php');
  51. } elseif (strpos($class, 'SimplePie') === 0) {
  52. include(LIB_PATH . '/SimplePie/' . str_replace('_', '/', $class) . '.php');
  53. }
  54. }
  55. spl_autoload_register('classAutoloader');
  56. //</Auto-loading>
  57. function idn_to_puny($url) {
  58. if (function_exists('idn_to_ascii')) {
  59. $parts = parse_url($url);
  60. if (!empty($parts['host'])) {
  61. $idn = $parts['host'];
  62. $puny = idn_to_ascii($idn, 0, INTL_IDNA_VARIANT_UTS46);
  63. $pos = strpos($url, $idn);
  64. if ($pos !== false) {
  65. return substr_replace($url, $puny, $pos, strlen($idn));
  66. }
  67. }
  68. }
  69. return $url;
  70. }
  71. function checkUrl($url) {
  72. if ($url == '') {
  73. return '';
  74. }
  75. if (!preg_match('#^https?://#i', $url)) {
  76. $url = 'http://' . $url;
  77. }
  78. $url = idn_to_puny($url); //PHP bug #53474 IDN
  79. if (filter_var($url, FILTER_VALIDATE_URL)) {
  80. return $url;
  81. } else {
  82. return false;
  83. }
  84. }
  85. function safe_ascii($text) {
  86. return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
  87. }
  88. function escapeToUnicodeAlternative($text, $extended = true) {
  89. $text = htmlspecialchars_decode($text, ENT_QUOTES);
  90. //Problematic characters
  91. $problem = array('&', '<', '>');
  92. //Use their fullwidth Unicode form instead:
  93. $replace = array('&', '<', '>');
  94. // https://raw.githubusercontent.com/mihaip/google-reader-api/master/wiki/StreamId.wiki
  95. if ($extended) {
  96. $problem += array("'", '"', '^', '?', '\\', '/', ',', ';');
  97. $replace += array("’", '"', '^', '?', '\', '/', ',', ';');
  98. }
  99. return trim(str_replace($problem, $replace, $text));
  100. }
  101. /**
  102. * Test if a given server address is publicly accessible.
  103. *
  104. * Note: for the moment it tests only if address is corresponding to a
  105. * localhost address.
  106. *
  107. * @param $address the address to test, can be an IP or a URL.
  108. * @return true if server is accessible, false otherwise.
  109. * @todo improve test with a more valid technique (e.g. test with an external server?)
  110. */
  111. function server_is_public($address) {
  112. $host = parse_url($address, PHP_URL_HOST);
  113. $is_public = !in_array($host, array(
  114. 'localhost',
  115. 'localhost.localdomain',
  116. '[::1]',
  117. 'ip6-localhost',
  118. 'localhost6',
  119. 'localhost6.localdomain6',
  120. ));
  121. if ($is_public) {
  122. $is_public &= !preg_match('/^(10|127|172[.]16|192[.]168)[.]/', $host);
  123. $is_public &= !preg_match('/^(\[)?(::1$|fc00::|fe80::)/i', $host);
  124. }
  125. return (bool)$is_public;
  126. }
  127. function format_number($n, $precision = 0) {
  128. // number_format does not seem to be Unicode-compatible
  129. return str_replace(' ', ' ', //Espace fine insécable
  130. number_format($n, $precision, '.', ' ')
  131. );
  132. }
  133. function format_bytes($bytes, $precision = 2, $system = 'IEC') {
  134. if ($system === 'IEC') {
  135. $base = 1024;
  136. $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB');
  137. } elseif ($system === 'SI') {
  138. $base = 1000;
  139. $units = array('B', 'KB', 'MB', 'GB', 'TB');
  140. } else {
  141. return format_number($bytes, $precision);
  142. }
  143. $bytes = max(intval($bytes), 0);
  144. $pow = $bytes === 0 ? 0 : floor(log($bytes) / log($base));
  145. $pow = min($pow, count($units) - 1);
  146. $bytes /= pow($base, $pow);
  147. return format_number($bytes, $precision) . ' ' . $units[$pow];
  148. }
  149. function timestamptodate ($t, $hour = true) {
  150. $month = _t('gen.date.' . date('M', $t));
  151. if ($hour) {
  152. $date = _t('gen.date.format_date_hour', $month);
  153. } else {
  154. $date = _t('gen.date.format_date', $month);
  155. }
  156. return @date ($date, $t);
  157. }
  158. function html_only_entity_decode($text) {
  159. static $htmlEntitiesOnly = null;
  160. if ($htmlEntitiesOnly === null) {
  161. $htmlEntitiesOnly = array_flip(array_diff(
  162. get_html_translation_table(HTML_ENTITIES, ENT_NOQUOTES, 'UTF-8'), //Decode HTML entities
  163. get_html_translation_table(HTML_SPECIALCHARS, ENT_NOQUOTES, 'UTF-8') //Preserve XML entities
  164. ));
  165. }
  166. return strtr($text, $htmlEntitiesOnly);
  167. }
  168. function prepareSyslog() {
  169. return COPY_SYSLOG_TO_STDERR ? openlog("FreshRSS", LOG_PERROR | LOG_PID, LOG_USER) : false;
  170. }
  171. function customSimplePie($attributes = array()) {
  172. $system_conf = Minz_Configuration::get('system');
  173. $limits = $system_conf->limits;
  174. $simplePie = new SimplePie();
  175. $simplePie->set_useragent(FRESHRSS_USERAGENT);
  176. $simplePie->set_syslog($system_conf->simplepie_syslog_enabled);
  177. if ($system_conf->simplepie_syslog_enabled) {
  178. prepareSyslog();
  179. }
  180. $simplePie->set_cache_location(CACHE_PATH);
  181. $simplePie->set_cache_duration($limits['cache_duration']);
  182. $feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
  183. $simplePie->set_timeout($feed_timeout > 0 ? $feed_timeout : $limits['timeout']);
  184. $curl_options = $system_conf->curl_options;
  185. if (isset($attributes['ssl_verify'])) {
  186. $curl_options[CURLOPT_SSL_VERIFYHOST] = $attributes['ssl_verify'] ? 2 : 0;
  187. $curl_options[CURLOPT_SSL_VERIFYPEER] = $attributes['ssl_verify'] ? true : false;
  188. }
  189. $simplePie->set_curl_options($curl_options);
  190. $simplePie->strip_htmltags(array(
  191. 'base', 'blink', 'body', 'doctype', 'embed',
  192. 'font', 'form', 'frame', 'frameset', 'html',
  193. 'link', 'input', 'marquee', 'meta', 'noscript',
  194. 'object', 'param', 'plaintext', 'script', 'style',
  195. 'svg', //TODO: Support SVG after sanitizing and URL rewriting of xlink:href
  196. ));
  197. $simplePie->strip_attributes(array_merge($simplePie->strip_attributes, array(
  198. 'autoplay', 'class', 'onload', 'onunload', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup',
  199. 'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur',
  200. 'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless', 'sizes', 'srcset')));
  201. $simplePie->add_attributes(array(
  202. 'audio' => array('controls' => 'controls', 'preload' => 'none'),
  203. 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'),
  204. 'video' => array('controls' => 'controls', 'preload' => 'none'),
  205. ));
  206. $simplePie->set_url_replacements(array(
  207. 'a' => 'href',
  208. 'area' => 'href',
  209. 'audio' => 'src',
  210. 'blockquote' => 'cite',
  211. 'del' => 'cite',
  212. 'form' => 'action',
  213. 'iframe' => 'src',
  214. 'img' => array(
  215. 'longdesc',
  216. 'src'
  217. ),
  218. 'input' => 'src',
  219. 'ins' => 'cite',
  220. 'q' => 'cite',
  221. 'source' => 'src',
  222. 'track' => 'src',
  223. 'video' => array(
  224. 'poster',
  225. 'src',
  226. ),
  227. ));
  228. $https_domains = array();
  229. $force = @file(FRESHRSS_PATH . '/force-https.default.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  230. if (is_array($force)) {
  231. $https_domains = array_merge($https_domains, $force);
  232. }
  233. $force = @file(DATA_PATH . '/force-https.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  234. if (is_array($force)) {
  235. $https_domains = array_merge($https_domains, $force);
  236. }
  237. $simplePie->set_https_domains($https_domains);
  238. return $simplePie;
  239. }
  240. function sanitizeHTML($data, $base = '') {
  241. if (!is_string($data)) {
  242. return '';
  243. }
  244. static $simplePie = null;
  245. if ($simplePie == null) {
  246. $simplePie = customSimplePie();
  247. $simplePie->init();
  248. }
  249. return html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_HTML, $base));
  250. }
  251. /**
  252. * Add support of image lazy loading
  253. * Move content from src attribute to data-original
  254. * @param content is the text we want to parse
  255. */
  256. function lazyimg($content) {
  257. return preg_replace(
  258. '/<((?:img|iframe)[^>]+?)src=[\'"]([^"\']+)[\'"]([^>]*)>/i',
  259. '<$1src="' . Minz_Url::display('/themes/icons/grey.gif') . '" data-original="$2"$3>',
  260. $content
  261. );
  262. }
  263. function uTimeString() {
  264. $t = @gettimeofday();
  265. return $t['sec'] . str_pad($t['usec'], 6, '0', STR_PAD_LEFT);
  266. }
  267. function invalidateHttpCache($username = '') {
  268. if (!FreshRSS_user_Controller::checkUsername($username)) {
  269. Minz_Session::_param('touch', uTimeString());
  270. $username = Minz_Session::param('currentUser', '_');
  271. }
  272. return touch(join_path(DATA_PATH, 'users', $username, 'log.txt'));
  273. }
  274. function listUsers() {
  275. $final_list = array();
  276. $base_path = join_path(DATA_PATH, 'users');
  277. $dir_list = array_values(array_diff(
  278. scandir($base_path),
  279. array('..', '.', '_')
  280. ));
  281. foreach ($dir_list as $file) {
  282. if ($file[0] !== '.' && is_dir(join_path($base_path, $file)) && file_exists(join_path($base_path, $file, 'config.php'))) {
  283. $final_list[] = $file;
  284. }
  285. }
  286. return $final_list;
  287. }
  288. /**
  289. * Return if the maximum number of registrations has been reached.
  290. *
  291. * Note a max_regstrations of 0 means there is no limit.
  292. *
  293. * @return true if number of users >= max registrations, false else.
  294. */
  295. function max_registrations_reached() {
  296. $system_conf = Minz_Configuration::get('system');
  297. $limit_registrations = $system_conf->limits['max_registrations'];
  298. $number_accounts = count(listUsers());
  299. return $limit_registrations > 0 && $number_accounts >= $limit_registrations;
  300. }
  301. /**
  302. * Register and return the configuration for a given user.
  303. *
  304. * Note this function has been created to generate temporary configuration
  305. * objects. If you need a long-time configuration, please don't use this function.
  306. *
  307. * @param $username the name of the user of which we want the configuration.
  308. * @return a Minz_Configuration object, null if the configuration cannot be loaded.
  309. */
  310. function get_user_configuration($username) {
  311. if (!FreshRSS_user_Controller::checkUsername($username)) {
  312. return null;
  313. }
  314. $namespace = 'user_' . $username;
  315. try {
  316. Minz_Configuration::register($namespace,
  317. join_path(USERS_PATH, $username, 'config.php'),
  318. join_path(FRESHRSS_PATH, 'config-user.default.php'));
  319. } catch (Minz_ConfigurationNamespaceException $e) {
  320. // namespace already exists, do nothing.
  321. Minz_Log::warning($e->getMessage(), USERS_PATH . '/_/log.txt');
  322. } catch (Minz_FileNotExistException $e) {
  323. Minz_Log::warning($e->getMessage(), USERS_PATH . '/_/log.txt');
  324. return null;
  325. }
  326. return Minz_Configuration::get($namespace);
  327. }
  328. function httpAuthUser() {
  329. if (!empty($_SERVER['REMOTE_USER'])) {
  330. return $_SERVER['REMOTE_USER'];
  331. } elseif (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
  332. return $_SERVER['REDIRECT_REMOTE_USER'];
  333. } elseif (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) {
  334. return $_SERVER['HTTP_X_WEBAUTH_USER'];
  335. }
  336. return '';
  337. }
  338. function cryptAvailable() {
  339. try {
  340. $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
  341. return $hash === @crypt('password', $hash);
  342. } catch (Exception $e) {
  343. Minz_Log::warning($e->getMessage());
  344. }
  345. return false;
  346. }
  347. function is_referer_from_same_domain() {
  348. if (empty($_SERVER['HTTP_REFERER'])) {
  349. return true; //Accept empty referer while waiting for good support of meta referrer same-origin policy in browsers
  350. }
  351. $host = parse_url(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://') .
  352. (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST']));
  353. $referer = parse_url($_SERVER['HTTP_REFERER']);
  354. if (empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) {
  355. return false;
  356. }
  357. //TODO: check 'scheme', taking into account the case of a proxy
  358. if ((isset($host['port']) ? $host['port'] : 0) !== (isset($referer['port']) ? $referer['port'] : 0)) {
  359. return false;
  360. }
  361. return true;
  362. }
  363. /**
  364. * Check PHP and its extensions are well-installed.
  365. *
  366. * @return array of tested values.
  367. */
  368. function check_install_php() {
  369. $pdo_mysql = extension_loaded('pdo_mysql');
  370. $pdo_pgsql = extension_loaded('pdo_pgsql');
  371. $pdo_sqlite = extension_loaded('pdo_sqlite');
  372. return array(
  373. 'php' => version_compare(PHP_VERSION, '5.5.0') >= 0,
  374. 'minz' => file_exists(LIB_PATH . '/Minz'),
  375. 'curl' => extension_loaded('curl'),
  376. 'pdo' => $pdo_mysql || $pdo_sqlite || $pdo_pgsql,
  377. 'pcre' => extension_loaded('pcre'),
  378. 'ctype' => extension_loaded('ctype'),
  379. 'fileinfo' => extension_loaded('fileinfo'),
  380. 'dom' => class_exists('DOMDocument'),
  381. 'json' => extension_loaded('json'),
  382. 'mbstring' => extension_loaded('mbstring'),
  383. 'zip' => extension_loaded('zip'),
  384. );
  385. }
  386. /**
  387. * Check different data files and directories exist.
  388. *
  389. * @return array of tested values.
  390. */
  391. function check_install_files() {
  392. return array(
  393. 'data' => DATA_PATH && is_writable(DATA_PATH),
  394. 'cache' => CACHE_PATH && is_writable(CACHE_PATH),
  395. 'users' => USERS_PATH && is_writable(USERS_PATH),
  396. 'favicons' => is_writable(DATA_PATH . '/favicons'),
  397. 'tokens' => is_writable(DATA_PATH . '/tokens'),
  398. );
  399. }
  400. /**
  401. * Check database is well-installed.
  402. *
  403. * @return array of tested values.
  404. */
  405. function check_install_database() {
  406. $status = array(
  407. 'connection' => true,
  408. 'tables' => false,
  409. 'categories' => false,
  410. 'feeds' => false,
  411. 'entries' => false,
  412. 'entrytmp' => false,
  413. 'tag' => false,
  414. 'entrytag' => false,
  415. );
  416. try {
  417. $dbDAO = FreshRSS_Factory::createDatabaseDAO();
  418. $status['tables'] = $dbDAO->tablesAreCorrect();
  419. $status['categories'] = $dbDAO->categoryIsCorrect();
  420. $status['feeds'] = $dbDAO->feedIsCorrect();
  421. $status['entries'] = $dbDAO->entryIsCorrect();
  422. $status['entrytmp'] = $dbDAO->entrytmpIsCorrect();
  423. $status['tag'] = $dbDAO->tagIsCorrect();
  424. $status['entrytag'] = $dbDAO->entrytagIsCorrect();
  425. } catch(Minz_PDOConnectionException $e) {
  426. $status['connection'] = false;
  427. }
  428. return $status;
  429. }
  430. /**
  431. * Remove a directory recursively.
  432. *
  433. * From http://php.net/rmdir#110489
  434. *
  435. * @param $dir the directory to remove
  436. */
  437. function recursive_unlink($dir) {
  438. if (!is_dir($dir)) {
  439. return true;
  440. }
  441. $files = array_diff(scandir($dir), array('.', '..'));
  442. foreach ($files as $filename) {
  443. $filename = $dir . '/' . $filename;
  444. if (is_dir($filename)) {
  445. @chmod($filename, 0777);
  446. recursive_unlink($filename);
  447. } else {
  448. unlink($filename);
  449. }
  450. }
  451. return rmdir($dir);
  452. }
  453. /**
  454. * Remove queries where $get is appearing.
  455. * @param $get the get attribute which should be removed.
  456. * @param $queries an array of queries.
  457. * @return the same array whithout those where $get is appearing.
  458. */
  459. function remove_query_by_get($get, $queries) {
  460. $final_queries = array();
  461. foreach ($queries as $key => $query) {
  462. if (empty($query['get']) || $query['get'] !== $get) {
  463. $final_queries[$key] = $query;
  464. }
  465. }
  466. return $final_queries;
  467. }
  468. //RFC 4648
  469. function base64url_encode($data) {
  470. return strtr(rtrim(base64_encode($data), '='), '+/', '-_');
  471. }
  472. //RFC 4648
  473. function base64url_decode($data) {
  474. return base64_decode(strtr($data, '-_', '+/'));
  475. }
  476. function _i($icon, $url_only = false) {
  477. return FreshRSS_Themes::icon($icon, $url_only);
  478. }
  479. $SHORTCUT_KEYS = array( //No const for < PHP 5.6 compatibility
  480. '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  481. 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
  482. 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
  483. 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
  484. 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Backspace', 'Delete',
  485. 'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab',
  486. );
  487. function validateShortcutList($shortcuts) {
  488. global $SHORTCUT_KEYS;
  489. $legacy = array(
  490. 'down' => 'ArrowDown', 'left' => 'ArrowLeft', 'page_down' => 'PageDown', 'page_up' => 'PageUp',
  491. 'right' => 'ArrowRight', 'up' => 'ArrowUp',
  492. );
  493. $upper = null;
  494. $shortcuts_ok = array();
  495. foreach ($shortcuts as $key => $value) {
  496. if (in_array($value, $SHORTCUT_KEYS)) {
  497. $shortcuts_ok[$key] = $value;
  498. } elseif (isset($legacy[$value])) {
  499. $shortcuts_ok[$key] = $legacy[$value];
  500. } else { //Case-insensitive search
  501. if ($upper === null) {
  502. $upper = array_map('strtoupper', $SHORTCUT_KEYS);
  503. }
  504. $i = array_search(strtoupper($value), $upper);
  505. if ($i !== false) {
  506. $shortcuts_ok[$key] = $SHORTCUT_KEYS[$i];
  507. }
  508. }
  509. }
  510. return $shortcuts_ok;
  511. }