diff --git a/resources/sql/patches/033.privtest.sql b/resources/sql/patches/033.privtest.sql new file mode 100644 index 0000000000..f6ed8e5f7c --- /dev/null +++ b/resources/sql/patches/033.privtest.sql @@ -0,0 +1,2 @@ +# This is a no-op, just testing the upgrade_schema.php script. +SELECT 1; diff --git a/scripts/sql/upgrade_schema.php b/scripts/sql/upgrade_schema.php index 2b2d590639..5de9a5711a 100755 --- a/scripts/sql/upgrade_schema.php +++ b/scripts/sql/upgrade_schema.php @@ -25,14 +25,14 @@ phutil_require_module('phutil', 'console'); define('SCHEMA_VERSION_TABLE_NAME', 'schema_version'); -if (isset($argv[1]) && !is_numeric($argv[1])) { - print - "USAGE: ./update_schema.php [first_patch_version]\n\n". - "run './update_schema.php 12' to apply all patches starting from ". - "version 12.\n". - "run './update_schema.php' to apply all patches that are new since\n". - "the last time this script was run\n\n"; - exit(0); +$options = getopt('v:u:p:') + array( + 'v' => null, + 'u' => null, + 'p' => null, +); + +if ($options['v'] && !is_numeric($options['v'])) { + usage(); } echo phutil_console_wrap( @@ -45,113 +45,141 @@ if (!phutil_console_confirm('Are you ready to continue?')) { } // Use always the version from the commandline if it is defined -$next_version = isset($argv[1]) ? (int)$argv[1] : null; +$next_version = isset($options['v']) ? (int)$options['v'] : null; -// Dummy class needed for creating our database -class DummyUser extends PhabricatorLiskDAO { - public function getApplicationName() { - return 'user'; - } +if ($options['u']) { + $conn_user = $options['u']; + $conn_pass = $options['p']; +} else { + $conn_user = PhabricatorEnv::getEnvConfig('mysql.user'); + $conn_pass = PhabricatorEnv::getEnvConfig('mysql.pass'); } +$conn_host = PhabricatorEnv::getEnvConfig('mysql.host'); -// Class needed for setting up the actual SQL connection -class PhabricatorSchemaVersion extends PhabricatorLiskDAO { - public function getApplicationName() { - return 'meta_data'; - } -} +$conn = new AphrontMySQLDatabaseConnection( + array( + 'user' => $conn_user, + 'pass' => $conn_pass, + 'host' => $conn_host, + 'database' => null, + )); -// Connect to 'phabricator_user' db first to create our db -$conn = id(new DummyUser())->establishConnection('w'); -$create_sql = <<establishConnection('w'); -$create_sql = <<withSuffix('sql'); + $results = $finder->find(); -// Find the patch files -$patches_dir = $root.'/resources/sql/patches/'; -$finder = id(new FileFinder($patches_dir)) - ->withSuffix('sql'); -$results = $finder->find(); - -$patches = array(); -foreach ($results as $r) { - $matches = array(); - if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) { - $patches[] = array('version' => (int)$matches[1], - 'file' => $r); - } else { - print - "*** WARNING : File {$r} does not follow the normal naming ". - "convention. ***\n"; - } -} - -// Files are in some 'random' order returned by the operating system -// We need to apply them in proper order -$patches = isort($patches, 'version'); - -$patch_applied = false; -foreach ($patches as $patch) { - if ($patch['version'] < $next_version) { - continue; + $patches = array(); + foreach ($results as $r) { + $matches = array(); + if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) { + $patches[] = array('version' => (int)$matches[1], + 'file' => $r); + } else { + print + "*** WARNING : File {$r} does not follow the normal naming ". + "convention. ***\n"; + } } - print "Applying patch {$patch['file']}\n"; + // Files are in some 'random' order returned by the operating system + // We need to apply them in proper order + $patches = isort($patches, 'version'); - $path = Filesystem::resolvePath($patches_dir.$patch['file']); + $patch_applied = false; + foreach ($patches as $patch) { + if ($patch['version'] < $next_version) { + continue; + } - $user = PhabricatorEnv::getEnvConfig('mysql.user'); - $pass = PhabricatorEnv::getEnvConfig('mysql.pass'); - $host = PhabricatorEnv::getEnvConfig('mysql.host'); + print "Applying patch {$patch['file']}\n"; - list($stdout, $stderr) = execx( - "mysql --user=%s --password=%s --host=%s < %s", - $user, $pass, $host, $path); + $path = Filesystem::resolvePath($patches_dir.$patch['file']); - if ($stderr) { - print $stderr; - exit(-1); + list($stdout, $stderr) = execx( + "mysql --user=%s --password=%s --host=%s < %s", + $conn_user, + $conn_pass, + $conn_host, + $path); + + if ($stderr) { + print $stderr; + exit(-1); + } + + // Patch was successful, update the db with the latest applied patch version + // 'DELETE' and 'INSERT' instead of update, because the table might be empty + queryfx( + $conn, + 'DELETE FROM phabricator_meta_data.%T', + SCHEMA_VERSION_TABLE_NAME); + queryfx( + $conn, + 'INSERT INTO phabricator_meta_data.%T VALUES (%d)', + SCHEMA_VERSION_TABLE_NAME, + $patch['version']); + + $patch_applied = true; } - // Patch was successful, update the db with the latest applied patch version - // 'DELETE' and 'INSERT' instead of update, because the table might be empty - queryfx($conn, 'DELETE FROM %T', SCHEMA_VERSION_TABLE_NAME); - queryfx( - $conn, - 'INSERT INTO %T values (%d)', - SCHEMA_VERSION_TABLE_NAME, - $patch['version']); + if (!$patch_applied) { + print "Your database is already up-to-date.\n"; + } - $patch_applied = true; +} catch (AphrontQueryAccessDeniedException $ex) { + echo + "ACCESS DENIED\n". + "The user '{$conn_user}' does not have sufficient MySQL privileges to\n". + "execute the schema upgrade. Use the -u and -p flags to run as a user\n". + "with more privileges (e.g., root).". + "\n\n". + "EXCEPTION:\n". + $ex->getMessage(). + "\n\n"; + exit(1); } -if (!$patch_applied) { - print "Your database is already up-to-date.\n"; +function usage() { + echo + "usage: upgrade_schema.php [-v version] [-u user -p pass]". + "\n\n". + "Run 'upgrade_schema.php -v 12' to apply all patches starting from ". + "version 12.\n". + "Run 'upgrade_schema.php -u root -p hunter2' to override the configured ". + "default user.\n"; + exit(1); } + diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5bbb5e1a98..943958f229 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -45,6 +45,7 @@ phutil_register_library_map(array( 'AphrontPageView' => 'view/page/base', 'AphrontPagerView' => 'view/control/pager', 'AphrontPanelView' => 'view/layout/panel', + 'AphrontQueryAccessDeniedException' => 'storage/exception/accessdenied', 'AphrontQueryConnectionException' => 'storage/exception/connection', 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost', 'AphrontQueryCountException' => 'storage/exception/count', @@ -509,6 +510,7 @@ phutil_register_library_map(array( 'AphrontPageView' => 'AphrontView', 'AphrontPagerView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView', + 'AphrontQueryAccessDeniedException' => 'AphrontQueryRecoverableException', 'AphrontQueryConnectionException' => 'AphrontQueryException', 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException', 'AphrontQueryCountException' => 'AphrontQueryException', diff --git a/src/docs/configuration_guide.diviner b/src/docs/configuration_guide.diviner index 8597425ab9..fd247d159a 100644 --- a/src/docs/configuration_guide.diviner +++ b/src/docs/configuration_guide.diviner @@ -119,4 +119,5 @@ change by providing overrides in ##myconfig.conf.php##. = Upgrading Schema = After you have configured Phabricator, you need to upgrade the database -schema, see @{article:Upgrading Schema} +schema, see @{article:Upgrading Schema}. You'll also need to do this after you +update the code in the future. diff --git a/src/docs/upgrade_schema.diviner b/src/docs/upgrade_schema.diviner index 7c3c2c940d..8da7f2c7ab 100644 --- a/src/docs/upgrade_schema.diviner +++ b/src/docs/upgrade_schema.diviner @@ -14,19 +14,25 @@ configured your Phabricator environment. If you haven't, see If you are doing this for the first time to a freshly installed MySQL database, run the following command: - PHABRICATOR_ENV= php path/to/phabricator/scripts/sql/upgrade_schema.php 0 + PHABRICATOR_ENV= path/to/phabricator/scripts/sql/upgrade_schema.php -v 0 This will install all the patches starting from 0. Running this script will store the information of the latest installed patch in the Phabricator database. Next time you want to upgrade your schema, just run: - PHABRICATOR_ENV= php path/to/phabricator/scripts/sql/upgrade_schema.php + PHABRICATOR_ENV= path/to/phabricator/scripts/sql/upgrade_schema.php This will install all the patches that are new since the last time you ran this script. +If your configuration uses an unprivileged user to connect to the database, you +may have to override the default user so the schema changes can be applied with +root or some other admin user: + + PHABRICATOR_ENV= path/to/phabricator/scripts/sql/upgrade_schema.php -u -p + If you need to upgrade the schema starting from a specific patch, just run: - PHABRICATOR_ENV= php path/to/phabricator/scripts/sql/upgrade_schema.php + PHABRICATOR_ENV= path/to/phabricator/scripts/sql/upgrade_schema.php -v However, this isn't usually needed. diff --git a/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php b/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php index 1463f6ef4f..4578909ebb 100644 --- a/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php +++ b/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php @@ -144,9 +144,11 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection { "{$error}."); } - $ret = @mysql_select_db($database, $conn); - if (!$ret) { - $this->throwQueryException($conn); + if ($database !== null) { + $ret = @mysql_select_db($database, $conn); + if (!$ret) { + $this->throwQueryException($conn); + } } $end = microtime(true); @@ -249,6 +251,11 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection { // portable to parse the key out of the error and attach it to the // exception. throw new AphrontQueryDuplicateKeyException("{$errno}: {$error}"); + case 1044: // Access denied to database + case 1045: // Access denied (auth) + case 1142: // Access denied to table + case 1143: // Access denied to column + throw new AphrontQueryAccessDeniedException("#{$errno}: {$error}"); default: // TODO: 1064 is syntax error, and quite terrible in production. throw new AphrontQueryException("#{$errno}: {$error}"); diff --git a/src/storage/connection/mysql/__init__.php b/src/storage/connection/mysql/__init__.php index 05fd2062c7..ae66d4b178 100644 --- a/src/storage/connection/mysql/__init__.php +++ b/src/storage/connection/mysql/__init__.php @@ -8,6 +8,7 @@ phutil_require_module('phabricator', 'aphront/console/plugin/services/api'); phutil_require_module('phabricator', 'storage/connection/base'); +phutil_require_module('phabricator', 'storage/exception/accessdenied'); phutil_require_module('phabricator', 'storage/exception/base'); phutil_require_module('phabricator', 'storage/exception/connection'); phutil_require_module('phabricator', 'storage/exception/connectionlost'); diff --git a/src/storage/exception/accessdenied/AphrontQueryAccessDeniedException.php b/src/storage/exception/accessdenied/AphrontQueryAccessDeniedException.php new file mode 100644 index 0000000000..0ce8cf7d38 --- /dev/null +++ b/src/storage/exception/accessdenied/AphrontQueryAccessDeniedException.php @@ -0,0 +1,23 @@ +