Sql.php 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Display\Results as DisplayResults;
  5. use PhpMyAdmin\Html\Generator;
  6. use PhpMyAdmin\Html\MySQLDocumentation;
  7. use PhpMyAdmin\Query\Generator as QueryGenerator;
  8. use PhpMyAdmin\Query\Utilities;
  9. use PhpMyAdmin\SqlParser\Statements\AlterStatement;
  10. use PhpMyAdmin\SqlParser\Statements\DropStatement;
  11. use PhpMyAdmin\SqlParser\Statements\SelectStatement;
  12. use PhpMyAdmin\SqlParser\Utils\Query;
  13. use function array_map;
  14. use function array_sum;
  15. use function bin2hex;
  16. use function ceil;
  17. use function count;
  18. use function explode;
  19. use function htmlspecialchars;
  20. use function in_array;
  21. use function is_array;
  22. use function is_bool;
  23. use function microtime;
  24. use function session_start;
  25. use function session_write_close;
  26. use function sprintf;
  27. use function str_replace;
  28. use function stripos;
  29. use function strlen;
  30. use function strpos;
  31. use function ucwords;
  32. use function defined;
  33. /**
  34. * Set of functions for the SQL executor
  35. */
  36. class Sql
  37. {
  38. /** @var DatabaseInterface */
  39. private $dbi;
  40. /** @var Relation */
  41. private $relation;
  42. /** @var RelationCleanup */
  43. private $relationCleanup;
  44. /** @var Transformations */
  45. private $transformations;
  46. /** @var Operations */
  47. private $operations;
  48. /** @var Template */
  49. private $template;
  50. public function __construct(
  51. DatabaseInterface $dbi,
  52. Relation $relation,
  53. RelationCleanup $relationCleanup,
  54. Operations $operations,
  55. Transformations $transformations,
  56. Template $template
  57. ) {
  58. $this->dbi = $dbi;
  59. $this->relation = $relation;
  60. $this->relationCleanup = $relationCleanup;
  61. $this->operations = $operations;
  62. $this->transformations = $transformations;
  63. $this->template = $template;
  64. }
  65. /**
  66. * Handle remembered sorting order, only for single table query
  67. *
  68. * @param string $db database name
  69. * @param string $table table name
  70. * @param array $analyzed_sql_results the analyzed query results
  71. * @param string $full_sql_query SQL query
  72. *
  73. * @return void
  74. */
  75. private function handleSortOrder(
  76. $db,
  77. $table,
  78. array &$analyzed_sql_results,
  79. &$full_sql_query
  80. ) {
  81. $pmatable = new Table($table, $db);
  82. if (empty($analyzed_sql_results['order'])) {
  83. // Retrieving the name of the column we should sort after.
  84. $sortCol = $pmatable->getUiProp(Table::PROP_SORTED_COLUMN);
  85. if (empty($sortCol)) {
  86. return;
  87. }
  88. // Remove the name of the table from the retrieved field name.
  89. $sortCol = str_replace(
  90. Util::backquote($table) . '.',
  91. '',
  92. $sortCol
  93. );
  94. // Create the new query.
  95. $full_sql_query = Query::replaceClause(
  96. $analyzed_sql_results['statement'],
  97. $analyzed_sql_results['parser']->list,
  98. 'ORDER BY ' . $sortCol
  99. );
  100. // TODO: Avoid reparsing the query.
  101. $analyzed_sql_results = Query::getAll($full_sql_query);
  102. } else {
  103. // Store the remembered table into session.
  104. $pmatable->setUiProp(
  105. Table::PROP_SORTED_COLUMN,
  106. Query::getClause(
  107. $analyzed_sql_results['statement'],
  108. $analyzed_sql_results['parser']->list,
  109. 'ORDER BY'
  110. )
  111. );
  112. }
  113. }
  114. /**
  115. * Append limit clause to SQL query
  116. *
  117. * @param array $analyzed_sql_results the analyzed query results
  118. *
  119. * @return string limit clause appended SQL query
  120. */
  121. private function getSqlWithLimitClause(array &$analyzed_sql_results)
  122. {
  123. return Query::replaceClause(
  124. $analyzed_sql_results['statement'],
  125. $analyzed_sql_results['parser']->list,
  126. 'LIMIT ' . $_SESSION['tmpval']['pos'] . ', '
  127. . $_SESSION['tmpval']['max_rows']
  128. );
  129. }
  130. /**
  131. * Verify whether the result set has columns from just one table
  132. *
  133. * @param array $fields_meta meta fields
  134. *
  135. * @return bool whether the result set has columns from just one table
  136. */
  137. private function resultSetHasJustOneTable(array $fields_meta)
  138. {
  139. $just_one_table = true;
  140. $prev_table = '';
  141. foreach ($fields_meta as $one_field_meta) {
  142. if ($one_field_meta->table != ''
  143. && $prev_table != ''
  144. && $one_field_meta->table != $prev_table
  145. ) {
  146. $just_one_table = false;
  147. }
  148. if ($one_field_meta->table == '') {
  149. continue;
  150. }
  151. $prev_table = $one_field_meta->table;
  152. }
  153. return $just_one_table && $prev_table != '';
  154. }
  155. /**
  156. * Verify whether the result set contains all the columns
  157. * of at least one unique key
  158. *
  159. * @param string $db database name
  160. * @param string $table table name
  161. * @param array $fields_meta meta fields
  162. *
  163. * @return bool whether the result set contains a unique key
  164. */
  165. private function resultSetContainsUniqueKey($db, $table, array $fields_meta)
  166. {
  167. $columns = $this->dbi->getColumns($db, $table);
  168. $resultSetColumnNames = [];
  169. foreach ($fields_meta as $oneMeta) {
  170. $resultSetColumnNames[] = $oneMeta->name;
  171. }
  172. foreach (Index::getFromTable($table, $db) as $index) {
  173. if (! $index->isUnique()) {
  174. continue;
  175. }
  176. $indexColumns = $index->getColumns();
  177. $numberFound = 0;
  178. foreach ($indexColumns as $indexColumnName => $dummy) {
  179. if (in_array($indexColumnName, $resultSetColumnNames)) {
  180. $numberFound++;
  181. } elseif (! in_array($indexColumnName, $columns)) {
  182. $numberFound++;
  183. } elseif (strpos($columns[$indexColumnName]['Extra'], 'INVISIBLE') !== false) {
  184. $numberFound++;
  185. }
  186. }
  187. if ($numberFound == count($indexColumns)) {
  188. return true;
  189. }
  190. }
  191. return false;
  192. }
  193. /**
  194. * Get the HTML for relational column dropdown
  195. * During grid edit, if we have a relational field, returns the html for the
  196. * dropdown
  197. *
  198. * @param string $db current database
  199. * @param string $table current table
  200. * @param string $column current column
  201. * @param string $curr_value current selected value
  202. *
  203. * @return string html for the dropdown
  204. */
  205. public function getHtmlForRelationalColumnDropdown($db, $table, $column, $curr_value)
  206. {
  207. $foreigners = $this->relation->getForeigners($db, $table, $column);
  208. $foreignData = $this->relation->getForeignData(
  209. $foreigners,
  210. $column,
  211. false,
  212. '',
  213. ''
  214. );
  215. if ($foreignData['disp_row'] == null) {
  216. //Handle the case when number of values
  217. //is more than $cfg['ForeignKeyMaxLimit']
  218. $_url_params = [
  219. 'db' => $db,
  220. 'table' => $table,
  221. 'field' => $column,
  222. ];
  223. $dropdown = $this->template->render('sql/relational_column_dropdown', [
  224. 'current_value' => $_POST['curr_value'],
  225. 'params' => $_url_params,
  226. ]);
  227. } else {
  228. $dropdown = $this->relation->foreignDropdown(
  229. $foreignData['disp_row'],
  230. $foreignData['foreign_field'],
  231. $foreignData['foreign_display'],
  232. $curr_value,
  233. $GLOBALS['cfg']['ForeignKeyMaxLimit']
  234. );
  235. $dropdown = '<select>' . $dropdown . '</select>';
  236. }
  237. return $dropdown;
  238. }
  239. /** @return array<string, int|array> */
  240. private function getDetailedProfilingStats(array $profilingResults): array
  241. {
  242. $profiling = [
  243. 'total_time' => 0,
  244. 'states' => [],
  245. 'chart' => [],
  246. 'profile' => [],
  247. ];
  248. foreach ($profilingResults as $oneResult) {
  249. $status = ucwords($oneResult['Status']);
  250. $profiling['total_time'] += $oneResult['Duration'];
  251. $profiling['profile'][] = [
  252. 'status' => $status,
  253. 'duration' => Util::formatNumber($oneResult['Duration'], 3, 1),
  254. 'duration_raw' => $oneResult['Duration'],
  255. ];
  256. if (! isset($profiling['states'][$status])) {
  257. $profiling['states'][$status] = [
  258. 'total_time' => $oneResult['Duration'],
  259. 'calls' => 1,
  260. ];
  261. $profiling['chart'][$status] = $oneResult['Duration'];
  262. } else {
  263. $profiling['states'][$status]['calls']++;
  264. $profiling['chart'][$status] += $oneResult['Duration'];
  265. }
  266. }
  267. return $profiling;
  268. }
  269. /**
  270. * Get value of a column for a specific row (marked by $whereClause)
  271. */
  272. public function getFullValuesForSetColumn(
  273. string $db,
  274. string $table,
  275. string $column,
  276. string $whereClause
  277. ): string {
  278. $row = $this->dbi->fetchSingleRow(sprintf(
  279. 'SELECT `%s` FROM `%s`.`%s` WHERE %s',
  280. $column,
  281. $db,
  282. $table,
  283. $whereClause
  284. ));
  285. if ($row === null) {
  286. return '';
  287. }
  288. return $row[$column];
  289. }
  290. /**
  291. * Get all the values for a enum column or set column in a table
  292. *
  293. * @param string $db current database
  294. * @param string $table current table
  295. * @param string $column current column
  296. *
  297. * @return array array containing the value list for the column
  298. */
  299. public function getValuesForColumn($db, $table, $column)
  300. {
  301. $field_info_query = QueryGenerator::getColumnsSql($db, $table, $this->dbi->escapeString($column));
  302. $field_info_result = $this->dbi->fetchResult(
  303. $field_info_query,
  304. null,
  305. null,
  306. DatabaseInterface::CONNECT_USER,
  307. DatabaseInterface::QUERY_STORE
  308. );
  309. return Util::parseEnumSetValues($field_info_result[0]['Type']);
  310. }
  311. /**
  312. * Function to check whether to remember the sorting order or not
  313. *
  314. * @param array $analyzed_sql_results the analyzed query and other variables set
  315. * after analyzing the query
  316. *
  317. * @return bool
  318. */
  319. private function isRememberSortingOrder(array $analyzed_sql_results)
  320. {
  321. return isset($analyzed_sql_results['select_expr'], $analyzed_sql_results['select_tables'])
  322. && $GLOBALS['cfg']['RememberSorting']
  323. && ! ($analyzed_sql_results['is_count']
  324. || $analyzed_sql_results['is_export']
  325. || $analyzed_sql_results['is_func']
  326. || $analyzed_sql_results['is_analyse'])
  327. && $analyzed_sql_results['select_from']
  328. && (empty($analyzed_sql_results['select_expr'])
  329. || ((count($analyzed_sql_results['select_expr']) === 1)
  330. && ($analyzed_sql_results['select_expr'][0] === '*')))
  331. && count($analyzed_sql_results['select_tables']) === 1;
  332. }
  333. /**
  334. * Function to check whether the LIMIT clause should be appended or not
  335. *
  336. * @param array $analyzed_sql_results the analyzed query and other variables set
  337. * after analyzing the query
  338. *
  339. * @return bool
  340. */
  341. private function isAppendLimitClause(array $analyzed_sql_results)
  342. {
  343. // Assigning LIMIT clause to an syntactically-wrong query
  344. // is not needed. Also we would want to show the true query
  345. // and the true error message to the query executor
  346. return (isset($analyzed_sql_results['parser'])
  347. && count($analyzed_sql_results['parser']->errors) === 0)
  348. && ($_SESSION['tmpval']['max_rows'] !== 'all')
  349. && ! ($analyzed_sql_results['is_export']
  350. || $analyzed_sql_results['is_analyse'])
  351. && ($analyzed_sql_results['select_from']
  352. || $analyzed_sql_results['is_subquery'])
  353. && empty($analyzed_sql_results['limit']);
  354. }
  355. /**
  356. * Function to check whether this query is for just browsing
  357. *
  358. * @param array<string, mixed> $analyzed_sql_results the analyzed query and other variables set
  359. * after analyzing the query
  360. * @param bool|null $find_real_end whether the real end should be found
  361. */
  362. public static function isJustBrowsing(array $analyzed_sql_results, ?bool $find_real_end): bool
  363. {
  364. return ! $analyzed_sql_results['is_group']
  365. && ! $analyzed_sql_results['is_func']
  366. && empty($analyzed_sql_results['union'])
  367. && empty($analyzed_sql_results['distinct'])
  368. && $analyzed_sql_results['select_from']
  369. && (count($analyzed_sql_results['select_tables']) === 1)
  370. && (empty($analyzed_sql_results['statement']->where)
  371. || (count($analyzed_sql_results['statement']->where) === 1
  372. && $analyzed_sql_results['statement']->where[0]->expr === '1'))
  373. && empty($analyzed_sql_results['group'])
  374. && ! isset($find_real_end)
  375. && ! $analyzed_sql_results['is_subquery']
  376. && ! $analyzed_sql_results['join']
  377. && empty($analyzed_sql_results['having']);
  378. }
  379. /**
  380. * Function to check whether the related transformation information should be deleted
  381. *
  382. * @param array $analyzed_sql_results the analyzed query and other variables set
  383. * after analyzing the query
  384. *
  385. * @return bool
  386. */
  387. private function isDeleteTransformationInfo(array $analyzed_sql_results)
  388. {
  389. return ! empty($analyzed_sql_results['querytype'])
  390. && (($analyzed_sql_results['querytype'] === 'ALTER')
  391. || ($analyzed_sql_results['querytype'] === 'DROP'));
  392. }
  393. /**
  394. * Function to check whether the user has rights to drop the database
  395. *
  396. * @param array $analyzed_sql_results the analyzed query and other variables set
  397. * after analyzing the query
  398. * @param bool $allowUserDropDatabase whether the user is allowed to drop db
  399. * @param bool $is_superuser whether this user is a superuser
  400. *
  401. * @return bool
  402. */
  403. public function hasNoRightsToDropDatabase(
  404. array $analyzed_sql_results,
  405. $allowUserDropDatabase,
  406. $is_superuser
  407. ) {
  408. return ! $allowUserDropDatabase
  409. && isset($analyzed_sql_results['drop_database'])
  410. && $analyzed_sql_results['drop_database']
  411. && ! $is_superuser;
  412. }
  413. /**
  414. * Function to set a column property
  415. *
  416. * @param Table $pmatable Table instance
  417. * @param string $request_index col_order|col_visib
  418. *
  419. * @return bool|Message
  420. */
  421. public function setColumnProperty($pmatable, $request_index)
  422. {
  423. $property_value = array_map('intval', explode(',', $_POST[$request_index]));
  424. switch ($request_index) {
  425. case 'col_order':
  426. $property_to_set = Table::PROP_COLUMN_ORDER;
  427. break;
  428. case 'col_visib':
  429. $property_to_set = Table::PROP_COLUMN_VISIB;
  430. break;
  431. default:
  432. $property_to_set = '';
  433. }
  434. return $pmatable->setUiProp(
  435. $property_to_set,
  436. $property_value,
  437. $_POST['table_create_time'] ?? null
  438. );
  439. }
  440. /**
  441. * Function to find the real end of rows
  442. *
  443. * @param string $db the current database
  444. * @param string $table the current table
  445. *
  446. * @return mixed the number of rows if "retain" param is true, otherwise true
  447. */
  448. public function findRealEndOfRows($db, $table)
  449. {
  450. $unlim_num_rows = $this->dbi->getTable($db, $table)->countRecords(true);
  451. $_SESSION['tmpval']['pos'] = $this->getStartPosToDisplayRow($unlim_num_rows);
  452. return $unlim_num_rows;
  453. }
  454. /**
  455. * Function to get the default sql query for browsing page
  456. *
  457. * @param string $db the current database
  458. * @param string $table the current table
  459. *
  460. * @return string the default $sql_query for browse page
  461. */
  462. public function getDefaultSqlQueryForBrowse($db, $table)
  463. {
  464. $bookmark = Bookmark::get(
  465. $this->dbi,
  466. $GLOBALS['cfg']['Server']['user'],
  467. $db,
  468. $table,
  469. 'label',
  470. false,
  471. true
  472. );
  473. if (! empty($bookmark) && ! empty($bookmark->getQuery())) {
  474. $GLOBALS['using_bookmark_message'] = Message::notice(
  475. __('Using bookmark "%s" as default browse query.')
  476. );
  477. $GLOBALS['using_bookmark_message']->addParam($table);
  478. $GLOBALS['using_bookmark_message']->addHtml(
  479. MySQLDocumentation::showDocumentation('faq', 'faq6-22')
  480. );
  481. $sql_query = $bookmark->getQuery();
  482. } else {
  483. $defaultOrderByClause = '';
  484. if (isset($GLOBALS['cfg']['TablePrimaryKeyOrder'])
  485. && ($GLOBALS['cfg']['TablePrimaryKeyOrder'] !== 'NONE')
  486. ) {
  487. $primaryKey = null;
  488. $primary = Index::getPrimary($table, $db);
  489. if ($primary !== false) {
  490. $primarycols = $primary->getColumns();
  491. foreach ($primarycols as $col) {
  492. $primaryKey = $col->getName();
  493. break;
  494. }
  495. if ($primaryKey != null) {
  496. $defaultOrderByClause = ' ORDER BY '
  497. . Util::backquote($table) . '.'
  498. . Util::backquote($primaryKey) . ' '
  499. . $GLOBALS['cfg']['TablePrimaryKeyOrder'];
  500. }
  501. }
  502. }
  503. $sql_query = 'SELECT * FROM ' . Util::backquote($table)
  504. . $defaultOrderByClause;
  505. }
  506. return $sql_query;
  507. }
  508. /**
  509. * Responds an error when an error happens when executing the query
  510. *
  511. * @param bool $is_gotofile whether goto file or not
  512. * @param string $error error after executing the query
  513. * @param string $full_sql_query full sql query
  514. *
  515. * @return void
  516. */
  517. private function handleQueryExecuteError($is_gotofile, $error, $full_sql_query)
  518. {
  519. if ($is_gotofile) {
  520. $message = Message::rawError($error);
  521. $response = Response::getInstance();
  522. $response->setRequestStatus(false);
  523. $response->addJSON('message', $message);
  524. } else {
  525. Generator::mysqlDie($error, $full_sql_query, '', '');
  526. }
  527. exit;
  528. }
  529. /**
  530. * Function to store the query as a bookmark
  531. *
  532. * @param string $db the current database
  533. * @param string $bkm_user the bookmarking user
  534. * @param string $sql_query_for_bookmark the query to be stored in bookmark
  535. * @param string $bkm_label bookmark label
  536. * @param bool $bkm_replace whether to replace existing bookmarks
  537. *
  538. * @return void
  539. */
  540. public function storeTheQueryAsBookmark(
  541. $db,
  542. $bkm_user,
  543. $sql_query_for_bookmark,
  544. $bkm_label,
  545. bool $bkm_replace
  546. ) {
  547. $bfields = [
  548. 'bkm_database' => $db,
  549. 'bkm_user' => $bkm_user,
  550. 'bkm_sql_query' => $sql_query_for_bookmark,
  551. 'bkm_label' => $bkm_label,
  552. ];
  553. // Should we replace bookmark?
  554. if ($bkm_replace) {
  555. $bookmarks = Bookmark::getList(
  556. $this->dbi,
  557. $GLOBALS['cfg']['Server']['user'],
  558. $db
  559. );
  560. foreach ($bookmarks as $bookmark) {
  561. if ($bookmark->getLabel() != $bkm_label) {
  562. continue;
  563. }
  564. $bookmark->delete();
  565. }
  566. }
  567. $bookmark = Bookmark::createBookmark(
  568. $this->dbi,
  569. $GLOBALS['cfg']['Server']['user'],
  570. $bfields,
  571. isset($_POST['bkm_all_users'])
  572. );
  573. $bookmark->save();
  574. }
  575. /**
  576. * Executes the SQL query and measures its execution time
  577. *
  578. * @param string $full_sql_query the full sql query
  579. *
  580. * @return array ($result, $querytime)
  581. */
  582. private function executeQueryAndMeasureTime($full_sql_query)
  583. {
  584. if (! defined('TESTSUITE')) {
  585. // close session in case the query takes too long
  586. session_write_close();
  587. }
  588. // Measure query time.
  589. $querytime_before = array_sum(explode(' ', microtime()));
  590. $result = @$this->dbi->tryQuery(
  591. $full_sql_query,
  592. DatabaseInterface::CONNECT_USER,
  593. DatabaseInterface::QUERY_STORE
  594. );
  595. $querytime_after = array_sum(explode(' ', microtime()));
  596. if (! defined('TESTSUITE')) {
  597. // reopen session
  598. session_start();
  599. }
  600. return [
  601. $result,
  602. $querytime_after - $querytime_before,
  603. ];
  604. }
  605. /**
  606. * Function to get the affected or changed number of rows after executing a query
  607. *
  608. * @param bool $is_affected whether the query affected a table
  609. * @param mixed $result results of executing the query
  610. *
  611. * @return int number of rows affected or changed
  612. */
  613. private function getNumberOfRowsAffectedOrChanged($is_affected, $result)
  614. {
  615. if (! $is_affected) {
  616. $num_rows = $result ? @$this->dbi->numRows($result) : 0;
  617. } else {
  618. $num_rows = @$this->dbi->affectedRows();
  619. }
  620. return $num_rows;
  621. }
  622. /**
  623. * Checks if the current database has changed
  624. * This could happen if the user sends a query like "USE `database`;"
  625. *
  626. * @param string $db the database in the query
  627. *
  628. * @return bool whether to reload the navigation(1) or not(0)
  629. */
  630. private function hasCurrentDbChanged($db): bool
  631. {
  632. if (strlen($db) > 0) {
  633. $current_db = $this->dbi->fetchValue('SELECT DATABASE()');
  634. // $current_db is false, except when a USE statement was sent
  635. return ($current_db != false) && ($db !== $current_db);
  636. }
  637. return false;
  638. }
  639. /**
  640. * If a table, database or column gets dropped, clean comments.
  641. *
  642. * @param string $db current database
  643. * @param string $table current table
  644. * @param string|null $column current column
  645. * @param bool $purge whether purge set or not
  646. *
  647. * @return void
  648. */
  649. private function cleanupRelations($db, $table, ?string $column, $purge)
  650. {
  651. if (empty($purge) || strlen($db) <= 0) {
  652. return;
  653. }
  654. if (strlen($table) > 0) {
  655. if (isset($column) && strlen($column) > 0) {
  656. $this->relationCleanup->column($db, $table, $column);
  657. } else {
  658. $this->relationCleanup->table($db, $table);
  659. }
  660. } else {
  661. $this->relationCleanup->database($db);
  662. }
  663. }
  664. /**
  665. * Function to count the total number of rows for the same 'SELECT' query without
  666. * the 'LIMIT' clause that may have been programatically added
  667. *
  668. * @param int $num_rows number of rows affected/changed by the query
  669. * @param bool $justBrowsing whether just browsing or not
  670. * @param string $db the current database
  671. * @param string $table the current table
  672. * @param array $analyzed_sql_results the analyzed query and other variables set
  673. * after analyzing the query
  674. *
  675. * @return int unlimited number of rows
  676. */
  677. private function countQueryResults(
  678. $num_rows,
  679. $justBrowsing,
  680. $db,
  681. $table,
  682. array $analyzed_sql_results
  683. ) {
  684. /* Shortcut for not analyzed/empty query */
  685. if (empty($analyzed_sql_results)) {
  686. return 0;
  687. }
  688. if (! $this->isAppendLimitClause($analyzed_sql_results)) {
  689. // if we did not append a limit, set this to get a correct
  690. // "Showing rows..." message
  691. // $_SESSION['tmpval']['max_rows'] = 'all';
  692. $unlim_num_rows = $num_rows;
  693. } elseif ($this->isAppendLimitClause($analyzed_sql_results) && $_SESSION['tmpval']['max_rows'] > $num_rows) {
  694. // When user has not defined a limit in query and total rows in
  695. // result are less than max_rows to display, there is no need
  696. // to count total rows for that query again
  697. $unlim_num_rows = $_SESSION['tmpval']['pos'] + $num_rows;
  698. } elseif ($analyzed_sql_results['querytype'] === 'SELECT'
  699. || $analyzed_sql_results['is_subquery']
  700. ) {
  701. // c o u n t q u e r y
  702. // If we are "just browsing", there is only one table (and no join),
  703. // and no WHERE clause (or just 'WHERE 1 '),
  704. // we do a quick count (which uses MaxExactCount) because
  705. // SQL_CALC_FOUND_ROWS is not quick on large InnoDB tables
  706. // However, do not count again if we did it previously
  707. // due to $find_real_end == true
  708. if ($justBrowsing) {
  709. // Get row count (is approximate for InnoDB)
  710. $unlim_num_rows = $this->dbi->getTable($db, $table)->countRecords();
  711. /**
  712. * @todo Can we know at this point that this is InnoDB,
  713. * (in this case there would be no need for getting
  714. * an exact count)?
  715. */
  716. if ($unlim_num_rows < $GLOBALS['cfg']['MaxExactCount']) {
  717. // Get the exact count if approximate count
  718. // is less than MaxExactCount
  719. /**
  720. * @todo In countRecords(), MaxExactCount is also verified,
  721. * so can we avoid checking it twice?
  722. */
  723. $unlim_num_rows = $this->dbi->getTable($db, $table)
  724. ->countRecords(true);
  725. }
  726. } else {
  727. $statement = $analyzed_sql_results['statement'];
  728. $token_list = $analyzed_sql_results['parser']->list;
  729. $replaces = [
  730. // Remove ORDER BY to decrease unnecessary sorting time
  731. [
  732. 'ORDER BY',
  733. '',
  734. ],
  735. // Removes LIMIT clause that might have been added
  736. [
  737. 'LIMIT',
  738. '',
  739. ],
  740. ];
  741. $count_query = 'SELECT COUNT(*) FROM (' . Query::replaceClauses(
  742. $statement,
  743. $token_list,
  744. $replaces
  745. ) . ') as cnt';
  746. $unlim_num_rows = $this->dbi->fetchValue($count_query);
  747. if ($unlim_num_rows === false) {
  748. $unlim_num_rows = 0;
  749. }
  750. }
  751. } else {// not $is_select
  752. $unlim_num_rows = 0;
  753. }
  754. return $unlim_num_rows;
  755. }
  756. /**
  757. * Function to handle all aspects relating to executing the query
  758. *
  759. * @param array $analyzed_sql_results analyzed sql results
  760. * @param string $full_sql_query full sql query
  761. * @param bool $is_gotofile whether to go to a file
  762. * @param string|null $db current database
  763. * @param string|null $table current table
  764. * @param bool|null $find_real_end whether to find the real end
  765. * @param string $sql_query_for_bookmark sql query to be stored as bookmark
  766. * @param array $extra_data extra data
  767. *
  768. * @return mixed
  769. */
  770. private function executeTheQuery(
  771. array $analyzed_sql_results,
  772. $full_sql_query,
  773. $is_gotofile,
  774. $db,
  775. $table,
  776. ?bool $find_real_end,
  777. $sql_query_for_bookmark,
  778. $extra_data
  779. ) {
  780. $response = Response::getInstance();
  781. $response->getHeader()->getMenu()->setTable($table);
  782. // Only if we ask to see the php code
  783. if (isset($GLOBALS['show_as_php'])) {
  784. $result = null;
  785. $num_rows = 0;
  786. $unlim_num_rows = 0;
  787. } else { // If we don't ask to see the php code
  788. Profiling::enable($this->dbi);
  789. [
  790. $result,
  791. $GLOBALS['querytime'],
  792. ] = $this->executeQueryAndMeasureTime($full_sql_query);
  793. // Displays an error message if required and stop parsing the script
  794. $error = $this->dbi->getError();
  795. if ($error && $GLOBALS['cfg']['IgnoreMultiSubmitErrors']) {
  796. $extra_data['error'] = $error;
  797. } elseif ($error) {
  798. $this->handleQueryExecuteError($is_gotofile, $error, $full_sql_query);
  799. }
  800. // If there are no errors and bookmarklabel was given,
  801. // store the query as a bookmark
  802. if (! empty($_POST['bkm_label']) && ! empty($sql_query_for_bookmark)) {
  803. $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']);
  804. $this->storeTheQueryAsBookmark(
  805. $db,
  806. is_array($cfgBookmark) ? $cfgBookmark['user'] : '',
  807. $sql_query_for_bookmark,
  808. $_POST['bkm_label'],
  809. isset($_POST['bkm_replace'])
  810. );
  811. }
  812. // Gets the number of rows affected/returned
  813. // (This must be done immediately after the query because
  814. // mysql_affected_rows() reports about the last query done)
  815. $num_rows = $this->getNumberOfRowsAffectedOrChanged(
  816. $analyzed_sql_results['is_affected'],
  817. $result
  818. );
  819. $profiling_results = Profiling::getInformation($this->dbi);
  820. $justBrowsing = self::isJustBrowsing(
  821. $analyzed_sql_results,
  822. $find_real_end ?? null
  823. );
  824. $unlim_num_rows = $this->countQueryResults(
  825. $num_rows,
  826. $justBrowsing,
  827. $db,
  828. $table,
  829. $analyzed_sql_results
  830. );
  831. $this->cleanupRelations(
  832. $db ?? '',
  833. $table ?? '',
  834. $_POST['dropped_column'] ?? null,
  835. $_POST['purge'] ?? null
  836. );
  837. if (isset($_POST['dropped_column'])
  838. && isset($db) && strlen($db) > 0
  839. && isset($table) && strlen($table) > 0
  840. ) {
  841. // to refresh the list of indexes (Ajax mode)
  842. $indexes = Index::getFromTable($table, $db);
  843. $indexesDuplicates = Index::findDuplicates($table, $db);
  844. $template = new Template();
  845. $extra_data['indexes_list'] = $template->render('indexes', [
  846. 'url_params' => $GLOBALS['url_params'],
  847. 'indexes' => $indexes,
  848. 'indexes_duplicates' => $indexesDuplicates,
  849. ]);
  850. }
  851. }
  852. return [
  853. $result,
  854. $num_rows,
  855. $unlim_num_rows,
  856. $profiling_results ?? null,
  857. $extra_data,
  858. ];
  859. }
  860. /**
  861. * Delete related transformation information
  862. *
  863. * @param string $db current database
  864. * @param string $table current table
  865. * @param array $analyzed_sql_results analyzed sql results
  866. *
  867. * @return void
  868. */
  869. private function deleteTransformationInfo($db, $table, array $analyzed_sql_results)
  870. {
  871. if (! isset($analyzed_sql_results['statement'])) {
  872. return;
  873. }
  874. $statement = $analyzed_sql_results['statement'];
  875. if ($statement instanceof AlterStatement) {
  876. if (! empty($statement->altered[0])
  877. && $statement->altered[0]->options->has('DROP')
  878. ) {
  879. if (! empty($statement->altered[0]->field->column)) {
  880. $this->transformations->clear(
  881. $db,
  882. $table,
  883. $statement->altered[0]->field->column
  884. );
  885. }
  886. }
  887. } elseif ($statement instanceof DropStatement) {
  888. $this->transformations->clear($db, $table);
  889. }
  890. }
  891. /**
  892. * Function to get the message for the no rows returned case
  893. *
  894. * @param string $message_to_show message to show
  895. * @param array $analyzed_sql_results analyzed sql results
  896. * @param int $num_rows number of rows
  897. *
  898. * @return Message
  899. */
  900. private function getMessageForNoRowsReturned(
  901. $message_to_show,
  902. array $analyzed_sql_results,
  903. $num_rows
  904. ) {
  905. if ($analyzed_sql_results['querytype'] === 'DELETE"') {
  906. $message = Message::getMessageForDeletedRows($num_rows);
  907. } elseif ($analyzed_sql_results['is_insert']) {
  908. if ($analyzed_sql_results['querytype'] === 'REPLACE') {
  909. // For REPLACE we get DELETED + INSERTED row count,
  910. // so we have to call it affected
  911. $message = Message::getMessageForAffectedRows($num_rows);
  912. } else {
  913. $message = Message::getMessageForInsertedRows($num_rows);
  914. }
  915. $insert_id = $this->dbi->insertId();
  916. if ($insert_id != 0) {
  917. // insert_id is id of FIRST record inserted in one insert,
  918. // so if we inserted multiple rows, we had to increment this
  919. $message->addText('[br]');
  920. // need to use a temporary because the Message class
  921. // currently supports adding parameters only to the first
  922. // message
  923. $_inserted = Message::notice(__('Inserted row id: %1$d'));
  924. $_inserted->addParam($insert_id + $num_rows - 1);
  925. $message->addMessage($_inserted);
  926. }
  927. } elseif ($analyzed_sql_results['is_affected']) {
  928. $message = Message::getMessageForAffectedRows($num_rows);
  929. // Ok, here is an explanation for the !$is_select.
  930. // The form generated by PhpMyAdmin\SqlQueryForm
  931. // and /database/sql has many submit buttons
  932. // on the same form, and some confusion arises from the
  933. // fact that $message_to_show is sent for every case.
  934. // The $message_to_show containing a success message and sent with
  935. // the form should not have priority over errors
  936. } elseif (! empty($message_to_show)
  937. && $analyzed_sql_results['querytype'] !== 'SELECT'
  938. ) {
  939. $message = Message::rawSuccess(htmlspecialchars($message_to_show));
  940. } elseif (! empty($GLOBALS['show_as_php'])) {
  941. $message = Message::success(__('Showing as PHP code'));
  942. } elseif (isset($GLOBALS['show_as_php'])) {
  943. /* User disable showing as PHP, query is only displayed */
  944. $message = Message::notice(__('Showing SQL query'));
  945. } else {
  946. $message = Message::success(
  947. __('MySQL returned an empty result set (i.e. zero rows).')
  948. );
  949. }
  950. if (isset($GLOBALS['querytime'])) {
  951. $_querytime = Message::notice(
  952. '(' . __('Query took %01.4f seconds.') . ')'
  953. );
  954. $_querytime->addParam($GLOBALS['querytime']);
  955. $message->addMessage($_querytime);
  956. }
  957. // In case of ROLLBACK, notify the user.
  958. if (isset($_POST['rollback_query'])) {
  959. $message->addText(__('[ROLLBACK occurred.]'));
  960. }
  961. return $message;
  962. }
  963. /**
  964. * Function to respond back when the query returns zero rows
  965. * This method is called
  966. * 1-> When browsing an empty table
  967. * 2-> When executing a query on a non empty table which returns zero results
  968. * 3-> When executing a query on an empty table
  969. * 4-> When executing an INSERT, UPDATE, DELETE query from the SQL tab
  970. * 5-> When deleting a row from BROWSE tab
  971. * 6-> When searching using the SEARCH tab which returns zero results
  972. * 7-> When changing the structure of the table except change operation
  973. *
  974. * @param array $analyzed_sql_results analyzed sql results
  975. * @param string $db current database
  976. * @param string $table current table
  977. * @param string|null $message_to_show message to show
  978. * @param int $num_rows number of rows
  979. * @param DisplayResults $displayResultsObject DisplayResult instance
  980. * @param array|null $extra_data extra data
  981. * @param string $themeImagePath uri of the theme image
  982. * @param array|null $profiling_results profiling results
  983. * @param object $result executed query results
  984. * @param string $sql_query sql query
  985. * @param string|null $complete_query complete sql query
  986. *
  987. * @return string html
  988. */
  989. private function getQueryResponseForNoResultsReturned(
  990. array $analyzed_sql_results,
  991. $db,
  992. $table,
  993. ?string $message_to_show,
  994. $num_rows,
  995. $displayResultsObject,
  996. ?array $extra_data,
  997. $themeImagePath,
  998. ?array $profiling_results,
  999. $result,
  1000. $sql_query,
  1001. ?string $complete_query
  1002. ) {
  1003. if ($this->isDeleteTransformationInfo($analyzed_sql_results)) {
  1004. $this->deleteTransformationInfo($db, $table, $analyzed_sql_results);
  1005. }
  1006. if (isset($extra_data['error'])) {
  1007. $message = Message::rawError($extra_data['error']);
  1008. } else {
  1009. $message = $this->getMessageForNoRowsReturned(
  1010. $message_to_show ?? null,
  1011. $analyzed_sql_results,
  1012. $num_rows
  1013. );
  1014. }
  1015. $queryMessage = Generator::getMessage(
  1016. $message,
  1017. $GLOBALS['sql_query'],
  1018. 'success'
  1019. );
  1020. if (isset($GLOBALS['show_as_php'])) {
  1021. return $queryMessage;
  1022. }
  1023. if (! empty($GLOBALS['reload'])) {
  1024. $extra_data['reload'] = 1;
  1025. $extra_data['db'] = $GLOBALS['db'];
  1026. }
  1027. // For ajax requests add message and sql_query as JSON
  1028. if (empty($_REQUEST['ajax_page_request'])) {
  1029. $extra_data['message'] = $message;
  1030. if ($GLOBALS['cfg']['ShowSQL']) {
  1031. $extra_data['sql_query'] = $queryMessage;
  1032. }
  1033. }
  1034. $response = Response::getInstance();
  1035. $response->addJSON($extra_data ?? []);
  1036. if (empty($analyzed_sql_results['is_select']) || isset($extra_data['error'])) {
  1037. return $queryMessage;
  1038. }
  1039. $displayParts = [
  1040. 'edit_lnk' => null,
  1041. 'del_lnk' => null,
  1042. 'sort_lnk' => '1',
  1043. 'nav_bar' => '0',
  1044. 'bkm_form' => '1',
  1045. 'text_btn' => '1',
  1046. 'pview_lnk' => '1',
  1047. ];
  1048. $sqlQueryResultsTable = $this->getHtmlForSqlQueryResultsTable(
  1049. $displayResultsObject,
  1050. $themeImagePath,
  1051. $displayParts,
  1052. false,
  1053. 0,
  1054. $num_rows,
  1055. true,
  1056. $result,
  1057. $analyzed_sql_results,
  1058. true
  1059. );
  1060. $profilingChart = '';
  1061. if ($profiling_results !== null) {
  1062. $header = $response->getHeader();
  1063. $scripts = $header->getScripts();
  1064. $scripts->addFile('vendor/stickyfill.min.js');
  1065. $scripts->addFile('sql.js');
  1066. $profiling = $this->getDetailedProfilingStats($profiling_results);
  1067. $profilingChart = $this->template->render('sql/profiling_chart', ['profiling' => $profiling]);
  1068. }
  1069. $bookmark = '';
  1070. $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']);
  1071. if (is_array($cfgBookmark)
  1072. && $displayParts['bkm_form'] == '1'
  1073. && (! empty($cfgBookmark) && empty($_GET['id_bookmark']))
  1074. && ! empty($sql_query)
  1075. ) {
  1076. $bookmark = $this->template->render('sql/bookmark', [
  1077. 'db' => $db,
  1078. 'goto' => Url::getFromRoute('/sql', [
  1079. 'db' => $db,
  1080. 'table' => $table,
  1081. 'sql_query' => $sql_query,
  1082. 'id_bookmark' => 1,
  1083. ]),
  1084. 'user' => $cfgBookmark['user'],
  1085. 'sql_query' => $complete_query ?? $sql_query,
  1086. ]);
  1087. }
  1088. return $this->template->render('sql/no_results_returned', [
  1089. 'message' => $queryMessage,
  1090. 'sql_query_results_table' => $sqlQueryResultsTable,
  1091. 'profiling_chart' => $profilingChart,
  1092. 'bookmark' => $bookmark,
  1093. 'db' => $db,
  1094. 'table' => $table,
  1095. 'sql_query' => $sql_query,
  1096. 'is_procedure' => ! empty($analyzed_sql_results['procedure']),
  1097. ]);
  1098. }
  1099. /**
  1100. * Function to send response for ajax grid edit
  1101. *
  1102. * @param object $result result of the executed query
  1103. */
  1104. private function getResponseForGridEdit($result): void
  1105. {
  1106. $row = $this->dbi->fetchRow($result);
  1107. $field_flags = $this->dbi->fieldFlags($result, 0);
  1108. if (stripos($field_flags, DisplayResults::BINARY_FIELD) !== false) {
  1109. $row[0] = bin2hex($row[0]);
  1110. }
  1111. $response = Response::getInstance();
  1112. $response->addJSON('value', $row[0]);
  1113. }
  1114. /**
  1115. * Returns a message for successful creation of a bookmark or null if a bookmark
  1116. * was not created
  1117. */
  1118. private function getBookmarkCreatedMessage(): string
  1119. {
  1120. $output = '';
  1121. if (isset($_GET['label'])) {
  1122. $message = Message::success(
  1123. __('Bookmark %s has been created.')
  1124. );
  1125. $message->addParam($_GET['label']);
  1126. $output = $message->getDisplay();
  1127. }
  1128. return $output;
  1129. }
  1130. /**
  1131. * Function to get html for the sql query results table
  1132. *
  1133. * @param DisplayResults $displayResultsObject instance of DisplayResult
  1134. * @param string $themeImagePath theme image uri
  1135. * @param array $displayParts the parts to display
  1136. * @param bool $editable whether the result table is
  1137. * editable or not
  1138. * @param int $unlim_num_rows unlimited number of rows
  1139. * @param int $num_rows number of rows
  1140. * @param bool $showtable whether to show table or not
  1141. * @param object|bool|null $result result of the executed query
  1142. * @param array $analyzed_sql_results analyzed sql results
  1143. * @param bool $is_limited_display Show only limited operations or not
  1144. *
  1145. * @return string
  1146. */
  1147. private function getHtmlForSqlQueryResultsTable(
  1148. $displayResultsObject,
  1149. $themeImagePath,
  1150. array $displayParts,
  1151. $editable,
  1152. $unlim_num_rows,
  1153. $num_rows,
  1154. $showtable,
  1155. $result,
  1156. array $analyzed_sql_results,
  1157. $is_limited_display = false
  1158. ) {
  1159. $printview = isset($_POST['printview']) && $_POST['printview'] == '1' ? '1' : null;
  1160. $table_html = '';
  1161. $browse_dist = ! empty($_POST['is_browse_distinct']);
  1162. if ($analyzed_sql_results['is_procedure']) {
  1163. do {
  1164. if (! isset($result)) {
  1165. $result = $this->dbi->storeResult();
  1166. }
  1167. $num_rows = $this->dbi->numRows($result);
  1168. if ($result !== false && $num_rows > 0) {
  1169. $fields_meta = $this->dbi->getFieldsMeta($result);
  1170. if (! is_array($fields_meta)) {
  1171. $fields_cnt = 0;
  1172. } else {
  1173. $fields_cnt = count($fields_meta);
  1174. }
  1175. $displayResultsObject->setProperties(
  1176. $num_rows,
  1177. $fields_meta,
  1178. $analyzed_sql_results['is_count'],
  1179. $analyzed_sql_results['is_export'],
  1180. $analyzed_sql_results['is_func'],
  1181. $analyzed_sql_results['is_analyse'],
  1182. $num_rows,
  1183. $fields_cnt,
  1184. $GLOBALS['querytime'],
  1185. $themeImagePath,
  1186. $GLOBALS['text_dir'],
  1187. $analyzed_sql_results['is_maint'],
  1188. $analyzed_sql_results['is_explain'],
  1189. $analyzed_sql_results['is_show'],
  1190. $showtable,
  1191. $printview,
  1192. $editable,
  1193. $browse_dist
  1194. );
  1195. $displayParts = [
  1196. 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE,
  1197. 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE,
  1198. 'sort_lnk' => '1',
  1199. 'nav_bar' => '1',
  1200. 'bkm_form' => '1',
  1201. 'text_btn' => '1',
  1202. 'pview_lnk' => '1',
  1203. ];
  1204. $table_html .= $displayResultsObject->getTable(
  1205. $result,
  1206. $displayParts,
  1207. $analyzed_sql_results,
  1208. $is_limited_display
  1209. );
  1210. }
  1211. $this->dbi->freeResult($result);
  1212. } while ($this->dbi->moreResults() && $this->dbi->nextResult());
  1213. } else {
  1214. $fields_meta = [];
  1215. if (isset($result) && ! is_bool($result)) {
  1216. $fields_meta = $this->dbi->getFieldsMeta($result);
  1217. }
  1218. $fields_cnt = count($fields_meta);
  1219. $_SESSION['is_multi_query'] = false;
  1220. $displayResultsObject->setProperties(
  1221. $unlim_num_rows,
  1222. $fields_meta,
  1223. $analyzed_sql_results['is_count'],
  1224. $analyzed_sql_results['is_export'],
  1225. $analyzed_sql_results['is_func'],
  1226. $analyzed_sql_results['is_analyse'],
  1227. $num_rows,
  1228. $fields_cnt,
  1229. $GLOBALS['querytime'],
  1230. $themeImagePath,
  1231. $GLOBALS['text_dir'],
  1232. $analyzed_sql_results['is_maint'],
  1233. $analyzed_sql_results['is_explain'],
  1234. $analyzed_sql_results['is_show'],
  1235. $showtable,
  1236. $printview,
  1237. $editable,
  1238. $browse_dist
  1239. );
  1240. if (! is_bool($result)) {
  1241. $table_html .= $displayResultsObject->getTable(
  1242. $result,
  1243. $displayParts,
  1244. $analyzed_sql_results,
  1245. $is_limited_display
  1246. );
  1247. }
  1248. $this->dbi->freeResult($result);
  1249. }
  1250. return $table_html;
  1251. }
  1252. /**
  1253. * Function to get html for the previous query if there is such. If not will return
  1254. * null
  1255. *
  1256. * @param string|null $displayQuery display query
  1257. * @param bool $showSql whether to show sql
  1258. * @param array $sqlData sql data
  1259. * @param Message|string $displayMessage display message
  1260. */
  1261. private function getHtmlForPreviousUpdateQuery(
  1262. ?string $displayQuery,
  1263. bool $showSql,
  1264. $sqlData,
  1265. $displayMessage
  1266. ): string {
  1267. $output = '';
  1268. if (isset($displayQuery) && ($showSql === true) && empty($sqlData)) {
  1269. $output = Generator::getMessage(
  1270. $displayMessage,
  1271. $displayQuery,
  1272. 'success'
  1273. );
  1274. }
  1275. return $output;
  1276. }
  1277. /**
  1278. * To get the message if a column index is missing. If not will return null
  1279. *
  1280. * @param string $table current table
  1281. * @param string $database current database
  1282. * @param bool $editable whether the results table can be editable or not
  1283. * @param bool $hasUniqueKey whether there is a unique key
  1284. */
  1285. private function getMessageIfMissingColumnIndex($table, $database, $editable, $hasUniqueKey): string
  1286. {
  1287. $output = '';
  1288. if (! empty($table) && (Utilities::isSystemSchema($database) || ! $editable)) {
  1289. $output = Message::notice(
  1290. sprintf(
  1291. __(
  1292. 'Current selection does not contain a unique column.'
  1293. . ' Grid edit, checkbox, Edit, Copy and Delete features'
  1294. . ' are not available. %s'
  1295. ),
  1296. MySQLDocumentation::showDocumentation(
  1297. 'config',
  1298. 'cfg_RowActionLinksWithoutUnique'
  1299. )
  1300. )
  1301. )->getDisplay();
  1302. } elseif (! empty($table) && ! $hasUniqueKey) {
  1303. $output = Message::notice(
  1304. sprintf(
  1305. __(
  1306. 'Current selection does not contain a unique column.'
  1307. . ' Grid edit, Edit, Copy and Delete features may result in'
  1308. . ' undesired behavior. %s'
  1309. ),
  1310. MySQLDocumentation::showDocumentation(
  1311. 'config',
  1312. 'cfg_RowActionLinksWithoutUnique'
  1313. )
  1314. )
  1315. )->getDisplay();
  1316. }
  1317. return $output;
  1318. }
  1319. /**
  1320. * Function to display results when the executed query returns non empty results
  1321. *
  1322. * @param object|null $result executed query results
  1323. * @param array $analyzed_sql_results analysed sql results
  1324. * @param string $db current database
  1325. * @param string $table current table
  1326. * @param array|null $sql_data sql data
  1327. * @param DisplayResults $displayResultsObject Instance of DisplayResults
  1328. * @param string $themeImagePath uri of the theme image
  1329. * @param int $unlim_num_rows unlimited number of rows
  1330. * @param int $num_rows number of rows
  1331. * @param string|null $disp_query display query
  1332. * @param Message|string|null $disp_message display message
  1333. * @param array|null $profiling_results profiling results
  1334. * @param string $sql_query sql query
  1335. * @param string|null $complete_query complete sql query
  1336. *
  1337. * @return string html
  1338. */
  1339. private function getQueryResponseForResultsReturned(
  1340. $result,
  1341. array $analyzed_sql_results,
  1342. $db,
  1343. $table,
  1344. ?array $sql_data,
  1345. $displayResultsObject,
  1346. $themeImagePath,
  1347. $unlim_num_rows,
  1348. $num_rows,
  1349. ?string $disp_query,
  1350. $disp_message,
  1351. ?array $profiling_results,
  1352. $sql_query,
  1353. ?string $complete_query
  1354. ) {
  1355. global $showtable;
  1356. // If we are retrieving the full value of a truncated field or the original
  1357. // value of a transformed field, show it here
  1358. if (isset($_POST['grid_edit']) && $_POST['grid_edit'] == true) {
  1359. $this->getResponseForGridEdit($result);
  1360. exit;
  1361. }
  1362. // Gets the list of fields properties
  1363. if (isset($result) && $result) {
  1364. $fields_meta = $this->dbi->getFieldsMeta($result);
  1365. } else {
  1366. $fields_meta = [];
  1367. }
  1368. // Should be initialized these parameters before parsing
  1369. $showtable = $showtable ?? null;
  1370. $response = Response::getInstance();
  1371. $header = $response->getHeader();
  1372. $scripts = $header->getScripts();
  1373. $just_one_table = $this->resultSetHasJustOneTable($fields_meta);
  1374. // hide edit and delete links:
  1375. // - for information_schema
  1376. // - if the result set does not contain all the columns of a unique key
  1377. // (unless this is an updatable view)
  1378. // - if the SELECT query contains a join or a subquery
  1379. $updatableView = false;
  1380. $statement = $analyzed_sql_results['statement'] ?? null;
  1381. if ($statement instanceof SelectStatement) {
  1382. if (! empty($statement->expr)) {
  1383. if ($statement->expr[0]->expr === '*') {
  1384. $_table = new Table($table, $db);
  1385. $updatableView = $_table->isUpdatableView();
  1386. }
  1387. }
  1388. if ($analyzed_sql_results['join']
  1389. || $analyzed_sql_results['is_subquery']
  1390. || count($analyzed_sql_results['select_tables']) !== 1
  1391. ) {
  1392. $just_one_table = false;
  1393. }
  1394. }
  1395. $has_unique = $this->resultSetContainsUniqueKey(
  1396. $db,
  1397. $table,
  1398. $fields_meta
  1399. );
  1400. $editable = ($has_unique
  1401. || $GLOBALS['cfg']['RowActionLinksWithoutUnique']
  1402. || $updatableView)
  1403. && $just_one_table;
  1404. $_SESSION['tmpval']['possible_as_geometry'] = $editable;
  1405. $displayParts = [
  1406. 'edit_lnk' => $displayResultsObject::UPDATE_ROW,
  1407. 'del_lnk' => $displayResultsObject::DELETE_ROW,
  1408. 'sort_lnk' => '1',
  1409. 'nav_bar' => '1',
  1410. 'bkm_form' => '1',
  1411. 'text_btn' => '0',
  1412. 'pview_lnk' => '1',
  1413. ];
  1414. if (Utilities::isSystemSchema($db) || ! $editable) {
  1415. $displayParts = [
  1416. 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE,
  1417. 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE,
  1418. 'sort_lnk' => '1',
  1419. 'nav_bar' => '1',
  1420. 'bkm_form' => '1',
  1421. 'text_btn' => '1',
  1422. 'pview_lnk' => '1',
  1423. ];
  1424. }
  1425. if (isset($_POST['printview']) && $_POST['printview'] == '1') {
  1426. $displayParts = [
  1427. 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE,
  1428. 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE,
  1429. 'sort_lnk' => '0',
  1430. 'nav_bar' => '0',
  1431. 'bkm_form' => '0',
  1432. 'text_btn' => '0',
  1433. 'pview_lnk' => '0',
  1434. ];
  1435. }
  1436. if (! isset($_POST['printview']) || $_POST['printview'] != '1') {
  1437. $scripts->addFile('makegrid.js');
  1438. $scripts->addFile('vendor/stickyfill.min.js');
  1439. $scripts->addFile('sql.js');
  1440. unset($GLOBALS['message']);
  1441. //we don't need to buffer the output in getMessage here.
  1442. //set a global variable and check against it in the function
  1443. $GLOBALS['buffer_message'] = false;
  1444. }
  1445. $previousUpdateQueryHtml = $this->getHtmlForPreviousUpdateQuery(
  1446. $disp_query ?? null,
  1447. (bool) $GLOBALS['cfg']['ShowSQL'],
  1448. $sql_data ?? null,
  1449. $disp_message ?? null
  1450. );
  1451. $profilingChartHtml = '';
  1452. if (! empty($profiling_results)) {
  1453. $profiling = $this->getDetailedProfilingStats($profiling_results);
  1454. $profilingChartHtml = $this->template->render('sql/profiling_chart', ['profiling' => $profiling]);
  1455. }
  1456. $missingUniqueColumnMessage = $this->getMessageIfMissingColumnIndex(
  1457. $table,
  1458. $db,
  1459. $editable,
  1460. $has_unique
  1461. );
  1462. $bookmarkCreatedMessage = $this->getBookmarkCreatedMessage();
  1463. $tableHtml = $this->getHtmlForSqlQueryResultsTable(
  1464. $displayResultsObject,
  1465. $themeImagePath,
  1466. $displayParts,
  1467. $editable,
  1468. $unlim_num_rows,
  1469. $num_rows,
  1470. $showtable,
  1471. $result,
  1472. $analyzed_sql_results
  1473. );
  1474. $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']);
  1475. $bookmarkSupportHtml = '';
  1476. if (is_array($cfgBookmark)
  1477. && $displayParts['bkm_form'] == '1'
  1478. && (! empty($cfgBookmark) && empty($_GET['id_bookmark']))
  1479. && ! empty($sql_query)
  1480. ) {
  1481. $bookmarkSupportHtml = $this->template->render('sql/bookmark', [
  1482. 'db' => $db,
  1483. 'goto' => Url::getFromRoute('/sql', [
  1484. 'db' => $db,
  1485. 'table' => $table,
  1486. 'sql_query' => $sql_query,
  1487. 'id_bookmark' => 1,
  1488. ]),
  1489. 'user' => $cfgBookmark['user'],
  1490. 'sql_query' => $complete_query ?? $sql_query,
  1491. ]);
  1492. }
  1493. return $this->template->render('sql/sql_query_results', [
  1494. 'previous_update_query' => $previousUpdateQueryHtml,
  1495. 'profiling_chart' => $profilingChartHtml,
  1496. 'missing_unique_column_message' => $missingUniqueColumnMessage,
  1497. 'bookmark_created_message' => $bookmarkCreatedMessage,
  1498. 'table' => $tableHtml,
  1499. 'bookmark_support' => $bookmarkSupportHtml,
  1500. ]);
  1501. }
  1502. /**
  1503. * Function to execute the query and send the response
  1504. *
  1505. * @param array|null $analyzed_sql_results analysed sql results
  1506. * @param bool $is_gotofile whether goto file or not
  1507. * @param string $db current database
  1508. * @param string|null $table current table
  1509. * @param bool|null $find_real_end whether to find real end or not
  1510. * @param string|null $sql_query_for_bookmark the sql query to be stored as bookmark
  1511. * @param array|null $extra_data extra data
  1512. * @param string|null $message_to_show message to show
  1513. * @param array|null $sql_data sql data
  1514. * @param string $goto goto page url
  1515. * @param string $themeImagePath uri of the PMA theme image
  1516. * @param string|null $disp_query display query
  1517. * @param Message|string|null $disp_message display message
  1518. * @param string $sql_query sql query
  1519. * @param string|null $complete_query complete query
  1520. */
  1521. public function executeQueryAndSendQueryResponse(
  1522. $analyzed_sql_results,
  1523. $is_gotofile,
  1524. $db,
  1525. $table,
  1526. $find_real_end,
  1527. $sql_query_for_bookmark,
  1528. $extra_data,
  1529. $message_to_show,
  1530. $sql_data,
  1531. $goto,
  1532. $themeImagePath,
  1533. $disp_query,
  1534. $disp_message,
  1535. $sql_query,
  1536. $complete_query
  1537. ): string {
  1538. if ($analyzed_sql_results == null) {
  1539. // Parse and analyze the query
  1540. [
  1541. $analyzed_sql_results,
  1542. $db,
  1543. $table_from_sql,
  1544. ] = ParseAnalyze::sqlQuery($sql_query, $db);
  1545. if ($table != $table_from_sql && ! empty($table_from_sql)) {
  1546. $table = $table_from_sql;
  1547. }
  1548. }
  1549. return $this->executeQueryAndGetQueryResponse(
  1550. $analyzed_sql_results, // analyzed_sql_results
  1551. $is_gotofile, // is_gotofile
  1552. $db, // db
  1553. $table, // table
  1554. $find_real_end, // find_real_end
  1555. $sql_query_for_bookmark, // sql_query_for_bookmark
  1556. $extra_data, // extra_data
  1557. $message_to_show, // message_to_show
  1558. $sql_data, // sql_data
  1559. $goto, // goto
  1560. $themeImagePath,
  1561. $disp_query, // disp_query
  1562. $disp_message, // disp_message
  1563. $sql_query, // sql_query
  1564. $complete_query // complete_query
  1565. );
  1566. }
  1567. /**
  1568. * Function to execute the query and send the response
  1569. *
  1570. * @param array $analyzed_sql_results analysed sql results
  1571. * @param bool $is_gotofile whether goto file or not
  1572. * @param string|null $db current database
  1573. * @param string|null $table current table
  1574. * @param bool|null $find_real_end whether to find real end or not
  1575. * @param string|null $sql_query_for_bookmark the sql query to be stored as bookmark
  1576. * @param array|null $extra_data extra data
  1577. * @param string|null $message_to_show message to show
  1578. * @param array|null $sql_data sql data
  1579. * @param string $goto goto page url
  1580. * @param string $themeImagePath uri of the PMA theme image
  1581. * @param string|null $disp_query display query
  1582. * @param Message|string|null $disp_message display message
  1583. * @param string $sql_query sql query
  1584. * @param string|null $complete_query complete query
  1585. *
  1586. * @return string html
  1587. */
  1588. public function executeQueryAndGetQueryResponse(
  1589. array $analyzed_sql_results,
  1590. $is_gotofile,
  1591. $db,
  1592. $table,
  1593. $find_real_end,
  1594. ?string $sql_query_for_bookmark,
  1595. $extra_data,
  1596. ?string $message_to_show,
  1597. $sql_data,
  1598. $goto,
  1599. $themeImagePath,
  1600. ?string $disp_query,
  1601. $disp_message,
  1602. $sql_query,
  1603. ?string $complete_query
  1604. ) {
  1605. // Handle disable/enable foreign key checks
  1606. $default_fk_check = Util::handleDisableFKCheckInit();
  1607. // Handle remembered sorting order, only for single table query.
  1608. // Handling is not required when it's a union query
  1609. // (the parser never sets the 'union' key to 0).
  1610. // Handling is also not required if we came from the "Sort by key"
  1611. // drop-down.
  1612. if (! empty($analyzed_sql_results)
  1613. && $this->isRememberSortingOrder($analyzed_sql_results)
  1614. && empty($analyzed_sql_results['union'])
  1615. && ! isset($_POST['sort_by_key'])
  1616. ) {
  1617. if (! isset($_SESSION['sql_from_query_box'])) {
  1618. $this->handleSortOrder($db, $table, $analyzed_sql_results, $sql_query);
  1619. } else {
  1620. unset($_SESSION['sql_from_query_box']);
  1621. }
  1622. }
  1623. $displayResultsObject = new DisplayResults(
  1624. $GLOBALS['db'],
  1625. $GLOBALS['table'],
  1626. $GLOBALS['server'],
  1627. $goto,
  1628. $sql_query
  1629. );
  1630. $displayResultsObject->setConfigParamsForDisplayTable();
  1631. // assign default full_sql_query
  1632. $full_sql_query = $sql_query;
  1633. // Do append a "LIMIT" clause?
  1634. if ($this->isAppendLimitClause($analyzed_sql_results)) {
  1635. $full_sql_query = $this->getSqlWithLimitClause($analyzed_sql_results);
  1636. }
  1637. $GLOBALS['reload'] = $this->hasCurrentDbChanged($db);
  1638. $this->dbi->selectDb($db);
  1639. [
  1640. $result,
  1641. $num_rows,
  1642. $unlim_num_rows,
  1643. $profiling_results,
  1644. $extra_data,
  1645. ] = $this->executeTheQuery(
  1646. $analyzed_sql_results,
  1647. $full_sql_query,
  1648. $is_gotofile,
  1649. $db,
  1650. $table,
  1651. $find_real_end ?? null,
  1652. $sql_query_for_bookmark ?? null,
  1653. $extra_data ?? null
  1654. );
  1655. if ($this->dbi->moreResults()) {
  1656. $this->dbi->nextResult();
  1657. }
  1658. $warning_messages = $this->operations->getWarningMessagesArray();
  1659. // No rows returned -> move back to the calling page
  1660. if (($num_rows == 0 && $unlim_num_rows == 0)
  1661. || $analyzed_sql_results['is_affected']
  1662. ) {
  1663. $html_output = $this->getQueryResponseForNoResultsReturned(
  1664. $analyzed_sql_results,
  1665. $db,
  1666. $table,
  1667. $message_to_show ?? null,
  1668. $num_rows,
  1669. $displayResultsObject,
  1670. $extra_data,
  1671. $themeImagePath,
  1672. $profiling_results,
  1673. $result ?? null,
  1674. $sql_query,
  1675. $complete_query ?? null
  1676. );
  1677. } else {
  1678. // At least one row is returned -> displays a table with results
  1679. $html_output = $this->getQueryResponseForResultsReturned(
  1680. $result ?? null,
  1681. $analyzed_sql_results,
  1682. $db,
  1683. $table,
  1684. $sql_data ?? null,
  1685. $displayResultsObject,
  1686. $themeImagePath,
  1687. $unlim_num_rows,
  1688. $num_rows,
  1689. $disp_query ?? null,
  1690. $disp_message ?? null,
  1691. $profiling_results,
  1692. $sql_query,
  1693. $complete_query ?? null
  1694. );
  1695. }
  1696. // Handle disable/enable foreign key checks
  1697. Util::handleDisableFKCheckCleanup($default_fk_check);
  1698. foreach ($warning_messages as $warning) {
  1699. $message = Message::notice(Message::sanitize($warning));
  1700. $html_output .= $message->getDisplay();
  1701. }
  1702. return $html_output;
  1703. }
  1704. /**
  1705. * Function to define pos to display a row
  1706. *
  1707. * @param int $number_of_line Number of the line to display
  1708. * @param int $max_rows Number of rows by page
  1709. *
  1710. * @return int Start position to display the line
  1711. */
  1712. private function getStartPosToDisplayRow($number_of_line, $max_rows = null)
  1713. {
  1714. if ($max_rows === null) {
  1715. $max_rows = $_SESSION['tmpval']['max_rows'];
  1716. }
  1717. return @((int) ceil($number_of_line / $max_rows) - 1) * $max_rows;
  1718. }
  1719. /**
  1720. * Function to calculate new pos if pos is higher than number of rows
  1721. * of displayed table
  1722. *
  1723. * @param string $db Database name
  1724. * @param string $table Table name
  1725. * @param int|null $pos Initial position
  1726. *
  1727. * @return int Number of pos to display last page
  1728. */
  1729. public function calculatePosForLastPage($db, $table, $pos)
  1730. {
  1731. if ($pos === null) {
  1732. $pos = $_SESSION['tmpval']['pos'];
  1733. }
  1734. $_table = new Table($table, $db);
  1735. $unlim_num_rows = $_table->countRecords(true);
  1736. //If position is higher than number of rows
  1737. if ($unlim_num_rows <= $pos && $pos != 0) {
  1738. $pos = $this->getStartPosToDisplayRow($unlim_num_rows);
  1739. }
  1740. return $pos;
  1741. }
  1742. }