| 1: | <?php |
| 2: | declare(strict_types=1); |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | use Module\Support\Sql; |
| 16: | |
| 17: | |
| 18: | |
| 19: | |
| 20: | |
| 21: | |
| 22: | class Pgsql_Module extends Sql |
| 23: | { |
| 24: | const DEPENDENCY_MAP = [ |
| 25: | 'siteinfo', |
| 26: | 'users', |
| 27: | 'diskquota' |
| 28: | ]; |
| 29: | |
| 30: | const IDENTIFIER_MAXLEN = 63; |
| 31: | |
| 32: | const PG_TEMP_PASSWORD = '23f!eoj3'; |
| 33: | const PGSQL_DATADIR = '/var/lib/pgsql'; |
| 34: | |
| 35: | |
| 36: | |
| 37: | |
| 38: | const PER_DATABASE_CONNECTION_LIMIT = PGSQL_DATABASE_CONCURRENCY_LIMIT ?: DATABASE_CONCURRENCY_LIMIT; |
| 39: | |
| 40: | |
| 41: | const MASTER_USER = 'root'; |
| 42: | |
| 43: | protected const PGSQL_PERMITTED_EXTENSIONS = PGSQL_USER_EXTENSIONS; |
| 44: | |
| 45: | |
| 46: | |
| 47: | |
| 48: | |
| 49: | |
| 50: | public function __construct() |
| 51: | { |
| 52: | parent::__construct(); |
| 53: | $this->exportedFunctions = array( |
| 54: | '*' => PRIVILEGE_SITE, |
| 55: | 'version' => PRIVILEGE_ALL, |
| 56: | 'get_elevated_password_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
| 57: | 'prep_tablespace_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 58: | 'vacuum_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 59: | 'get_uptime' => PRIVILEGE_ALL, |
| 60: | 'get_username' => PRIVILEGE_ALL, |
| 61: | 'get_password' => PRIVILEGE_ALL, |
| 62: | 'set_password' => PRIVILEGE_ALL, |
| 63: | 'enabled' => PRIVILEGE_SITE | PRIVILEGE_USER, |
| 64: | 'get_prefix' => PRIVILEGE_SITE | PRIVILEGE_USER, |
| 65: | |
| 66: | 'export_pipe_real' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
| 67: | 'resolve_site_from_database' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 68: | |
| 69: | |
| 70: | 'get_database_size' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 71: | 'get_table_sizes' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 72: | 'database_exists' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 73: | 'site_from_tablespace' => PRIVILEGE_ADMIN |
| 74: | ); |
| 75: | } |
| 76: | |
| 77: | public function __destruct() |
| 78: | { |
| 79: | foreach ($this->_tempUsers as $user) { |
| 80: | if (!$this->user_exists($user)) { |
| 81: | continue; |
| 82: | } |
| 83: | $this->_delete_temp_user($user); |
| 84: | } |
| 85: | } |
| 86: | |
| 87: | |
| 88: | |
| 89: | |
| 90: | |
| 91: | |
| 92: | |
| 93: | |
| 94: | |
| 95: | public function user_exists($user, $host = 'localhost') |
| 96: | { |
| 97: | $db = \PostgreSQL::initialize(); |
| 98: | $prefix = $this->get_prefix(); |
| 99: | if (!($this->permission_level & PRIVILEGE_ADMIN) && $user != $this->databaseAdmin() && |
| 100: | 0 !== strpos($user, $prefix) |
| 101: | ) { |
| 102: | $user = $prefix . $user; |
| 103: | } |
| 104: | $q = $db->query_params('SELECT 1 FROM pg_authid WHERE rolname = $1', array($db->escape_string($user))); |
| 105: | |
| 106: | return !$q || $db->num_rows() > 0; |
| 107: | } |
| 108: | |
| 109: | public function get_prefix() |
| 110: | { |
| 111: | return $this->getServiceValue('pgsql', 'dbaseprefix'); |
| 112: | } |
| 113: | |
| 114: | private function _delete_temp_user($user) |
| 115: | { |
| 116: | if (!$this->delete_user($user)) { |
| 117: | return false; |
| 118: | } |
| 119: | |
| 120: | $idx = array_search($user, $this->_tempUsers); |
| 121: | if ($idx !== false) { |
| 122: | unset($this->_tempUsers[$idx]); |
| 123: | } |
| 124: | |
| 125: | return true; |
| 126: | } |
| 127: | |
| 128: | |
| 129: | |
| 130: | |
| 131: | |
| 132: | |
| 133: | |
| 134: | |
| 135: | |
| 136: | |
| 137: | |
| 138: | public function delete_user($user, $cascade = false) |
| 139: | { |
| 140: | if ($user == $this->databaseAdmin() && !Util_Account_Hooks::is_mode('delete')) { |
| 141: | return error('Cannot remove main user'); |
| 142: | } |
| 143: | if (!$this->user_exists($user)) { |
| 144: | return error("db user `$user' not found"); |
| 145: | } |
| 146: | |
| 147: | $prefix = $this->get_prefix(); |
| 148: | if ($user !== $this->databaseAdmin() && strncmp($user, $prefix, strlen($prefix))) { |
| 149: | $user = $prefix . $user; |
| 150: | } |
| 151: | $tblspace = $this->get_tablespace(); |
| 152: | $pghandler = \PostgreSQL::initialize(); |
| 153: | Opcenter\Database\PostgreSQL::terminateUser($user); |
| 154: | if (function_exists('pg_escape_literal')) { |
| 155: | $usersafe = pg_escape_identifier($pghandler->getHandler(), $user); |
| 156: | } else { |
| 157: | $usersafe = '"' . pg_escape_string($pghandler->getHandler(), $user) . '"'; |
| 158: | } |
| 159: | $pghandler->query('REVOKE ALL ON TABLESPACE ' . $tblspace . ' FROM ' . $usersafe . ($cascade ? ' CASCADE' : '')); |
| 160: | $pghandler->query('DROP ROLE ' . $usersafe); |
| 161: | |
| 162: | if ($pghandler->error) { |
| 163: | return new PostgreSQLError('Invalid query, ' . $pghandler->error); |
| 164: | } |
| 165: | |
| 166: | return true; |
| 167: | |
| 168: | } |
| 169: | |
| 170: | |
| 171: | |
| 172: | |
| 173: | |
| 174: | |
| 175: | |
| 176: | |
| 177: | public function get_tablespace(): ?string |
| 178: | { |
| 179: | return $this->getServiceValue( |
| 180: | 'pgsql', |
| 181: | 'tablespace', |
| 182: | \Opcenter\Database\PostgreSQL::getTablespaceFromUser($this->databaseAdmin()) |
| 183: | ); |
| 184: | } |
| 185: | |
| 186: | |
| 187: | |
| 188: | |
| 189: | |
| 190: | |
| 191: | |
| 192: | public function site_from_tablespace(string $tblspace): ?string |
| 193: | { |
| 194: | $db = \PostgreSQL::pdo(); |
| 195: | $query = "SELECT rolname FROM pg_authid JOIN pg_tablespace ON (pg_authid.oid = pg_tablespace.spcowner) WHERE spcname = " . $db->quote($tblspace); |
| 196: | $rs = $db->query($query); |
| 197: | if (!$rs) { |
| 198: | return null; |
| 199: | } |
| 200: | $user = $rs->fetchObject()->rolname; |
| 201: | if ($siteid = \Auth::get_site_id_from_admin($user)) { |
| 202: | return 'site' . $siteid; |
| 203: | } |
| 204: | |
| 205: | $map = \Opcenter\Map::load('pgsql.usermap'); |
| 206: | |
| 207: | return $map->fetch($user) ?: null; |
| 208: | } |
| 209: | |
| 210: | public function set_username($user) |
| 211: | { |
| 212: | if (!IS_CLI) { |
| 213: | return $this->query('pgsql_set_username', $user); |
| 214: | } |
| 215: | |
| 216: | return $this->_set_pg_param('username', $user); |
| 217: | |
| 218: | } |
| 219: | |
| 220: | private function _set_pg_param($param, $val) |
| 221: | { |
| 222: | $pwd = $this->user_getpwnam(); |
| 223: | $file = $this->domain_fs_path() . $pwd['home'] . '/.pgpass'; |
| 224: | |
| 225: | if (!file_exists($file)) { |
| 226: | \Opcenter\Filesystem::touch($file, $this->user_id, $this->group_id, 0600); |
| 227: | } |
| 228: | |
| 229: | return \Opcenter\Database\PostgreSQL::setUserConfigurationField($file, $param, $val); |
| 230: | |
| 231: | } |
| 232: | |
| 233: | public function get_password($user = null) |
| 234: | { |
| 235: | if (DEMO_ADMIN_LOCK && ($this->permission_level & PRIVILEGE_ADMIN) && posix_getuid()) { |
| 236: | return error("Demo may not alter ~/.pgpass"); |
| 237: | } |
| 238: | |
| 239: | if (!IS_CLI) { |
| 240: | return $this->query('pgsql_get_password', $user); |
| 241: | } |
| 242: | if (!$user) { |
| 243: | $user = $this->databaseAdmin(); |
| 244: | } |
| 245: | $pwd = $this->user_getpwnam($user); |
| 246: | if (!$pwd) { |
| 247: | return error('unknown system user `%s\'', $user); |
| 248: | } |
| 249: | $file = $this->domain_fs_path() . $pwd['home'] . '/.pgpass'; |
| 250: | if (!file_exists($file)) { |
| 251: | return false; |
| 252: | } |
| 253: | |
| 254: | return \Opcenter\Database\PostgreSQL::getUserConfiguration($file)['password']; |
| 255: | } |
| 256: | |
| 257: | public function get_elevated_password_backend() |
| 258: | { |
| 259: | return Opcenter\Database\MySQL::rootPassword(); |
| 260: | } |
| 261: | |
| 262: | |
| 263: | |
| 264: | |
| 265: | |
| 266: | |
| 267: | |
| 268: | public function change_prefix($prefix) |
| 269: | { |
| 270: | return error('use sql_change_prefix'); |
| 271: | } |
| 272: | |
| 273: | public function get_sql_prefix() |
| 274: | { |
| 275: | deprecated('use pgsql_get_prefix'); |
| 276: | |
| 277: | return $this->get_prefix(); |
| 278: | } |
| 279: | |
| 280: | |
| 281: | |
| 282: | |
| 283: | |
| 284: | |
| 285: | |
| 286: | |
| 287: | |
| 288: | public function service_enabled() |
| 289: | { |
| 290: | deprecated('use enabled()'); |
| 291: | |
| 292: | return $this->enabled(); |
| 293: | } |
| 294: | |
| 295: | |
| 296: | |
| 297: | |
| 298: | |
| 299: | |
| 300: | |
| 301: | |
| 302: | public function enabled() |
| 303: | { |
| 304: | return parent::svc_enabled('pgsql'); |
| 305: | } |
| 306: | |
| 307: | |
| 308: | |
| 309: | |
| 310: | |
| 311: | |
| 312: | |
| 313: | public function create_database($db) |
| 314: | { |
| 315: | if (!$this->enabled()) { |
| 316: | return error("%(service)s is disabled", ['service' => 'PostgreSQL']); |
| 317: | } |
| 318: | |
| 319: | if (!preg_match(\Regex::SQL_DATABASE, $db)) { |
| 320: | return error("invalid database name `%s'", $db); |
| 321: | } |
| 322: | if ($this->database_exists($db)) { |
| 323: | return error("database `$db' exists"); |
| 324: | } |
| 325: | $prefix = $this->get_prefix(); |
| 326: | |
| 327: | |
| 328: | if (0 !== strpos($db, $prefix)) { |
| 329: | $db = $prefix . $db; |
| 330: | } |
| 331: | |
| 332: | if (null !== ($limit = $this->getConfig('pgsql', 'dbasenum', null)) && $limit >= 0) { |
| 333: | $count = \count($this->list_databases()); |
| 334: | if ($count >= $limit) { |
| 335: | return error("Database limit `%d' reached - cannot create additional databases", $limit); |
| 336: | } |
| 337: | } |
| 338: | |
| 339: | if (!$this->prep_tablespace()) { |
| 340: | return false; |
| 341: | } |
| 342: | |
| 343: | $dbaseadmin = $this->databaseAdmin(); |
| 344: | $pghandler = \PostgreSQL::initialize(); |
| 345: | $pghandler->query('CREATE DATABASE ' . pg_escape_identifier($pghandler->getHandler(), $db) . ' WITH OWNER = ' . |
| 346: | pg_escape_identifier($pghandler->getHandler(), $dbaseadmin) . ' TABLESPACE = ' . |
| 347: | pg_escape_identifier($pghandler->getHandler(), $this->get_tablespace()) . ' CONNECTION LIMIT = ' . |
| 348: | static::PER_DATABASE_CONNECTION_LIMIT); |
| 349: | if ($pghandler->error) { |
| 350: | return error('error while creating database: %s', $pghandler->error); |
| 351: | } |
| 352: | |
| 353: | $version = $this->version(); |
| 354: | |
| 355: | if ($version >= 150000 && $version < 160000) { |
| 356: | $this->set_owner($db, $dbaseadmin); |
| 357: | } |
| 358: | |
| 359: | return info("created database `%s'", $db); |
| 360: | } |
| 361: | |
| 362: | |
| 363: | |
| 364: | |
| 365: | |
| 366: | |
| 367: | |
| 368: | public function database_exists($db) |
| 369: | { |
| 370: | if (!($this->permission_level & PRIVILEGE_ADMIN)) { |
| 371: | $prefix = $this->get_prefix(); |
| 372: | if (0 !== strpos($db, $prefix)) { |
| 373: | $db = $prefix . $db; |
| 374: | } |
| 375: | } |
| 376: | $pgdb = \PostgreSQL::initialize(); |
| 377: | $q = $pgdb->query_params('SELECT 1 FROM pg_database WHERE datname = $1', array($pgdb->escape_string($db))); |
| 378: | |
| 379: | return !$q || $pgdb->num_rows() > 0; |
| 380: | } |
| 381: | |
| 382: | |
| 383: | |
| 384: | |
| 385: | |
| 386: | |
| 387: | |
| 388: | public function prep_tablespace() |
| 389: | { |
| 390: | |
| 391: | $user = $this->databaseAdmin(); |
| 392: | if (\Opcenter\Database\PostgreSQL::getTablespaceFromUser($user)) { |
| 393: | return true; |
| 394: | } |
| 395: | $path = $this->domain_fs_path() . self::PGSQL_DATADIR; |
| 396: | if (!file_exists($path)) { |
| 397: | $this->query('pgsql_prep_tablespace_backend', $path); |
| 398: | } |
| 399: | |
| 400: | return \Opcenter\Database\PostgreSQL::initializeTablespace($this->site, $path, $user); |
| 401: | } |
| 402: | |
| 403: | |
| 404: | |
| 405: | |
| 406: | |
| 407: | |
| 408: | |
| 409: | |
| 410: | public function add_extension(string $db, string $extension): bool |
| 411: | { |
| 412: | if (!IS_CLI) { |
| 413: | return $this->query('pgsql_add_extension', $db, $extension); |
| 414: | } |
| 415: | |
| 416: | $extensions = $this->_getPermittedExtensions(); |
| 417: | if (!in_array($extension, $extensions, true)) { |
| 418: | return error("extension `%s' unrecognized or disallowed usage", $extension); |
| 419: | } |
| 420: | |
| 421: | $prefix = $this->get_prefix(); |
| 422: | if (0 !== strpos($db, $prefix)) { |
| 423: | $db = $prefix . $db; |
| 424: | } |
| 425: | |
| 426: | $dbs = $this->list_databases(); |
| 427: | if (!in_array($db, $dbs, true)) { |
| 428: | return error("database `%s' unknown", $db); |
| 429: | } |
| 430: | |
| 431: | $cmd = 'CREATE EXTENSION IF NOT EXISTS ' . $extension . ' WITH SCHEMA public'; |
| 432: | $proc = Util_Process_Safe::exec('psql -c %s %s', $cmd, $db); |
| 433: | if (!$proc['success']) { |
| 434: | return error('extension creation failed - %s', $proc['stderr']); |
| 435: | } |
| 436: | |
| 437: | return $proc['success']; |
| 438: | } |
| 439: | |
| 440: | |
| 441: | |
| 442: | |
| 443: | |
| 444: | |
| 445: | protected function _getPermittedExtensions() |
| 446: | { |
| 447: | return static::PGSQL_PERMITTED_EXTENSIONS; |
| 448: | } |
| 449: | |
| 450: | |
| 451: | |
| 452: | |
| 453: | |
| 454: | |
| 455: | |
| 456: | public function list_databases(): array |
| 457: | { |
| 458: | |
| 459: | $prefix = $this->get_prefix(); |
| 460: | if (!$prefix) { |
| 461: | |
| 462: | if (!$this->enabled()) { |
| 463: | return []; |
| 464: | } |
| 465: | if ($this->enabled()) { |
| 466: | report('Prefixless site - bug'); |
| 467: | } |
| 468: | |
| 469: | return []; |
| 470: | } |
| 471: | $pgdb = \PostgreSQL::initialize(); |
| 472: | $pgdb->query("SELECT datname FROM pg_database WHERE datname LIKE '" |
| 473: | . str_replace(array('-', '_'), array('', '\_'), $prefix) . "%' OR datdba = " |
| 474: | . "(SELECT oid FROM pg_roles WHERE rolname = '" . $this->databaseAdmin() . "')"); |
| 475: | $dbs = array(); |
| 476: | while ($row = $pgdb->fetch_object()) { |
| 477: | $dbs[] = $row->datname; |
| 478: | } |
| 479: | |
| 480: | return $dbs; |
| 481: | } |
| 482: | |
| 483: | |
| 484: | |
| 485: | |
| 486: | |
| 487: | |
| 488: | |
| 489: | |
| 490: | public function set_owner(string $db, string $owner): bool |
| 491: | { |
| 492: | $pgdb = \PostgreSQL::initialize(); |
| 493: | |
| 494: | $db = $pgdb->escape_string($this->canonicalize($db)); |
| 495: | $users = $this->list_users(); |
| 496: | |
| 497: | if (!isset($users[$owner])) { |
| 498: | $tmp = $this->canonicalize($owner); |
| 499: | if (!isset($users[$tmp])) { |
| 500: | return error("Unknown pgsql user `%s'", $owner); |
| 501: | } |
| 502: | $owner = $tmp; |
| 503: | } |
| 504: | $dbs = $this->list_databases(); |
| 505: | if (!in_array($db, $dbs, true)) { |
| 506: | return error("Unknown database `%s'", $db); |
| 507: | } |
| 508: | |
| 509: | $q = 'ALTER DATABASE ' . pg_escape_identifier($pgdb->getHandler(), $db) . ' OWNER TO ' . pg_escape_identifier($pgdb->getHandler(), $owner); |
| 510: | if (false === $pgdb->query($q)) { |
| 511: | return error("Failed to change owner to `%s'", $owner); |
| 512: | } |
| 513: | |
| 514: | if ($this->version() >= 150000) { |
| 515: | |
| 516: | $pgdb = \PostgreSQL::stub(); |
| 517: | $pgdb->connect(null, null, null, $db); |
| 518: | $pgdb->query( |
| 519: | "GRANT ALL ON DATABASE " . pg_escape_identifier($pgdb->getHandler(), $db) . " TO " . |
| 520: | pg_escape_identifier($pgdb->getHandler(), $owner) |
| 521: | ); |
| 522: | $q = 'GRANT USAGE, CREATE ON SCHEMA public TO ' . pg_escape_identifier($pgdb->getHandler(), $owner); |
| 523: | $pgdb->query($q); |
| 524: | } |
| 525: | |
| 526: | return true; |
| 527: | } |
| 528: | |
| 529: | |
| 530: | |
| 531: | |
| 532: | |
| 533: | |
| 534: | |
| 535: | private function canonicalize(string $name): string |
| 536: | { |
| 537: | $prefix = $this->get_prefix(); |
| 538: | if ($prefix && 0 !== strpos($name, $prefix)) { |
| 539: | $name = $prefix . $name; |
| 540: | } |
| 541: | |
| 542: | return $name; |
| 543: | } |
| 544: | |
| 545: | |
| 546: | |
| 547: | |
| 548: | |
| 549: | |
| 550: | |
| 551: | public function list_users(): array |
| 552: | { |
| 553: | |
| 554: | if (!$prefix = $this->get_prefix()) { |
| 555: | return []; |
| 556: | } |
| 557: | |
| 558: | $pgdb = \PostgreSQL::initialize(); |
| 559: | $q = $pgdb->query("SELECT rolname, rolpassword, rolconnlimit FROM pg_authid WHERE rolname = '" |
| 560: | . $this->databaseAdmin() . "' OR rolname LIKE '" . str_replace(array('-', '_'), array('', '\_'), |
| 561: | $prefix) . "%' ORDER BY rolname"); |
| 562: | $users = array(); |
| 563: | while ($row = $pgdb->fetch_object()) { |
| 564: | $users[$row->rolname] = array( |
| 565: | 'max_connections' => (int)$row->rolconnlimit, |
| 566: | 'password' => $row->rolpassword |
| 567: | ); |
| 568: | } |
| 569: | |
| 570: | return $users; |
| 571: | } |
| 572: | |
| 573: | |
| 574: | |
| 575: | |
| 576: | |
| 577: | |
| 578: | |
| 579: | |
| 580: | |
| 581: | |
| 582: | public function set_privileges(string $user, string $host, string $db, array $privileges): bool |
| 583: | { |
| 584: | deprecated_func('Use change_owner() to change database owner'); |
| 585: | return $this->change_owner($db, $user); |
| 586: | } |
| 587: | |
| 588: | |
| 589: | |
| 590: | |
| 591: | |
| 592: | |
| 593: | |
| 594: | |
| 595: | |
| 596: | public function change_owner(string $db, string $newowner): bool |
| 597: | { |
| 598: | $prefix = $this->get_prefix(); |
| 599: | |
| 600: | if (!$this->user_exists($newowner)) { |
| 601: | if (0 !== strpos($newowner, $prefix)) { |
| 602: | return $this->change_owner($db, $prefix . $newowner); |
| 603: | } |
| 604: | return error("User `%s' does not exist", $newowner); |
| 605: | } |
| 606: | |
| 607: | if (!$this->database_exists($db)) { |
| 608: | if (0 !== strpos($db, $prefix)) { |
| 609: | return $this->change_owner($prefix . $db, $newowner); |
| 610: | } |
| 611: | return error("Database `%s' does not exist", $db); |
| 612: | } |
| 613: | |
| 614: | $pgdb = \PostgreSQL::initialize(); |
| 615: | |
| 616: | $vendor = \Opcenter\Database\PostgreSQL::vendor(); |
| 617: | $res = \PostgreSQL::initialize()->query($vendor->changeDatabaseOwner($db, $newowner)); |
| 618: | |
| 619: | |
| 620: | return $res && $res->affected_rows() > 0; |
| 621: | } |
| 622: | |
| 623: | |
| 624: | |
| 625: | |
| 626: | |
| 627: | |
| 628: | |
| 629: | public function get_owner(string $database): ?string |
| 630: | { |
| 631: | if (!$this->database_exists($database)) { |
| 632: | $prefix = $this->get_prefix(); |
| 633: | if (0 !== strpos($database, $prefix)) { |
| 634: | return $this->get_owner($prefix . $database); |
| 635: | } |
| 636: | } |
| 637: | if (!($tblspace = $this->get_tablespace())) { |
| 638: | report('Failed tablespace inquiry for %s on %s', $database, $this->site); |
| 639: | return null; |
| 640: | } |
| 641: | $pgdb = \PostgreSQL::initialize(); |
| 642: | $vendor = \Opcenter\Database\PostgreSQL::vendor(); |
| 643: | |
| 644: | $res = \PostgreSQL::initialize()->query($vendor->ownerFromDatabase($database, $tblspace)); |
| 645: | if (!$res) { |
| 646: | return null; |
| 647: | } |
| 648: | |
| 649: | return array_get($res->fetch_assoc(), 'owner', null); |
| 650: | } |
| 651: | |
| 652: | |
| 653: | |
| 654: | |
| 655: | |
| 656: | |
| 657: | |
| 658: | |
| 659: | public function prep_tablespace_backend($location) |
| 660: | { |
| 661: | if (!is_dir($location)) { |
| 662: | mkdir($location) || fatal("failed to create pgsql data directory `%s'", $location); |
| 663: | } |
| 664: | chown($location, 'postgres'); |
| 665: | chgrp($location, (int)\Session::get('group_id', posix_getgrnam('postgres'))); |
| 666: | chmod($location, 02750); |
| 667: | } |
| 668: | |
| 669: | |
| 670: | |
| 671: | |
| 672: | |
| 673: | |
| 674: | |
| 675: | |
| 676: | |
| 677: | |
| 678: | public function add_user_permissions($user, $db, array $opts) |
| 679: | { |
| 680: | return error('Function not implemented in PostgreSQL'); |
| 681: | } |
| 682: | |
| 683: | public function delete_user_permissions($user, $db) |
| 684: | { |
| 685: | return error('Function not implemented in PostgreSQL'); |
| 686: | } |
| 687: | |
| 688: | |
| 689: | |
| 690: | |
| 691: | |
| 692: | |
| 693: | |
| 694: | public function get_user_permissions($user, $db) |
| 695: | { |
| 696: | return error('Function not implemented in PostgreSQL'); |
| 697: | } |
| 698: | |
| 699: | |
| 700: | |
| 701: | |
| 702: | |
| 703: | |
| 704: | |
| 705: | |
| 706: | public function delete_database($db) |
| 707: | { |
| 708: | $pgdb = \PostgreSQL::initialize(); |
| 709: | $prefix = $this->get_prefix(); |
| 710: | if (!$prefix) { |
| 711: | report('Prefixless site - bug'); |
| 712: | |
| 713: | return false; |
| 714: | } |
| 715: | if (0 !== strpos($db, $prefix)) { |
| 716: | $db = $prefix . $db; |
| 717: | } |
| 718: | $db = $pgdb->escape_string($db); |
| 719: | if (!in_array($db, $this->list_databases())) { |
| 720: | return error("Unknown database `%s'", $db); |
| 721: | } |
| 722: | $resp = \Opcenter\Database\PostgreSQL::dropDatabase($db); |
| 723: | |
| 724: | if (in_array($db, $this->list_backups(), true)) { |
| 725: | $this->delete_backup($db); |
| 726: | } |
| 727: | |
| 728: | if (!$resp) { |
| 729: | return error('Error while dropping database, ' . $pgdb->error); |
| 730: | } |
| 731: | |
| 732: | return true; |
| 733: | } |
| 734: | |
| 735: | |
| 736: | |
| 737: | |
| 738: | |
| 739: | |
| 740: | |
| 741: | public function delete_backup($db) |
| 742: | { |
| 743: | return parent::delete_backup_real('pgsql', $db); |
| 744: | } |
| 745: | |
| 746: | |
| 747: | |
| 748: | |
| 749: | |
| 750: | |
| 751: | |
| 752: | |
| 753: | |
| 754: | |
| 755: | |
| 756: | |
| 757: | |
| 758: | |
| 759: | |
| 760: | public function edit_user($user, $password, $maxconn = null) |
| 761: | { |
| 762: | $prefix = str_replace('-', '', $this->get_prefix()); |
| 763: | if ($user != $this->databaseAdmin() && |
| 764: | strncmp($user, $prefix, strlen($prefix)) |
| 765: | ) { |
| 766: | $user = $prefix . $user; |
| 767: | } |
| 768: | if (is_int($maxconn) && ($maxconn < 1)) { |
| 769: | $maxconn = self::DEFAULT_CONCURRENCY_LIMIT; |
| 770: | } |
| 771: | if (!$password && !$maxconn) { |
| 772: | return warn("no action taken for `$user'"); |
| 773: | } |
| 774: | if ($password && strlen($password) < self::MIN_PASSWORD_LENGTH) { |
| 775: | return error('pgsql password must be at least %d characters long', self::MIN_PASSWORD_LENGTH); |
| 776: | } |
| 777: | $pgdb = \PostgreSQL::pdo(); |
| 778: | $params = [ |
| 779: | ':name' => $user, |
| 780: | ':password' => $password, |
| 781: | ':connlimit' => $maxconn |
| 782: | ]; |
| 783: | |
| 784: | if (!$password && is_int($maxconn)) { |
| 785: | $query = 'UPDATE pg_authid SET rolconnlimit = :connlimit WHERE rolname = :name'; |
| 786: | unset($params[':password']); |
| 787: | } else if ($password && is_int($maxconn)) { |
| 788: | $query = 'UPDATE pg_authid SET rolpassword = :password, rolconnlimit = :connlimit WHERE rolname = :name'; |
| 789: | } else if ($password && !is_int($maxconn)) { |
| 790: | $query = \Opcenter\Database\PostgreSQL::vendor()->setPasswordCrypted($user, $password); |
| 791: | $params = []; |
| 792: | } |
| 793: | |
| 794: | $stmt = $pgdb->prepare($query); |
| 795: | |
| 796: | if (!$stmt->execute($params)) { |
| 797: | return error("Failed to edit user `%s': %s", |
| 798: | $user, |
| 799: | array_get($stmt->errorInfo(), 2, 'UNKNOWN') |
| 800: | ); |
| 801: | } |
| 802: | |
| 803: | |
| 804: | if ($password && (str_starts_with($password, 'md5') && strlen($password) !== 35 || !str_starts_with($password, 'md5') && !str_starts_with($password, 'SCRAM-SHA-256$'))) { |
| 805: | $query = \Opcenter\Database\PostgreSQL::vendor()->setPassword($user, $password); |
| 806: | |
| 807: | if ($user === $this->get_username()) { |
| 808: | defer($_, fn() => $this->set_password($password)); |
| 809: | } |
| 810: | |
| 811: | return (bool)(\PostgreSQL::initialize()->query($query) ?: |
| 812: | error("failed to update postgresql password for user `%s'", $user)); |
| 813: | |
| 814: | } |
| 815: | |
| 816: | return true; |
| 817: | } |
| 818: | |
| 819: | public function get_username() |
| 820: | { |
| 821: | if (!IS_CLI) { |
| 822: | return $this->query('pgsql_get_username'); |
| 823: | } |
| 824: | |
| 825: | $user = ($this->permission_level & PRIVILEGE_SITE) ? $this->databaseAdmin() : $this->username; |
| 826: | $pwd = $this->user_getpwnam($user); |
| 827: | if (!$pwd) { |
| 828: | return error('unknown system user `%s\'', $user); |
| 829: | } |
| 830: | $file = $this->domain_fs_path() . $pwd['home'] . '/.pgpass'; |
| 831: | if (!file_exists($file)) { |
| 832: | return $this->username; |
| 833: | } |
| 834: | $contents = file_get_contents($file); |
| 835: | if (!preg_match(Regex::SQL_PGPASS, $contents, $matches)) { |
| 836: | return $user; |
| 837: | } |
| 838: | |
| 839: | if (!$matches['username'] || $matches['username'] === '*') { |
| 840: | return $this->username; |
| 841: | } |
| 842: | |
| 843: | return $matches['username']; |
| 844: | } |
| 845: | |
| 846: | public function set_password($password) |
| 847: | { |
| 848: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
| 849: | return error("Demo may not alter ~/.pgpass"); |
| 850: | } |
| 851: | |
| 852: | if (!IS_CLI) { |
| 853: | return $this->query('pgsql_set_password', $password); |
| 854: | } |
| 855: | |
| 856: | return $this->_set_pg_param('password', $password); |
| 857: | |
| 858: | } |
| 859: | |
| 860: | |
| 861: | |
| 862: | |
| 863: | |
| 864: | |
| 865: | |
| 866: | public function vacuum($db) |
| 867: | { |
| 868: | $pgdb = \PostgreSQL::initialize(); |
| 869: | $db = $pgdb->escape_string($db); |
| 870: | $prefix = $this->get_prefix(); |
| 871: | |
| 872: | |
| 873: | if (0 !== strpos($db, $prefix)) { |
| 874: | $db = $prefix . $db; |
| 875: | } |
| 876: | $q = 'SELECT 1 FROM pg_database WHERE datname = $1 ' . |
| 877: | "AND datdba = (SELECT oid FROM pg_roles WHERE rolname = '" . $this->databaseAdmin() . "')"; |
| 878: | $pgdb->query_params($q, array($db)); |
| 879: | if ($pgdb->num_rows() < 1) { |
| 880: | return error("Database `$db' not owned by main user"); |
| 881: | } |
| 882: | |
| 883: | return $this->query('pgsql_vacuum_backend', $db); |
| 884: | } |
| 885: | |
| 886: | public function vacuum_backend($db) |
| 887: | { |
| 888: | $status = Util_Process::exec(['vacuumdb', '-zfq', '-d', $db]); |
| 889: | |
| 890: | return $status['success'] ?: error('Vacuum failed: %s', coalesce($status['stderr'], $status['stdout'])); |
| 891: | } |
| 892: | |
| 893: | public function truncate_database($db) |
| 894: | { |
| 895: | |
| 896: | return $this->_empty_truncate_wrapper($db, 'truncate'); |
| 897: | } |
| 898: | |
| 899: | private function _empty_truncate_wrapper($db, $mode) |
| 900: | { |
| 901: | if ($mode != 'truncate' && $mode != 'empty') { |
| 902: | return error("unknown mode `%s'", $mode); |
| 903: | } |
| 904: | if ($mode == 'empty') { |
| 905: | |
| 906: | $mode = 'drop'; |
| 907: | } |
| 908: | |
| 909: | $prefix = $this->get_prefix(); |
| 910: | if (strncmp($db, $prefix, strlen($prefix))) { |
| 911: | $db = $prefix . $db; |
| 912: | } |
| 913: | |
| 914: | if (!$this->database_exists($db)) { |
| 915: | return error("unknown database, `%s'", $db); |
| 916: | } |
| 917: | |
| 918: | $user = $this->_create_temp_user($db); |
| 919: | if (!$user) { |
| 920: | return error("failed to %s db `%s'", $mode, $db); |
| 921: | } |
| 922: | $dsn = 'host=localhost dbname=' . $db . ' user=' . $user . ' password=' . self::PG_TEMP_PASSWORD; |
| 923: | $sqldb = pg_connect($dsn); |
| 924: | if (!$sqldb) { |
| 925: | $this->_delete_temp_user($user); |
| 926: | |
| 927: | return error("failed to %s db `%s', db connection failed", $mode, $db); |
| 928: | } |
| 929: | |
| 930: | $q = 'SELECT n.nspname as "schema", ' . |
| 931: | 'c.relname as "name", ' . |
| 932: | 'r.rolname as "owner"' . |
| 933: | 'FROM pg_catalog.pg_class c ' . |
| 934: | 'JOIN pg_catalog.pg_roles r ON r.oid = c.relowner ' . |
| 935: | 'LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace ' . |
| 936: | "WHERE c.relkind IN ('r','') " . |
| 937: | "AND n.nspname <> 'pg_catalog' " . |
| 938: | "AND n.nspname !~ '^pg_toast' " . |
| 939: | 'AND pg_catalog.pg_table_is_visible(c.oid) ' . |
| 940: | 'ORDER BY 1,2;'; |
| 941: | $rs = \pg_query($sqldb, $q); |
| 942: | $pgver = $this->version(); |
| 943: | |
| 944: | $identity = ($mode !== 'drop' && $pgver >= 80400) ? 'RESTART IDENTITY' : ''; |
| 945: | while (false !== ($res = pg_fetch_object($rs))) { |
| 946: | if (function_exists('pg_escape_identifier')) { |
| 947: | $tablesafe = \pg_escape_identifier($sqldb, $res->name); |
| 948: | } else { |
| 949: | $tablesafe = '"' . \pg_escape_string($sqldb, $res->name) . '"'; |
| 950: | } |
| 951: | $q = strtoupper($mode) . ' TABLE ' . $tablesafe . ' ' . $identity . ' CASCADE'; |
| 952: | if (!($res = \pg_query($sqldb, $q))) { |
| 953: | warn("failed to %s table `%s': %s", $mode, $res->name, pg_errormessage($sqldb)); |
| 954: | } |
| 955: | } |
| 956: | $this->_delete_temp_user($user); |
| 957: | |
| 958: | return true; |
| 959: | } |
| 960: | |
| 961: | |
| 962: | |
| 963: | private function _create_temp_user($db) |
| 964: | { |
| 965: | |
| 966: | $prefix = $this->get_prefix(); |
| 967: | $maxlen = self::IDENTIFIER_MAXLEN - strlen($prefix); |
| 968: | if ($maxlen < 1) { |
| 969: | warn('temp pgsql user exceeds field length'); |
| 970: | return false; |
| 971: | } |
| 972: | |
| 973: | $user = $prefix . \Opcenter\Auth\Password::generate(min(6, $maxlen), 'a-f0-9'); |
| 974: | |
| 975: | if ($this->user_exists($user)) { |
| 976: | return error('cannot create temp pgsql user'); |
| 977: | } |
| 978: | |
| 979: | if (!$this->add_user($user, self::PG_TEMP_PASSWORD, 1)) { |
| 980: | return error('unable to create role on pgsql database %s', $db); |
| 981: | } |
| 982: | |
| 983: | $sqldb = \PostgreSQL::stub()->connect(null, null, null, $db); |
| 984: | |
| 985: | $sqldb->query('GRANT "' . $this->databaseAdmin() . '" TO "' . $user . '"'); |
| 986: | |
| 987: | $this->_register_temp_user($user); |
| 988: | |
| 989: | return $user; |
| 990: | |
| 991: | } |
| 992: | |
| 993: | |
| 994: | |
| 995: | |
| 996: | public function add_user(string $user, string $password, int $maxconn = self::DEFAULT_CONCURRENCY_LIMIT): bool |
| 997: | { |
| 998: | if (!$this->enabled()) { |
| 999: | return error("%(service)s is disabled", ['service' => 'PostgreSQL']); |
| 1000: | } |
| 1001: | if (!$user) { |
| 1002: | return error('no username specified'); |
| 1003: | } |
| 1004: | $prefix = str_replace('-', '', $this->get_prefix()); |
| 1005: | if ($user !== $this->databaseAdmin() && |
| 1006: | 0 !== strpos($user, $prefix)) { |
| 1007: | $user = $prefix . $user; |
| 1008: | } |
| 1009: | |
| 1010: | if ($this->user_exists($user)) { |
| 1011: | return error("pg user `$user' exists"); |
| 1012: | } |
| 1013: | |
| 1014: | if ($maxconn < 0) { |
| 1015: | $maxconn = self::PER_DATABASE_CONNECTION_LIMIT; |
| 1016: | } |
| 1017: | if (strlen($password) < self::MIN_PASSWORD_LENGTH) { |
| 1018: | return error('Password must be at least %d characters', self::MIN_PASSWORD_LENGTH); |
| 1019: | } else if ($maxconn < 0) { |
| 1020: | return error('Max connections, queries, and updates must be greater than -1'); |
| 1021: | } |
| 1022: | if (!\Opcenter\Database\PostgreSQL::createUser($user, $password)) { |
| 1023: | return false; |
| 1024: | } |
| 1025: | \Opcenter\Database\PostgreSQL::setRole($user, $this->databaseAdmin()); |
| 1026: | $vendor = \Opcenter\Database\PostgreSQL::vendor(); |
| 1027: | |
| 1028: | return (bool)\PostgreSQL::initialize()->query($vendor->setMaxConnections($user, $maxconn)); |
| 1029: | } |
| 1030: | |
| 1031: | public function version(bool $pretty = false): int|string |
| 1032: | { |
| 1033: | $version = \Opcenter\Database\PostgreSQL::version(); |
| 1034: | if (!$pretty) { |
| 1035: | return $version; |
| 1036: | } |
| 1037: | $pgver = array(); |
| 1038: | foreach (array('patch', 'minor', 'major') as $v) { |
| 1039: | $pgver[$v] = (int)$version % 100; |
| 1040: | $version /= 100; |
| 1041: | } |
| 1042: | |
| 1043: | return $pgver['major'] . '.' . $pgver['minor'] . '.' . |
| 1044: | $pgver['patch']; |
| 1045: | } |
| 1046: | |
| 1047: | |
| 1048: | |
| 1049: | |
| 1050: | |
| 1051: | |
| 1052: | |
| 1053: | public function empty($db) |
| 1054: | { |
| 1055: | if (!$this->database_exists($db)) { |
| 1056: | return true; |
| 1057: | } |
| 1058: | |
| 1059: | return \count(\Opcenter\Database\PostgreSQL::getTablesFromDatabase($db)) === 0; |
| 1060: | } |
| 1061: | |
| 1062: | public function empty_database($db) |
| 1063: | { |
| 1064: | return $this->_empty_truncate_wrapper($db, 'empty'); |
| 1065: | } |
| 1066: | |
| 1067: | |
| 1068: | |
| 1069: | |
| 1070: | public function import($db, $file) |
| 1071: | { |
| 1072: | if (!IS_CLI) { |
| 1073: | return $this->query('pgsql_import', $db, $file); |
| 1074: | } |
| 1075: | |
| 1076: | $prefix = $this->get_prefix(); |
| 1077: | |
| 1078: | if (strncmp($db, $prefix, strlen($prefix))) { |
| 1079: | $db = $prefix . $db; |
| 1080: | } |
| 1081: | |
| 1082: | $dbs = $this->list_databases(); |
| 1083: | if (false === array_search($db, $dbs, true)) { |
| 1084: | return error("database `%s' does not exist", $db); |
| 1085: | } |
| 1086: | $unlink = null; |
| 1087: | if (false === ($realfile = $this->_preImport($file, $unlink))) { |
| 1088: | return false; |
| 1089: | } |
| 1090: | $user = $this->_create_temp_user($db); |
| 1091: | if (!$user) { |
| 1092: | return error('import failed - cannot create temp user'); |
| 1093: | } |
| 1094: | $proc = new Util_Process_Safe(); |
| 1095: | $proc->setEnvironment('PGPASSWORD', self::PG_TEMP_PASSWORD); |
| 1096: | $cmd = 'psql --set=ON_ERROR_STOP=on -q -h 127.0.0.1 -f %(file)s -U %(user)s %(db)s'; |
| 1097: | if (\basename($realfile) === 'toc.dat' && file_exists(\dirname($realfile) . '/restore.sql')) { |
| 1098: | |
| 1099: | $realfile = \dirname($realfile); |
| 1100: | $cmd = 'pg_restore -h 127.0.0.1 -x -O -U %(user)s -d %(db)s %(file)s'; |
| 1101: | } |
| 1102: | $args = array( |
| 1103: | 'password' => self::PG_TEMP_PASSWORD, |
| 1104: | 'file' => $realfile, |
| 1105: | 'user' => $user, |
| 1106: | 'db' => $db |
| 1107: | ); |
| 1108: | $proc->setPriority(19); |
| 1109: | $status = $proc->run($cmd, $args); |
| 1110: | $this->_delete_temp_user($user); |
| 1111: | $this->_postImport($unlink); |
| 1112: | |
| 1113: | if (!$status['success']) { |
| 1114: | return error('import failed: %s', $status['error']); |
| 1115: | } |
| 1116: | |
| 1117: | return $status['success']; |
| 1118: | } |
| 1119: | |
| 1120: | public function export($db, $file = null) |
| 1121: | { |
| 1122: | if (!IS_CLI) { |
| 1123: | return $this->query('pgsql_export', $db, $file); |
| 1124: | } |
| 1125: | if (is_null($file)) { |
| 1126: | $file = $db . '.sql'; |
| 1127: | } |
| 1128: | |
| 1129: | if ($file[0] !== '/' && $file[0] !== '.' && $file[0] !== '~') { |
| 1130: | $path = $this->domain_fs_path() . '/tmp/' . $file; |
| 1131: | } else { |
| 1132: | $path = $this->file_make_path($file); |
| 1133: | } |
| 1134: | if (!$path) { |
| 1135: | return error("invalid file `%s'", $file); |
| 1136: | } |
| 1137: | |
| 1138: | if (file_exists($path) && |
| 1139: | (filesize($path) > 0 || realpath($path) !== $path || fileowner($path) < USER_MIN_UID)) |
| 1140: | { |
| 1141: | return error('%s: file exists, cannot overwrite', $file); |
| 1142: | } |
| 1143: | |
| 1144: | $file = $this->file_unmake_path($path); |
| 1145: | $pdir = dirname($file); |
| 1146: | if (!$this->file_exists($pdir) && !$this->file_create_directory($pdir, 0755, true)) { |
| 1147: | return error("failed to create parent directory, `%s'", $pdir); |
| 1148: | } |
| 1149: | |
| 1150: | if (!in_array($db, $this->list_databases())) { |
| 1151: | return error("invalid database `%s'", $db); |
| 1152: | } |
| 1153: | |
| 1154: | $user = $this->_create_temp_user($db); |
| 1155: | if (!$user) { |
| 1156: | return error('pgsql export failed - unable to create user'); |
| 1157: | } |
| 1158: | |
| 1159: | $fsizelimit = Util_Ulimit::get('fsize'); |
| 1160: | if ($this->get_database_size($db) > $fsizelimit / self::DB_BIN2TXT_MULT) { |
| 1161: | |
| 1162: | Util_Ulimit::set('fsize', 'unlimited'); |
| 1163: | } else { |
| 1164: | $fsizelimit = null; |
| 1165: | } |
| 1166: | |
| 1167: | $cmd = 'cd %s && umask 077 && env PGPASSWORD=%s pg_dump -h 127.0.0.1 --if-exists -c -U %s -x --file=%s %s'; |
| 1168: | |
| 1169: | $proc = new Util_Process_Safe(); |
| 1170: | |
| 1171: | $proc->setSgid($this->group_id); |
| 1172: | $proc->setSuid($this->user_id); |
| 1173: | $proc->setPriority(19); |
| 1174: | $status = $proc->run($cmd, |
| 1175: | sys_get_temp_dir(), |
| 1176: | self::PG_TEMP_PASSWORD, |
| 1177: | $user, |
| 1178: | $path, |
| 1179: | $db |
| 1180: | ); |
| 1181: | |
| 1182: | if ($user != self::MASTER_USER) { |
| 1183: | $this->_delete_temp_user($user); |
| 1184: | } |
| 1185: | if (!is_null($fsizelimit)) { |
| 1186: | Util_Ulimit::set('fsize', $fsizelimit); |
| 1187: | } |
| 1188: | |
| 1189: | if (!file_exists($path) || !$status['success']) { |
| 1190: | return error('export failed: %s', $status['stderr']); |
| 1191: | } |
| 1192: | |
| 1193: | return $this->file_unmake_path($path); |
| 1194: | } |
| 1195: | |
| 1196: | |
| 1197: | |
| 1198: | |
| 1199: | |
| 1200: | |
| 1201: | |
| 1202: | |
| 1203: | |
| 1204: | public function get_database_size($db) |
| 1205: | { |
| 1206: | $pgdb = \PostgreSQL::initialize(); |
| 1207: | $size = $pgdb->query('SELECT pg_database_size(' . pg_escape_literal($pgdb->getHandler(), $db) . ') as size')->fetch_object(); |
| 1208: | |
| 1209: | return (int)$size->size; |
| 1210: | } |
| 1211: | |
| 1212: | public function get_table_sizes(string $db) |
| 1213: | { |
| 1214: | if (!$this->database_exists($db)) { |
| 1215: | return error("Unknown database `%s'", $db); |
| 1216: | } |
| 1217: | $db = $this->canonicalize($db); |
| 1218: | $conn = \PostgreSQL::stub(); |
| 1219: | $conn->connect(mDatabase: $db); |
| 1220: | $relations = []; |
| 1221: | $query = \Opcenter\Database\PostgreSQL::vendor()->tableSize($db); |
| 1222: | $res = $conn->query($query); |
| 1223: | while($r = $res->fetch_object()) { |
| 1224: | $relations[] = [ |
| 1225: | 'schema' => $r->schema_name, |
| 1226: | 'table' => null !== $r->type, |
| 1227: | 'size' => (int)$r->table_size, |
| 1228: | 'name' => $r->relname |
| 1229: | ]; |
| 1230: | } |
| 1231: | |
| 1232: | return $relations; |
| 1233: | } |
| 1234: | |
| 1235: | |
| 1236: | |
| 1237: | |
| 1238: | |
| 1239: | |
| 1240: | |
| 1241: | |
| 1242: | public function clone(string $from, string $to): bool |
| 1243: | { |
| 1244: | if ($this->database_exists($to) && !$this->empty($to)) { |
| 1245: | return error("Database `%s' already exists", $to); |
| 1246: | } |
| 1247: | if (!$this->database_exists($from)) { |
| 1248: | return error("Database `%s' does not exist", $from); |
| 1249: | } |
| 1250: | if (!$this->database_exists($to) && !$this->create_database($to)) { |
| 1251: | return false; |
| 1252: | } |
| 1253: | |
| 1254: | $pipe = $this->export_pipe($from); |
| 1255: | defer($_, static function () use ($pipe) { |
| 1256: | if (file_exists($pipe)) { |
| 1257: | unlink($pipe); |
| 1258: | } |
| 1259: | }); |
| 1260: | |
| 1261: | return $this->import($to, $this->file_unmake_path($pipe)); |
| 1262: | } |
| 1263: | |
| 1264: | |
| 1265: | |
| 1266: | |
| 1267: | |
| 1268: | |
| 1269: | |
| 1270: | public function export_pipe($db) |
| 1271: | { |
| 1272: | $dbs = $this->list_databases(); |
| 1273: | if (!\in_array($db, $dbs, true)) { |
| 1274: | $originalDb = $db; |
| 1275: | $db = $this->get_prefix() . $db; |
| 1276: | if (!\in_array($db, $dbs, true)) { |
| 1277: | return error('Invalid database %s', $originalDb); |
| 1278: | } |
| 1279: | } |
| 1280: | |
| 1281: | $user = $this->_create_temp_user($db); |
| 1282: | |
| 1283: | return $this->query('pgsql_export_pipe_real', $db, $user); |
| 1284: | } |
| 1285: | |
| 1286: | |
| 1287: | |
| 1288: | |
| 1289: | |
| 1290: | |
| 1291: | |
| 1292: | public function resolve_site_from_database(string $db): ?string |
| 1293: | { |
| 1294: | $db = strtok($db, '/'); |
| 1295: | $prefix = strtok($db, '_') . '_'; |
| 1296: | |
| 1297: | if ($this->site) { |
| 1298: | return $this->database_exists($db) ? $this->site : null; |
| 1299: | } |
| 1300: | |
| 1301: | if (!preg_match(Regex::SQL_DATABASE, $db)) { |
| 1302: | error("Invalid database name"); |
| 1303: | |
| 1304: | return null; |
| 1305: | } |
| 1306: | |
| 1307: | if (1 || $db === $prefix) { |
| 1308: | $path = Opcenter\Database\PostgreSQL::tablespaceLocationFromDatabase($db); |
| 1309: | |
| 1310: | if (!$path || !str_starts_with($path, FILESYSTEM_VIRTBASE)) { |
| 1311: | return null; |
| 1312: | } |
| 1313: | $site = strtok(substr($path, strlen(FILESYSTEM_VIRTBASE)), '/'); |
| 1314: | |
| 1315: | } else { |
| 1316: | $site = Opcenter\Map::read(\Opcenter\Database\PostgreSQL::PREFIX_MAP)[$prefix]; |
| 1317: | } |
| 1318: | |
| 1319: | |
| 1320: | return $site ?: null; |
| 1321: | } |
| 1322: | |
| 1323: | |
| 1324: | |
| 1325: | |
| 1326: | |
| 1327: | |
| 1328: | |
| 1329: | |
| 1330: | |
| 1331: | |
| 1332: | |
| 1333: | public function export_pipe_real($db, $user) |
| 1334: | { |
| 1335: | if (!IS_CLI) { |
| 1336: | return $this->query('pgsql_export_pipe_real', $db, $user); |
| 1337: | } |
| 1338: | |
| 1339: | |
| 1340: | $cmd = '/usr/bin/pg_dump -h 127.0.0.1 -U %s -x --file=%s %s'; |
| 1341: | |
| 1342: | |
| 1343: | $fifo = tempnam($this->domain_fs_path('/tmp'), 'id-' . $this->site); |
| 1344: | unlink($fifo); |
| 1345: | if (!posix_mkfifo($fifo, 0600)) { |
| 1346: | return error('failed to ready pipe for export'); |
| 1347: | } |
| 1348: | chown($fifo, File_Module::UPLOAD_UID); |
| 1349: | $proc = new Util_Process_Fork(); |
| 1350: | $proc->setUser(APNSCP_SYSTEM_USER); |
| 1351: | |
| 1352: | |
| 1353: | $proc->setPriority(19); |
| 1354: | $proc->setEnvironment('PGPASSWORD', self::PG_TEMP_PASSWORD); |
| 1355: | $status = $proc->run($cmd, |
| 1356: | $user, |
| 1357: | $fifo, |
| 1358: | $db |
| 1359: | ); |
| 1360: | |
| 1361: | if (!$status['success'] || !file_exists($fifo)) { |
| 1362: | return error('export failed: %s', $status['stderr']); |
| 1363: | } |
| 1364: | register_shutdown_function(static function () use ($fifo) { |
| 1365: | if (file_exists($fifo)) { |
| 1366: | unlink($fifo); |
| 1367: | } |
| 1368: | |
| 1369: | }); |
| 1370: | |
| 1371: | return $fifo; |
| 1372: | } |
| 1373: | |
| 1374: | |
| 1375: | |
| 1376: | |
| 1377: | |
| 1378: | |
| 1379: | public function get_uptime(): int |
| 1380: | { |
| 1381: | $q = $this->psql->query('SELECT pg_postmaster_start_time() as st')->fetch_object(); |
| 1382: | |
| 1383: | return $q->st; |
| 1384: | } |
| 1385: | |
| 1386: | |
| 1387: | public function add_backup( |
| 1388: | string $db, |
| 1389: | string $extension = DATABASE_BACKUP_EXTENSION, |
| 1390: | int $span = DATABASE_BACKUP_SPAN, |
| 1391: | int $preserve = DATABASE_BACKUP_PRESERVE, |
| 1392: | $email = '' |
| 1393: | ) |
| 1394: | { |
| 1395: | return parent::add_backup_real('pgsql', $db, $extension, $span, $preserve, $email); |
| 1396: | } |
| 1397: | |
| 1398: | public function edit_backup( |
| 1399: | string $db, |
| 1400: | string $extension = DATABASE_BACKUP_EXTENSION, |
| 1401: | int $span = DATABASE_BACKUP_SPAN, |
| 1402: | int $preserve = DATABASE_BACKUP_PRESERVE, |
| 1403: | $email = '' |
| 1404: | ) |
| 1405: | { |
| 1406: | return parent::edit_backup_real('pgsql', $db, $extension, $span, $preserve, $email); |
| 1407: | } |
| 1408: | |
| 1409: | public function list_backups() |
| 1410: | { |
| 1411: | return parent::list_backups_real('pgsql'); |
| 1412: | } |
| 1413: | |
| 1414: | |
| 1415: | |
| 1416: | |
| 1417: | |
| 1418: | |
| 1419: | |
| 1420: | |
| 1421: | |
| 1422: | |
| 1423: | |
| 1424: | |
| 1425: | |
| 1426: | public function get_backup_config($db) |
| 1427: | { |
| 1428: | return parent::get_backup_config_real('pgsql', $db); |
| 1429: | } |
| 1430: | } |