Improve the unattended installation process for developers
authorAlexander Ebert <ebert@woltlab.com>
Fri, 24 Nov 2017 19:16:29 +0000 (20:16 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 24 Nov 2017 19:16:29 +0000 (20:16 +0100)
Closes #2477

extra/examples/wsc-dev-config-31.json [new file with mode: 0644]
wcfsetup/install/files/lib/acp/action/InstallPackageAction.class.php
wcfsetup/install/files/lib/system/WCFSetup.class.php
wcfsetup/install/files/lib/system/database/Database.class.php
wcfsetup/install/files/lib/system/database/MySQLDatabase.class.php
wcfsetup/install/files/lib/system/devtools/DevtoolsSetup.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php

diff --git a/extra/examples/wsc-dev-config-31.json b/extra/examples/wsc-dev-config-31.json
new file mode 100644 (file)
index 0000000..2ae9526
--- /dev/null
@@ -0,0 +1,30 @@
+{
+    "setup": {
+        "database": {
+            "auto": true,
+            "host": "localhost",
+            "password": "root",
+            "username": "root",
+            "dbNumber": "2"
+        },
+        "useDefaultInstallPath": true
+    },
+    "configuration": {
+        "option": {
+            "captcha_type": "",
+            "module_cookie_policy_page": "0"
+        }
+    },
+    "user": [
+        {
+            "username": "test",
+            "password": "test",
+            "email": "test@example.com"
+        },
+        {
+            "username": "test2",
+            "password": "test",
+            "email": "test2@example.com"
+        }
+    ]
+}
index 1db9891ab0ad17c6e7cb9392a96196ad61a3e76d..5e38ad6b187622b98c767ab58a8dcb948d1614db 100755 (executable)
@@ -4,6 +4,7 @@ use wcf\action\AbstractDialogAction;
 use wcf\data\application\Application;
 use wcf\data\package\installation\queue\PackageInstallationQueue;
 use wcf\system\cache\CacheHandler;
+use wcf\system\devtools\DevtoolsSetup;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\package\PackageInstallationDispatcher;
 use wcf\system\search\SearchIndexManager;
index 0405c4edfe5b2aabb27198a5f04592e085bcb046..8a130a10a1d0c8452efd02a0b4f34b518e85cee8 100644 (file)
@@ -10,6 +10,7 @@ use wcf\system\cache\builder\LanguageCacheBuilder;
 use wcf\system\database\exception\DatabaseException;
 use wcf\system\database\util\SQLParser;
 use wcf\system\database\MySQLDatabase;
+use wcf\system\devtools\DevtoolsSetup;
 use wcf\system\exception\SystemException;
 use wcf\system\exception\UserInputException;
 use wcf\system\io\File;
@@ -458,7 +459,7 @@ class WCFSetup extends WCF {
                }
                
                $documentRoot = FileUtil::unifyDirSeparator(realpath($_SERVER['DOCUMENT_ROOT']));
-               if (self::$developerMode && isset($_ENV['WCFSETUP_USEDEFAULTWCFDIR'])) {
+               if (self::$developerMode && (isset($_ENV['WCFSETUP_USEDEFAULTWCFDIR']) || DevtoolsSetup::getInstance()->useDefaultInstallPath())) {
                        // resolve path relative to document root
                        $relativePath = FileUtil::getRelativePath($documentRoot, INSTALL_SCRIPT_DIR);
                        foreach ($packages as $application => $packageData) {
@@ -616,12 +617,25 @@ class WCFSetup extends WCF {
         * Shows the page for configuring the database connection.
         */
        protected function configureDB() {
+               $attemptConnection = isset($_POST['send']);
+               
                if (self::$developerMode && isset($_ENV['WCFSETUP_DBHOST'])) {
                        $dbHost = $_ENV['WCFSETUP_DBHOST'];
                        $dbUser = $_ENV['WCFSETUP_DBUSER'];
                        $dbPassword = $_ENV['WCFSETUP_DBPASSWORD'];
                        $dbName = $_ENV['WCFSETUP_DBNAME'];
                        $dbNumber = 1;
+                       
+                       $attemptConnection = true;
+               }
+               else if (self::$developerMode && ($config = DevtoolsSetup::getInstance()->getDatabaseConfig()) !== null) {
+                       $dbHost = $config['host'];
+                       $dbUser = $config['username'];
+                       $dbPassword = $config['password'];
+                       $dbName = $config['dbName'];
+                       $dbNumber = $config['dbNumber'];
+                       
+                       if ($config['auto']) $attemptConnection = true;
                }
                else {
                        $dbHost = 'localhost';
@@ -631,7 +645,7 @@ class WCFSetup extends WCF {
                        $dbNumber = 1;
                }
                
-               if (isset($_POST['send']) || (self::$developerMode && isset($_ENV['WCFSETUP_DBHOST']))) {
+               if ($attemptConnection) {
                        if (isset($_POST['dbHost'])) $dbHost = $_POST['dbHost'];
                        if (isset($_POST['dbUser'])) $dbUser = $_POST['dbUser'];
                        if (isset($_POST['dbPassword'])) $dbPassword = $_POST['dbPassword'];
@@ -652,7 +666,7 @@ class WCFSetup extends WCF {
                                // check connection data
                                /** @var \wcf\system\database\Database $db */
                                try {
-                                       $db = new MySQLDatabase($dbHost, $dbUser, $dbPassword, $dbName, $dbPort, true);
+                                       $db = new MySQLDatabase($dbHost, $dbUser, $dbPassword, $dbName, $dbPort, true, !!(self::$developerMode));
                                }
                                catch (DatabaseException $e) {
                                        // work-around for older MySQL versions that don't know utf8mb4
index 51650d6a0dc15a2093e6bb5949b20a9cff2a8b26..9255f2a0ad492b85fb57832fab42b1efd99f6e00 100644 (file)
@@ -89,6 +89,12 @@ abstract class Database {
         */
        protected $activeTransactions = 0;
        
+       /**
+        * attempts to create the database after the connection has been established
+        * @var boolean
+        */
+       protected $tryToCreateDatabase = false;
+       
        /**
         * Creates a Database Object.
         * 
@@ -98,14 +104,16 @@ abstract class Database {
         * @param       string          $database               SQL database server database name
         * @param       integer         $port                   SQL database server port
         * @param       boolean         $failsafeTest
+        * @param       boolean         $tryToCreateDatabase
         */
-       public function __construct($host, $user, $password, $database, $port, $failsafeTest = false) {
+       public function __construct($host, $user, $password, $database, $port, $failsafeTest = false, $tryToCreateDatabase = false) {
                $this->host = $host;
                $this->port = $port;
                $this->user = $user;
                $this->password = $password;
                $this->database = $database;
                $this->failsafeTest = $failsafeTest;
+               $this->tryToCreateDatabase = $tryToCreateDatabase;
                
                // connect database
                $this->connect();
index 02134e9d84f10c01cd9bb88c2ddaf094532febdb..24155ed91fef578e47285d8103e24c40e766cafa 100644 (file)
@@ -39,8 +39,32 @@ class MySQLDatabase extends Database {
                        // throw PDOException instead of dumb false return values
                        $driverOptions[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION;
                        
-                       $this->pdo = new \PDO('mysql:host='.$this->host.';port='.$this->port.';dbname='.$this->database, $this->user, $this->password, $driverOptions);
+                       $dsn = 'mysql:host='.$this->host.';port='.$this->port;
+                       if (!$this->tryToCreateDatabase) $dsn .= ';dbname='.$this->database;
+                       
+                       $this->pdo = new \PDO($dsn, $this->user, $this->password, $driverOptions);
                        $this->setAttributes();
+                       
+                       if ($this->tryToCreateDatabase) {
+                               try {
+                                       $this->pdo->exec("USE ".$this->database);
+                               }
+                               catch (\PDOException $e) {
+                                       // 1049 = Unknown database
+                                       if ($this->pdo->errorInfo()[1] == 1049) {
+                                               try {
+                                                       $this->pdo->exec("CREATE DATABASE " . $this->database);
+                                                       $this->pdo->exec("USE " . $this->database);
+                                               }
+                                               catch (\PDOException $e) {
+                                                       wcfDebug($e);
+                                               }
+                                       }
+                                       else {
+                                               throw $e;
+                                       }
+                               }
+                       }
                }
                catch (\PDOException $e) {
                        throw new GenericDatabaseException("Connecting to MySQL server '".$this->host."' failed", $e);
diff --git a/wcfsetup/install/files/lib/system/devtools/DevtoolsSetup.class.php b/wcfsetup/install/files/lib/system/devtools/DevtoolsSetup.class.php
new file mode 100644 (file)
index 0000000..755dcf7
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+namespace wcf\system\devtools;
+use wcf\system\SingletonFactory;
+use wcf\util\FileUtil;
+use wcf\util\JSON;
+
+/**
+ * Enables the rapid deployment of new installations using a central configuration file
+ * in the document root. Requires the developer mode to work.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Package
+ * @since       3.1
+ */
+class DevtoolsSetup extends SingletonFactory {
+       /**
+        * configuration file in the server's document root
+        * @var string
+        */
+       const CONFIGURATION_FILE = 'wsc-dev-config-31.json';
+       
+       /**
+        * configuration data
+        * @var array
+        */
+       protected $configuration = [];
+       
+       /**
+        * @inheritDoc
+        */
+       protected function init() {
+               if (empty($_SERVER['DOCUMENT_ROOT'])) return;
+               
+               $docRoot = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator($_SERVER['DOCUMENT_ROOT']));
+               if (!file_exists($docRoot . self::CONFIGURATION_FILE)) return;
+               
+               $contents = file_get_contents($docRoot . self::CONFIGURATION_FILE);
+               
+               // allow the exception to go rampage
+               $this->configuration = JSON::decode($contents);
+       }
+       
+       /**
+        * Returns the database configuration.
+        * 
+        * @return array|null
+        */
+       public function getDatabaseConfig() {
+               if (!isset($this->configuration['setup']) || !isset($this->configuration['setup']['database'])) return null;
+               
+               // dirname return a single backslash on Windows if there are no parent directories 
+               $dir = dirname($_SERVER['SCRIPT_NAME']);
+               $dir = ($dir === '\\') ? '/' : FileUtil::addTrailingSlash($dir);
+               if ($dir === '/') throw new \RuntimeException("Refusing to install in the document root.");
+               
+               $dir = FileUtil::removeLeadingSlash(FileUtil::removeTrailingSlash($dir));
+               $dbName = implode('_', explode('/', $dir));
+               
+               $dbConfig = $this->configuration['setup']['database'];
+               return [
+                       'auto' => $dbConfig['auto'],
+                       'host' => $dbConfig['host'],
+                       'password' => $dbConfig['password'],
+                       'username' => $dbConfig['username'],
+                       'dbName' => $dbName,
+                       'dbNumber' => $dbConfig['dbNumber']
+               ];
+       }
+       
+       /**
+        * Returns true if the suggested default paths for the Core and, if exists,
+        * the bundled app should be used.
+        * 
+        * @return      boolean
+        */
+       public function useDefaultInstallPath() {
+               return (isset($this->configuration['setup']) && isset($this->configuration['setup']['useDefaultInstallPath']) && $this->configuration['setup']['useDefaultInstallPath'] === true);
+       }
+       
+       /**
+        * List of option values that will be set after the setup has completed.
+        * 
+        * @return      string[]
+        */
+       public function getOptionOverrides() {
+               if (!isset($this->configuration['configuration']) || empty($this->configuration['configuration']['option'])) return [];
+               
+               return $this->configuration['configuration']['option'];
+       }
+       
+       /**
+        * Returns a list of users that should be automatically created during setup.
+        * 
+        * @return      array|\Generator
+        */
+       public function getUsers() {
+               if (empty($this->configuration['user'])) return [];
+               
+               foreach ($this->configuration['user'] as $user) {
+                       if ($user['username'] === 'root') throw new \LogicException("The 'root' user is automatically created.");
+                       
+                       yield [
+                               'username' => $user['username'],
+                               'password' => $user['password'],
+                               'email' => $user['email']
+                       ];
+               }
+       }
+       
+       /**
+        * Returns the raw configuration data.
+        * 
+        * @return array
+        */
+       public function getRawConfiguration() {
+               return $this->configuration;
+       }
+}
\ No newline at end of file
index 98afc7b912bbd60e383ff0aa169d1afe5750468e..dac3166e83c26272da3630570e28c8a48156be93 100644 (file)
@@ -11,11 +11,13 @@ use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
 use wcf\data\package\Package;
 use wcf\data\package\PackageEditor;
 use wcf\data\user\User;
+use wcf\data\user\UserAction;
 use wcf\system\application\ApplicationHandler;
 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder;
 use wcf\system\cache\CacheHandler;
 use wcf\system\database\statement\PreparedStatement;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\devtools\DevtoolsSetup;
 use wcf\system\event\EventHandler;
 use wcf\system\exception\ImplementationException;
 use wcf\system\exception\SystemException;
@@ -247,6 +249,27 @@ class PackageInstallationDispatcher {
                                                        1,
                                                        'enable_developer_tools'
                                                ]);
+                                               
+                                               foreach (DevtoolsSetup::getInstance()->getOptionOverrides() as $optionName => $optionValue) {
+                                                       $statement->execute([
+                                                               $optionValue,
+                                                               $optionName
+                                                       ]);
+                                               }
+                                               
+                                               foreach (DevtoolsSetup::getInstance()->getUsers() as $newUser) {
+                                                       (new UserAction([], 'create', [
+                                                               'data' => [
+                                                                       'email' => $newUser['email'],
+                                                                       'password' => $newUser['password'],
+                                                                       'username' => $newUser['username']
+                                                               ],
+                                                               'groups' => [
+                                                                       1,
+                                                                       3
+                                                               ]
+                                                       ]))->executeAction();
+                                               }
                                        }
                                        
                                        // update options.inc.php