lib_rss.php 17 KB

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