lib_rss.php 17 KB

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