PageRenderTime 55ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Database/Qbe.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 1830 lines | 1147 code | 212 blank | 471 comment | 213 complexity | a0ec51bc652c121cbb0f79b2173bd989 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Handles DB QBE search
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Database;
  7. use PhpMyAdmin\DatabaseInterface;
  8. use PhpMyAdmin\Html\Generator;
  9. use PhpMyAdmin\Message;
  10. use PhpMyAdmin\Relation;
  11. use PhpMyAdmin\SavedSearches;
  12. use PhpMyAdmin\Table;
  13. use PhpMyAdmin\Template;
  14. use PhpMyAdmin\Util;
  15. use function __;
  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 is_array;
  27. use function is_numeric;
  28. use function key;
  29. use function max;
  30. use function mb_strlen;
  31. use function mb_strtoupper;
  32. use function mb_substr;
  33. use function min;
  34. use function reset;
  35. use function str_replace;
  36. use function stripos;
  37. use function strlen;
  38. /**
  39. * Class to handle database QBE search
  40. */
  41. class Qbe
  42. {
  43. /**
  44. * Database name
  45. *
  46. * @access private
  47. * @var string
  48. */
  49. private $db;
  50. /**
  51. * Table Names (selected/non-selected)
  52. *
  53. * @access private
  54. * @var array
  55. */
  56. private $criteriaTables;
  57. /**
  58. * Column Names
  59. *
  60. * @access private
  61. * @var array
  62. */
  63. private $columnNames;
  64. /**
  65. * Number of columns
  66. *
  67. * @access private
  68. * @var int
  69. */
  70. private $criteriaColumnCount;
  71. /**
  72. * Number of Rows
  73. *
  74. * @access private
  75. * @var int
  76. */
  77. private $criteriaRowCount;
  78. /**
  79. * Whether to insert a new column
  80. *
  81. * @access private
  82. * @var array|null
  83. */
  84. private $criteriaColumnInsert;
  85. /**
  86. * Whether to delete a column
  87. *
  88. * @access private
  89. * @var array|null
  90. */
  91. private $criteriaColumnDelete;
  92. /**
  93. * Whether to insert a new row
  94. *
  95. * @access private
  96. * @var array
  97. */
  98. private $criteriaRowInsert;
  99. /**
  100. * Whether to delete a row
  101. *
  102. * @access private
  103. * @var array
  104. */
  105. private $criteriaRowDelete;
  106. /**
  107. * Already set criteria values
  108. *
  109. * @access private
  110. * @var array
  111. */
  112. private $criteria;
  113. /**
  114. * Previously set criteria values
  115. *
  116. * @access private
  117. * @var array
  118. */
  119. private $prevCriteria;
  120. /**
  121. * AND/OR relation b/w criteria columns
  122. *
  123. * @access private
  124. * @var array
  125. */
  126. private $criteriaAndOrColumn;
  127. /**
  128. * AND/OR relation b/w criteria rows
  129. *
  130. * @access private
  131. * @var array
  132. */
  133. private $criteriaAndOrRow;
  134. /**
  135. * Large width of a column
  136. *
  137. * @access private
  138. * @var string
  139. */
  140. private $realwidth;
  141. /**
  142. * Minimum width of a column
  143. *
  144. * @access private
  145. * @var int
  146. */
  147. private $formColumnWidth;
  148. /**
  149. * Selected columns in the form
  150. *
  151. * @access private
  152. * @var array
  153. */
  154. private $formColumns;
  155. /**
  156. * Entered aliases in the form
  157. *
  158. * @access private
  159. * @var array
  160. */
  161. private $formAliases;
  162. /**
  163. * Chosen sort options in the form
  164. *
  165. * @access private
  166. * @var array
  167. */
  168. private $formSorts;
  169. /**
  170. * Chosen sort orders in the form
  171. *
  172. * @access private
  173. * @var array
  174. */
  175. private $formSortOrders;
  176. /**
  177. * Show checkboxes in the form
  178. *
  179. * @access private
  180. * @var array
  181. */
  182. private $formShows;
  183. /**
  184. * Entered criteria values in the form
  185. *
  186. * @access private
  187. * @var array
  188. */
  189. private $formCriterions;
  190. /**
  191. * AND/OR column radio buttons in the form
  192. *
  193. * @access private
  194. * @var array
  195. */
  196. private $formAndOrCols;
  197. /**
  198. * AND/OR row radio buttons in the form
  199. *
  200. * @access private
  201. * @var array
  202. */
  203. private $formAndOrRows;
  204. /**
  205. * New column count in case of add/delete
  206. *
  207. * @access private
  208. * @var int
  209. */
  210. private $newColumnCount;
  211. /**
  212. * New row count in case of add/delete
  213. *
  214. * @access private
  215. * @var int
  216. */
  217. private $newRowCount;
  218. /**
  219. * List of saved searches
  220. *
  221. * @access private
  222. * @var array
  223. */
  224. private $savedSearchList = null;
  225. /**
  226. * Current search
  227. *
  228. * @access private
  229. * @var SavedSearches
  230. */
  231. private $currentSearch = null;
  232. /** @var Relation */
  233. private $relation;
  234. /** @var DatabaseInterface */
  235. public $dbi;
  236. /** @var Template */
  237. public $template;
  238. /**
  239. * @param Relation $relation Relation object
  240. * @param Template $template Template object
  241. * @param DatabaseInterface $dbi DatabaseInterface object
  242. * @param string $dbname Database name
  243. * @param array $savedSearchList List of saved searches
  244. * @param SavedSearches $currentSearch Current search id
  245. */
  246. public function __construct(
  247. Relation $relation,
  248. Template $template,
  249. $dbi,
  250. $dbname,
  251. array $savedSearchList = [],
  252. $currentSearch = null
  253. ) {
  254. $this->db = $dbname;
  255. $this->savedSearchList = $savedSearchList;
  256. $this->currentSearch = $currentSearch;
  257. $this->dbi = $dbi;
  258. $this->relation = $relation;
  259. $this->template = $template;
  260. $this->loadCriterias();
  261. // Sets criteria parameters
  262. $this->setSearchParams();
  263. $this->setCriteriaTablesAndColumns();
  264. }
  265. /**
  266. * Initialize criterias
  267. *
  268. * @return static
  269. */
  270. private function loadCriterias()
  271. {
  272. if ($this->currentSearch === null || $this->currentSearch->getCriterias() === null) {
  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. private function setSearchParams(): void
  292. {
  293. $criteriaColumnCount = $this->initializeCriteriasCount();
  294. $this->criteriaColumnInsert = isset($_POST['criteriaColumnInsert']) && is_array($_POST['criteriaColumnInsert'])
  295. ? $_POST['criteriaColumnInsert']
  296. : null;
  297. $this->criteriaColumnDelete = isset($_POST['criteriaColumnDelete']) && is_array($_POST['criteriaColumnDelete'])
  298. ? $_POST['criteriaColumnDelete']
  299. : null;
  300. $this->prevCriteria = $_POST['prev_criteria'] ?? [];
  301. $this->criteria = $_POST['criteria'] ?? array_fill(0, $criteriaColumnCount, '');
  302. $this->criteriaRowInsert = $_POST['criteriaRowInsert'] ?? array_fill(0, $criteriaColumnCount, '');
  303. $this->criteriaRowDelete = $_POST['criteriaRowDelete'] ?? array_fill(0, $criteriaColumnCount, '');
  304. $this->criteriaAndOrRow = $_POST['criteriaAndOrRow'] ?? array_fill(0, $criteriaColumnCount, '');
  305. $this->criteriaAndOrColumn = $_POST['criteriaAndOrColumn'] ?? array_fill(0, $criteriaColumnCount, '');
  306. // sets minimum width
  307. $this->formColumnWidth = 12;
  308. $this->formColumns = [];
  309. $this->formSorts = [];
  310. $this->formShows = [];
  311. $this->formCriterions = [];
  312. $this->formAndOrRows = [];
  313. $this->formAndOrCols = [];
  314. }
  315. /**
  316. * Sets criteria tables and columns
  317. */
  318. private function setCriteriaTablesAndColumns(): void
  319. {
  320. // The tables list sent by a previously submitted form
  321. if (isset($_POST['TableList']) && is_array($_POST['TableList'])) {
  322. foreach ($_POST['TableList'] as $eachTable) {
  323. $this->criteriaTables[$eachTable] = ' selected="selected"';
  324. }
  325. }
  326. $allTables = $this->dbi->query(
  327. 'SHOW TABLES FROM ' . Util::backquote($this->db) . ';',
  328. DatabaseInterface::CONNECT_USER,
  329. DatabaseInterface::QUERY_STORE
  330. );
  331. $allTablesCount = $this->dbi->numRows($allTables);
  332. if ($allTablesCount == 0) {
  333. echo Message::error(__('No tables found in database.'))->getDisplay();
  334. exit;
  335. }
  336. // The tables list gets from MySQL
  337. while ([$table] = $this->dbi->fetchRow($allTables)) {
  338. $columns = $this->dbi->getColumns($this->db, $table);
  339. if (empty($this->criteriaTables[$table]) && ! empty($_POST['TableList'])) {
  340. $this->criteriaTables[$table] = '';
  341. } else {
  342. $this->criteriaTables[$table] = ' selected="selected"';
  343. }
  344. // The fields list per selected tables
  345. if ($this->criteriaTables[$table] !== ' selected="selected"') {
  346. continue;
  347. }
  348. $eachTable = Util::backquote($table);
  349. $this->columnNames[] = $eachTable . '.*';
  350. foreach ($columns as $eachColumn) {
  351. $eachColumn = $eachTable . '.'
  352. . Util::backquote($eachColumn['Field']);
  353. $this->columnNames[] = $eachColumn;
  354. // increase the width if necessary
  355. $this->formColumnWidth = max(
  356. mb_strlen($eachColumn),
  357. $this->formColumnWidth
  358. );
  359. }
  360. }
  361. $this->dbi->freeResult($allTables);
  362. // sets the largest width found
  363. $this->realwidth = $this->formColumnWidth . 'ex';
  364. }
  365. /**
  366. * Provides select options list containing column names
  367. *
  368. * @param int $columnNumber Column Number (0,1,2) or more
  369. * @param string $selected Selected criteria column name
  370. *
  371. * @return string HTML for select options
  372. */
  373. private function showColumnSelectCell($columnNumber, $selected = '')
  374. {
  375. return $this->template->render('database/qbe/column_select_cell', [
  376. 'column_number' => $columnNumber,
  377. 'column_names' => $this->columnNames,
  378. 'selected' => $selected,
  379. ]);
  380. }
  381. /**
  382. * Provides select options list containing sort options (ASC/DESC)
  383. *
  384. * @param int $columnNumber Column Number (0,1,2) or more
  385. * @param string $selected Selected criteria 'ASC' or 'DESC'
  386. *
  387. * @return string HTML for select options
  388. */
  389. private function getSortSelectCell(
  390. $columnNumber,
  391. $selected = ''
  392. ) {
  393. return $this->template->render('database/qbe/sort_select_cell', [
  394. 'real_width' => $this->realwidth,
  395. 'column_number' => $columnNumber,
  396. 'selected' => $selected,
  397. ]);
  398. }
  399. /**
  400. * Provides select options list containing sort order
  401. *
  402. * @param int $columnNumber Column Number (0,1,2) or more
  403. * @param int $sortOrder Sort order
  404. *
  405. * @return string HTML for select options
  406. */
  407. private function getSortOrderSelectCell($columnNumber, $sortOrder)
  408. {
  409. $totalColumnCount = $this->getNewColumnCount();
  410. return $this->template->render('database/qbe/sort_order_select_cell', [
  411. 'total_column_count' => $totalColumnCount,
  412. 'column_number' => $columnNumber,
  413. 'sort_order' => $sortOrder,
  414. ]);
  415. }
  416. /**
  417. * Returns the new column count after adding and removing columns as instructed
  418. *
  419. * @return int new column count
  420. */
  421. private function getNewColumnCount()
  422. {
  423. $totalColumnCount = $this->criteriaColumnCount;
  424. if (! empty($this->criteriaColumnInsert)) {
  425. $totalColumnCount += count($this->criteriaColumnInsert);
  426. }
  427. if (! empty($this->criteriaColumnDelete)) {
  428. $totalColumnCount -= count($this->criteriaColumnDelete);
  429. }
  430. return $totalColumnCount;
  431. }
  432. /**
  433. * Provides search form's row containing column select options
  434. *
  435. * @return string HTML for search table's row
  436. */
  437. private function getColumnNamesRow()
  438. {
  439. $htmlOutput = '';
  440. $newColumnCount = 0;
  441. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  442. if (
  443. isset($this->criteriaColumnInsert[$columnIndex])
  444. && $this->criteriaColumnInsert[$columnIndex] === 'on'
  445. ) {
  446. $htmlOutput .= $this->showColumnSelectCell($newColumnCount);
  447. $newColumnCount++;
  448. }
  449. if (
  450. ! empty($this->criteriaColumnDelete)
  451. && isset($this->criteriaColumnDelete[$columnIndex])
  452. && $this->criteriaColumnDelete[$columnIndex] === 'on'
  453. ) {
  454. continue;
  455. }
  456. $selected = '';
  457. if (isset($_POST['criteriaColumn'][$columnIndex])) {
  458. $selected = $_POST['criteriaColumn'][$columnIndex];
  459. $this->formColumns[$newColumnCount] = $_POST['criteriaColumn'][$columnIndex];
  460. }
  461. $htmlOutput .= $this->showColumnSelectCell($newColumnCount, $selected);
  462. $newColumnCount++;
  463. }
  464. $this->newColumnCount = $newColumnCount;
  465. return $htmlOutput;
  466. }
  467. /**
  468. * Provides search form's row containing column aliases
  469. *
  470. * @return string HTML for search table's row
  471. */
  472. private function getColumnAliasRow()
  473. {
  474. $htmlOutput = '';
  475. $newColumnCount = 0;
  476. for ($colInd = 0; $colInd < $this->criteriaColumnCount; $colInd++) {
  477. if (
  478. ! empty($this->criteriaColumnInsert)
  479. && isset($this->criteriaColumnInsert[$colInd])
  480. && $this->criteriaColumnInsert[$colInd] === 'on'
  481. ) {
  482. $htmlOutput .= '<td class="text-center">';
  483. $htmlOutput .= '<input type="text"'
  484. . ' name="criteriaAlias[' . $newColumnCount . ']">';
  485. $htmlOutput .= '</td>';
  486. $newColumnCount++;
  487. }
  488. if (
  489. ! empty($this->criteriaColumnDelete)
  490. && isset($this->criteriaColumnDelete[$colInd])
  491. && $this->criteriaColumnDelete[$colInd] === 'on'
  492. ) {
  493. continue;
  494. }
  495. $tmpAlias = '';
  496. if (! empty($_POST['criteriaAlias'][$colInd])) {
  497. $tmpAlias = $this->formAliases[$newColumnCount] = $_POST['criteriaAlias'][$colInd];
  498. }
  499. $htmlOutput .= '<td class="text-center">';
  500. $htmlOutput .= '<input type="text"'
  501. . ' name="criteriaAlias[' . $newColumnCount . ']"'
  502. . ' value="' . htmlspecialchars($tmpAlias) . '">';
  503. $htmlOutput .= '</td>';
  504. $newColumnCount++;
  505. }
  506. return $htmlOutput;
  507. }
  508. /**
  509. * Provides search form's row containing sort(ASC/DESC) select options
  510. *
  511. * @return string HTML for search table's row
  512. */
  513. private function getSortRow()
  514. {
  515. $htmlOutput = '';
  516. $newColumnCount = 0;
  517. for ($colInd = 0; $colInd < $this->criteriaColumnCount; $colInd++) {
  518. if (
  519. ! empty($this->criteriaColumnInsert)
  520. && isset($this->criteriaColumnInsert[$colInd])
  521. && $this->criteriaColumnInsert[$colInd] === 'on'
  522. ) {
  523. $htmlOutput .= $this->getSortSelectCell($newColumnCount);
  524. $newColumnCount++;
  525. }
  526. if (
  527. ! empty($this->criteriaColumnDelete)
  528. && isset($this->criteriaColumnDelete[$colInd])
  529. && $this->criteriaColumnDelete[$colInd] === 'on'
  530. ) {
  531. continue;
  532. }
  533. // If they have chosen all fields using the * selector,
  534. // then sorting is not available, Fix for Bug #570698
  535. if (
  536. isset($_POST['criteriaSort'][$colInd], $_POST['criteriaColumn'][$colInd])
  537. && mb_substr($_POST['criteriaColumn'][$colInd], -2) === '.*'
  538. ) {
  539. $_POST['criteriaSort'][$colInd] = '';
  540. }
  541. $selected = '';
  542. if (isset($_POST['criteriaSort'][$colInd])) {
  543. $this->formSorts[$newColumnCount] = $_POST['criteriaSort'][$colInd];
  544. if ($_POST['criteriaSort'][$colInd] === 'ASC') {
  545. $selected = 'ASC';
  546. } elseif ($_POST['criteriaSort'][$colInd] === 'DESC') {
  547. $selected = 'DESC';
  548. }
  549. } else {
  550. $this->formSorts[$newColumnCount] = '';
  551. }
  552. $htmlOutput .= $this->getSortSelectCell($newColumnCount, $selected);
  553. $newColumnCount++;
  554. }
  555. return $htmlOutput;
  556. }
  557. /**
  558. * Provides search form's row containing sort order
  559. *
  560. * @return string HTML for search table's row
  561. */
  562. private function getSortOrder()
  563. {
  564. $htmlOutput = '';
  565. $newColumnCount = 0;
  566. for ($colInd = 0; $colInd < $this->criteriaColumnCount; $colInd++) {
  567. if (
  568. ! empty($this->criteriaColumnInsert)
  569. && isset($this->criteriaColumnInsert[$colInd])
  570. && $this->criteriaColumnInsert[$colInd] === 'on'
  571. ) {
  572. $htmlOutput .= $this->getSortOrderSelectCell($newColumnCount, null);
  573. $newColumnCount++;
  574. }
  575. if (
  576. ! empty($this->criteriaColumnDelete)
  577. && isset($this->criteriaColumnDelete[$colInd])
  578. && $this->criteriaColumnDelete[$colInd] === 'on'
  579. ) {
  580. continue;
  581. }
  582. $sortOrder = null;
  583. if (! empty($_POST['criteriaSortOrder'][$colInd])) {
  584. $sortOrder = $this->formSortOrders[$newColumnCount] = $_POST['criteriaSortOrder'][$colInd];
  585. }
  586. $htmlOutput .= $this->getSortOrderSelectCell($newColumnCount, $sortOrder);
  587. $newColumnCount++;
  588. }
  589. return $htmlOutput;
  590. }
  591. /**
  592. * Provides search form's row containing SHOW checkboxes
  593. *
  594. * @return string HTML for search table's row
  595. */
  596. private function getShowRow()
  597. {
  598. $htmlOutput = '';
  599. $newColumnCount = 0;
  600. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  601. if (
  602. ! empty($this->criteriaColumnInsert)
  603. && isset($this->criteriaColumnInsert[$columnIndex])
  604. && $this->criteriaColumnInsert[$columnIndex] === 'on'
  605. ) {
  606. $htmlOutput .= '<td class="text-center">';
  607. $htmlOutput .= '<input type="checkbox"'
  608. . ' name="criteriaShow[' . $newColumnCount . ']">';
  609. $htmlOutput .= '</td>';
  610. $newColumnCount++;
  611. }
  612. if (
  613. ! empty($this->criteriaColumnDelete)
  614. && isset($this->criteriaColumnDelete[$columnIndex])
  615. && $this->criteriaColumnDelete[$columnIndex] === 'on'
  616. ) {
  617. continue;
  618. }
  619. if (isset($_POST['criteriaShow'][$columnIndex])) {
  620. $checkedOptions = ' checked="checked"';
  621. $this->formShows[$newColumnCount] = $_POST['criteriaShow'][$columnIndex];
  622. } else {
  623. $checkedOptions = '';
  624. }
  625. $htmlOutput .= '<td class="text-center">';
  626. $htmlOutput .= '<input type="checkbox"'
  627. . ' name="criteriaShow[' . $newColumnCount . ']"'
  628. . $checkedOptions . '>';
  629. $htmlOutput .= '</td>';
  630. $newColumnCount++;
  631. }
  632. return $htmlOutput;
  633. }
  634. /**
  635. * Provides search form's row containing criteria Inputboxes
  636. *
  637. * @return string HTML for search table's row
  638. */
  639. private function getCriteriaInputboxRow()
  640. {
  641. $htmlOutput = '';
  642. $newColumnCount = 0;
  643. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  644. if (
  645. ! empty($this->criteriaColumnInsert)
  646. && isset($this->criteriaColumnInsert[$columnIndex])
  647. && $this->criteriaColumnInsert[$columnIndex] === 'on'
  648. ) {
  649. $htmlOutput .= '<td class="text-center">';
  650. $htmlOutput .= '<input type="text"'
  651. . ' name="criteria[' . $newColumnCount . ']"'
  652. . ' class="textfield"'
  653. . ' style="width: ' . $this->realwidth . '"'
  654. . ' size="20">';
  655. $htmlOutput .= '</td>';
  656. $newColumnCount++;
  657. }
  658. if (
  659. ! empty($this->criteriaColumnDelete)
  660. && isset($this->criteriaColumnDelete[$columnIndex])
  661. && $this->criteriaColumnDelete[$columnIndex] === 'on'
  662. ) {
  663. continue;
  664. }
  665. $tmpCriteria = '';
  666. if (isset($this->criteria[$columnIndex])) {
  667. $tmpCriteria = $this->criteria[$columnIndex];
  668. }
  669. if (
  670. (empty($this->prevCriteria)
  671. || ! isset($this->prevCriteria[$columnIndex]))
  672. || $this->prevCriteria[$columnIndex] != htmlspecialchars($tmpCriteria)
  673. ) {
  674. $this->formCriterions[$newColumnCount] = $tmpCriteria;
  675. } else {
  676. $this->formCriterions[$newColumnCount] = $this->prevCriteria[$columnIndex];
  677. }
  678. $htmlOutput .= '<td class="text-center">';
  679. $htmlOutput .= '<input type="hidden"'
  680. . ' name="prev_criteria[' . $newColumnCount . ']"'
  681. . ' value="'
  682. . htmlspecialchars($this->formCriterions[$newColumnCount])
  683. . '">';
  684. $htmlOutput .= '<input type="text"'
  685. . ' name="criteria[' . $newColumnCount . ']"'
  686. . ' value="' . htmlspecialchars($tmpCriteria) . '"'
  687. . ' class="textfield"'
  688. . ' style="width: ' . $this->realwidth . '"'
  689. . ' size="20">';
  690. $htmlOutput .= '</td>';
  691. $newColumnCount++;
  692. }
  693. return $htmlOutput;
  694. }
  695. /**
  696. * Provides And/Or modification cell along with Insert/Delete options
  697. * (For modifying search form's table columns)
  698. *
  699. * @param int $columnNumber Column Number (0,1,2) or more
  700. * @param array|null $selected Selected criteria column name
  701. * @param bool $lastColumn Whether this is the last column
  702. *
  703. * @return string HTML for modification cell
  704. */
  705. private function getAndOrColCell(
  706. $columnNumber,
  707. $selected = null,
  708. $lastColumn = false
  709. ) {
  710. $htmlOutput = '<td class="text-center">';
  711. if (! $lastColumn) {
  712. $htmlOutput .= '<strong>' . __('Or:') . '</strong>';
  713. $htmlOutput .= '<input type="radio"'
  714. . ' name="criteriaAndOrColumn[' . $columnNumber . ']"'
  715. . ' value="or"' . ($selected['or'] ?? '') . '>';
  716. $htmlOutput .= '&nbsp;&nbsp;<strong>' . __('And:') . '</strong>';
  717. $htmlOutput .= '<input type="radio"'
  718. . ' name="criteriaAndOrColumn[' . $columnNumber . ']"'
  719. . ' value="and"' . ($selected['and'] ?? '') . '>';
  720. }
  721. $htmlOutput .= '<br>' . __('Ins');
  722. $htmlOutput .= '<input type="checkbox"'
  723. . ' name="criteriaColumnInsert[' . $columnNumber . ']">';
  724. $htmlOutput .= '&nbsp;&nbsp;' . __('Del');
  725. $htmlOutput .= '<input type="checkbox"'
  726. . ' name="criteriaColumnDelete[' . $columnNumber . ']">';
  727. $htmlOutput .= '</td>';
  728. return $htmlOutput;
  729. }
  730. /**
  731. * Provides search form's row containing column modifications options
  732. * (For modifying search form's table columns)
  733. *
  734. * @return string HTML for search table's row
  735. */
  736. private function getModifyColumnsRow()
  737. {
  738. $htmlOutput = '';
  739. $newColumnCount = 0;
  740. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  741. if (
  742. ! empty($this->criteriaColumnInsert)
  743. && isset($this->criteriaColumnInsert[$columnIndex])
  744. && $this->criteriaColumnInsert[$columnIndex] === 'on'
  745. ) {
  746. $htmlOutput .= $this->getAndOrColCell($newColumnCount);
  747. $newColumnCount++;
  748. }
  749. if (
  750. ! empty($this->criteriaColumnDelete)
  751. && isset($this->criteriaColumnDelete[$columnIndex])
  752. && $this->criteriaColumnDelete[$columnIndex] === 'on'
  753. ) {
  754. continue;
  755. }
  756. if (isset($this->criteriaAndOrColumn[$columnIndex])) {
  757. $this->formAndOrCols[$newColumnCount] = $this->criteriaAndOrColumn[$columnIndex];
  758. }
  759. $checkedOptions = [];
  760. if (isset($this->criteriaAndOrColumn[$columnIndex]) && $this->criteriaAndOrColumn[$columnIndex] === 'or') {
  761. $checkedOptions['or'] = ' checked="checked"';
  762. $checkedOptions['and'] = '';
  763. } else {
  764. $checkedOptions['and'] = ' checked="checked"';
  765. $checkedOptions['or'] = '';
  766. }
  767. $htmlOutput .= $this->getAndOrColCell(
  768. $newColumnCount,
  769. $checkedOptions,
  770. $columnIndex + 1 == $this->criteriaColumnCount
  771. );
  772. $newColumnCount++;
  773. }
  774. return $htmlOutput;
  775. }
  776. /**
  777. * Provides rows for criteria inputbox Insert/Delete options
  778. * with AND/OR relationship modification options
  779. *
  780. * @param int $newRowIndex New row index if rows are added/deleted
  781. *
  782. * @return string HTML table rows
  783. */
  784. private function getInputboxRow($newRowIndex)
  785. {
  786. $htmlOutput = '';
  787. $newColumnCount = 0;
  788. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  789. if (
  790. ! empty($this->criteriaColumnInsert)
  791. && isset($this->criteriaColumnInsert[$columnIndex])
  792. && $this->criteriaColumnInsert[$columnIndex] === 'on'
  793. ) {
  794. $orFieldName = 'Or' . $newRowIndex . '[' . $newColumnCount . ']';
  795. $htmlOutput .= '<td class="text-center">';
  796. $htmlOutput .= '<input type="text"'
  797. . ' name="Or' . $orFieldName . '" class="textfield"'
  798. . ' style="width: ' . $this->realwidth . '" size="20">';
  799. $htmlOutput .= '</td>';
  800. $newColumnCount++;
  801. }
  802. if (
  803. ! empty($this->criteriaColumnDelete)
  804. && isset($this->criteriaColumnDelete[$columnIndex])
  805. && $this->criteriaColumnDelete[$columnIndex] === 'on'
  806. ) {
  807. continue;
  808. }
  809. $or = 'Or' . $newRowIndex;
  810. if (! empty($_POST[$or]) && isset($_POST[$or][$columnIndex])) {
  811. $tmpOr = $_POST[$or][$columnIndex];
  812. } else {
  813. $tmpOr = '';
  814. }
  815. $htmlOutput .= '<td class="text-center">';
  816. $htmlOutput .= '<input type="text"'
  817. . ' name="Or' . $newRowIndex . '[' . $newColumnCount . ']"'
  818. . ' value="' . htmlspecialchars($tmpOr) . '" class="textfield"'
  819. . ' style="width: ' . $this->realwidth . '" size="20">';
  820. $htmlOutput .= '</td>';
  821. if (! empty(${$or}) && isset(${$or}[$columnIndex])) {
  822. $GLOBALS[${'cur' . $or}][$newColumnCount] = ${$or}[$columnIndex];
  823. }
  824. $newColumnCount++;
  825. }
  826. return $htmlOutput;
  827. }
  828. /**
  829. * Provides rows for criteria inputbox Insert/Delete options
  830. * with AND/OR relationship modification options
  831. *
  832. * @return string HTML table rows
  833. */
  834. private function getInsDelAndOrCriteriaRows()
  835. {
  836. $htmlOutput = '';
  837. $newRowCount = 0;
  838. $checkedOptions = [];
  839. for ($rowIndex = 0; $rowIndex <= $this->criteriaRowCount; $rowIndex++) {
  840. if (isset($this->criteriaRowInsert[$rowIndex]) && $this->criteriaRowInsert[$rowIndex] === 'on') {
  841. $checkedOptions['or'] = true;
  842. $checkedOptions['and'] = false;
  843. $htmlOutput .= '<tr class="noclick">';
  844. $htmlOutput .= $this->template->render('database/qbe/ins_del_and_or_cell', [
  845. 'row_index' => $newRowCount,
  846. 'checked_options' => $checkedOptions,
  847. ]);
  848. $htmlOutput .= $this->getInputboxRow($newRowCount);
  849. $newRowCount++;
  850. $htmlOutput .= '</tr>';
  851. }
  852. if (isset($this->criteriaRowDelete[$rowIndex]) && $this->criteriaRowDelete[$rowIndex] === 'on') {
  853. continue;
  854. }
  855. if (isset($this->criteriaAndOrRow[$rowIndex])) {
  856. $this->formAndOrRows[$newRowCount] = $this->criteriaAndOrRow[$rowIndex];
  857. }
  858. if (isset($this->criteriaAndOrRow[$rowIndex]) && $this->criteriaAndOrRow[$rowIndex] === 'and') {
  859. $checkedOptions['and'] = true;
  860. $checkedOptions['or'] = false;
  861. } else {
  862. $checkedOptions['or'] = true;
  863. $checkedOptions['and'] = false;
  864. }
  865. $htmlOutput .= '<tr class="noclick">';
  866. $htmlOutput .= $this->template->render('database/qbe/ins_del_and_or_cell', [
  867. 'row_index' => $newRowCount,
  868. 'checked_options' => $checkedOptions,
  869. ]);
  870. $htmlOutput .= $this->getInputboxRow($newRowCount);
  871. $newRowCount++;
  872. $htmlOutput .= '</tr>';
  873. }
  874. $this->newRowCount = $newRowCount;
  875. return $htmlOutput;
  876. }
  877. /**
  878. * Provides SELECT clause for building SQL query
  879. *
  880. * @return string Select clause
  881. */
  882. private function getSelectClause()
  883. {
  884. $selectClause = '';
  885. $selectClauses = [];
  886. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  887. if (
  888. empty($this->formColumns[$columnIndex])
  889. || ! isset($this->formShows[$columnIndex])
  890. || $this->formShows[$columnIndex] !== 'on'
  891. ) {
  892. continue;
  893. }
  894. $select = $this->formColumns[$columnIndex];
  895. if (! empty($this->formAliases[$columnIndex])) {
  896. $select .= ' AS '
  897. . Util::backquote($this->formAliases[$columnIndex]);
  898. }
  899. $selectClauses[] = $select;
  900. }
  901. if (! empty($selectClauses)) {
  902. $selectClause = 'SELECT '
  903. . htmlspecialchars(implode(', ', $selectClauses)) . "\n";
  904. }
  905. return $selectClause;
  906. }
  907. /**
  908. * Provides WHERE clause for building SQL query
  909. *
  910. * @return string Where clause
  911. */
  912. private function getWhereClause()
  913. {
  914. $whereClause = '';
  915. $criteriaCount = 0;
  916. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  917. if (
  918. isset($lastWhere, $this->formAndOrCols)
  919. && ! empty($this->formColumns[$columnIndex])
  920. && ! empty($this->formCriterions[$columnIndex])
  921. && $columnIndex
  922. ) {
  923. $whereClause .= ' '
  924. . mb_strtoupper($this->formAndOrCols[$lastWhere])
  925. . ' ';
  926. }
  927. if (empty($this->formColumns[$columnIndex]) || empty($this->formCriterions[$columnIndex])) {
  928. continue;
  929. }
  930. $whereClause .= '(' . $this->formColumns[$columnIndex] . ' '
  931. . $this->formCriterions[$columnIndex] . ')';
  932. $lastWhere = $columnIndex;
  933. $criteriaCount++;
  934. }
  935. if ($criteriaCount > 1) {
  936. $whereClause = '(' . $whereClause . ')';
  937. }
  938. // OR rows ${'cur' . $or}[$column_index]
  939. if (! isset($this->formAndOrRows)) {
  940. $this->formAndOrRows = [];
  941. }
  942. for ($rowIndex = 0; $rowIndex <= $this->criteriaRowCount; $rowIndex++) {
  943. $criteriaCount = 0;
  944. $queryOrWhere = '';
  945. $lastOrWhere = '';
  946. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  947. if (
  948. ! empty($this->formColumns[$columnIndex])
  949. && ! empty($_POST['Or' . $rowIndex][$columnIndex])
  950. && $columnIndex
  951. ) {
  952. $queryOrWhere .= ' '
  953. . mb_strtoupper($this->formAndOrCols[$lastOrWhere])
  954. . ' ';
  955. }
  956. if (empty($this->formColumns[$columnIndex]) || empty($_POST['Or' . $rowIndex][$columnIndex])) {
  957. continue;
  958. }
  959. $queryOrWhere .= '(' . $this->formColumns[$columnIndex]
  960. . ' '
  961. . $_POST['Or' . $rowIndex][$columnIndex]
  962. . ')';
  963. $lastOrWhere = $columnIndex;
  964. $criteriaCount++;
  965. }
  966. if ($criteriaCount > 1) {
  967. $queryOrWhere = '(' . $queryOrWhere . ')';
  968. }
  969. if (empty($queryOrWhere)) {
  970. continue;
  971. }
  972. $whereClause .= "\n"
  973. . mb_strtoupper(isset($this->formAndOrRows[$rowIndex]) ? $this->formAndOrRows[$rowIndex] . ' ' : '')
  974. . $queryOrWhere;
  975. }
  976. if (! empty($whereClause) && $whereClause !== '()') {
  977. $whereClause = 'WHERE ' . $whereClause . "\n";
  978. }
  979. return $whereClause;
  980. }
  981. /**
  982. * Provides ORDER BY clause for building SQL query
  983. *
  984. * @return string Order By clause
  985. */
  986. private function getOrderByClause()
  987. {
  988. $orderByClause = '';
  989. $orderByClauses = [];
  990. // Create copy of instance variables
  991. $columns = $this->formColumns;
  992. $sort = $this->formSorts;
  993. $sortOrder = $this->formSortOrders;
  994. if (! empty($sortOrder) && count($sortOrder) == count($sort) && count($sortOrder) == count($columns)) {
  995. // Sort all three arrays based on sort order
  996. array_multisort($sortOrder, $sort, $columns);
  997. }
  998. for ($columnIndex = 0; $columnIndex < $this->criteriaColumnCount; $columnIndex++) {
  999. // if all columns are chosen with * selector,
  1000. // then sorting isn't available
  1001. // Fix for Bug #570698
  1002. if (empty($columns[$columnIndex]) && empty($sort[$columnIndex])) {
  1003. continue;
  1004. }
  1005. if (mb_substr($columns[$columnIndex], -2) === '.*') {
  1006. continue;
  1007. }
  1008. if (empty($sort[$columnIndex])) {
  1009. continue;
  1010. }
  1011. $orderByClauses[] = $columns[$columnIndex] . ' '
  1012. . $sort[$columnIndex];
  1013. }
  1014. if (! empty($orderByClauses)) {
  1015. $orderByClause = 'ORDER BY '
  1016. . htmlspecialchars(implode(', ', $orderByClauses)) . "\n";
  1017. }
  1018. return $orderByClause;
  1019. }
  1020. /**
  1021. * Provides UNIQUE columns and INDEX columns present in criteria tables
  1022. *
  1023. * @param array $searchTables Tables involved in the search
  1024. * @param array $searchColumns Columns involved in the search
  1025. * @param array $whereClauseColumns Columns having criteria where clause
  1026. *
  1027. * @return array having UNIQUE and INDEX columns
  1028. */
  1029. private function getIndexes(
  1030. array $searchTables,
  1031. array $searchColumns,
  1032. array $whereClauseColumns
  1033. ) {
  1034. $uniqueColumns = [];
  1035. $indexColumns = [];
  1036. foreach ($searchTables as $table) {
  1037. $indexes = $this->dbi->getTableIndexes($this->db, $table);
  1038. foreach ($indexes as $index) {
  1039. $column = $table . '.' . $index['Column_name'];
  1040. if (! isset($searchColumns[$column])) {
  1041. continue;
  1042. }
  1043. if ($index['Non_unique'] == 0) {
  1044. if (isset($whereClauseColumns[$column])) {
  1045. $uniqueColumns[$column] = 'Y';
  1046. } else {
  1047. $uniqueColumns[$column] = 'N';
  1048. }
  1049. } else {
  1050. if (isset($whereClauseColumns[$column])) {
  1051. $indexColumns[$column] = 'Y';
  1052. } else {
  1053. $indexColumns[$column] = 'N';
  1054. }
  1055. }
  1056. }
  1057. }
  1058. return [
  1059. 'unique' => $uniqueColumns,
  1060. 'index' => $indexColumns,
  1061. ];
  1062. }
  1063. /**
  1064. * Provides UNIQUE columns and INDEX columns present in criteria tables
  1065. *
  1066. * @param array $searchTables Tables involved in the search
  1067. * @param array $searchColumns Columns involved in the search
  1068. * @param array $whereClauseColumns Columns having criteria where clause
  1069. *
  1070. * @return array having UNIQUE and INDEX columns
  1071. */
  1072. private function getLeftJoinColumnCandidates(
  1073. array $searchTables,
  1074. array $searchColumns,
  1075. array $whereClauseColumns
  1076. ) {
  1077. $this->dbi->selectDb($this->db);
  1078. // Get unique columns and index columns
  1079. $indexes = $this->getIndexes($searchTables, $searchColumns, $whereClauseColumns);
  1080. $uniqueColumns = $indexes['unique'];
  1081. $indexColumns = $indexes['index'];
  1082. [$candidateColumns, $needSort] = $this->getLeftJoinColumnCandidatesBest(
  1083. $searchTables,
  1084. $whereClauseColumns,
  1085. $uniqueColumns,
  1086. $indexColumns
  1087. );
  1088. // If we came up with $unique_columns (very good) or $index_columns (still
  1089. // good) as $candidate_columns we want to check if we have any 'Y' there
  1090. // (that would mean that they were also found in the whereclauses
  1091. // which would be great). if yes, we take only those
  1092. if ($needSort != 1) {
  1093. return $candidateColumns;
  1094. }
  1095. $veryGood = [];
  1096. $stillGood = [];
  1097. foreach ($candidateColumns as $column => $isWhere) {
  1098. $table = explode('.', $column);
  1099. $table = $table[0];
  1100. if ($isWhere === 'Y') {
  1101. $veryGood[$column] = $table;
  1102. } else {
  1103. $stillGood[$column] = $table;
  1104. }
  1105. }
  1106. if (count($veryGood) > 0) {
  1107. $candidateColumns = $veryGood;
  1108. // Candidates restricted in index+where
  1109. } else {
  1110. $candidateColumns = $stillGood;
  1111. // None of the candidates where in a where-clause
  1112. }
  1113. return $candidateColumns;
  1114. }
  1115. /**
  1116. * Provides the main table to form the LEFT JOIN clause
  1117. *
  1118. * @param array $searchTables Tables involved in the search
  1119. * @param array $searchColumns Columns involved in the search
  1120. * @param array $whereClauseColumns Columns having criteria where clause
  1121. * @param array $whereClauseTables Tables having criteria where clause
  1122. *
  1123. * @return string table name
  1124. */
  1125. private function getMasterTable(
  1126. array $searchTables,
  1127. array $searchColumns,
  1128. array $whereClauseColumns,
  1129. array $whereClauseTables
  1130. ) {
  1131. if (count($whereClauseTables) === 1) {
  1132. // If there is exactly one column that has a decent where-clause
  1133. // we will just use this
  1134. return key($whereClauseTables);
  1135. }
  1136. // Now let's find out which of the tables has an index
  1137. // (When the control user is the same as the normal user
  1138. // because they are using one of their databases as pmadb,
  1139. // the last db selected is not always the one where we need to work)
  1140. $candidateColumns = $this->getLeftJoinColumnCandidates($searchTables, $searchColumns, $whereClauseColumns);
  1141. // Generally, we need to display all the rows of foreign (referenced)
  1142. // table, whether they have any matching row in child table or not.
  1143. // So we select candidate tables which are foreign tables.
  1144. $foreignTables = [];
  1145. foreach ($candidateColumns as $oneTable) {
  1146. $foreigners = $this->relation->getForeigners($this->db, $oneTable);
  1147. foreach ($foreigners as $key => $foreigner) {
  1148. if ($key !== 'foreign_keys_data') {
  1149. if (in_array($foreigner['foreign_table'], $candidateColumns)) {
  1150. $foreignTables[$foreigner['foreign_table']] = $foreigner['foreign_table'];
  1151. }
  1152. continue;
  1153. }
  1154. foreach ($foreigner as $oneKey) {
  1155. if (! in_array($oneKey['ref_table_name'], $candidateColumns)) {
  1156. continue;
  1157. }
  1158. $foreignTables[$oneKey['ref_table_name']] = $oneKey['ref_table_name'];
  1159. }
  1160. }
  1161. }
  1162. if (count($foreignTables)) {
  1163. $candidateColumns = $foreignTables;
  1164. }
  1165. // If our array of candidates has more than one member we'll just
  1166. // find the smallest table.
  1167. // Of course the actual query would be faster if we check for
  1168. // the Criteria which gives the smallest result set in its table,
  1169. // but it would take too much time to check this
  1170. if (! (count($candidateColumns) > 1)) {
  1171. // Only one single candidate
  1172. return reset($candidateColumns);
  1173. }
  1174. // Of course we only want to check each table once
  1175. $checkedTables = $candidateColumns;
  1176. $tsize = [];
  1177. $maxsize = -1;
  1178. $result = '';
  1179. foreach ($candidateColumns as $table) {
  1180. if ($checkedTables[$table] != 1) {
  1181. $tableObj = new Table($table, $this->db);
  1182. $tsize[$table] = $tableObj->countRecords();
  1183. $checkedTables[$table] = 1;
  1184. }
  1185. if ($tsize[$table] <= $maxsize) {
  1186. continue;
  1187. }
  1188. $maxsize = $tsize[$table];
  1189. $result = $table;
  1190. }
  1191. // Return largest table
  1192. return $result;
  1193. }
  1194. /**
  1195. * Provides columns and tables that have valid where clause criteria
  1196. *
  1197. * @return array
  1198. */
  1199. private function getWhereClauseTablesAndColumns()
  1200. {
  1201. $whereClauseColumns = [];
  1202. $whereClauseTables = [];
  1203. // Now we need all tables that we have in the where clause
  1204. for ($columnIndex = 0, $nb = count($this->criteria); $columnIndex < $nb; $columnIndex++) {
  1205. $currentTable = explode('.', $_POST['criteriaColumn'][$columnIndex]);
  1206. if (empty($currentTable[0]) || empty($currentTable[1])) {
  1207. continue;
  1208. }
  1209. $table = str_replace('`', '', $currentTable[0]);
  1210. $column = str_replace('`', '', $currentTable[1]);
  1211. $column = $table . '.' . $column;
  1212. // Now we know that our array has the same numbers as $criteria
  1213. // we can check which of our columns has a where clause
  1214. if (empty($this->criteria[$columnIndex])) {
  1215. continue;
  1216. }
  1217. if (
  1218. mb_substr($this->criteria[$columnIndex], 0, 1) !== '='
  1219. && stripos($this->criteria[$columnIndex], 'is') === false
  1220. ) {
  1221. continue;
  1222. }
  1223. $whereClauseColumns[$column] = $column;
  1224. $whereClauseTables[$table] = $table;
  1225. }
  1226. return [
  1227. 'where_clause_tables' => $whereClauseTables,
  1228. 'where_clause_columns' => $whereClauseColumns,
  1229. ];
  1230. }
  1231. /**
  1232. * Provides FROM clause for building SQL query
  1233. *
  1234. * @param array $formColumns List of selected columns in the form
  1235. *
  1236. * @return string FROM clause
  1237. */
  1238. private function getFromClause(array $formColumns)
  1239. {
  1240. $fromClause = '';
  1241. if (empty($formColumns)) {
  1242. return $fromClause;
  1243. }
  1244. // Initialize some variables
  1245. $searchTables = $searchColumns = [];
  1246. // We only start this if we have fields, otherwise it would be dumb
  1247. foreach ($formColumns as $value) {
  1248. $parts = explode('.', $value);
  1249. if (empty($parts[0]) || empty($parts[1])) {
  1250. continue;
  1251. }
  1252. $table = str_replace('`', '', $parts[0]);
  1253. $searchTables[$table] = $table;
  1254. $searchColumns[] = $table . '.' . str_replace('`', '', $parts[1]);
  1255. }
  1256. // Create LEFT JOINS out of Relations
  1257. $fromClause = $this->getJoinForFromClause($searchTables, $searchColumns);
  1258. // In case relations are not defined, just generate the FROM clause
  1259. // from the list of tables, however we don't generate any JOIN
  1260. if (empty($fromClause)) {
  1261. // Create cartesian product
  1262. $fromClause = implode(
  1263. ', ',
  1264. array_map([Util::class, 'backquote'], $searchTables)
  1265. );
  1266. }
  1267. return $fromClause;
  1268. }
  1269. /**
  1270. * Formulates the WHERE clause by JOINing tables
  1271. *
  1272. * @param array $searchTables Tables involved in the search
  1273. * @param array $searchColumns Columns involved in the search
  1274. *
  1275. * @return string table name
  1276. */
  1277. private function getJoinForFromClause(array $searchTables, array $searchColumns)
  1278. {
  1279. // $relations[master_table][foreign_table] => clause
  1280. $relations = [];
  1281. // Fill $relations with inter table relationship data
  1282. foreach ($searchTables as $oneTable) {
  1283. $this->loadRelationsForTable($relations, $oneTable);
  1284. }
  1285. // Get tables and columns with valid where clauses
  1286. $validWhereClauses = $this->getWhereClauseTablesAndColumns();
  1287. $whereClauseTables = $validWhereClauses['where_clause_tables'];
  1288. $whereClauseColumns = $validWhereClauses['where_clause_columns'];
  1289. // Get master table
  1290. $master = $this->getMasterTable($searchTables, $searchColumns, $whereClauseColumns, $whereClauseTables);
  1291. // Will include master tables and all tables that can be combined into
  1292. // a cluster by their relation
  1293. $finalized = [];
  1294. if (strlen((string) $master) > 0) {
  1295. // Add master tables
  1296. $finalized[$master] = '';
  1297. }
  1298. // Fill the $finalized array with JOIN clauses for each table
  1299. $this->fillJoinClauses($finalized, $relations, $searchTables);
  1300. // JOIN clause
  1301. $join = '';
  1302. // Tables that can not be combined with the table cluster
  1303. // which includes master table
  1304. $unfinalized = array_diff($searchTables, array_keys($finalized));
  1305. if (count($unfinalized) > 0) {
  1306. // We need to look for intermediary tables to JOIN unfinalized tables
  1307. // Heuristic to chose intermediary tables is to look for tables
  1308. // having relationships with unfinalized tables
  1309. foreach ($unfinalized as $oneTable) {
  1310. $references = $this->relation->getChildReferences($this->db, $oneTable);
  1311. foreach ($references as $column => $columnReferences) {
  1312. foreach ($columnReferences as $reference) {
  1313. // Only from this schema
  1314. if ($reference['table_schema'] != $this->db) {
  1315. continue;
  1316. }
  1317. $table = $reference['table_name'];
  1318. $this->loadRelationsForTable($relations, $table);
  1319. // Make copies
  1320. $tempFinalized = $finalized;
  1321. $tempSearchTables = $searchTables;
  1322. $tempSearchTables[] = $table;
  1323. // Try joining with the added table
  1324. $this->fillJoinClauses($tempFinalized, $relations, $tempSearchTables);
  1325. $tempUnfinalized = array_diff(
  1326. $tempSearchTables,
  1327. array_keys($tempFinalized)
  1328. );
  1329. // Take greedy approach.
  1330. // If the unfinalized count drops we keep the new table
  1331. // and switch temporary varibles with the original ones
  1332. if (count($tempUnfinalized) < count($unfinalized)) {
  1333. $finalized = $tempFinalized;
  1334. $searchTables = $tempSearchTables;
  1335. }
  1336. // We are done if no unfinalized tables anymore
  1337. if (count($tempUnfinalized) === 0) {
  1338. break 3;
  1339. }
  1340. }
  1341. }
  1342. }
  1343. $unfinalized = array_diff($searchTables, array_keys($finalized));
  1344. // If there are still unfinalized tables
  1345. if (count($unfinalized) > 0) {
  1346. // Add these tables as cartesian product before joined tables
  1347. $join .= implode(
  1348. ', ',
  1349. array_map([Util::class, 'backquote'], $unfinalized)
  1350. );
  1351. }
  1352. }
  1353. $first = true;
  1354. // Add joined tables
  1355. foreach ($finalized as $table => $clause) {
  1356. if ($first) {
  1357. if (! empty(

Large files files are truncated, but you can click here to view the full file