| 1: | <?php |
| 2: | declare(strict_types=1); |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | use Daphnie\Collector; |
| 16: | use Daphnie\Metrics\Mysql as MysqlMetrics; |
| 17: | use Module\Skeleton\Contracts\Tasking; |
| 18: | use Module\Support\Sql; |
| 19: | use Opcenter\Database\MySQL\DefinerRemap; |
| 20: | use Opcenter\Net\IpCommon; |
| 21: | |
| 22: | |
| 23: | |
| 24: | |
| 25: | |
| 26: | |
| 27: | class Mysql_Module extends Sql implements Tasking |
| 28: | { |
| 29: | const DEPENDENCY_MAP = [ |
| 30: | 'siteinfo', |
| 31: | 'diskquota' |
| 32: | ]; |
| 33: | const MYSQL_DATADIR = '/var/lib/mysql'; |
| 34: | |
| 35: | const NEW_API_VERSION = 50720; |
| 36: | |
| 37: | const DEFAULT_CONCURRENCY_LIMIT = 10; |
| 38: | |
| 39: | const MAX_CONCURRENCY_LIMIT = MYSQL_CONCURRENCY_LIMIT ?: DATABASE_CONCURRENCY_LIMIT; |
| 40: | |
| 41: | |
| 42: | const EXPORT_CMD = '/usr/bin/mysqldump --quick --single-transaction --add-drop-table --add-drop-trigger --triggers --events -q -R'; |
| 43: | |
| 44: | protected $exportedFunctions = array( |
| 45: | '*' => PRIVILEGE_SITE, |
| 46: | 'version' => PRIVILEGE_ALL, |
| 47: | 'get_elevated_password_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
| 48: | 'create_database_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 49: | 'delete_database_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 50: | 'get_uptime' => PRIVILEGE_ALL, |
| 51: | 'assert_permissions' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 52: | 'set_option' => PRIVILEGE_ALL, |
| 53: | 'get_option' => PRIVILEGE_ALL, |
| 54: | 'export_pipe_real' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 55: | 'enabled' => PRIVILEGE_SITE | PRIVILEGE_USER, |
| 56: | 'repair_database' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 57: | 'get_prefix' => PRIVILEGE_SITE | PRIVILEGE_USER, |
| 58: | |
| 59: | |
| 60: | 'get_database_size' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 61: | 'database_exists' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 62: | '_export_old' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 63: | 'resolve_site_from_database' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 64: | 'recover_innodb_from_disk' => PRIVILEGE_ADMIN, |
| 65: | 'stats' => PRIVILEGE_ALL |
| 66: | ); |
| 67: | |
| 68: | public function __destruct() |
| 69: | { |
| 70: | foreach ($this->_tempUsers as $user) { |
| 71: | if (!$this->user_exists($user)) { |
| 72: | continue; |
| 73: | } |
| 74: | $this->_delete_temp_user($user); |
| 75: | } |
| 76: | } |
| 77: | |
| 78: | public function user_exists($user, $host = 'localhost') |
| 79: | { |
| 80: | if (!$user) { |
| 81: | return false; |
| 82: | } |
| 83: | $conn = $this->_connect_root(); |
| 84: | $prefix = $this->get_prefix(); |
| 85: | if (!($this->permission_level & PRIVILEGE_ADMIN) && $user !== $this->databaseAdmin() && |
| 86: | 0 !== strpos($user, $prefix) |
| 87: | ) { |
| 88: | $user = $prefix . $user; |
| 89: | } |
| 90: | |
| 91: | $q = $conn->query("SELECT user FROM user WHERE user = '" . |
| 92: | $conn->escape_string($user) . "' AND host = '" . $conn->escape_string($host) . "'"); |
| 93: | |
| 94: | return !$q || $q->num_rows > 0; |
| 95: | } |
| 96: | |
| 97: | public function get_prefix() |
| 98: | { |
| 99: | return $this->getServiceValue('mysql', 'dbaseprefix'); |
| 100: | } |
| 101: | |
| 102: | |
| 103: | |
| 104: | |
| 105: | |
| 106: | |
| 107: | |
| 108: | |
| 109: | private function _delete_temp_user($user) |
| 110: | { |
| 111: | if (!Opcenter\Database\MySQL::deleteUser($user, 'localhost')) { |
| 112: | return false; |
| 113: | } |
| 114: | |
| 115: | |
| 116: | $idx = array_search($user, $this->_tempUsers, true); |
| 117: | if ($idx !== false) { |
| 118: | unset($this->_tempUsers[$idx]); |
| 119: | } |
| 120: | |
| 121: | return true; |
| 122: | } |
| 123: | |
| 124: | |
| 125: | |
| 126: | |
| 127: | |
| 128: | |
| 129: | |
| 130: | |
| 131: | public function delete_user($user, $host) |
| 132: | { |
| 133: | if ($user === $this->databaseAdmin() && !Util_Account_Hooks::is_mode('delete')) { |
| 134: | return error('Cannot remove main user'); |
| 135: | } else if (!$this->user_exists($user, $host)) { |
| 136: | return error("user `%s' on `%s' does not exist", $user, $host); |
| 137: | } |
| 138: | $prefix = $this->get_prefix(); |
| 139: | if ($user !== $this->databaseAdmin() && strpos($user, $prefix) !== 0) { |
| 140: | $user = $prefix . $user; |
| 141: | } |
| 142: | if (\Opcenter\Database\MySQL::version() >= static::NEW_API_VERSION) { |
| 143: | return Opcenter\Database\MySQL::deleteUser($user, $host); |
| 144: | } |
| 145: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 146: | $conn->select_db('mysql'); |
| 147: | $stmt = $conn->prepare('DELETE FROM user WHERE user = ? AND host = ?'); |
| 148: | $stmt->bind_param('ss', $user, $host); |
| 149: | $stmt->execute(); |
| 150: | if ($stmt->error) { |
| 151: | return new MySQLError('Invalid query, ' . $stmt->error); |
| 152: | } |
| 153: | |
| 154: | $stmt2 = $conn->prepare('DELETE FROM db WHERE user = ? AND host = ?'); |
| 155: | $stmt2->bind_param('ss', $user, $host); |
| 156: | $stmt2->execute(); |
| 157: | if (!$stmt2->error) { |
| 158: | $conn->query('FLUSH PRIVILEGES'); |
| 159: | } else { |
| 160: | return new MySQLError('Invalid query, ' . $stmt2->error); |
| 161: | } |
| 162: | |
| 163: | return ($stmt->affected_rows > 0); |
| 164: | |
| 165: | } |
| 166: | |
| 167: | |
| 168: | |
| 169: | |
| 170: | |
| 171: | |
| 172: | |
| 173: | public function store_password($sqlpasswd) |
| 174: | { |
| 175: | return $this->set_option('password', $sqlpasswd, 'client'); |
| 176: | } |
| 177: | |
| 178: | |
| 179: | |
| 180: | |
| 181: | |
| 182: | |
| 183: | |
| 184: | |
| 185: | |
| 186: | |
| 187: | |
| 188: | |
| 189: | public function set_option(string $option, string $value = null, string $group = 'client') |
| 190: | { |
| 191: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
| 192: | return error("Demo may not alter ~/.my.cnf"); |
| 193: | } |
| 194: | |
| 195: | if (!IS_CLI) { |
| 196: | return $this->query('mysql_set_option', $option, $value, $group); |
| 197: | } |
| 198: | $home = $this->user_get_user_home(); |
| 199: | $path = $this->domain_fs_path() . "{$home}/.my.cnf"; |
| 200: | if (is_link($path) && ( |
| 201: | false === ($link = readlink($path)) || |
| 202: | 0 !== strpos($this->domain_fs_path(), realpath($link))) |
| 203: | ) |
| 204: | { |
| 205: | unlink($path); |
| 206: | } |
| 207: | if (!file_exists($path)) { |
| 208: | \Opcenter\Filesystem::touch($path, $this->user_id, $this->group_id, 0600); |
| 209: | } |
| 210: | |
| 211: | return \Opcenter\Database\MySQL::setUserConfigurationField($path, $option, $value, $group); |
| 212: | } |
| 213: | |
| 214: | |
| 215: | |
| 216: | |
| 217: | |
| 218: | |
| 219: | public function get_password() |
| 220: | { |
| 221: | return $this->get_option('password'); |
| 222: | } |
| 223: | |
| 224: | |
| 225: | |
| 226: | |
| 227: | |
| 228: | |
| 229: | |
| 230: | |
| 231: | public function get_option($option, $group = 'client') |
| 232: | { |
| 233: | if (DEMO_ADMIN_LOCK && ($this->permission_level & PRIVILEGE_ADMIN) && posix_getuid()) { |
| 234: | return error("Demo may not alter ~/.my.cnf"); |
| 235: | } |
| 236: | |
| 237: | if (!IS_CLI) { |
| 238: | return $this->query('mysql_get_option', $option, $group); |
| 239: | } |
| 240: | $home = $this->user_get_user_home(); |
| 241: | $paths = [ |
| 242: | $this->domain_fs_path() . "{$home}/.my.cnf", |
| 243: | '/etc/my.cnf' |
| 244: | ]; |
| 245: | foreach ($paths as $path) { |
| 246: | if (is_link($path) && 0 !== strpos($this->domain_fs_path(), realpath(readlink($path)))) { |
| 247: | |
| 248: | continue; |
| 249: | } |
| 250: | if (null !== ($val = array_get(\Opcenter\Database\MySQL::getUserConfiguration($path), |
| 251: | "{$group}.{$option}", null))) { |
| 252: | return $val; |
| 253: | } |
| 254: | } |
| 255: | |
| 256: | return null; |
| 257: | } |
| 258: | |
| 259: | public function get_elevated_password_backend() |
| 260: | { |
| 261: | if (!IS_CLI) { |
| 262: | fatal('needs execution from backend'); |
| 263: | } |
| 264: | |
| 265: | return Opcenter\Database\MySQL::rootPassword(); |
| 266: | } |
| 267: | |
| 268: | |
| 269: | |
| 270: | |
| 271: | |
| 272: | |
| 273: | |
| 274: | |
| 275: | |
| 276: | |
| 277: | public function import($db, $file) |
| 278: | { |
| 279: | if (!IS_CLI) { |
| 280: | return $this->query('mysql_import', $db, $file); |
| 281: | } |
| 282: | |
| 283: | $prefix = $this->get_prefix(); |
| 284: | |
| 285: | if (strncmp($db, $prefix, strlen($prefix))) { |
| 286: | $db = $prefix . $db; |
| 287: | } |
| 288: | |
| 289: | $dbs = $this->list_databases(); |
| 290: | if (false === array_search($db, $dbs, true)) { |
| 291: | return error("database `%s' does not exist", $db); |
| 292: | } |
| 293: | $unlink = null; |
| 294: | if (false === ($realfile = $this->_preImport($file, $unlink))) { |
| 295: | return false; |
| 296: | } |
| 297: | |
| 298: | $tempUser = $this->_create_temp_user($db); |
| 299: | if (!$tempUser) { |
| 300: | $this->_postImport($unlink); |
| 301: | return error('unable to import database'); |
| 302: | } |
| 303: | |
| 304: | |
| 305: | |
| 306: | |
| 307: | |
| 308: | |
| 309: | |
| 310: | |
| 311: | |
| 312: | |
| 313: | |
| 314: | $bulk = []; |
| 315: | |
| 316: | if (filetype($realfile) === 'file') { |
| 317: | $ret = Util_Process_Safe::exec('sed -i %s %s', [ |
| 318: | '{ |
| 319: | /DEFINER=[^* ]*\(\*\| \)/w /dev/stdout |
| 320: | s/DEFINER=[^* ]*\(\*\| \)/\1/g |
| 321: | }', |
| 322: | $realfile |
| 323: | ]); |
| 324: | |
| 325: | if (!$ret['success']) { |
| 326: | return error("Aborting import from `%s'. Failed to perform DEFINER pass: %s", $file, $ret['error']); |
| 327: | } |
| 328: | |
| 329: | |
| 330: | if (preg_match_all(Regex::SQL_MYSQL_DEFINER, $ret['stdout'], $matches, PREG_SET_ORDER)) { |
| 331: | |
| 332: | $bulkChangeCredentials = []; |
| 333: | $prefix = $this->get_prefix(); |
| 334: | $users = []; |
| 335: | foreach ($this->list_users() as $user => $hosts) { |
| 336: | if (0 === strpos($user, $prefix)) { |
| 337: | $user = substr($user, \strlen($prefix)); |
| 338: | } |
| 339: | $users[$user] = []; |
| 340: | foreach (array_keys($hosts) as $host) { |
| 341: | $users[$user][$host] = 1; |
| 342: | } |
| 343: | } |
| 344: | |
| 345: | foreach ($matches as $gm) { |
| 346: | |
| 347: | if ($gm['type'] === 'FUNCTION') { |
| 348: | $gm['type'] = 'PROCEDURE'; |
| 349: | } |
| 350: | |
| 351: | if (false !== ($pos = strpos($gm['user'], '_'))) { |
| 352: | $gm['user'] = substr($gm['user'], ++$pos); |
| 353: | } |
| 354: | |
| 355: | if (!isset($users[$gm['user']])) { |
| 356: | warn( |
| 357: | 'User %s not found in user table. Converting %s grant for %s to %s', |
| 358: | $gm['user'], $gm['type'], $gm['name'], $this->databaseAdmin() |
| 359: | ); |
| 360: | $gm['user'] = $this->databaseAdmin(); |
| 361: | $gm['host'] = 'localhost'; |
| 362: | } else if (!isset($users[$gm['user']][$gm['host']])) { |
| 363: | warn( |
| 364: | 'Host %s not found in user table. Converting %s grant for %s to localhost', |
| 365: | $gm['host'], $gm['type'], $gm['user'] |
| 366: | ); |
| 367: | $gm['host'] = 'localhost'; |
| 368: | } |
| 369: | |
| 370: | if (empty($bulkChangeCredentials)) { |
| 371: | $bulkChangeCredentials = [ |
| 372: | $gm['user'], |
| 373: | $gm['host'] |
| 374: | ]; |
| 375: | } else if ($bulkChangeCredentials !== [$gm['user'], $gm['host']]) { |
| 376: | warn( |
| 377: | 'Import only supports 1 DEFINER adjustment. Converting %s (type: %s) from `%s`@`%s` to `%s`@`%s`', |
| 378: | $gm['name'], $gm['type'], $gm['user'], $gm['host'], $bulkChangeCredentials[0], |
| 379: | $bulkChangeCredentials[1] |
| 380: | ); |
| 381: | } |
| 382: | if (empty($bulk[$gm['type']])) { |
| 383: | $bulk[$gm['type']] = []; |
| 384: | } |
| 385: | $bulk[$gm['type']][] = $gm['name']; |
| 386: | } |
| 387: | |
| 388: | if (isset($bulkChangeCredentials[0]) && $bulkChangeCredentials[0] !== $this->databaseAdmin()) { |
| 389: | $bulkChangeCredentials[0] = $this->getConfig('mysql', |
| 390: | 'dbaseprefix') . $bulkChangeCredentials[0]; |
| 391: | } |
| 392: | } |
| 393: | } |
| 394: | |
| 395: | |
| 396: | $safe = new Util_Process_Safe(); |
| 397: | $safe->setDescriptor(0, 'file', [$realfile, 'rb'], null, ['mute_stdin' => false]); |
| 398: | $safe->setPriority(19); |
| 399: | $safe->addCallback(function() use ($unlink, $tempUser) { |
| 400: | $this->_postImport($unlink); |
| 401: | $this->_delete_temp_user($tempUser); |
| 402: | return true; |
| 403: | }, 'close'); |
| 404: | |
| 405: | $status = $safe->run('mysql -u %s %s', $tempUser, $db); |
| 406: | if (!$status['success']) { |
| 407: | return false; |
| 408: | } |
| 409: | if ($bulk) { |
| 410: | $handler = new DefinerRemap($db, $this->_connect_root(true)); |
| 411: | |
| 412: | $handler->remapAll(...$bulkChangeCredentials); |
| 413: | } |
| 414: | |
| 415: | return $status['success']; |
| 416: | } |
| 417: | |
| 418: | |
| 419: | |
| 420: | |
| 421: | |
| 422: | |
| 423: | |
| 424: | public function resolve_site_from_database(string $db): ?string |
| 425: | { |
| 426: | $db = strtok($db, '/'); |
| 427: | $prefix = strtok($db, '_') . '_'; |
| 428: | |
| 429: | if ($this->site) { |
| 430: | return $this->database_exists($db) ? $this->site : null; |
| 431: | } |
| 432: | |
| 433: | if (!preg_match(Regex::SQL_DATABASE, $db)) { |
| 434: | error("Invalid database name"); |
| 435: | return null; |
| 436: | } |
| 437: | |
| 438: | if ($db === $prefix) { |
| 439: | $path = realpath(self::MYSQL_DATADIR . '/' . $prefix); |
| 440: | if (!$path || !str_starts_with($path, FILESYSTEM_VIRTBASE)) { |
| 441: | return null; |
| 442: | } |
| 443: | $site = strtok(substr($path, strlen(FILESYSTEM_VIRTBASE)), '/'); |
| 444: | } else { |
| 445: | $site = Opcenter\Map::read(\Opcenter\Database\MySQL::PREFIX_MAP)[$prefix]; |
| 446: | } |
| 447: | |
| 448: | if (!$site && $this->site && !str_starts_with($db, $prefix)) { |
| 449: | $db = $this->get_prefix() . $db; |
| 450: | |
| 451: | return $this->resolve_site_from_database($db); |
| 452: | } |
| 453: | |
| 454: | return $site ?: null; |
| 455: | } |
| 456: | |
| 457: | |
| 458: | |
| 459: | |
| 460: | |
| 461: | |
| 462: | |
| 463: | |
| 464: | |
| 465: | |
| 466: | |
| 467: | public function recover_innodb_from_disk(string $db, string $srcdir, bool $force = false): bool |
| 468: | { |
| 469: | if (!IS_CLI) { |
| 470: | return $this->query('mysql_recover_innodb_from_disk', $db, $srcdir, $force); |
| 471: | } |
| 472: | |
| 473: | if (!\Opcenter\Database\MySQL::databaseExists($db)) { |
| 474: | return error("Database `%s' does not exist", $db); |
| 475: | } |
| 476: | if (!is_dir($srcdir)) { |
| 477: | return error("Source path `%s' is not a directory", $srcdir); |
| 478: | } |
| 479: | |
| 480: | $glob = array_map( |
| 481: | static function ($file) { return basename($file, '.ibd'); }, |
| 482: | glob("{$srcdir}/*.ibd", GLOB_NOESCAPE) |
| 483: | ); |
| 484: | |
| 485: | if (empty($glob)) { |
| 486: | return error("No matching *.ibd files found in `%s'", $srcdir); |
| 487: | } |
| 488: | $tables = \Opcenter\Database\MySQL::getTablesFromDatabase($db); |
| 489: | |
| 490: | |
| 491: | $missingSource = array_values(array_diff($tables, $glob)); |
| 492: | |
| 493: | $missingSchema = array_values(array_diff($glob, $tables)); |
| 494: | |
| 495: | $user = $this->_create_temp_user($db); |
| 496: | |
| 497: | $datadir = realpath(self::MYSQL_DATADIR . '/' . \Opcenter\Database\MySQL::canonicalize($db)); |
| 498: | if (!$datadir || !($stat = stat($datadir))) { |
| 499: | return error( |
| 500: | "Unable to stat `%s'", |
| 501: | self::MYSQL_DATADIR . '/' . \Opcenter\Database\MySQL::canonicalize($db) |
| 502: | ); |
| 503: | } |
| 504: | |
| 505: | $conn = MySQL::stub(); |
| 506: | $conn->connect('localhost', $user, null, $db); |
| 507: | $conn->query('SET FOREIGN_KEY_CHECKS=0'); |
| 508: | |
| 509: | $importer = static function($table) use ($conn, $srcdir, $datadir, $stat): bool { |
| 510: | $ex = \Error_Reporter::exception_upgrade(\Error_Reporter::E_FATAL); |
| 511: | try { |
| 512: | $conn->query("LOCK TABLES `{$table}` WRITE"); |
| 513: | } catch (apnscpException $e) { |
| 514: | |
| 515: | } finally { |
| 516: | \Error_Reporter::exception_upgrade($ex); |
| 517: | } |
| 518: | |
| 519: | $conn->query("ALTER TABLE `{$table}` DISCARD TABLESPACE"); |
| 520: | |
| 521: | if (!copy("{$srcdir}/{$table}.ibd", "{$datadir}/{$table}.ibd")) { |
| 522: | return false; |
| 523: | } |
| 524: | if (!\Opcenter\Filesystem::chogp("{$datadir}/{$table}.ibd", $stat['uid'], $stat['gid'], 0600)) { |
| 525: | return false; |
| 526: | } |
| 527: | return (bool)$conn->query("ALTER TABLE `{$table}` IMPORT TABLESPACE"); |
| 528: | }; |
| 529: | |
| 530: | try { |
| 531: | |
| 532: | foreach (array_intersect($glob, $tables) as $table) { |
| 533: | if (!$force && file_exists("{$datadir}/{$table}.ibd")) { |
| 534: | info("InnoDB file `%(path)s/%(table)s.ibd' already exists - skipping %(db)s.%(table)s", |
| 535: | ['path' => $datadir, 'table' => $table, 'db' => $db]); |
| 536: | continue; |
| 537: | } |
| 538: | if (!$importer($table)) { |
| 539: | warn('Failed to replace table %(table)s in %(db)s', |
| 540: | ['table' => $table, 'db' => $db]); |
| 541: | } else { |
| 542: | info('Restored %(db)s.%(table)s', ['db' => $db, 'table' => $table]); |
| 543: | } |
| 544: | } |
| 545: | |
| 546: | |
| 547: | foreach ($missingSchema as $table) { |
| 548: | if (!file_exists("$srcdir/{$table}.frm")) { |
| 549: | |
| 550: | continue; |
| 551: | } |
| 552: | warn('%s.%s present in backup, .frm present in data directory, but .ibd missing - continuing import', $db, $table); |
| 553: | foreach (['ibd', 'frm'] as $ext) { |
| 554: | if (!$force && file_exists("{$datadir}/{$table}.{$ext}")) { |
| 555: | info("InnoDB file `%s/%s.%s' already exists - skipping %s.%s", |
| 556: | $datadir, $table, $ext, $db, $table); |
| 557: | continue; |
| 558: | } |
| 559: | $ret = copy("{$srcdir}/{$table}.{$ext}", "{$datadir}/{$table}.{$ext}") && |
| 560: | \Opcenter\Filesystem::chogp("{$datadir}/{$table}.{$ext}", $stat['uid'], $stat['gid'], 0600); |
| 561: | |
| 562: | if (!$ret) { |
| 563: | warn('Failed to replace table %s in %s', $table, $db); |
| 564: | continue 2; |
| 565: | } |
| 566: | } |
| 567: | |
| 568: | info('Restored %s.%s', $db, $table); |
| 569: | } |
| 570: | |
| 571: | foreach ($missingSource as $table) { |
| 572: | if (file_exists("{$datadir}/{$table}.ibd") || file_exists("{$datadir}/{$table}.MYI")) { |
| 573: | continue; |
| 574: | } |
| 575: | warn("%(db)s.%(table)s reported in `%(path)s' but no backup in %(srcdir)s", |
| 576: | ['db' => $db, 'table' => $table, 'path' => $datadir, 'srcdir' => $srcdir]); |
| 577: | } |
| 578: | } catch (\Throwable $e) { |
| 579: | return error("Failed to restore backup into `%(db)s': %(err)s", |
| 580: | ['db' => $db, 'err' => $e->getMessage()]); |
| 581: | } finally { |
| 582: | $conn->query('SET FOREIGN_KEY_CHECKS=1'); |
| 583: | } |
| 584: | |
| 585: | $conn->query('UNLOCK TABLES'); |
| 586: | return true; |
| 587: | } |
| 588: | |
| 589: | |
| 590: | |
| 591: | |
| 592: | |
| 593: | |
| 594: | |
| 595: | public function list_databases() |
| 596: | { |
| 597: | $prefix = $this->get_prefix(); |
| 598: | if (!$prefix) { |
| 599: | |
| 600: | if (!$this->enabled()) { |
| 601: | return []; |
| 602: | } |
| 603: | report('Prefixless site - bug'); |
| 604: | return []; |
| 605: | } |
| 606: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 607: | $conn->select_db('mysql'); |
| 608: | $q = $conn->query("SELECT DISTINCT(REPLACE(db,'\\_','_')) AS db FROM db WHERE db LIKE '" . str_replace('_', '\\_', $prefix) . "%' OR db LIKE '" . str_replace('_', '\\\\_', $prefix) . "%' OR user = '" . $this->databaseAdmin() . "'"); |
| 609: | $dbs = array(); |
| 610: | while (null !== ($row = $q->fetch_object())) { |
| 611: | $dbs[] = $row->db; |
| 612: | } |
| 613: | |
| 614: | $conn->close(); |
| 615: | |
| 616: | return $dbs; |
| 617: | } |
| 618: | |
| 619: | |
| 620: | |
| 621: | |
| 622: | |
| 623: | |
| 624: | |
| 625: | private function _create_temp_user($db) |
| 626: | { |
| 627: | |
| 628: | $prefix = (string)$this->get_prefix(); |
| 629: | $maxlen = \Opcenter\Database\MySQL::fieldLength('user') - strlen($prefix); |
| 630: | if ($maxlen < 1) { |
| 631: | warn('temp mysql user exceeds field length, cannot create user'); |
| 632: | return false; |
| 633: | } |
| 634: | $chars = array( |
| 635: | 'a', |
| 636: | 'b', |
| 637: | 'c', |
| 638: | 'd', |
| 639: | 'e', |
| 640: | 'f', |
| 641: | '0', |
| 642: | '1', |
| 643: | '2', |
| 644: | '3', |
| 645: | '4', |
| 646: | '5', |
| 647: | '6', |
| 648: | '7', |
| 649: | '8', |
| 650: | '9', |
| 651: | ); |
| 652: | $maxlen = min(8, $maxlen); |
| 653: | |
| 654: | $user = $prefix; |
| 655: | for ($i = 0; $i < $maxlen; $i++) { |
| 656: | $n = random_int(0, 15); |
| 657: | $user .= $chars[$n]; |
| 658: | } |
| 659: | |
| 660: | |
| 661: | $sqldb = $this->_connect_root(); |
| 662: | $q = "SELECT user FROM user WHERE user = '" . $user . "'"; |
| 663: | $rs = $sqldb->query($q); |
| 664: | if ($rs->num_rows > 0) { |
| 665: | return error('cannot create temp mysql user'); |
| 666: | } |
| 667: | $q = "CREATE USER '" . $user . "'@'localhost' IDENTIFIED BY ''"; |
| 668: | $rs = $sqldb->query($q); |
| 669: | if (!$rs) { |
| 670: | return error('failed to create temp mysql user'); |
| 671: | } |
| 672: | $q = 'GRANT ALL ON `' . $db . "`.* to '" . $sqldb->escape_string($user) . "'@localhost"; |
| 673: | $rs = $sqldb->query($q); |
| 674: | if (!$rs) { |
| 675: | return error('failed to create temp mysql user'); |
| 676: | } |
| 677: | $q = "GRANT SELECT ON mysql.proc TO '" . $sqldb->escape_string($user) . "'@localhost"; |
| 678: | $sqldb->query($q); |
| 679: | $this->_register_temp_user($user); |
| 680: | |
| 681: | return $user; |
| 682: | } |
| 683: | |
| 684: | |
| 685: | |
| 686: | |
| 687: | |
| 688: | |
| 689: | |
| 690: | public function change_prefix($prefix) |
| 691: | { |
| 692: | return error('use sql_change_prefix'); |
| 693: | } |
| 694: | |
| 695: | public function get_sql_prefix() |
| 696: | { |
| 697: | deprecated('use sql_get_prefix'); |
| 698: | |
| 699: | return $this->get_prefix(); |
| 700: | } |
| 701: | |
| 702: | |
| 703: | |
| 704: | |
| 705: | |
| 706: | public function list_users() |
| 707: | { |
| 708: | |
| 709: | if (!$prefix = $this->getServiceValue('mysql', 'dbaseprefix')) { |
| 710: | return []; |
| 711: | } |
| 712: | |
| 713: | $prefix = str_replace('_', '\_', $prefix); |
| 714: | if (!$prefix) { |
| 715: | report('Prefixless site - bug'); |
| 716: | |
| 717: | return []; |
| 718: | } |
| 719: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 720: | $conn->select_db('mysql'); |
| 721: | $q = $conn->query("SELECT host, |
| 722: | user, |
| 723: | password, |
| 724: | ssl_type, |
| 725: | ssl_cipher, |
| 726: | x509_issuer, |
| 727: | x509_subject, |
| 728: | max_questions, |
| 729: | max_updates, |
| 730: | max_connections, |
| 731: | max_user_connections FROM user WHERE user = '" . $this->databaseAdmin() . "' OR user LIKE '" . $prefix . "%'"); |
| 732: | $users = array(); |
| 733: | while (null !== ($row = $q->fetch_array(MYSQLI_ASSOC))) { |
| 734: | $row = array_change_key_case($row, CASE_LOWER); |
| 735: | $user = $row['user']; $host = $row['host']; |
| 736: | $users[$user][$host] = array( |
| 737: | 'ssl_type' => $row['ssl_type'], |
| 738: | 'ssl_cipher' => $row['ssl_cipher'], |
| 739: | 'x509_issuer' => $row['x509_issuer'], |
| 740: | 'x509_subject' => $row['x509_subject'], |
| 741: | 'max_questions' => (int)$row['max_questions'], |
| 742: | 'max_updates' => (int)$row['max_updates'], |
| 743: | 'max_user_connections' => (int)$row['max_user_connections'], |
| 744: | 'max_connections' => (int)$row['max_connections'], |
| 745: | 'password' => $row['password'], |
| 746: | ); |
| 747: | } |
| 748: | |
| 749: | return $users; |
| 750: | } |
| 751: | |
| 752: | |
| 753: | |
| 754: | |
| 755: | |
| 756: | |
| 757: | |
| 758: | |
| 759: | |
| 760: | |
| 761: | |
| 762: | |
| 763: | |
| 764: | |
| 765: | |
| 766: | |
| 767: | public function add_user( |
| 768: | $user, |
| 769: | $host, |
| 770: | $password, |
| 771: | $maxconn = self::DEFAULT_CONCURRENCY_LIMIT, |
| 772: | $maxupdates = 0, |
| 773: | $maxquery = 0, |
| 774: | $ssl = '', |
| 775: | $cipher = '', |
| 776: | $issuer = '', |
| 777: | $subject = '' |
| 778: | ) { |
| 779: | if (!$this->enabled()) { |
| 780: | return error("%(service)s is disabled", 'mysql'); |
| 781: | } |
| 782: | |
| 783: | if (!$user) { |
| 784: | return error('no username specified'); |
| 785: | } |
| 786: | $dbaseadmin = $this->databaseAdmin(); |
| 787: | if ($user === $dbaseadmin && !IS_SOAP) { |
| 788: | return error("cannot name user after primary account user, `%s'", $dbaseadmin); |
| 789: | } |
| 790: | |
| 791: | $ssl = strtoupper($ssl); |
| 792: | if (!$maxconn) { |
| 793: | $maxconn = self::DEFAULT_CONCURRENCY_LIMIT; |
| 794: | } |
| 795: | $host = trim($host); |
| 796: | if ($host != 'localhost' && !IpCommon::valid($host) && !preg_match(Regex::SQL_MYSQL_IP_WILDCARD, $host)) { |
| 797: | return error("rejected host `%s': only numeric IP addresses are permitted, not hostnames", $host); |
| 798: | } |
| 799: | if (strlen($password) < self::MIN_PASSWORD_LENGTH) { |
| 800: | return error('Password must be at least %d characters', self::MIN_PASSWORD_LENGTH); |
| 801: | } else if ($ssl !== '' && $ssl != 'ANY' && $ssl != 'X509' && $ssl != 'SPECIFIED') { |
| 802: | return error('Invalid SSL type'); |
| 803: | } else if ($maxconn < 1 || $maxquery < 0 || $maxupdates < 0) { |
| 804: | return error('Max connections, queries, and updates must be greater than 0'); |
| 805: | } else if ($maxconn > static::MAX_CONCURRENCY_LIMIT) { |
| 806: | return error('Max concurrent connections cannot exceed %d. ' . |
| 807: | 'Contact support if you need more than %d.', |
| 808: | static::MAX_CONCURRENCY_LIMIT, static::MAX_CONCURRENCY_LIMIT); |
| 809: | } else if ($this->user_exists($user, $host)) { |
| 810: | return error("mysql user `$user' on `$host' exists"); |
| 811: | } |
| 812: | $conn = $this->_connect_root(); |
| 813: | $prefix = $this->get_prefix(); |
| 814: | if ($user !== $this->databaseAdmin() && 0 !== strpos($user, $prefix)) { |
| 815: | |
| 816: | |
| 817: | $user = $prefix . $user; |
| 818: | } |
| 819: | if (Opcenter\Database\MySQL::version() >= static::NEW_API_VERSION) { |
| 820: | return \Opcenter\Database\MySQL::createUser( |
| 821: | $user, |
| 822: | $password, |
| 823: | $host, |
| 824: | [ |
| 825: | 'ssl' => $ssl, |
| 826: | 'cipher' => $cipher, |
| 827: | 'issuer' => $issuer, |
| 828: | 'subject' => $subject |
| 829: | ], |
| 830: | [ |
| 831: | 'connections' => (int)$maxconn, |
| 832: | 'query' => (int)$maxquery, |
| 833: | 'updates' => (int)$maxupdates |
| 834: | ] |
| 835: | ) ?: error("User creation failed on `%s@%s'", $user, $host); |
| 836: | } |
| 837: | $pwclause = 'password(?)'; |
| 838: | |
| 839: | if ((\strlen($password) === 16 && ctype_xdigit($password)) || |
| 840: | ($password[0] == '*' && \strlen($password) === 41 |
| 841: | && ctype_xdigit(substr($password, 1)))) |
| 842: | { |
| 843: | $pwclause = '?'; |
| 844: | } |
| 845: | $needAuth = $conn->columnExists('authentication_string', 'user'); |
| 846: | $query = 'INSERT INTO user |
| 847: | (host, |
| 848: | user, |
| 849: | password, |
| 850: | ssl_type, |
| 851: | ssl_cipher, |
| 852: | x509_issuer, |
| 853: | x509_subject, |
| 854: | max_questions, |
| 855: | max_updates, |
| 856: | max_user_connections' . ($needAuth ? ', authentication_string' : '') . ') |
| 857: | VALUES |
| 858: | (?, |
| 859: | ?, |
| 860: | ' . $pwclause . ', |
| 861: | ?, |
| 862: | ?, |
| 863: | ?, |
| 864: | ?, |
| 865: | ?, |
| 866: | ?, |
| 867: | ?' . ($needAuth ? ',""' : '') . ');'; |
| 868: | $stmt = $conn->prepare($query); |
| 869: | $stmt->bind_param('sssssssiii', $host, $user, $password, $ssl, $cipher, |
| 870: | $issuer, $subject, $maxquery, $maxupdates, $maxconn); |
| 871: | $stmt->execute(); |
| 872: | if ($stmt->error) { |
| 873: | return new MySQLError('Invalid query, ' . $stmt->error); |
| 874: | } |
| 875: | $conn->query('FLUSH PRIVILEGES;'); |
| 876: | if ($stmt->affected_rows < 1) { |
| 877: | return error("user creation `%s@%s' failed", $user, $host); |
| 878: | } |
| 879: | |
| 880: | return true; |
| 881: | } |
| 882: | |
| 883: | |
| 884: | |
| 885: | |
| 886: | |
| 887: | |
| 888: | |
| 889: | public function get_database_charset(string $db): ?string |
| 890: | { |
| 891: | if ($this->permission_level & (PRIVILEGE_USER|PRIVILEGE_SITE)) { |
| 892: | $prefix = $this->getServiceValue('mysql', 'dbaseprefix'); |
| 893: | if (0 !== strpos($db, $prefix)) { |
| 894: | $db = $prefix . $db; |
| 895: | } |
| 896: | } |
| 897: | if (!\in_array($db, $this->list_databases(), true)) { |
| 898: | error('Invalid database %s', $db); |
| 899: | return null; |
| 900: | } |
| 901: | $conn = $this->_connect_root(); |
| 902: | $q = "SELECT default_character_set_name FROM information_schema.SCHEMATA |
| 903: | WHERE schema_name = '" . $conn->escape_string($db). "';"; |
| 904: | $rs = $conn->query($q); |
| 905: | |
| 906: | if (!$rs->num_rows) { |
| 907: | return null; |
| 908: | } |
| 909: | return $rs->fetch_object()->default_character_set_name; |
| 910: | } |
| 911: | |
| 912: | |
| 913: | |
| 914: | |
| 915: | |
| 916: | |
| 917: | |
| 918: | |
| 919: | |
| 920: | |
| 921: | public function create_database($db, $charset = 'latin1', $collation = 'latin1_swedish_ci') |
| 922: | { |
| 923: | if (!IS_CLI) { |
| 924: | return $this->query('mysql_create_database', $db, $charset, $collation); |
| 925: | } |
| 926: | |
| 927: | if (!$this->enabled()) { |
| 928: | return error("%(service)s is disabled", 'mysql'); |
| 929: | } |
| 930: | |
| 931: | $charset = strtolower($charset); |
| 932: | $collation = strtolower($collation); |
| 933: | |
| 934: | if (!preg_match(Regex::SQL_DATABASE, $db)) { |
| 935: | return error("invalid database name `%s'", $db); |
| 936: | } |
| 937: | if (!$this->charset_valid($charset)) { |
| 938: | return error("unrecognized mysql charset `%s'", $charset); |
| 939: | } |
| 940: | if (!$this->collation_valid($collation)) { |
| 941: | return error("invalid mysql collation `%s'", $collation); |
| 942: | } else if (!$this->collation_compatible($collation, $charset)) { |
| 943: | warn("collation `%s' for charset `%s' not sensible", $collation, $charset); |
| 944: | } |
| 945: | |
| 946: | $prefix = $this->get_prefix(); |
| 947: | |
| 948: | |
| 949: | if (0 !== strpos($db, $prefix)) { |
| 950: | $db = $prefix . $db; |
| 951: | } |
| 952: | |
| 953: | if ($this->database_exists($db)) { |
| 954: | return error("database `$db' exists"); |
| 955: | } |
| 956: | |
| 957: | if (null !== ($limit = $this->getConfig('mysql', 'dbasenum', null)) && $limit >= 0) { |
| 958: | $count = \count($this->list_databases()); |
| 959: | if ($count >= $limit) { |
| 960: | return error("Database limit `%d' reached - cannot create additional databases", $limit); |
| 961: | } |
| 962: | } |
| 963: | |
| 964: | $status = $this->query('mysql_create_database_backend', $db, $charset, $collation); |
| 965: | if (!$status) { |
| 966: | return $status; |
| 967: | } |
| 968: | |
| 969: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 970: | $conn->select_db('mysql'); |
| 971: | $conn->query('GRANT ALL ON `' . str_replace('_', '\\_', $db) . "`.* to '" . $conn->escape_string($this->databaseAdmin()) . "'@localhost;"); |
| 972: | if ($conn->error) { |
| 973: | \Opcenter\Database\MySQL::dropDatabase($db); |
| 974: | |
| 975: | return error("failed to create db `%s'. Error while applying grants: `%s' " . |
| 976: | "- is control user `%s' missing?", |
| 977: | $db, |
| 978: | $conn->error, |
| 979: | $this->databaseAdmin() |
| 980: | ); |
| 981: | } |
| 982: | |
| 983: | return info("created database `%s'", $db); |
| 984: | |
| 985: | } |
| 986: | |
| 987: | public function charset_valid($charset): bool |
| 988: | { |
| 989: | $charset = strtolower($charset); |
| 990: | $charsets = $this->get_supported_charsets(); |
| 991: | |
| 992: | return array_key_exists($charset, $charsets); |
| 993: | } |
| 994: | |
| 995: | public function get_supported_charsets(): array |
| 996: | { |
| 997: | $cache = Cache_Global::spawn(); |
| 998: | $key = 's:mysql.char'; |
| 999: | $charsets = $cache->get($key); |
| 1000: | if ($charsets) { |
| 1001: | return $charsets; |
| 1002: | } |
| 1003: | $db = MySQL::initialize(); |
| 1004: | $rs = $db->query('SELECT CHARACTER_SET_NAME AS charset, DESCRIPTION AS `desc` FROM INFORMATION_SCHEMA.character_sets'); |
| 1005: | $charsets = array(); |
| 1006: | while (null !== ($row = $rs->fetch_object())) { |
| 1007: | $charsets[$row->charset] = $row->desc; |
| 1008: | } |
| 1009: | $cache->set($key, $charsets); |
| 1010: | |
| 1011: | return $charsets; |
| 1012: | } |
| 1013: | |
| 1014: | |
| 1015: | |
| 1016: | |
| 1017: | |
| 1018: | |
| 1019: | |
| 1020: | public function collation_valid($collation): bool |
| 1021: | { |
| 1022: | $collations = $this->get_supported_collations(); |
| 1023: | $collation = strtolower($collation); |
| 1024: | foreach ($collations as $c) { |
| 1025: | if ($c['collation'] === $collation) { |
| 1026: | return true; |
| 1027: | } |
| 1028: | } |
| 1029: | |
| 1030: | return false; |
| 1031: | } |
| 1032: | |
| 1033: | public function get_supported_collations() |
| 1034: | { |
| 1035: | $cache = Cache_Global::spawn(); |
| 1036: | $key = 's:mysql.coll'; |
| 1037: | $collations = $cache->get($key); |
| 1038: | if ($collations) { |
| 1039: | return $collations; |
| 1040: | } |
| 1041: | $collations = []; |
| 1042: | $db = MySQL::initialize(); |
| 1043: | $q = 'SELECT collation_name, character_set_name FROM ' . |
| 1044: | "INFORMATION_SCHEMA.collations WHERE is_compiled = 'Yes'"; |
| 1045: | $rs = $db->query($q); |
| 1046: | if (!$rs) { |
| 1047: | return $collations; |
| 1048: | } |
| 1049: | |
| 1050: | while (null !== ($row = $rs->fetch_object())) { |
| 1051: | $collations[] = array( |
| 1052: | 'collation' => $row->collation_name, |
| 1053: | 'charset' => $row->character_set_name |
| 1054: | ); |
| 1055: | } |
| 1056: | $cache->set($key, $collations); |
| 1057: | |
| 1058: | return $collations; |
| 1059: | } |
| 1060: | |
| 1061: | |
| 1062: | |
| 1063: | |
| 1064: | |
| 1065: | |
| 1066: | |
| 1067: | |
| 1068: | |
| 1069: | public function collation_compatible($collation, $charset): bool |
| 1070: | { |
| 1071: | $db = MySQL::initialize(); |
| 1072: | $q = 'SELECT 1 FROM INFORMATION_SCHEMA.COLLATION_CHARACTER_SET_APPLICABILITY ' . |
| 1073: | "WHERE collation_name = '" . $db->escape($collation) . "' AND " . |
| 1074: | "character_set_name = '" . $db->escape($charset) . "'"; |
| 1075: | $rs = $db->query($q); |
| 1076: | if (!$rs) { |
| 1077: | return false; |
| 1078: | } |
| 1079: | |
| 1080: | return $rs->num_rows > 0; |
| 1081: | } |
| 1082: | |
| 1083: | |
| 1084: | |
| 1085: | |
| 1086: | |
| 1087: | |
| 1088: | |
| 1089: | public function database_exists($db): bool |
| 1090: | { |
| 1091: | if (!IS_CLI) { |
| 1092: | return $this->query('mysql_database_exists', $db); |
| 1093: | } |
| 1094: | if (!$db) { |
| 1095: | return false; |
| 1096: | } |
| 1097: | $prefix = ''; |
| 1098: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) { |
| 1099: | $sqlroot = $this->domain_shadow_path() . self::MYSQL_DATADIR; |
| 1100: | $normal = \Opcenter\Database\MySQL::canonicalize($db); |
| 1101: | $prefix = $this->get_prefix(); |
| 1102: | if (!file_exists($sqlroot . '/' . $normal)) { |
| 1103: | |
| 1104: | $db = $prefix . $db; |
| 1105: | } |
| 1106: | } |
| 1107: | if (\Opcenter\Database\MySQL::databaseExists($db)) { |
| 1108: | return true; |
| 1109: | } else if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 1110: | |
| 1111: | |
| 1112: | return false; |
| 1113: | } |
| 1114: | $user = $this->databaseAdmin(); |
| 1115: | |
| 1116: | if (0 === strpos($db, $prefix . $prefix)) { |
| 1117: | $db = (string)substr($db, strlen($prefix)); |
| 1118: | } |
| 1119: | return \Opcenter\Database\MySQL::databaseExists($db, $user); |
| 1120: | } |
| 1121: | |
| 1122: | |
| 1123: | |
| 1124: | |
| 1125: | |
| 1126: | public function create_database_backend($db, $charset, $collation) |
| 1127: | { |
| 1128: | $dboptData = 'default-character-set=' . $charset . "\n" . |
| 1129: | 'default-collation=' . $collation; |
| 1130: | |
| 1131: | |
| 1132: | |
| 1133: | |
| 1134: | |
| 1135: | |
| 1136: | $path = $this->domain_shadow_path(); |
| 1137: | if (!Opcenter\Database\MySQL::prepBackend($path, $db)) { |
| 1138: | return error('Failed to prepare database backend storage'); |
| 1139: | } |
| 1140: | $dbRoot = $path . self::MYSQL_DATADIR . '/' . \Opcenter\Database\MySQL::canonicalize($db); |
| 1141: | $fp = fopen($dbRoot . '/db.opt', 'w'); |
| 1142: | fwrite($fp, $dboptData); |
| 1143: | fclose($fp); |
| 1144: | \Opcenter\Filesystem::chogp($dbRoot . '/db.opt', 'mysql', (int)$this->group_id, 0640); |
| 1145: | |
| 1146: | return file_exists(self::MYSQL_DATADIR . '/' . Opcenter\Database\MySQL::canonicalize($db)) && file_exists($path); |
| 1147: | } |
| 1148: | |
| 1149: | |
| 1150: | |
| 1151: | |
| 1152: | |
| 1153: | |
| 1154: | |
| 1155: | |
| 1156: | |
| 1157: | |
| 1158: | |
| 1159: | |
| 1160: | |
| 1161: | public function add_user_permissions($user, $host, $db, array $opts) |
| 1162: | { |
| 1163: | deprecated_func('use set_mysql_privileges()'); |
| 1164: | |
| 1165: | return $this->set_privileges($user, $host, $db, $opts); |
| 1166: | } |
| 1167: | |
| 1168: | |
| 1169: | |
| 1170: | |
| 1171: | |
| 1172: | |
| 1173: | |
| 1174: | |
| 1175: | |
| 1176: | |
| 1177: | public function set_privileges(string $user, string $host, string $db, array $privileges): bool |
| 1178: | { |
| 1179: | if (!$host) { |
| 1180: | return error("invalid hostname `$host'"); |
| 1181: | } |
| 1182: | |
| 1183: | $privileges = array_change_key_case($privileges); |
| 1184: | $prefix = $this->get_prefix(); |
| 1185: | if ($user != $this->databaseAdmin() && strncmp($user, $prefix, strlen($prefix))) { |
| 1186: | $user = $prefix . $user; |
| 1187: | } |
| 1188: | if ($user != $this->databaseAdmin() && !preg_match('/^' . $prefix . '/', $user)) { |
| 1189: | return error("invalid user `%s'", $user); |
| 1190: | } |
| 1191: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 1192: | $conn->select_db('mysql'); |
| 1193: | |
| 1194: | if ($db === '%') { |
| 1195: | if ($user === $this->databaseAdmin()) { |
| 1196: | return warn('User %s already has permission to all databases - ignoring', $this->databaseAdmin()); |
| 1197: | } |
| 1198: | |
| 1199: | $db = $prefix . '%'; |
| 1200: | } else if (0 === strpos($db, $prefix)) { |
| 1201: | $rs = $conn->query("SELECT 1 FROM db WHERE user = '" . $this->databaseAdmin() . "' AND (db = '" . $db . "' OR db = '" . str_replace('_', '\\_', $db) . "')"); |
| 1202: | if ($rs->num_rows < 1) { |
| 1203: | return error("No grants found in database on `%s' for user `%s'", $db, $this->databaseAdmin()); |
| 1204: | } |
| 1205: | } |
| 1206: | |
| 1207: | $valid_opts = array( |
| 1208: | 'select' => false, |
| 1209: | 'insert' => false, |
| 1210: | 'update' => false, |
| 1211: | 'delete' => false, |
| 1212: | 'create' => false, |
| 1213: | 'drop' => false, |
| 1214: | 'grant' => false, |
| 1215: | 'references' => false, |
| 1216: | 'index' => false, |
| 1217: | 'alter' => false, |
| 1218: | 'create_tmp_table' => false, |
| 1219: | 'show_view' => false, |
| 1220: | 'create_view' => false, |
| 1221: | 'create_routine' => false, |
| 1222: | 'alter_routine' => false, |
| 1223: | 'lock_tables' => false, |
| 1224: | 'execute' => false, |
| 1225: | 'event' => false, |
| 1226: | 'trigger' => false |
| 1227: | ); |
| 1228: | |
| 1229: | if (count($privileges) <= 2 && (isset($privileges['read']) || isset($privileges['write']))) { |
| 1230: | |
| 1231: | $tmp = array(); |
| 1232: | if (!empty($privileges['read'])) { |
| 1233: | $tmp['select'] = $tmp['show_view'] = $tmp['execute'] = true; |
| 1234: | } |
| 1235: | |
| 1236: | if (!empty($privileges['write'])) { |
| 1237: | $write = array_diff(array_keys($valid_opts), array('select', 'show_view', 'execute')); |
| 1238: | $tmp2 = array_fill_keys($write, true); |
| 1239: | $tmp = array_merge($tmp, $tmp2); |
| 1240: | } |
| 1241: | $privileges = $tmp; |
| 1242: | } |
| 1243: | |
| 1244: | $opts_copy = $valid_opts; |
| 1245: | foreach ($valid_opts as $opt => $enabled) { |
| 1246: | if (isset($privileges[($opt)]) && $privileges[$opt]) { |
| 1247: | $valid_opts[$opt] = $opt . '_priv'; |
| 1248: | } else { |
| 1249: | unset($valid_opts[$opt]); |
| 1250: | } |
| 1251: | } |
| 1252: | |
| 1253: | $revoke_opts = array_diff_key($opts_copy, $valid_opts); |
| 1254: | foreach (array_keys($opts_copy) as $name) { |
| 1255: | $opts[] = (isset($valid_opts[$name])) ? 'Y' : 'N'; |
| 1256: | } |
| 1257: | |
| 1258: | array_walk($opts_copy, static function (&$key, $val) { |
| 1259: | $key = $val . '_priv'; |
| 1260: | }); |
| 1261: | if ($db !== $prefix . '%') { |
| 1262: | $rs = $conn->query("SELECT db FROM db WHERE user = '" . $user . "' AND host = '" . $host . "' AND db = '" . str_replace('_', '\\_', $prefix) . '%' . "'"); |
| 1263: | if ($rs->num_rows > 0) { |
| 1264: | return error('User %s@%s has wildcard grants applied. Per-database grants cannot be applied over wildcard grant. User may only be deleted.', $user, $host); |
| 1265: | } |
| 1266: | } |
| 1267: | |
| 1268: | $rs = $conn->query("SELECT 1 FROM db where user = '" . $user . "' AND host = '" . $host . "' AND db = '" . str_replace('_', |
| 1269: | '\\_', $db) . "'"); |
| 1270: | if ($db === $prefix . '%' || $rs->num_rows > 0) { |
| 1271: | $db = str_replace('_', '\\_', $db); |
| 1272: | } |
| 1273: | |
| 1274: | $conn->query('REPLACE INTO db (' . implode(', ', |
| 1275: | $opts_copy) . ", `host`, `db`, `user`) VALUES ('" . implode("', '", $opts) . |
| 1276: | "', '" . $host . "', '" . $db . "', '" . $user . "');"); |
| 1277: | $ar = $conn->affected_rows; |
| 1278: | |
| 1279: | if ($conn->error) { |
| 1280: | return error('Error when applying grants, ' . $conn->error); |
| 1281: | } |
| 1282: | |
| 1283: | $conn->query('FLUSH PRIVILEGES;'); |
| 1284: | |
| 1285: | return $ar > 0; |
| 1286: | } |
| 1287: | |
| 1288: | |
| 1289: | |
| 1290: | |
| 1291: | |
| 1292: | |
| 1293: | public function delete_user_permissions($user, $host, $db) |
| 1294: | { |
| 1295: | deprecated_func('use revoke_from_mysql_db()'); |
| 1296: | |
| 1297: | return $this->revoke_privileges($user, $host, $db); |
| 1298: | } |
| 1299: | |
| 1300: | |
| 1301: | |
| 1302: | |
| 1303: | |
| 1304: | |
| 1305: | |
| 1306: | |
| 1307: | |
| 1308: | public function revoke_privileges($user, $host, $db) |
| 1309: | { |
| 1310: | $prefix = $this->get_prefix(); |
| 1311: | if ($user != $this->databaseAdmin() && strncmp($user, $prefix, strlen($prefix))) { |
| 1312: | $user = $prefix . $user; |
| 1313: | } |
| 1314: | if ($user != $this->databaseAdmin() && !preg_match('/^' . $prefix . '/', $user)) { |
| 1315: | return error("invalid user `$user'"); |
| 1316: | } |
| 1317: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 1318: | $conn->select_db('mysql'); |
| 1319: | $stmt = $conn->prepare('DELETE FROM db WHERE user = ? AND host = ? AND (db = ? OR db = ?)'); |
| 1320: | $canonical = str_replace('_', '\_', $db); |
| 1321: | $stmt->bind_param('ssss', $user, $host, $db, $canonical); |
| 1322: | $res = $stmt->execute(); |
| 1323: | if (!$res) { |
| 1324: | return error('Query error while dropping permissions, ' . $stmt->error); |
| 1325: | } |
| 1326: | |
| 1327: | $conn->query('FLUSH PRIVILEGES;'); |
| 1328: | |
| 1329: | return $stmt->affected_rows > 0; |
| 1330: | } |
| 1331: | |
| 1332: | |
| 1333: | |
| 1334: | public function get_user_permissions($user, $host, $db) |
| 1335: | { |
| 1336: | deprecated_func('use get_privileges()'); |
| 1337: | |
| 1338: | return $this->get_privileges($user, $host, $db); |
| 1339: | } |
| 1340: | |
| 1341: | |
| 1342: | |
| 1343: | |
| 1344: | |
| 1345: | |
| 1346: | |
| 1347: | |
| 1348: | |
| 1349: | |
| 1350: | |
| 1351: | public function get_privileges($user, $host, $db) |
| 1352: | { |
| 1353: | $prefix = $this->get_prefix(); |
| 1354: | if ($user != $this->databaseAdmin() && strncmp($user, $prefix, strlen($prefix))) { |
| 1355: | $user = $prefix . $user; |
| 1356: | } |
| 1357: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 1358: | $conn->select_db('mysql'); |
| 1359: | |
| 1360: | $stmt = $conn->prepare('SELECT Select_priv, Insert_priv, Update_priv, DELETE_PRIV, |
| 1361: | CREATE_PRIV, DROP_PRIV, GRANT_PRIV, REFERENCES_PRIV, |
| 1362: | INDEX_PRIV, ALTER_PRIV, CREATE_TMP_TABLE_PRIV, |
| 1363: | LOCK_TABLES_PRIV, CREATE_VIEW_PRIV, SHOW_VIEW_PRIV, |
| 1364: | CREATE_ROUTINE_PRIV, ALTER_ROUTINE_PRIV, EXECUTE_PRIV, |
| 1365: | EVENT_PRIV, TRIGGER_PRIV FROM db WHERE user = ? AND db IN (?,?,?) AND host = ?'); |
| 1366: | $dbSafe = str_replace('_', '\\_', $db); |
| 1367: | $wcSafe = str_replace('_', '\\_', $prefix . '%'); |
| 1368: | $stmt->bind_param('sssss', $user, $db, $dbSafe, $wcSafe, $host); |
| 1369: | $stmt->execute(); |
| 1370: | $select = $insert = $update = $delete = $create = $drop = $domain = |
| 1371: | $grant = $references = $index = $alter = $create_tmp_table = |
| 1372: | $lock_tables = $create_view = $show_view = $create_routine = |
| 1373: | $alter_routine = $execute = $event = $trigger = null; |
| 1374: | $stmt->bind_result($select, $insert, $update, $delete, $create, $drop, $grant, |
| 1375: | $references, $index, $alter, $create_tmp_table, $lock_tables, |
| 1376: | $create_view, $show_view, $create_routine, $alter_routine, |
| 1377: | $execute, $event, $trigger); |
| 1378: | |
| 1379: | if ($stmt->fetch()) { |
| 1380: | $priv = array( |
| 1381: | 'select' => $select, |
| 1382: | 'insert' => $insert, |
| 1383: | 'update' => $update, |
| 1384: | 'delete' => $delete, |
| 1385: | 'create' => $create, |
| 1386: | 'drop' => $drop, |
| 1387: | 'grant' => $grant, |
| 1388: | 'references' => $references, |
| 1389: | 'index' => $index, |
| 1390: | 'alter' => $alter, |
| 1391: | 'create_tmp_table' => $create_tmp_table, |
| 1392: | 'lock_tables' => $lock_tables, |
| 1393: | 'create_view' => $create_view, |
| 1394: | 'show_view' => $show_view, |
| 1395: | 'create_routine' => $create_routine, |
| 1396: | 'alter_routine' => $alter_routine, |
| 1397: | 'execute' => $execute, |
| 1398: | 'trigger' => $trigger, |
| 1399: | 'event' => $event |
| 1400: | ); |
| 1401: | array_walk($priv, static function (&$key, $val) { |
| 1402: | $key = $key == 'Y'; |
| 1403: | }); |
| 1404: | $stmt->close(); |
| 1405: | } else { |
| 1406: | $priv = array( |
| 1407: | 'select' => false, |
| 1408: | 'insert' => false, |
| 1409: | 'update' => false, |
| 1410: | 'delete' => false, |
| 1411: | 'create' => false, |
| 1412: | 'drop' => false, |
| 1413: | 'grant' => false, |
| 1414: | 'references' => false, |
| 1415: | 'index' => false, |
| 1416: | 'alter' => false, |
| 1417: | 'create_tmp_table' => false, |
| 1418: | 'lock_tables' => false, |
| 1419: | 'create_view' => false, |
| 1420: | 'show_view' => false, |
| 1421: | 'create_routine' => false, |
| 1422: | 'alter_routine' => false, |
| 1423: | 'execute' => false, |
| 1424: | ); |
| 1425: | } |
| 1426: | return $priv; |
| 1427: | } |
| 1428: | |
| 1429: | |
| 1430: | |
| 1431: | |
| 1432: | |
| 1433: | |
| 1434: | |
| 1435: | |
| 1436: | |
| 1437: | |
| 1438: | |
| 1439: | |
| 1440: | public function version($pretty = false) |
| 1441: | { |
| 1442: | $version = \Opcenter\Database\MySQL::version(); |
| 1443: | if (!$pretty) { |
| 1444: | return $version; |
| 1445: | } |
| 1446: | |
| 1447: | $mysqlver = array(); |
| 1448: | foreach (array('patch', 'minor', 'major') as $v) { |
| 1449: | $mysqlver[$v] = (int)$version % 100; |
| 1450: | $version /= 100; |
| 1451: | } |
| 1452: | |
| 1453: | return $mysqlver['major'] . '.' . $mysqlver['minor'] . '.' . |
| 1454: | $mysqlver['patch']; |
| 1455: | |
| 1456: | } |
| 1457: | |
| 1458: | |
| 1459: | |
| 1460: | |
| 1461: | |
| 1462: | |
| 1463: | |
| 1464: | public function delete_database($db) |
| 1465: | { |
| 1466: | $db = str_replace('\\\\', '\\', $db); |
| 1467: | $prefix = $this->get_prefix(); |
| 1468: | if (!$prefix) { |
| 1469: | report('Prefixless site - bug'); |
| 1470: | |
| 1471: | return false; |
| 1472: | } |
| 1473: | $admin = $this->databaseAdmin(); |
| 1474: | $prefixwc = str_replace('_', '\_', $prefix) . '%'; |
| 1475: | $conn = new mysqli('localhost', self::MASTER_USER, $this->_get_elevated_password()); |
| 1476: | $conn->select_db('mysql'); |
| 1477: | $stmt = $conn->prepare('SELECT user FROM db WHERE (user = ? OR db LIKE ?) AND (db = ? OR db = ?)'); |
| 1478: | $canonical = str_replace('_', '\_', $db); |
| 1479: | $stmt->bind_param('ssss', $admin, $prefixwc, $db, $canonical); |
| 1480: | $stmt->execute(); |
| 1481: | $stmt->store_result(); |
| 1482: | if ($stmt->num_rows < 1) { |
| 1483: | $stmt->free_result(); |
| 1484: | |
| 1485: | |
| 1486: | if (strncmp($db, $prefix, strlen($prefix))) { |
| 1487: | $db = $prefix . $db; |
| 1488: | |
| 1489: | return $this->delete_database($db); |
| 1490: | } |
| 1491: | if (!in_array($db, $this->list_databases(), true)) { |
| 1492: | |
| 1493: | return error("Unknown database `%s'", $db); |
| 1494: | } |
| 1495: | } |
| 1496: | $stmt->free_result(); |
| 1497: | $this->query('mysql_assert_permissions'); |
| 1498: | \Opcenter\Database\MySQL::dropDatabaseGrants($db); |
| 1499: | |
| 1500: | |
| 1501: | |
| 1502: | \Opcenter\Database\MySQL::flush(); |
| 1503: | \Opcenter\Database\MySQL::dropDatabase($db); |
| 1504: | |
| 1505: | $this->query('mysql_delete_database_backend', $db); |
| 1506: | |
| 1507: | if (in_array($db, $this->list_backups(), true)) { |
| 1508: | $this->delete_backup($db); |
| 1509: | } |
| 1510: | |
| 1511: | |
| 1512: | if ($conn->error) { |
| 1513: | return error("error while removing database `$db' - " . $conn->error); |
| 1514: | } |
| 1515: | |
| 1516: | return true; |
| 1517: | } |
| 1518: | |
| 1519: | |
| 1520: | |
| 1521: | |
| 1522: | |
| 1523: | |
| 1524: | |
| 1525: | public function delete_backup($db) |
| 1526: | { |
| 1527: | return parent::delete_backup_real('mysql', $db); |
| 1528: | } |
| 1529: | |
| 1530: | |
| 1531: | |
| 1532: | |
| 1533: | public function assert_permissions() |
| 1534: | { |
| 1535: | if (!file_exists($this->domain_fs_path() . self::MYSQL_DATADIR)) { |
| 1536: | return false; |
| 1537: | } |
| 1538: | |
| 1539: | chown($this->domain_fs_path() . self::MYSQL_DATADIR, 'mysql'); |
| 1540: | chgrp($this->domain_fs_path() . self::MYSQL_DATADIR, $this->group_id); |
| 1541: | |
| 1542: | return true; |
| 1543: | } |
| 1544: | |
| 1545: | public function delete_database_backend($db) |
| 1546: | { |
| 1547: | $db = \Opcenter\Database\MySQL::canonicalize($db); |
| 1548: | if (is_link(self::MYSQL_DATADIR . '/' . $db)) { |
| 1549: | @unlink(self::MYSQL_DATADIR . '/' . $db); |
| 1550: | } |
| 1551: | |
| 1552: | return true; |
| 1553: | |
| 1554: | } |
| 1555: | |
| 1556: | |
| 1557: | |
| 1558: | |
| 1559: | |
| 1560: | |
| 1561: | |
| 1562: | |
| 1563: | |
| 1564: | |
| 1565: | |
| 1566: | |
| 1567: | |
| 1568: | |
| 1569: | |
| 1570: | |
| 1571: | |
| 1572: | |
| 1573: | |
| 1574: | |
| 1575: | |
| 1576: | public function edit_user(string $user, string $host, array $opts): bool |
| 1577: | { |
| 1578: | $prefix = $this->get_prefix(); |
| 1579: | if ($user !== $this->databaseAdmin() && 0 !== strpos($user, $prefix)) { |
| 1580: | $user = $prefix . $user; |
| 1581: | } |
| 1582: | if (!is_array($opts)) { |
| 1583: | return error('Options must be an array'); |
| 1584: | } |
| 1585: | if (isset($opts['ssl_type'])) { |
| 1586: | $opts['ssl_type'] = strtoupper($opts['ssl_type']); |
| 1587: | if ($opts['ssl_type'] != '' && $opts['ssl_type'] != 'ANY' && |
| 1588: | $opts['ssl_type'] != 'SPECIFIED' && $opts['ssl_type'] != 'X509' |
| 1589: | ) { |
| 1590: | return error('Invalid cipher type'); |
| 1591: | } |
| 1592: | } |
| 1593: | |
| 1594: | if (isset($opts['host']) && $opts['host'] != 'localhost') { |
| 1595: | |
| 1596: | if (!IpCommon::valid($opts['host']) && !preg_match(Regex::SQL_MYSQL_IP_WILDCARD, $opts['host'])) { |
| 1597: | return error("rejected host `%s': only numeric IP addresses are permitted, not hostnames", |
| 1598: | $opts['host']); |
| 1599: | } |
| 1600: | } |
| 1601: | |
| 1602: | $defaults = array( |
| 1603: | 'host' => $host, |
| 1604: | 'password' => null, |
| 1605: | 'max_user_connections' => self::DEFAULT_CONCURRENCY_LIMIT, |
| 1606: | 'max_updates' => 0, |
| 1607: | 'max_questions' => 0, |
| 1608: | 'use_ssl' => false, |
| 1609: | 'ssl_type' => '', |
| 1610: | 'ssl_cipher' => '', |
| 1611: | 'x509_subject' => '', |
| 1612: | 'x509_issuer' => '' |
| 1613: | ); |
| 1614: | |
| 1615: | |
| 1616: | |
| 1617: | $mergeopts = $opts; |
| 1618: | foreach ($defaults as $def_nam => $def_val) { |
| 1619: | if (!isset($mergeopts[$def_nam])) { |
| 1620: | $mergeopts[$def_nam] = $def_val; |
| 1621: | } |
| 1622: | } |
| 1623: | |
| 1624: | if (!$mergeopts['use_ssl']) { |
| 1625: | $mergeopts['x509_subject'] = $mergeopts['x509_issuer'] = $mergeopts['ssl_cipher'] = $mergeopts['ssl_type'] = ''; |
| 1626: | } else if (!isset($mergeopts['ssl_type'])) { |
| 1627: | $mergeopts['ssl_type'] = 'ANY'; |
| 1628: | } |
| 1629: | |
| 1630: | if ($mergeopts['max_user_connections'] < 1) { |
| 1631: | $mergeopts['max_user_connections'] = self::DEFAULT_CONCURRENCY_LIMIT; |
| 1632: | } |
| 1633: | |
| 1634: | if ($mergeopts['max_questions'] < 0 || $mergeopts['max_updates'] < 0) { |
| 1635: | return error('Max queries and updates must be greater than 0'); |
| 1636: | } |
| 1637: | if (isset($opts['max_user_connections']) && $opts['max_user_connections'] > static::MAX_CONCURRENCY_LIMIT) { |
| 1638: | return error('Max connection limit %d. Must file a ticket justifying need. ' . |
| 1639: | 'Check index placements first.', static::MAX_CONCURRENCY_LIMIT); |
| 1640: | } |
| 1641: | if (!is_null($mergeopts['password']) && strlen($mergeopts['password']) < self::MIN_PASSWORD_LENGTH) { |
| 1642: | return error('password must be at least %d characters long', self::MIN_PASSWORD_LENGTH); |
| 1643: | } |
| 1644: | |
| 1645: | if (!empty($mergeopts['use_ssl']) && $user === $this->databaseAdmin() && $host === 'localhost') { |
| 1646: | warn("Enabling SSL for localhost will break phpMyAdmin access on this account"); |
| 1647: | } |
| 1648: | |
| 1649: | $conn = $this->_connect_root(); |
| 1650: | |
| 1651: | $stmt = $conn->prepare('SELECT user FROM user WHERE user = ? AND host = ?'); |
| 1652: | $stmt->bind_param('ss', $user, $host); |
| 1653: | $stmt->execute(); |
| 1654: | $stmt->store_result(); |
| 1655: | if ($stmt->num_rows < 1) { |
| 1656: | $stmt->free_result(); |
| 1657: | return error('invalid user@host specified: %s@%s', $user, $host); |
| 1658: | } |
| 1659: | $stmt->free_result(); |
| 1660: | |
| 1661: | if ($host === 'localhost' && |
| 1662: | $user === $this->databaseAdmin() && ($mergeopts['host'] ?: $host) !== $host) |
| 1663: | { |
| 1664: | return error("Cannot modify the hostname attribute for %(user)s on %(host)s", ['user' => $user, 'host' => $host]); |
| 1665: | } |
| 1666: | |
| 1667: | if (Opcenter\Database\MySQL::version() >= self::NEW_API_VERSION) { |
| 1668: | $params = [ |
| 1669: | $user, |
| 1670: | $host, |
| 1671: | [ |
| 1672: | 'ssl' => $mergeopts['use_ssl'] ?: false, |
| 1673: | 'subject' => $mergeopts['x509_subject'] ?: null, |
| 1674: | 'issuer' => $mergeopts['x509_issuer'] ?: null, |
| 1675: | 'ssl_type' => $mergeopts['ssl_type'] ?: null, |
| 1676: | 'password' => $mergeopts['password'] ?: null, |
| 1677: | 'connections' => (int)($mergeopts['max_user_connections'] ?: 0), |
| 1678: | 'updates' => (int)($mergeopts['max_updates'] ?: 0), |
| 1679: | 'query' => (int)($mergeopts['max_questions'] ?: 0), |
| 1680: | 'host' => $mergeopts['host'] ?: null |
| 1681: | ] |
| 1682: | ]; |
| 1683: | if (!Opcenter\Database\MySQL::alterUser(...$params)) { |
| 1684: | return false; |
| 1685: | } |
| 1686: | } else { |
| 1687: | $stmt = $conn->prepare('UPDATE user |
| 1688: | SET |
| 1689: | host = ?, |
| 1690: | ssl_type = ?, |
| 1691: | ssl_cipher = ?, |
| 1692: | x509_issuer = ? , |
| 1693: | x509_subject = ?, |
| 1694: | max_questions = ?, |
| 1695: | max_updates = ?, |
| 1696: | max_user_connections = ? |
| 1697: | WHERE |
| 1698: | user = ? |
| 1699: | AND |
| 1700: | host = ?'); |
| 1701: | |
| 1702: | $stmt->bind_param('sssssiiiss', |
| 1703: | $mergeopts['host'], |
| 1704: | $mergeopts['ssl_type'], |
| 1705: | $mergeopts['ssl_cipher'], |
| 1706: | $mergeopts['x509_issuer'], |
| 1707: | $mergeopts['x509_subject'], |
| 1708: | $mergeopts['max_questions'], |
| 1709: | $mergeopts['max_updates'], |
| 1710: | $mergeopts['max_user_connections'], |
| 1711: | $user, |
| 1712: | $host |
| 1713: | ); |
| 1714: | $stmt->execute(); |
| 1715: | if ($stmt->error) { |
| 1716: | return new MySQLError('Invalid query, ' . $stmt->error); |
| 1717: | } |
| 1718: | |
| 1719: | if ($mergeopts['host'] !== $defaults['host']) { |
| 1720: | $stmt = $conn->prepare('UPDATE db SET host = ? WHERE user = ? AND host = ?'); |
| 1721: | $stmt->bind_param('sss', $mergeopts['host'], $user, $defaults['host']); |
| 1722: | $stmt->execute(); |
| 1723: | if ($stmt->error) { |
| 1724: | return error('error while updating DB grants, %s', $stmt->error); |
| 1725: | } |
| 1726: | } |
| 1727: | |
| 1728: | if ($mergeopts['password']) { |
| 1729: | $pwclause = 'password(?)'; |
| 1730: | $password = $mergeopts['password']; |
| 1731: | |
| 1732: | if ($password[0] == '*' && strlen($password) == 41 |
| 1733: | && ctype_xdigit(substr($password, 1)) || |
| 1734: | |
| 1735: | strlen($password) == 16 && ctype_xdigit($password) && version_compare(platform_version(), '6', |
| 1736: | '<') |
| 1737: | ) { |
| 1738: | $pwclause = '?'; |
| 1739: | } |
| 1740: | $stmt2 = $conn->prepare('UPDATE user SET password = ' . $pwclause . ' WHERE user = ? AND host = ?;'); |
| 1741: | |
| 1742: | $stmt2->bind_param('sss', $password, $user, $mergeopts['host']); |
| 1743: | $stmt2->execute(); |
| 1744: | if ($stmt2->error) { |
| 1745: | return new MySQLError('Query error while updating password, ' . $stmt2->error); |
| 1746: | } |
| 1747: | } |
| 1748: | } |
| 1749: | |
| 1750: | $conn->query('FLUSH PRIVILEGES'); |
| 1751: | |
| 1752: | if ($user === $this->databaseAdmin() && $mergeopts['password']) { |
| 1753: | $this->set_option('user', $this->databaseAdmin(), 'client'); |
| 1754: | $this->set_option('password', |
| 1755: | str_replace(array('"'), array('\"'), $mergeopts['password']), |
| 1756: | 'client' |
| 1757: | ); |
| 1758: | } |
| 1759: | |
| 1760: | return true; |
| 1761: | } |
| 1762: | |
| 1763: | |
| 1764: | |
| 1765: | |
| 1766: | |
| 1767: | |
| 1768: | |
| 1769: | |
| 1770: | |
| 1771: | |
| 1772: | public function service_enabled() |
| 1773: | { |
| 1774: | deprecated('use enabled()'); |
| 1775: | |
| 1776: | return $this->enabled(); |
| 1777: | } |
| 1778: | |
| 1779: | |
| 1780: | |
| 1781: | |
| 1782: | |
| 1783: | |
| 1784: | |
| 1785: | |
| 1786: | public function enabled() |
| 1787: | { |
| 1788: | return parent::svc_enabled('mysql'); |
| 1789: | } |
| 1790: | |
| 1791: | public function truncate_database($db) |
| 1792: | { |
| 1793: | |
| 1794: | return $this->_mysql_empty_truncate_wrapper($db, 'truncate'); |
| 1795: | } |
| 1796: | |
| 1797: | private function _mysql_empty_truncate_wrapper($db, $mode) |
| 1798: | { |
| 1799: | if ($mode != 'truncate' && $mode != 'empty') { |
| 1800: | return error("unknown mode `%s'", $mode); |
| 1801: | } |
| 1802: | if ($mode == 'empty') { |
| 1803: | |
| 1804: | $mode = 'drop'; |
| 1805: | } |
| 1806: | |
| 1807: | $prefix = $this->getServiceValue('mysql', 'dbaseprefix'); |
| 1808: | if (0 !== strpos($db, $prefix)) { |
| 1809: | $db = $prefix . $db; |
| 1810: | } |
| 1811: | |
| 1812: | if (!$this->database_exists($db)) { |
| 1813: | return error("unknown database, `%s'", $db); |
| 1814: | } |
| 1815: | |
| 1816: | $user = $this->_create_temp_user($db); |
| 1817: | if (!$user) { |
| 1818: | return error("failed to %s db `%s'", $mode, $db); |
| 1819: | } |
| 1820: | $conn = new mysqli('localhost', $user); |
| 1821: | $conn->set_charset('utf8mb4'); |
| 1822: | if (!$conn->select_db($db)) { |
| 1823: | return error("unable to establish db connection for user `%s' on db `%s'", $user, $db); |
| 1824: | } |
| 1825: | |
| 1826: | $conn->query('SET FOREIGN_KEY_CHECKS=0'); |
| 1827: | |
| 1828: | $q = "SELECT CONCAT('" . strtoupper($mode) . " TABLE ','`', table_schema,'`','.','`',TABLE_NAME,'`', ';') |
| 1829: | FROM INFORMATION_SCHEMA.TABLES where table_schema in ('" . $conn->escape_string($db) . "');"; |
| 1830: | $res = $conn->query($q); |
| 1831: | while (null !== ($rs = $res->fetch_row())) { |
| 1832: | if (!$conn->query($rs[0])) { |
| 1833: | warn("failed to %s table `%s'", $mode, $rs[0]); |
| 1834: | } |
| 1835: | } |
| 1836: | |
| 1837: | $conn->query('SET @@FOREIGN_KEY_CHECKS=1;'); |
| 1838: | if (!$res) { |
| 1839: | return error("%s failed on database `%s': `%s'", $mode, $db, $conn->error); |
| 1840: | } |
| 1841: | $this->_delete_temp_user($user); |
| 1842: | |
| 1843: | return true; |
| 1844: | } |
| 1845: | |
| 1846: | |
| 1847: | |
| 1848: | |
| 1849: | |
| 1850: | |
| 1851: | |
| 1852: | public function empty($db) { |
| 1853: | if (!$this->database_exists($db)) { |
| 1854: | return true; |
| 1855: | } |
| 1856: | |
| 1857: | return \count(\Opcenter\Database\MySQL::getTablesFromDatabase($db)) === 0; |
| 1858: | } |
| 1859: | |
| 1860: | public function empty_database($db) |
| 1861: | { |
| 1862: | return $this->_mysql_empty_truncate_wrapper($db, 'empty'); |
| 1863: | } |
| 1864: | |
| 1865: | |
| 1866: | |
| 1867: | |
| 1868: | |
| 1869: | |
| 1870: | |
| 1871: | |
| 1872: | public function export($db, $file = null) |
| 1873: | { |
| 1874: | if (!IS_CLI) { |
| 1875: | return $this->query('mysql_export', $db, $file); |
| 1876: | } |
| 1877: | |
| 1878: | if (is_null($file)) { |
| 1879: | $file = $db . '.sql'; |
| 1880: | } |
| 1881: | if (!in_array($db, $this->list_databases(), true)) { |
| 1882: | return error('Invalid database ' . $db); |
| 1883: | } |
| 1884: | if ($file[0] !== '/' && $file[0] !== '.' && $file[0] !== '~') { |
| 1885: | $file = '/tmp/' . $file; |
| 1886: | } |
| 1887: | $pdir = dirname($file); |
| 1888: | if (!$this->file_exists($pdir) && !$this->file_create_directory($pdir, 0755, true)) { |
| 1889: | return error("failed to create parent directory, `%s'", $pdir); |
| 1890: | } |
| 1891: | $path = $this->file_make_path($file); |
| 1892: | if (!$path) { |
| 1893: | return error("invalid file `%s'", $file); |
| 1894: | } |
| 1895: | |
| 1896: | if (file_exists($path) && |
| 1897: | (filesize($path) > 0 || realpath($path) !== $path || fileowner($path) < USER_MIN_UID)) |
| 1898: | { |
| 1899: | return error('%s: file exists, cannot overwrite', $file); |
| 1900: | } |
| 1901: | $user = $this->_create_temp_user($db); |
| 1902: | $cmd = 'umask 077 && ' . self::EXPORT_CMD . ' -u %s %s > %s'; |
| 1903: | if (!$user) { |
| 1904: | $user = self::MASTER_USER; |
| 1905: | $rootpw = escapeshellarg($this->_get_elevated_password()); |
| 1906: | $cmd = str_replace('-u %s', '-u %s -p' . $rootpw, $cmd); |
| 1907: | } |
| 1908: | |
| 1909: | $fsizelimit = Util_Ulimit::get('fsize'); |
| 1910: | if ($this->get_database_size($db) > $fsizelimit / self::DB_BIN2TXT_MULT) { |
| 1911: | |
| 1912: | Util_Ulimit::set('fsize', 'unlimited'); |
| 1913: | } else { |
| 1914: | |
| 1915: | $fsizelimit = null; |
| 1916: | } |
| 1917: | $proc = new Util_Process_Safe(); |
| 1918: | |
| 1919: | $proc->setSgid($this->group_id); |
| 1920: | $proc->setSuid($this->user_id); |
| 1921: | $proc->setPriority(19); |
| 1922: | |
| 1923: | $status = $proc->run($cmd, |
| 1924: | $user, |
| 1925: | $db, |
| 1926: | $path |
| 1927: | ); |
| 1928: | |
| 1929: | if ($user != self::MASTER_USER) { |
| 1930: | $this->_delete_temp_user($user); |
| 1931: | } |
| 1932: | |
| 1933: | if (!is_null($fsizelimit)) { |
| 1934: | Util_Ulimit::set('fsize', $fsizelimit); |
| 1935: | } |
| 1936: | |
| 1937: | if (!$status['success'] || !file_exists($path)) { |
| 1938: | return error('export failed: %s', $status['stderr']); |
| 1939: | } |
| 1940: | |
| 1941: | if (!$status['success']) { |
| 1942: | return error('export failed: %s', $status['stderr']); |
| 1943: | } |
| 1944: | |
| 1945: | return $this->file_unmake_path($path); |
| 1946: | } |
| 1947: | |
| 1948: | |
| 1949: | |
| 1950: | |
| 1951: | |
| 1952: | |
| 1953: | |
| 1954: | |
| 1955: | public function clone(string $from, string $to): bool |
| 1956: | { |
| 1957: | if ($this->database_exists($to) && !$this->empty($to)) { |
| 1958: | return error("Database `%s' already exists", $to); |
| 1959: | } |
| 1960: | if (!$this->database_exists($from)) { |
| 1961: | return error("Database `%s' does not exist", $from); |
| 1962: | } |
| 1963: | if (!$this->database_exists($to) && !$this->create_database($to)) { |
| 1964: | return false; |
| 1965: | } |
| 1966: | $pipe = $this->export_pipe($from); |
| 1967: | defer($_, static function () use ($pipe) { |
| 1968: | if (file_exists($pipe)) { |
| 1969: | unlink($pipe); |
| 1970: | } |
| 1971: | }); |
| 1972: | return $this->import($to, $this->file_unmake_path($pipe)); |
| 1973: | } |
| 1974: | |
| 1975: | |
| 1976: | |
| 1977: | |
| 1978: | |
| 1979: | |
| 1980: | |
| 1981: | |
| 1982: | |
| 1983: | public function get_database_size($db) |
| 1984: | { |
| 1985: | if (!IS_CLI) { |
| 1986: | $resp = $this->query('mysql_get_database_size', $db); |
| 1987: | |
| 1988: | return (int)$resp; |
| 1989: | } |
| 1990: | |
| 1991: | |
| 1992: | $dir = self::MYSQL_DATADIR . '/' . \Opcenter\Database\MySQL::canonicalize($db); |
| 1993: | |
| 1994: | |
| 1995: | if (($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) && !is_link($dir)) { |
| 1996: | $this->_move_db($db); |
| 1997: | } |
| 1998: | if (!file_exists($dir) || (is_link($dir) && !readlink($dir))) { |
| 1999: | warn($db . ': database does not exist'); |
| 2000: | |
| 2001: | return 0; |
| 2002: | } |
| 2003: | |
| 2004: | $space = 0; |
| 2005: | $dh = opendir($dir); |
| 2006: | if (!$dh) { |
| 2007: | error("failed to open database directory `%s'", $dir); |
| 2008: | |
| 2009: | return 0; |
| 2010: | } |
| 2011: | while (($file = readdir($dh)) !== false) { |
| 2012: | if ($file == '.' || $file == '..') { |
| 2013: | continue; |
| 2014: | } |
| 2015: | $space += filesize($dir . '/' . $file); |
| 2016: | } |
| 2017: | closedir($dh); |
| 2018: | |
| 2019: | return $space; |
| 2020: | } |
| 2021: | |
| 2022: | |
| 2023: | |
| 2024: | private function _move_db($db) |
| 2025: | { |
| 2026: | |
| 2027: | |
| 2028: | |
| 2029: | |
| 2030: | |
| 2031: | $prefix = $this->databaseAdmin(); |
| 2032: | if (strncmp($db, $prefix, strlen($prefix))) { |
| 2033: | return true; |
| 2034: | } |
| 2035: | $dbfs = \Opcenter\Database\MySQL::canonicalize($db); |
| 2036: | $src = self::MYSQL_DATADIR . '/' . $dbfs; |
| 2037: | $dest = $this->domain_fs_path() . self::MYSQL_DATADIR . '/' . $dbfs; |
| 2038: | if (is_dir($dest)) { |
| 2039: | report('dest db exists - %s', $dest); |
| 2040: | |
| 2041: | return false; |
| 2042: | } |
| 2043: | \Opcenter\Filesystem::mkdir($dest, 'mysql', $this->group_id, 02750); |
| 2044: | |
| 2045: | |
| 2046: | |
| 2047: | |
| 2048: | |
| 2049: | |
| 2050: | |
| 2051: | |
| 2052: | |
| 2053: | $dh = opendir($src); |
| 2054: | if (!$dh) { |
| 2055: | return error("could not relocate database `%s'", $db); |
| 2056: | } |
| 2057: | |
| 2058: | while (false !== ($file = readdir($dh))) { |
| 2059: | if ($file == '.' || $file == '..') { |
| 2060: | continue; |
| 2061: | } |
| 2062: | rename($src . '/' . $file, $dest . '/' . $file); |
| 2063: | chown($dest . '/' . $file, 'mysql'); |
| 2064: | chgrp($dest . '/' . $file, $this->group_id); |
| 2065: | } |
| 2066: | |
| 2067: | rmdir($src); |
| 2068: | symlink($dest, $src); |
| 2069: | warn("database `%s' relocated under account filesystem root", $db); |
| 2070: | $db = $this->_connect_root(); |
| 2071: | $db->query('FLUSH TABLES'); |
| 2072: | |
| 2073: | return true; |
| 2074: | } |
| 2075: | |
| 2076: | |
| 2077: | |
| 2078: | |
| 2079: | |
| 2080: | |
| 2081: | |
| 2082: | |
| 2083: | |
| 2084: | |
| 2085: | public function export_pipe($db) |
| 2086: | { |
| 2087: | if (version_compare(platform_version(), '4.5', '<=')) { |
| 2088: | return error('platform version too old to support download feature'); |
| 2089: | } |
| 2090: | |
| 2091: | $dbs = $this->list_databases(); |
| 2092: | if (!\in_array($db, $dbs, true)) { |
| 2093: | $originalDb = $db; |
| 2094: | $db = $this->get_prefix() . $db; |
| 2095: | if (!\in_array($db, $dbs, true)) { |
| 2096: | return error('Invalid database %s', $originalDb); |
| 2097: | } |
| 2098: | } |
| 2099: | |
| 2100: | $user = $this->_create_temp_user($db); |
| 2101: | |
| 2102: | return $this->query('mysql_export_pipe_real', $db, $user); |
| 2103: | } |
| 2104: | |
| 2105: | |
| 2106: | |
| 2107: | |
| 2108: | |
| 2109: | |
| 2110: | |
| 2111: | |
| 2112: | |
| 2113: | |
| 2114: | |
| 2115: | |
| 2116: | |
| 2117: | |
| 2118: | |
| 2119: | public function export_pipe_real($db, $user) |
| 2120: | { |
| 2121: | if (!IS_CLI) { |
| 2122: | return $this->query('mysql_export_pipe_real', $db, $user); |
| 2123: | } |
| 2124: | |
| 2125: | |
| 2126: | |
| 2127: | |
| 2128: | $cmd = self::EXPORT_CMD . ' -u %s %s -r%s'; |
| 2129: | if (!$user) { |
| 2130: | $user = self::MASTER_USER; |
| 2131: | $rootpw = escapeshellarg($this->_get_elevated_password()); |
| 2132: | $cmd = str_replace('-u %s', '-u %s --password=' . $rootpw, $cmd); |
| 2133: | } |
| 2134: | |
| 2135: | |
| 2136: | $fifo = tempnam($this->domain_fs_path('/tmp'), 'id-' . $this->site); |
| 2137: | unlink($fifo); |
| 2138: | if (!posix_mkfifo($fifo, 0600)) { |
| 2139: | return error('failed to ready pipe for export'); |
| 2140: | } |
| 2141: | chown($fifo, File_Module::UPLOAD_UID); |
| 2142: | $proc = new Util_Process_Fork(); |
| 2143: | |
| 2144: | |
| 2145: | $proc->setPriority(19); |
| 2146: | $proc->setUser(APNSCP_SYSTEM_USER); |
| 2147: | |
| 2148: | $status = $proc->run($cmd, |
| 2149: | $user, |
| 2150: | $db, |
| 2151: | $fifo |
| 2152: | ); |
| 2153: | |
| 2154: | if (!$status['success'] || !file_exists($fifo)) { |
| 2155: | return error('export failed: %s', $status['stderr']); |
| 2156: | } |
| 2157: | register_shutdown_function(static function () use ($fifo) { |
| 2158: | if (file_exists($fifo)) { |
| 2159: | unlink($fifo); |
| 2160: | } |
| 2161: | |
| 2162: | }); |
| 2163: | |
| 2164: | return $fifo; |
| 2165: | } |
| 2166: | |
| 2167: | |
| 2168: | |
| 2169: | |
| 2170: | |
| 2171: | |
| 2172: | |
| 2173: | |
| 2174: | public function get_uptime(): int |
| 2175: | { |
| 2176: | $db = MySQL::initialize(); |
| 2177: | |
| 2178: | return $db->query("SHOW status LIKE 'uptime'")->fetch_object()->value; |
| 2179: | |
| 2180: | } |
| 2181: | |
| 2182: | |
| 2183: | |
| 2184: | public function add_backup( |
| 2185: | string $db, |
| 2186: | string $extension = DATABASE_BACKUP_EXTENSION, |
| 2187: | int $span = DATABASE_BACKUP_SPAN, |
| 2188: | int $preserve = DATABASE_BACKUP_PRESERVE, |
| 2189: | $email = '' |
| 2190: | ) |
| 2191: | { |
| 2192: | return parent::add_backup_real('mysql', $db, $extension, $span, $preserve, $email); |
| 2193: | } |
| 2194: | |
| 2195: | public function edit_backup( |
| 2196: | string $db, |
| 2197: | string $extension = DATABASE_BACKUP_EXTENSION, |
| 2198: | int $span = DATABASE_BACKUP_SPAN, |
| 2199: | int $preserve = DATABASE_BACKUP_PRESERVE, |
| 2200: | $email = '' |
| 2201: | ) |
| 2202: | { |
| 2203: | return $this->edit_backup_real('mysql', $db, $extension, $span, $preserve, $email); |
| 2204: | } |
| 2205: | |
| 2206: | public function list_backups() |
| 2207: | { |
| 2208: | return parent::list_backups_real('mysql'); |
| 2209: | } |
| 2210: | |
| 2211: | |
| 2212: | |
| 2213: | |
| 2214: | |
| 2215: | |
| 2216: | |
| 2217: | |
| 2218: | |
| 2219: | |
| 2220: | |
| 2221: | |
| 2222: | |
| 2223: | public function get_backup_config($db) |
| 2224: | { |
| 2225: | return parent::get_backup_config_real('mysql', $db); |
| 2226: | } |
| 2227: | |
| 2228: | public function repair_database($db) |
| 2229: | { |
| 2230: | if (!IS_CLI) { |
| 2231: | return $this->query('mysql_repair_database', $db); |
| 2232: | } |
| 2233: | |
| 2234: | if (!$this->database_exists($db)) { |
| 2235: | return error("unknown database `%s'", $db); |
| 2236: | } |
| 2237: | |
| 2238: | $sqlroot = $this->domain_fs_path() . self::MYSQL_DATADIR; |
| 2239: | if (!file_exists($sqlroot . '/' . $db)) { |
| 2240: | |
| 2241: | $prefix = $this->get_prefix(); |
| 2242: | $db = $prefix . $db; |
| 2243: | } |
| 2244: | |
| 2245: | $files = glob($sqlroot . '/' . $db . '/*'); |
| 2246: | if (count($files) < 2) { |
| 2247: | return true; |
| 2248: | } |
| 2249: | |
| 2250: | |
| 2251: | $quota = $this->site_get_account_quota(); |
| 2252: | $conn = $this->_connect_root(); |
| 2253: | $q = 'SELECT MAX(Data_length) AS max FROM ' . |
| 2254: | "information_schema.tables WHERE table_schema = '" . |
| 2255: | $conn->real_escape_string($db) . "'"; |
| 2256: | $rs = $conn->query($q); |
| 2257: | $row = $rs->fetch_object(); |
| 2258: | $tblsz = $row->max / 1024 * 1.25; |
| 2259: | |
| 2260: | $qfree = $quota['qhard'] - $quota['qused']; |
| 2261: | $cmd = 'env HOME=/root mysqlcheck --auto-repair %s'; |
| 2262: | if ($tblsz > $qfree) { |
| 2263: | warn('not enough storage to safely use mysqlcheck (need %d KB have %d KB free): reverting to direct IO', |
| 2264: | $tblsz, $qfree |
| 2265: | ); |
| 2266: | $cmd = 'myisamchk -r -c ' . $sqlroot . '/%s/*.MYI'; |
| 2267: | } |
| 2268: | $fsizelimit = Util_Ulimit::get('fsize'); |
| 2269: | Util_Ulimit::set('fsize', 'unlimited'); |
| 2270: | $ret = Util_Process_Safe::exec($cmd, array($db), ['mute_stderr' => true]); |
| 2271: | Util_Ulimit::set('fsize', $fsizelimit); |
| 2272: | if (!$ret['success'] && false === strpos($ret['stderr'], "doesn't exist")) { |
| 2273: | return error("`%s' repair failed:\n%s", $db, $ret['stderr']); |
| 2274: | } |
| 2275: | |
| 2276: | return info("`%s' repair succeeded:\n%s", $db, $ret['output']); |
| 2277: | } |
| 2278: | |
| 2279: | |
| 2280: | |
| 2281: | |
| 2282: | |
| 2283: | |
| 2284: | |
| 2285: | |
| 2286: | public function kill($id) |
| 2287: | { |
| 2288: | $db = $this->_connect_root(); |
| 2289: | $id = intval($id); |
| 2290: | $procs = $this->get_processlist(); |
| 2291: | $found = 0; |
| 2292: | foreach ($procs as $p) { |
| 2293: | if ($p['id'] == $id) { |
| 2294: | $found = 1; |
| 2295: | break; |
| 2296: | } |
| 2297: | } |
| 2298: | if (!$found) { |
| 2299: | return error("`%d': invalid query id specified", $id); |
| 2300: | } |
| 2301: | $q = "KILL $id"; |
| 2302: | $rs = $db->query($q); |
| 2303: | |
| 2304: | return (bool)$rs; |
| 2305: | } |
| 2306: | |
| 2307: | |
| 2308: | |
| 2309: | |
| 2310: | |
| 2311: | |
| 2312: | |
| 2313: | |
| 2314: | |
| 2315: | |
| 2316: | |
| 2317: | |
| 2318: | |
| 2319: | |
| 2320: | |
| 2321: | |
| 2322: | |
| 2323: | |
| 2324: | |
| 2325: | |
| 2326: | public function get_processlist() |
| 2327: | { |
| 2328: | $conns = array(); |
| 2329: | $db = $this->_connect_root(); |
| 2330: | $user = $this->databaseAdmin(); |
| 2331: | $prefix = $this->get_prefix(); |
| 2332: | $q = 'SELECT id, user, host, db, command, time, state, info FROM ' . |
| 2333: | "information_schema.processlist WHERE user = '" . |
| 2334: | $db->real_escape_string($user) . "' OR user LIKE '" . $db->real_escape_string($prefix) . "%'"; |
| 2335: | $rs = $db->query($q); |
| 2336: | while (null != ($row = $rs->fetch_object())) { |
| 2337: | $conns[] = array( |
| 2338: | 'id' => $row->id, |
| 2339: | 'user' => $row->user, |
| 2340: | 'host' => $row->host, |
| 2341: | 'db' => $row->db, |
| 2342: | 'command' => $row->command, |
| 2343: | 'state' => $row->state, |
| 2344: | 'info' => $row->info |
| 2345: | ); |
| 2346: | } |
| 2347: | |
| 2348: | return $conns; |
| 2349: | } |
| 2350: | |
| 2351: | |
| 2352: | |
| 2353: | |
| 2354: | |
| 2355: | |
| 2356: | public function stats(): array { |
| 2357: | $conn = \MySQL::initialize(); |
| 2358: | $vars = [ |
| 2359: | 'Queries', |
| 2360: | 'Uptime', |
| 2361: | 'Connections', |
| 2362: | 'Questions', |
| 2363: | 'TABLE_OPEN_CACHE_HITS', |
| 2364: | 'MAX_USED_CONNECTIONS', |
| 2365: | 'TABLE_OPEN_CACHE_MISSES', |
| 2366: | 'OPENED_FILES', |
| 2367: | 'OPENED_TABLES', |
| 2368: | 'THREADS_RUNNING', |
| 2369: | 'THREADS_CREATED' |
| 2370: | ]; |
| 2371: | |
| 2372: | $fields = implode(',', array_map(static function ($var) use ($conn) { |
| 2373: | return "'$var'"; |
| 2374: | }, $vars)); |
| 2375: | $rs = $conn->query("SELECT LOWER(variable_name) AS name, variable_value AS val FROM information_schema.global_status WHERE variable_name IN($fields);"); |
| 2376: | |
| 2377: | $items = array_build($rs->fetch_all(\MYSQLI_ASSOC), static function ($k, $v) { |
| 2378: | return [$v['name'], (int)$v['val']]; |
| 2379: | }); |
| 2380: | |
| 2381: | $items['observed_queries_per_second'] = null; |
| 2382: | $items['queries_per_second'] = round($items['queries']/$items['uptime'], 4); |
| 2383: | |
| 2384: | $key = 'mysql.qpslocf'; |
| 2385: | $cache = \Cache_Global::spawn(); |
| 2386: | if (false !== ($locf = $cache->get($key))) { |
| 2387: | if ($items['uptime'] > $locf[0]) { |
| 2388: | |
| 2389: | $items['observed_queries_per_second'] = round(($items['queries'] - $locf[1]) / ($items['uptime'] - $locf[0]), 4); |
| 2390: | $cache->del($key); |
| 2391: | } |
| 2392: | return $items; |
| 2393: | } else { |
| 2394: | $cache->set($key, [$items['uptime'], $items['queries']]); |
| 2395: | } |
| 2396: | |
| 2397: | |
| 2398: | return $items; |
| 2399: | } |
| 2400: | |
| 2401: | public function _cron(Cronus $c) |
| 2402: | { |
| 2403: | if (!TELEMETRY_ENABLED) { |
| 2404: | return; |
| 2405: | } |
| 2406: | |
| 2407: | $collector = new Collector(PostgreSQL::pdo()); |
| 2408: | $status = $this->stats(); |
| 2409: | foreach (MysqlMetrics::getAttributeMap() as $attr => $metric) { |
| 2410: | if (!isset($status[$metric])) { |
| 2411: | |
| 2412: | continue; |
| 2413: | } |
| 2414: | $val = $status[$metric]; |
| 2415: | |
| 2416: | if ($val instanceof Closure) { |
| 2417: | $val = $val($status); |
| 2418: | } |
| 2419: | $collector->add("mysql-{$attr}", null, $val); |
| 2420: | } |
| 2421: | |
| 2422: | } |
| 2423: | |
| 2424: | |
| 2425: | |
| 2426: | |
| 2427: | |
| 2428: | |
| 2429: | |
| 2430: | public function schema_column_maxlen($field): ?int |
| 2431: | { |
| 2432: | if (!IS_CLI) { |
| 2433: | return $this->query('mysql_schema_column_maxlen', $field); |
| 2434: | } |
| 2435: | if ($field !== 'user' && $field !== 'db') { |
| 2436: | error("unsupported field `%s' requested", $field); |
| 2437: | return null; |
| 2438: | } |
| 2439: | return \Opcenter\Database\MySQL::fieldLength($field); |
| 2440: | } |
| 2441: | } |