Url.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. /**
  3. * Static methods for URL/hidden inputs generating
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\Crypto\Crypto;
  8. use function base64_decode;
  9. use function base64_encode;
  10. use function htmlspecialchars;
  11. use function http_build_query;
  12. use function in_array;
  13. use function ini_get;
  14. use function is_array;
  15. use function is_string;
  16. use function json_encode;
  17. use function method_exists;
  18. use function str_contains;
  19. use function strlen;
  20. use function strtr;
  21. /**
  22. * Static methods for URL/hidden inputs generating
  23. */
  24. class Url
  25. {
  26. /** @var string|null */
  27. private static $inputArgSeparator = null;
  28. /**
  29. * Generates text with hidden inputs.
  30. *
  31. * @see Url::getCommon()
  32. *
  33. * @param string|array $db optional database name
  34. * (can also be an array of parameters)
  35. * @param string $table optional table name
  36. * @param int $indent indenting level
  37. * @param string|array $skip do not generate a hidden field for this parameter
  38. * (can be an array of strings)
  39. *
  40. * @return string string with input fields
  41. */
  42. public static function getHiddenInputs(
  43. $db = '',
  44. $table = '',
  45. $indent = 0,
  46. $skip = []
  47. ) {
  48. global $config;
  49. if (is_array($db)) {
  50. $params =& $db;
  51. } else {
  52. $params = [];
  53. if (strlen((string) $db) > 0) {
  54. $params['db'] = $db;
  55. }
  56. if (strlen((string) $table) > 0) {
  57. $params['table'] = $table;
  58. }
  59. }
  60. if (! empty($GLOBALS['server']) && $GLOBALS['server'] != $GLOBALS['cfg']['ServerDefault']) {
  61. $params['server'] = $GLOBALS['server'];
  62. }
  63. if (empty($config->getCookie('pma_lang')) && ! empty($GLOBALS['lang'])) {
  64. $params['lang'] = $GLOBALS['lang'];
  65. }
  66. if (! is_array($skip)) {
  67. if (isset($params[$skip])) {
  68. unset($params[$skip]);
  69. }
  70. } else {
  71. foreach ($skip as $skipping) {
  72. if (! isset($params[$skipping])) {
  73. continue;
  74. }
  75. unset($params[$skipping]);
  76. }
  77. }
  78. return self::getHiddenFields($params);
  79. }
  80. /**
  81. * create hidden form fields from array with name => value
  82. *
  83. * <code>
  84. * $values = array(
  85. * 'aaa' => aaa,
  86. * 'bbb' => array(
  87. * 'bbb_0',
  88. * 'bbb_1',
  89. * ),
  90. * 'ccc' => array(
  91. * 'a' => 'ccc_a',
  92. * 'b' => 'ccc_b',
  93. * ),
  94. * );
  95. * echo Url::getHiddenFields($values);
  96. *
  97. * // produces:
  98. * <input type="hidden" name="aaa" Value="aaa">
  99. * <input type="hidden" name="bbb[0]" Value="bbb_0">
  100. * <input type="hidden" name="bbb[1]" Value="bbb_1">
  101. * <input type="hidden" name="ccc[a]" Value="ccc_a">
  102. * <input type="hidden" name="ccc[b]" Value="ccc_b">
  103. * </code>
  104. *
  105. * @param array $values hidden values
  106. * @param string $pre prefix
  107. * @param bool $is_token if token already added in hidden input field
  108. *
  109. * @return string form fields of type hidden
  110. */
  111. public static function getHiddenFields(array $values, $pre = '', $is_token = false)
  112. {
  113. $fields = '';
  114. /* Always include token in plain forms */
  115. if ($is_token === false && isset($_SESSION[' PMA_token '])) {
  116. $values['token'] = $_SESSION[' PMA_token '];
  117. }
  118. foreach ($values as $name => $value) {
  119. if (! empty($pre)) {
  120. $name = $pre . '[' . $name . ']';
  121. }
  122. if (is_array($value)) {
  123. $fields .= self::getHiddenFields($value, $name, true);
  124. } else {
  125. // do not generate an ending "\n" because
  126. // Url::getHiddenInputs() is sometimes called
  127. // from a JS document.write()
  128. $fields .= '<input type="hidden" name="' . htmlspecialchars((string) $name)
  129. . '" value="' . htmlspecialchars((string) $value) . '">';
  130. }
  131. }
  132. return $fields;
  133. }
  134. /**
  135. * Generates text with URL parameters.
  136. *
  137. * <code>
  138. * $params['myparam'] = 'myvalue';
  139. * $params['db'] = 'mysql';
  140. * $params['table'] = 'rights';
  141. * // note the missing ?
  142. * echo 'script.php' . Url::getCommon($params);
  143. * // produces with cookies enabled:
  144. * // script.php?myparam=myvalue&db=mysql&table=rights
  145. * // with cookies disabled:
  146. * // script.php?server=1&lang=en&myparam=myvalue&db=mysql
  147. * // &table=rights
  148. *
  149. * // note the missing ?
  150. * echo 'script.php' . Url::getCommon();
  151. * // produces with cookies enabled:
  152. * // script.php
  153. * // with cookies disabled:
  154. * // script.php?server=1&lang=en
  155. * </code>
  156. *
  157. * @param array<string,int|string|bool> $params optional, Contains an associative array with url params
  158. * @param string $divider optional character to use instead of '?'
  159. * @param bool $encrypt whether to encrypt URL params
  160. *
  161. * @return string string with URL parameters
  162. */
  163. public static function getCommon(array $params = [], $divider = '?', $encrypt = true)
  164. {
  165. return self::getCommonRaw($params, $divider, $encrypt);
  166. }
  167. /**
  168. * Generates text with URL parameters.
  169. *
  170. * <code>
  171. * $params['myparam'] = 'myvalue';
  172. * $params['db'] = 'mysql';
  173. * $params['table'] = 'rights';
  174. * // note the missing ?
  175. * echo 'script.php' . Url::getCommon($params);
  176. * // produces with cookies enabled:
  177. * // script.php?myparam=myvalue&db=mysql&table=rights
  178. * // with cookies disabled:
  179. * // script.php?server=1&lang=en&myparam=myvalue&db=mysql
  180. * // &table=rights
  181. *
  182. * // note the missing ?
  183. * echo 'script.php' . Url::getCommon();
  184. * // produces with cookies enabled:
  185. * // script.php
  186. * // with cookies disabled:
  187. * // script.php?server=1&lang=en
  188. * </code>
  189. *
  190. * @param array<string|int,int|string|bool> $params optional, Contains an associative array with url params
  191. * @param string $divider optional character to use instead of '?'
  192. * @param bool $encrypt whether to encrypt URL params
  193. *
  194. * @return string string with URL parameters
  195. */
  196. public static function getCommonRaw(array $params = [], $divider = '?', $encrypt = true)
  197. {
  198. global $config;
  199. // avoid overwriting when creating navigation panel links to servers
  200. if (
  201. isset($GLOBALS['server'])
  202. && $GLOBALS['server'] != $GLOBALS['cfg']['ServerDefault']
  203. && ! isset($params['server'])
  204. && ! $config->get('is_setup')
  205. ) {
  206. $params['server'] = $GLOBALS['server'];
  207. }
  208. // Can be null when the user is missing an extension.
  209. if ($config !== null && empty($config->getCookie('pma_lang')) && ! empty($GLOBALS['lang'])) {
  210. $params['lang'] = $GLOBALS['lang'];
  211. }
  212. $query = self::buildHttpQuery($params, $encrypt);
  213. if (($divider !== '?' && $divider !== self::getArgSeparator()) || strlen($query) > 0) {
  214. return $divider . $query;
  215. }
  216. return '';
  217. }
  218. /**
  219. * @param array<int|string, mixed> $params
  220. * @param bool $encrypt whether to encrypt URL params
  221. *
  222. * @return string
  223. */
  224. public static function buildHttpQuery($params, $encrypt = true)
  225. {
  226. global $config;
  227. $separator = self::getArgSeparator();
  228. if (! $encrypt || $config === null || ! $config->get('URLQueryEncryption')) {
  229. return http_build_query($params, '', $separator);
  230. }
  231. $data = $params;
  232. $keys = [
  233. 'db',
  234. 'table',
  235. 'field',
  236. 'sql_query',
  237. 'sql_signature',
  238. 'where_clause',
  239. 'goto',
  240. 'back',
  241. 'message_to_show',
  242. 'username',
  243. 'hostname',
  244. 'dbname',
  245. 'tablename',
  246. 'checkprivsdb',
  247. 'checkprivstable',
  248. ];
  249. $paramsToEncrypt = [];
  250. foreach ($params as $paramKey => $paramValue) {
  251. if (! in_array($paramKey, $keys)) {
  252. continue;
  253. }
  254. $paramsToEncrypt[$paramKey] = $paramValue;
  255. unset($data[$paramKey]);
  256. }
  257. if ($paramsToEncrypt !== []) {
  258. $data['eq'] = self::encryptQuery((string) json_encode($paramsToEncrypt));
  259. }
  260. return http_build_query($data, '', $separator);
  261. }
  262. public static function encryptQuery(string $query): string
  263. {
  264. $crypto = new Crypto();
  265. return strtr(base64_encode($crypto->encrypt($query)), '+/', '-_');
  266. }
  267. public static function decryptQuery(string $query): ?string
  268. {
  269. $crypto = new Crypto();
  270. return $crypto->decrypt(base64_decode(strtr($query, '-_', '+/')));
  271. }
  272. /**
  273. * Returns url separator character used for separating url parts.
  274. *
  275. * Extracted from 'arg_separator.input' as set in php.ini, but prefers '&' and ';'.
  276. *
  277. * @see https://www.php.net/manual/en/ini.core.php#ini.arg-separator.input
  278. * @see https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2
  279. */
  280. public static function getArgSeparator(): string
  281. {
  282. if (is_string(self::$inputArgSeparator)) {
  283. return self::$inputArgSeparator;
  284. }
  285. $separator = self::getArgSeparatorValueFromIni();
  286. if (! is_string($separator) || $separator === '' || str_contains($separator, '&')) {
  287. return self::$inputArgSeparator = '&';
  288. }
  289. if (str_contains($separator, ';')) {
  290. return self::$inputArgSeparator = ';';
  291. }
  292. // uses first character
  293. return self::$inputArgSeparator = $separator[0];
  294. }
  295. /** @return string|false */
  296. private static function getArgSeparatorValueFromIni()
  297. {
  298. /** @psalm-suppress ArgumentTypeCoercion */
  299. if (method_exists('PhpMyAdmin\Tests\UrlTest', 'getInputArgSeparator')) {
  300. // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName
  301. return \PhpMyAdmin\Tests\UrlTest::getInputArgSeparator();
  302. }
  303. return ini_get('arg_separator.input');
  304. }
  305. /**
  306. * @param string $route Route to use
  307. * @param array $additionalParameters Additional URL parameters
  308. */
  309. public static function getFromRoute(string $route, array $additionalParameters = [], bool $encrypt = true): string
  310. {
  311. return 'index.php?route=' . $route . self::getCommon($additionalParameters, self::getArgSeparator(), $encrypt);
  312. }
  313. }