3 * Functions for the replication GUI
6 declare(strict_types
=1);
8 namespace PhpMyAdmin\Replication
;
10 use PhpMyAdmin\Config
;
11 use PhpMyAdmin\DatabaseInterface
;
12 use PhpMyAdmin\Dbal\ConnectionType
;
13 use PhpMyAdmin\Message
;
14 use PhpMyAdmin\Query\Utilities
;
15 use PhpMyAdmin\ResponseRenderer
;
16 use PhpMyAdmin\Template
;
21 use function htmlspecialchars
;
22 use function in_array
;
23 use function mb_strrpos
;
24 use function mb_strtolower
;
25 use function mb_substr
;
27 use function str_replace
;
32 * Functions for the replication GUI
36 public function __construct(private Replication
$replication, private Template
$template)
41 * returns HTML for error message
43 * @return string HTML code
45 public function getHtmlForErrorMessage(): string
48 if (isset($_SESSION['replication']['sr_action_status'], $_SESSION['replication']['sr_action_info'])) {
49 if ($_SESSION['replication']['sr_action_status'] === 'error') {
50 $errorMessage = $_SESSION['replication']['sr_action_info'];
51 $html .= Message
::error($errorMessage)->getDisplay();
52 $_SESSION['replication']['sr_action_status'] = 'unknown';
53 } elseif ($_SESSION['replication']['sr_action_status'] === 'success') {
54 $successMessage = $_SESSION['replication']['sr_action_info'];
55 $html .= Message
::success($successMessage)->getDisplay();
56 $_SESSION['replication']['sr_action_status'] = 'unknown';
64 * returns HTML for primary replication
66 * @return string HTML code
68 public function getHtmlForPrimaryReplication(
69 string|
null $connection,
70 bool $hasReplicaClearScreen,
71 string|
null $primaryAddUser,
72 string|
null $username,
73 string|
null $hostname,
75 if (! $hasReplicaClearScreen) {
76 $primaryStatusTable = $this->getHtmlForReplicationStatusTable($connection, 'primary', true, false);
77 $replicas = DatabaseInterface
::getInstance()->fetchResult('SHOW SLAVE HOSTS');
79 $urlParams = $GLOBALS['urlParams'];
80 $urlParams['primary_add_user'] = true;
81 $urlParams['replica_clear_screen'] = true;
84 if ($primaryAddUser !== null) {
85 $primaryAddReplicaUser = $this->getHtmlForReplicationPrimaryAddReplicaUser($username, $hostname);
88 return $this->template
->render('server/replication/primary_replication', [
89 'clear_screen' => $hasReplicaClearScreen,
90 'primary_status_table' => $primaryStatusTable ??
'',
91 'replicas' => $replicas ??
[],
92 'url_params' => $urlParams ??
[],
93 'primary_add_user' => $primaryAddUser !== null,
94 'primary_add_replica_user' => $primaryAddReplicaUser ??
'',
99 * returns HTML for primary replication configuration
101 * @return string HTML code
103 public function getHtmlForPrimaryConfiguration(): string
105 $databaseMultibox = $this->getHtmlForReplicationDbMultibox();
107 return $this->template
->render(
108 'server/replication/primary_configuration',
109 ['database_multibox' => $databaseMultibox],
114 * returns HTML for replica replication configuration
116 * @param string|null $connection Primary connection
117 * @param bool $serverReplicaStatus Whether it is Primary or Replica
118 * @param mixed[] $serverReplicaReplication Replica replication
119 * @param bool $replicaConfigure Replica configure
121 * @return string HTML code
123 public function getHtmlForReplicaConfiguration(
124 string|
null $connection,
125 bool $serverReplicaStatus,
126 array $serverReplicaReplication,
127 bool $replicaConfigure,
129 $serverReplicaMultiReplication = DatabaseInterface
::getInstance()->fetchResult('SHOW ALL SLAVES STATUS');
130 if ($serverReplicaStatus) {
131 $urlParams = $GLOBALS['urlParams'];
132 $urlParams['sr_take_action'] = true;
133 $urlParams['sr_replica_server_control'] = true;
135 if ($serverReplicaReplication[0]['Slave_IO_Running'] === 'No') {
136 $urlParams['sr_replica_action'] = 'start';
138 $urlParams['sr_replica_action'] = 'stop';
141 $urlParams['sr_replica_control_param'] = 'IO_THREAD';
142 $replicaControlIoLink = Url
::getCommon($urlParams, '', false);
144 if ($serverReplicaReplication[0]['Slave_SQL_Running'] === 'No') {
145 $urlParams['sr_replica_action'] = 'start';
147 $urlParams['sr_replica_action'] = 'stop';
150 $urlParams['sr_replica_control_param'] = 'SQL_THREAD';
151 $replicaControlSqlLink = Url
::getCommon($urlParams, '', false);
154 $serverReplicaReplication[0]['Slave_IO_Running'] === 'No'
155 ||
$serverReplicaReplication[0]['Slave_SQL_Running'] === 'No'
157 $urlParams['sr_replica_action'] = 'start';
159 $urlParams['sr_replica_action'] = 'stop';
162 $urlParams['sr_replica_control_param'] = null;
163 $replicaControlFullLink = Url
::getCommon($urlParams, '', false);
165 $urlParams['sr_replica_action'] = 'reset';
166 $replicaControlResetLink = Url
::getCommon($urlParams, '', false);
168 $urlParams = $GLOBALS['urlParams'];
169 $urlParams['sr_take_action'] = true;
170 $urlParams['sr_replica_skip_error'] = true;
171 $replicaSkipErrorLink = Url
::getCommon($urlParams, '', false);
173 $urlParams = $GLOBALS['urlParams'];
174 $urlParams['replica_configure'] = true;
175 $urlParams['replica_clear_screen'] = true;
177 $reconfigurePrimaryLink = Url
::getCommon($urlParams, '', false);
179 $replicaStatusTable = $this->getHtmlForReplicationStatusTable($connection, 'replica', true, false);
181 $replicaIoRunning = $serverReplicaReplication[0]['Slave_IO_Running'] !== 'No';
182 $replicaSqlRunning = $serverReplicaReplication[0]['Slave_SQL_Running'] !== 'No';
185 return $this->template
->render('server/replication/replica_configuration', [
186 'server_replica_multi_replication' => $serverReplicaMultiReplication,
187 'url_params' => $GLOBALS['urlParams'],
188 'primary_connection' => $connection ??
'',
189 'server_replica_status' => $serverReplicaStatus,
190 'replica_status_table' => $replicaStatusTable ??
'',
191 'replica_sql_running' => $replicaSqlRunning ??
false,
192 'replica_io_running' => $replicaIoRunning ??
false,
193 'replica_control_full_link' => $replicaControlFullLink ??
'',
194 'replica_control_reset_link' => $replicaControlResetLink ??
'',
195 'replica_control_sql_link' => $replicaControlSqlLink ??
'',
196 'replica_control_io_link' => $replicaControlIoLink ??
'',
197 'replica_skip_error_link' => $replicaSkipErrorLink ??
'',
198 'reconfigure_primary_link' => $reconfigurePrimaryLink ??
'',
199 'has_replica_configure' => $replicaConfigure,
204 * returns HTML code for selecting databases
206 * @return string HTML code
208 public function getHtmlForReplicationDbMultibox(): string
211 foreach (DatabaseInterface
::getInstance()->getDatabaseList() as $database) {
212 if (Utilities
::isSystemSchema($database)) {
216 $databases[] = $database;
219 return $this->template
->render('server/replication/database_multibox', ['databases' => $databases]);
223 * returns HTML for changing primary
225 * @param string $submitName submit button name
227 * @return string HTML code
229 public function getHtmlForReplicationChangePrimary(string $submitName): string
231 [$usernameLength, $hostnameLength] = $this->getUsernameHostnameLength();
233 return $this->template
->render('server/replication/change_primary', [
234 'server_id' => time(),
235 'username_length' => $usernameLength,
236 'hostname_length' => $hostnameLength,
237 'submit_name' => $submitName,
242 * This function returns html code for table with replication status.
244 * @param string|null $connection primary connection
245 * @param string $type either primary or replica
246 * @param bool $isHidden if true, then default style is set to hidden, default value false
247 * @param bool $hasTitle if true, then title is displayed, default true
249 * @return string HTML code
251 public function getHtmlForReplicationStatusTable(
252 string|
null $connection,
254 bool $isHidden = false,
255 bool $hasTitle = true,
257 $replicationInfo = new ReplicationInfo(DatabaseInterface
::getInstance());
258 $replicationInfo->load($connection);
260 $replicationVariables = $replicationInfo->primaryVariables
;
261 $variablesAlerts = null;
262 $variablesOks = null;
263 $serverReplication = $replicationInfo->getPrimaryStatus();
264 if ($type === 'replica') {
265 $replicationVariables = $replicationInfo->replicaVariables
;
266 $variablesAlerts = ['Slave_IO_Running' => 'No', 'Slave_SQL_Running' => 'No'];
267 $variablesOks = ['Slave_IO_Running' => 'Yes', 'Slave_SQL_Running' => 'Yes'];
268 $serverReplication = $replicationInfo->getReplicaStatus();
272 foreach ($replicationVariables as $variable) {
273 $serverReplicationVariable = isset($serverReplication[0])
274 ?
$serverReplication[0][$variable]
277 $variables[$variable] = ['name' => $variable, 'status' => '', 'value' => $serverReplicationVariable];
279 if (isset($variablesAlerts[$variable]) && $variablesAlerts[$variable] === $serverReplicationVariable) {
280 $variables[$variable]['status'] = 'text-danger';
281 } elseif (isset($variablesOks[$variable]) && $variablesOks[$variable] === $serverReplicationVariable) {
282 $variables[$variable]['status'] = 'text-success';
287 'Replicate_Ignore_DB',
288 'Replicate_Do_Table',
289 'Replicate_Ignore_Table',
290 'Replicate_Wild_Do_Table',
291 'Replicate_Wild_Ignore_Table',
293 if (! in_array($variable, $variablesWrap, true)) {
297 $variables[$variable]['value'] = str_replace(',', ', ', $serverReplicationVariable);
300 return $this->template
->render('server/replication/status_table', [
302 'is_hidden' => $isHidden,
303 'has_title' => $hasTitle,
304 'variables' => $variables,
309 * get the correct username and hostname lengths for this MySQL server
311 * @return array<int,int> username length, hostname length
313 public function getUsernameHostnameLength(): array
315 $fieldsInfo = DatabaseInterface
::getInstance()->getColumns('mysql', 'user');
316 $usernameLength = 16;
317 $hostnameLength = 41;
318 foreach ($fieldsInfo as $val) {
319 if ($val->field
=== 'User') {
320 strtok($val->type
, '()');
322 if (Util
::isInteger($v)) {
323 $usernameLength = (int) $v;
325 } elseif ($val->field
=== 'Host') {
326 strtok($val->type
, '()');
328 if (Util
::isInteger($v)) {
329 $hostnameLength = (int) $v;
334 return [$usernameLength, $hostnameLength];
338 * returns html code to add a replication replica user to the primary
340 * @return string HTML code
342 public function getHtmlForReplicationPrimaryAddReplicaUser(string|
null $postUsername, string|
null $hostname): string
344 [$usernameLength, $hostnameLength] = $this->getUsernameHostnameLength();
347 if ($postUsername === '') {
348 $GLOBALS['pred_username'] = 'any';
349 } elseif ($postUsername !== null && $postUsername !== '0') {
350 $username = $GLOBALS['new_username'] ??
$postUsername;
353 $currentUser = DatabaseInterface
::getInstance()->fetchValue('SELECT USER();');
354 if ($currentUser !== '' && $currentUser !== false && $currentUser !== null) {
355 $userHost = str_replace(
360 mb_strrpos($currentUser, '@') +
1,
363 if ($userHost !== 'localhost' && $userHost !== '127.0.0.1') {
364 $thisHost = $userHost;
368 // when we start editing a user, $GLOBALS['pred_hostname'] is not defined
369 if (! isset($GLOBALS['pred_hostname']) && $hostname !== null) {
370 $GLOBALS['pred_hostname'] = match (mb_strtolower($hostname)) {
371 'localhost', '127.0.0.1' => 'localhost',
373 default => 'userdefined',
377 return $this->template
->render('server/replication/primary_add_replica_user', [
378 'username_length' => $usernameLength,
379 'hostname_length' => $hostnameLength,
380 'has_username' => $postUsername !== null,
381 'username' => $username,
382 'hostname' => $hostname ??
'',
383 'predefined_username' => $GLOBALS['pred_username'] ??
'',
384 'predefined_hostname' => $GLOBALS['pred_hostname'] ??
'',
385 'this_host' => $thisHost ??
null,
390 * handle control requests
392 public function handleControlRequest(
394 bool $replicaChangePrimary,
395 bool $srReplicaServerControl,
396 string|
null $srReplicaAction,
397 bool $srReplicaSkipError,
398 int $srSkipErrorsCount,
399 string|
null $srReplicaControlParam,
405 if (! $srTakeAction) {
411 $messageSuccess = '';
414 if ($replicaChangePrimary && ! Config
::getInstance()->settings
['AllowArbitraryServer']) {
415 $_SESSION['replication']['sr_action_status'] = 'error';
416 $_SESSION['replication']['sr_action_info'] = __(
417 'Connection to server is disabled, please enable'
418 . ' $cfg[\'AllowArbitraryServer\'] in phpMyAdmin configuration.',
420 } elseif ($replicaChangePrimary) {
421 $result = $this->handleRequestForReplicaChangePrimary($username, $pmaPassword, $hostname, $port);
422 } elseif ($srReplicaServerControl) {
423 $result = $this->handleRequestForReplicaServerControl($srReplicaAction, $srReplicaControlParam);
426 switch ($srReplicaAction) {
428 $messageSuccess = __('Replication started successfully.');
429 $messageError = __('Error starting replication.');
432 $messageSuccess = __('Replication stopped successfully.');
433 $messageError = __('Error stopping replication.');
436 $messageSuccess = __('Replication resetting successfully.');
437 $messageError = __('Error resetting replication.');
440 $messageSuccess = __('Success.');
441 $messageError = __('Error.');
444 } elseif ($srReplicaSkipError) {
445 $result = $this->handleRequestForReplicaSkipError($srSkipErrorsCount);
449 $response = ResponseRenderer
::getInstance();
450 if ($response->isAjax()) {
451 $response->setRequestStatus($result);
455 ? Message
::success($messageSuccess)
456 : Message
::error($messageError),
460 './index.php?route=/server/replication' . Url
::getCommonRaw($GLOBALS['urlParams'], '&'),
468 public function handleRequestForReplicaChangePrimary(
474 $_SESSION['replication']['m_username'] = $username;
475 $_SESSION['replication']['m_password'] = $pmaPassword;
476 $_SESSION['replication']['m_hostname'] = $hostname;
477 $_SESSION['replication']['m_port'] = $port;
478 $_SESSION['replication']['m_correct'] = '';
480 // Attempt to connect to the new primary server
481 $connectionToPrimary = $this->replication
->connectToPrimary($username, $pmaPassword, $hostname, $port);
483 if ($connectionToPrimary === null) {
484 $_SESSION['replication']['sr_action_status'] = 'error';
485 $_SESSION['replication']['sr_action_info'] = sprintf(
486 __('Unable to connect to primary %s.'),
487 htmlspecialchars($hostname),
490 // Read the current primary position
491 $position = $this->replication
->replicaBinLogPrimary(ConnectionType
::Auxiliary
);
493 if ($position === []) {
494 $_SESSION['replication']['sr_action_status'] = 'error';
495 $_SESSION['replication']['sr_action_info'] = __(
496 'Unable to read primary log position. Possible privilege problem on primary.',
499 $_SESSION['replication']['m_correct'] = true;
502 ! $this->replication
->replicaChangePrimary(
510 ConnectionType
::User
,
513 $_SESSION['replication']['sr_action_status'] = 'error';
514 $_SESSION['replication']['sr_action_info'] = __('Unable to change primary!');
516 $_SESSION['replication']['sr_action_status'] = 'success';
517 $_SESSION['replication']['sr_action_info'] = sprintf(
518 __('Primary server changed successfully to %s.'),
519 htmlspecialchars($hostname),
525 return $_SESSION['replication']['sr_action_status'] === 'success';
528 public function handleRequestForReplicaServerControl(string|
null $srReplicaAction, string|
null $control): bool
530 if ($srReplicaAction === 'reset') {
531 $qStop = $this->replication
->replicaControl('STOP', null, ConnectionType
::User
);
532 $qReset = DatabaseInterface
::getInstance()->tryQuery('RESET SLAVE;');
533 $qStart = $this->replication
->replicaControl('START', null, ConnectionType
::User
);
535 return $qStop !== false && $qStop !== -1 && $qReset !== false && $qStart !== false && $qStart !== -1;
538 $qControl = $this->replication
->replicaControl($srReplicaAction, $control, ConnectionType
::User
);
540 return $qControl !== false && $qControl !== -1;
543 public function handleRequestForReplicaSkipError(int $srSkipErrorsCount): bool
545 $dbi = DatabaseInterface
::getInstance();
546 $qStop = $this->replication
->replicaControl('STOP', null, ConnectionType
::User
);
547 $qSkip = $dbi->tryQuery('SET GLOBAL SQL_SLAVE_SKIP_COUNTER = ' . $srSkipErrorsCount . ';');
548 $qStart = $this->replication
->replicaControl('START', null, ConnectionType
::User
);
550 return $qStop !== false && $qStop !== -1 && $qSkip !== false && $qStart !== false && $qStart !== -1;