Relation.php 77 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273
  1. <?php
  2. /**
  3. * Set of functions used with the relation and PDF feature
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\Html\MySQLDocumentation;
  8. use PhpMyAdmin\SqlParser\Parser;
  9. use PhpMyAdmin\SqlParser\Statements\CreateStatement;
  10. use PhpMyAdmin\SqlParser\Utils\Table as TableUtils;
  11. use function array_reverse;
  12. use function array_search;
  13. use function array_shift;
  14. use function asort;
  15. use function bin2hex;
  16. use function count;
  17. use function defined;
  18. use function explode;
  19. use function file_get_contents;
  20. use function htmlspecialchars;
  21. use function implode;
  22. use function in_array;
  23. use function is_array;
  24. use function is_bool;
  25. use function is_string;
  26. use function ksort;
  27. use function mb_check_encoding;
  28. use function mb_strlen;
  29. use function mb_strtolower;
  30. use function mb_strtoupper;
  31. use function mb_substr;
  32. use function natcasesort;
  33. use function preg_match;
  34. use function sprintf;
  35. use function str_replace;
  36. use function strlen;
  37. use function strpos;
  38. use function trim;
  39. use function uksort;
  40. use function usort;
  41. /**
  42. * Set of functions used with the relation and PDF feature
  43. */
  44. class Relation
  45. {
  46. /** @var DatabaseInterface */
  47. public $dbi;
  48. /** @var Template */
  49. public $template;
  50. /**
  51. * @param DatabaseInterface|null $dbi Database interface
  52. * @param Template|null $template Template instance
  53. */
  54. public function __construct(?DatabaseInterface $dbi, ?Template $template = null)
  55. {
  56. $this->dbi = $dbi;
  57. $this->template = $template ?? new Template();
  58. }
  59. /**
  60. * Executes a query as controluser if possible, otherwise as normal user
  61. *
  62. * @param string $sql the query to execute
  63. * @param bool $show_error whether to display SQL error messages or not
  64. * @param int $options query options
  65. *
  66. * @return mixed|bool the result set, or false if no result set
  67. *
  68. * @access public
  69. */
  70. public function queryAsControlUser($sql, $show_error = true, $options = 0)
  71. {
  72. // Avoid caching of the number of rows affected; for example, this function
  73. // is called for tracking purposes but we want to display the correct number
  74. // of rows affected by the original query, not by the query generated for
  75. // tracking.
  76. $cache_affected_rows = false;
  77. if ($show_error) {
  78. $result = $this->dbi->query(
  79. $sql,
  80. DatabaseInterface::CONNECT_CONTROL,
  81. $options,
  82. $cache_affected_rows
  83. );
  84. } else {
  85. $result = @$this->dbi->tryQuery(
  86. $sql,
  87. DatabaseInterface::CONNECT_CONTROL,
  88. $options,
  89. $cache_affected_rows
  90. );
  91. }
  92. if ($result) {
  93. return $result;
  94. }
  95. return false;
  96. }
  97. /**
  98. * Returns current relation parameters
  99. *
  100. * @return string[]
  101. */
  102. public function getRelationsParam(): array
  103. {
  104. if (empty($_SESSION['relation'][$GLOBALS['server']])
  105. || empty($_SESSION['relation'][$GLOBALS['server']]['PMA_VERSION'])
  106. || $_SESSION['relation'][$GLOBALS['server']]['PMA_VERSION'] != PMA_VERSION
  107. ) {
  108. $_SESSION['relation'][$GLOBALS['server']] = $this->checkRelationsParam();
  109. }
  110. // just for BC but needs to be before getRelationsParamDiagnostic()
  111. // which uses it
  112. $GLOBALS['cfgRelation'] = $_SESSION['relation'][$GLOBALS['server']];
  113. return $_SESSION['relation'][$GLOBALS['server']];
  114. }
  115. /**
  116. * prints out diagnostic info for pma relation feature
  117. *
  118. * @param array $cfgRelation Relation configuration
  119. *
  120. * @return string
  121. */
  122. public function getRelationsParamDiagnostic(array $cfgRelation)
  123. {
  124. $retval = '<br>';
  125. $messages = [];
  126. $messages['error'] = '<span class="caution"><strong>'
  127. . __('not OK')
  128. . '</strong></span>';
  129. $messages['ok'] = '<span class="success"><strong>'
  130. . _pgettext('Correctly working', 'OK')
  131. . '</strong></span>';
  132. $messages['enabled'] = '<span class="success">' . __('Enabled') . '</span>';
  133. $messages['disabled'] = '<span class="caution">' . __('Disabled') . '</span>';
  134. if (strlen((string) $cfgRelation['db']) == 0) {
  135. $retval .= __('Configuration of pmadb…') . ' '
  136. . $messages['error']
  137. . MySQLDocumentation::showDocumentation('setup', 'linked-tables')
  138. . '<br>' . "\n"
  139. . __('General relation features')
  140. . ' <font color="green">' . __('Disabled')
  141. . '</font>' . "\n";
  142. if ($GLOBALS['cfg']['ZeroConf']) {
  143. if (strlen($GLOBALS['db']) == 0) {
  144. $retval .= $this->getHtmlFixPmaTables(true, true);
  145. } else {
  146. $retval .= $this->getHtmlFixPmaTables(true);
  147. }
  148. }
  149. } else {
  150. $retval .= '<table class="pma-table">' . "\n";
  151. if (! $cfgRelation['allworks']
  152. && $GLOBALS['cfg']['ZeroConf']
  153. // Avoid showing a "Create missing tables" link if it's a
  154. // problem of missing definition
  155. && $this->arePmadbTablesDefined()
  156. ) {
  157. $retval .= $this->getHtmlFixPmaTables(false);
  158. $retval .= '<br>';
  159. }
  160. $retval .= $this->getDiagMessageForParameter(
  161. 'pmadb',
  162. $cfgRelation['db'],
  163. $messages,
  164. 'pmadb'
  165. );
  166. $retval .= $this->getDiagMessageForParameter(
  167. 'relation',
  168. isset($cfgRelation['relation']),
  169. $messages,
  170. 'relation'
  171. );
  172. $retval .= $this->getDiagMessageForFeature(
  173. __('General relation features'),
  174. 'relwork',
  175. $messages
  176. );
  177. $retval .= $this->getDiagMessageForParameter(
  178. 'table_info',
  179. isset($cfgRelation['table_info']),
  180. $messages,
  181. 'table_info'
  182. );
  183. $retval .= $this->getDiagMessageForFeature(
  184. __('Display Features'),
  185. 'displaywork',
  186. $messages
  187. );
  188. $retval .= $this->getDiagMessageForParameter(
  189. 'table_coords',
  190. isset($cfgRelation['table_coords']),
  191. $messages,
  192. 'table_coords'
  193. );
  194. $retval .= $this->getDiagMessageForParameter(
  195. 'pdf_pages',
  196. isset($cfgRelation['pdf_pages']),
  197. $messages,
  198. 'pdf_pages'
  199. );
  200. $retval .= $this->getDiagMessageForFeature(
  201. __('Designer and creation of PDFs'),
  202. 'pdfwork',
  203. $messages
  204. );
  205. $retval .= $this->getDiagMessageForParameter(
  206. 'column_info',
  207. isset($cfgRelation['column_info']),
  208. $messages,
  209. 'column_info'
  210. );
  211. $retval .= $this->getDiagMessageForFeature(
  212. __('Displaying Column Comments'),
  213. 'commwork',
  214. $messages,
  215. false
  216. );
  217. $retval .= $this->getDiagMessageForFeature(
  218. __('Browser transformation'),
  219. 'mimework',
  220. $messages
  221. );
  222. if ($cfgRelation['commwork'] && ! $cfgRelation['mimework']) {
  223. $retval .= '<tr><td colspan=2 class="left error">';
  224. $retval .= __(
  225. 'Please see the documentation on how to'
  226. . ' update your column_info table.'
  227. );
  228. $retval .= MySQLDocumentation::showDocumentation(
  229. 'config',
  230. 'cfg_Servers_column_info'
  231. );
  232. $retval .= '</td></tr>';
  233. }
  234. $retval .= $this->getDiagMessageForParameter(
  235. 'bookmarktable',
  236. isset($cfgRelation['bookmark']),
  237. $messages,
  238. 'bookmark'
  239. );
  240. $retval .= $this->getDiagMessageForFeature(
  241. __('Bookmarked SQL query'),
  242. 'bookmarkwork',
  243. $messages
  244. );
  245. $retval .= $this->getDiagMessageForParameter(
  246. 'history',
  247. isset($cfgRelation['history']),
  248. $messages,
  249. 'history'
  250. );
  251. $retval .= $this->getDiagMessageForFeature(
  252. __('SQL history'),
  253. 'historywork',
  254. $messages
  255. );
  256. $retval .= $this->getDiagMessageForParameter(
  257. 'recent',
  258. isset($cfgRelation['recent']),
  259. $messages,
  260. 'recent'
  261. );
  262. $retval .= $this->getDiagMessageForFeature(
  263. __('Persistent recently used tables'),
  264. 'recentwork',
  265. $messages
  266. );
  267. $retval .= $this->getDiagMessageForParameter(
  268. 'favorite',
  269. isset($cfgRelation['favorite']),
  270. $messages,
  271. 'favorite'
  272. );
  273. $retval .= $this->getDiagMessageForFeature(
  274. __('Persistent favorite tables'),
  275. 'favoritework',
  276. $messages
  277. );
  278. $retval .= $this->getDiagMessageForParameter(
  279. 'table_uiprefs',
  280. isset($cfgRelation['table_uiprefs']),
  281. $messages,
  282. 'table_uiprefs'
  283. );
  284. $retval .= $this->getDiagMessageForFeature(
  285. __('Persistent tables\' UI preferences'),
  286. 'uiprefswork',
  287. $messages
  288. );
  289. $retval .= $this->getDiagMessageForParameter(
  290. 'tracking',
  291. isset($cfgRelation['tracking']),
  292. $messages,
  293. 'tracking'
  294. );
  295. $retval .= $this->getDiagMessageForFeature(
  296. __('Tracking'),
  297. 'trackingwork',
  298. $messages
  299. );
  300. $retval .= $this->getDiagMessageForParameter(
  301. 'userconfig',
  302. isset($cfgRelation['userconfig']),
  303. $messages,
  304. 'userconfig'
  305. );
  306. $retval .= $this->getDiagMessageForFeature(
  307. __('User preferences'),
  308. 'userconfigwork',
  309. $messages
  310. );
  311. $retval .= $this->getDiagMessageForParameter(
  312. 'users',
  313. isset($cfgRelation['users']),
  314. $messages,
  315. 'users'
  316. );
  317. $retval .= $this->getDiagMessageForParameter(
  318. 'usergroups',
  319. isset($cfgRelation['usergroups']),
  320. $messages,
  321. 'usergroups'
  322. );
  323. $retval .= $this->getDiagMessageForFeature(
  324. __('Configurable menus'),
  325. 'menuswork',
  326. $messages
  327. );
  328. $retval .= $this->getDiagMessageForParameter(
  329. 'navigationhiding',
  330. isset($cfgRelation['navigationhiding']),
  331. $messages,
  332. 'navigationhiding'
  333. );
  334. $retval .= $this->getDiagMessageForFeature(
  335. __('Hide/show navigation items'),
  336. 'navwork',
  337. $messages
  338. );
  339. $retval .= $this->getDiagMessageForParameter(
  340. 'savedsearches',
  341. isset($cfgRelation['savedsearches']),
  342. $messages,
  343. 'savedsearches'
  344. );
  345. $retval .= $this->getDiagMessageForFeature(
  346. __('Saving Query-By-Example searches'),
  347. 'savedsearcheswork',
  348. $messages
  349. );
  350. $retval .= $this->getDiagMessageForParameter(
  351. 'central_columns',
  352. isset($cfgRelation['central_columns']),
  353. $messages,
  354. 'central_columns'
  355. );
  356. $retval .= $this->getDiagMessageForFeature(
  357. __('Managing Central list of columns'),
  358. 'centralcolumnswork',
  359. $messages
  360. );
  361. $retval .= $this->getDiagMessageForParameter(
  362. 'designer_settings',
  363. isset($cfgRelation['designer_settings']),
  364. $messages,
  365. 'designer_settings'
  366. );
  367. $retval .= $this->getDiagMessageForFeature(
  368. __('Remembering Designer Settings'),
  369. 'designersettingswork',
  370. $messages
  371. );
  372. $retval .= $this->getDiagMessageForParameter(
  373. 'export_templates',
  374. isset($cfgRelation['export_templates']),
  375. $messages,
  376. 'export_templates'
  377. );
  378. $retval .= $this->getDiagMessageForFeature(
  379. __('Saving export templates'),
  380. 'exporttemplateswork',
  381. $messages
  382. );
  383. $retval .= '</table>' . "\n";
  384. if (! $cfgRelation['allworks']) {
  385. $retval .= $this->template->render('relation/diagnostic_info', ['sql_dir' => SQL_DIR]);
  386. }
  387. }
  388. return $retval;
  389. }
  390. /**
  391. * prints out one diagnostic message for a feature
  392. *
  393. * @param string $feature_name feature name in a message string
  394. * @param string $relation_parameter the $GLOBALS['cfgRelation'] parameter to check
  395. * @param array $messages utility messages
  396. * @param bool $skip_line whether to skip a line after the message
  397. *
  398. * @return string
  399. */
  400. public function getDiagMessageForFeature(
  401. $feature_name,
  402. $relation_parameter,
  403. array $messages,
  404. $skip_line = true
  405. ) {
  406. $retval = ' <tr><td colspan=2 class="right">' . $feature_name . ': ';
  407. if (isset($GLOBALS['cfgRelation'][$relation_parameter])
  408. && $GLOBALS['cfgRelation'][$relation_parameter]
  409. ) {
  410. $retval .= $messages['enabled'];
  411. } else {
  412. $retval .= $messages['disabled'];
  413. }
  414. $retval .= '</td></tr>';
  415. if ($skip_line) {
  416. $retval .= '<tr><td>&nbsp;</td></tr>';
  417. }
  418. return $retval;
  419. }
  420. /**
  421. * prints out one diagnostic message for a configuration parameter
  422. *
  423. * @param string $parameter config parameter name to display
  424. * @param bool $relationParameterSet whether this parameter is set
  425. * @param array $messages utility messages
  426. * @param string $docAnchor anchor in documentation
  427. *
  428. * @return string
  429. */
  430. public function getDiagMessageForParameter(
  431. $parameter,
  432. $relationParameterSet,
  433. array $messages,
  434. $docAnchor
  435. ) {
  436. $retval = '<tr><th class="left">';
  437. $retval .= '$cfg[\'Servers\'][$i][\'' . $parameter . '\'] ... ';
  438. $retval .= '</th><td class="right">';
  439. if ($relationParameterSet) {
  440. $retval .= $messages['ok'];
  441. } else {
  442. $retval .= sprintf(
  443. $messages['error'],
  444. MySQLDocumentation::getDocumentationLink('config', 'cfg_Servers_' . $docAnchor)
  445. );
  446. }
  447. $retval .= '</td></tr>' . "\n";
  448. return $retval;
  449. }
  450. private function checkTableAccess(array $cfgRelation): array
  451. {
  452. if (isset($cfgRelation['relation'], $cfgRelation['table_info'])) {
  453. if ($this->canAccessStorageTable($cfgRelation['table_info'])) {
  454. $cfgRelation['displaywork'] = true;
  455. }
  456. }
  457. if (isset($cfgRelation['table_coords'], $cfgRelation['pdf_pages'])) {
  458. if ($this->canAccessStorageTable($cfgRelation['table_coords'])) {
  459. if ($this->canAccessStorageTable($cfgRelation['pdf_pages'])) {
  460. $cfgRelation['pdfwork'] = true;
  461. }
  462. }
  463. }
  464. if (isset($cfgRelation['column_info'])) {
  465. if ($this->canAccessStorageTable($cfgRelation['column_info'])) {
  466. $cfgRelation['commwork'] = true;
  467. // phpMyAdmin 4.3+
  468. // Check for input transformations upgrade.
  469. $cfgRelation['mimework'] = $this->tryUpgradeTransformations();
  470. }
  471. }
  472. if (isset($cfgRelation['users'], $cfgRelation['usergroups'])) {
  473. if ($this->canAccessStorageTable($cfgRelation['users'])) {
  474. if ($this->canAccessStorageTable($cfgRelation['usergroups'])) {
  475. $cfgRelation['menuswork'] = true;
  476. }
  477. }
  478. }
  479. $settings = [
  480. 'export_templates' => 'exporttemplateswork',
  481. 'designer_settings' => 'designersettingswork',
  482. 'central_columns' => 'centralcolumnswork',
  483. 'savedsearches' => 'savedsearcheswork',
  484. 'navigationhiding' => 'navwork',
  485. 'bookmark' => 'bookmarkwork',
  486. 'userconfig' => 'userconfigwork',
  487. 'tracking' => 'trackingwork',
  488. 'table_uiprefs' => 'uiprefswork',
  489. 'favorite' => 'favoritework',
  490. 'recent' => 'recentwork',
  491. 'history' => 'historywork',
  492. 'relation' => 'relwork',
  493. ];
  494. foreach ($settings as $setingName => $worksKey) {
  495. if (! isset($cfgRelation[$setingName])) {
  496. continue;
  497. }
  498. if (! $this->canAccessStorageTable($cfgRelation[$setingName])) {
  499. continue;
  500. }
  501. $cfgRelation[$worksKey] = true;
  502. }
  503. return $cfgRelation;
  504. }
  505. private function fillCfgRelationWithTableNames(array $cfgRelation): ?array
  506. {
  507. $tabQuery = 'SHOW TABLES FROM '
  508. . Util::backquote(
  509. $GLOBALS['cfg']['Server']['pmadb']
  510. );
  511. $tableRes = $this->queryAsControlUser(
  512. $tabQuery,
  513. false,
  514. DatabaseInterface::QUERY_STORE
  515. );
  516. if (is_bool($tableRes)) {
  517. return null;
  518. }
  519. while ($currTable = @$this->dbi->fetchRow($tableRes)) {
  520. if ($currTable[0] == $GLOBALS['cfg']['Server']['bookmarktable']) {
  521. $cfgRelation['bookmark'] = $currTable[0];
  522. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['relation']) {
  523. $cfgRelation['relation'] = $currTable[0];
  524. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['table_info']) {
  525. $cfgRelation['table_info'] = $currTable[0];
  526. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['table_coords']) {
  527. $cfgRelation['table_coords'] = $currTable[0];
  528. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['column_info']) {
  529. $cfgRelation['column_info'] = $currTable[0];
  530. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['pdf_pages']) {
  531. $cfgRelation['pdf_pages'] = $currTable[0];
  532. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['history']) {
  533. $cfgRelation['history'] = $currTable[0];
  534. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['recent']) {
  535. $cfgRelation['recent'] = $currTable[0];
  536. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['favorite']) {
  537. $cfgRelation['favorite'] = $currTable[0];
  538. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['table_uiprefs']) {
  539. $cfgRelation['table_uiprefs'] = $currTable[0];
  540. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['tracking']) {
  541. $cfgRelation['tracking'] = $currTable[0];
  542. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['userconfig']) {
  543. $cfgRelation['userconfig'] = $currTable[0];
  544. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['users']) {
  545. $cfgRelation['users'] = $currTable[0];
  546. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['usergroups']) {
  547. $cfgRelation['usergroups'] = $currTable[0];
  548. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['navigationhiding']) {
  549. $cfgRelation['navigationhiding'] = $currTable[0];
  550. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['savedsearches']) {
  551. $cfgRelation['savedsearches'] = $currTable[0];
  552. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['central_columns']) {
  553. $cfgRelation['central_columns'] = $currTable[0];
  554. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['designer_settings']) {
  555. $cfgRelation['designer_settings'] = $currTable[0];
  556. } elseif ($currTable[0] == $GLOBALS['cfg']['Server']['export_templates']) {
  557. $cfgRelation['export_templates'] = $currTable[0];
  558. }
  559. }
  560. $this->dbi->freeResult($tableRes);
  561. return $cfgRelation;
  562. }
  563. /**
  564. * Defines the relation parameters for the current user
  565. * just a copy of the functions used for relations ;-)
  566. * but added some stuff to check what will work
  567. *
  568. * @return string[] the relation parameters for the current user
  569. *
  570. * @access protected
  571. */
  572. public function checkRelationsParam(): array
  573. {
  574. $cfgRelation = [];
  575. $cfgRelation['PMA_VERSION'] = PMA_VERSION;
  576. $workToTable = [
  577. 'relwork' => 'relation',
  578. 'displaywork' => [
  579. 'relation',
  580. 'table_info',
  581. ],
  582. 'bookmarkwork' => 'bookmarktable',
  583. 'pdfwork' => [
  584. 'table_coords',
  585. 'pdf_pages',
  586. ],
  587. 'commwork' => 'column_info',
  588. 'mimework' => 'column_info',
  589. 'historywork' => 'history',
  590. 'recentwork' => 'recent',
  591. 'favoritework' => 'favorite',
  592. 'uiprefswork' => 'table_uiprefs',
  593. 'trackingwork' => 'tracking',
  594. 'userconfigwork' => 'userconfig',
  595. 'menuswork' => [
  596. 'users',
  597. 'usergroups',
  598. ],
  599. 'navwork' => 'navigationhiding',
  600. 'savedsearcheswork' => 'savedsearches',
  601. 'centralcolumnswork' => 'central_columns',
  602. 'designersettingswork' => 'designer_settings',
  603. 'exporttemplateswork' => 'export_templates',
  604. ];
  605. foreach ($workToTable as $work => $table) {
  606. $cfgRelation[$work] = false;
  607. }
  608. $cfgRelation['allworks'] = false;
  609. $cfgRelation['user'] = null;
  610. $cfgRelation['db'] = null;
  611. if ($GLOBALS['server'] == 0
  612. || empty($GLOBALS['cfg']['Server']['pmadb'])
  613. || ! $this->dbi->selectDb(
  614. $GLOBALS['cfg']['Server']['pmadb'],
  615. DatabaseInterface::CONNECT_CONTROL
  616. )
  617. ) {
  618. // No server selected -> no bookmark table
  619. // we return the array with the falses in it,
  620. // to avoid some 'Uninitialized string offset' errors later
  621. $GLOBALS['cfg']['Server']['pmadb'] = false;
  622. return $cfgRelation;
  623. }
  624. $cfgRelation['user'] = $GLOBALS['cfg']['Server']['user'];
  625. $cfgRelation['db'] = $GLOBALS['cfg']['Server']['pmadb'];
  626. // Now I just check if all tables that i need are present so I can for
  627. // example enable relations but not pdf...
  628. // I was thinking of checking if they have all required columns but I
  629. // fear it might be too slow
  630. $cfgRelationFilled = $this->fillCfgRelationWithTableNames($cfgRelation);
  631. if ($cfgRelationFilled === null) {
  632. // query failed ... ?
  633. //$GLOBALS['cfg']['Server']['pmadb'] = false;
  634. return $cfgRelation;
  635. }
  636. // Filling did success
  637. $cfgRelation = $cfgRelationFilled;
  638. $cfgRelation = $this->checkTableAccess($cfgRelation);
  639. $allWorks = true;
  640. foreach ($workToTable as $work => $table) {
  641. if ($cfgRelation[$work]) {
  642. continue;
  643. }
  644. if (is_string($table)) {
  645. if (isset($GLOBALS['cfg']['Server'][$table])
  646. && $GLOBALS['cfg']['Server'][$table] !== false
  647. ) {
  648. $allWorks = false;
  649. break;
  650. }
  651. } elseif (is_array($table)) {
  652. $oneNull = false;
  653. foreach ($table as $t) {
  654. if (isset($GLOBALS['cfg']['Server'][$t])
  655. && $GLOBALS['cfg']['Server'][$t] === false
  656. ) {
  657. $oneNull = true;
  658. break;
  659. }
  660. }
  661. if (! $oneNull) {
  662. $allWorks = false;
  663. break;
  664. }
  665. }
  666. }
  667. $cfgRelation['allworks'] = $allWorks;
  668. return $cfgRelation;
  669. }
  670. /**
  671. * Check if the table is accessible
  672. *
  673. * @param string $tableDbName The table or table.db
  674. *
  675. * @return bool The table is accessible
  676. */
  677. public function canAccessStorageTable(string $tableDbName): bool
  678. {
  679. $result = $this->queryAsControlUser(
  680. 'SELECT NULL FROM ' . $tableDbName . ' LIMIT 0',
  681. false,
  682. DatabaseInterface::QUERY_STORE
  683. );
  684. return $result !== false;
  685. }
  686. /**
  687. * Check whether column_info table input transformation
  688. * upgrade is required and try to upgrade silently
  689. *
  690. * @return bool false if upgrade failed
  691. *
  692. * @access public
  693. */
  694. public function tryUpgradeTransformations()
  695. {
  696. // From 4.3, new input oriented transformation feature was introduced.
  697. // Check whether column_info table has input transformation columns
  698. $new_cols = [
  699. 'input_transformation',
  700. 'input_transformation_options',
  701. ];
  702. $query = 'SHOW COLUMNS FROM '
  703. . Util::backquote($GLOBALS['cfg']['Server']['pmadb'])
  704. . '.' . Util::backquote(
  705. $GLOBALS['cfg']['Server']['column_info']
  706. )
  707. . ' WHERE Field IN (\'' . implode('\', \'', $new_cols) . '\')';
  708. $result = $this->queryAsControlUser(
  709. $query,
  710. false,
  711. DatabaseInterface::QUERY_STORE
  712. );
  713. if ($result) {
  714. $rows = $this->dbi->numRows($result);
  715. $this->dbi->freeResult($result);
  716. // input transformations are present
  717. // no need to upgrade
  718. if ($rows === 2) {
  719. return true;
  720. // try silent upgrade without disturbing the user
  721. }
  722. // read upgrade query file
  723. $query = @file_get_contents(SQL_DIR . 'upgrade_column_info_4_3_0+.sql');
  724. // replace database name from query to with set in config.inc.php
  725. // replace pma__column_info table name from query
  726. // to with set in config.inc.php
  727. $query = str_replace(
  728. [
  729. '`phpmyadmin`',
  730. '`pma__column_info`',
  731. ],
  732. [
  733. Util::backquote($GLOBALS['cfg']['Server']['pmadb']),
  734. Util::backquote($GLOBALS['cfg']['Server']['column_info']),
  735. ],
  736. (string) $query
  737. );
  738. $this->dbi->tryMultiQuery($query, DatabaseInterface::CONNECT_CONTROL);
  739. // skips result sets of query as we are not interested in it
  740. do {
  741. $hasResult = (
  742. $this->dbi->moreResults(DatabaseInterface::CONNECT_CONTROL)
  743. && $this->dbi->nextResult(DatabaseInterface::CONNECT_CONTROL)
  744. );
  745. } while ($hasResult);
  746. $error = $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL);
  747. // return true if no error exists otherwise false
  748. return empty($error);
  749. }
  750. // some failure, either in upgrading or something else
  751. // make some noise, time to wake up user.
  752. return false;
  753. }
  754. /**
  755. * Gets all Relations to foreign tables for a given table or
  756. * optionally a given column in a table
  757. *
  758. * @param string $db the name of the db to check for
  759. * @param string $table the name of the table to check for
  760. * @param string $column the name of the column to check for
  761. * @param string $source the source for foreign key information
  762. *
  763. * @return array db,table,column
  764. *
  765. * @access public
  766. */
  767. public function getForeigners($db, $table, $column = '', $source = 'both')
  768. {
  769. $cfgRelation = $this->getRelationsParam();
  770. $foreign = [];
  771. if ($cfgRelation['relwork'] && ($source === 'both' || $source === 'internal')) {
  772. $rel_query = 'SELECT `master_field`, `foreign_db`, '
  773. . '`foreign_table`, `foreign_field`'
  774. . ' FROM ' . Util::backquote($cfgRelation['db'])
  775. . '.' . Util::backquote($cfgRelation['relation'])
  776. . ' WHERE `master_db` = \'' . $this->dbi->escapeString($db) . '\''
  777. . ' AND `master_table` = \'' . $this->dbi->escapeString($table) . '\'';
  778. if (strlen($column) > 0) {
  779. $rel_query .= ' AND `master_field` = '
  780. . '\'' . $this->dbi->escapeString($column) . '\'';
  781. }
  782. $foreign = $this->dbi->fetchResult(
  783. $rel_query,
  784. 'master_field',
  785. null,
  786. DatabaseInterface::CONNECT_CONTROL
  787. );
  788. }
  789. if (($source === 'both' || $source === 'foreign') && strlen($table) > 0) {
  790. $tableObj = new Table($table, $db);
  791. $show_create_table = $tableObj->showCreate();
  792. if ($show_create_table) {
  793. $parser = new Parser($show_create_table);
  794. /**
  795. * @var CreateStatement $stmt
  796. */
  797. $stmt = $parser->statements[0];
  798. $foreign['foreign_keys_data'] = TableUtils::getForeignKeys(
  799. $stmt
  800. );
  801. }
  802. }
  803. /**
  804. * Emulating relations for some information_schema tables
  805. */
  806. $isInformationSchema = mb_strtolower($db) === 'information_schema';
  807. $isMysql = mb_strtolower($db) === 'mysql';
  808. if (($isInformationSchema || $isMysql)
  809. && ($source === 'internal' || $source === 'both')
  810. ) {
  811. if ($isInformationSchema) {
  812. $internalRelations = InternalRelations::getInformationSchema();
  813. } else {
  814. $internalRelations = InternalRelations::getMySql();
  815. }
  816. if (isset($internalRelations[$table])) {
  817. foreach ($internalRelations[$table] as $field => $relations) {
  818. if ((strlen($column) !== 0 && $column != $field)
  819. || (isset($foreign[$field])
  820. && strlen($foreign[$field]) !== 0)
  821. ) {
  822. continue;
  823. }
  824. $foreign[$field] = $relations;
  825. }
  826. }
  827. }
  828. return $foreign;
  829. }
  830. /**
  831. * Gets the display field of a table
  832. *
  833. * @param string $db the name of the db to check for
  834. * @param string $table the name of the table to check for
  835. *
  836. * @return string|false field name or false
  837. *
  838. * @access public
  839. */
  840. public function getDisplayField($db, $table)
  841. {
  842. $cfgRelation = $this->getRelationsParam();
  843. /**
  844. * Try to fetch the display field from DB.
  845. */
  846. if ($cfgRelation['displaywork']) {
  847. $disp_query = 'SELECT `display_field`'
  848. . ' FROM ' . Util::backquote($cfgRelation['db'])
  849. . '.' . Util::backquote($cfgRelation['table_info'])
  850. . ' WHERE `db_name` = \'' . $this->dbi->escapeString((string) $db) . '\''
  851. . ' AND `table_name` = \'' . $this->dbi->escapeString((string) $table) . '\'';
  852. $row = $this->dbi->fetchSingleRow(
  853. $disp_query,
  854. 'ASSOC',
  855. DatabaseInterface::CONNECT_CONTROL
  856. );
  857. if (isset($row['display_field'])) {
  858. return $row['display_field'];
  859. }
  860. }
  861. /**
  862. * Emulating the display field for some information_schema tables.
  863. */
  864. if ($db === 'information_schema') {
  865. switch ($table) {
  866. case 'CHARACTER_SETS':
  867. return 'DESCRIPTION';
  868. case 'TABLES':
  869. return 'TABLE_COMMENT';
  870. }
  871. }
  872. /**
  873. * Pick first char field
  874. */
  875. $columns = $this->dbi->getColumnsFull($db, $table);
  876. if ($columns) {
  877. foreach ($columns as $column) {
  878. if ($this->dbi->types->getTypeClass($column['DATA_TYPE']) === 'CHAR') {
  879. return $column['COLUMN_NAME'];
  880. }
  881. }
  882. }
  883. return false;
  884. }
  885. /**
  886. * Gets the comments for all columns of a table or the db itself
  887. *
  888. * @param string $db the name of the db to check for
  889. * @param string $table the name of the table to check for
  890. *
  891. * @return array [column_name] = comment
  892. *
  893. * @access public
  894. */
  895. public function getComments($db, $table = '')
  896. {
  897. $comments = [];
  898. if ($table != '') {
  899. // MySQL native column comments
  900. $columns = $this->dbi->getColumns($db, $table, null, true);
  901. if ($columns) {
  902. foreach ($columns as $column) {
  903. if (empty($column['Comment'])) {
  904. continue;
  905. }
  906. $comments[$column['Field']] = $column['Comment'];
  907. }
  908. }
  909. } else {
  910. $comments[] = $this->getDbComment($db);
  911. }
  912. return $comments;
  913. }
  914. /**
  915. * Gets the comment for a db
  916. *
  917. * @param string $db the name of the db to check for
  918. *
  919. * @return string comment
  920. *
  921. * @access public
  922. */
  923. public function getDbComment($db)
  924. {
  925. $cfgRelation = $this->getRelationsParam();
  926. $comment = '';
  927. if ($cfgRelation['commwork']) {
  928. // pmadb internal db comment
  929. $com_qry = 'SELECT `comment`'
  930. . ' FROM ' . Util::backquote($cfgRelation['db'])
  931. . '.' . Util::backquote($cfgRelation['column_info'])
  932. . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''
  933. . ' AND table_name = \'\''
  934. . ' AND column_name = \'(db_comment)\'';
  935. $com_rs = $this->queryAsControlUser(
  936. $com_qry,
  937. false,
  938. DatabaseInterface::QUERY_STORE
  939. );
  940. if ($com_rs && $this->dbi->numRows($com_rs) > 0) {
  941. $row = $this->dbi->fetchAssoc($com_rs);
  942. $comment = $row['comment'];
  943. }
  944. $this->dbi->freeResult($com_rs);
  945. }
  946. return $comment;
  947. }
  948. /**
  949. * Gets the comment for a db
  950. *
  951. * @return array comments
  952. *
  953. * @access public
  954. */
  955. public function getDbComments()
  956. {
  957. $cfgRelation = $this->getRelationsParam();
  958. $comments = [];
  959. if ($cfgRelation['commwork']) {
  960. // pmadb internal db comment
  961. $com_qry = 'SELECT `db_name`, `comment`'
  962. . ' FROM ' . Util::backquote($cfgRelation['db'])
  963. . '.' . Util::backquote($cfgRelation['column_info'])
  964. . ' WHERE `column_name` = \'(db_comment)\'';
  965. $com_rs = $this->queryAsControlUser(
  966. $com_qry,
  967. false,
  968. DatabaseInterface::QUERY_STORE
  969. );
  970. if ($com_rs && $this->dbi->numRows($com_rs) > 0) {
  971. while ($row = $this->dbi->fetchAssoc($com_rs)) {
  972. $comments[$row['db_name']] = $row['comment'];
  973. }
  974. }
  975. $this->dbi->freeResult($com_rs);
  976. }
  977. return $comments;
  978. }
  979. /**
  980. * Set a database comment to a certain value.
  981. *
  982. * @param string $db the name of the db
  983. * @param string $comment the value of the column
  984. *
  985. * @return bool true, if comment-query was made.
  986. *
  987. * @access public
  988. */
  989. public function setDbComment($db, $comment = '')
  990. {
  991. $cfgRelation = $this->getRelationsParam();
  992. if (! $cfgRelation['commwork']) {
  993. return false;
  994. }
  995. if (strlen($comment) > 0) {
  996. $upd_query = 'INSERT INTO '
  997. . Util::backquote($cfgRelation['db']) . '.'
  998. . Util::backquote($cfgRelation['column_info'])
  999. . ' (`db_name`, `table_name`, `column_name`, `comment`)'
  1000. . ' VALUES (\''
  1001. . $this->dbi->escapeString($db)
  1002. . "', '', '(db_comment)', '"
  1003. . $this->dbi->escapeString($comment)
  1004. . "') "
  1005. . ' ON DUPLICATE KEY UPDATE '
  1006. . "`comment` = '" . $this->dbi->escapeString($comment) . "'";
  1007. } else {
  1008. $upd_query = 'DELETE FROM '
  1009. . Util::backquote($cfgRelation['db']) . '.'
  1010. . Util::backquote($cfgRelation['column_info'])
  1011. . ' WHERE `db_name` = \'' . $this->dbi->escapeString($db)
  1012. . '\'
  1013. AND `table_name` = \'\'
  1014. AND `column_name` = \'(db_comment)\'';
  1015. }
  1016. return $this->queryAsControlUser($upd_query);
  1017. }
  1018. /**
  1019. * Set a SQL history entry
  1020. *
  1021. * @param string $db the name of the db
  1022. * @param string $table the name of the table
  1023. * @param string $username the username
  1024. * @param string $sqlquery the sql query
  1025. *
  1026. * @return void
  1027. *
  1028. * @access public
  1029. */
  1030. public function setHistory($db, $table, $username, $sqlquery)
  1031. {
  1032. $maxCharactersInDisplayedSQL = $GLOBALS['cfg']['MaxCharactersInDisplayedSQL'];
  1033. // Prevent to run this automatically on Footer class destroying in testsuite
  1034. if (defined('TESTSUITE')
  1035. || mb_strlen($sqlquery) > $maxCharactersInDisplayedSQL
  1036. ) {
  1037. return;
  1038. }
  1039. $cfgRelation = $this->getRelationsParam();
  1040. if (! isset($_SESSION['sql_history'])) {
  1041. $_SESSION['sql_history'] = [];
  1042. }
  1043. $_SESSION['sql_history'][] = [
  1044. 'db' => $db,
  1045. 'table' => $table,
  1046. 'sqlquery' => $sqlquery,
  1047. ];
  1048. if (count($_SESSION['sql_history']) > $GLOBALS['cfg']['QueryHistoryMax']) {
  1049. // history should not exceed a maximum count
  1050. array_shift($_SESSION['sql_history']);
  1051. }
  1052. if (! $cfgRelation['historywork'] || ! $GLOBALS['cfg']['QueryHistoryDB']) {
  1053. return;
  1054. }
  1055. $this->queryAsControlUser(
  1056. 'INSERT INTO '
  1057. . Util::backquote($cfgRelation['db']) . '.'
  1058. . Util::backquote($cfgRelation['history']) . '
  1059. (`username`,
  1060. `db`,
  1061. `table`,
  1062. `timevalue`,
  1063. `sqlquery`)
  1064. VALUES
  1065. (\'' . $this->dbi->escapeString($username) . '\',
  1066. \'' . $this->dbi->escapeString($db) . '\',
  1067. \'' . $this->dbi->escapeString($table) . '\',
  1068. NOW(),
  1069. \'' . $this->dbi->escapeString($sqlquery) . '\')'
  1070. );
  1071. $this->purgeHistory($username);
  1072. }
  1073. /**
  1074. * Gets a SQL history entry
  1075. *
  1076. * @param string $username the username
  1077. *
  1078. * @return array|bool list of history items
  1079. *
  1080. * @access public
  1081. */
  1082. public function getHistory($username)
  1083. {
  1084. $cfgRelation = $this->getRelationsParam();
  1085. if (! $cfgRelation['historywork']) {
  1086. return false;
  1087. }
  1088. /**
  1089. * if db-based history is disabled but there exists a session-based
  1090. * history, use it
  1091. */
  1092. if (! $GLOBALS['cfg']['QueryHistoryDB']) {
  1093. if (isset($_SESSION['sql_history'])) {
  1094. return array_reverse($_SESSION['sql_history']);
  1095. }
  1096. return false;
  1097. }
  1098. $hist_query = '
  1099. SELECT `db`,
  1100. `table`,
  1101. `sqlquery`,
  1102. `timevalue`
  1103. FROM ' . Util::backquote($cfgRelation['db'])
  1104. . '.' . Util::backquote($cfgRelation['history']) . '
  1105. WHERE `username` = \'' . $this->dbi->escapeString($username) . '\'
  1106. ORDER BY `id` DESC';
  1107. return $this->dbi->fetchResult(
  1108. $hist_query,
  1109. null,
  1110. null,
  1111. DatabaseInterface::CONNECT_CONTROL
  1112. );
  1113. }
  1114. /**
  1115. * purges SQL history
  1116. *
  1117. * deletes entries that exceeds $cfg['QueryHistoryMax'], oldest first, for the
  1118. * given user
  1119. *
  1120. * @param string $username the username
  1121. *
  1122. * @return void
  1123. *
  1124. * @access public
  1125. */
  1126. public function purgeHistory($username)
  1127. {
  1128. $cfgRelation = $this->getRelationsParam();
  1129. if (! $GLOBALS['cfg']['QueryHistoryDB'] || ! $cfgRelation['historywork']) {
  1130. return;
  1131. }
  1132. if (! $cfgRelation['historywork']) {
  1133. return;
  1134. }
  1135. $search_query = '
  1136. SELECT `timevalue`
  1137. FROM ' . Util::backquote($cfgRelation['db'])
  1138. . '.' . Util::backquote($cfgRelation['history']) . '
  1139. WHERE `username` = \'' . $this->dbi->escapeString($username) . '\'
  1140. ORDER BY `timevalue` DESC
  1141. LIMIT ' . $GLOBALS['cfg']['QueryHistoryMax'] . ', 1';
  1142. $max_time = $this->dbi->fetchValue(
  1143. $search_query,
  1144. 0,
  1145. 0,
  1146. DatabaseInterface::CONNECT_CONTROL
  1147. );
  1148. if (! $max_time) {
  1149. return;
  1150. }
  1151. $this->queryAsControlUser(
  1152. 'DELETE FROM '
  1153. . Util::backquote($cfgRelation['db']) . '.'
  1154. . Util::backquote($cfgRelation['history']) . '
  1155. WHERE `username` = \'' . $this->dbi->escapeString($username)
  1156. . '\'
  1157. AND `timevalue` <= \'' . $max_time . '\''
  1158. );
  1159. }
  1160. /**
  1161. * Prepares the dropdown for one mode
  1162. *
  1163. * @param array $foreign the keys and values for foreigns
  1164. * @param string $data the current data of the dropdown
  1165. * @param string $mode the needed mode
  1166. *
  1167. * @return array the <option value=""><option>s
  1168. *
  1169. * @access protected
  1170. */
  1171. public function buildForeignDropdown(array $foreign, $data, $mode)
  1172. {
  1173. $reloptions = [];
  1174. // id-only is a special mode used when no foreign display column
  1175. // is available
  1176. if ($mode === 'id-content' || $mode === 'id-only') {
  1177. // sort for id-content
  1178. if ($GLOBALS['cfg']['NaturalOrder']) {
  1179. uksort($foreign, 'strnatcasecmp');
  1180. } else {
  1181. ksort($foreign);
  1182. }
  1183. } elseif ($mode === 'content-id') {
  1184. // sort for content-id
  1185. if ($GLOBALS['cfg']['NaturalOrder']) {
  1186. natcasesort($foreign);
  1187. } else {
  1188. asort($foreign);
  1189. }
  1190. }
  1191. foreach ($foreign as $key => $value) {
  1192. $vtitle = '';
  1193. $key = (string) $key;
  1194. $value = (string) $value;
  1195. $data = (string) $data;
  1196. if (mb_check_encoding($key, 'utf-8')
  1197. && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $key)
  1198. ) {
  1199. $selected = ($key == $data);
  1200. // show as text if it's valid utf-8
  1201. $key = htmlspecialchars($key);
  1202. } else {
  1203. $key = '0x' . bin2hex($key);
  1204. if (strpos($data, '0x') !== false) {
  1205. $selected = ($key == trim($data));
  1206. } else {
  1207. $selected = ($key == '0x' . $data);
  1208. }
  1209. $key .= $selected;
  1210. }
  1211. if (mb_check_encoding($value, 'utf-8')
  1212. && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $value)
  1213. ) {
  1214. if (mb_strlen($value) <= $GLOBALS['cfg']['LimitChars']
  1215. ) {
  1216. // show as text if it's valid utf-8
  1217. $value = htmlspecialchars($value);
  1218. } else {
  1219. // show as truncated text if it's valid utf-8
  1220. $value = htmlspecialchars(
  1221. mb_substr(
  1222. $value,
  1223. 0,
  1224. (int) $GLOBALS['cfg']['LimitChars']
  1225. ) . '...'
  1226. );
  1227. }
  1228. } else {
  1229. $value = '0x' . bin2hex($value);
  1230. }
  1231. $reloption = '<option value="' . $key . '"';
  1232. if ($vtitle != '') {
  1233. $reloption .= ' title="' . $vtitle . '"';
  1234. }
  1235. if ($selected) {
  1236. $reloption .= ' selected="selected"';
  1237. }
  1238. if ($mode === 'content-id') {
  1239. $reloptions[] = $reloption . '>'
  1240. . $value . '&nbsp;-&nbsp;' . $key . '</option>';
  1241. } elseif ($mode === 'id-content') {
  1242. $reloptions[] = $reloption . '>'
  1243. . $key . '&nbsp;-&nbsp;' . $value . '</option>';
  1244. } elseif ($mode === 'id-only') {
  1245. $reloptions[] = $reloption . '>'
  1246. . $key . '</option>';
  1247. }
  1248. }
  1249. return $reloptions;
  1250. }
  1251. /**
  1252. * Outputs dropdown with values of foreign fields
  1253. *
  1254. * @param array $disp_row array of the displayed row
  1255. * @param string $foreign_field the foreign field
  1256. * @param string $foreign_display the foreign field to display
  1257. * @param string $data the current data of the dropdown (field in row)
  1258. * @param int $max maximum number of items in the dropdown
  1259. *
  1260. * @return string the <option value=""><option>s
  1261. *
  1262. * @access public
  1263. */
  1264. public function foreignDropdown(
  1265. array $disp_row,
  1266. $foreign_field,
  1267. $foreign_display,
  1268. $data,
  1269. $max = null
  1270. ) {
  1271. if ($max === null) {
  1272. $max = $GLOBALS['cfg']['ForeignKeyMaxLimit'];
  1273. }
  1274. $foreign = [];
  1275. // collect the data
  1276. foreach ($disp_row as $relrow) {
  1277. $key = $relrow[$foreign_field];
  1278. // if the display field has been defined for this foreign table
  1279. if ($foreign_display) {
  1280. $value = $relrow[$foreign_display];
  1281. } else {
  1282. $value = '';
  1283. }
  1284. $foreign[$key] = $value;
  1285. }
  1286. // put the dropdown sections in correct order
  1287. $top = [];
  1288. $bottom = [];
  1289. if ($foreign_display) {
  1290. if (Core::isValid($GLOBALS['cfg']['ForeignKeyDropdownOrder'], 'array')) {
  1291. if (Core::isValid($GLOBALS['cfg']['ForeignKeyDropdownOrder'][0])) {
  1292. $top = $this->buildForeignDropdown(
  1293. $foreign,
  1294. $data,
  1295. $GLOBALS['cfg']['ForeignKeyDropdownOrder'][0]
  1296. );
  1297. }
  1298. if (Core::isValid($GLOBALS['cfg']['ForeignKeyDropdownOrder'][1])) {
  1299. $bottom = $this->buildForeignDropdown(
  1300. $foreign,
  1301. $data,
  1302. $GLOBALS['cfg']['ForeignKeyDropdownOrder'][1]
  1303. );
  1304. }
  1305. } else {
  1306. $top = $this->buildForeignDropdown($foreign, $data, 'id-content');
  1307. $bottom = $this->buildForeignDropdown($foreign, $data, 'content-id');
  1308. }
  1309. } else {
  1310. $top = $this->buildForeignDropdown($foreign, $data, 'id-only');
  1311. }
  1312. // beginning of dropdown
  1313. $ret = '<option value="">&nbsp;</option>';
  1314. $top_count = count($top);
  1315. if ($max == -1 || $top_count < $max) {
  1316. $ret .= implode('', $top);
  1317. if ($foreign_display && $top_count > 0) {
  1318. // this empty option is to visually mark the beginning of the
  1319. // second series of values (bottom)
  1320. $ret .= '<option value="">&nbsp;</option>';
  1321. }
  1322. }
  1323. if ($foreign_display) {
  1324. $ret .= implode('', $bottom);
  1325. }
  1326. return $ret;
  1327. }
  1328. /**
  1329. * Gets foreign keys in preparation for a drop-down selector
  1330. *
  1331. * @param array|bool $foreigners array of the foreign keys
  1332. * @param string $field the foreign field name
  1333. * @param bool $override_total whether to override the total
  1334. * @param string $foreign_filter a possible filter
  1335. * @param string $foreign_limit a possible LIMIT clause
  1336. * @param bool $get_total optional, whether to get total num of rows
  1337. * in $foreignData['the_total;]
  1338. * (has an effect of performance)
  1339. *
  1340. * @return array data about the foreign keys
  1341. *
  1342. * @access public
  1343. */
  1344. public function getForeignData(
  1345. $foreigners,
  1346. $field,
  1347. $override_total,
  1348. $foreign_filter,
  1349. $foreign_limit,
  1350. $get_total = false
  1351. ) {
  1352. // we always show the foreign field in the drop-down; if a display
  1353. // field is defined, we show it besides the foreign field
  1354. $foreign_link = false;
  1355. do {
  1356. if (! $foreigners) {
  1357. break;
  1358. }
  1359. $foreigner = $this->searchColumnInForeigners($foreigners, $field);
  1360. if ($foreigner == false) {
  1361. break;
  1362. }
  1363. $foreign_db = $foreigner['foreign_db'];
  1364. $foreign_table = $foreigner['foreign_table'];
  1365. $foreign_field = $foreigner['foreign_field'];
  1366. // Count number of rows in the foreign table. Currently we do
  1367. // not use a drop-down if more than ForeignKeyMaxLimit rows in the
  1368. // foreign table,
  1369. // for speed reasons and because we need a better interface for this.
  1370. //
  1371. // We could also do the SELECT anyway, with a LIMIT, and ensure that
  1372. // the current value of the field is one of the choices.
  1373. // Check if table has more rows than specified by
  1374. // $GLOBALS['cfg']['ForeignKeyMaxLimit']
  1375. $moreThanLimit = $this->dbi->getTable($foreign_db, $foreign_table)
  1376. ->checkIfMinRecordsExist($GLOBALS['cfg']['ForeignKeyMaxLimit']);
  1377. if ($override_total === true
  1378. || ! $moreThanLimit
  1379. ) {
  1380. // foreign_display can be false if no display field defined:
  1381. $foreign_display = $this->getDisplayField($foreign_db, $foreign_table);
  1382. $f_query_main = 'SELECT ' . Util::backquote($foreign_field)
  1383. . (
  1384. $foreign_display === false
  1385. ? ''
  1386. : ', ' . Util::backquote($foreign_display)
  1387. );
  1388. $f_query_from = ' FROM ' . Util::backquote($foreign_db)
  1389. . '.' . Util::backquote($foreign_table);
  1390. $f_query_filter = empty($foreign_filter) ? '' : ' WHERE '
  1391. . Util::backquote($foreign_field)
  1392. . ' LIKE "%' . $this->dbi->escapeString($foreign_filter) . '%"'
  1393. . (
  1394. $foreign_display === false
  1395. ? ''
  1396. : ' OR ' . Util::backquote($foreign_display)
  1397. . ' LIKE "%' . $this->dbi->escapeString($foreign_filter)
  1398. . '%"'
  1399. );
  1400. $f_query_order = $foreign_display === false ? '' : ' ORDER BY '
  1401. . Util::backquote($foreign_table) . '.'
  1402. . Util::backquote($foreign_display);
  1403. $f_query_limit = ! empty($foreign_limit) ? $foreign_limit : '';
  1404. if (! empty($foreign_filter)) {
  1405. $the_total = $this->dbi->fetchValue(
  1406. 'SELECT COUNT(*)' . $f_query_from . $f_query_filter
  1407. );
  1408. if ($the_total === false) {
  1409. $the_total = 0;
  1410. }
  1411. }
  1412. $disp = $this->dbi->tryQuery(
  1413. $f_query_main . $f_query_from . $f_query_filter
  1414. . $f_query_order . $f_query_limit
  1415. );
  1416. if ($disp && $this->dbi->numRows($disp) > 0) {
  1417. // If a resultset has been created, pre-cache it in the $disp_row
  1418. // array. This helps us from not needing to use mysql_data_seek by
  1419. // accessing a pre-cached PHP array. Usually those resultsets are
  1420. // not that big, so a performance hit should not be expected.
  1421. $disp_row = [];
  1422. while ($single_disp_row = @$this->dbi->fetchAssoc($disp)) {
  1423. $disp_row[] = $single_disp_row;
  1424. }
  1425. @$this->dbi->freeResult($disp);
  1426. } else {
  1427. // Either no data in the foreign table or
  1428. // user does not have select permission to foreign table/field
  1429. // Show an input field with a 'Browse foreign values' link
  1430. $disp_row = null;
  1431. $foreign_link = true;
  1432. }
  1433. } else {
  1434. $disp_row = null;
  1435. $foreign_link = true;
  1436. }
  1437. } while (false);
  1438. if ($get_total && isset($foreign_db, $foreign_table)) {
  1439. $the_total = $this->dbi->getTable($foreign_db, $foreign_table)
  1440. ->countRecords(true);
  1441. }
  1442. $foreignData = [];
  1443. $foreignData['foreign_link'] = $foreign_link;
  1444. $foreignData['the_total'] = $the_total ?? null;
  1445. $foreignData['foreign_display'] = (
  1446. $foreign_display ?? null
  1447. );
  1448. $foreignData['disp_row'] = $disp_row ?? null;
  1449. $foreignData['foreign_field'] = $foreign_field ?? null;
  1450. return $foreignData;
  1451. }
  1452. /**
  1453. * Rename a field in relation tables
  1454. *
  1455. * usually called after a column in a table was renamed
  1456. *
  1457. * @param string $db database name
  1458. * @param string $table table name
  1459. * @param string $field old field name
  1460. * @param string $new_name new field name
  1461. *
  1462. * @return void
  1463. */
  1464. public function renameField($db, $table, $field, $new_name)
  1465. {
  1466. $cfgRelation = $this->getRelationsParam();
  1467. if ($cfgRelation['displaywork']) {
  1468. $table_query = 'UPDATE '
  1469. . Util::backquote($cfgRelation['db']) . '.'
  1470. . Util::backquote($cfgRelation['table_info'])
  1471. . ' SET display_field = \'' . $this->dbi->escapeString(
  1472. $new_name
  1473. ) . '\''
  1474. . ' WHERE db_name = \'' . $this->dbi->escapeString($db)
  1475. . '\''
  1476. . ' AND table_name = \'' . $this->dbi->escapeString($table)
  1477. . '\''
  1478. . ' AND display_field = \'' . $this->dbi->escapeString($field)
  1479. . '\'';
  1480. $this->queryAsControlUser($table_query);
  1481. }
  1482. if (! $cfgRelation['relwork']) {
  1483. return;
  1484. }
  1485. $table_query = 'UPDATE '
  1486. . Util::backquote($cfgRelation['db']) . '.'
  1487. . Util::backquote($cfgRelation['relation'])
  1488. . ' SET master_field = \'' . $this->dbi->escapeString(
  1489. $new_name
  1490. ) . '\''
  1491. . ' WHERE master_db = \'' . $this->dbi->escapeString($db)
  1492. . '\''
  1493. . ' AND master_table = \'' . $this->dbi->escapeString($table)
  1494. . '\''
  1495. . ' AND master_field = \'' . $this->dbi->escapeString($field)
  1496. . '\'';
  1497. $this->queryAsControlUser($table_query);
  1498. $table_query = 'UPDATE '
  1499. . Util::backquote($cfgRelation['db']) . '.'
  1500. . Util::backquote($cfgRelation['relation'])
  1501. . ' SET foreign_field = \'' . $this->dbi->escapeString(
  1502. $new_name
  1503. ) . '\''
  1504. . ' WHERE foreign_db = \'' . $this->dbi->escapeString($db)
  1505. . '\''
  1506. . ' AND foreign_table = \'' . $this->dbi->escapeString($table)
  1507. . '\''
  1508. . ' AND foreign_field = \'' . $this->dbi->escapeString($field)
  1509. . '\'';
  1510. $this->queryAsControlUser($table_query);
  1511. }
  1512. /**
  1513. * Performs SQL query used for renaming table.
  1514. *
  1515. * @param string $table Relation table to use
  1516. * @param string $source_db Source database name
  1517. * @param string $target_db Target database name
  1518. * @param string $source_table Source table name
  1519. * @param string $target_table Target table name
  1520. * @param string $db_field Name of database field
  1521. * @param string $table_field Name of table field
  1522. */
  1523. public function renameSingleTable(
  1524. string $table,
  1525. string $source_db,
  1526. string $target_db,
  1527. string $source_table,
  1528. string $target_table,
  1529. string $db_field,
  1530. string $table_field
  1531. ): void {
  1532. $query = 'UPDATE '
  1533. . Util::backquote($GLOBALS['cfgRelation']['db']) . '.'
  1534. . Util::backquote($GLOBALS['cfgRelation'][$table])
  1535. . ' SET '
  1536. . $db_field . ' = \'' . $this->dbi->escapeString($target_db)
  1537. . '\', '
  1538. . $table_field . ' = \'' . $this->dbi->escapeString($target_table)
  1539. . '\''
  1540. . ' WHERE '
  1541. . $db_field . ' = \'' . $this->dbi->escapeString($source_db) . '\''
  1542. . ' AND '
  1543. . $table_field . ' = \'' . $this->dbi->escapeString($source_table)
  1544. . '\'';
  1545. $this->queryAsControlUser($query);
  1546. }
  1547. /**
  1548. * Rename a table in relation tables
  1549. *
  1550. * usually called after table has been moved
  1551. *
  1552. * @param string $source_db Source database name
  1553. * @param string $target_db Target database name
  1554. * @param string $source_table Source table name
  1555. * @param string $target_table Target table name
  1556. *
  1557. * @return void
  1558. */
  1559. public function renameTable($source_db, $target_db, $source_table, $target_table)
  1560. {
  1561. // Move old entries from PMA-DBs to new table
  1562. if ($GLOBALS['cfgRelation']['commwork']) {
  1563. $this->renameSingleTable(
  1564. 'column_info',
  1565. $source_db,
  1566. $target_db,
  1567. $source_table,
  1568. $target_table,
  1569. 'db_name',
  1570. 'table_name'
  1571. );
  1572. }
  1573. // updating bookmarks is not possible since only a single table is
  1574. // moved, and not the whole DB.
  1575. if ($GLOBALS['cfgRelation']['displaywork']) {
  1576. $this->renameSingleTable(
  1577. 'table_info',
  1578. $source_db,
  1579. $target_db,
  1580. $source_table,
  1581. $target_table,
  1582. 'db_name',
  1583. 'table_name'
  1584. );
  1585. }
  1586. if ($GLOBALS['cfgRelation']['relwork']) {
  1587. $this->renameSingleTable(
  1588. 'relation',
  1589. $source_db,
  1590. $target_db,
  1591. $source_table,
  1592. $target_table,
  1593. 'foreign_db',
  1594. 'foreign_table'
  1595. );
  1596. $this->renameSingleTable(
  1597. 'relation',
  1598. $source_db,
  1599. $target_db,
  1600. $source_table,
  1601. $target_table,
  1602. 'master_db',
  1603. 'master_table'
  1604. );
  1605. }
  1606. if ($GLOBALS['cfgRelation']['pdfwork']) {
  1607. if ($source_db == $target_db) {
  1608. // rename within the database can be handled
  1609. $this->renameSingleTable(
  1610. 'table_coords',
  1611. $source_db,
  1612. $target_db,
  1613. $source_table,
  1614. $target_table,
  1615. 'db_name',
  1616. 'table_name'
  1617. );
  1618. } else {
  1619. // if the table is moved out of the database we can no loger keep the
  1620. // record for table coordinate
  1621. $remove_query = 'DELETE FROM '
  1622. . Util::backquote($GLOBALS['cfgRelation']['db']) . '.'
  1623. . Util::backquote($GLOBALS['cfgRelation']['table_coords'])
  1624. . " WHERE db_name = '" . $this->dbi->escapeString($source_db) . "'"
  1625. . " AND table_name = '" . $this->dbi->escapeString($source_table)
  1626. . "'";
  1627. $this->queryAsControlUser($remove_query);
  1628. }
  1629. }
  1630. if ($GLOBALS['cfgRelation']['uiprefswork']) {
  1631. $this->renameSingleTable(
  1632. 'table_uiprefs',
  1633. $source_db,
  1634. $target_db,
  1635. $source_table,
  1636. $target_table,
  1637. 'db_name',
  1638. 'table_name'
  1639. );
  1640. }
  1641. if (! $GLOBALS['cfgRelation']['navwork']) {
  1642. return;
  1643. }
  1644. // update hidden items inside table
  1645. $this->renameSingleTable(
  1646. 'navigationhiding',
  1647. $source_db,
  1648. $target_db,
  1649. $source_table,
  1650. $target_table,
  1651. 'db_name',
  1652. 'table_name'
  1653. );
  1654. // update data for hidden table
  1655. $query = 'UPDATE '
  1656. . Util::backquote($GLOBALS['cfgRelation']['db']) . '.'
  1657. . Util::backquote(
  1658. $GLOBALS['cfgRelation']['navigationhiding']
  1659. )
  1660. . " SET db_name = '" . $this->dbi->escapeString($target_db)
  1661. . "',"
  1662. . " item_name = '" . $this->dbi->escapeString($target_table)
  1663. . "'"
  1664. . " WHERE db_name = '" . $this->dbi->escapeString($source_db)
  1665. . "'"
  1666. . " AND item_name = '" . $this->dbi->escapeString($source_table)
  1667. . "'"
  1668. . " AND item_type = 'table'";
  1669. $this->queryAsControlUser($query);
  1670. }
  1671. /**
  1672. * Create a PDF page
  1673. *
  1674. * @param string|null $newpage name of the new PDF page
  1675. * @param array $cfgRelation Relation configuration
  1676. * @param string $db database name
  1677. *
  1678. * @return int
  1679. */
  1680. public function createPage(?string $newpage, array $cfgRelation, $db)
  1681. {
  1682. if (! isset($newpage) || $newpage == '') {
  1683. $newpage = __('no description');
  1684. }
  1685. $ins_query = 'INSERT INTO '
  1686. . Util::backquote($GLOBALS['cfgRelation']['db']) . '.'
  1687. . Util::backquote($cfgRelation['pdf_pages'])
  1688. . ' (db_name, page_descr)'
  1689. . ' VALUES (\''
  1690. . $this->dbi->escapeString($db) . '\', \''
  1691. . $this->dbi->escapeString($newpage) . '\')';
  1692. $this->queryAsControlUser($ins_query, false);
  1693. return $this->dbi->insertId(DatabaseInterface::CONNECT_CONTROL);
  1694. }
  1695. /**
  1696. * Get child table references for a table column.
  1697. * This works only if 'DisableIS' is false. An empty array is returned otherwise.
  1698. *
  1699. * @param string $db name of master table db.
  1700. * @param string $table name of master table.
  1701. * @param string $column name of master table column.
  1702. *
  1703. * @return array
  1704. */
  1705. public function getChildReferences($db, $table, $column = '')
  1706. {
  1707. $child_references = [];
  1708. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  1709. $rel_query = 'SELECT `column_name`, `table_name`,'
  1710. . ' `table_schema`, `referenced_column_name`'
  1711. . ' FROM `information_schema`.`key_column_usage`'
  1712. . " WHERE `referenced_table_name` = '"
  1713. . $this->dbi->escapeString($table) . "'"
  1714. . " AND `referenced_table_schema` = '"
  1715. . $this->dbi->escapeString($db) . "'";
  1716. if ($column) {
  1717. $rel_query .= " AND `referenced_column_name` = '"
  1718. . $this->dbi->escapeString($column) . "'";
  1719. }
  1720. $child_references = $this->dbi->fetchResult(
  1721. $rel_query,
  1722. [
  1723. 'referenced_column_name',
  1724. null,
  1725. ]
  1726. );
  1727. }
  1728. return $child_references;
  1729. }
  1730. /**
  1731. * Check child table references and foreign key for a table column.
  1732. *
  1733. * @param string $db name of master table db.
  1734. * @param string $table name of master table.
  1735. * @param string $column name of master table column.
  1736. * @param array|null $foreigners_full foreigners array for the whole table.
  1737. * @param array|null $child_references_full child references for the whole table.
  1738. *
  1739. * @return array telling about references if foreign key.
  1740. */
  1741. public function checkChildForeignReferences(
  1742. $db,
  1743. $table,
  1744. $column,
  1745. $foreigners_full = null,
  1746. $child_references_full = null
  1747. ) {
  1748. $column_status = [];
  1749. $column_status['isEditable'] = false;
  1750. $column_status['isReferenced'] = false;
  1751. $column_status['isForeignKey'] = false;
  1752. $column_status['references'] = [];
  1753. $foreigners = [];
  1754. if ($foreigners_full !== null) {
  1755. if (isset($foreigners_full[$column])) {
  1756. $foreigners[$column] = $foreigners_full[$column];
  1757. }
  1758. if (isset($foreigners_full['foreign_keys_data'])) {
  1759. $foreigners['foreign_keys_data'] = $foreigners_full['foreign_keys_data'];
  1760. }
  1761. } else {
  1762. $foreigners = $this->getForeigners($db, $table, $column, 'foreign');
  1763. }
  1764. $foreigner = $this->searchColumnInForeigners($foreigners, $column);
  1765. $child_references = [];
  1766. if ($child_references_full !== null) {
  1767. if (isset($child_references_full[$column])) {
  1768. $child_references = $child_references_full[$column];
  1769. }
  1770. } else {
  1771. $child_references = $this->getChildReferences($db, $table, $column);
  1772. }
  1773. if (count($child_references) > 0
  1774. || $foreigner
  1775. ) {
  1776. if (count($child_references) > 0) {
  1777. $column_status['isReferenced'] = true;
  1778. foreach ($child_references as $columns) {
  1779. $column_status['references'][] = Util::backquote($columns['table_schema'])
  1780. . '.' . Util::backquote($columns['table_name']);
  1781. }
  1782. }
  1783. if ($foreigner) {
  1784. $column_status['isForeignKey'] = true;
  1785. }
  1786. } else {
  1787. $column_status['isEditable'] = true;
  1788. }
  1789. return $column_status;
  1790. }
  1791. /**
  1792. * Search a table column in foreign data.
  1793. *
  1794. * @param array $foreigners Table Foreign data
  1795. * @param string $column Column name
  1796. *
  1797. * @return array|false
  1798. */
  1799. public function searchColumnInForeigners(array $foreigners, $column)
  1800. {
  1801. if (isset($foreigners[$column])) {
  1802. return $foreigners[$column];
  1803. }
  1804. $foreigner = [];
  1805. foreach ($foreigners['foreign_keys_data'] as $one_key) {
  1806. $column_index = array_search($column, $one_key['index_list']);
  1807. if ($column_index !== false) {
  1808. $foreigner['foreign_field']
  1809. = $one_key['ref_index_list'][$column_index];
  1810. $foreigner['foreign_db'] = $one_key['ref_db_name'] ?? $GLOBALS['db'];
  1811. $foreigner['foreign_table'] = $one_key['ref_table_name'];
  1812. $foreigner['constraint'] = $one_key['constraint'];
  1813. $foreigner['on_update'] = $one_key['on_update'] ?? 'RESTRICT';
  1814. $foreigner['on_delete'] = $one_key['on_delete'] ?? 'RESTRICT';
  1815. return $foreigner;
  1816. }
  1817. }
  1818. return false;
  1819. }
  1820. /**
  1821. * Returns default PMA table names and their create queries.
  1822. *
  1823. * @return array table name, create query
  1824. */
  1825. public function getDefaultPmaTableNames()
  1826. {
  1827. $pma_tables = [];
  1828. $create_tables_file = (string) file_get_contents(
  1829. SQL_DIR . 'create_tables.sql'
  1830. );
  1831. $queries = explode(';', $create_tables_file);
  1832. foreach ($queries as $query) {
  1833. if (! preg_match(
  1834. '/CREATE TABLE IF NOT EXISTS `(.*)` \(/',
  1835. $query,
  1836. $table
  1837. )
  1838. ) {
  1839. continue;
  1840. }
  1841. $pma_tables[$table[1]] = $query . ';';
  1842. }
  1843. return $pma_tables;
  1844. }
  1845. /**
  1846. * Create a table named phpmyadmin to be used as configuration storage
  1847. *
  1848. * @return bool
  1849. */
  1850. public function createPmaDatabase()
  1851. {
  1852. $this->dbi->tryQuery('CREATE DATABASE IF NOT EXISTS `phpmyadmin`');
  1853. $error = $this->dbi->getError();
  1854. if (! $error) {
  1855. return true;
  1856. }
  1857. $GLOBALS['message'] = $error;
  1858. if ($GLOBALS['errno'] === 1044) {
  1859. $GLOBALS['message'] = __(
  1860. 'You do not have necessary privileges to create a database named'
  1861. . ' \'phpmyadmin\'. You may go to \'Operations\' tab of any'
  1862. . ' database to set up the phpMyAdmin configuration storage there.'
  1863. );
  1864. }
  1865. return false;
  1866. }
  1867. /**
  1868. * Creates PMA tables in the given db, updates if already exists.
  1869. *
  1870. * @param string $db database
  1871. * @param bool $create whether to create tables if they don't exist.
  1872. *
  1873. * @return void
  1874. */
  1875. public function fixPmaTables($db, $create = true)
  1876. {
  1877. $tablesToFeatures = [
  1878. 'pma__bookmark' => 'bookmarktable',
  1879. 'pma__relation' => 'relation',
  1880. 'pma__table_info' => 'table_info',
  1881. 'pma__table_coords' => 'table_coords',
  1882. 'pma__pdf_pages' => 'pdf_pages',
  1883. 'pma__column_info' => 'column_info',
  1884. 'pma__history' => 'history',
  1885. 'pma__recent' => 'recent',
  1886. 'pma__favorite' => 'favorite',
  1887. 'pma__table_uiprefs' => 'table_uiprefs',
  1888. 'pma__tracking' => 'tracking',
  1889. 'pma__userconfig' => 'userconfig',
  1890. 'pma__users' => 'users',
  1891. 'pma__usergroups' => 'usergroups',
  1892. 'pma__navigationhiding' => 'navigationhiding',
  1893. 'pma__savedsearches' => 'savedsearches',
  1894. 'pma__central_columns' => 'central_columns',
  1895. 'pma__designer_settings' => 'designer_settings',
  1896. 'pma__export_templates' => 'export_templates',
  1897. ];
  1898. $existingTables = $this->dbi->getTables($db, DatabaseInterface::CONNECT_CONTROL);
  1899. $createQueries = null;
  1900. $foundOne = false;
  1901. foreach ($tablesToFeatures as $table => $feature) {
  1902. if (! in_array($table, $existingTables)) {
  1903. if ($create) {
  1904. if ($createQueries == null) { // first create
  1905. $createQueries = $this->getDefaultPmaTableNames();
  1906. $this->dbi->selectDb($db);
  1907. }
  1908. $this->dbi->tryQuery($createQueries[$table]);
  1909. $error = $this->dbi->getError();
  1910. if ($error) {
  1911. $GLOBALS['message'] = $error;
  1912. return;
  1913. }
  1914. $foundOne = true;
  1915. $GLOBALS['cfg']['Server'][$feature] = $table;
  1916. }
  1917. } else {
  1918. $foundOne = true;
  1919. $GLOBALS['cfg']['Server'][$feature] = $table;
  1920. }
  1921. }
  1922. if (! $foundOne) {
  1923. return;
  1924. }
  1925. $GLOBALS['cfg']['Server']['pmadb'] = $db;
  1926. $_SESSION['relation'][$GLOBALS['server']] = $this->checkRelationsParam();
  1927. $cfgRelation = $this->getRelationsParam();
  1928. if (! $cfgRelation['recentwork'] && ! $cfgRelation['favoritework']) {
  1929. return;
  1930. }
  1931. // Since configuration storage is updated, we need to
  1932. // re-initialize the favorite and recent tables stored in the
  1933. // session from the current configuration storage.
  1934. if ($cfgRelation['favoritework']) {
  1935. $fav_tables = RecentFavoriteTable::getInstance('favorite');
  1936. $_SESSION['tmpval']['favoriteTables'][$GLOBALS['server']]
  1937. = $fav_tables->getFromDb();
  1938. }
  1939. if ($cfgRelation['recentwork']) {
  1940. $recent_tables = RecentFavoriteTable::getInstance('recent');
  1941. $_SESSION['tmpval']['recentTables'][$GLOBALS['server']]
  1942. = $recent_tables->getFromDb();
  1943. }
  1944. // Reload navi panel to update the recent/favorite lists.
  1945. $GLOBALS['reload'] = true;
  1946. }
  1947. /**
  1948. * Get Html for PMA tables fixing anchor.
  1949. *
  1950. * @param bool $allTables whether to create all tables
  1951. * @param bool $createDb whether to create the pmadb also
  1952. *
  1953. * @return string Html
  1954. */
  1955. public function getHtmlFixPmaTables($allTables, $createDb = false)
  1956. {
  1957. $retval = '';
  1958. $params = [
  1959. 'db' => $GLOBALS['db'],
  1960. 'goto' => Url::getFromRoute('/database/operations'),
  1961. ];
  1962. if ($allTables) {
  1963. if ($createDb) {
  1964. $params['create_pmadb'] = 1;
  1965. $message = Message::notice(
  1966. __(
  1967. '%sCreate%s a database named \'phpmyadmin\' and setup '
  1968. . 'the phpMyAdmin configuration storage there.'
  1969. )
  1970. );
  1971. } else {
  1972. $params['fixall_pmadb'] = 1;
  1973. $message = Message::notice(
  1974. __(
  1975. '%sCreate%s the phpMyAdmin configuration storage in the '
  1976. . 'current database.'
  1977. )
  1978. );
  1979. }
  1980. } else {
  1981. $params['fix_pmadb'] = 1;
  1982. $message = Message::notice(
  1983. __('%sCreate%s missing phpMyAdmin configuration storage tables.')
  1984. );
  1985. }
  1986. $message->addParamHtml(
  1987. '<a href="' . Url::getFromRoute('/check-relations') . '" data-post="' . Url::getCommon($params, '') . '">'
  1988. );
  1989. $message->addParamHtml('</a>');
  1990. return $retval . $message->getDisplay();
  1991. }
  1992. /**
  1993. * Gets the relations info and status, depending on the condition
  1994. *
  1995. * @param bool $condition whether to look for foreigners or not
  1996. * @param string $db database name
  1997. * @param string $table table name
  1998. *
  1999. * @return array ($res_rel, $have_rel)
  2000. */
  2001. public function getRelationsAndStatus($condition, $db, $table)
  2002. {
  2003. if ($condition) {
  2004. // Find which tables are related with the current one and write it in
  2005. // an array
  2006. $res_rel = $this->getForeigners($db, $table);
  2007. if (count($res_rel) > 0) {
  2008. $have_rel = true;
  2009. } else {
  2010. $have_rel = false;
  2011. }
  2012. } else {
  2013. $have_rel = false;
  2014. $res_rel = [];
  2015. }
  2016. return [
  2017. $res_rel,
  2018. $have_rel,
  2019. ];
  2020. }
  2021. /**
  2022. * Verifies if all the pmadb tables are defined
  2023. *
  2024. * @return bool
  2025. */
  2026. public function arePmadbTablesDefined()
  2027. {
  2028. return ! (empty($GLOBALS['cfg']['Server']['bookmarktable'])
  2029. || empty($GLOBALS['cfg']['Server']['relation'])
  2030. || empty($GLOBALS['cfg']['Server']['table_info'])
  2031. || empty($GLOBALS['cfg']['Server']['table_coords'])
  2032. || empty($GLOBALS['cfg']['Server']['column_info'])
  2033. || empty($GLOBALS['cfg']['Server']['pdf_pages'])
  2034. || empty($GLOBALS['cfg']['Server']['history'])
  2035. || empty($GLOBALS['cfg']['Server']['recent'])
  2036. || empty($GLOBALS['cfg']['Server']['favorite'])
  2037. || empty($GLOBALS['cfg']['Server']['table_uiprefs'])
  2038. || empty($GLOBALS['cfg']['Server']['tracking'])
  2039. || empty($GLOBALS['cfg']['Server']['userconfig'])
  2040. || empty($GLOBALS['cfg']['Server']['users'])
  2041. || empty($GLOBALS['cfg']['Server']['usergroups'])
  2042. || empty($GLOBALS['cfg']['Server']['navigationhiding'])
  2043. || empty($GLOBALS['cfg']['Server']['savedsearches'])
  2044. || empty($GLOBALS['cfg']['Server']['central_columns'])
  2045. || empty($GLOBALS['cfg']['Server']['designer_settings'])
  2046. || empty($GLOBALS['cfg']['Server']['export_templates']));
  2047. }
  2048. /**
  2049. * Get tables for foreign key constraint
  2050. *
  2051. * @param string $foreignDb Database name
  2052. * @param string $tblStorageEngine Table storage engine
  2053. *
  2054. * @return array Table names
  2055. */
  2056. public function getTables($foreignDb, $tblStorageEngine)
  2057. {
  2058. $tables = [];
  2059. $tablesRows = $this->dbi->query(
  2060. 'SHOW TABLE STATUS FROM ' . Util::backquote($foreignDb),
  2061. DatabaseInterface::CONNECT_USER,
  2062. DatabaseInterface::QUERY_STORE
  2063. );
  2064. while ($row = $this->dbi->fetchRow($tablesRows)) {
  2065. if (! isset($row[1]) || mb_strtoupper($row[1]) != $tblStorageEngine) {
  2066. continue;
  2067. }
  2068. $tables[] = $row[0];
  2069. }
  2070. if ($GLOBALS['cfg']['NaturalOrder']) {
  2071. usort($tables, 'strnatcasecmp');
  2072. }
  2073. return $tables;
  2074. }
  2075. }