diff options
36 files changed, 1995 insertions, 114 deletions
diff --git a/apps/twofactor_backupcodes/appinfo/database.xml b/apps/twofactor_backupcodes/appinfo/database.xml deleted file mode 100644 index 93b0c1ad563..00000000000 --- a/apps/twofactor_backupcodes/appinfo/database.xml +++ /dev/null @@ -1,48 +0,0 @@ -<?xml version="1.0" encoding="ISO-8859-1" ?> -<database> - <name>*dbname*</name> - <create>true</create> - <overwrite>false</overwrite> - <charset>utf8</charset> - <table> - <name>*dbprefix*twofactor_backupcodes</name> - <declaration> - <field> - <name>id</name> - <type>integer</type> - <autoincrement>1</autoincrement> - <default>0</default> - <notnull>true</notnull> - <length>4</length> - </field> - <field> - <name>user_id</name> - <type>text</type> - <default></default> - <notnull>true</notnull> - <length>64</length> - </field> - <field> - <name>code</name> - <type>text</type> - <notnull>true</notnull> - <length>64</length> - </field> - <field> - <name>used</name> - <type>integer</type> - <notnull>true</notnull> - <default>0</default> - <length>1</length> - </field> - - <index> - <name>twofactor_backupcodes_uid</name> - <field> - <name>user_id</name> - <sorting>ascending</sorting> - </field> - </index> - </declaration> - </table> -</database> diff --git a/apps/twofactor_backupcodes/appinfo/info.xml b/apps/twofactor_backupcodes/appinfo/info.xml index 7faf2825bed..565bd1ebcce 100644 --- a/apps/twofactor_backupcodes/appinfo/info.xml +++ b/apps/twofactor_backupcodes/appinfo/info.xml @@ -5,7 +5,7 @@ <description>A two-factor auth backup codes provider</description> <licence>agpl</licence> <author>Christoph Wurst</author> - <version>1.2.0</version> + <version>1.2.1</version> <namespace>TwoFactorBackupCodes</namespace> <category>other</category> @@ -23,12 +23,6 @@ </providers> </activity> - <repair-steps> - <post-migration> - <step>OCA\TwoFactorBackupCodes\Migration\CopyEntriesFromOldTable</step> - </post-migration> - </repair-steps> - <settings> <personal>OCA\TwoFactorBackupCodes\Settings\Personal</personal> </settings> diff --git a/apps/twofactor_backupcodes/lib/Migration/Version1002Date20170607104347.php b/apps/twofactor_backupcodes/lib/Migration/Version1002Date20170607104347.php new file mode 100644 index 00000000000..a7823c5b7a8 --- /dev/null +++ b/apps/twofactor_backupcodes/lib/Migration/Version1002Date20170607104347.php @@ -0,0 +1,71 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\TwoFactorBackupCodes\Migration; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\Type; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version1002Date20170607104347 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var Schema $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('twofactor_backupcodes')) { + $table = $schema->createTable('twofactor_backupcodes'); + + $table->addColumn('id', Type::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('user_id', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('code', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('used', Type::INTEGER, [ + 'notnull' => true, + 'length' => 1, + 'default' => 0, + ]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['user_id'], 'twofactor_backupcodes_uid'); + } + + return $schema; + } +} diff --git a/apps/twofactor_backupcodes/lib/Migration/CopyEntriesFromOldTable.php b/apps/twofactor_backupcodes/lib/Migration/Version1002Date20170607113030.php index 6d65a7d0c14..dae9e011787 100644 --- a/apps/twofactor_backupcodes/lib/Migration/CopyEntriesFromOldTable.php +++ b/apps/twofactor_backupcodes/lib/Migration/Version1002Date20170607113030.php @@ -23,54 +23,35 @@ namespace OCA\TwoFactorBackupCodes\Migration; +use Doctrine\DBAL\Schema\Schema; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IConfig; use OCP\IDBConnection; +use OCP\Migration\SimpleMigrationStep; use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; -class CopyEntriesFromOldTable implements IRepairStep { +class Version1002Date20170607113030 extends SimpleMigrationStep { /** @var IDBConnection */ protected $connection; - /** @var IConfig */ - protected $config; - /** * @param IDBConnection $connection - * @param IConfig $config */ - public function __construct(IDBConnection $connection, IConfig $config) { + public function __construct(IDBConnection $connection) { $this->connection = $connection; - $this->config = $config; } /** - * Returns the step's name - * - * @return string - * @since 9.1.0 - */ - public function getName() { - return 'Copy twofactor backup codes from legacy table'; - } - - /** - * Run repair step. - * Must throw exception on error. - * - * @since 9.1.0 * @param IOutput $output - * @throws \Exception in case of failure + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 */ - public function run(IOutput $output) { - $version = $this->config->getAppValue('twofactor_backupcodes', 'installed_version', '0.0.0'); - if (version_compare($version, '1.1.1', '>=')) { - return; - } + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var Schema $schema */ + $schema = $schemaClosure(); - if (!$this->connection->tableExists('twofactor_backup_codes')) { + if (!$schema->hasTable('twofactor_backup_codes')) { // Legacy table does not exist return; } @@ -102,7 +83,23 @@ class CopyEntriesFromOldTable implements IRepairStep { ->execute(); } $output->finishProgress(); + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var Schema $schema */ + $schema = $schemaClosure(); - $this->connection->dropTable('twofactor_backup_codes'); + if ($schema->hasTable('twofactor_backup_codes')) { + $schema->dropTable('twofactor_backup_codes'); + return $schema; + } + return null; } } diff --git a/core/Command/App/CheckCode.php b/core/Command/App/CheckCode.php index 48662409dcf..a7ef9024326 100644 --- a/core/Command/App/CheckCode.php +++ b/core/Command/App/CheckCode.php @@ -97,7 +97,7 @@ class CheckCode extends Command implements CompletionAwareInterface { $checkList = new $checkerClass($checkList); } - $codeChecker = new CodeChecker($checkList); + $codeChecker = new CodeChecker($checkList, !$input->getOption('skip-validate-info')); $codeChecker->listen('CodeChecker', 'analyseFileBegin', function($params) use ($output) { if(OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { diff --git a/core/Command/Db/Migrations/ExecuteCommand.php b/core/Command/Db/Migrations/ExecuteCommand.php new file mode 100644 index 00000000000..0f21bdf28eb --- /dev/null +++ b/core/Command/Db/Migrations/ExecuteCommand.php @@ -0,0 +1,92 @@ +<?php +/** + * @author Joas Schilling <coding@schilljs.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * @copyright Copyright (c) 2017, ownCloud GmbH + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Command\Db\Migrations; + + +use OC\DB\MigrationService; +use OC\Migration\ConsoleOutput; +use OCP\IConfig; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ExecuteCommand extends Command { + + /** @var IDBConnection */ + private $connection; + /** @var IConfig */ + private $config; + + /** + * ExecuteCommand constructor. + * + * @param IDBConnection $connection + * @param IConfig $config + */ + public function __construct(IDBConnection $connection, IConfig $config) { + $this->connection = $connection; + $this->config = $config; + + parent::__construct(); + } + + protected function configure() { + $this + ->setName('migrations:execute') + ->setDescription('Execute a single migration version manually.') + ->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on') + ->addArgument('version', InputArgument::REQUIRED, 'The version to execute.', null); + + parent::configure(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + public function execute(InputInterface $input, OutputInterface $output) { + $appName = $input->getArgument('app'); + $ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); + $version = $input->getArgument('version'); + + if ($this->config->getSystemValue('debug', false) === false) { + $olderVersions = $ms->getMigratedVersions(); + $olderVersions[] = '0'; + $olderVersions[] = 'prev'; + if (in_array($version, $olderVersions, true)) { + $output->writeln('<error>Can not go back to previous migration without debug enabled</error>'); + return 1; + } + } + + + $ms->executeStep($version); + return 0; + } + +} diff --git a/core/Command/Db/Migrations/GenerateCommand.php b/core/Command/Db/Migrations/GenerateCommand.php new file mode 100644 index 00000000000..b6e1a17d683 --- /dev/null +++ b/core/Command/Db/Migrations/GenerateCommand.php @@ -0,0 +1,148 @@ +<?php +/** + * @author Joas Schilling <coding@schilljs.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * @copyright Copyright (c) 2017, ownCloud GmbH + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Command\Db\Migrations; + + +use OC\DB\MigrationService; +use OC\Migration\ConsoleOutput; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class GenerateCommand extends Command { + + private static $_templateSimple = + '<?php +namespace <namespace>; + +use Doctrine\DBAL\Schema\Schema; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class <classname> extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + return null; + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + } +} +'; + + /** @var IDBConnection */ + private $connection; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + + parent::__construct(); + } + + protected function configure() { + $this + ->setName('migrations:generate') + ->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on') + ->addArgument('version', InputArgument::REQUIRED, 'Major version of this app, to allow versions on parallel development branches') + ; + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) { + $appName = $input->getArgument('app'); + $version = $input->getArgument('version'); + + if (!preg_match('/^\d{1,16}$/',$version)) { + $output->writeln('<error>The given version is invalid. Only 0-9 are allowed (max. 16 digits)</error>'); + return 1; + } + + $ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); + + $date = date('YmdHis'); + $path = $this->generateMigration($ms, 'Version' . $version . 'Date' . $date); + + $output->writeln("New migration class has been generated to <info>$path</info>"); + return 0; + } + + /** + * @param MigrationService $ms + * @param string $className + * @return string + */ + private function generateMigration(MigrationService $ms, $className) { + $placeHolders = [ + '<namespace>', + '<classname>', + ]; + $replacements = [ + $ms->getMigrationsNamespace(), + $className, + ]; + $code = str_replace($placeHolders, $replacements, self::$_templateSimple); + $dir = $ms->getMigrationsDirectory(); + $path = $dir . '/' . $className . '.php'; + + if (file_put_contents($path, $code) === false) { + throw new RuntimeException('Failed to generate new migration step.'); + } + + return $path; + } + +} diff --git a/core/Command/Db/Migrations/MigrateCommand.php b/core/Command/Db/Migrations/MigrateCommand.php new file mode 100644 index 00000000000..2b0e082acaa --- /dev/null +++ b/core/Command/Db/Migrations/MigrateCommand.php @@ -0,0 +1,64 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Command\Db\Migrations; + + +use OC\DB\MigrationService; +use OC\Migration\ConsoleOutput; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class MigrateCommand extends Command { + + /** @var IDBConnection */ + private $connection; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('migrations:migrate') + ->setDescription('Execute a migration to a specified version or the latest available version.') + ->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on') + ->addArgument('version', InputArgument::OPTIONAL, 'The version number (YYYYMMDDHHMMSS) or alias (first, prev, next, latest) to migrate to.', 'latest'); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) { + $appName = $input->getArgument('app'); + $ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); + $version = $input->getArgument('version'); + + $ms->migrate($version); + } + +} diff --git a/core/Command/Db/Migrations/StatusCommand.php b/core/Command/Db/Migrations/StatusCommand.php new file mode 100644 index 00000000000..20172000ee3 --- /dev/null +++ b/core/Command/Db/Migrations/StatusCommand.php @@ -0,0 +1,115 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Command\Db\Migrations; + +use OC\DB\MigrationService; +use OC\Migration\ConsoleOutput; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class StatusCommand extends Command { + + /** @var IDBConnection */ + private $connection; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('migrations:status') + ->setDescription('View the status of a set of migrations.') + ->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on'); + } + + public function execute(InputInterface $input, OutputInterface $output) { + $appName = $input->getArgument('app'); + $ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); + + $infos = $this->getMigrationsInfos($ms); + foreach ($infos as $key => $value) { + $output->writeln(" <comment>>></comment> $key: " . str_repeat(' ', 50 - strlen($key)) . $value); + } + } + + /** + * @param MigrationService $ms + * @return array associative array of human readable info name as key and the actual information as value + */ + public function getMigrationsInfos(MigrationService $ms) { + + $executedMigrations = $ms->getMigratedVersions(); + $availableMigrations = $ms->getAvailableVersions(); + $executedUnavailableMigrations = array_diff($executedMigrations, array_keys($availableMigrations)); + + $numExecutedUnavailableMigrations = count($executedUnavailableMigrations); + $numNewMigrations = count(array_diff(array_keys($availableMigrations), $executedMigrations)); + + $infos = [ + 'App' => $ms->getApp(), + 'Version Table Name' => $ms->getMigrationsTableName(), + 'Migrations Namespace' => $ms->getMigrationsNamespace(), + 'Migrations Directory' => $ms->getMigrationsDirectory(), + 'Previous Version' => $this->getFormattedVersionAlias($ms, 'prev'), + 'Current Version' => $this->getFormattedVersionAlias($ms, 'current'), + 'Next Version' => $this->getFormattedVersionAlias($ms, 'next'), + 'Latest Version' => $this->getFormattedVersionAlias($ms, 'latest'), + 'Executed Migrations' => count($executedMigrations), + 'Executed Unavailable Migrations' => $numExecutedUnavailableMigrations, + 'Available Migrations' => count($availableMigrations), + 'New Migrations' => $numNewMigrations, + ]; + + return $infos; + } + + /** + * @param MigrationService $migrationService + * @param string $alias + * @return mixed|null|string + */ + private function getFormattedVersionAlias(MigrationService $migrationService, $alias) { + $migration = $migrationService->getMigration($alias); + //No version found + if ($migration === null) { + if ($alias === 'next') { + return 'Already at latest migration step'; + } + + if ($alias === 'prev') { + return 'Already at first migration step'; + } + } + + return $migration; + } + + +} diff --git a/core/Migrations/Version13000Date20170705121758.php b/core/Migrations/Version13000Date20170705121758.php new file mode 100644 index 00000000000..6f9c2d243f8 --- /dev/null +++ b/core/Migrations/Version13000Date20170705121758.php @@ -0,0 +1,93 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Core\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\Type; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version13000Date20170705121758 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var Schema $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('personal_sections')) { + $table = $schema->createTable('personal_sections'); + + $table->addColumn('id', Type::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('class', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('priority', Type::INTEGER, [ + 'notnull' => true, + 'length' => 6, + 'default' => 0, + ]); + + $table->setPrimaryKey(['id'], 'personal_sections_id_index'); + $table->addUniqueIndex(['class'], 'personal_sections_class'); + } + + if (!$schema->hasTable('personal_settings')) { + $table = $schema->createTable('personal_settings'); + + $table->addColumn('id', Type::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('class', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('section', Type::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('priority', Type::INTEGER, [ + 'notnull' => true, + 'length' => 6, + 'default' => 0, + ]); + + $table->setPrimaryKey(['id'], 'personal_settings_id_index'); + $table->addUniqueIndex(['class'], 'personal_settings_class'); + $table->addIndex(['section'], 'personal_settings_section'); + } + + return $schema; + } +} diff --git a/core/register_command.php b/core/register_command.php index 59fc65edbc8..bfb1138c5e3 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -85,6 +85,10 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\Db\GenerateChangeScript()); $application->add(new OC\Core\Command\Db\ConvertType(\OC::$server->getConfig(), new \OC\DB\ConnectionFactory(\OC::$server->getSystemConfig()))); $application->add(new OC\Core\Command\Db\ConvertMysqlToMB4(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection(), \OC::$server->getURLGenerator(), \OC::$server->getLogger())); + $application->add(new OC\Core\Command\Db\Migrations\StatusCommand(\OC::$server->getDatabaseConnection())); + $application->add(new OC\Core\Command\Db\Migrations\MigrateCommand(\OC::$server->getDatabaseConnection())); + $application->add(new OC\Core\Command\Db\Migrations\GenerateCommand(\OC::$server->getDatabaseConnection())); + $application->add(new OC\Core\Command\Db\Migrations\ExecuteCommand(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig())); $application->add(new OC\Core\Command\Encryption\Disable(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Encryption\Enable(\OC::$server->getConfig(), \OC::$server->getEncryptionManager())); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 43ed4db9307..ee1dc08021c 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -220,8 +220,10 @@ return array( 'OCP\\Lockdown\\ILockdownManager' => $baseDir . '/lib/public/Lockdown/ILockdownManager.php', 'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php', + 'OCP\\Migration\\IMigrationStep' => $baseDir . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => $baseDir . '/lib/public/Migration/IOutput.php', 'OCP\\Migration\\IRepairStep' => $baseDir . '/lib/public/Migration/IRepairStep.php', + 'OCP\\Migration\\SimpleMigrationStep' => $baseDir . '/lib/public/Migration/SimpleMigrationStep.php', 'OCP\\Notification\\IAction' => $baseDir . '/lib/public/Notification/IAction.php', 'OCP\\Notification\\IApp' => $baseDir . '/lib/public/Notification/IApp.php', 'OCP\\Notification\\IManager' => $baseDir . '/lib/public/Notification/IManager.php', @@ -339,6 +341,7 @@ return array( 'OC\\App\\CodeChecker\\ICheck' => $baseDir . '/lib/private/App/CodeChecker/ICheck.php', 'OC\\App\\CodeChecker\\InfoChecker' => $baseDir . '/lib/private/App/CodeChecker/InfoChecker.php', 'OC\\App\\CodeChecker\\LanguageParseChecker' => $baseDir . '/lib/private/App/CodeChecker/LanguageParseChecker.php', + 'OC\\App\\CodeChecker\\MigrationSchemaChecker' => $baseDir . '/lib/private/App/CodeChecker/MigrationSchemaChecker.php', 'OC\\App\\CodeChecker\\NodeVisitor' => $baseDir . '/lib/private/App/CodeChecker/NodeVisitor.php', 'OC\\App\\CodeChecker\\PrivateCheck' => $baseDir . '/lib/private/App/CodeChecker/PrivateCheck.php', 'OC\\App\\CodeChecker\\StrongComparisonCheck' => $baseDir . '/lib/private/App/CodeChecker/StrongComparisonCheck.php', @@ -420,6 +423,10 @@ return array( 'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => $baseDir . '/core/Command/Db/ConvertMysqlToMB4.php', 'OC\\Core\\Command\\Db\\ConvertType' => $baseDir . '/core/Command/Db/ConvertType.php', 'OC\\Core\\Command\\Db\\GenerateChangeScript' => $baseDir . '/core/Command/Db/GenerateChangeScript.php', + 'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => $baseDir . '/core/Command/Db/Migrations/ExecuteCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => $baseDir . '/core/Command/Db/Migrations/GenerateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => $baseDir . '/core/Command/Db/Migrations/MigrateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => $baseDir . '/core/Command/Db/Migrations/StatusCommand.php', 'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => $baseDir . '/core/Command/Encryption/ChangeKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\DecryptAll' => $baseDir . '/core/Command/Encryption/DecryptAll.php', 'OC\\Core\\Command\\Encryption\\Disable' => $baseDir . '/core/Command/Encryption/Disable.php', @@ -480,6 +487,7 @@ return array( 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => $baseDir . '/core/Middleware/TwoFactorMiddleware.php', + 'OC\\Core\\Migrations\\Version13000Date20170705121758' => $baseDir . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', @@ -491,6 +499,7 @@ return array( 'OC\\DB\\MDB2SchemaReader' => $baseDir . '/lib/private/DB/MDB2SchemaReader.php', 'OC\\DB\\MDB2SchemaWriter' => $baseDir . '/lib/private/DB/MDB2SchemaWriter.php', 'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php', + 'OC\\DB\\MigrationService' => $baseDir . '/lib/private/DB/MigrationService.php', 'OC\\DB\\Migrator' => $baseDir . '/lib/private/DB/Migrator.php', 'OC\\DB\\MySQLMigrator' => $baseDir . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => $baseDir . '/lib/private/DB/MySqlTools.php', @@ -517,6 +526,7 @@ return array( 'OC\\DB\\QueryBuilder\\QuoteHelper' => $baseDir . '/lib/private/DB/QueryBuilder/QuoteHelper.php', 'OC\\DB\\SQLiteMigrator' => $baseDir . '/lib/private/DB/SQLiteMigrator.php', 'OC\\DB\\SQLiteSessionInit' => $baseDir . '/lib/private/DB/SQLiteSessionInit.php', + 'OC\\DB\\SchemaWrapper' => $baseDir . '/lib/private/DB/SchemaWrapper.php', 'OC\\DatabaseException' => $baseDir . '/lib/private/DatabaseException.php', 'OC\\DatabaseSetupException' => $baseDir . '/lib/private/DatabaseSetupException.php', 'OC\\DateTimeFormatter' => $baseDir . '/lib/private/DateTimeFormatter.php', @@ -682,6 +692,7 @@ return array( 'OC\\Memcache\\XCache' => $baseDir . '/lib/private/Memcache/XCache.php', 'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => $baseDir . '/lib/private/Migration/ConsoleOutput.php', + 'OC\\Migration\\SimpleOutput' => $baseDir . '/lib/private/Migration/SimpleOutput.php', 'OC\\NaturalSort' => $baseDir . '/lib/private/NaturalSort.php', 'OC\\NaturalSort_DefaultCollator' => $baseDir . '/lib/private/NaturalSort_DefaultCollator.php', 'OC\\NavigationManager' => $baseDir . '/lib/private/NavigationManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 00b7908f848..7b133d54a20 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -250,8 +250,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Lockdown\\ILockdownManager' => __DIR__ . '/../../..' . '/lib/public/Lockdown/ILockdownManager.php', 'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php', + 'OCP\\Migration\\IMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Migration/IOutput.php', 'OCP\\Migration\\IRepairStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IRepairStep.php', + 'OCP\\Migration\\SimpleMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/SimpleMigrationStep.php', 'OCP\\Notification\\IAction' => __DIR__ . '/../../..' . '/lib/public/Notification/IAction.php', 'OCP\\Notification\\IApp' => __DIR__ . '/../../..' . '/lib/public/Notification/IApp.php', 'OCP\\Notification\\IManager' => __DIR__ . '/../../..' . '/lib/public/Notification/IManager.php', @@ -369,6 +371,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\App\\CodeChecker\\ICheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/ICheck.php', 'OC\\App\\CodeChecker\\InfoChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/InfoChecker.php', 'OC\\App\\CodeChecker\\LanguageParseChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/LanguageParseChecker.php', + 'OC\\App\\CodeChecker\\MigrationSchemaChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/MigrationSchemaChecker.php', 'OC\\App\\CodeChecker\\NodeVisitor' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/NodeVisitor.php', 'OC\\App\\CodeChecker\\PrivateCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/PrivateCheck.php', 'OC\\App\\CodeChecker\\StrongComparisonCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/StrongComparisonCheck.php', @@ -450,6 +453,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertMysqlToMB4.php', 'OC\\Core\\Command\\Db\\ConvertType' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertType.php', 'OC\\Core\\Command\\Db\\GenerateChangeScript' => __DIR__ . '/../../..' . '/core/Command/Db/GenerateChangeScript.php', + 'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/ExecuteCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/GenerateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/MigrateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/StatusCommand.php', 'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => __DIR__ . '/../../..' . '/core/Command/Encryption/ChangeKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/core/Command/Encryption/DecryptAll.php', 'OC\\Core\\Command\\Encryption\\Disable' => __DIR__ . '/../../..' . '/core/Command/Encryption/Disable.php', @@ -510,6 +517,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => __DIR__ . '/../../..' . '/core/Middleware/TwoFactorMiddleware.php', + 'OC\\Core\\Migrations\\Version13000Date20170705121758' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', @@ -521,6 +529,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\MDB2SchemaReader' => __DIR__ . '/../../..' . '/lib/private/DB/MDB2SchemaReader.php', 'OC\\DB\\MDB2SchemaWriter' => __DIR__ . '/../../..' . '/lib/private/DB/MDB2SchemaWriter.php', 'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php', + 'OC\\DB\\MigrationService' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationService.php', 'OC\\DB\\Migrator' => __DIR__ . '/../../..' . '/lib/private/DB/Migrator.php', 'OC\\DB\\MySQLMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/MySqlTools.php', @@ -547,6 +556,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\QueryBuilder\\QuoteHelper' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/QuoteHelper.php', 'OC\\DB\\SQLiteMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteMigrator.php', 'OC\\DB\\SQLiteSessionInit' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteSessionInit.php', + 'OC\\DB\\SchemaWrapper' => __DIR__ . '/../../..' . '/lib/private/DB/SchemaWrapper.php', 'OC\\DatabaseException' => __DIR__ . '/../../..' . '/lib/private/DatabaseException.php', 'OC\\DatabaseSetupException' => __DIR__ . '/../../..' . '/lib/private/DatabaseSetupException.php', 'OC\\DateTimeFormatter' => __DIR__ . '/../../..' . '/lib/private/DateTimeFormatter.php', @@ -712,6 +722,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Memcache\\XCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/XCache.php', 'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/ConsoleOutput.php', + 'OC\\Migration\\SimpleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/SimpleOutput.php', 'OC\\NaturalSort' => __DIR__ . '/../../..' . '/lib/private/NaturalSort.php', 'OC\\NaturalSort_DefaultCollator' => __DIR__ . '/../../..' . '/lib/private/NaturalSort_DefaultCollator.php', 'OC\\NavigationManager' => __DIR__ . '/../../..' . '/lib/private/NavigationManager.php', diff --git a/lib/private/App/CodeChecker/CodeChecker.php b/lib/private/App/CodeChecker/CodeChecker.php index 291bedee92f..a6368ab683f 100644 --- a/lib/private/App/CodeChecker/CodeChecker.php +++ b/lib/private/App/CodeChecker/CodeChecker.php @@ -51,8 +51,12 @@ class CodeChecker extends BasicEmitter { /** @var ICheck */ protected $checkList; - public function __construct(ICheck $checkList) { + /** @var bool */ + protected $checkMigrationSchema; + + public function __construct(ICheck $checkList, $checkMigrationSchema) { $this->checkList = $checkList; + $this->checkMigrationSchema = $checkMigrationSchema; $this->parser = new Parser(new Lexer); } @@ -120,11 +124,16 @@ class CodeChecker extends BasicEmitter { $statements = $this->parser->parse($code); $visitor = new NodeVisitor($this->checkList); + $migrationVisitor = new MigrationSchemaChecker(); $traverser = new NodeTraverser; $traverser->addVisitor($visitor); + if ($this->checkMigrationSchema && preg_match('#^.+\\/Migration\\/Version[^\\/]{1,255}\\.php$#i', $file)) { + $traverser->addVisitor($migrationVisitor); + } + $traverser->traverse($statements); - return $visitor->errors; + return array_merge($visitor->errors, $migrationVisitor->errors); } } diff --git a/lib/private/App/CodeChecker/MigrationSchemaChecker.php b/lib/private/App/CodeChecker/MigrationSchemaChecker.php new file mode 100644 index 00000000000..9dee358327d --- /dev/null +++ b/lib/private/App/CodeChecker/MigrationSchemaChecker.php @@ -0,0 +1,201 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\CodeChecker; + +use PhpParser\Node; +use PhpParser\Node\Name; +use PhpParser\NodeVisitorAbstract; + +class MigrationSchemaChecker extends NodeVisitorAbstract { + + /** @var string */ + protected $schemaVariableName = null; + /** @var array */ + protected $tableVariableNames = []; + /** @var array */ + public $errors = []; + + public function enterNode(Node $node) { + /** + * Check tables + */ + if ($this->schemaVariableName !== null && + $node instanceof Node\Expr\Assign && + $node->var instanceof Node\Expr\Variable && + $node->expr instanceof Node\Expr\MethodCall && + $node->expr->var instanceof Node\Expr\Variable && + $node->expr->var->name === $this->schemaVariableName) { + + if ($node->expr->name === 'createTable') { + if (isset($node->expr->args[0]) && $node->expr->args[0]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->expr->args[0]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->expr->args[0]->value->value, + 'reason' => 'Table name is too long (max. 27)', + ]; + } else { + $this->tableVariableNames[$node->var->name] = $node->expr->args[0]->value->value; + } + } + } else if ($node->expr->name === 'getTable') { + if (isset($node->expr->args[0]) && $node->expr->args[0]->value instanceof Node\Scalar\String_) { + $this->tableVariableNames[$node->var->name] = $node->expr->args[0]->value->value; + } + } + } else if ($this->schemaVariableName !== null && + $node instanceof Node\Expr\MethodCall && + $node->var instanceof Node\Expr\Variable && + $node->var->name === $this->schemaVariableName) { + + if ($node->name === 'renameTable') { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => 'Deprecated method', + 'reason' => sprintf( + '`$%s->renameTable()` must not be used', + $node->var->name + ), + ]; + } + + /** + * Check columns and Indexes + */ + } else if (!empty($this->tableVariableNames) && + $node instanceof Node\Expr\MethodCall && + $node->var instanceof Node\Expr\Variable && + isset($this->tableVariableNames[$node->var->name])) { + + if ($node->name === 'addColumn' || $node->name === 'changeColumn') { + if (isset($node->args[0]) && $node->args[0]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->args[0]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->args[0]->value->value, + 'reason' => sprintf( + 'Column name is too long on table `%s` (max. 27)', + $this->tableVariableNames[$node->var->name] + ), + ]; + } + + // On autoincrement the max length of the table name is 21 instead of 27 + if (isset($node->args[2]) && $node->args[2]->value instanceof Node\Expr\Array_) { + /** @var Node\Expr\Array_ $options */ + $options = $node->args[2]->value; + if ($this->checkColumnForAutoincrement($options)) { + if (!$this->checkNameLength($this->tableVariableNames[$node->var->name], true)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $this->tableVariableNames[$node->var->name], + 'reason' => 'Table name is too long because of autoincrement (max. 21)', + ]; + } + } + } + } + } else if ($node->name === 'addIndex' || + $node->name === 'addUniqueIndex' || + $node->name === 'renameIndex' || + $node->name === 'setPrimaryKey') { + if (isset($node->args[1]) && $node->args[1]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->args[1]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->args[1]->value->value, + 'reason' => sprintf( + 'Index name is too long on table `%s` (max. 27)', + $this->tableVariableNames[$node->var->name] + ), + ]; + } + } + } else if ($node->name === 'addForeignKeyConstraint') { + if (isset($node->args[4]) && $node->args[4]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->args[4]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->args[4]->value->value, + 'reason' => sprintf( + 'Constraint name is too long on table `%s` (max. 27)', + $this->tableVariableNames[$node->var->name] + ), + ]; + } + } + } else if ($node->name === 'renameColumn') { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => 'Deprecated method', + 'reason' => sprintf( + '`$%s->renameColumn()` must not be used', + $node->var->name + ), + ]; + } + + /** + * Find the schema + */ + } else if ($node instanceof Node\Expr\Assign && + $node->expr instanceof Node\Expr\FuncCall && + $node->var instanceof Node\Expr\Variable && + $node->expr->name instanceof Node\Expr\Variable && + $node->expr->name->name === 'schemaClosure') { + // E.g. $schema = $schemaClosure(); + $this->schemaVariableName = $node->var->name; + } + } + + protected function checkNameLength($tableName, $hasAutoincrement = false) { + if ($hasAutoincrement) { + return strlen($tableName) <= 21; + } + return strlen($tableName) <= 27; + } + + /** + * @param Node\Expr\Array_ $optionsArray + * @return bool Whether the column is an autoincrement column + */ + protected function checkColumnForAutoincrement(Node\Expr\Array_ $optionsArray) { + foreach ($optionsArray->items as $option) { + if ($option->key instanceof Node\Scalar\String_) { + if ($option->key->value === 'autoincrement' && + $option->value instanceof Node\Expr\ConstFetch) { + /** @var Node\Expr\ConstFetch $const */ + $const = $option->value; + + if ($const->name instanceof Name && + $const->name->parts === ['true']) { + return true; + } + } + } + } + + return false; + } +} diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 6b56ae8ad5c..563c077b04a 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -35,6 +35,7 @@ use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\Common\EventManager; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Exception\ConstraintViolationException; +use Doctrine\DBAL\Schema\Schema; use OC\DB\QueryBuilder\QueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -418,4 +419,27 @@ class Connection extends \Doctrine\DBAL\Connection implements IDBConnection { } return $this->getParams()['charset'] === 'utf8mb4'; } + + + /** + * Create the schema of the connected database + * + * @return Schema + */ + public function createSchema() { + $schemaManager = new MDB2SchemaManager($this); + $migrator = $schemaManager->getMigrator(); + return $migrator->createSchema(); + } + + /** + * Migrate the database to the given schema + * + * @param Schema $toSchema + */ + public function migrateToSchema(Schema $toSchema) { + $schemaManager = new MDB2SchemaManager($this); + $migrator = $schemaManager->getMigrator(); + $migrator->migrate($toSchema); + } } diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php new file mode 100644 index 00000000000..a4276b08c12 --- /dev/null +++ b/lib/private/DB/MigrationService.php @@ -0,0 +1,422 @@ +<?php +/** + * @author Joas Schilling <coding@schilljs.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * @copyright Copyright (c) 2017, ownCloud GmbH + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; +use OC\IntegrityCheck\Helpers\AppLocator; +use OC\Migration\SimpleOutput; +use OCP\AppFramework\App; +use OCP\AppFramework\QueryException; +use OCP\IDBConnection; +use OCP\Migration\IMigrationStep; +use OCP\Migration\IOutput; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\Type; + +class MigrationService { + + /** @var boolean */ + private $migrationTableCreated; + /** @var array */ + private $migrations; + /** @var IOutput */ + private $output; + /** @var Connection */ + private $connection; + /** @var string */ + private $appName; + + /** + * MigrationService constructor. + * + * @param $appName + * @param IDBConnection $connection + * @param AppLocator $appLocator + * @param IOutput|null $output + * @throws \Exception + */ + public function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) { + $this->appName = $appName; + $this->connection = $connection; + $this->output = $output; + if (null === $this->output) { + $this->output = new SimpleOutput(\OC::$server->getLogger(), $appName); + } + + if ($appName === 'core') { + $this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations'; + $this->migrationsNamespace = 'OC\\Core\\Migrations'; + } else { + if (null === $appLocator) { + $appLocator = new AppLocator(); + } + $appPath = $appLocator->getAppPath($appName); + $namespace = App::buildAppNamespace($appName); + $this->migrationsPath = "$appPath/lib/Migration"; + $this->migrationsNamespace = $namespace . '\\Migration'; + + if (!@mkdir($appPath . '/lib') && !is_dir($appPath . '/lib')) { + throw new \RuntimeException("Could not create migration folder \"{$this->migrationsPath}\""); + } + } + + if (!@mkdir($this->migrationsPath) && !is_dir($this->migrationsPath)) { + throw new \RuntimeException("Could not create migration folder \"{$this->migrationsPath}\""); + } + } + + /** + * Returns the name of the app for which this migration is executed + * + * @return string + */ + public function getApp() { + return $this->appName; + } + + /** + * @return bool + * @codeCoverageIgnore - this will implicitly tested on installation + */ + private function createMigrationTable() { + if ($this->migrationTableCreated) { + return false; + } + + if ($this->connection->tableExists('migrations')) { + $this->migrationTableCreated = true; + return false; + } + + $tableName = $this->connection->getPrefix() . 'migrations'; + $tableName = $this->connection->getDatabasePlatform()->quoteIdentifier($tableName); + + $columns = [ + 'app' => new Column($this->connection->getDatabasePlatform()->quoteIdentifier('app'), Type::getType('string'), ['length' => 255]), + 'version' => new Column($this->connection->getDatabasePlatform()->quoteIdentifier('version'), Type::getType('string'), ['length' => 255]), + ]; + $table = new Table($tableName, $columns); + $table->setPrimaryKey([ + $this->connection->getDatabasePlatform()->quoteIdentifier('app'), + $this->connection->getDatabasePlatform()->quoteIdentifier('version')]); + $this->connection->getSchemaManager()->createTable($table); + + $this->migrationTableCreated = true; + + return true; + } + + /** + * Returns all versions which have already been applied + * + * @return string[] + * @codeCoverageIgnore - no need to test this + */ + public function getMigratedVersions() { + $this->createMigrationTable(); + $qb = $this->connection->getQueryBuilder(); + + $qb->select('version') + ->from('migrations') + ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp()))) + ->orderBy('version'); + + $result = $qb->execute(); + $rows = $result->fetchAll(\PDO::FETCH_COLUMN); + $result->closeCursor(); + + return $rows; + } + + /** + * Returns all versions which are available in the migration folder + * + * @return array + */ + public function getAvailableVersions() { + $this->ensureMigrationsAreLoaded(); + return array_map('strval', array_keys($this->migrations)); + } + + protected function findMigrations() { + $directory = realpath($this->migrationsPath); + $iterator = new \RegexIterator( + new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ), + '#^.+\\/Version[^\\/]{1,255}\\.php$#i', + \RegexIterator::GET_MATCH); + + $files = array_keys(iterator_to_array($iterator)); + uasort($files, function ($a, $b) { + preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA); + preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB); + if (!empty($matchA) && !empty($matchB)) { + if ($matchA[1] !== $matchB[1]) { + return ($matchA[1] < $matchB[1]) ? -1 : 1; + } + return ($matchA[2] < $matchB[2]) ? -1 : 1; + } + return (basename($a) < basename($b)) ? -1 : 1; + }); + + $migrations = []; + + foreach ($files as $file) { + $className = basename($file, '.php'); + $version = (string) substr($className, 7); + if ($version === '0') { + throw new \InvalidArgumentException( + "Cannot load a migrations with the name '$version' because it is a reserved number" + ); + } + $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className); + } + + return $migrations; + } + + /** + * @param string $to + * @return string[] + */ + private function getMigrationsToExecute($to) { + $knownMigrations = $this->getMigratedVersions(); + $availableMigrations = $this->getAvailableVersions(); + + $toBeExecuted = []; + foreach ($availableMigrations as $v) { + if ($to !== 'latest' && $v > $to) { + continue; + } + if ($this->shallBeExecuted($v, $knownMigrations)) { + $toBeExecuted[] = $v; + } + } + + return $toBeExecuted; + } + + /** + * @param string $m + * @param string[] $knownMigrations + * @return bool + */ + private function shallBeExecuted($m, $knownMigrations) { + if (in_array($m, $knownMigrations)) { + return false; + } + + return true; + } + + /** + * @param string $version + */ + private function markAsExecuted($version) { + $this->connection->insertIfNotExist('*PREFIX*migrations', [ + 'app' => $this->appName, + 'version' => $version + ]); + } + + /** + * Returns the name of the table which holds the already applied versions + * + * @return string + */ + public function getMigrationsTableName() { + return $this->connection->getPrefix() . 'migrations'; + } + + /** + * Returns the namespace of the version classes + * + * @return string + */ + public function getMigrationsNamespace() { + return $this->migrationsNamespace; + } + + /** + * Returns the directory which holds the versions + * + * @return string + */ + public function getMigrationsDirectory() { + return $this->migrationsPath; + } + + /** + * Return the explicit version for the aliases; current, next, prev, latest + * + * @param string $alias + * @return mixed|null|string + */ + public function getMigration($alias) { + switch($alias) { + case 'current': + return $this->getCurrentVersion(); + case 'next': + return $this->getRelativeVersion($this->getCurrentVersion(), 1); + case 'prev': + return $this->getRelativeVersion($this->getCurrentVersion(), -1); + case 'latest': + $this->ensureMigrationsAreLoaded(); + + return @end($this->getAvailableVersions()); + } + return '0'; + } + + /** + * @param string $version + * @param int $delta + * @return null|string + */ + private function getRelativeVersion($version, $delta) { + $this->ensureMigrationsAreLoaded(); + + $versions = $this->getAvailableVersions(); + array_unshift($versions, 0); + $offset = array_search($version, $versions, true); + if ($offset === false || !isset($versions[$offset + $delta])) { + // Unknown version or delta out of bounds. + return null; + } + + return (string) $versions[$offset + $delta]; + } + + /** + * @return string + */ + private function getCurrentVersion() { + $m = $this->getMigratedVersions(); + if (count($m) === 0) { + return '0'; + } + return @end(array_values($m)); + } + + /** + * @param string $version + * @return string + * @throws \InvalidArgumentException + */ + private function getClass($version) { + $this->ensureMigrationsAreLoaded(); + + if (isset($this->migrations[$version])) { + return $this->migrations[$version]; + } + + throw new \InvalidArgumentException("Version $version is unknown."); + } + + /** + * Allows to set an IOutput implementation which is used for logging progress and messages + * + * @param IOutput $output + */ + public function setOutput(IOutput $output) { + $this->output = $output; + } + + /** + * Applies all not yet applied versions up to $to + * + * @param string $to + * @throws \InvalidArgumentException + */ + public function migrate($to = 'latest') { + // read known migrations + $toBeExecuted = $this->getMigrationsToExecute($to); + foreach ($toBeExecuted as $version) { + $this->executeStep($version); + } + } + + /** + * @param string $version + * @return mixed + * @throws \InvalidArgumentException + */ + protected function createInstance($version) { + $class = $this->getClass($version); + try { + $s = \OC::$server->query($class); + } catch (QueryException $e) { + if (class_exists($class)) { + $s = new $class(); + } else { + throw new \InvalidArgumentException("Migration step '$class' is unknown"); + } + } + + return $s; + } + + /** + * Executes one explicit version + * + * @param string $version + * @throws \InvalidArgumentException + */ + public function executeStep($version) { + $instance = $this->createInstance($version); + if (!$instance instanceof IMigrationStep) { + throw new \InvalidArgumentException('Not a valid migration'); + } + + $instance->preSchemaChange($this->output, function() { + return $this->connection->createSchema(); + }, ['tablePrefix' => $this->connection->getPrefix()]); + + $toSchema = $instance->changeSchema($this->output, function() { + return new SchemaWrapper($this->connection); + }, ['tablePrefix' => $this->connection->getPrefix()]); + + if ($toSchema instanceof SchemaWrapper) { + $this->connection->migrateToSchema($toSchema->getWrappedSchema()); + $toSchema->performDropTableCalls(); + } + + $instance->postSchemaChange($this->output, function() { + return $this->connection->createSchema(); + }, ['tablePrefix' => $this->connection->getPrefix()]); + + $this->markAsExecuted($version); + } + + private function ensureMigrationsAreLoaded() { + if (empty($this->migrations)) { + $this->migrations = $this->findMigrations(); + } + } +} diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php index 1d00d9a1b45..da381ba0284 100644 --- a/lib/private/DB/Migrator.php +++ b/lib/private/DB/Migrator.php @@ -43,14 +43,10 @@ use Symfony\Component\EventDispatcher\GenericEvent; class Migrator { - /** - * @var \Doctrine\DBAL\Connection $connection - */ + /** @var \Doctrine\DBAL\Connection */ protected $connection; - /** - * @var ISecureRandom - */ + /** @var ISecureRandom */ private $random; /** @var IConfig */ @@ -197,6 +193,12 @@ class Migrator { return new Table($newName, $table->getColumns(), $newIndexes, array(), 0, $table->getOptions()); } + public function createSchema() { + $filterExpression = $this->getFilterExpression(); + $this->connection->getConfiguration()->setFilterSchemaAssetsExpression($filterExpression); + return $this->connection->getSchemaManager()->createSchema(); + } + /** * @param Schema $targetSchema * @param \Doctrine\DBAL\Connection $connection @@ -217,8 +219,7 @@ class Migrator { } $filterExpression = $this->getFilterExpression(); - $this->connection->getConfiguration()-> - setFilterSchemaAssetsExpression($filterExpression); + $this->connection->getConfiguration()->setFilterSchemaAssetsExpression($filterExpression); $sourceSchema = $connection->getSchemaManager()->createSchema(); // remove tables we don't know about diff --git a/lib/private/DB/OracleConnection.php b/lib/private/DB/OracleConnection.php index 08d71365172..51faf21970c 100644 --- a/lib/private/DB/OracleConnection.php +++ b/lib/private/DB/OracleConnection.php @@ -30,9 +30,14 @@ class OracleConnection extends Connection { * Quote the keys of the array */ private function quoteKeys(array $data) { - $return = array(); + $return = []; + $c = $this->getDatabasePlatform()->getIdentifierQuoteCharacter(); foreach($data as $key => $value) { - $return[$this->quoteIdentifier($key)] = $value; + if ($key[0] !== $c) { + $return[$this->quoteIdentifier($key)] = $value; + } else { + $return[$key] = $value; + } } return $return; } @@ -41,7 +46,9 @@ class OracleConnection extends Connection { * {@inheritDoc} */ public function insert($tableName, array $data, array $types = array()) { - $tableName = $this->quoteIdentifier($tableName); + if ($tableName[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) { + $tableName = $this->quoteIdentifier($tableName); + } $data = $this->quoteKeys($data); return parent::insert($tableName, $data, $types); } @@ -50,7 +57,9 @@ class OracleConnection extends Connection { * {@inheritDoc} */ public function update($tableName, array $data, array $identifier, array $types = array()) { - $tableName = $this->quoteIdentifier($tableName); + if ($tableName[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) { + $tableName = $this->quoteIdentifier($tableName); + } $data = $this->quoteKeys($data); $identifier = $this->quoteKeys($identifier); return parent::update($tableName, $data, $identifier, $types); @@ -60,9 +69,11 @@ class OracleConnection extends Connection { * {@inheritDoc} */ public function delete($tableExpression, array $identifier, array $types = array()) { - $tableName = $this->quoteIdentifier($tableExpression); + if ($tableExpression[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) { + $tableExpression = $this->quoteIdentifier($tableExpression); + } $identifier = $this->quoteKeys($identifier); - return parent::delete($tableName, $identifier); + return parent::delete($tableExpression, $identifier); } /** diff --git a/lib/private/DB/OracleMigrator.php b/lib/private/DB/OracleMigrator.php index 908b2dedf03..2735529b5e2 100644 --- a/lib/private/DB/OracleMigrator.php +++ b/lib/private/DB/OracleMigrator.php @@ -24,19 +24,75 @@ namespace OC\DB; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\ColumnDiff; +use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; class OracleMigrator extends NoCheckMigrator { /** * @param Schema $targetSchema * @param \Doctrine\DBAL\Connection $connection * @return \Doctrine\DBAL\Schema\SchemaDiff + * @throws DBALException */ protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { $schemaDiff = parent::getDiff($targetSchema, $connection); // oracle forces us to quote the identifiers + $schemaDiff->newTables = array_map(function(Table $table) { + return new Table( + $this->connection->quoteIdentifier($table->getName()), + array_map(function(Column $column) { + $newColumn = new Column( + $this->connection->quoteIdentifier($column->getName()), + $column->getType() + ); + $newColumn->setAutoincrement($column->getAutoincrement()); + $newColumn->setColumnDefinition($column->getColumnDefinition()); + $newColumn->setComment($column->getComment()); + $newColumn->setDefault($column->getDefault()); + $newColumn->setFixed($column->getFixed()); + $newColumn->setLength($column->getLength()); + $newColumn->setNotnull($column->getNotnull()); + $newColumn->setPrecision($column->getPrecision()); + $newColumn->setScale($column->getScale()); + $newColumn->setUnsigned($column->getUnsigned()); + $newColumn->setPlatformOptions($column->getPlatformOptions()); + $newColumn->setCustomSchemaOptions($column->getPlatformOptions()); + return $newColumn; + }, $table->getColumns()), + array_map(function(Index $index) { + return new Index( + $this->connection->quoteIdentifier($index->getName()), + array_map(function($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $index->getColumns()), + $index->isUnique(), + $index->isPrimary(), + $index->getFlags(), + $index->getOptions() + ); + }, $table->getIndexes()), + $table->getForeignKeys(), + 0, + $table->getOptions() + ); + }, $schemaDiff->newTables); + + $schemaDiff->removedTables = array_map(function(Table $table) { + return new Table( + $this->connection->quoteIdentifier($table->getName()), + $table->getColumns(), + $table->getIndexes(), + $table->getForeignKeys(), + 0, + $table->getOptions() + ); + }, $schemaDiff->removedTables); + foreach ($schemaDiff->changedTables as $tableDiff) { $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); foreach ($tableDiff->changedColumns as $column) { diff --git a/lib/private/DB/SchemaWrapper.php b/lib/private/DB/SchemaWrapper.php new file mode 100644 index 00000000000..0be45d8d3f7 --- /dev/null +++ b/lib/private/DB/SchemaWrapper.php @@ -0,0 +1,139 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Schema; +use OCP\IDBConnection; + +class SchemaWrapper { + + /** @var IDBConnection|Connection */ + protected $connection; + + /** @var Schema */ + protected $schema; + + /** @var array */ + protected $tablesToDelete; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + $this->schema = $this->connection->createSchema(); + } + + public function getWrappedSchema() { + return $this->schema; + } + + public function performDropTableCalls() { + foreach ($this->tablesToDelete as $tableName => $true) { + $this->connection->dropTable($tableName); + unset($this->tablesToDelete[$tableName]); + } + } + + /** + * Gets all table names + * + * @return array + */ + public function getTableNamesWithoutPrefix() { + $tableNames = $this->schema->getTableNames(); + return array_map(function($tableName) { + if (strpos($tableName, $this->connection->getPrefix()) === 0) { + return substr($tableName, strlen($this->connection->getPrefix())); + } + + return $tableName; + }, $tableNames); + } + + // Overwritten methods + + /** + * @param string $tableName + * + * @return \Doctrine\DBAL\Schema\Table + * @throws \Doctrine\DBAL\Schema\SchemaException + */ + public function getTable($tableName) { + return $this->schema->getTable($this->connection->getPrefix() . $tableName); + } + + /** + * Does this schema have a table with the given name? + * + * @param string $tableName + * + * @return boolean + */ + public function hasTable($tableName) { + return $this->schema->hasTable($this->connection->getPrefix() . $tableName); + } + + /** + * Creates a new table. + * + * @param string $tableName + * @return \Doctrine\DBAL\Schema\Table + */ + public function createTable($tableName) { + return $this->schema->createTable($this->connection->getPrefix() . $tableName); + } + + /** + * Renames a table. + * + * @param string $oldTableName + * @param string $newTableName + * + * @return \Doctrine\DBAL\Schema\Schema + * @throws DBALException + */ + public function renameTable($oldTableName, $newTableName) { + throw new DBALException('Renaming tables is not supported. Please create and drop the tables manually.'); + } + + /** + * Drops a table from the schema. + * + * @param string $tableName + * @return \Doctrine\DBAL\Schema\Schema + */ + public function dropTable($tableName) { + $this->tablesToDelete[$tableName] = true; + return $this->schema->dropTable($this->connection->getPrefix() . $tableName); + } + + /** + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call($name, $arguments) { + return call_user_func_array([$this->schema, $name], $arguments); + } +} diff --git a/lib/private/Installer.php b/lib/private/Installer.php index 35f51b19b07..60b0336fb5a 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -139,6 +139,9 @@ class Installer { } else { OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml'); } + } else { + $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection()); + $ms->migrate(); } \OC_App::registerAutoloading($appId, $basedir); @@ -530,6 +533,8 @@ class Installer { public static function installShippedApp($app) { //install the database $appPath = OC_App::getAppPath($app); + \OC_App::registerAutoloading($app, $appPath); + if(is_file("$appPath/appinfo/database.xml")) { try { OC_DB::createDbFromStructure("$appPath/appinfo/database.xml"); @@ -540,10 +545,12 @@ class Installer { 0, $e ); } + } else { + $ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection()); + $ms->migrate(); } //run appinfo/install.php - \OC_App::registerAutoloading($app, $appPath); self::includeAppScript("$appPath/appinfo/install.php"); $info = OC_App::getAppInfo($app); diff --git a/lib/private/Migration/SimpleOutput.php b/lib/private/Migration/SimpleOutput.php new file mode 100644 index 00000000000..b28fcbd7628 --- /dev/null +++ b/lib/private/Migration/SimpleOutput.php @@ -0,0 +1,84 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\Migration; + + +use OCP\ILogger; +use OCP\Migration\IOutput; + +/** + * Class SimpleOutput + * + * Just a simple IOutput implementation with writes messages to the log file. + * Alternative implementations will write to the console or to the web ui (web update case) + * + * @package OC\Migration + */ +class SimpleOutput implements IOutput { + + /** @var ILogger */ + private $logger; + private $appName; + + public function __construct(ILogger $logger, $appName) { + $this->logger = $logger; + $this->appName = $appName; + } + + /** + * @param string $message + * @since 9.1.0 + */ + public function info($message) { + $this->logger->info($message, ['app' => $this->appName]); + } + + /** + * @param string $message + * @since 9.1.0 + */ + public function warning($message) { + $this->logger->warning($message, ['app' => $this->appName]); + } + + /** + * @param int $max + * @since 9.1.0 + */ + public function startProgress($max = 0) { + } + + /** + * @param int $step + * @param string $description + * @since 9.1.0 + */ + public function advance($step = 1, $description = '') { + } + + /** + * @since 9.1.0 + */ + public function finishProgress() { + } +} diff --git a/lib/private/Setup.php b/lib/private/Setup.php index b8a861fd296..5cd3c84ce92 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -332,6 +332,8 @@ class Setup { try { $dbSetup->initialize($options); $dbSetup->setupDatabase($username); + // apply necessary migrations + $dbSetup->runMigrations(); } catch (\OC\DatabaseSetupException $e) { $error[] = array( 'error' => $e->getMessage(), diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index d5c34291e60..2fbec326a5d 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -27,6 +27,7 @@ namespace OC\Setup; use OC\DB\ConnectionFactory; +use OC\DB\MigrationService; use OC\SystemConfig; use OCP\IL10N; use OCP\ILogger; @@ -143,4 +144,12 @@ abstract class AbstractDatabase { * @param string $userName */ abstract public function setupDatabase($userName); + + public function runMigrations() { + if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) { + return; + } + $ms = new MigrationService('core', \OC::$server->getDatabaseConnection()); + $ms->migrate(); + } } diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 6d08e5d4cc0..6c23a43c354 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -32,6 +32,7 @@ namespace OC; +use OC\DB\MigrationService; use OC\Hooks\BasicEmitter; use OC\IntegrityCheck\Checker; use OC_App; @@ -300,8 +301,9 @@ class Updater extends BasicEmitter { protected function doCoreUpgrade() { $this->emit('\OC\Updater', 'dbUpgradeBefore'); - // do the real upgrade - \OC_DB::updateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml'); + // execute core migrations + $ms = new MigrationService('core', \OC::$server->getDatabaseConnection()); + $ms->migrate(); $this->emit('\OC\Updater', 'dbUpgrade'); } diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index 1bdbd1e2a83..463e13da915 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -50,6 +50,7 @@ use OC\App\DependencyAnalyzer; use OC\App\InfoParser; use OC\App\Platform; +use OC\DB\MigrationService; use OC\Installer; use OC\Repair; use OCP\App\ManagerEvent; @@ -1041,20 +1042,27 @@ class OC_App { if($appPath === false) { return false; } + self::registerAutoloading($appId, $appPath); + $appData = self::getAppInfo($appId); self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); + if (file_exists($appPath . '/appinfo/database.xml')) { OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml'); + } else { + $ms = new MigrationService($appId, \OC::$server->getDatabaseConnection()); + $ms->migrate(); } + self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']); unset(self::$appVersion[$appId]); + // run upgrade code if (file_exists($appPath . '/appinfo/update.php')) { self::loadApp($appId); include $appPath . '/appinfo/update.php'; } - self::registerAutoloading($appId, $appPath); self::setupBackgroundJobs($appData['background-jobs']); if(isset($appData['settings']) && is_array($appData['settings'])) { \OC::$server->getSettingsManager()->setupSettings($appData['settings']); diff --git a/lib/public/IDBConnection.php b/lib/public/IDBConnection.php index efd65d55f7e..56cf50c5fb3 100644 --- a/lib/public/IDBConnection.php +++ b/lib/public/IDBConnection.php @@ -34,6 +34,7 @@ // use OCP namespace for all classes that are considered public. // This means that they should be used by apps instead of the internal ownCloud classes namespace OCP; +use Doctrine\DBAL\Schema\Schema; use OCP\DB\QueryBuilder\IQueryBuilder; /** @@ -259,4 +260,20 @@ interface IDBConnection { * @since 11.0.0 */ public function supports4ByteText(); + + /** + * Create the schema of the connected database + * + * @return Schema + * @since 13.0.0 + */ + public function createSchema(); + + /** + * Migrate the database to the given schema + * + * @param Schema $toSchema + * @since 13.0.0 + */ + public function migrateToSchema(Schema $toSchema); } diff --git a/lib/public/Migration/IMigrationStep.php b/lib/public/Migration/IMigrationStep.php new file mode 100644 index 00000000000..49bb236ab7b --- /dev/null +++ b/lib/public/Migration/IMigrationStep.php @@ -0,0 +1,55 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Migration; + +use Doctrine\DBAL\Schema\Schema; + +/** + * @since 13.0.0 + */ +interface IMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options); + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options); + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options); +} diff --git a/lib/public/Migration/SimpleMigrationStep.php b/lib/public/Migration/SimpleMigrationStep.php new file mode 100644 index 00000000000..df4ae4e2eb6 --- /dev/null +++ b/lib/public/Migration/SimpleMigrationStep.php @@ -0,0 +1,59 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Migration; + +use Doctrine\DBAL\Schema\Schema; + +/** + * @since 13.0.0 + */ +abstract class SimpleMigrationStep implements IMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + return null; + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + } +} diff --git a/tests/lib/App/CodeChecker/CodeCheckerTest.php b/tests/lib/App/CodeChecker/CodeCheckerTest.php index cdbb7c17da5..bb121eccc44 100644 --- a/tests/lib/App/CodeChecker/CodeCheckerTest.php +++ b/tests/lib/App/CodeChecker/CodeCheckerTest.php @@ -23,7 +23,8 @@ class CodeCheckerTest extends TestCase { */ public function testFindInvalidUsage($expectedErrorToken, $expectedErrorCode, $fileToVerify) { $checker = new CodeChecker( - new PrivateCheck(new EmptyCheck()) + new PrivateCheck(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); @@ -49,7 +50,8 @@ class CodeCheckerTest extends TestCase { */ public function testPassValidUsage($fileToVerify) { $checker = new CodeChecker( - new PrivateCheck(new EmptyCheck()) + new PrivateCheck(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); diff --git a/tests/lib/App/CodeChecker/DeprecationCheckTest.php b/tests/lib/App/CodeChecker/DeprecationCheckTest.php index ee69f075a61..1a16e7e4920 100644 --- a/tests/lib/App/CodeChecker/DeprecationCheckTest.php +++ b/tests/lib/App/CodeChecker/DeprecationCheckTest.php @@ -23,7 +23,8 @@ class DeprecationCheckTest extends TestCase { */ public function testFindInvalidUsage($expectedErrorToken, $expectedErrorCode, $fileToVerify) { $checker = new CodeChecker( - new DeprecationCheck(new EmptyCheck()) + new DeprecationCheck(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); @@ -48,7 +49,8 @@ class DeprecationCheckTest extends TestCase { */ public function testPassValidUsage($fileToVerify) { $checker = new CodeChecker( - new DeprecationCheck(new EmptyCheck()) + new DeprecationCheck(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); diff --git a/tests/lib/App/CodeChecker/NodeVisitorTest.php b/tests/lib/App/CodeChecker/NodeVisitorTest.php index ca0b6e9ccc1..d1b1f643677 100644 --- a/tests/lib/App/CodeChecker/NodeVisitorTest.php +++ b/tests/lib/App/CodeChecker/NodeVisitorTest.php @@ -59,7 +59,8 @@ class NodeVisitorTest extends TestCase { */ public function testMethodsToCheck($expectedErrors, $fileToVerify) { $checker = new CodeChecker( - new TestList(new EmptyCheck()) + new TestList(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); diff --git a/tests/lib/App/CodeChecker/StrongComparisonCheckTest.php b/tests/lib/App/CodeChecker/StrongComparisonCheckTest.php index c73eae286ab..6d3d2f50efd 100644 --- a/tests/lib/App/CodeChecker/StrongComparisonCheckTest.php +++ b/tests/lib/App/CodeChecker/StrongComparisonCheckTest.php @@ -23,7 +23,8 @@ class StrongComparisonCheckTest extends TestCase { */ public function testFindInvalidUsage($expectedErrorToken, $expectedErrorCode, $fileToVerify) { $checker = new CodeChecker( - new StrongComparisonCheck(new EmptyCheck()) + new StrongComparisonCheck(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); @@ -45,7 +46,8 @@ class StrongComparisonCheckTest extends TestCase { */ public function testPassValidUsage($fileToVerify) { $checker = new CodeChecker( - new StrongComparisonCheck(new EmptyCheck()) + new StrongComparisonCheck(new EmptyCheck()), + false ); $errors = $checker->analyseFile(\OC::$SERVERROOT . "/tests/data/app/code-checker/$fileToVerify"); diff --git a/tests/lib/DB/MigrationsTest.php b/tests/lib/DB/MigrationsTest.php new file mode 100644 index 00000000000..9c06fe4cec5 --- /dev/null +++ b/tests/lib/DB/MigrationsTest.php @@ -0,0 +1,210 @@ +<?php + +/** + * Copyright (c) 2016 Thomas Müller <thomas.mueller@tmit.eu> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + + +namespace Test\DB; + +use Doctrine\DBAL\Schema\Schema; +use OC\DB\Connection; +use OC\DB\MigrationService; +use OC\DB\SchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IMigrationStep; +use OCP\Migration\ISchemaMigration; +use OCP\Migration\ISqlMigration; + +/** + * Class MigrationsTest + * + * @package Test\DB + */ +class MigrationsTest extends \Test\TestCase { + + /** @var MigrationService | \PHPUnit_Framework_MockObject_MockObject */ + private $migrationService; + /** @var \PHPUnit_Framework_MockObject_MockObject | IDBConnection $db */ + private $db; + + public function setUp() { + parent::setUp(); + + $this->db = $this->createMock(Connection::class); + $this->db->expects($this->any())->method('getPrefix')->willReturn('test_oc_'); + $this->migrationService = new MigrationService('testing', $this->db); + } + + public function testGetters() { + $this->assertEquals('testing', $this->migrationService->getApp()); + $this->assertEquals(\OC::$SERVERROOT . '/apps/testing/lib/Migration', $this->migrationService->getMigrationsDirectory()); + $this->assertEquals('OCA\Testing\Migration', $this->migrationService->getMigrationsNamespace()); + $this->assertEquals('test_oc_migrations', $this->migrationService->getMigrationsTableName()); + } + + public function testCore() { + $this->migrationService = new MigrationService('core', $this->db); + + $this->assertEquals('core', $this->migrationService->getApp()); + $this->assertEquals(\OC::$SERVERROOT . '/core/Migrations', $this->migrationService->getMigrationsDirectory()); + $this->assertEquals('OC\Core\Migrations', $this->migrationService->getMigrationsNamespace()); + $this->assertEquals('test_oc_migrations', $this->migrationService->getMigrationsTableName()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Version 20170130180000 is unknown. + */ + public function testExecuteUnknownStep() { + $this->migrationService->executeStep('20170130180000'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage App not found + */ + public function testUnknownApp() { + $migrationService = new MigrationService('unknown-bloody-app', $this->db); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Migration step 'X' is unknown + */ + public function testExecuteStepWithUnknownClass() { + $this->migrationService = $this->getMockBuilder(MigrationService::class) + ->setMethods(['findMigrations']) + ->setConstructorArgs(['testing', $this->db]) + ->getMock(); + $this->migrationService->expects($this->any())->method('findMigrations')->willReturn( + ['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] + ); + $this->migrationService->executeStep('20170130180000'); + } + + public function testExecuteStepWithSchemaChange() { + + $schema = $this->createMock(Schema::class); + $this->db->expects($this->any()) + ->method('createSchema') + ->willReturn($schema); + + $this->db->expects($this->once()) + ->method('migrateToSchema'); + + $schemaResult = $this->createMock(SchemaWrapper::class); + $schemaResult->expects($this->once()) + ->method('getWrappedSchema') + ->willReturn($this->createMock(Schema::class)); + + $step = $this->createMock(IMigrationStep::class); + $step->expects($this->at(0)) + ->method('preSchemaChange'); + $step->expects($this->at(1)) + ->method('changeSchema') + ->willReturn($schemaResult); + $step->expects($this->at(2)) + ->method('postSchemaChange'); + + $this->migrationService = $this->getMockBuilder(MigrationService::class) + ->setMethods(['createInstance']) + ->setConstructorArgs(['testing', $this->db]) + ->getMock(); + + $this->migrationService->expects($this->any()) + ->method('createInstance') + ->with('20170130180000') + ->willReturn($step); + $this->migrationService->executeStep('20170130180000'); + } + + public function testExecuteStepWithoutSchemaChange() { + + $schema = $this->createMock(Schema::class); + $this->db->expects($this->any()) + ->method('createSchema') + ->willReturn($schema); + + $this->db->expects($this->never()) + ->method('migrateToSchema'); + + $step = $this->createMock(IMigrationStep::class); + $step->expects($this->at(0)) + ->method('preSchemaChange'); + $step->expects($this->at(1)) + ->method('changeSchema') + ->willReturn(null); + $step->expects($this->at(2)) + ->method('postSchemaChange'); + + $this->migrationService = $this->getMockBuilder(MigrationService::class) + ->setMethods(['createInstance']) + ->setConstructorArgs(['testing', $this->db]) + ->getMock(); + + $this->migrationService->expects($this->any()) + ->method('createInstance') + ->with('20170130180000') + ->willReturn($step); + $this->migrationService->executeStep('20170130180000'); + } + + public function dataGetMigration() { + return [ + ['current', '20170130180001'], + ['prev', '20170130180000'], + ['next', '20170130180002'], + ['latest', '20170130180003'], + ]; + } + + /** + * @dataProvider dataGetMigration + * @param string $alias + * @param string $expected + */ + public function testGetMigration($alias, $expected) { + $this->migrationService = $this->getMockBuilder(MigrationService::class) + ->setMethods(['getMigratedVersions', 'findMigrations']) + ->setConstructorArgs(['testing', $this->db]) + ->getMock(); + $this->migrationService->expects($this->any())->method('getMigratedVersions')->willReturn( + ['20170130180000', '20170130180001'] + ); + $this->migrationService->expects($this->any())->method('findMigrations')->willReturn( + ['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] + ); + + $this->assertEquals( + ['20170130180000', '20170130180001', '20170130180002', '20170130180003'], + $this->migrationService->getAvailableVersions()); + + $migration = $this->migrationService->getMigration($alias); + $this->assertEquals($expected, $migration); + } + + public function testMigrate() { + $this->migrationService = $this->getMockBuilder(MigrationService::class) + ->setMethods(['getMigratedVersions', 'findMigrations', 'executeStep']) + ->setConstructorArgs(['testing', $this->db]) + ->getMock(); + $this->migrationService->expects($this->any())->method('getMigratedVersions')->willReturn( + ['20170130180000', '20170130180001'] + ); + $this->migrationService->expects($this->any())->method('findMigrations')->willReturn( + ['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] + ); + + $this->assertEquals( + ['20170130180000', '20170130180001', '20170130180002', '20170130180003'], + $this->migrationService->getAvailableVersions()); + + $this->migrationService->expects($this->exactly(2))->method('executeStep') + ->withConsecutive(['20170130180002'], ['20170130180003']); + $this->migrationService->migrate(); + } +} diff --git a/tests/lib/InstallerTest.php b/tests/lib/InstallerTest.php index a31c8826bd9..107b9dcb41f 100644 --- a/tests/lib/InstallerTest.php +++ b/tests/lib/InstallerTest.php @@ -19,6 +19,12 @@ use OCP\IConfig; use OCP\ILogger; use OCP\ITempManager; +/** + * Class InstallerTest + * + * @package Test + * @group DB + */ class InstallerTest extends TestCase { private static $appid = 'testapp'; |