PageRenderTime 31ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Core.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 998 lines | 581 code | 144 blank | 273 comment | 81 complexity | fb982d7477778ce47b491f287ba47a02 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use Symfony\Component\Config\FileLocator;
  5. use Symfony\Component\DependencyInjection\ContainerBuilder;
  6. use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
  7. use function __;
  8. use function array_keys;
  9. use function array_pop;
  10. use function array_walk_recursive;
  11. use function chr;
  12. use function count;
  13. use function defined;
  14. use function explode;
  15. use function filter_var;
  16. use function function_exists;
  17. use function getenv;
  18. use function gmdate;
  19. use function hash_equals;
  20. use function hash_hmac;
  21. use function header;
  22. use function htmlspecialchars;
  23. use function http_build_query;
  24. use function in_array;
  25. use function intval;
  26. use function is_array;
  27. use function is_string;
  28. use function json_encode;
  29. use function mb_strlen;
  30. use function mb_strpos;
  31. use function mb_substr;
  32. use function parse_str;
  33. use function parse_url;
  34. use function preg_match;
  35. use function preg_replace;
  36. use function session_write_close;
  37. use function sprintf;
  38. use function str_contains;
  39. use function str_replace;
  40. use function strlen;
  41. use function strpos;
  42. use function strtolower;
  43. use function strtr;
  44. use function substr;
  45. use function trigger_error;
  46. use function unserialize;
  47. use function urldecode;
  48. use function vsprintf;
  49. use const DATE_RFC1123;
  50. use const E_USER_ERROR;
  51. use const E_USER_WARNING;
  52. use const FILTER_VALIDATE_IP;
  53. /**
  54. * Core functions used all over the scripts.
  55. */
  56. class Core
  57. {
  58. /**
  59. * Removes insecure parts in a path; used before include() or
  60. * require() when a part of the path comes from an insecure source
  61. * like a cookie or form.
  62. *
  63. * @param string $path The path to check
  64. */
  65. public static function securePath(string $path): string
  66. {
  67. // change .. to .
  68. return (string) preg_replace('@\.\.*@', '.', $path);
  69. }
  70. /**
  71. * displays the given error message on phpMyAdmin error page in foreign language,
  72. * ends script execution and closes session
  73. *
  74. * loads language file if not loaded already
  75. *
  76. * @param string $error_message the error message or named error message
  77. * @param string|array $message_args arguments applied to $error_message
  78. */
  79. public static function fatalError(
  80. string $error_message,
  81. $message_args = null
  82. ): void {
  83. global $dbi;
  84. /* Use format string if applicable */
  85. if (is_string($message_args)) {
  86. $error_message = sprintf($error_message, $message_args);
  87. } elseif (is_array($message_args)) {
  88. $error_message = vsprintf($error_message, $message_args);
  89. }
  90. /*
  91. * Avoid using Response class as config does not have to be loaded yet
  92. * (this can happen on early fatal error)
  93. */
  94. if (
  95. isset($dbi, $GLOBALS['config']) && $dbi !== null
  96. && $GLOBALS['config']->get('is_setup') === false
  97. && ResponseRenderer::getInstance()->isAjax()
  98. ) {
  99. $response = ResponseRenderer::getInstance();
  100. $response->setRequestStatus(false);
  101. $response->addJSON('message', Message::error($error_message));
  102. if (! defined('TESTSUITE')) {
  103. exit;
  104. }
  105. return;
  106. }
  107. if (! empty($_REQUEST['ajax_request'])) {
  108. // Generate JSON manually
  109. self::headerJSON();
  110. echo json_encode(
  111. [
  112. 'success' => false,
  113. 'message' => Message::error($error_message)->getDisplay(),
  114. ]
  115. );
  116. if (! defined('TESTSUITE')) {
  117. exit;
  118. }
  119. return;
  120. }
  121. $error_message = strtr($error_message, ['<br>' => '[br]']);
  122. $template = new Template();
  123. echo $template->render('error/generic', [
  124. 'lang' => $GLOBALS['lang'] ?? 'en',
  125. 'dir' => $GLOBALS['text_dir'] ?? 'ltr',
  126. 'error_message' => Sanitize::sanitizeMessage($error_message),
  127. ]);
  128. if (! defined('TESTSUITE')) {
  129. exit;
  130. }
  131. }
  132. /**
  133. * Returns a link to the PHP documentation
  134. *
  135. * @param string $target anchor in documentation
  136. *
  137. * @return string the URL
  138. *
  139. * @access public
  140. */
  141. public static function getPHPDocLink(string $target): string
  142. {
  143. /* List of PHP documentation translations */
  144. $php_doc_languages = [
  145. 'pt_BR',
  146. 'zh',
  147. 'fr',
  148. 'de',
  149. 'it',
  150. 'ja',
  151. 'ro',
  152. 'ru',
  153. 'es',
  154. 'tr',
  155. ];
  156. $lang = 'en';
  157. if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $php_doc_languages)) {
  158. $lang = $GLOBALS['lang'];
  159. }
  160. return self::linkURL('https://www.php.net/manual/' . $lang . '/' . $target);
  161. }
  162. /**
  163. * Warn or fail on missing extension.
  164. *
  165. * @param string $extension Extension name
  166. * @param bool $fatal Whether the error is fatal.
  167. * @param string $extra Extra string to append to message.
  168. */
  169. public static function warnMissingExtension(
  170. string $extension,
  171. bool $fatal = false,
  172. string $extra = ''
  173. ): void {
  174. global $errorHandler;
  175. $message = 'The %s extension is missing. Please check your PHP configuration.';
  176. /* Gettext does not have to be loaded yet here */
  177. if (function_exists('__')) {
  178. $message = __('The %s extension is missing. Please check your PHP configuration.');
  179. }
  180. $doclink = self::getPHPDocLink('book.' . $extension . '.php');
  181. $message = sprintf($message, '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]');
  182. if ($extra != '') {
  183. $message .= ' ' . $extra;
  184. }
  185. if ($fatal) {
  186. self::fatalError($message);
  187. return;
  188. }
  189. $errorHandler->addError($message, E_USER_WARNING, '', 0, false);
  190. }
  191. /**
  192. * returns count of tables in given db
  193. *
  194. * @param string $db database to count tables for
  195. *
  196. * @return int count of tables in $db
  197. */
  198. public static function getTableCount(string $db): int
  199. {
  200. global $dbi;
  201. $tables = $dbi->tryQuery(
  202. 'SHOW TABLES FROM ' . Util::backquote($db) . ';',
  203. DatabaseInterface::CONNECT_USER,
  204. DatabaseInterface::QUERY_STORE
  205. );
  206. if ($tables) {
  207. $numTables = $dbi->numRows($tables);
  208. $dbi->freeResult($tables);
  209. return $numTables;
  210. }
  211. return 0;
  212. }
  213. /**
  214. * Converts numbers like 10M into bytes
  215. * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas
  216. * (renamed with PMA prefix to avoid double definition when embedded
  217. * in Moodle)
  218. *
  219. * @param string|int $size size (Default = 0)
  220. */
  221. public static function getRealSize($size = 0): int
  222. {
  223. if (! $size) {
  224. return 0;
  225. }
  226. $binaryprefixes = [
  227. 'T' => 1099511627776,
  228. 't' => 1099511627776,
  229. 'G' => 1073741824,
  230. 'g' => 1073741824,
  231. 'M' => 1048576,
  232. 'm' => 1048576,
  233. 'K' => 1024,
  234. 'k' => 1024,
  235. ];
  236. if (preg_match('/^([0-9]+)([KMGT])/i', (string) $size, $matches)) {
  237. return (int) ($matches[1] * $binaryprefixes[$matches[2]]);
  238. }
  239. return (int) $size;
  240. }
  241. /**
  242. * Checks given $page against given $allowList and returns true if valid
  243. * it optionally ignores query parameters in $page (script.php?ignored)
  244. *
  245. * @param string $page page to check
  246. * @param array $allowList allow list to check page against
  247. * @param bool $include whether the page is going to be included
  248. */
  249. public static function checkPageValidity(&$page, array $allowList = [], $include = false): bool
  250. {
  251. if (empty($allowList)) {
  252. $allowList = ['index.php'];
  253. }
  254. if (empty($page)) {
  255. return false;
  256. }
  257. if (in_array($page, $allowList)) {
  258. return true;
  259. }
  260. if ($include) {
  261. return false;
  262. }
  263. $_page = mb_substr(
  264. $page,
  265. 0,
  266. (int) mb_strpos($page . '?', '?')
  267. );
  268. if (in_array($_page, $allowList)) {
  269. return true;
  270. }
  271. $_page = urldecode($page);
  272. $_page = mb_substr(
  273. $_page,
  274. 0,
  275. (int) mb_strpos($_page . '?', '?')
  276. );
  277. return in_array($_page, $allowList);
  278. }
  279. /**
  280. * tries to find the value for the given environment variable name
  281. *
  282. * searches in $_SERVER, $_ENV then tries getenv() and apache_getenv()
  283. * in this order
  284. *
  285. * @param string $var_name variable name
  286. *
  287. * @return string value of $var or empty string
  288. */
  289. public static function getenv(string $var_name): string
  290. {
  291. if (isset($_SERVER[$var_name])) {
  292. return (string) $_SERVER[$var_name];
  293. }
  294. if (isset($_ENV[$var_name])) {
  295. return (string) $_ENV[$var_name];
  296. }
  297. if (getenv($var_name)) {
  298. return (string) getenv($var_name);
  299. }
  300. if (function_exists('apache_getenv') && apache_getenv($var_name, true)) {
  301. return (string) apache_getenv($var_name, true);
  302. }
  303. return '';
  304. }
  305. /**
  306. * Send HTTP header, taking IIS limits into account (600 seems ok)
  307. *
  308. * @param string $uri the header to send
  309. * @param bool $use_refresh whether to use Refresh: header when running on IIS
  310. */
  311. public static function sendHeaderLocation(string $uri, bool $use_refresh = false): void
  312. {
  313. if ($GLOBALS['config']->get('PMA_IS_IIS') && mb_strlen($uri) > 600) {
  314. ResponseRenderer::getInstance()->disable();
  315. $template = new Template();
  316. echo $template->render('header_location', ['uri' => $uri]);
  317. return;
  318. }
  319. /*
  320. * Avoid relative path redirect problems in case user entered URL
  321. * like /phpmyadmin/index.php/ which some web servers happily accept.
  322. */
  323. if ($uri[0] === '.') {
  324. $uri = $GLOBALS['config']->getRootPath() . substr($uri, 2);
  325. }
  326. $response = ResponseRenderer::getInstance();
  327. session_write_close();
  328. if ($response->headersSent()) {
  329. trigger_error('Core::sendHeaderLocation called when headers are already sent!', E_USER_ERROR);
  330. }
  331. // bug #1523784: IE6 does not like 'Refresh: 0', it
  332. // results in a blank page
  333. // but we need it when coming from the cookie login panel)
  334. if ($GLOBALS['config']->get('PMA_IS_IIS') && $use_refresh) {
  335. $response->header('Refresh: 0; ' . $uri);
  336. return;
  337. }
  338. $response->header('Location: ' . $uri);
  339. }
  340. /**
  341. * Outputs application/json headers. This includes no caching.
  342. */
  343. public static function headerJSON(): void
  344. {
  345. if (defined('TESTSUITE')) {
  346. return;
  347. }
  348. // No caching
  349. $headers = self::getNoCacheHeaders();
  350. // Media type
  351. $headers['Content-Type'] = 'application/json; charset=UTF-8';
  352. /**
  353. * Disable content sniffing in browser.
  354. * This is needed in case we include HTML in JSON, browser might assume it's html to display.
  355. */
  356. $headers['X-Content-Type-Options'] = 'nosniff';
  357. foreach ($headers as $name => $value) {
  358. header(sprintf('%s: %s', $name, $value));
  359. }
  360. }
  361. /**
  362. * Outputs headers to prevent caching in browser (and on the way).
  363. */
  364. public static function noCacheHeader(): void
  365. {
  366. if (defined('TESTSUITE')) {
  367. return;
  368. }
  369. $headers = self::getNoCacheHeaders();
  370. foreach ($headers as $name => $value) {
  371. header(sprintf('%s: %s', $name, $value));
  372. }
  373. }
  374. /**
  375. * @return array<string, string>
  376. */
  377. public static function getNoCacheHeaders(): array
  378. {
  379. $headers = [];
  380. $date = (string) gmdate(DATE_RFC1123);
  381. // rfc2616 - Section 14.21
  382. $headers['Expires'] = $date;
  383. // HTTP/1.1
  384. $headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0';
  385. // HTTP/1.0
  386. $headers['Pragma'] = 'no-cache';
  387. // test case: exporting a database into a .gz file with Safari
  388. // would produce files not having the current time
  389. // (added this header for Safari but should not harm other browsers)
  390. $headers['Last-Modified'] = $date;
  391. return $headers;
  392. }
  393. /**
  394. * Sends header indicating file download.
  395. *
  396. * @param string $filename Filename to include in headers if empty,
  397. * none Content-Disposition header will be sent.
  398. * @param string $mimetype MIME type to include in headers.
  399. * @param int $length Length of content (optional)
  400. * @param bool $no_cache Whether to include no-caching headers.
  401. */
  402. public static function downloadHeader(
  403. string $filename,
  404. string $mimetype,
  405. int $length = 0,
  406. bool $no_cache = true
  407. ): void {
  408. $headers = [];
  409. if ($no_cache) {
  410. $headers = self::getNoCacheHeaders();
  411. }
  412. /* Replace all possibly dangerous chars in filename */
  413. $filename = Sanitize::sanitizeFilename($filename);
  414. if (! empty($filename)) {
  415. $headers['Content-Description'] = 'File Transfer';
  416. $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
  417. }
  418. $headers['Content-Type'] = $mimetype;
  419. // inform the server that compression has been done,
  420. // to avoid a double compression (for example with Apache + mod_deflate)
  421. $notChromeOrLessThan43 = $GLOBALS['config']->get('PMA_USR_BROWSER_AGENT') !== 'CHROME' // see bug #4942
  422. || ($GLOBALS['config']->get('PMA_USR_BROWSER_AGENT') === 'CHROME'
  423. && $GLOBALS['config']->get('PMA_USR_BROWSER_VER') < 43);
  424. if (str_contains($mimetype, 'gzip') && $notChromeOrLessThan43) {
  425. $headers['Content-Encoding'] = 'gzip';
  426. }
  427. $headers['Content-Transfer-Encoding'] = 'binary';
  428. if ($length > 0) {
  429. $headers['Content-Length'] = (string) $length;
  430. }
  431. foreach ($headers as $name => $value) {
  432. header(sprintf('%s: %s', $name, $value));
  433. }
  434. }
  435. /**
  436. * Returns value of an element in $array given by $path.
  437. * $path is a string describing position of an element in an associative array,
  438. * eg. Servers/1/host refers to $array[Servers][1][host]
  439. *
  440. * @param string $path path in the array
  441. * @param array $array the array
  442. * @param mixed $default default value
  443. *
  444. * @return array|mixed|null array element or $default
  445. */
  446. public static function arrayRead(string $path, array $array, $default = null)
  447. {
  448. $keys = explode('/', $path);
  449. $value =& $array;
  450. foreach ($keys as $key) {
  451. if (! isset($value[$key])) {
  452. return $default;
  453. }
  454. $value =& $value[$key];
  455. }
  456. return $value;
  457. }
  458. /**
  459. * Stores value in an array
  460. *
  461. * @param string $path path in the array
  462. * @param array $array the array
  463. * @param mixed $value value to store
  464. */
  465. public static function arrayWrite(string $path, array &$array, $value): void
  466. {
  467. $keys = explode('/', $path);
  468. $last_key = array_pop($keys);
  469. $a =& $array;
  470. foreach ($keys as $key) {
  471. if (! isset($a[$key])) {
  472. $a[$key] = [];
  473. }
  474. $a =& $a[$key];
  475. }
  476. $a[$last_key] = $value;
  477. }
  478. /**
  479. * Removes value from an array
  480. *
  481. * @param string $path path in the array
  482. * @param array $array the array
  483. */
  484. public static function arrayRemove(string $path, array &$array): void
  485. {
  486. $keys = explode('/', $path);
  487. $keys_last = array_pop($keys);
  488. $path = [];
  489. $depth = 0;
  490. $path[0] =& $array;
  491. $found = true;
  492. // go as deep as required or possible
  493. foreach ($keys as $key) {
  494. if (! isset($path[$depth][$key])) {
  495. $found = false;
  496. break;
  497. }
  498. $depth++;
  499. $path[$depth] =& $path[$depth - 1][$key];
  500. }
  501. // if element found, remove it
  502. if ($found) {
  503. unset($path[$depth][$keys_last]);
  504. $depth--;
  505. }
  506. // remove empty nested arrays
  507. for (; $depth >= 0; $depth--) {
  508. if (isset($path[$depth + 1]) && count($path[$depth + 1]) !== 0) {
  509. break;
  510. }
  511. unset($path[$depth][$keys[$depth]]);
  512. }
  513. }
  514. /**
  515. * Returns link to (possibly) external site using defined redirector.
  516. *
  517. * @param string $url URL where to go.
  518. *
  519. * @return string URL for a link.
  520. */
  521. public static function linkURL(string $url): string
  522. {
  523. if (! preg_match('#^https?://#', $url)) {
  524. return $url;
  525. }
  526. $params = [];
  527. $params['url'] = $url;
  528. $url = Url::getCommon($params);
  529. //strip off token and such sensitive information. Just keep url.
  530. $arr = parse_url($url);
  531. if (! is_array($arr)) {
  532. $arr = [];
  533. }
  534. parse_str($arr['query'] ?? '', $vars);
  535. $query = http_build_query(['url' => $vars['url']]);
  536. if ($GLOBALS['config'] !== null && $GLOBALS['config']->get('is_setup')) {
  537. return '../url.php?' . $query;
  538. }
  539. return './url.php?' . $query;
  540. }
  541. /**
  542. * Checks whether domain of URL is an allowed domain or not.
  543. * Use only for URLs of external sites.
  544. *
  545. * @param string $url URL of external site.
  546. */
  547. public static function isAllowedDomain(string $url): bool
  548. {
  549. $arr = parse_url($url);
  550. if (! is_array($arr)) {
  551. $arr = [];
  552. }
  553. // We need host to be set
  554. if (! isset($arr['host']) || strlen($arr['host']) == 0) {
  555. return false;
  556. }
  557. // We do not want these to be present
  558. $blocked = [
  559. 'user',
  560. 'pass',
  561. 'port',
  562. ];
  563. foreach ($blocked as $part) {
  564. if (isset($arr[$part]) && strlen((string) $arr[$part]) != 0) {
  565. return false;
  566. }
  567. }
  568. $domain = $arr['host'];
  569. $domainAllowList = [
  570. /* Include current domain */
  571. $_SERVER['SERVER_NAME'],
  572. /* phpMyAdmin domains */
  573. 'wiki.phpmyadmin.net',
  574. 'www.phpmyadmin.net',
  575. 'phpmyadmin.net',
  576. 'demo.phpmyadmin.net',
  577. 'docs.phpmyadmin.net',
  578. /* mysql.com domains */
  579. 'dev.mysql.com',
  580. 'bugs.mysql.com',
  581. /* mariadb domains */
  582. 'mariadb.org',
  583. 'mariadb.com',
  584. /* php.net domains */
  585. 'php.net',
  586. 'www.php.net',
  587. /* Github domains*/
  588. 'github.com',
  589. 'www.github.com',
  590. /* Percona domains */
  591. 'www.percona.com',
  592. /* Following are doubtful ones. */
  593. 'mysqldatabaseadministration.blogspot.com',
  594. ];
  595. return in_array($domain, $domainAllowList);
  596. }
  597. /**
  598. * Replace some html-unfriendly stuff
  599. *
  600. * @param string $buffer String to process
  601. *
  602. * @return string Escaped and cleaned up text suitable for html
  603. */
  604. public static function mimeDefaultFunction(string $buffer): string
  605. {
  606. $buffer = htmlspecialchars($buffer);
  607. $buffer = str_replace(' ', ' &nbsp;', $buffer);
  608. return (string) preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer);
  609. }
  610. /**
  611. * Displays SQL query before executing.
  612. *
  613. * @param array|string $query_data Array containing queries or query itself
  614. */
  615. public static function previewSQL($query_data): void
  616. {
  617. $retval = '<div class="preview_sql">';
  618. if (empty($query_data)) {
  619. $retval .= __('No change');
  620. } elseif (is_array($query_data)) {
  621. foreach ($query_data as $query) {
  622. $retval .= Html\Generator::formatSql($query);
  623. }
  624. } else {
  625. $retval .= Html\Generator::formatSql($query_data);
  626. }
  627. $retval .= '</div>';
  628. $response = ResponseRenderer::getInstance();
  629. $response->addJSON('sql_data', $retval);
  630. }
  631. /**
  632. * recursively check if variable is empty
  633. *
  634. * @param mixed $value the variable
  635. */
  636. public static function emptyRecursive($value): bool
  637. {
  638. if (is_array($value)) {
  639. $empty = true;
  640. array_walk_recursive(
  641. $value,
  642. /**
  643. * @param mixed $item
  644. */
  645. static function ($item) use (&$empty): void {
  646. $empty = $empty && empty($item);
  647. }
  648. );
  649. return $empty;
  650. }
  651. return empty($value);
  652. }
  653. /**
  654. * Creates some globals from $_POST variables matching a pattern
  655. *
  656. * @param array $post_patterns The patterns to search for
  657. */
  658. public static function setPostAsGlobal(array $post_patterns): void
  659. {
  660. global $containerBuilder;
  661. foreach (array_keys($_POST) as $post_key) {
  662. foreach ($post_patterns as $one_post_pattern) {
  663. if (! preg_match($one_post_pattern, $post_key)) {
  664. continue;
  665. }
  666. $GLOBALS[$post_key] = $_POST[$post_key];
  667. $containerBuilder->setParameter($post_key, $GLOBALS[$post_key]);
  668. }
  669. }
  670. }
  671. /**
  672. * Gets the "true" IP address of the current user
  673. *
  674. * @return string|bool the ip of the user
  675. *
  676. * @access private
  677. */
  678. public static function getIp()
  679. {
  680. /* Get the address of user */
  681. if (empty($_SERVER['REMOTE_ADDR'])) {
  682. /* We do not know remote IP */
  683. return false;
  684. }
  685. $direct_ip = $_SERVER['REMOTE_ADDR'];
  686. /* Do we trust this IP as a proxy? If yes we will use it's header. */
  687. if (! isset($GLOBALS['cfg']['TrustedProxies'][$direct_ip])) {
  688. /* Return true IP */
  689. return $direct_ip;
  690. }
  691. /**
  692. * Parse header in form:
  693. * X-Forwarded-For: client, proxy1, proxy2
  694. */
  695. // Get header content
  696. $value = self::getenv($GLOBALS['cfg']['TrustedProxies'][$direct_ip]);
  697. // Grab first element what is client adddress
  698. $value = explode(',', $value)[0];
  699. // checks that the header contains only one IP address,
  700. $is_ip = filter_var($value, FILTER_VALIDATE_IP);
  701. if ($is_ip !== false) {
  702. // True IP behind a proxy
  703. return $value;
  704. }
  705. // We could not parse header
  706. return false;
  707. }
  708. /**
  709. * Sanitizes MySQL hostname
  710. *
  711. * * strips p: prefix(es)
  712. *
  713. * @param string $name User given hostname
  714. */
  715. public static function sanitizeMySQLHost(string $name): string
  716. {
  717. while (strtolower(substr($name, 0, 2)) === 'p:') {
  718. $name = substr($name, 2);
  719. }
  720. return $name;
  721. }
  722. /**
  723. * Sanitizes MySQL username
  724. *
  725. * * strips part behind null byte
  726. *
  727. * @param string $name User given username
  728. */
  729. public static function sanitizeMySQLUser(string $name): string
  730. {
  731. $position = strpos($name, chr(0));
  732. if ($position !== false) {
  733. return substr($name, 0, $position);
  734. }
  735. return $name;
  736. }
  737. /**
  738. * Safe unserializer wrapper
  739. *
  740. * It does not unserialize data containing objects
  741. *
  742. * @param string $data Data to unserialize
  743. *
  744. * @return mixed|null
  745. */
  746. public static function safeUnserialize(string $data)
  747. {
  748. if (! is_string($data)) {
  749. return null;
  750. }
  751. /* validate serialized data */
  752. $length = strlen($data);
  753. $depth = 0;
  754. for ($i = 0; $i < $length; $i++) {
  755. $value = $data[$i];
  756. switch ($value) {
  757. case '}':
  758. /* end of array */
  759. if ($depth <= 0) {
  760. return null;
  761. }
  762. $depth--;
  763. break;
  764. case 's':
  765. /* string */
  766. // parse sting length
  767. $strlen = intval(substr($data, $i + 2));
  768. // string start
  769. $i = strpos($data, ':', $i + 2);
  770. if ($i === false) {
  771. return null;
  772. }
  773. // skip string, quotes and ;
  774. $i += 2 + $strlen + 1;
  775. if ($data[$i] !== ';') {
  776. return null;
  777. }
  778. break;
  779. case 'b':
  780. case 'i':
  781. case 'd':
  782. /* bool, integer or double */
  783. // skip value to separator
  784. $i = strpos($data, ';', $i);
  785. if ($i === false) {
  786. return null;
  787. }
  788. break;
  789. case 'a':
  790. /* array */
  791. // find array start
  792. $i = strpos($data, '{', $i);
  793. if ($i === false) {
  794. return null;
  795. }
  796. // remember nesting
  797. $depth++;
  798. break;
  799. case 'N':
  800. /* null */
  801. // skip to end
  802. $i = strpos($data, ';', $i);
  803. if ($i === false) {
  804. return null;
  805. }
  806. break;
  807. default:
  808. /* any other elements are not wanted */
  809. return null;
  810. }
  811. }
  812. // check unterminated arrays
  813. if ($depth > 0) {
  814. return null;
  815. }
  816. return unserialize($data);
  817. }
  818. /**
  819. * Sign the sql query using hmac using the session token
  820. *
  821. * @param string $sqlQuery The sql query
  822. *
  823. * @return string
  824. */
  825. public static function signSqlQuery($sqlQuery)
  826. {
  827. global $cfg;
  828. $secret = $_SESSION[' HMAC_secret '] ?? '';
  829. return hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
  830. }
  831. /**
  832. * Check that the sql query has a valid hmac signature
  833. *
  834. * @param string $sqlQuery The sql query
  835. * @param string $signature The Signature to check
  836. */
  837. public static function checkSqlQuerySignature($sqlQuery, $signature): bool
  838. {
  839. global $cfg;
  840. $secret = $_SESSION[' HMAC_secret '] ?? '';
  841. $hmac = hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
  842. return hash_equals($hmac, $signature);
  843. }
  844. /**
  845. * Get the container builder
  846. */
  847. public static function getContainerBuilder(): ContainerBuilder
  848. {
  849. $containerBuilder = new ContainerBuilder();
  850. $loader = new PhpFileLoader($containerBuilder, new FileLocator(ROOT_PATH . 'libraries'));
  851. $loader->load('services_loader.php');
  852. return $containerBuilder;
  853. }
  854. }