Qbe.php 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859
  1. <?php
  2. /**
  3. * Handles DB QBE search
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Database;
  7. use PhpMyAdmin\Core;
  8. use PhpMyAdmin\DatabaseInterface;
  9. use PhpMyAdmin\Html\Generator;
  10. use PhpMyAdmin\Message;
  11. use PhpMyAdmin\Relation;
  12. use PhpMyAdmin\SavedSearches;
  13. use PhpMyAdmin\Table;
  14. use PhpMyAdmin\Template;
  15. use PhpMyAdmin\Util;
  16. use function array_diff;
  17. use function array_fill;
  18. use function array_keys;
  19. use function array_map;
  20. use function array_multisort;
  21. use function count;
  22. use function explode;
  23. use function htmlspecialchars;
  24. use function implode;
  25. use function in_array;
  26. use function key;
  27. use function max;
  28. use function mb_strlen;
  29. use function mb_strtoupper;
  30. use function mb_substr;
  31. use function min;
  32. use function reset;
  33. use function str_replace;
  34. use function stripos;
  35. use function strlen;
  36. /**
  37. * Class to handle database QBE search
  38. */
  39. class Qbe
  40. {
  41. /**
  42. * Database name
  43. *
  44. * @access private
  45. * @var string
  46. */
  47. private $db;
  48. /**
  49. * Table Names (selected/non-selected)
  50. *
  51. * @access private
  52. * @var array
  53. */
  54. private $criteriaTables;
  55. /**
  56. * Column Names
  57. *
  58. * @access private
  59. * @var array
  60. */
  61. private $columnNames;
  62. /**
  63. * Number of columns
  64. *
  65. * @access private
  66. * @var int
  67. */
  68. private $criteriaColumnCount;
  69. /**
  70. * Number of Rows
  71. *
  72. * @access private
  73. * @var int
  74. */
  75. private $criteriaRowCount;
  76. /**
  77. * Whether to insert a new column
  78. *
  79. * @access private
  80. * @var array
  81. */
  82. private $criteriaColumnInsert;
  83. /**
  84. * Whether to delete a column
  85. *
  86. * @access private
  87. * @var array
  88. */
  89. private $criteriaColumnDelete;
  90. /**
  91. * Whether to insert a new row
  92. *
  93. * @access private
  94. * @var array
  95. */
  96. private $criteriaRowInsert;
  97. /**
  98. * Whether to delete a row
  99. *
  100. * @access private
  101. * @var array
  102. */
  103. private $criteriaRowDelete;
  104. /**
  105. * Already set criteria values
  106. *
  107. * @access private
  108. * @var array
  109. */
  110. private $criteria;
  111. /**
  112. * Previously set criteria values
  113. *
  114. * @access private
  115. * @var array
  116. */
  117. private $prevCriteria;
  118. /**
  119. * AND/OR relation b/w criteria columns
  120. *
  121. * @access private
  122. * @var array
  123. */
  124. private $criteriaAndOrColumn;
  125. /**
  126. * AND/OR relation b/w criteria rows
  127. *
  128. * @access private
  129. * @var array
  130. */
  131. private $criteriaAndOrRow;
  132. /**
  133. * Large width of a column
  134. *
  135. * @access private
  136. * @var string
  137. */
  138. private $realwidth;
  139. /**
  140. * Minimum width of a column
  141. *
  142. * @access private
  143. * @var int
  144. */
  145. private $formColumnWidth;
  146. /**
  147. * Selected columns in the form
  148. *
  149. * @access private
  150. * @var array
  151. */
  152. private $formColumns;
  153. /**
  154. * Entered aliases in the form
  155. *
  156. * @access private
  157. * @var array
  158. */
  159. private $formAliases;
  160. /**
  161. * Chosen sort options in the form
  162. *
  163. * @access private
  164. * @var array
  165. */
  166. private $formSorts;
  167. /**
  168. * Chosen sort orders in the form
  169. *
  170. * @access private
  171. * @var array
  172. */
  173. private $formSortOrders;
  174. /**
  175. * Show checkboxes in the form
  176. *
  177. * @access private
  178. * @var array
  179. */
  180. private $formShows;
  181. /**
  182. * Entered criteria values in the form
  183. *
  184. * @access private
  185. * @var array
  186. */
  187. private $formCriterions;
  188. /**
  189. * AND/OR column radio buttons in the form
  190. *
  191. * @access private
  192. * @var array
  193. */
  194. private $formAndOrCols;
  195. /**
  196. * AND/OR row radio buttons in the form
  197. *
  198. * @access private
  199. * @var array
  200. */
  201. private $formAndOrRows;
  202. /**
  203. * New column count in case of add/delete
  204. *
  205. * @access private
  206. * @var int
  207. */
  208. private $newColumnCount;
  209. /**
  210. * New row count in case of add/delete
  211. *
  212. * @access private
  213. * @var int
  214. */
  215. private $newRowCount;
  216. /**
  217. * List of saved searches
  218. *
  219. * @access private
  220. * @var array
  221. */
  222. private $savedSearchList = null;
  223. /**
  224. * Current search
  225. *
  226. * @access private
  227. * @var SavedSearches
  228. */
  229. private $currentSearch = null;
  230. /** @var Relation */
  231. private $relation;
  232. /** @var DatabaseInterface */
  233. public $dbi;
  234. /** @var Template */
  235. public $template;
  236. /**
  237. * @param Relation $relation Relation object
  238. * @param Template $template Template object
  239. * @param DatabaseInterface $dbi DatabaseInterface object
  240. * @param string $dbname Database name
  241. * @param array $savedSearchList List of saved searches
  242. * @param SavedSearches $currentSearch Current search id
  243. */
  244. public function __construct(
  245. Relation $relation,
  246. Template $template,
  247. $dbi,
  248. $dbname,
  249. array $savedSearchList = [],
  250. $currentSearch = null
  251. ) {
  252. $this->db = $dbname;
  253. $this->savedSearchList = $savedSearchList;
  254. $this->currentSearch = $currentSearch;
  255. $this->dbi = $dbi;
  256. $this->relation = $relation;
  257. $this->template = $template;
  258. $this->loadCriterias();
  259. // Sets criteria parameters
  260. $this->setSearchParams();
  261. $this->setCriteriaTablesAndColumns();
  262. }
  263. /**
  264. * Initialize criterias
  265. *
  266. * @return static
  267. */
  268. private function loadCriterias()
  269. {
  270. if ($this->currentSearch === null
  271. || $this->currentSearch->getCriterias() === null
  272. ) {
  273. return $this;
  274. }
  275. $criterias = $this->currentSearch->getCriterias();
  276. $_POST = $criterias + $_POST;
  277. return $this;
  278. }
  279. /**
  280. * Getter for current search
  281. *
  282. * @return SavedSearches
  283. */
  284. private function getCurrentSearch()
  285. {
  286. return $this->currentSearch;
  287. }
  288. /**
  289. * Sets search parameters
  290. *
  291. * @return void
  292. */
  293. private function setSearchParams()
  294. {
  295. $criteriaColumnCount = $this->initializeCriteriasCount();
  296. $this->criteriaColumnInsert = Core::ifSetOr(
  297. $_POST['criteriaColumnInsert'],
  298. null,
  299. 'array'
  300. );
  301. $this->criteriaColumnDelete = Core::ifSetOr(
  302. $_POST['criteriaColumnDelete'],
  303. null,
  304. 'array'
  305. );
  306. $this->prevCriteria = $_POST['prev_criteria'] ?? [];
  307. $this->criteria = $_POST['criteria'] ?? array_fill(0, $criteriaColumnCount, '');
  308. $this->criteriaRowInsert = $_POST['criteriaRowInsert'] ?? array_fill(0, $criteriaColumnCount, '');
  309. $this->criteriaRowDelete = $_POST['criteriaRowDelete'] ?? array_fill(0, $criteriaColumnCount, '');
  310. $this->criteriaAndOrRow = $_POST['criteriaAndOrRow'] ?? array_fill(0, $criteriaColumnCount, '');
  311. $this->criteriaAndOrColumn = $_POST['criteriaAndOrColumn'] ?? array_fill(0, $criteriaColumnCount, '');
  312. // sets minimum width
  313. $this->formColumnWidth = 12;
  314. $this->formColumns = [];
  315. $this->formSorts = [];
  316. $this->formShows = [];
  317. $this->formCriterions = [];
  318. $this->formAndOrRows = [];
  319. $this->formAndOrCols = [];
  320. }
  321. /**
  322. * Sets criteria tables and columns
  323. *
  324. * @return void
  325. */
  326. private function setCriteriaTablesAndColumns()
  327. {
  328. // The tables list sent by a previously submitted form
  329. if (Core::isValid($_POST['TableList'], 'array')) {
  330. foreach ($_POST['TableList'] as $each_table) {
  331. $this->criteriaTables[$each_table] = ' selected="selected"';
  332. }
  333. }
  334. $all_tables = $this->dbi->query(
  335. 'SHOW TABLES FROM ' . Util::backquote($this->db) . ';',
  336. DatabaseInterface::CONNECT_USER,
  337. DatabaseInterface::QUERY_STORE
  338. );
  339. $all_tables_count = $this->dbi->numRows($all_tables);
  340. if ($all_tables_count == 0) {
  341. echo Message::error(__('No tables found in database.'))->getDisplay();
  342. exit;
  343. }
  344. // The tables list gets from MySQL
  345. while ([$table] = $this->dbi->fetchRow($all_tables)) {
  346. $columns = $this->dbi->getColumns($this->db, $table);
  347. if (empty($this->criteriaTables[$table])
  348. && ! empty($_POST['TableList'])
  349. ) {
  350. $this->criteriaTables[$table] = '';
  351. } else {
  352. $this->criteriaTables[$table] = ' selected="selected"';
  353. }
  354. // The fields list per selected tables
  355. if ($this->criteriaTables[$table] !== ' selected="selected"') {
  356. continue;
  357. }
  358. $each_table = Util::backquote($table);
  359. $this->columnNames[] = $each_table . '.*';
  360. foreach ($columns as $each_column) {
  361. $each_column = $each_table . '.'
  362. . Util::backquote($each_column['Field']);
  363. $this->columnNames[] = $each_column;
  364. // increase the width if necessary
  365. $this->formColumnWidth = max(
  366. mb_strlen($each_column),
  367. $this->formColumnWidth
  368. );
  369. }
  370. }
  371. $this->dbi->freeResult($all_tables);
  372. // sets the largest width found
  373. $this->realwidth = $this->formColumnWidth . 'ex';
  374. }
  375. /**
  376. * Provides select options list containing column names
  377. *
  378. * @param int $column_number Column Number (0,1,2) or more
  379. * @param string $selected Selected criteria column name
  380. *
  381. * @return string HTML for select options
  382. */
  383. private function showColumnSelectCell($column_number, $selected = '')
  384. {
  385. return $this->template->render('database/qbe/column_select_cell', [
  386. 'column_number' => $column_number,
  387. 'column_names' => $this->columnNames,
  388. 'selected' => $selected,
  389. ]);
  390. }
  391. /**
  392. * Provides select options list containing sort options (ASC/DESC)
  393. *
  394. * @param int $columnNumber Column Number (0,1,2) or more
  395. * @param string $selected Selected criteria 'ASC' or 'DESC'
  396. *
  397. * @return string HTML for select options
  398. */
  399. private function getSortSelectCell(
  400. $columnNumber,
  401. $selected = ''
  402. ) {
  403. return $this->template->render('database/qbe/sort_select_cell', [
  404. 'real_width' => $this->realwidth,
  405. 'column_number' => $columnNumber,
  406. 'selected' => $selected,
  407. ]);
  408. }
  409. /**
  410. * Provides select options list containing sort order
  411. *
  412. * @param int $columnNumber Column Number (0,1,2) or more
  413. * @param int $sortOrder Sort order
  414. *
  415. * @return string HTML for select options
  416. */
  417. private function getSortOrderSelectCell($columnNumber, $sortOrder)
  418. {
  419. $totalColumnCount = $this->getNewColumnCount();
  420. return $this->template->render('database/qbe/sort_order_select_cell', [
  421. 'total_column_count' => $totalColumnCount,
  422. 'column_number' => $columnNumber,
  423. 'sort_order' => $sortOrder,
  424. ]);
  425. }
  426. /**
  427. * Returns the new column count after adding and removing columns as instructed
  428. *
  429. * @return int new column count
  430. */
  431. private function getNewColumnCount()
  432. {
  433. $totalColumnCount = $this->criteriaColumnCount;
  434. if (! empty($this->criteriaColumnInsert)) {
  435. $totalColumnCount += count($this->criteriaColumnInsert);
  436. }
  437. if (! empty($this->criteriaColumnDelete)) {
  438. $totalColumnCount -= count($this->criteriaColumnDelete);
  439. }
  440. return $totalColumnCount;
  441. }
  442. /**
  443. * Provides search form's row containing column select options
  444. *
  445. * @return string HTML for search table's row
  446. */
  447. private function getColumnNamesRow()
  448. {
  449. $html_output = '';
  450. $new_column_count = 0;
  451. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  452. if (isset($this->criteriaColumnInsert[$column_index])
  453. && $this->criteriaColumnInsert[$column_index] === 'on'
  454. ) {
  455. $html_output .= $this->showColumnSelectCell(
  456. $new_column_count
  457. );
  458. $new_column_count++;
  459. }
  460. if (! empty($this->criteriaColumnDelete)
  461. && isset($this->criteriaColumnDelete[$column_index])
  462. && $this->criteriaColumnDelete[$column_index] === 'on'
  463. ) {
  464. continue;
  465. }
  466. $selected = '';
  467. if (isset($_POST['criteriaColumn'][$column_index])) {
  468. $selected = $_POST['criteriaColumn'][$column_index];
  469. $this->formColumns[$new_column_count]
  470. = $_POST['criteriaColumn'][$column_index];
  471. }
  472. $html_output .= $this->showColumnSelectCell(
  473. $new_column_count,
  474. $selected
  475. );
  476. $new_column_count++;
  477. }
  478. $this->newColumnCount = $new_column_count;
  479. return $html_output;
  480. }
  481. /**
  482. * Provides search form's row containing column aliases
  483. *
  484. * @return string HTML for search table's row
  485. */
  486. private function getColumnAliasRow()
  487. {
  488. $html_output = '';
  489. $new_column_count = 0;
  490. for ($colInd = 0; $colInd < $this->criteriaColumnCount; $colInd++) {
  491. if (! empty($this->criteriaColumnInsert)
  492. && isset($this->criteriaColumnInsert[$colInd])
  493. && $this->criteriaColumnInsert[$colInd] === 'on'
  494. ) {
  495. $html_output .= '<td class="text-center">';
  496. $html_output .= '<input type="text"'
  497. . ' name="criteriaAlias[' . $new_column_count . ']">';
  498. $html_output .= '</td>';
  499. $new_column_count++;
  500. }
  501. if (! empty($this->criteriaColumnDelete)
  502. && isset($this->criteriaColumnDelete[$colInd])
  503. && $this->criteriaColumnDelete[$colInd] === 'on'
  504. ) {
  505. continue;
  506. }
  507. $tmp_alias = '';
  508. if (! empty($_POST['criteriaAlias'][$colInd])) {
  509. $tmp_alias
  510. = $this->formAliases[$new_column_count]
  511. = $_POST['criteriaAlias'][$colInd];
  512. }
  513. $html_output .= '<td class="text-center">';
  514. $html_output .= '<input type="text"'
  515. . ' name="criteriaAlias[' . $new_column_count . ']"'
  516. . ' value="' . htmlspecialchars($tmp_alias) . '">';
  517. $html_output .= '</td>';
  518. $new_column_count++;
  519. }
  520. return $html_output;
  521. }
  522. /**
  523. * Provides search form's row containing sort(ASC/DESC) select options
  524. *
  525. * @return string HTML for search table's row
  526. */
  527. private function getSortRow()
  528. {
  529. $html_output = '';
  530. $new_column_count = 0;
  531. for ($colInd = 0; $colInd < $this->criteriaColumnCount; $colInd++) {
  532. if (! empty($this->criteriaColumnInsert)
  533. && isset($this->criteriaColumnInsert[$colInd])
  534. && $this->criteriaColumnInsert[$colInd] === 'on'
  535. ) {
  536. $html_output .= $this->getSortSelectCell($new_column_count);
  537. $new_column_count++;
  538. }
  539. if (! empty($this->criteriaColumnDelete)
  540. && isset($this->criteriaColumnDelete[$colInd])
  541. && $this->criteriaColumnDelete[$colInd] === 'on'
  542. ) {
  543. continue;
  544. }
  545. // If they have chosen all fields using the * selector,
  546. // then sorting is not available, Fix for Bug #570698
  547. if (isset($_POST['criteriaSort'][$colInd], $_POST['criteriaColumn'][$colInd])
  548. && mb_substr($_POST['criteriaColumn'][$colInd], -2) === '.*'
  549. ) {
  550. $_POST['criteriaSort'][$colInd] = '';
  551. }
  552. $selected = '';
  553. if (isset($_POST['criteriaSort'][$colInd])) {
  554. $this->formSorts[$new_column_count]
  555. = $_POST['criteriaSort'][$colInd];
  556. if ($_POST['criteriaSort'][$colInd] === 'ASC') {
  557. $selected = 'ASC';
  558. } elseif ($_POST['criteriaSort'][$colInd] === 'DESC') {
  559. $selected = 'DESC';
  560. }
  561. } else {
  562. $this->formSorts[$new_column_count] = '';
  563. }
  564. $html_output .= $this->getSortSelectCell(
  565. $new_column_count,
  566. $selected
  567. );
  568. $new_column_count++;
  569. }
  570. return $html_output;
  571. }
  572. /**
  573. * Provides search form's row containing sort order
  574. *
  575. * @return string HTML for search table's row
  576. */
  577. private function getSortOrder()
  578. {
  579. $html_output = '';
  580. $new_column_count = 0;
  581. for ($colInd = 0; $colInd < $this->criteriaColumnCount; $colInd++) {
  582. if (! empty($this->criteriaColumnInsert)
  583. && isset($this->criteriaColumnInsert[$colInd])
  584. && $this->criteriaColumnInsert[$colInd] === 'on'
  585. ) {
  586. $html_output .= $this->getSortOrderSelectCell(
  587. $new_column_count,
  588. null
  589. );
  590. $new_column_count++;
  591. }
  592. if (! empty($this->criteriaColumnDelete)
  593. && isset($this->criteriaColumnDelete[$colInd])
  594. && $this->criteriaColumnDelete[$colInd] === 'on'
  595. ) {
  596. continue;
  597. }
  598. $sortOrder = null;
  599. if (! empty($_POST['criteriaSortOrder'][$colInd])) {
  600. $sortOrder
  601. = $this->formSortOrders[$new_column_count]
  602. = $_POST['criteriaSortOrder'][$colInd];
  603. }
  604. $html_output .= $this->getSortOrderSelectCell(
  605. $new_column_count,
  606. $sortOrder
  607. );
  608. $new_column_count++;
  609. }
  610. return $html_output;
  611. }
  612. /**
  613. * Provides search form's row containing SHOW checkboxes
  614. *
  615. * @return string HTML for search table's row
  616. */
  617. private function getShowRow()
  618. {
  619. $html_output = '';
  620. $new_column_count = 0;
  621. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  622. if (! empty($this->criteriaColumnInsert)
  623. && isset($this->criteriaColumnInsert[$column_index])
  624. && $this->criteriaColumnInsert[$column_index] === 'on'
  625. ) {
  626. $html_output .= '<td class="text-center">';
  627. $html_output .= '<input type="checkbox"'
  628. . ' name="criteriaShow[' . $new_column_count . ']">';
  629. $html_output .= '</td>';
  630. $new_column_count++;
  631. }
  632. if (! empty($this->criteriaColumnDelete)
  633. && isset($this->criteriaColumnDelete[$column_index])
  634. && $this->criteriaColumnDelete[$column_index] === 'on'
  635. ) {
  636. continue;
  637. }
  638. if (isset($_POST['criteriaShow'][$column_index])) {
  639. $checked_options = ' checked="checked"';
  640. $this->formShows[$new_column_count]
  641. = $_POST['criteriaShow'][$column_index];
  642. } else {
  643. $checked_options = '';
  644. }
  645. $html_output .= '<td class="text-center">';
  646. $html_output .= '<input type="checkbox"'
  647. . ' name="criteriaShow[' . $new_column_count . ']"'
  648. . $checked_options . '>';
  649. $html_output .= '</td>';
  650. $new_column_count++;
  651. }
  652. return $html_output;
  653. }
  654. /**
  655. * Provides search form's row containing criteria Inputboxes
  656. *
  657. * @return string HTML for search table's row
  658. */
  659. private function getCriteriaInputboxRow()
  660. {
  661. $html_output = '';
  662. $new_column_count = 0;
  663. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  664. if (! empty($this->criteriaColumnInsert)
  665. && isset($this->criteriaColumnInsert[$column_index])
  666. && $this->criteriaColumnInsert[$column_index] === 'on'
  667. ) {
  668. $html_output .= '<td class="text-center">';
  669. $html_output .= '<input type="text"'
  670. . ' name="criteria[' . $new_column_count . ']"'
  671. . ' class="textfield"'
  672. . ' style="width: ' . $this->realwidth . '"'
  673. . ' size="20">';
  674. $html_output .= '</td>';
  675. $new_column_count++;
  676. }
  677. if (! empty($this->criteriaColumnDelete)
  678. && isset($this->criteriaColumnDelete[$column_index])
  679. && $this->criteriaColumnDelete[$column_index] === 'on'
  680. ) {
  681. continue;
  682. }
  683. $tmp_criteria = '';
  684. if (isset($this->criteria[$column_index])) {
  685. $tmp_criteria = $this->criteria[$column_index];
  686. }
  687. if ((empty($this->prevCriteria)
  688. || ! isset($this->prevCriteria[$column_index]))
  689. || $this->prevCriteria[$column_index] != htmlspecialchars($tmp_criteria)
  690. ) {
  691. $this->formCriterions[$new_column_count] = $tmp_criteria;
  692. } else {
  693. $this->formCriterions[$new_column_count]
  694. = $this->prevCriteria[$column_index];
  695. }
  696. $html_output .= '<td class="text-center">';
  697. $html_output .= '<input type="hidden"'
  698. . ' name="prev_criteria[' . $new_column_count . ']"'
  699. . ' value="'
  700. . htmlspecialchars($this->formCriterions[$new_column_count])
  701. . '">';
  702. $html_output .= '<input type="text"'
  703. . ' name="criteria[' . $new_column_count . ']"'
  704. . ' value="' . htmlspecialchars($tmp_criteria) . '"'
  705. . ' class="textfield"'
  706. . ' style="width: ' . $this->realwidth . '"'
  707. . ' size="20">';
  708. $html_output .= '</td>';
  709. $new_column_count++;
  710. }
  711. return $html_output;
  712. }
  713. /**
  714. * Provides And/Or modification cell along with Insert/Delete options
  715. * (For modifying search form's table columns)
  716. *
  717. * @param int $column_number Column Number (0,1,2) or more
  718. * @param array|null $selected Selected criteria column name
  719. * @param bool $last_column Whether this is the last column
  720. *
  721. * @return string HTML for modification cell
  722. */
  723. private function getAndOrColCell(
  724. $column_number,
  725. $selected = null,
  726. $last_column = false
  727. ) {
  728. $html_output = '<td class="text-center">';
  729. if (! $last_column) {
  730. $html_output .= '<strong>' . __('Or:') . '</strong>';
  731. $html_output .= '<input type="radio"'
  732. . ' name="criteriaAndOrColumn[' . $column_number . ']"'
  733. . ' value="or"' . ($selected['or'] ?? '') . '>';
  734. $html_output .= '&nbsp;&nbsp;<strong>' . __('And:') . '</strong>';
  735. $html_output .= '<input type="radio"'
  736. . ' name="criteriaAndOrColumn[' . $column_number . ']"'
  737. . ' value="and"' . ($selected['and'] ?? '') . '>';
  738. }
  739. $html_output .= '<br>' . __('Ins');
  740. $html_output .= '<input type="checkbox"'
  741. . ' name="criteriaColumnInsert[' . $column_number . ']">';
  742. $html_output .= '&nbsp;&nbsp;' . __('Del');
  743. $html_output .= '<input type="checkbox"'
  744. . ' name="criteriaColumnDelete[' . $column_number . ']">';
  745. $html_output .= '</td>';
  746. return $html_output;
  747. }
  748. /**
  749. * Provides search form's row containing column modifications options
  750. * (For modifying search form's table columns)
  751. *
  752. * @return string HTML for search table's row
  753. */
  754. private function getModifyColumnsRow()
  755. {
  756. $html_output = '';
  757. $new_column_count = 0;
  758. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  759. if (! empty($this->criteriaColumnInsert)
  760. && isset($this->criteriaColumnInsert[$column_index])
  761. && $this->criteriaColumnInsert[$column_index] === 'on'
  762. ) {
  763. $html_output .= $this->getAndOrColCell($new_column_count);
  764. $new_column_count++;
  765. }
  766. if (! empty($this->criteriaColumnDelete)
  767. && isset($this->criteriaColumnDelete[$column_index])
  768. && $this->criteriaColumnDelete[$column_index] === 'on'
  769. ) {
  770. continue;
  771. }
  772. if (isset($this->criteriaAndOrColumn[$column_index])) {
  773. $this->formAndOrCols[$new_column_count]
  774. = $this->criteriaAndOrColumn[$column_index];
  775. }
  776. $checked_options = [];
  777. if (isset($this->criteriaAndOrColumn[$column_index])
  778. && $this->criteriaAndOrColumn[$column_index] === 'or'
  779. ) {
  780. $checked_options['or'] = ' checked="checked"';
  781. $checked_options['and'] = '';
  782. } else {
  783. $checked_options['and'] = ' checked="checked"';
  784. $checked_options['or'] = '';
  785. }
  786. $html_output .= $this->getAndOrColCell(
  787. $new_column_count,
  788. $checked_options,
  789. $column_index + 1 == $this->criteriaColumnCount
  790. );
  791. $new_column_count++;
  792. }
  793. return $html_output;
  794. }
  795. /**
  796. * Provides rows for criteria inputbox Insert/Delete options
  797. * with AND/OR relationship modification options
  798. *
  799. * @param int $new_row_index New row index if rows are added/deleted
  800. *
  801. * @return string HTML table rows
  802. */
  803. private function getInputboxRow($new_row_index)
  804. {
  805. $html_output = '';
  806. $new_column_count = 0;
  807. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  808. if (! empty($this->criteriaColumnInsert)
  809. && isset($this->criteriaColumnInsert[$column_index])
  810. && $this->criteriaColumnInsert[$column_index] === 'on'
  811. ) {
  812. $orFieldName = 'Or' . $new_row_index . '[' . $new_column_count . ']';
  813. $html_output .= '<td class="text-center">';
  814. $html_output .= '<input type="text"'
  815. . ' name="Or' . $orFieldName . '" class="textfield"'
  816. . ' style="width: ' . $this->realwidth . '" size="20">';
  817. $html_output .= '</td>';
  818. $new_column_count++;
  819. }
  820. if (! empty($this->criteriaColumnDelete)
  821. && isset($this->criteriaColumnDelete[$column_index])
  822. && $this->criteriaColumnDelete[$column_index] === 'on'
  823. ) {
  824. continue;
  825. }
  826. $or = 'Or' . $new_row_index;
  827. if (! empty($_POST[$or]) && isset($_POST[$or][$column_index])) {
  828. $tmp_or = $_POST[$or][$column_index];
  829. } else {
  830. $tmp_or = '';
  831. }
  832. $html_output .= '<td class="text-center">';
  833. $html_output .= '<input type="text"'
  834. . ' name="Or' . $new_row_index . '[' . $new_column_count . ']"'
  835. . ' value="' . htmlspecialchars($tmp_or) . '" class="textfield"'
  836. . ' style="width: ' . $this->realwidth . '" size="20">';
  837. $html_output .= '</td>';
  838. if (! empty(${$or}) && isset(${$or}[$column_index])) {
  839. $GLOBALS[${'cur' . $or}][$new_column_count]
  840. = ${$or}[$column_index];
  841. }
  842. $new_column_count++;
  843. }
  844. return $html_output;
  845. }
  846. /**
  847. * Provides rows for criteria inputbox Insert/Delete options
  848. * with AND/OR relationship modification options
  849. *
  850. * @return string HTML table rows
  851. */
  852. private function getInsDelAndOrCriteriaRows()
  853. {
  854. $html_output = '';
  855. $new_row_count = 0;
  856. $checked_options = [];
  857. for ($row_index = 0; $row_index <= $this->criteriaRowCount; $row_index++) {
  858. if (isset($this->criteriaRowInsert[$row_index])
  859. && $this->criteriaRowInsert[$row_index] === 'on'
  860. ) {
  861. $checked_options['or'] = true;
  862. $checked_options['and'] = false;
  863. $html_output .= '<tr class="noclick">';
  864. $html_output .= $this->template->render('database/qbe/ins_del_and_or_cell', [
  865. 'row_index' => $new_row_count,
  866. 'checked_options' => $checked_options,
  867. ]);
  868. $html_output .= $this->getInputboxRow(
  869. $new_row_count
  870. );
  871. $new_row_count++;
  872. $html_output .= '</tr>';
  873. }
  874. if (isset($this->criteriaRowDelete[$row_index])
  875. && $this->criteriaRowDelete[$row_index] === 'on'
  876. ) {
  877. continue;
  878. }
  879. if (isset($this->criteriaAndOrRow[$row_index])) {
  880. $this->formAndOrRows[$new_row_count]
  881. = $this->criteriaAndOrRow[$row_index];
  882. }
  883. if (isset($this->criteriaAndOrRow[$row_index])
  884. && $this->criteriaAndOrRow[$row_index] === 'and'
  885. ) {
  886. $checked_options['and'] = true;
  887. $checked_options['or'] = false;
  888. } else {
  889. $checked_options['or'] = true;
  890. $checked_options['and'] = false;
  891. }
  892. $html_output .= '<tr class="noclick">';
  893. $html_output .= $this->template->render('database/qbe/ins_del_and_or_cell', [
  894. 'row_index' => $new_row_count,
  895. 'checked_options' => $checked_options,
  896. ]);
  897. $html_output .= $this->getInputboxRow(
  898. $new_row_count
  899. );
  900. $new_row_count++;
  901. $html_output .= '</tr>';
  902. }
  903. $this->newRowCount = $new_row_count;
  904. return $html_output;
  905. }
  906. /**
  907. * Provides SELECT clause for building SQL query
  908. *
  909. * @return string Select clause
  910. */
  911. private function getSelectClause()
  912. {
  913. $select_clause = '';
  914. $select_clauses = [];
  915. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  916. if (empty($this->formColumns[$column_index])
  917. || ! isset($this->formShows[$column_index])
  918. || $this->formShows[$column_index] !== 'on'
  919. ) {
  920. continue;
  921. }
  922. $select = $this->formColumns[$column_index];
  923. if (! empty($this->formAliases[$column_index])) {
  924. $select .= ' AS '
  925. . Util::backquote($this->formAliases[$column_index]);
  926. }
  927. $select_clauses[] = $select;
  928. }
  929. if (! empty($select_clauses)) {
  930. $select_clause = 'SELECT '
  931. . htmlspecialchars(implode(', ', $select_clauses)) . "\n";
  932. }
  933. return $select_clause;
  934. }
  935. /**
  936. * Provides WHERE clause for building SQL query
  937. *
  938. * @return string Where clause
  939. */
  940. private function getWhereClause()
  941. {
  942. $where_clause = '';
  943. $criteria_cnt = 0;
  944. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  945. if (isset($last_where, $this->formAndOrCols)
  946. && ! empty($this->formColumns[$column_index])
  947. && ! empty($this->formCriterions[$column_index])
  948. && $column_index
  949. ) {
  950. $where_clause .= ' '
  951. . mb_strtoupper($this->formAndOrCols[$last_where])
  952. . ' ';
  953. }
  954. if (empty($this->formColumns[$column_index])
  955. || empty($this->formCriterions[$column_index])
  956. ) {
  957. continue;
  958. }
  959. $where_clause .= '(' . $this->formColumns[$column_index] . ' '
  960. . $this->formCriterions[$column_index] . ')';
  961. $last_where = $column_index;
  962. $criteria_cnt++;
  963. }
  964. if ($criteria_cnt > 1) {
  965. $where_clause = '(' . $where_clause . ')';
  966. }
  967. // OR rows ${'cur' . $or}[$column_index]
  968. if (! isset($this->formAndOrRows)) {
  969. $this->formAndOrRows = [];
  970. }
  971. for ($row_index = 0; $row_index <= $this->criteriaRowCount; $row_index++) {
  972. $criteria_cnt = 0;
  973. $qry_orwhere = '';
  974. $last_orwhere = '';
  975. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  976. if (! empty($this->formColumns[$column_index])
  977. && ! empty($_POST['Or' . $row_index][$column_index])
  978. && $column_index
  979. ) {
  980. $qry_orwhere .= ' '
  981. . mb_strtoupper(
  982. $this->formAndOrCols[$last_orwhere]
  983. )
  984. . ' ';
  985. }
  986. if (empty($this->formColumns[$column_index])
  987. || empty($_POST['Or' . $row_index][$column_index])
  988. ) {
  989. continue;
  990. }
  991. $qry_orwhere .= '(' . $this->formColumns[$column_index]
  992. . ' '
  993. . $_POST['Or' . $row_index][$column_index]
  994. . ')';
  995. $last_orwhere = $column_index;
  996. $criteria_cnt++;
  997. }
  998. if ($criteria_cnt > 1) {
  999. $qry_orwhere = '(' . $qry_orwhere . ')';
  1000. }
  1001. if (empty($qry_orwhere)) {
  1002. continue;
  1003. }
  1004. $where_clause .= "\n"
  1005. . mb_strtoupper(
  1006. isset($this->formAndOrRows[$row_index])
  1007. ? $this->formAndOrRows[$row_index] . ' '
  1008. : ''
  1009. )
  1010. . $qry_orwhere;
  1011. }
  1012. if (! empty($where_clause) && $where_clause !== '()') {
  1013. $where_clause = 'WHERE ' . $where_clause . "\n";
  1014. }
  1015. return $where_clause;
  1016. }
  1017. /**
  1018. * Provides ORDER BY clause for building SQL query
  1019. *
  1020. * @return string Order By clause
  1021. */
  1022. private function getOrderByClause()
  1023. {
  1024. $orderby_clause = '';
  1025. $orderby_clauses = [];
  1026. // Create copy of instance variables
  1027. $columns = $this->formColumns;
  1028. $sort = $this->formSorts;
  1029. $sortOrder = $this->formSortOrders;
  1030. if (! empty($sortOrder)
  1031. && count($sortOrder) == count($sort)
  1032. && count($sortOrder) == count($columns)
  1033. ) {
  1034. // Sort all three arrays based on sort order
  1035. array_multisort($sortOrder, $sort, $columns);
  1036. }
  1037. for ($column_index = 0; $column_index < $this->criteriaColumnCount; $column_index++) {
  1038. // if all columns are chosen with * selector,
  1039. // then sorting isn't available
  1040. // Fix for Bug #570698
  1041. if (empty($columns[$column_index])
  1042. && empty($sort[$column_index])
  1043. ) {
  1044. continue;
  1045. }
  1046. if (mb_substr($columns[$column_index], -2) === '.*') {
  1047. continue;
  1048. }
  1049. if (empty($sort[$column_index])) {
  1050. continue;
  1051. }
  1052. $orderby_clauses[] = $columns[$column_index] . ' '
  1053. . $sort[$column_index];
  1054. }
  1055. if (! empty($orderby_clauses)) {
  1056. $orderby_clause = 'ORDER BY '
  1057. . htmlspecialchars(implode(', ', $orderby_clauses)) . "\n";
  1058. }
  1059. return $orderby_clause;
  1060. }
  1061. /**
  1062. * Provides UNIQUE columns and INDEX columns present in criteria tables
  1063. *
  1064. * @param array $search_tables Tables involved in the search
  1065. * @param array $search_columns Columns involved in the search
  1066. * @param array $where_clause_columns Columns having criteria where clause
  1067. *
  1068. * @return array having UNIQUE and INDEX columns
  1069. */
  1070. private function getIndexes(
  1071. array $search_tables,
  1072. array $search_columns,
  1073. array $where_clause_columns
  1074. ) {
  1075. $unique_columns = [];
  1076. $index_columns = [];
  1077. foreach ($search_tables as $table) {
  1078. $indexes = $this->dbi->getTableIndexes($this->db, $table);
  1079. foreach ($indexes as $index) {
  1080. $column = $table . '.' . $index['Column_name'];
  1081. if (! isset($search_columns[$column])) {
  1082. continue;
  1083. }
  1084. if ($index['Non_unique'] == 0) {
  1085. if (isset($where_clause_columns[$column])) {
  1086. $unique_columns[$column] = 'Y';
  1087. } else {
  1088. $unique_columns[$column] = 'N';
  1089. }
  1090. } else {
  1091. if (isset($where_clause_columns[$column])) {
  1092. $index_columns[$column] = 'Y';
  1093. } else {
  1094. $index_columns[$column] = 'N';
  1095. }
  1096. }
  1097. }
  1098. }
  1099. return [
  1100. 'unique' => $unique_columns,
  1101. 'index' => $index_columns,
  1102. ];
  1103. }
  1104. /**
  1105. * Provides UNIQUE columns and INDEX columns present in criteria tables
  1106. *
  1107. * @param array $search_tables Tables involved in the search
  1108. * @param array $search_columns Columns involved in the search
  1109. * @param array $where_clause_columns Columns having criteria where clause
  1110. *
  1111. * @return array having UNIQUE and INDEX columns
  1112. */
  1113. private function getLeftJoinColumnCandidates(
  1114. array $search_tables,
  1115. array $search_columns,
  1116. array $where_clause_columns
  1117. ) {
  1118. $this->dbi->selectDb($this->db);
  1119. // Get unique columns and index columns
  1120. $indexes = $this->getIndexes(
  1121. $search_tables,
  1122. $search_columns,
  1123. $where_clause_columns
  1124. );
  1125. $unique_columns = $indexes['unique'];
  1126. $index_columns = $indexes['index'];
  1127. [$candidate_columns, $needsort]
  1128. = $this->getLeftJoinColumnCandidatesBest(
  1129. $search_tables,
  1130. $where_clause_columns,
  1131. $unique_columns,
  1132. $index_columns
  1133. );
  1134. // If we came up with $unique_columns (very good) or $index_columns (still
  1135. // good) as $candidate_columns we want to check if we have any 'Y' there
  1136. // (that would mean that they were also found in the whereclauses
  1137. // which would be great). if yes, we take only those
  1138. if ($needsort != 1) {
  1139. return $candidate_columns;
  1140. }
  1141. $very_good = [];
  1142. $still_good = [];
  1143. foreach ($candidate_columns as $column => $is_where) {
  1144. $table = explode('.', $column);
  1145. $table = $table[0];
  1146. if ($is_where === 'Y') {
  1147. $very_good[$column] = $table;
  1148. } else {
  1149. $still_good[$column] = $table;
  1150. }
  1151. }
  1152. if (count($very_good) > 0) {
  1153. $candidate_columns = $very_good;
  1154. // Candidates restricted in index+where
  1155. } else {
  1156. $candidate_columns = $still_good;
  1157. // None of the candidates where in a where-clause
  1158. }
  1159. return $candidate_columns;
  1160. }
  1161. /**
  1162. * Provides the main table to form the LEFT JOIN clause
  1163. *
  1164. * @param array $search_tables Tables involved in the search
  1165. * @param array $search_columns Columns involved in the search
  1166. * @param array $where_clause_columns Columns having criteria where clause
  1167. * @param array $where_clause_tables Tables having criteria where clause
  1168. *
  1169. * @return string table name
  1170. */
  1171. private function getMasterTable(
  1172. array $search_tables,
  1173. array $search_columns,
  1174. array $where_clause_columns,
  1175. array $where_clause_tables
  1176. ) {
  1177. if (count($where_clause_tables) === 1) {
  1178. // If there is exactly one column that has a decent where-clause
  1179. // we will just use this
  1180. return key($where_clause_tables);
  1181. }
  1182. // Now let's find out which of the tables has an index
  1183. // (When the control user is the same as the normal user
  1184. // because they are using one of their databases as pmadb,
  1185. // the last db selected is not always the one where we need to work)
  1186. $candidate_columns = $this->getLeftJoinColumnCandidates(
  1187. $search_tables,
  1188. $search_columns,
  1189. $where_clause_columns
  1190. );
  1191. // Generally, we need to display all the rows of foreign (referenced)
  1192. // table, whether they have any matching row in child table or not.
  1193. // So we select candidate tables which are foreign tables.
  1194. $foreign_tables = [];
  1195. foreach ($candidate_columns as $one_table) {
  1196. $foreigners = $this->relation->getForeigners($this->db, $one_table);
  1197. foreach ($foreigners as $key => $foreigner) {
  1198. if ($key !== 'foreign_keys_data') {
  1199. if (in_array($foreigner['foreign_table'], $candidate_columns)) {
  1200. $foreign_tables[$foreigner['foreign_table']]
  1201. = $foreigner['foreign_table'];
  1202. }
  1203. continue;
  1204. }
  1205. foreach ($foreigner as $one_key) {
  1206. if (! in_array($one_key['ref_table_name'], $candidate_columns)) {
  1207. continue;
  1208. }
  1209. $foreign_tables[$one_key['ref_table_name']]
  1210. = $one_key['ref_table_name'];
  1211. }
  1212. }
  1213. }
  1214. if (count($foreign_tables)) {
  1215. $candidate_columns = $foreign_tables;
  1216. }
  1217. // If our array of candidates has more than one member we'll just
  1218. // find the smallest table.
  1219. // Of course the actual query would be faster if we check for
  1220. // the Criteria which gives the smallest result set in its table,
  1221. // but it would take too much time to check this
  1222. if (! (count($candidate_columns) > 1)) {
  1223. // Only one single candidate
  1224. return reset($candidate_columns);
  1225. }
  1226. // Of course we only want to check each table once
  1227. $checked_tables = $candidate_columns;
  1228. $tsize = [];
  1229. $maxsize = -1;
  1230. $result = '';
  1231. foreach ($candidate_columns as $table) {
  1232. if ($checked_tables[$table] != 1) {
  1233. $_table = new Table($table, $this->db);
  1234. $tsize[$table] = $_table->countRecords();
  1235. $checked_tables[$table] = 1;
  1236. }
  1237. if ($tsize[$table] <= $maxsize) {
  1238. continue;
  1239. }
  1240. $maxsize = $tsize[$table];
  1241. $result = $table;
  1242. }
  1243. // Return largest table
  1244. return $result;
  1245. }
  1246. /**
  1247. * Provides columns and tables that have valid where clause criteria
  1248. *
  1249. * @return array
  1250. */
  1251. private function getWhereClauseTablesAndColumns()
  1252. {
  1253. $where_clause_columns = [];
  1254. $where_clause_tables = [];
  1255. // Now we need all tables that we have in the where clause
  1256. for ($column_index = 0, $nb = count($this->criteria); $column_index < $nb; $column_index++) {
  1257. $current_table = explode('.', $_POST['criteriaColumn'][$column_index]);
  1258. if (empty($current_table[0]) || empty($current_table[1])) {
  1259. continue;
  1260. }
  1261. $table = str_replace('`', '', $current_table[0]);
  1262. $column = str_replace('`', '', $current_table[1]);
  1263. $column = $table . '.' . $column;
  1264. // Now we know that our array has the same numbers as $criteria
  1265. // we can check which of our columns has a where clause
  1266. if (empty($this->criteria[$column_index])) {
  1267. continue;
  1268. }
  1269. if (mb_substr($this->criteria[$column_index], 0, 1) !== '='
  1270. && stripos($this->criteria[$column_index], 'is') === false
  1271. ) {
  1272. continue;
  1273. }
  1274. $where_clause_columns[$column] = $column;
  1275. $where_clause_tables[$table] = $table;
  1276. }
  1277. return [
  1278. 'where_clause_tables' => $where_clause_tables,
  1279. 'where_clause_columns' => $where_clause_columns,
  1280. ];
  1281. }
  1282. /**
  1283. * Provides FROM clause for building SQL query
  1284. *
  1285. * @param array $formColumns List of selected columns in the form
  1286. *
  1287. * @return string FROM clause
  1288. */
  1289. private function getFromClause(array $formColumns)
  1290. {
  1291. $from_clause = '';
  1292. if (empty($formColumns)) {
  1293. return $from_clause;
  1294. }
  1295. // Initialize some variables
  1296. $search_tables = $search_columns = [];
  1297. // We only start this if we have fields, otherwise it would be dumb
  1298. foreach ($formColumns as $value) {
  1299. $parts = explode('.', $value);
  1300. if (empty($parts[0]) || empty($parts[1])) {
  1301. continue;
  1302. }
  1303. $table = str_replace('`', '', $parts[0]);
  1304. $search_tables[$table] = $table;
  1305. $search_columns[] = $table . '.' . str_replace(
  1306. '`',
  1307. '',
  1308. $parts[1]
  1309. );
  1310. }
  1311. // Create LEFT JOINS out of Relations
  1312. $from_clause = $this->getJoinForFromClause(
  1313. $search_tables,
  1314. $search_columns
  1315. );
  1316. // In case relations are not defined, just generate the FROM clause
  1317. // from the list of tables, however we don't generate any JOIN
  1318. if (empty($from_clause)) {
  1319. // Create cartesian product
  1320. $from_clause = implode(
  1321. ', ',
  1322. array_map([Util::class, 'backquote'], $search_tables)
  1323. );
  1324. }
  1325. return $from_clause;
  1326. }
  1327. /**
  1328. * Formulates the WHERE clause by JOINing tables
  1329. *
  1330. * @param array $searchTables Tables involved in the search
  1331. * @param array $searchColumns Columns involved in the search
  1332. *
  1333. * @return string table name
  1334. */
  1335. private function getJoinForFromClause(array $searchTables, array $searchColumns)
  1336. {
  1337. // $relations[master_table][foreign_table] => clause
  1338. $relations = [];
  1339. // Fill $relations with inter table relationship data
  1340. foreach ($searchTables as $oneTable) {
  1341. $this->loadRelationsForTable($relations, $oneTable);
  1342. }
  1343. // Get tables and columns with valid where clauses
  1344. $validWhereClauses = $this->getWhereClauseTablesAndColumns();
  1345. $whereClauseTables = $validWhereClauses['where_clause_tables'];
  1346. $whereClauseColumns = $validWhereClauses['where_clause_columns'];
  1347. // Get master table
  1348. $master = $this->getMasterTable(
  1349. $searchTables,
  1350. $searchColumns,
  1351. $whereClauseColumns,
  1352. $whereClauseTables
  1353. );
  1354. // Will include master tables and all tables that can be combined into
  1355. // a cluster by their relation
  1356. $finalized = [];
  1357. if (strlen((string) $master) > 0) {
  1358. // Add master tables
  1359. $finalized[$master] = '';
  1360. }
  1361. // Fill the $finalized array with JOIN clauses for each table
  1362. $this->fillJoinClauses($finalized, $relations, $searchTables);
  1363. // JOIN clause
  1364. $join = '';
  1365. // Tables that can not be combined with the table cluster
  1366. // which includes master table
  1367. $unfinalized = array_diff($searchTables, array_keys($finalized));
  1368. if (count($unfinalized) > 0) {
  1369. // We need to look for intermediary tables to JOIN unfinalized tables
  1370. // Heuristic to chose intermediary tables is to look for tables
  1371. // having relationships with unfinalized tables
  1372. foreach ($unfinalized as $oneTable) {
  1373. $references = $this->relation->getChildReferences($this->db, $oneTable);
  1374. foreach ($references as $column => $columnReferences) {
  1375. foreach ($columnReferences as $reference) {
  1376. // Only from this schema
  1377. if ($reference['table_schema'] != $this->db) {
  1378. continue;
  1379. }
  1380. $table = $reference['table_name'];
  1381. $this->loadRelationsForTable($relations, $table);
  1382. // Make copies
  1383. $tempFinalized = $finalized;
  1384. $tempSearchTables = $searchTables;
  1385. $tempSearchTables[] = $table;
  1386. // Try joining with the added table
  1387. $this->fillJoinClauses(
  1388. $tempFinalized,
  1389. $relations,
  1390. $tempSearchTables
  1391. );
  1392. $tempUnfinalized = array_diff(
  1393. $tempSearchTables,
  1394. array_keys($tempFinalized)
  1395. );
  1396. // Take greedy approach.
  1397. // If the unfinalized count drops we keep the new table
  1398. // and switch temporary varibles with the original ones
  1399. if (count($tempUnfinalized) < count($unfinalized)) {
  1400. $finalized = $tempFinalized;
  1401. $searchTables = $tempSearchTables;
  1402. }
  1403. // We are done if no unfinalized tables anymore
  1404. if (count($tempUnfinalized) === 0) {
  1405. break 3;
  1406. }
  1407. }
  1408. }
  1409. }
  1410. $unfinalized = array_diff($searchTables, array_keys($finalized));
  1411. // If there are still unfinalized tables
  1412. if (count($unfinalized) > 0) {
  1413. // Add these tables as cartesian product before joined tables
  1414. $join .= implode(
  1415. ', ',
  1416. array_map([Util::class, 'backquote'], $unfinalized)
  1417. );
  1418. }
  1419. }
  1420. $first = true;
  1421. // Add joined tables
  1422. foreach ($finalized as $table => $clause) {
  1423. if ($first) {
  1424. if (! empty($join)) {
  1425. $join .= ', ';
  1426. }
  1427. $join .= Util::backquote($table);
  1428. $first = false;
  1429. } else {
  1430. $join .= "\n LEFT JOIN " . Util::backquote(
  1431. $table
  1432. ) . ' ON ' . $clause;
  1433. }
  1434. }
  1435. return $join;
  1436. }
  1437. /**
  1438. * Loads relations for a given table into the $relations array
  1439. *
  1440. * @param array $relations array of relations
  1441. * @param string $oneTable the table
  1442. *
  1443. * @return void
  1444. */
  1445. private function loadRelationsForTable(array &$relations, $oneTable)
  1446. {
  1447. $relations[$oneTable] = [];
  1448. $foreigners = $this->relation->getForeigners($GLOBALS['db'], $oneTable);
  1449. foreach ($foreigners as $field => $foreigner) {
  1450. // Foreign keys data
  1451. if ($field === 'foreign_keys_data') {
  1452. foreach ($foreigner as $oneKey) {
  1453. $clauses = [];
  1454. // There may be multiple column relations
  1455. foreach ($oneKey['index_list'] as $index => $oneField) {
  1456. $clauses[]
  1457. = Util::backquote($oneTable) . '.'
  1458. . Util::backquote($oneField) . ' = '
  1459. . Util::backquote($oneKey['ref_table_name']) . '.'
  1460. . Util::backquote($oneKey['ref_index_list'][$index]);
  1461. }
  1462. // Combine multiple column relations with AND
  1463. $relations[$oneTable][$oneKey['ref_table_name']]
  1464. = implode(' AND ', $clauses);
  1465. }
  1466. } else { // Internal relations
  1467. $relations[$oneTable][$foreigner['foreign_table']]
  1468. = Util::backquote($oneTable) . '.'
  1469. . Util::backquote((string) $field) . ' = '
  1470. . Util::backquote($foreigner['foreign_table']) . '.'
  1471. . Util::backquote($foreigner['foreign_field']);
  1472. }
  1473. }
  1474. }
  1475. /**
  1476. * Fills the $finalized arrays with JOIN clauses for each of the tables
  1477. *
  1478. * @param array $finalized JOIN clauses for each table
  1479. * @param array $relations Relations among tables
  1480. * @param array $searchTables Tables involved in the search
  1481. *
  1482. * @return void
  1483. */
  1484. private function fillJoinClauses(array &$finalized, array $relations, array $searchTables)
  1485. {
  1486. while (true) {
  1487. $added = false;
  1488. foreach ($searchTables as $masterTable) {
  1489. $foreignData = $relations[$masterTable];
  1490. foreach ($foreignData as $foreignTable => $clause) {
  1491. if (! isset($finalized[$masterTable])
  1492. && isset($finalized[$foreignTable])
  1493. ) {
  1494. $finalized[$masterTable] = $clause;
  1495. $added = true;
  1496. } elseif (! isset($finalized[$foreignTable])
  1497. && isset($finalized[$masterTable])
  1498. && in_array($foreignTable, $searchTables)
  1499. ) {
  1500. $finalized[$foreignTable] = $clause;
  1501. $added = true;
  1502. }
  1503. if (! $added) {
  1504. continue;
  1505. }
  1506. // We are done if all tables are in $finalized
  1507. if (count($finalized) == count($searchTables)) {
  1508. return;
  1509. }
  1510. }
  1511. }
  1512. // If no new tables were added during this iteration, break;
  1513. if (! $added) {
  1514. return;
  1515. }
  1516. }
  1517. }
  1518. /**
  1519. * Provides the generated SQL query
  1520. *
  1521. * @param array $formColumns List of selected columns in the form
  1522. *
  1523. * @return string SQL query
  1524. */
  1525. private function getSQLQuery(array $formColumns)
  1526. {
  1527. $sql_query = '';
  1528. // get SELECT clause
  1529. $sql_query .= $this->getSelectClause();
  1530. // get FROM clause
  1531. $from_clause = $this->getFromClause($formColumns);
  1532. if (! empty($from_clause)) {
  1533. $sql_query .= 'FROM ' . htmlspecialchars($from_clause) . "\n";
  1534. }
  1535. // get WHERE clause
  1536. $sql_query .= $this->getWhereClause();
  1537. // get ORDER BY clause
  1538. $sql_query .= $this->getOrderByClause();
  1539. return $sql_query;
  1540. }
  1541. public function getSelectionForm(): string
  1542. {
  1543. global $cfgRelation;
  1544. $savedSearchesField = $cfgRelation['savedsearcheswork'] ? $this->getSavedSearchesField() : '';
  1545. $columnNamesRow = $this->getColumnNamesRow();
  1546. $columnAliasRow = $this->getColumnAliasRow();
  1547. $showRow = $this->getShowRow();
  1548. $sortRow = $this->getSortRow();
  1549. $sortOrder = $this->getSortOrder();
  1550. $criteriaInputBoxRow = $this->getCriteriaInputboxRow();
  1551. $insDelAndOrCriteriaRows = $this->getInsDelAndOrCriteriaRows();
  1552. $modifyColumnsRow = $this->getModifyColumnsRow();
  1553. $this->newRowCount--;
  1554. $url_params = [];
  1555. $url_params['db'] = $this->db;
  1556. $url_params['criteriaColumnCount'] = $this->newColumnCount;
  1557. $url_params['rows'] = $this->newRowCount;
  1558. if (empty($this->formColumns)) {
  1559. $this->formColumns = [];
  1560. }
  1561. $sqlQuery = $this->getSQLQuery($this->formColumns);
  1562. return $this->template->render('database/qbe/selection_form', [
  1563. 'db' => $this->db,
  1564. 'url_params' => $url_params,
  1565. 'db_link' => Generator::getDbLink($this->db),
  1566. 'criteria_tables' => $this->criteriaTables,
  1567. 'saved_searches_field' => $savedSearchesField,
  1568. 'column_names_row' => $columnNamesRow,
  1569. 'column_alias_row' => $columnAliasRow,
  1570. 'show_row' => $showRow,
  1571. 'sort_row' => $sortRow,
  1572. 'sort_order' => $sortOrder,
  1573. 'criteria_input_box_row' => $criteriaInputBoxRow,
  1574. 'ins_del_and_or_criteria_rows' => $insDelAndOrCriteriaRows,
  1575. 'modify_columns_row' => $modifyColumnsRow,
  1576. 'sql_query' => $sqlQuery,
  1577. ]);
  1578. }
  1579. /**
  1580. * Get fields to display
  1581. *
  1582. * @return string
  1583. */
  1584. private function getSavedSearchesField()
  1585. {
  1586. $html_output = __('Saved bookmarked search:');
  1587. $html_output .= ' <select name="searchId" id="searchId">';
  1588. $html_output .= '<option value="">' . __('New bookmark') . '</option>';
  1589. $currentSearch = $this->getCurrentSearch();
  1590. $currentSearchId = null;
  1591. $currentSearchName = null;
  1592. if ($currentSearch != null) {
  1593. $currentSearchId = $currentSearch->getId();
  1594. $currentSearchName = $currentSearch->getSearchName();
  1595. }
  1596. foreach ($this->savedSearchList as $id => $name) {
  1597. $html_output .= '<option value="' . htmlspecialchars((string) $id)
  1598. . '" ' . (
  1599. $id == $currentSearchId
  1600. ? 'selected="selected" '
  1601. : ''
  1602. )
  1603. . '>'
  1604. . htmlspecialchars($name)
  1605. . '</option>';
  1606. }
  1607. $html_output .= '</select>';
  1608. $html_output .= '<input type="text" name="searchName" id="searchName" '
  1609. . 'value="' . htmlspecialchars((string) $currentSearchName) . '">';
  1610. $html_output .= '<input type="hidden" name="action" id="action" value="">';
  1611. $html_output .= '<input class="btn btn-secondary" type="submit" name="saveSearch" id="saveSearch" '
  1612. . 'value="' . __('Create bookmark') . '">';
  1613. if ($currentSearchId !== null) {
  1614. $html_output .= '<input class="btn btn-secondary" type="submit" name="updateSearch" '
  1615. . 'id="updateSearch" value="' . __('Update bookmark') . '">';
  1616. $html_output .= '<input class="btn btn-secondary" type="submit" name="deleteSearch" '
  1617. . 'id="deleteSearch" value="' . __('Delete bookmark') . '">';
  1618. }
  1619. return $html_output;
  1620. }
  1621. /**
  1622. * Initialize _criteria_column_count
  1623. *
  1624. * @return int Previous number of columns
  1625. */
  1626. private function initializeCriteriasCount(): int
  1627. {
  1628. // sets column count
  1629. $criteriaColumnCount = Core::ifSetOr(
  1630. $_POST['criteriaColumnCount'],
  1631. 3,
  1632. 'numeric'
  1633. );
  1634. $criteriaColumnAdd = Core::ifSetOr(
  1635. $_POST['criteriaColumnAdd'],
  1636. 0,
  1637. 'numeric'
  1638. );
  1639. $this->criteriaColumnCount = max(
  1640. $criteriaColumnCount + $criteriaColumnAdd,
  1641. 0
  1642. );
  1643. // sets row count
  1644. $rows = Core::ifSetOr($_POST['rows'], 0, 'numeric');
  1645. $criteriaRowAdd = Core::ifSetOr($_POST['criteriaRowAdd'], 0, 'numeric');
  1646. $this->criteriaRowCount = min(
  1647. 100,
  1648. max($rows + $criteriaRowAdd, 0)
  1649. );
  1650. return (int) $criteriaColumnCount;
  1651. }
  1652. /**
  1653. * Get best
  1654. *
  1655. * @param array $search_tables Tables involved in the search
  1656. * @param array|null $where_clause_columns Columns with where clause
  1657. * @param array|null $unique_columns Unique columns
  1658. * @param array|null $index_columns Indexed columns
  1659. *
  1660. * @return array
  1661. */
  1662. private function getLeftJoinColumnCandidatesBest(
  1663. array $search_tables,
  1664. ?array $where_clause_columns,
  1665. ?array $unique_columns,
  1666. ?array $index_columns
  1667. ) {
  1668. // now we want to find the best.
  1669. if (isset($unique_columns) && count($unique_columns) > 0) {
  1670. $candidate_columns = $unique_columns;
  1671. $needsort = 1;
  1672. return [
  1673. $candidate_columns,
  1674. $needsort,
  1675. ];
  1676. }
  1677. if (isset($index_columns) && count($index_columns) > 0) {
  1678. $candidate_columns = $index_columns;
  1679. $needsort = 1;
  1680. return [
  1681. $candidate_columns,
  1682. $needsort,
  1683. ];
  1684. }
  1685. if (isset($where_clause_columns) && count($where_clause_columns) > 0) {
  1686. $candidate_columns = $where_clause_columns;
  1687. $needsort = 0;
  1688. return [
  1689. $candidate_columns,
  1690. $needsort,
  1691. ];
  1692. }
  1693. $candidate_columns = $search_tables;
  1694. $needsort = 0;
  1695. return [
  1696. $candidate_columns,
  1697. $needsort,
  1698. ];
  1699. }
  1700. }