├── .gitignore ├── README.md ├── bin └── craft ├── box.json ├── composer.json ├── github-release.sh ├── sample.craft-cli.php └── src ├── Application.php ├── Command ├── AssetsPullCommand.php ├── AssetsPushCommand.php ├── ClearCacheCommand.php ├── ConsoleCommand.php ├── DbBackupCommand.php ├── DbCreateCommand.php ├── DbPullCommand.php ├── DbPushCommand.php ├── DbRestoreCommand.php ├── DownloadCraftCommand.php ├── ExemptFromBootstrapInterface.php ├── GenerateCommandCommand.php ├── InitCommand.php ├── InstallCraftCommand.php ├── InstallPluginCommand.php ├── RebuildSearchIndexesCommand.php ├── RunTasksCommand.php ├── ShowConfigCommand.php ├── TailCommand.php └── UpdateAssetIndexesCommand.php ├── Console └── GlobalArgvInput.php ├── Handlebars └── Loader │ └── FilesystemLoader.php ├── Support ├── AbstractMysqlCommand.php ├── ClassFinder.php ├── Downloader │ ├── BaseDownloader.php │ ├── Downloader.php │ └── TempDownloader.php ├── MysqlCommand.php ├── MysqlCreateDatabaseCommand.php ├── MysqlDumpCommand.php ├── PluginReader.php ├── RsyncCommand.php ├── SshCommand.php └── TarExtractor.php ├── bootstrap.php ├── stub.php └── templates ├── Command.php.handlebars └── db.php.handlebars /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | /craft.phar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Craft CLI 2 | 3 | Command line interface for Craft CMS. 4 | 5 | ## Installation 6 | 7 | If you are on Mac, you should install via [Homebrew](http://brew.sh/). 8 | 9 | ``` 10 | brew tap rsanchez/homebrew-craft-cli 11 | brew install craft-cli 12 | ``` 13 | 14 | Otherwise, you should download the phar: https://github.com/rsanchez/craft-cli/releases/latest 15 | 16 | ``` 17 | php craft.phar 18 | ``` 19 | 20 | See [Composer Installation](#composer-installation) for alternate ways to install. 21 | 22 | ## Usage 23 | 24 | If you are using a multi-environment config, you must specify your environment either using the `--environment=` flag on your commands, or set the `CRAFT_ENVIRONMENT` env variable. 25 | 26 | ``` 27 | craft --environment="mysite.dev" show:config 28 | ``` 29 | 30 | ``` 31 | CRAFT_ENVIRONMENT="mysite.dev" craft show:config 32 | ``` 33 | 34 | Craft CLI is [dotenv](https://github.com/vlucas/phpdotenv) aware, and will automagically load environment variables from a `.env` file in your project root (path can be customised via `.craft-cli.php`). You may set your Craft environment by setting a `CRAFT_ENVIRONMENT` variable in your `.env` file. 35 | 36 | ## Commands 37 | 38 | - [`assets:pull`](#assets-pull) 39 | - [`assets:push`](#assets-push) 40 | - [`clear:cache`](#clear-cache) 41 | - [`console`](#console) 42 | - [`db:backup`](#db-backup) 43 | - [`db:create`](#db-create) 44 | - [`db:pull`](#db-pull) 45 | - [`db:push`](#db-push) 46 | - [`db:restore`](#db-restore) 47 | - [`download`](#download-craft) 48 | - [`generate:command`](#generate-command) 49 | - [`help`](#help) 50 | - [`init`](#init) 51 | - [`install`](#install-craft) 52 | - [`install:plugin`](#install-plugin) 53 | - [`list`](#list) 54 | - [`rebuild:searchindexes`](#rebuild-search-indexes) 55 | - [`run:tasks`](#run-tasks) 56 | - [`show:config`](#show-config) 57 | - [`tail`](#tail) 58 | - [`update:assetsindexes`](#update-asset-indexes) 59 | 60 | ### Assets Pull 61 | 62 | Pull Asset files from a remote environment. 63 | 64 | ``` 65 | craft assets:pull --ssh-host=your.remote.server.com --ssh-user=yourUserName yourRemoteEnvironmentName 66 | ``` 67 | 68 | ### Assets Push 69 | 70 | Push Asset files to a remote environment 71 | 72 | ``` 73 | craft assets:push --ssh-host=your.remote.server.com --ssh-user=yourUserName yourRemoteEnvironmentName 74 | ``` 75 | 76 | ### Clear Cache 77 | 78 | Clear all Craft caches. 79 | 80 | ``` 81 | craft clear:cache 82 | ``` 83 | 84 | Select which cache(s) to clear from an interactive list. 85 | 86 | ``` 87 | craft clear:cache -s 88 | ``` 89 | 90 | ### Console 91 | 92 | Start an interactive shell. 93 | 94 | ``` 95 | craft console 96 | ``` 97 | 98 | ### DB Backup 99 | 100 | Backup your database to `craft/storage/backups`. 101 | 102 | ``` 103 | craft db:backup 104 | ``` 105 | 106 | Backup your database to the specified path. 107 | 108 | ``` 109 | craft db:backup ./backup.sql 110 | ``` 111 | 112 | ### DB Create 113 | 114 | Create a database 115 | 116 | ``` 117 | craft db:create --host=localhost --port=3306 --name=yourDbName --user=yourUserName --password=yourPassword 118 | ``` 119 | 120 | ### DB Pull 121 | 122 | Pull a remote database to the local database. 123 | 124 | ``` 125 | craft db:pull --ssh-host=your.remote.server.com --ssh-user=yourUserName --force yourRemoteEnvironmentName 126 | ``` 127 | 128 | ### DB Push 129 | 130 | Push your local database to a remote database. 131 | 132 | ``` 133 | craft db:push --ssh-host=your.remote.server.com --ssh-user=yourUserName --force yourRemoteEnvironmentName 134 | ``` 135 | 136 | ### DB Restore 137 | 138 | Restore the database from the most recent backup from `craft/storage/backups`. 139 | 140 | ``` 141 | craft db:restore --force 142 | ``` 143 | 144 | Restore the database from the specified `.sql` file. 145 | 146 | ``` 147 | craft db:restore --force ./backup.sql 148 | ``` 149 | 150 | ### Download Craft 151 | 152 | Download Craft to the current directory. 153 | 154 | ``` 155 | craft download 156 | ``` 157 | 158 | Create the specified directory and download Craft into it. 159 | 160 | ``` 161 | craft download path/to/directory 162 | ``` 163 | 164 | ### Generate Command 165 | 166 | Generate a custom command file in the specified directory. 167 | 168 | ``` 169 | craft generate:command your:custom_command ./commands/ 170 | ``` 171 | 172 | Generate a custom command file with a namespace. 173 | 174 | ``` 175 | craft generate:command --namespace="YourSite\Command" your:custom_command ./src/YourSite/Command/ 176 | ``` 177 | 178 | Generate a custom command with arguments and options. 179 | 180 | ``` 181 | craft generate:command --options --arguments your_command ./commands/ 182 | ``` 183 | 184 | ### Help 185 | 186 | Display information about a command and its arguments/options. 187 | 188 | ``` 189 | craft help 190 | ``` 191 | 192 | ### Init 193 | 194 | Create an `.craft-cli.php` config file in the current directory 195 | 196 | ``` 197 | craft init 198 | ``` 199 | 200 | This config file is only necessary if you if you are using [Custom Commands](#custom-commands) or have renamed your `craft` folder. 201 | 202 | ### Install Craft 203 | 204 | Download and install Craft to the current directory. 205 | 206 | ``` 207 | craft install 208 | ``` 209 | 210 | Create the specified directory and install Craft into it. 211 | 212 | ``` 213 | craft install path/to/directory 214 | ``` 215 | 216 | ### Install Plugin 217 | 218 | Install a plugin from a GitHub repository. 219 | 220 | ``` 221 | craft install:plugin pixelandtonic/ElementApi 222 | ``` 223 | 224 | ### List 225 | 226 | List the available commands. 227 | 228 | ``` 229 | craft list 230 | ``` 231 | 232 | ### Rebuild Search Indexes 233 | 234 | ``` 235 | craft rebuild:searchindexes 236 | ``` 237 | 238 | ### Run Tasks 239 | 240 | Run all pending tasks. 241 | 242 | ``` 243 | craft run:tasks 244 | ``` 245 | 246 | Reset "running" (stalled) tasks and then run all tasks. 247 | 248 | ``` 249 | craft run:tasks --reset-running 250 | ``` 251 | 252 | Reset failed tasks and then run all tasks. 253 | 254 | ``` 255 | craft run:tasks --reset-failed 256 | ``` 257 | 258 | ### Show Config 259 | 260 | Show all config items. 261 | 262 | ``` 263 | craft show:config 264 | ``` 265 | 266 | Show the specified config item. 267 | 268 | ``` 269 | craft show:config db.user 270 | ``` 271 | 272 | ### Tail 273 | 274 | Show a tail of craft.log 275 | 276 | ``` 277 | craft tail 278 | ``` 279 | 280 | ### Update Asset Indexes 281 | 282 | ``` 283 | craft update:assetsindexes 284 | ``` 285 | 286 | ## Configuration 287 | 288 | Craft CLI can be configured in two ways. You may use the `craft init` command to generate a `.craft-cli.php` file. Or, if you have installed Craft CLI via composer, you may add an `extra` object to your `composer.json` and a `craft-cli` object within the `extra` object: 289 | 290 | ``` 291 | { 292 | "extra": { 293 | "craft-cli": { 294 | "commandDirs": { 295 | "\\Your\\Namespace": "path/to/commands/" 296 | } 297 | } 298 | } 299 | } 300 | ``` 301 | 302 | ## Custom Commands 303 | 304 | Craft CLI custom commands are [Symfony Console](http://symfony.com/doc/current/components/console/introduction.html) Command objects. You can add custom commands to your `.craft-cli.php` or `composer.json` config by adding a namespace and folder path to the `commandDirs` object. 305 | 306 | You can generate a custom command file using the `craft generate:command` command. 307 | 308 | ## Troubleshooting 309 | 310 | ### Your command-line PHP cannot connect to MySQL 311 | 312 | You can test this by running this at the command line (change the DB credentials to your actual credentials): 313 | 314 | ``` 315 | php -r "var_dump(@mysql_connect('hostname', 'username', 'password', 'database_name'));" 316 | ``` 317 | 318 | If this prints false, then you know that your CLI PHP is not configured to connect to your database. This is frequently caused by an incorrect default MySQL socket setting. 319 | 320 | If you are running MAMP, for instance, and are using the stock Mac OS command-line PHP, you will not be able to connect out-of-the-box. You will need to edit your `/etc/php.ini` (or wherever your php.ini file is located) file and change the `mysql.default_socket` and/or the `mysqli.default_socket` to `/Applications/MAMP/tmp/mysql/mysql.sock`. 321 | 322 | ## Composer Installation 323 | 324 | You can install globally: 325 | 326 | ``` 327 | composer global require craft-cli/cli 328 | ``` 329 | 330 | Make sure your global composer installation is added to your PATH in your `~/.bash_profile` (or `~/.profile` or `~/.bashrc` or `~/.zshrc`) so that you may run the binary from the command line: 331 | 332 | ``` 333 | export PATH=~/.composer/vendor/bin:$PATH 334 | ``` 335 | 336 | Or, you can install on a per project basis, rather than globally to your host system. 337 | 338 | ``` 339 | composer require craft-cli/cli 340 | ``` 341 | 342 | Then the command would be found in your `vendor/bin` folder, so you'd run this at your command line: 343 | 344 | ``` 345 | vendor/bin/craft 346 | ``` 347 | -------------------------------------------------------------------------------- /bin/craft: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 6 | # 7 | # 1) Change dir to craft-cli repo 8 | # 2) Bumps version number in src/Application.php 9 | # 3) Commit, tag new version and push those changes/tags to origin 10 | # 4) Build new craft.phar 11 | # 5) Create new github release based on new tag 12 | # 6) Upload craft.phar to release 13 | # 7) Change dir to homebrew-craft-cli 14 | # 8) Update craft-cli homebrew formula with latest version (version number, SHA1 hash, download url) 15 | # 9) Commit and push changes to homebrew formula 16 | 17 | TAG=$1 18 | 19 | if [[ -z $CRAFT_CLI_HOMEBREW_DIR ]]; then 20 | CRAFT_CLI_HOMEBREW_DIR="../../homebrew-craft-cli" 21 | fi 22 | 23 | if [[ -z $(which github-release) ]]; then 24 | echo "\x1B[31mYou must first install github-release.\x1B[39m" 25 | exit 1 26 | fi 27 | 28 | if [[ -z $(which box) ]]; then 29 | echo "\x1B[31mYou must first install box.\x1B[39m" 30 | exit 1 31 | fi 32 | 33 | # check for a valid tag 34 | if [[ -z $TAG ]]; then 35 | echo 'Usage: ./github-release.sh ' 36 | exit 1 37 | fi 38 | 39 | # check if correct repo 40 | if [[ $(git config --get remote.origin.url) != "git@github.com:rsanchez/craft-cli.git" ]]; then 41 | echo "\x1B[31mYou must be in the rsanchez/craft-cli repo.\x1B[39m" 42 | exit 1 43 | fi 44 | 45 | # go to root of repo 46 | if [[ -n $(git rev-parse --show-cdup) ]]; then 47 | echo "\x1B[31mYou must be in root of the repo.\x1B[39m" 48 | exit 1 49 | fi 50 | 51 | # check if master branch 52 | if [[ $(git rev-parse --abbrev-ref HEAD) != "master" ]]; then 53 | echo "\x1B[31mYou must be in the master branch.\x1B[39m" 54 | exit 1 55 | fi 56 | 57 | # check if there are any changes 58 | if [[ -n $(git status -s -uno) ]]; then 59 | echo "\x1B[31mYou have uncommitted changes.\x1B[39m" 60 | exit 1 61 | fi 62 | 63 | # test github-release credentials 64 | github-release info --user "rsanchez" --repo "craft-cli" >/dev/null || { echo "\x1B[31mYou have invalid github-release credentials.\x1B[39m"; exit 1; } 65 | 66 | # replace tag in text 67 | perl -pi -w -e "s/const VERSION = '.*?';/const VERSION = '$TAG';/g;" src/Application.php 68 | 69 | # commit version number changes 70 | git add src/Application.php 71 | 72 | git commit -m "Release $TAG" 73 | 74 | # add new tag 75 | git tag "$TAG" 76 | 77 | # push changes 78 | git push origin master 79 | 80 | # and tags 81 | git push --tags 82 | 83 | # build the phar 84 | box build 85 | 86 | # create the github release 87 | github-release release --user "rsanchez" --repo "craft-cli" --tag "$TAG" 88 | 89 | # upload the phar to the release 90 | github-release upload --user "rsanchez" --repo "craft-cli" --tag "$TAG" --name craft.phar --file craft.phar 91 | 92 | # get sha hash of phar 93 | SHA=$(shasum -a 256 craft.phar | cut -d ' ' -f 1) 94 | 95 | cd $CRAFT_CLI_HOMEBREW_DIR 96 | 97 | perl -pi -w -e "s/download\/.*?\/craft\.phar/download\/$TAG\/craft.phar/g;" craft-cli.rb 98 | perl -pi -w -e "s/version '.*?'/version '$TAG'/g;" craft-cli.rb 99 | perl -pi -w -e "s/sha1 '.*?'/sha256 '$SHA'/g;" craft-cli.rb 100 | perl -pi -w -e "s/sha256 '.*?'/sha256 '$SHA'/g;" craft-cli.rb 101 | 102 | git add craft-cli.rb 103 | 104 | git commit -m "Release $TAG" 105 | 106 | git push origin master 107 | -------------------------------------------------------------------------------- /sample.craft-cli.php: -------------------------------------------------------------------------------- 1 | /craft 21 | */ 22 | 'craft_path' => __DIR__.'/craft', 23 | 24 | /** 25 | * Craft app path 26 | * 27 | * Specify the path to your craft app folder 28 | * If you leave this blank, it will assume your 29 | * folder is /craft/app 30 | */ 31 | 'craft_app_path' => null, 32 | 33 | /** 34 | * Craft framework path 35 | * 36 | * Specify the path to your Yii framework folder 37 | * If you leave this blank, it will assume your 38 | * folder is /craft/app/framework 39 | */ 40 | 'craft_framework_path' => null, 41 | 42 | /** 43 | * Craft config path 44 | * 45 | * Specify the path to your craft config folder 46 | * If you leave this blank, it will assume your 47 | * folder is /craft/config 48 | */ 49 | 'craft_config_path' => null, 50 | 51 | /** 52 | * Craft plugins path 53 | * 54 | * Specify the path to your craft plugins folder 55 | * If you leave this blank, it will assume your 56 | * folder is /craft/plugins 57 | */ 58 | 'craft_plugins_path' => null, 59 | 60 | /** 61 | * Craft storage path 62 | * 63 | * Specify the path to your craft storage folder 64 | * If you leave this blank, it will assume your 65 | * folder is /craft/storage 66 | */ 67 | 'craft_storage_path' => null, 68 | 69 | /** 70 | * Craft templates path 71 | * 72 | * Specify the path to your craft templates folder 73 | * If you leave this blank, it will assume your 74 | * folder is /craft/templates 75 | */ 76 | 'craft_templates_path' => null, 77 | 78 | /** 79 | * Craft translations path 80 | * 81 | * Specify the path to your craft translations folder 82 | * If you leave this blank, it will assume your 83 | * folder is /craft/translations 84 | */ 85 | 'craft_translations_path' => null, 86 | 87 | /** 88 | * Craft vendor path 89 | * 90 | * Specify the path to your craft vendor folder 91 | * If you leave this blank, it will assume your 92 | * folder is /craft/app/vendor 93 | */ 94 | 'craft_vendor_path' => null, 95 | 96 | /** 97 | * Craft environment 98 | * 99 | * The server name of your craft environment. 100 | */ 101 | 'environment' => null, 102 | 103 | /** 104 | * Dotenv path 105 | * 106 | * (optional) If using phpdotenv, specify the path to 107 | * the directory containing your .env file, and 108 | * craft-cli load with load environment variables 109 | * from it 110 | */ 111 | 'dotenv_path' => __DIR__, 112 | 113 | /** 114 | * Custom command directories 115 | * 116 | * An array of directories, keyed by a namespace prefix, 117 | * which will be crawled for Command classes. 118 | */ 119 | 'commandDirs' => array( 120 | /* 121 | '\\Your\\Namespace' => '/path/to/commands', 122 | */ 123 | ), 124 | ); 125 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | vendorPath = $vendorPath; 87 | 88 | $dispatcher = new EventDispatcher(); 89 | 90 | $dispatcher->addListener(ConsoleEvents::COMMAND, array($this, 'onCommand')); 91 | 92 | $this->setDispatcher($dispatcher); 93 | 94 | $this->loadConfig(); 95 | 96 | foreach ($this->findCommandsInDir(__DIR__.'/Command', '\\CraftCli\\Command') as $command) { 97 | $this->registerCommand($command); 98 | } 99 | } 100 | 101 | /** 102 | * On Command Event Handler 103 | * 104 | * Check if the current command requires EE bootstrapping 105 | * and throw an exception if EE is not bootstrapped 106 | * 107 | * @param ConsoleCommandEvent $event 108 | * @return void 109 | */ 110 | public function onCommand(ConsoleCommandEvent $event) 111 | { 112 | $command = $event->getCommand(); 113 | 114 | $command->setApplication($this); 115 | 116 | $output = $event->getOutput(); 117 | 118 | if (! $this->isCommandExemptFromBootstrap($command)) { 119 | if (! $this->canBeBootstrapped()) { 120 | throw new \Exception('Your craft path could not be found.'); 121 | } 122 | 123 | $craft = $this->bootstrap(); 124 | 125 | if ($command instanceof NeedsCraftInterface) { 126 | $command->setCraft($craft); 127 | $command->setEnvironment(CRAFT_ENVIRONMENT); 128 | $command->setAppPath(CRAFT_APP_PATH); 129 | $command->setBasePath(CRAFT_BASE_PATH); 130 | $command->setConfigPath(CRAFT_CONFIG_PATH); 131 | $command->setPluginsPath(CRAFT_PLUGINS_PATH); 132 | $command->setStoragePath(CRAFT_STORAGE_PATH); 133 | $command->setTemplatesPath(CRAFT_TEMPLATES_PATH); 134 | $command->setTranslationsPath(CRAFT_TRANSLATIONS_PATH); 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Check whether a command should be exempt from bootstrapping 141 | * @param \Symfony\Component\Console\Command\Command $command 142 | * @return boolean 143 | */ 144 | protected function isCommandExemptFromBootstrap(SymfonyCommand $command) 145 | { 146 | $commandName = $command->getName(); 147 | 148 | if ($commandName === 'help' || $commandName === 'list') { 149 | return true; 150 | } 151 | 152 | return $command instanceof ExemptFromBootstrapInterface; 153 | } 154 | 155 | /** 156 | * Whether or not a valid system folder was found 157 | * @return bool 158 | */ 159 | public function canBeBootstrapped() 160 | { 161 | return file_exists($this->appPath.'/Craft.php'); 162 | } 163 | 164 | /** 165 | * Boot up craft 166 | * @param bool $isInstalling 167 | * @return \Craft\ConsoleApp 168 | */ 169 | public function bootstrap($isInstalling = false) 170 | { 171 | $craftPath = $this->craftPath; 172 | $appPath = $this->appPath; 173 | 174 | if ($this->vendorPath) { 175 | return require $this->vendorPath.'/craft-cli/bootstrap/src/bootstrap-craft2.php'; 176 | } 177 | 178 | return require __DIR__.'/../../bootstrap/src/bootstrap-craft2.php'; 179 | } 180 | 181 | /** 182 | * Get the environment from the --environment option 183 | * or from the CRAFT_ENVIRONMENT env variable 184 | * @return string|null 185 | */ 186 | protected function getEnvironmentOption() 187 | { 188 | $definition = new InputDefinition(); 189 | 190 | $definition->addOption(new InputOption('environment', null, InputOption::VALUE_REQUIRED)); 191 | 192 | $input = new Console\GlobalArgvInput(null, $definition); 193 | 194 | return $input->getOption('environment') ?: getenv('CRAFT_ENVIRONMENT'); 195 | } 196 | 197 | /** 198 | * Traverse up a directory to find a config file 199 | * 200 | * @param string|null $dir defaults to getcwd if null 201 | * @return string|null 202 | */ 203 | protected function findConfigFile($dir = null) 204 | { 205 | if (is_null($dir)) { 206 | $dir = getcwd(); 207 | } 208 | 209 | if ($dir === '/') { 210 | return null; 211 | } 212 | 213 | if (file_exists($dir.'/'.self::FILENAME)) { 214 | return $dir.'/'.self::FILENAME; 215 | } 216 | 217 | $parentDir = dirname($dir); 218 | 219 | if ($parentDir && is_dir($parentDir)) { 220 | return $this->findConfigFile($parentDir); 221 | } 222 | 223 | return null; 224 | } 225 | 226 | /** 227 | * Looks for ~/.craft-cli.php and ./.craft-cli.php 228 | * and combines them into an array 229 | * 230 | * @return void 231 | */ 232 | protected function loadConfig() 233 | { 234 | // Load configuration file(s) 235 | $config = array(); 236 | 237 | // Look for ~/.craft-cli.php in the user's home directory 238 | if (isset($_SERVER['HOME']) && file_exists($_SERVER['HOME'].self::FILENAME)) { 239 | $temp = require $_SERVER['HOME'].self::FILENAME; 240 | 241 | if (is_array($temp)) { 242 | $config = array_merge($config, $temp); 243 | } 244 | 245 | unset($temp); 246 | } 247 | 248 | $configFile = $this->findConfigFile(); 249 | 250 | // Look for the config file in the current working directory 251 | if ($configFile) { 252 | $temp = require $configFile; 253 | 254 | if (is_array($temp)) { 255 | $config = array_merge($config, $temp); 256 | } 257 | 258 | unset($temp); 259 | } 260 | 261 | $config = array_merge($config, $this->getComposerConfig()); 262 | 263 | $this->config = $config; 264 | 265 | // Set the craft path 266 | if (isset($config['craft_path'])) { 267 | $this->craftPath = $config['craft_path']; 268 | } 269 | 270 | $dotenvPath = isset($config['dotenv_path']) ? $config['dotenv_path'] : getcwd(); 271 | 272 | if (file_exists($dotenvPath.'/.env')) { 273 | $dotenv = new Dotenv($dotenvPath); 274 | $dotenv->load(); 275 | } 276 | 277 | $environment = empty($config['environment']) ? $this->getEnvironmentOption() : $config['environment']; 278 | 279 | if ($environment) { 280 | define('CRAFT_ENVIRONMENT', $environment); 281 | } 282 | 283 | if (isset($config['craft_app_path'])) { 284 | $this->appPath = $config['craft_app_path']; 285 | define('CRAFT_APP_PATH', rtrim($config['craft_app_path'], '/').'/'); 286 | } 287 | 288 | if (isset($config['craft_framework_path'])) { 289 | define('CRAFT_FRAMEWORK_PATH', rtrim($config['craft_framework_path'], '/').'/'); 290 | } 291 | 292 | if (isset($config['craft_config_path'])) { 293 | define('CRAFT_CONFIG_PATH', rtrim($config['craft_config_path'], '/').'/'); 294 | } 295 | 296 | if (isset($config['craft_plugins_path'])) { 297 | define('CRAFT_PLUGINS_PATH', rtrim($config['craft_plugins_path'], '/').'/'); 298 | } 299 | 300 | if (isset($config['craft_storage_path'])) { 301 | define('CRAFT_STORAGE_PATH', rtrim($config['craft_storage_path'], '/').'/'); 302 | } 303 | 304 | if (isset($config['craft_templates_path'])) { 305 | define('CRAFT_TEMPLATES_PATH', rtrim($config['craft_templates_path'], '/').'/'); 306 | } 307 | 308 | if (isset($config['craft_translations_path'])) { 309 | define('CRAFT_TRANSLATIONS_PATH', rtrim($config['craft_translations_path'], '/').'/'); 310 | } 311 | 312 | if (isset($config['craft_vendor_path'])) { 313 | define('CRAFT_VENDOR_PATH', rtrim($config['craft_vendor_path'], '/').'/'); 314 | } 315 | } 316 | 317 | /** 318 | * Get craft-cli configuration stored in the root composer.json 319 | * @return array 320 | */ 321 | public function getComposerConfig() 322 | { 323 | if (! $this->vendorPath) { 324 | return array(); 325 | } 326 | 327 | $jsonPath = rtrim($this->vendorPath, '/').'/../composer.json'; 328 | 329 | if (! file_exists($jsonPath)) { 330 | return array(); 331 | } 332 | 333 | $jsonContents = file_get_contents($jsonPath); 334 | 335 | if (! $jsonContents) { 336 | return array(); 337 | } 338 | 339 | $package = json_decode($jsonContents, true); 340 | 341 | if (! isset($package['extra']['craft-cli'])) { 342 | return array(); 343 | } 344 | 345 | $config = $package['extra']['craft-cli']; 346 | 347 | $finder = new ClassFinder(); 348 | 349 | if (isset($config['commandDirs'])) { 350 | foreach ($config['commandDirs'] as $namespace => $commandDir) { 351 | if (!$finder->isPathAbsolute($commandDir)) { 352 | $config['commandDirs'][$namespace] = rtrim($this->vendorPath, '/').'/../'.$commandDir; 353 | } 354 | } 355 | } 356 | 357 | return $config; 358 | } 359 | 360 | /** 361 | * Get all configuration items 362 | * @return array 363 | */ 364 | public function getConfig() 365 | { 366 | return $this->config; 367 | } 368 | 369 | /** 370 | * Get the specified config item 371 | * @param string $key 372 | * @return mixed 373 | */ 374 | public function getConfigItem($key) 375 | { 376 | return isset($this->config[$key]) ? $this->config[$key] : null; 377 | } 378 | 379 | /** 380 | * Get the path to the craft folder 381 | * @return string 382 | */ 383 | public function getCraftPath() 384 | { 385 | return $this->craftPath; 386 | } 387 | 388 | /** 389 | * Get the path to the craft folder 390 | * @param string $craftPath 391 | * @return string 392 | */ 393 | public function setCraftPath($craftPath) 394 | { 395 | return $this->craftPath = $craftPath; 396 | } 397 | 398 | /** 399 | * Get the name of the craft folder 400 | * @return string 401 | */ 402 | public function getCraftFolder() 403 | { 404 | return basename($this->craftPath); 405 | } 406 | 407 | /** 408 | * Get the default addon author name 409 | * @return string 410 | */ 411 | public function getAddonAuthorName() 412 | { 413 | return $this->addonAuthorName; 414 | } 415 | 416 | /** 417 | * Get the default addon author URL 418 | * @return string 419 | */ 420 | public function getAddonAuthorUrl() 421 | { 422 | return $this->addonAuthorUrl; 423 | } 424 | 425 | /** 426 | * Find any user-defined Commands in the config 427 | * and add them to the Application 428 | * @return void 429 | */ 430 | public function addUserDefinedCommands() 431 | { 432 | $namespace = isset($this->config['namespace']) 433 | ? rtrim($this->config['namespace'], '\\').'\\' 434 | : null; 435 | 436 | if (! empty($this->config['commandDirs'])) { 437 | $finder = new ClassFinder(); 438 | 439 | foreach ($this->config['commandDirs'] as $commandNamespace => $commandDir) { 440 | // handle deprecated indexed array 441 | if (is_numeric($commandNamespace)) { 442 | $commandNamespace = $namespace; 443 | } 444 | 445 | if ($finder->isPathAbsolute($commandDir)) { 446 | $path = $commandDir; 447 | } else { 448 | $path = getcwd().DIRECTORY_SEPARATOR.$commandDir; 449 | } 450 | 451 | if (is_dir($path)) { 452 | $commands = $this->findCommandsInDir($path, $commandNamespace, true); 453 | 454 | foreach ($commands as $command) { 455 | $this->registerCommand($command); 456 | } 457 | } 458 | } 459 | } 460 | 461 | if (! empty($this->config['commands'])) { 462 | foreach ($this->config['commands'] as $command) { 463 | $this->registerCommand($namespace.$command); 464 | } 465 | } 466 | } 467 | 468 | /** 469 | * Find any Command plugins installed via composer 470 | * and add them to the Application 471 | * @return void 472 | */ 473 | public function addComposerPlugins() 474 | { 475 | if (! $this->vendorPath) { 476 | return; 477 | } 478 | 479 | $installedJsonPath = rtrim($this->vendorPath, '/').'/composer/installed.json'; 480 | 481 | if (! file_exists($installedJsonPath)) { 482 | return; 483 | } 484 | 485 | $installedJsonContents = file_get_contents($installedJsonPath); 486 | 487 | if (! $installedJsonContents) { 488 | return; 489 | } 490 | 491 | $installed = json_decode($installedJsonContents, true); 492 | 493 | if (! $installed || ! is_array($installed)) { 494 | return; 495 | } 496 | 497 | $plugins = array_filter($installed, function ($package) { 498 | return ! empty($package['extra']['craft-cli']['commands']) || ! empty($package['extra']['craft-cli']['commandDirs']); 499 | }); 500 | 501 | foreach ($plugins as $package) { 502 | $namespace = isset($package['extra']['craft-cli']['namespace']) 503 | ? rtrim($package['extra']['craft-cli']['namespace'], '\\').'\\' 504 | : null; 505 | 506 | if (! empty($package['extra']['craft-cli']['commandDirs'])) { 507 | foreach ($package['extra']['craft-cli']['commandDirs'] as $commandNamespace => $commandDir) { 508 | // handle deprecated indexed array 509 | if (is_numeric($commandNamespace)) { 510 | $commandNamespace = $namespace; 511 | } 512 | 513 | $path = rtrim($this->vendorPath, '/').'/'.$package['name'].'/'.$commandDir; 514 | 515 | $commands = $this->findCommandsInDir($path, $commandNamespace); 516 | 517 | foreach ($commands as $command) { 518 | $this->registerCommand($command); 519 | } 520 | } 521 | } 522 | 523 | if (! empty($package['extra']['craft-cli']['commands'])) { 524 | foreach ($package['extra']['craft-cli']['commands'] as $command) { 525 | $this->registerCommand($namespace.$command); 526 | } 527 | } 528 | } 529 | } 530 | 531 | /** 532 | * Get a list of Symfony Console Commands classes 533 | * in the specified directory 534 | * 535 | * @param string $dir 536 | * @param string $namespace 537 | * @param bool $autoload 538 | * @return array 539 | */ 540 | public function findCommandsInDir($dir, $namespace = null, $autoload = false) 541 | { 542 | return (new ClassFinder)->findClassInDir('Symfony\\Component\\Console\\Command\\Command', $dir, $namespace, $autoload); 543 | } 544 | 545 | /** 546 | * Set the path to the Composer vendor folder 547 | * @param string $vendorPath 548 | */ 549 | public function setVendorPath($vendorPath) 550 | { 551 | $this->vendorPath = $vendorPath; 552 | } 553 | 554 | /** 555 | * Add a command to the Application by class name 556 | * or callback that return a Command class 557 | * @param string|callable $class class name or callback that returns a command 558 | * @return void 559 | */ 560 | public function registerCommand($class) 561 | { 562 | // is it a callback or a string? 563 | if (is_callable($class)) { 564 | $this->add(call_user_func($class, $this)); 565 | } else { 566 | $this->add(new $class()); 567 | } 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /src/Command/AssetsPullCommand.php: -------------------------------------------------------------------------------- 1 | $value) { 100 | if (!$isDest && $key === 'basePath' && $this->option('remote-base-path')) { 101 | $value = $this->option('remote-base-path'); 102 | } 103 | 104 | $str = str_replace('{'.$key.'}', $value, $str); 105 | } 106 | } 107 | 108 | return $str; 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | protected function fire() 115 | { 116 | $config = require $this->configPath.'general.php'; 117 | 118 | if (! isset($config['*'])) { 119 | return $this->fail('Could not find a multi-environment configuration in your general.php.'); 120 | } 121 | 122 | $remoteEnvironment = $this->argument('remote-environment'); 123 | 124 | $localConfig = $remoteConfig = $config['*']; 125 | 126 | if (isset($config[$this->environment])) { 127 | $localConfig = array_merge($localConfig, $config[$this->environment]); 128 | } 129 | 130 | if (isset($config[$remoteEnvironment])) { 131 | $remoteConfig = array_merge($remoteConfig, $config[$remoteEnvironment]); 132 | } 133 | 134 | $this->debug = $this->option('debug'); 135 | 136 | if ($this->option('ssh-host')) { 137 | $this->sshCredentials = [ 138 | 'host' => $this->option('ssh-host'), 139 | ]; 140 | 141 | if ($this->option('ssh-user')) { 142 | $this->sshCredentials['user'] = $this->option('ssh-user'); 143 | } 144 | 145 | if ($this->option('ssh-port')) { 146 | $this->sshCredentials['port'] = $this->option('ssh-port'); 147 | } 148 | 149 | if ($this->option('ssh-identity-file')) { 150 | $this->sshCredentials['identityFile'] = $this->option('ssh-identity-file'); 151 | } 152 | } 153 | 154 | // test the ssh credentials 155 | if ($this->sshCredentials) { 156 | $this->info('Testing SSH credentials...'); 157 | 158 | if (! $this->testSshCredentials()) { 159 | return $this->fail('Could not connect to remote server via SSH.'); 160 | } 161 | } 162 | 163 | $assetSources = array_filter( 164 | $this->craft->assetSources->getAllSources(), 165 | function ($source) { 166 | return $source->getSourceType() instanceof LocalAssetSourceType; 167 | } 168 | ); 169 | 170 | $this->info('Pulling remote assets...'); 171 | 172 | foreach ($assetSources as $source) { 173 | $path = $source->getSourceType()->getSettings()->path; 174 | 175 | $src = $this->parseEnvironmentVariables($path, $remoteConfig); 176 | 177 | $dest = $this->parseEnvironmentVariables($path, $localConfig, true); 178 | 179 | $command = (string) $this->makeRsyncCommand($src, $dest); 180 | 181 | if ($this->debug) { 182 | $this->output->writeln($command); 183 | } else { 184 | passthru($command); 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Make an SSH command object 191 | * @param string $command Command to execute over SSH 192 | * @return \CraftCli\Support\SshCommand 193 | */ 194 | protected function makeSshCommand($command) 195 | { 196 | $sshCommand = new SshCommand($this->sshCredentials['host'], $command); 197 | 198 | if (! empty($this->sshCredentials['user'])) { 199 | $sshCommand->user = $this->sshCredentials['user']; 200 | } 201 | 202 | if (! empty($this->sshCredentials['port'])) { 203 | $sshCommand->port = $this->sshCredentials['port']; 204 | } 205 | 206 | if (! empty($this->sshCredentials['identityFile'])) { 207 | $sshCommand->identityFile = $this->sshCredentials['identityFile']; 208 | } 209 | 210 | return $sshCommand; 211 | } 212 | 213 | /** 214 | * Make an rsync command object 215 | * @param string $command Command to execute over SSH 216 | * @return \CraftCli\Support\RsyncCommand 217 | */ 218 | protected function makeRsyncCommand($src, $dest) 219 | { 220 | $command = new RsyncCommand($src, $dest); 221 | 222 | if (!empty($this->sshCredentials['host'])) { 223 | $command->srcHost = $this->sshCredentials['host']; 224 | } 225 | 226 | if (!empty($this->sshCredentials['user'])) { 227 | $command->srcUser = $this->sshCredentials['user']; 228 | } 229 | 230 | if (!empty($this->sshCredentials['port'])) { 231 | $command->port = $this->sshCredentials['port']; 232 | } 233 | 234 | if (!empty($this->sshCredentials['identityFile'])) { 235 | $command->identityFile = $this->sshCredentials['identityFile']; 236 | } 237 | 238 | return $command; 239 | } 240 | 241 | /** 242 | * Test SSH credentials 243 | * @return boolean 244 | */ 245 | protected function testSshCredentials() 246 | { 247 | $sshCommand = (string) $this->makeSshCommand('echo "foo"'); 248 | 249 | if ($this->debug) { 250 | $this->output->writeln($sshCommand); 251 | 252 | return true; 253 | } 254 | 255 | exec($sshCommand, $output, $status); 256 | 257 | return $status === 0; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Command/AssetsPushCommand.php: -------------------------------------------------------------------------------- 1 | $value) { 100 | if ($isDest && $key === 'basePath' && $this->option('remote-base-path')) { 101 | $value = $this->option('remote-base-path'); 102 | } 103 | 104 | $str = str_replace('{'.$key.'}', $value, $str); 105 | } 106 | } 107 | 108 | return $str; 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | protected function fire() 115 | { 116 | $config = require $this->configPath.'general.php'; 117 | 118 | if (! isset($config['*'])) { 119 | return $this->fail('Could not find a multi-environment configuration in your general.php.'); 120 | } 121 | 122 | $remoteEnvironment = $this->argument('remote-environment'); 123 | 124 | $localConfig = $remoteConfig = $config['*']; 125 | 126 | if (isset($config[$this->environment])) { 127 | $localConfig = array_merge($localConfig, $config[$this->environment]); 128 | } 129 | 130 | if (isset($config[$remoteEnvironment])) { 131 | $remoteConfig = array_merge($remoteConfig, $config[$remoteEnvironment]); 132 | } 133 | 134 | $this->debug = $this->option('debug'); 135 | 136 | if ($this->option('ssh-host')) { 137 | $this->sshCredentials = [ 138 | 'host' => $this->option('ssh-host'), 139 | ]; 140 | 141 | if ($this->option('ssh-user')) { 142 | $this->sshCredentials['user'] = $this->option('ssh-user'); 143 | } 144 | 145 | if ($this->option('ssh-port')) { 146 | $this->sshCredentials['port'] = $this->option('ssh-port'); 147 | } 148 | 149 | if ($this->option('ssh-identity-file')) { 150 | $this->sshCredentials['identityFile'] = $this->option('ssh-identity-file'); 151 | } 152 | } 153 | 154 | // test the ssh credentials 155 | if ($this->sshCredentials) { 156 | $this->info('Testing SSH credentials...'); 157 | 158 | if (! $this->testSshCredentials()) { 159 | return $this->fail('Could not connect to remote server via SSH.'); 160 | } 161 | } 162 | 163 | $assetSources = array_filter( 164 | $this->craft->assetSources->getAllSources(), 165 | function ($source) { 166 | return $source->getSourceType() instanceof LocalAssetSourceType; 167 | } 168 | ); 169 | 170 | $this->info('Pushing local assets...'); 171 | 172 | foreach ($assetSources as $source) { 173 | $path = $source->getSourceType()->getSettings()->path; 174 | 175 | $src = $this->parseEnvironmentVariables($path, $localConfig); 176 | 177 | $dest = $this->parseEnvironmentVariables($path, $remoteConfig, true); 178 | 179 | $command = (string) $this->makeRsyncCommand($src, $dest); 180 | 181 | if ($this->debug) { 182 | $this->output->writeln($command); 183 | } else { 184 | passthru($command); 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Make an SSH command object 191 | * @param string $command Command to execute over SSH 192 | * @return \CraftCli\Support\SshCommand 193 | */ 194 | protected function makeSshCommand($command) 195 | { 196 | $sshCommand = new SshCommand($this->sshCredentials['host'], $command); 197 | 198 | if (! empty($this->sshCredentials['user'])) { 199 | $sshCommand->user = $this->sshCredentials['user']; 200 | } 201 | 202 | if (! empty($this->sshCredentials['port'])) { 203 | $sshCommand->port = $this->sshCredentials['port']; 204 | } 205 | 206 | if (! empty($this->sshCredentials['identityFile'])) { 207 | $sshCommand->identityFile = $this->sshCredentials['identityFile']; 208 | } 209 | 210 | return $sshCommand; 211 | } 212 | 213 | /** 214 | * Make an rsync command object 215 | * @param string $command Command to execute over SSH 216 | * @return \CraftCli\Support\RsyncCommand 217 | */ 218 | protected function makeRsyncCommand($src, $dest) 219 | { 220 | $command = new RsyncCommand($src, $dest); 221 | 222 | if (!empty($this->sshCredentials['host'])) { 223 | $command->destHost = $this->sshCredentials['host']; 224 | } 225 | 226 | if (!empty($this->sshCredentials['user'])) { 227 | $command->destUser = $this->sshCredentials['user']; 228 | } 229 | 230 | if (!empty($this->sshCredentials['port'])) { 231 | $command->port = $this->sshCredentials['port']; 232 | } 233 | 234 | if (!empty($this->sshCredentials['identityFile'])) { 235 | $command->identityFile = $this->sshCredentials['identityFile']; 236 | } 237 | 238 | return $command; 239 | } 240 | 241 | /** 242 | * Test SSH credentials 243 | * @return boolean 244 | */ 245 | protected function testSshCredentials() 246 | { 247 | $sshCommand = (string) $this->makeSshCommand('echo "foo"'); 248 | 249 | if ($this->debug) { 250 | $this->output->writeln($sshCommand); 251 | 252 | return true; 253 | } 254 | 255 | exec($sshCommand, $output, $status); 256 | 257 | return $status === 0; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Command/ClearCacheCommand.php: -------------------------------------------------------------------------------- 1 | craft->getComponent( 54 | 'components' 55 | )->getComponentByTypeAndClass(ComponentType::Tool, 'ClearCaches'); 56 | 57 | $reflectionMethod = new ReflectionMethod($tool, '_getFolders'); 58 | $reflectionMethod->setAccessible(true); 59 | 60 | $values = $reflectionMethod->invoke($tool); 61 | $values['assetTransformIndex'] = Craft::t('Asset transform index'); 62 | $values['assetIndexingData'] = Craft::t('Asset indexing data'); 63 | $values['templateCaches'] = Craft::t('Template caches'); 64 | 65 | $keys = array_keys($values); 66 | $options = array_values($values); 67 | 68 | 69 | if ($this->option('select')) { 70 | 71 | $questionHelper = $this->getHelperSet()->has('question') 72 | ? $this->getHelperSet()->get('question') 73 | : $this->getHelperSet()->get('dialog'); 74 | 75 | if ($questionHelper instanceof QuestionHelper) { 76 | $question = new ChoiceQuestion( 77 | 'Select which caches to clear (separate multiple by comma)', 78 | $options, 79 | 0 80 | ); 81 | $question->setMultiselect(true); 82 | $selected = $questionHelper->ask( 83 | $this->input, 84 | $this->output, 85 | $question 86 | ); 87 | } else { 88 | $selected = $questionHelper->select( 89 | $this->output, 90 | 'Select which caches to clear (separate multiple by comma)', 91 | $options, 92 | null, 93 | false, 94 | 'Value "%s" is invalid', 95 | true 96 | ); 97 | } 98 | 99 | $caches = array(); 100 | 101 | foreach ($selected as $index => $name) { 102 | $caches[] = $keys[$index]; 103 | } 104 | } 105 | 106 | if ($this->option('caches')) { 107 | $selected = array_intersect($options, $this->option('caches')); 108 | $caches = array(); 109 | foreach ($selected as $index => $name) { 110 | $caches[] = $keys[$index]; 111 | } 112 | } 113 | 114 | 115 | $this->suppressOutput( 116 | function () use ($tool, $caches) { 117 | $tool->performAction(compact('caches')); 118 | } 119 | ); 120 | 121 | $this->info('Cache(s) cleared.'); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Command/ConsoleCommand.php: -------------------------------------------------------------------------------- 1 | run(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Command/DbBackupCommand.php: -------------------------------------------------------------------------------- 1 | craft/storage.'; 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | protected function getArguments() 23 | { 24 | return array( 25 | array( 26 | 'path', 27 | InputArgument::OPTIONAL, 28 | 'Specify a path to write the backup to. Defaults to craft/storage/backups.', 29 | ), 30 | ); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function fire() 37 | { 38 | $db = $this->craft->getComponent('db'); 39 | 40 | $path = $this->suppressOutput([$db, 'backup']); 41 | 42 | $path = preg_replace('/^'.preg_quote(getcwd().DIRECTORY_SEPARATOR, '/').'/', '.'.DIRECTORY_SEPARATOR, $path); 43 | 44 | if ($this->argument('path')) { 45 | copy($path, $this->argument('path')); 46 | 47 | $path = $this->argument('path'); 48 | } 49 | 50 | $this->info(sprintf('Backup %s created.', $path)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/DbCreateCommand.php: -------------------------------------------------------------------------------- 1 | comment('Testing database credentials...'); 114 | 115 | if (! $this->testDbCredentials()) { 116 | throw new Exception('Could not connect to local mysql database.'); 117 | } 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | protected function fire() 124 | { 125 | $this->debug = $this->option('debug'); 126 | 127 | $name = $this->argument('db-name'); 128 | 129 | $this->dbCredentials = array( 130 | 'host' => $this->option('host'), 131 | 'port' => $this->option('port'), 132 | 'name' => $name, 133 | 'user' => $this->option('user'), 134 | 'password' => $this->option('password'), 135 | 'adminUser' => $this->option('admin-user'), 136 | 'adminPassword' => $this->option('admin-password'), 137 | ); 138 | 139 | try { 140 | $this->validate(); 141 | } catch (Exception $e) { 142 | return $this->fail($e->getMessage()); 143 | } 144 | 145 | if ($this->testDbExists()) { 146 | return $this->fail(sprintf('Database %s already exists.', $name)); 147 | } 148 | 149 | $this->comment(sprintf('Creating database %s...', $name)); 150 | 151 | if (! $this->createDb()) { 152 | return $this->fail(sprintf('Failed to create database %s.', $name)); 153 | } 154 | 155 | $this->info(sprintf('Database %s created.', $name)); 156 | } 157 | 158 | /** 159 | * Make a MysqlCommand object 160 | * @param string $query 161 | * @return \CraftCli\Support\MysqlCommand 162 | */ 163 | protected function makeMysqlCommand($query) 164 | { 165 | $mysqlCommand = new MysqlCommand(); 166 | 167 | if (! empty($this->dbCredentials['host'])) { 168 | $mysqlCommand->host = $this->dbCredentials['host']; 169 | } 170 | 171 | if (! empty($this->dbCredentials['adminUser'])) { 172 | $mysqlCommand->user = $this->dbCredentials['adminUser']; 173 | } else if (! empty($this->dbCredentials['user'])) { 174 | $mysqlCommand->user = $this->dbCredentials['user']; 175 | } 176 | 177 | if (! empty($this->dbCredentials['adminPassword'])) { 178 | $mysqlCommand->password = $this->dbCredentials['adminPassword']; 179 | } else if (! empty($this->dbCredentials['password'])) { 180 | $mysqlCommand->password = $this->dbCredentials['password']; 181 | } 182 | 183 | if (! empty($this->dbCredentials['port'])) { 184 | $mysqlCommand->port = $this->dbCredentials['port']; 185 | } 186 | 187 | if ($query) { 188 | $mysqlCommand->query = $query; 189 | } 190 | 191 | return $mysqlCommand; 192 | } 193 | 194 | /** 195 | * Make a MysqlCreateDatabaseCommand object 196 | * @return \CraftCli\Support\MysqlCreateDatabaseCommand 197 | */ 198 | protected function makeMysqlCreateDatabaseCommand() 199 | { 200 | $mysqlCommand = new MysqlCreateDatabaseCommand(); 201 | 202 | if (! empty($this->dbCredentials['host'])) { 203 | $mysqlCommand->host = $this->dbCredentials['host']; 204 | } 205 | 206 | if (! empty($this->dbCredentials['adminUser'])) { 207 | $mysqlCommand->adminUser = $this->dbCredentials['adminUser']; 208 | } 209 | 210 | if (! empty($this->dbCredentials['adminPassword'])) { 211 | $mysqlCommand->adminPassword = $this->dbCredentials['adminPassword']; 212 | } 213 | 214 | if (! empty($this->dbCredentials['user'])) { 215 | $mysqlCommand->user = $this->dbCredentials['user']; 216 | } 217 | 218 | if (! empty($this->dbCredentials['password'])) { 219 | $mysqlCommand->password = $this->dbCredentials['password']; 220 | } 221 | 222 | if (! empty($this->dbCredentials['port'])) { 223 | $mysqlCommand->port = $this->dbCredentials['port']; 224 | } 225 | 226 | if (! empty($this->dbCredentials['name'])) { 227 | $mysqlCommand->name = $this->dbCredentials['name']; 228 | } 229 | 230 | return $mysqlCommand; 231 | } 232 | 233 | /** 234 | * Test database credentials 235 | * @return boolean 236 | */ 237 | protected function testDbCredentials() 238 | { 239 | $mysqlCommand = (string) $this->makeMysqlCommand('SHOW DATABASES'); 240 | 241 | if ($this->debug) { 242 | $this->output->writeln($mysqlCommand); 243 | 244 | return true; 245 | } 246 | 247 | exec($mysqlCommand, $output, $status); 248 | 249 | return $status === 0; 250 | } 251 | 252 | /** 253 | * Test database credentials 254 | * @return boolean 255 | */ 256 | protected function testDbExists() 257 | { 258 | $mysqlCommand = $this->makeMysqlCommand("SHOW DATABASES LIKE '{$this->dbCredentials['name']}'"); 259 | 260 | $mysqlCommand->flags[] = '--batch'; 261 | $mysqlCommand->flags[] = '--skip-column-names'; 262 | $mysqlCommand->grep = $this->dbCredentials['name']; 263 | 264 | if ($this->debug) { 265 | $this->output->writeln((string) $mysqlCommand); 266 | 267 | return true; 268 | } 269 | 270 | exec($mysqlCommand, $output, $status); 271 | 272 | return !! $output; 273 | } 274 | 275 | /** 276 | * Create database 277 | * @return boolean 278 | */ 279 | protected function createDb() 280 | { 281 | $mysqlCommand = (string) $this->makeMysqlCreateDatabaseCommand(); 282 | 283 | if ($this->debug) { 284 | $this->output->writeln($mysqlCommand); 285 | 286 | return true; 287 | } 288 | 289 | exec($mysqlCommand, $output, $status); 290 | 291 | return $status === 0; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Command/DbPullCommand.php: -------------------------------------------------------------------------------- 1 | configPath.'db.php'; 155 | 156 | if (! isset($dbConfig['*'])) { 157 | throw new Exception('Could not find a multi-environment configuration in your db.php.'); 158 | } 159 | 160 | $remoteEnvironment = $this->argument('remote-environment'); 161 | 162 | $this->localCredentials = $this->remoteCredentials = $dbConfig['*']; 163 | 164 | if (isset($dbConfig[$this->environment])) { 165 | $this->localCredentials = array_merge($this->localCredentials, $dbConfig[$this->environment]); 166 | } 167 | 168 | if (isset($dbConfig[$remoteEnvironment])) { 169 | $this->remoteCredentials = array_merge($this->remoteCredentials, $dbConfig[$remoteEnvironment]); 170 | } 171 | 172 | if ($this->option('remote-host')) { 173 | $this->remoteCredentials['server'] = $this->option('remote-host'); 174 | } 175 | 176 | if ($this->option('remote-database')) { 177 | $this->remoteCredentials['database'] = $this->option('remote-database'); 178 | } 179 | 180 | if ($this->option('remote-user')) { 181 | $this->remoteCredentials['user'] = $this->option('remote-user'); 182 | } 183 | 184 | if ($this->option('remote-password')) { 185 | $this->remoteCredentials['password'] = $this->option('remote-password'); 186 | } 187 | 188 | if ($this->option('remote-port')) { 189 | $this->remoteCredentials['port'] = $this->option('remote-port'); 190 | } 191 | 192 | $this->debug = $this->option('debug'); 193 | 194 | if (! $this->debug && ! $this->option('force') && ! $this->confirm('This will overwrite your local database. Are you sure you want to continue?')) { 195 | throw new Exception('Transfer cancelled.'); 196 | } 197 | 198 | if ($this->option('ssh-host')) { 199 | $this->sshCredentials = [ 200 | 'host' => $this->option('ssh-host'), 201 | ]; 202 | 203 | if ($this->option('ssh-user')) { 204 | $this->sshCredentials['user'] = $this->option('ssh-user'); 205 | } 206 | 207 | if ($this->option('ssh-port')) { 208 | $this->sshCredentials['port'] = $this->option('ssh-port'); 209 | } 210 | 211 | if ($this->option('ssh-identity-file')) { 212 | $this->sshCredentials['identityFile'] = $this->option('ssh-identity-file'); 213 | } 214 | } 215 | 216 | // test the local db credentials 217 | $this->info('Testing local database credentials...'); 218 | 219 | if (! $this->testLocalCredentials()) { 220 | throw new Exception('Could not connect to local mysql database.'); 221 | } 222 | 223 | // test the ssh credentials 224 | if ($this->sshCredentials) { 225 | $this->info('Testing SSH credentials...'); 226 | 227 | if (! $this->testSshCredentials()) { 228 | throw new Exception('Could not connect to remote server via SSH.'); 229 | } 230 | } 231 | 232 | // test the remote db credentials 233 | $this->info('Testing remote database credentials...'); 234 | 235 | if (! $this->testRemoteCredentials()) { 236 | throw new Exception('Could not connect to remote mysql database.'); 237 | } 238 | } 239 | 240 | /** 241 | * {@inheritdoc} 242 | */ 243 | protected function fire() 244 | { 245 | try { 246 | $this->validate(); 247 | } catch (Exception $e) { 248 | return $this->fail($e->getMessage()); 249 | } 250 | 251 | $mysqlCommand = (string) $this->makeMysqlCommand(MysqlCommand::class, $this->localCredentials); 252 | 253 | $remoteCommand = (string) $this->makeMysqlCommand(MysqlDumpCommand::class, $this->remoteCredentials); 254 | 255 | if (! $this->option('no-gzip')) { 256 | $remoteCommand .= ' | gzip'; 257 | 258 | $mysqlCommand = 'gunzip | '.$mysqlCommand; 259 | } 260 | 261 | if ($this->sshCredentials) { 262 | $remoteCommand = $this->makeSshCommand($remoteCommand); 263 | } 264 | 265 | $command = "$remoteCommand | $mysqlCommand"; 266 | 267 | $this->info('Fetching remote database...'); 268 | 269 | if ($this->debug) { 270 | $this->output->writeln($command); 271 | } else { 272 | passthru($command); 273 | } 274 | } 275 | 276 | /** 277 | * Make an SSH command object 278 | * @param string $command Command to execute over SSH 279 | * @return \CraftCli\Support\SshCommand 280 | */ 281 | protected function makeSshCommand($command) 282 | { 283 | $sshCommand = new SshCommand($this->sshCredentials['host'], $command); 284 | 285 | if (! empty($this->sshCredentials['user'])) { 286 | $sshCommand->user = $this->sshCredentials['user']; 287 | } 288 | 289 | if (! empty($this->sshCredentials['port'])) { 290 | $sshCommand->port = $this->sshCredentials['port']; 291 | } 292 | 293 | if (! empty($this->sshCredentials['identityFile'])) { 294 | $sshCommand->identityFile = $this->sshCredentials['identityFile']; 295 | } 296 | 297 | return $sshCommand; 298 | } 299 | 300 | /** 301 | * Make a MysqlCommand object 302 | * @param string $class 303 | * @param array $credentials 304 | * @param string $query 305 | * @return \CraftCli\Support\AbstractMysqlCommand 306 | */ 307 | protected function makeMysqlCommand($class, $credentials, $query = null) 308 | { 309 | $mysqlCommand = new $class($credentials['database']); 310 | 311 | if (! empty($credentials['server'])) { 312 | $mysqlCommand->host = $credentials['server']; 313 | } 314 | 315 | if (! empty($credentials['user'])) { 316 | $mysqlCommand->user = $credentials['user']; 317 | } 318 | 319 | if (! empty($credentials['password'])) { 320 | $mysqlCommand->password = $credentials['password']; 321 | } 322 | 323 | if (! empty($credentials['port'])) { 324 | $mysqlCommand->port = $credentials['port']; 325 | } 326 | 327 | if ($query) { 328 | $mysqlCommand->query = $query; 329 | } 330 | 331 | return $mysqlCommand; 332 | } 333 | 334 | /** 335 | * Test local database credentials 336 | * @return boolean 337 | */ 338 | protected function testLocalCredentials() 339 | { 340 | $mysqlCommand = (string) $this->makeMysqlCommand(MysqlCommand::class, $this->localCredentials, 'SHOW TABLES'); 341 | 342 | if ($this->debug) { 343 | $this->output->writeln($mysqlCommand); 344 | 345 | return true; 346 | } 347 | 348 | exec($mysqlCommand, $output, $status); 349 | 350 | return $status === 0; 351 | } 352 | 353 | /** 354 | * Test SSH credentials 355 | * @return boolean 356 | */ 357 | protected function testSshCredentials() 358 | { 359 | $sshCommand = (string) $this->makeSshCommand('echo "foo"'); 360 | 361 | if ($this->debug) { 362 | $this->output->writeln($sshCommand); 363 | 364 | return true; 365 | } 366 | 367 | exec($sshCommand, $output, $status); 368 | 369 | return $status === 0; 370 | } 371 | 372 | /** 373 | * Test remote database credentials 374 | * @return boolean 375 | */ 376 | protected function testRemoteCredentials() 377 | { 378 | $remoteCommand = (string) $this->makeMysqlCommand(MysqlCommand::class, $this->remoteCredentials, 'SHOW TABLES'); 379 | 380 | if ($this->sshCredentials) { 381 | $remoteCommand = (string) $this->makeSshCommand($remoteCommand); 382 | } 383 | 384 | if ($this->debug) { 385 | $this->output->writeln($remoteCommand); 386 | 387 | return true; 388 | } 389 | 390 | exec($remoteCommand, $output, $status); 391 | 392 | return $status === 0; 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/Command/DbPushCommand.php: -------------------------------------------------------------------------------- 1 | validate(); 31 | } catch (Exception $e) { 32 | return $this->fail($e->getMessage()); 33 | } 34 | 35 | $mysqlDumpCommand = (string) $this->makeMysqlCommand(MysqlDumpCommand::class, $this->localCredentials); 36 | 37 | $remoteCommand = (string) $this->makeMysqlCommand(MysqlCommand::class, $this->remoteCredentials); 38 | 39 | if (! $this->option('no-gzip')) { 40 | $mysqlDumpCommand .= ' | gzip'; 41 | 42 | $remoteCommand = 'gunzip | '.$remoteCommand; 43 | } 44 | 45 | if ($this->sshCredentials) { 46 | $remoteCommand = $this->makeSshCommand($remoteCommand); 47 | } 48 | 49 | $command = "$mysqlDumpCommand | $remoteCommand"; 50 | 51 | $this->info('Pushing local database...'); 52 | 53 | if ($this->debug) { 54 | $this->output->writeln($command); 55 | } else { 56 | passthru($command); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Command/DbRestoreCommand.php: -------------------------------------------------------------------------------- 1 | configPath.'db.php'; 77 | 78 | if (isset($dbConfig['*'])) { 79 | $this->credentials = $dbConfig['*']; 80 | 81 | if (isset($dbConfig[$this->environment])) { 82 | $this->credentials = array_merge($this->credentials, $dbConfig[$this->environment]); 83 | } 84 | } else { 85 | $this->credentials = $dbConfig; 86 | } 87 | 88 | $this->debug = $this->option('debug'); 89 | 90 | if (! $this->debug && ! $this->option('force') && ! $this->confirm('This will overwrite your local database. Are you sure you want to continue?')) { 91 | throw new Exception('Transfer cancelled.'); 92 | } 93 | 94 | // test the db credentials 95 | $this->info('Testing database credentials...'); 96 | 97 | if (! $this->testCredentials()) { 98 | throw new Exception('Could not connect to mysql database.'); 99 | } 100 | } 101 | 102 | /** 103 | * Test local database credentials 104 | * @return boolean 105 | */ 106 | protected function testCredentials() 107 | { 108 | $mysqlCommand = (string) $this->makeMysqlCommand(MysqlCommand::class, $this->credentials, 'SHOW TABLES'); 109 | 110 | if ($this->debug) { 111 | $this->output->writeln($mysqlCommand); 112 | 113 | return true; 114 | } 115 | 116 | exec($mysqlCommand, $output, $status); 117 | 118 | return $status === 0; 119 | } 120 | 121 | /** 122 | * Make a MysqlCommand object 123 | * @param string $class 124 | * @param array $credentials 125 | * @param string $query 126 | * @return \CraftCli\Support\AbstractMysqlCommand 127 | */ 128 | protected function makeMysqlCommand($class, $credentials, $query = null) 129 | { 130 | $mysqlCommand = new $class($credentials['database']); 131 | 132 | if (! empty($credentials['server'])) { 133 | $mysqlCommand->host = $credentials['server']; 134 | } 135 | 136 | if (! empty($credentials['user'])) { 137 | $mysqlCommand->user = $credentials['user']; 138 | } 139 | 140 | if (! empty($credentials['password'])) { 141 | $mysqlCommand->password = $credentials['password']; 142 | } 143 | 144 | if (! empty($credentials['port'])) { 145 | $mysqlCommand->port = $credentials['port']; 146 | } 147 | 148 | if ($query) { 149 | $mysqlCommand->query = $query; 150 | } 151 | 152 | return $mysqlCommand; 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | protected function fire() 159 | { 160 | try { 161 | $this->validate(); 162 | } catch (Exception $e) { 163 | return $this->fail($e->getMessage()); 164 | } 165 | 166 | if ($this->argument('path')) { 167 | $path = $this->argument('path'); 168 | 169 | if (!file_exists($path)) { 170 | return $this->fail(sprintf('File %s not found.', $path)); 171 | } 172 | // find the latest backup 173 | } else { 174 | $files = glob(CRAFT_STORAGE_PATH.'backups/*.sql'); 175 | 176 | if (!$files) { 177 | return $this->fail('No backups found in craft/storage/backups.'); 178 | } 179 | 180 | // sort by latest 181 | usort($files, function($a, $b) { 182 | $a = filemtime($a); 183 | $b = filemtime($b); 184 | 185 | if ($a === $b) { 186 | return 0; 187 | } 188 | 189 | return $a > $b ? -1 : 1; 190 | }); 191 | 192 | $path = array_shift($files); 193 | } 194 | 195 | $mysqlCommand = (string) $this->makeMysqlCommand(MysqlCommand::class, $this->credentials); 196 | 197 | $command = "$mysqlCommand < $path"; 198 | 199 | $this->info('Restoring database...'); 200 | 201 | if ($this->debug) { 202 | $this->output->writeln($command); 203 | } else { 204 | passthru($command); 205 | } 206 | 207 | $this->info('Database restored.'); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Command/DownloadCraftCommand.php: -------------------------------------------------------------------------------- 1 | getPath(); 80 | 81 | if (! is_dir($path) && ! @mkdir($path, 0777, true)) { 82 | return $this->fail(sprintf('Could not create directory %s.', $path)); 83 | } 84 | 85 | // check terms and conditions 86 | if (! $this->option('terms') && ($this->option('no-prompt') || ! $this->confirm('I agree to the terms and conditions (https://buildwithcraft.com/license)'))) { 87 | return $this->fail('You did not agree to the terms and conditions (https://buildwithcraft.com/license)'); 88 | } 89 | 90 | // check if craft is already installed, and overwrite option 91 | if (file_exists($path.'/craft') && ! $this->option('overwrite')) { 92 | $this->error('Craft is already installed here!'); 93 | 94 | if ($this->option('no-prompt') || ! $this->confirm('Do you want to overwrite?')) { 95 | $this->info('Skipped download.'); 96 | 97 | return; 98 | } 99 | } 100 | 101 | $url = 'https://craftcms.com/latest-v2.tar.gz'; 102 | 103 | $this->comment('Downloading...'); 104 | 105 | $downloader = new TempDownloader($url, '.tar.gz'); 106 | 107 | $downloader->setOutput($this->output); 108 | 109 | try { 110 | $filePath = $downloader->download(); 111 | } catch (Exception $e) { 112 | return $this->fail($e->getMessage()); 113 | } 114 | 115 | $this->comment('Extracting...'); 116 | 117 | $tarExtractor = new TarExtractor($filePath, $path); 118 | 119 | $tarExtractor->extract(); 120 | 121 | // Rename .htaccess 122 | rename($path.'/public/htaccess', $path.'/public/.htaccess'); 123 | 124 | // change the name of the public folder 125 | if ($public = $this->option('public')) { 126 | rename($path.'/public', $path.'/'.$public); 127 | } 128 | 129 | $this->info('Download complete!'); 130 | } 131 | 132 | /** 133 | * Get download/install path 134 | * @return string 135 | */ 136 | protected function getPath() 137 | { 138 | $path = rtrim($this->argument('path'), DIRECTORY_SEPARATOR) ?: getcwd(); 139 | 140 | if (!preg_match('#^(\.|/|([A-Z]:\\\\))#i', $path)) { 141 | $path = '.'.DIRECTORY_SEPARATOR.$path; 142 | } 143 | 144 | return $path; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Command/ExemptFromBootstrapInterface.php: -------------------------------------------------------------------------------- 1 | argument('command_name'); 68 | $commandDescription = $this->option('description'); 69 | $namespace = $this->option('namespace'); 70 | 71 | // where to create the file, default to current directory 72 | $path = $this->argument('path') ?: '.'; 73 | 74 | if (!is_dir($path)) { 75 | mkdir($path, 0777, true); 76 | } 77 | 78 | // make sure it has a trailing slash 79 | $path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; 80 | 81 | // split command into individual words 82 | $words = preg_split('/[:_-]/', $commandName); 83 | 84 | // camel case 85 | $words = array_map(function ($word) { 86 | return mb_strtoupper(mb_substr($word, 0, 1)).mb_substr($word, 1); 87 | }, $words); 88 | 89 | $className = implode('', $words); 90 | 91 | $handlebars = new Handlebars(array( 92 | 'loader' => new FilesystemLoader(__DIR__.'/../templates/'), 93 | )); 94 | 95 | $destination = $path.$className.'Command.php'; 96 | 97 | $handle = fopen($destination, 'w'); 98 | 99 | $namespace = trim($namespace, '\\'); 100 | 101 | $output = $handlebars->render('Command.php', array( 102 | 'className' => $className, 103 | 'commandName' => $commandName, 104 | 'commandDescription' => $commandDescription, 105 | 'namespace' => $namespace, 106 | )); 107 | 108 | fwrite($handle, $output); 109 | 110 | fclose($handle); 111 | 112 | $this->info($destination.' created.'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Command/InitCommand.php: -------------------------------------------------------------------------------- 1 | confirm('A configuration file already exists. Do you want to overwrite? y[n]', false); 26 | 27 | if (! $confirmed) { 28 | return; 29 | } 30 | } 31 | 32 | $copy = copy(__DIR__.'/../../sample.craft-cli.php', $file); 33 | 34 | if ($copy === false) { 35 | return $this->fail('Could not create the file.'); 36 | } 37 | 38 | $this->info('Configuration file created.'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Command/InstallCraftCommand.php: -------------------------------------------------------------------------------- 1 | validateOptions(); 138 | 139 | if ($errors) { 140 | if ($this->option('no-prompt')) { 141 | foreach ($errors as $error) { 142 | $this->error($error); 143 | } 144 | 145 | return 1; 146 | } 147 | 148 | $this->promptForOptions(); 149 | } 150 | 151 | // Create database 152 | if ($this->option('create-db')) { 153 | $command = $this->getApplication()->find('db:create'); 154 | 155 | $arguments = array( 156 | 'command' => 'db:create', 157 | 'db-name' => $this->option('db-name'), 158 | '--host' => $this->option('db-host'), 159 | '--port' => $this->option('db-port'), 160 | '--user' => $this->option('db-user'), 161 | '--password' => $this->option('db-password'), 162 | '--admin-user' => $this->option('db-admin-user'), 163 | '--admin-password' => $this->option('db-admin-password'), 164 | ); 165 | 166 | $inputs = new ArrayInput($arguments); 167 | 168 | if ($command->run($inputs, $this->output) !== 0) { 169 | throw new Exception('Command db:create failed.'); 170 | } 171 | } 172 | 173 | parent::fire(); 174 | 175 | $path = $this->getPath(); 176 | 177 | // 3. Install Craft CMS 178 | // Creates a dummy license.key file for validations to pass. 179 | // This key needs to be deleted after installation. Otherwise, Craft 180 | // update and licensing functions won't work properly. 181 | // Creates a db.config in craft/config 182 | touch($path.'/craft/config/license.key'); 183 | 184 | $this->generateDbConfig(); 185 | 186 | chdir($path); 187 | 188 | $this->getApplication()->setCraftPath($path.'/craft'); 189 | 190 | $craft = $this->getApplication()->bootstrap(true); 191 | 192 | // The SERVER_NAME needs to defined for intallation to pass 193 | if (!isset($_SERVER['SERVER_NAME'])) { 194 | $_SERVER['SERVER_NAME'] = 'Craft'; 195 | } 196 | 197 | $craft->config->set('usePathInfo', false); 198 | $craft->config->set('omitScriptNameInUrls', true); 199 | 200 | $installerParams = array( 201 | 'username' => $this->option('admin-user'), 202 | 'email' => $this->option('admin-email'), 203 | 'password' => $this->option('admin-password'), 204 | 'siteName' => $this->option('site-name'), 205 | 'siteUrl' => $this->option('site-url'), 206 | 'locale' => $this->option('locale'), 207 | ); 208 | 209 | $this->comment('Installing Craft...'); 210 | 211 | $craft->setIsNotInstallingAfterNextCheck(); 212 | 213 | $this->suppressOutput(function () use ($craft, $installerParams) { 214 | $craft->install->run($installerParams); 215 | }); 216 | 217 | unlink($path.'/craft/config/license.key'); 218 | 219 | $this->info('Craft installed.'); 220 | } 221 | 222 | protected function validateOptions() 223 | { 224 | $errors = []; 225 | 226 | $requiredOptions = array( 227 | 'db-host', 228 | 'db-name', 229 | 'db-user', 230 | 'db-password', 231 | 'site-name', 232 | 'site-url', 233 | 'admin-user', 234 | 'admin-password', 235 | 'admin-email', 236 | 'locale', 237 | ); 238 | 239 | foreach ($requiredOptions as $option) { 240 | if (!$this->option($option)) { 241 | $errors[] = sprintf('--%s is required', $option); 242 | } 243 | } 244 | 245 | return $errors; 246 | } 247 | 248 | protected function promptForOption($question, $option, $allowEmpty = false) 249 | { 250 | do { 251 | $question = new Question($question, $this->option($option)); 252 | 253 | if ($allowEmpty) { 254 | $question->setValidator(function ($value) { 255 | return $value; 256 | }); 257 | } 258 | 259 | $value = $this->output->askQuestion($question, $this->option($option)); 260 | } while (!$value && !$allowEmpty); 261 | 262 | $this->input->setOption($option, $value); 263 | 264 | return $value; 265 | } 266 | 267 | protected function promptForOptions() 268 | { 269 | $this->promptForOption('Database server name or IP address', 'db-host'); 270 | $this->promptForOption('Database name', 'db-name'); 271 | $this->promptForOption('Database user', 'db-user'); 272 | $this->promptForOption('Database password', 'db-password'); 273 | $this->promptForOption('Database port', 'db-port', true); 274 | 275 | $createDb = $this->confirm('Do you want want to create this database?'); 276 | 277 | $this->input->setOption('create-db', $createDb); 278 | 279 | if ($createDb) { 280 | $adminDbUser = $this->promptForOption('What is your admin database user? (optional, only if different from your site database user)', 'db-admin-user', true); 281 | 282 | if ($adminDbUser) { 283 | $this->promptForOption('What is your admin database password?', 'db-admin-password'); 284 | } 285 | } 286 | 287 | $this->promptForOption('Site Name', 'site-name'); 288 | $this->promptForOption('Site URL', 'site-url'); 289 | $this->promptForOption('Craft admin username', 'admin-user'); 290 | $this->promptForOption('Craft admin password', 'admin-password'); 291 | $this->promptForOption('Craft admin email', 'admin-email'); 292 | 293 | $locale = $this->askWithCompletion( 294 | 'Default Locale', 295 | array('ar', 'ar_sa', 'bg', 'bg_bg', 'ca_es', 'cs', 'cy_gb', 'da', 'da_dk', 'de', 'de_at', 'de_ch', 'de_de', 'el', 'el_gr', 'en', 'en_as', 'en_au', 'en_bb', 'en_be', 'en_bm', 'en_bw', 'en_bz', 'en_ca', 'en_dsrt', 'en_dsrt_us', 'en_gb', 'en_gu', 'en_gy', 'en_hk', 'en_ie', 'en_in', 'en_jm', 'en_mh', 'en_mp', 'en_mt', 'en_mu', 'en_na', 'en_nz', 'en_ph', 'en_pk', 'en_sg', 'en_shaw', 'en_tt', 'en_um', 'en_us', 'en_us_posix', 'en_vi', 'en_za', 'en_zw', 'en_zz', 'es', 'es_cl', 'es_es', 'es_mx', 'es_us', 'es_ve', 'et', 'fi', 'fi_fi', 'fil', 'fr', 'fr_be', 'fr_ca', 'fr_ch', 'fr_fr', 'fr_ma', 'he', 'hr', 'hr_hr', 'hu', 'hu_hu', 'id', 'id_id', 'it', 'it_ch', 'it_it', 'ja', 'ja_jp', 'ko', 'ko_kr', 'lt', 'lv', 'ms', 'ms_my', 'nb', 'nb_no', 'nl', 'nl_be', 'nl_nl', 'nn', 'nn_no', 'no', 'pl', 'pl_pl', 'pt', 'pt_br', 'pt_pt', 'ro', 'ro_ro', 'ru', 'ru_ru', 'sk', 'sl', 'sr', 'sv', 'sv_se', 'th', 'th_th', 'tr', 'tr_tr', 'uk', 'vi', 'zh', 'zh_cn', 'zh_tw'), 296 | 'en_us' 297 | ); 298 | 299 | $this->input->setOption('locale', $locale); 300 | } 301 | 302 | protected function generateDbConfig() 303 | { 304 | $handlebars = new Handlebars(array( 305 | 'loader' => new FilesystemLoader(__DIR__.'/../templates/'), 306 | )); 307 | 308 | $destination = $this->getPath().'/craft/config/db.php'; 309 | 310 | $handle = fopen($destination, 'w'); 311 | 312 | $output = $handlebars->render('db.php', array( 313 | 'host' => $this->option('db-host'), 314 | 'port' => $this->option('db-port'), 315 | 'name' => $this->option('db-name'), 316 | 'user' => $this->option('db-user'), 317 | 'password' => $this->option('db-password'), 318 | )); 319 | 320 | fwrite($handle, $output); 321 | 322 | fclose($handle); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/Command/InstallPluginCommand.php: -------------------------------------------------------------------------------- 1 | argument('repo'); 67 | 68 | if (! preg_match('#^([\d\w_-]+)/([\d\w\._-]+)$#', $repo)) { 69 | throw new Exception('Repository must be formatted: username/repo.'); 70 | } 71 | 72 | $version = $this->argument('version'); 73 | 74 | $url = sprintf('https://github.com/%s/archive/%s.tar.gz', $repo, $version); 75 | 76 | $this->comment('Downloading...'); 77 | 78 | $downloader = new TempDownloader($url, '.tar.gz'); 79 | 80 | $downloader->setOutput($this->output); 81 | 82 | try { 83 | $filePath = $downloader->download(); 84 | } catch (Exception $e) { 85 | return $this->fail($e->getMessage()); 86 | } 87 | 88 | $this->comment('Extracting...'); 89 | 90 | $extractionPath = rtrim(sys_get_temp_dir(), '/').'/craft_plugin_'.uniqid(); 91 | 92 | if (! @mkdir($extractionPath)) { 93 | return $this->fail('Could not create directory in system temp directory.'); 94 | } 95 | 96 | $tarExtractor = new TarExtractor($filePath, $extractionPath); 97 | 98 | $tarExtractor->extract(); 99 | 100 | // determine the folder structure of the download in the temp path 101 | $pluginReader = new PluginReader($extractionPath); 102 | 103 | try { 104 | $pluginFile = $pluginReader->read(); 105 | } catch (Exception $e) { 106 | return $this->fail($e->getMessage()); 107 | } 108 | 109 | $folderName = strtolower($pluginFile->getBasename('Plugin.php')); 110 | 111 | // check if craft is already installed, and overwrite option 112 | if (file_exists($this->pluginsPath.$folderName) && ! $this->option('overwrite')) { 113 | $this->error(sprintf('%s is already installed!', $folderName)); 114 | 115 | if (! $this->confirm('Do you want to overwrite?')) { 116 | $this->info('Exited without installing.'); 117 | 118 | return; 119 | } 120 | } 121 | 122 | // move the plugin from the temp folder to the craft installation 123 | CFileHelper::copyDirectory($pluginFile->getPath(), $this->pluginsPath.$folderName); 124 | 125 | // delete the temp files 126 | CFileHelper::removeDirectory($extractionPath); 127 | 128 | $this->info('Installation complete!'); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Command/RebuildSearchIndexesCommand.php: -------------------------------------------------------------------------------- 1 | argument('offset'); 46 | 47 | // Get all the element IDs ever 48 | $result = $this->craft->db->createCommand() 49 | ->select('id, type') 50 | ->from('elements') 51 | ->offset($offset) 52 | ->queryAll(); 53 | 54 | $progressBar = new ProgressBar($this->output, count($result) + $offset); 55 | 56 | foreach ($result as $i => $row) { 57 | // Get the element type 58 | $elementType = $this->craft->elements->getElementType($row['type']); 59 | 60 | if (!$elementType) { 61 | $progressBar->setProgress($offset + $i + 1); 62 | 63 | continue; 64 | } 65 | 66 | // delete existing indexes 67 | $this->craft->db->createCommand()->delete('searchindex', 'elementId = :elementId', array(':elementId' => $row['id'])); 68 | 69 | if ($elementType->isLocalized()) { 70 | $localeIds = $this->craft->i18n->getSiteLocaleIds(); 71 | } else { 72 | $localeIds = array($this->craft->i18n->getPrimarySiteLocaleId()); 73 | } 74 | 75 | $criteria = $this->craft->elements->getCriteria($row['type'], array( 76 | 'id' => $row['id'], 77 | 'status' => null, 78 | 'localeEnabled' => null, 79 | )); 80 | 81 | foreach ($localeIds as $localeId) { 82 | $criteria->locale = $localeId; 83 | $element = $criteria->first(); 84 | 85 | if (!$element) { 86 | continue; 87 | } 88 | 89 | $this->craft->search->indexElementAttributes($element); 90 | 91 | if (!$elementType->hasContent()) { 92 | continue; 93 | } 94 | 95 | $fieldLayout = $element->getFieldLayout(); 96 | $keywords = array(); 97 | 98 | foreach ($fieldLayout->getFields() as $fieldLayoutField) { 99 | $field = $fieldLayoutField->getField(); 100 | 101 | if (!$field) { 102 | continue; 103 | } 104 | 105 | $fieldType = $field->getFieldType(); 106 | 107 | if (!$fieldType) { 108 | continue; 109 | } 110 | 111 | $fieldType->element = $element; 112 | 113 | $handle = $field->handle; 114 | 115 | // Set the keywords for the content's locale 116 | $fieldSearchKeywords = $fieldType->getSearchKeywords($element->getFieldValue($handle)); 117 | $keywords[$field->id] = $fieldSearchKeywords; 118 | 119 | $this->craft->search->indexElementFields($element->id, $localeId, $keywords); 120 | } 121 | } 122 | 123 | $progressBar->setProgress($offset + $i + 1); 124 | } 125 | 126 | $progressBar->finish(); 127 | 128 | $this->line(''); 129 | 130 | $this->info('Search indexes have been rebuilt.'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Command/RunTasksCommand.php: -------------------------------------------------------------------------------- 1 | craft->tasks->getAllTasks(); 62 | 63 | if (!$tasks) { 64 | $this->info('No pending tasks.'); 65 | 66 | return; 67 | } 68 | 69 | foreach ($tasks as $task) { 70 | if ($this->option('type') !== null) { 71 | // Iterate to next item if input type does not match the task type 72 | if ($this->option('type') != $task->type) { 73 | continue; 74 | } 75 | } 76 | if ($task->status === TaskStatus::Running) { 77 | if ($this->option('reset-running')) { 78 | $this->resetTask($task); 79 | } else { 80 | continue; 81 | } 82 | } 83 | 84 | if ($task->status === TaskStatus::Error) { 85 | if ($this->option('reset-failed')) { 86 | $this->resetTask($task); 87 | } else { 88 | continue; 89 | } 90 | } 91 | 92 | try { 93 | $taskRecord = TaskRecord::model()->findById($task->id); 94 | 95 | $taskType = $task->getTaskType(); 96 | 97 | if (!$taskType) { 98 | throw new Exception('Could not find task type for task ID '.$task->id); 99 | } 100 | 101 | $task->totalSteps = $taskType->getTotalSteps(); 102 | 103 | $task->status = TaskStatus::Running; 104 | 105 | $progressBar = new ProgressBar($this->output, $task->totalSteps); 106 | 107 | $this->info($task->description); 108 | 109 | for ($step = 0; $step < $task->totalSteps; $step++) { 110 | $task->currentStep = $step + 1; 111 | 112 | $this->craft->tasks->saveTask($task); 113 | 114 | $result = $taskType->runStep($step); 115 | 116 | if ($result !== true) { 117 | $error = is_string($result) ? $result : 'Unknown error'; 118 | 119 | $progressBar->finish(); 120 | 121 | $this->line(''); 122 | 123 | throw new Exception($error); 124 | } 125 | 126 | $progressBar->setProgress($task->currentStep); 127 | } 128 | 129 | $taskRecord->deleteNode(); 130 | 131 | $progressBar->finish(); 132 | 133 | $this->line(''); 134 | } catch (Exception $e) { 135 | $this->failTask($task); 136 | 137 | $this->error($e->getMessage()); 138 | } 139 | } 140 | 141 | $this->info('All tasks finished.'); 142 | } 143 | 144 | protected function failTask(TaskModel $task) 145 | { 146 | $this->craft->db->createCommand()->update( 147 | 'tasks', 148 | array( 149 | 'status' => TaskStatus::Error, 150 | ), 151 | 'id = :taskId', 152 | array( 153 | ':taskId' => $task->id, 154 | ) 155 | ); 156 | } 157 | 158 | protected function resetTask(TaskModel $task) 159 | { 160 | $this->craft->db->createCommand()->update( 161 | 'tasks', 162 | array( 163 | 'status' => TaskStatus::Pending, 164 | 'currentStep' => null, 165 | 'totalSteps' => null, 166 | ), 167 | 'id = :taskId', 168 | array( 169 | ':taskId' => $task->id, 170 | ) 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Command/ShowConfigCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 67 | 68 | $file = $this->argument('file'); 69 | 70 | if ($name) { 71 | if (strpos($name, '.') !== false) { 72 | list($file, $name) = explode('.', $name, 2); 73 | } 74 | 75 | if ($file) { 76 | $value = $this->craft->getComponent('config')->get($name, $file); 77 | } else { 78 | $value = $this->craft->getComponent('config')->get($name); 79 | } 80 | 81 | $this->line($this->dump($value)); 82 | 83 | return; 84 | } 85 | 86 | $headers = array('File', 'Name', 'Value'); 87 | 88 | $rows = array(); 89 | 90 | $configService = $this->craft->getComponent('config'); 91 | 92 | $reflectionClass = new ReflectionClass($configService); 93 | 94 | $reflectionProperty = $reflectionClass->getProperty('_loadedConfigFiles'); 95 | 96 | $reflectionProperty->setAccessible(true); 97 | 98 | $configFiles = $reflectionProperty->getValue($configService); 99 | 100 | foreach ($configFiles as $file => $config) { 101 | ksort($config); 102 | 103 | foreach ($config as $key => $value) { 104 | $rows[] = array($file, $key, $this->dump($value)); 105 | } 106 | } 107 | 108 | $this->table($headers, $rows); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Command/TailCommand.php: -------------------------------------------------------------------------------- 1 | argument('log'); 74 | 75 | $runtimePath = $this->storagePath.'runtime/logs/'; 76 | 77 | $logPath = "{$runtimePath}{$log}.log"; 78 | 79 | if (! file_exists($runtimePath)) { 80 | return $this->fail('Invalid log.'); 81 | } 82 | 83 | $args = ''; 84 | 85 | if ($this->option('lines')) { 86 | $args .= ' -n '.$this->option('lines'); 87 | } elseif ($this->option('blocks')) { 88 | $args .= ' -b '.$this->option('lines'); 89 | } elseif ($this->option('bytes')) { 90 | $args .= ' -c '.$this->option('bytes'); 91 | } 92 | 93 | if ($this->option('reverse')) { 94 | $args .= ' -r'; 95 | } 96 | 97 | $command = sprintf('tail%s %s', $args, escapeshellarg($logPath)); 98 | 99 | passthru($command); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Command/UpdateAssetIndexesCommand.php: -------------------------------------------------------------------------------- 1 | option('session-id') ?: craft()->assetIndexing->getIndexingSessionId(); 83 | 84 | $this->info('Fetching sources...'); 85 | 86 | $sources = craft()->assetSources->getAllSources(); 87 | 88 | $sourceIds = $this->argument('sourceIds'); 89 | 90 | if ($sourceIds) { 91 | $sources = array_filter($sources, function ($source) use ($sourceIds) { 92 | return in_array($source->id, $sourceIds) || in_array($source->handle, $sourceIds); 93 | }); 94 | } 95 | 96 | $sourceIds = array_values(array_map(function ($source) { 97 | return $source->id; 98 | }, $sources)); 99 | 100 | $singleIndex = $this->option('single-index'); 101 | 102 | if ($singleIndex || $singleIndex === '0') { 103 | return $this->processIndexForSource($sessionId, $singleIndex, $sourceIds[0]); 104 | } 105 | 106 | $missingFolders = array(); 107 | 108 | $grandTotal = 0; 109 | 110 | $this->info('Fetching indexes...'); 111 | 112 | foreach ($sourceIds as $sourceId) { 113 | // Get the indexing list 114 | $indexList = craft()->assetIndexing->getIndexListForSource($sessionId, $sourceId); 115 | 116 | if (!empty($indexList['error'])) { 117 | return $this->fail($indexList['error']); 118 | } 119 | 120 | if (isset($indexList['missingFolders'])) { 121 | $missingFolders += $indexList['missingFolders']; 122 | } 123 | 124 | $grandTotal += $indexList['total']; 125 | 126 | $indexes[$sourceId] = $indexList['total']; 127 | } 128 | 129 | $this->info('Updating indexes...'); 130 | 131 | $progressBar = new ProgressBar($this->output, $grandTotal); 132 | 133 | $count = 0; 134 | 135 | // Index the file 136 | foreach ($indexes as $sourceId => $total) { 137 | for ($i = 0; $i < $total; $i++) { 138 | $this->runSingleIndex($sessionId, $i, $sourceId); 139 | 140 | $progressBar->setProgress(++$count); 141 | } 142 | } 143 | 144 | $progressBar->finish(); 145 | 146 | $this->line(''); 147 | 148 | $this->info('Deleting stale index data...'); 149 | 150 | $missingFiles = craft()->assetIndexing->getMissingFiles($sourceIds, $sessionId); 151 | 152 | // Clean up stale indexing data (all sessions that have all recordIds set) 153 | $sessionsInProgress = craft()->db->createCommand() 154 | ->select('sessionId') 155 | ->from('assetindexdata') 156 | ->where('recordId IS NULL') 157 | ->group('sessionId') 158 | ->queryScalar(); 159 | 160 | if (empty($sessionsInProgress)) { 161 | craft()->db->createCommand()->delete('assetindexdata'); 162 | } else { 163 | craft()->db->createCommand()->delete('assetindexdata', array('not in', 'sessionId', $sessionsInProgress)); 164 | } 165 | 166 | if ($missingFiles && $this->option('delete-missing-files')) { 167 | $this->info('Deleting missing files...'); 168 | 169 | craft()->assetIndexing->removeObsoleteFileRecords(array_keys($missingFiles)); 170 | } 171 | 172 | if ($missingFolders && $this->option('delete-missing-folders')) { 173 | $this->info('Deleting missing folders...'); 174 | 175 | craft()->assetIndexing->removeObsoleteFolderRecords(array_keys($missingFolders)); 176 | } 177 | 178 | $this->info('Asset indexes have been updated.'); 179 | } 180 | 181 | protected function runSingleIndex($sessionId, $index, $sourceId) 182 | { 183 | $args = array( 184 | PHP_BINARY, 185 | $_SERVER['PHP_SELF'], 186 | 'update:assetsindexes', 187 | '--single-index='.$index, 188 | '--session-id='.$sessionId, 189 | $sourceId, 190 | ); 191 | 192 | $process = new Process($args); 193 | 194 | $process->run(); 195 | } 196 | 197 | protected function processIndexForSource($sessionId, $index, $sourceId) 198 | { 199 | $attempts = 0; 200 | 201 | $retries = 3; 202 | 203 | do { 204 | $attempts++; 205 | 206 | try { 207 | craft()->assetIndexing->processIndexForSource($sessionId, $index, $sourceId); 208 | 209 | return; 210 | } catch (Exception $e) { 211 | if ($attempts === $retries) { 212 | throw $e; 213 | } 214 | } 215 | } while (true); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Console/GlobalArgvInput.php: -------------------------------------------------------------------------------- 1 | originalTokens = $tokens; 32 | 33 | parent::__construct($argv, $definition); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function parse() 40 | { 41 | $parsed = $tokens = $this->originalTokens; 42 | 43 | while (null !== $token = array_shift($parsed)) { 44 | $this->setTokens(array($token)); 45 | try { 46 | parent::parse(); 47 | } catch (RuntimeException $e) { 48 | // ignore these errors, otherwise re-throw it 49 | if (! preg_match('/^Too many arguments\.$|^No arguments expected|does not exist\.$/', $e->getMessage())) { 50 | throw $e; 51 | } 52 | } 53 | } 54 | 55 | $this->setTokens($tokens); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Handlebars/Loader/FilesystemLoader.php: -------------------------------------------------------------------------------- 1 | normalizePath($dir), '/'); 36 | } 37 | 38 | /** 39 | * Normalize path. 40 | * 41 | * From https://github.com/thephpleague/flysystem/blob/master/src/Util.php 42 | * 43 | * @param string $path 44 | * 45 | * @throws LogicException 46 | * 47 | * @return string 48 | */ 49 | protected function normalizePath($path) 50 | { 51 | // Remove any kind of funky unicode whitespace 52 | $normalized = preg_replace('#\p{C}+|^\./#u', '', $path); 53 | $normalized = $this->normalizeRelativePath($normalized); 54 | if (preg_match('#/\.{2}|^\.{2}/|^\.{2}$#', $normalized)) { 55 | throw new LogicException( 56 | 'Path is outside of the defined root, path: [' . $path . '], resolved: [' . $normalized . ']' 57 | ); 58 | } 59 | $normalized = preg_replace('#\\\{2,}#', '\\', trim($normalized, '\\')); 60 | $normalized = preg_replace('#/{2,}#', '/', trim($normalized, '/')); 61 | return $normalized; 62 | } 63 | 64 | /** 65 | * Normalize relative directories in a path. 66 | * 67 | * From https://github.com/thephpleague/flysystem/blob/master/src/Util.php 68 | * 69 | * @param string $path 70 | * 71 | * @return string 72 | */ 73 | protected function normalizeRelativePath($path) 74 | { 75 | // Path remove self referring paths ("/./"). 76 | $path = preg_replace('#/\.(?=/)|^\./|/\./?$#', '', $path); 77 | // Regex for resolving relative paths 78 | $regex = '#/*[^/\.]+/\.\.#Uu'; 79 | while (preg_match($regex, $path)) { 80 | $path = preg_replace($regex, '', $path); 81 | } 82 | return $path; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Support/AbstractMysqlCommand.php: -------------------------------------------------------------------------------- 1 | db = $db; 56 | } 57 | 58 | /** 59 | * Get base shell command being used 60 | * @return string mysql or mysqldump 61 | */ 62 | protected function getBaseCommand() 63 | { 64 | return 'mysql'; 65 | } 66 | 67 | /** 68 | * Get command line arguments for shell command 69 | * @return array 70 | */ 71 | protected function getArguments() 72 | { 73 | $arguments = []; 74 | 75 | if ($this->password) { 76 | $arguments[] = 'MYSQL_PWD='.escapeshellarg($this->getPassword()); 77 | } 78 | 79 | $arguments[] = $this->getBaseCommand(); 80 | 81 | if ($this->host) { 82 | $arguments[] = '-h '.escapeshellarg($this->host); 83 | } 84 | 85 | if ($this->port) { 86 | $arguments[] = '-P '.$this->port; 87 | } 88 | 89 | if ($this->user) { 90 | $arguments[] = '-u '.escapeshellarg($this->getUser()); 91 | } 92 | 93 | if ($this->flags) { 94 | foreach ($this->flags as $flag) { 95 | $arguments[] = $flag; 96 | } 97 | } 98 | 99 | return $arguments; 100 | } 101 | 102 | /** 103 | * Get Mysql user 104 | * @var string 105 | */ 106 | protected function getUser() 107 | { 108 | return $this->user; 109 | } 110 | 111 | /** 112 | * Get Mysql password 113 | * @var string 114 | */ 115 | protected function getPassword() 116 | { 117 | return $this->password; 118 | } 119 | 120 | /** 121 | * Compile the shell command 122 | * @return string 123 | */ 124 | public function __toString() 125 | { 126 | $arguments = $this->getArguments(); 127 | 128 | if ($this->db) { 129 | array_push($arguments, $this->db); 130 | } 131 | 132 | if ($this->grep) { 133 | array_push($arguments, '| grep '.escapeshellarg($this->grep)); 134 | } 135 | 136 | // space prefix to prevent bash history 137 | return ' '.implode(' ', $arguments); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Support/ClassFinder.php: -------------------------------------------------------------------------------- 1 | isInstantiable()) { 55 | continue; 56 | } 57 | 58 | if (! $reflectionClass->isSubclassOf($subclassOf)) { 59 | continue; 60 | } 61 | 62 | $classes[] = $class; 63 | } 64 | 65 | return $classes; 66 | } 67 | 68 | /** 69 | * Check if path is absolute (or relative) 70 | * @param string $path 71 | * @return boolean 72 | */ 73 | public function isPathAbsolute($path) 74 | { 75 | // starts with dot 76 | if (strncmp($path, '.', 1) === 0) { 77 | return true; 78 | } 79 | 80 | $isWindows = strncmp(strtoupper(PHP_OS), 'WIN', 3) === 0; 81 | 82 | if ($isWindows) { 83 | // matches drive + path notation (C:\) or \\network-drive\ 84 | return (bool) preg_match('/^([a-zA-Z]:\\\\|\\\\\\\\)/', $path); 85 | } 86 | 87 | // starts with slash 88 | return strncmp($path, '/', 1) === 0; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Support/Downloader/BaseDownloader.php: -------------------------------------------------------------------------------- 1 | output = $output; 41 | } 42 | 43 | /** 44 | * Download the file to the specified path 45 | * @return string path to the downloaded file 46 | */ 47 | public function download() 48 | { 49 | if (! ini_get('allow_url_fopen')) { 50 | throw new Exception('allow_url_fopen is disabled.'); 51 | } 52 | 53 | // open the temp file for writing 54 | $fileHandle = fopen($this->path, 'wb'); 55 | 56 | if ($fileHandle === false) { 57 | throw new Exception('Could not open temp file.'); 58 | } 59 | 60 | $caPath = CaBundle::getSystemCaRootBundlePath(); 61 | 62 | if (is_dir($caPath)) { 63 | $streamOptions = array('ssl' => array('capath' => $caPath)); 64 | } else { 65 | $streamOptions = array('ssl' => array('cafile' => $caPath)); 66 | } 67 | 68 | $streamParams = array('notification' => array($this, 'showDownloadProgress')); 69 | 70 | // download context so we can track download progress 71 | $downloadContext = stream_context_create($streamOptions, $streamParams); 72 | 73 | // open the download url for reading 74 | $downloadHandle = @fopen($this->url, 'rb', false, $downloadContext); 75 | 76 | if ($downloadHandle === false) { 77 | throw new Exception('Could not download installation file.'); 78 | } 79 | 80 | while (! feof($downloadHandle)) { 81 | if (fwrite($fileHandle, fread($downloadHandle, 1024)) === false) { 82 | throw new Exception('Could not write installation file to disk.'); 83 | } 84 | } 85 | 86 | fclose($downloadHandle); 87 | 88 | fclose($fileHandle); 89 | 90 | if ($this->progressBar) { 91 | $this->progressBar->finish(); 92 | 93 | $this->output->writeln(''); 94 | } 95 | 96 | return $this->path; 97 | } 98 | 99 | /** 100 | * ProgressBar callback 101 | * @param $notificationCode 102 | * @param $severity 103 | * @param $message 104 | * @param $messageCode 105 | * @param $bytesTransferred 106 | * @param $bytesMax 107 | * @return void 108 | */ 109 | protected function showDownloadProgress( 110 | $notificationCode, 111 | $severity, 112 | $message, 113 | $messageCode, 114 | $bytesTransferred, 115 | $bytesMax 116 | ) { 117 | switch ($notificationCode) { 118 | case STREAM_NOTIFY_FILE_SIZE_IS: 119 | if ($this->output) { 120 | $this->progressBar = new ProgressBar($this->output, $bytesMax); 121 | } 122 | break; 123 | case STREAM_NOTIFY_PROGRESS: 124 | if ($this->progressBar) { 125 | $this->progressBar->setProgress($bytesTransferred); 126 | } 127 | break; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Support/Downloader/Downloader.php: -------------------------------------------------------------------------------- 1 | url = $url; 15 | 16 | $this->path = $path; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Support/Downloader/TempDownloader.php: -------------------------------------------------------------------------------- 1 | url = $url; 22 | 23 | $tempPath = tempnam(sys_get_temp_dir(), 'craft_cli_'); 24 | 25 | if ($extension) { 26 | $this->path = $tempPath.'.'.ltrim($extension, '.'); 27 | 28 | rename($tempPath, $this->path); 29 | } else { 30 | $this->path = $tempPath; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Support/MysqlCommand.php: -------------------------------------------------------------------------------- 1 | query) { 21 | $arguments[] = '-e '.escapeshellarg($this->query); 22 | } 23 | 24 | return $arguments; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/MysqlCreateDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | adminUser ?: $this->user; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function getPassword() 37 | { 38 | return $this->adminPassword ?: $this->password; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function getArguments() 45 | { 46 | $arguments = parent::getArguments(); 47 | 48 | $query = "CREATE DATABASE {$this->name};"; 49 | 50 | if ($this->user) { 51 | $query .= " GRANT ALL PRIVILEGES ON {$this->name}.* To '{$this->user}'@'%' IDENTIFIED BY '{$this->password}'; FLUSH PRIVILEGES;"; 52 | } 53 | 54 | $arguments[] = '-e '.escapeshellarg($query); 55 | 56 | return $arguments; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Support/MysqlDumpCommand.php: -------------------------------------------------------------------------------- 1 | directory = $directory; 24 | } 25 | 26 | /** 27 | * Locate a Plugin.php file in the specified directory 28 | * @return SplFileInfo 29 | */ 30 | public function read() 31 | { 32 | $files = CFileHelper::findFiles($this->directory, array( 33 | 'fileTypes' => array('php'), 34 | 'exclude' => array('/vendor', '/tests'), 35 | )); 36 | 37 | foreach ($files as $file) { 38 | if (preg_match('/Plugin$/', basename($file, '.php'))) { 39 | return new SplFileInfo($file); 40 | } 41 | } 42 | 43 | throw new Exception('Could not find a valid Craft plugin file.'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/RsyncCommand.php: -------------------------------------------------------------------------------- 1 | src = $src; 95 | $this->dest = $dest; 96 | } 97 | 98 | protected function getPath($which) 99 | { 100 | $path = $this->{$which}; 101 | 102 | if ($this->{$which.'Host'}) { 103 | $path = $this->{$which.'Host'}.':'.$path; 104 | } 105 | 106 | if ($this->{$which.'User'}) { 107 | $path = $this->{$which.'User'}.'@'.$path; 108 | } 109 | 110 | return $path; 111 | } 112 | 113 | protected function getSrcPath() 114 | { 115 | return $this->getPath('src'); 116 | } 117 | 118 | protected function getDestPath() 119 | { 120 | return $this->getPath('dest'); 121 | } 122 | 123 | /** 124 | * Get command line arguments for shell command 125 | * @return array 126 | */ 127 | protected function getArguments() 128 | { 129 | $arguments = [ 130 | 'rsync', 131 | ]; 132 | 133 | if ($this->flags) { 134 | $arguments[] = '-'.$this->flags; 135 | } 136 | 137 | if ($this->noPerms) { 138 | $arguments[] = '--no-perms'; 139 | } 140 | 141 | if ($this->noOwner) { 142 | $arguments[] = '--no-owner'; 143 | } 144 | 145 | if ($this->noGroup) { 146 | $arguments[] = '--no-group'; 147 | } 148 | 149 | $sshOpts = []; 150 | 151 | if ($this->port) { 152 | $sshOpts[] = '-p'; 153 | $sshOpts[] = escapeshellarg($this->port); 154 | } 155 | 156 | if ($this->identityFile) { 157 | $sshOpts[] = '-i'; 158 | $sshOpts[] = escapeshellarg($this->identityFile); 159 | } 160 | 161 | if ($sshOpts) { 162 | $arguments[] = '-e'; 163 | $arguments[] = sprintf("'ssh %s'", implode(' ', $sshOpts)); 164 | } 165 | 166 | foreach ($this->exclude as $exclude) { 167 | $arguments[] = '--exclude'; 168 | $arguments[] = escapeshellarg($exclude); 169 | } 170 | 171 | $arguments[] = escapeshellarg($this->getSrcPath()); 172 | 173 | $arguments[] = escapeshellarg($this->getDestPath()); 174 | 175 | return $arguments; 176 | } 177 | 178 | /** 179 | * Compile the shell command 180 | * @return string 181 | */ 182 | public function __toString() 183 | { 184 | $arguments = $this->getArguments(); 185 | 186 | // space prefix to prevent bash history 187 | return ' '.implode(' ', $arguments); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Support/SshCommand.php: -------------------------------------------------------------------------------- 1 | host = $host; 45 | $this->command = $command; 46 | } 47 | 48 | /** 49 | * Compile the shell command 50 | * @return string 51 | */ 52 | public function __toString() 53 | { 54 | $arguments = ['ssh']; 55 | 56 | if ($this->identityFile) { 57 | $arguments[] = '-i '.escapeshellarg($this->identityFile); 58 | } 59 | 60 | if ($this->port) { 61 | $arguments[] = '-p '.$this->port; 62 | } 63 | 64 | $arguments[] = '-o ConnectTimeout=10'; 65 | 66 | $arguments[] = $this->user ? $this->user.'@'.$this->host : $this->host; 67 | 68 | $arguments[] = escapeshellarg($this->command); 69 | 70 | return implode(' ', $arguments); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Support/TarExtractor.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 62 | 63 | $this->extractionPath = $extractionPath; 64 | 65 | $this->overwrite = $overwrite; 66 | } 67 | 68 | /** 69 | * Extract the archive to the specified path 70 | * @return void 71 | */ 72 | public function extract() 73 | { 74 | $tarPath = $this->filePath; 75 | 76 | // is it gzipped? 77 | if (preg_match('/^(.*?\.tar)\.gz$/', $this->filePath, $match)) { 78 | $pharData = new PharData($this->filePath); 79 | 80 | // creates a tar file in the same dir 81 | $pharData->decompress(); 82 | 83 | // set the tar path (file without the .gz extension) 84 | $tarPath = $match[1]; 85 | 86 | unset($pharData); 87 | 88 | // destroy the gzip file 89 | unlink($this->filePath); 90 | } 91 | 92 | // extract the tar 93 | $pharData = new PharData($tarPath); 94 | 95 | $pharData->extractTo($this->extractionPath, null, $this->overwrite); 96 | 97 | unset($pharData); 98 | 99 | // destroy the tar file 100 | unlink($tarPath); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/bootstrap.php: -------------------------------------------------------------------------------- 1 | addComposerPlugins(); 18 | $app->addUserDefinedCommands(); 19 | 20 | $app->run(); 21 | -------------------------------------------------------------------------------- /src/stub.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | '{{host}}', 14 | 15 | // The name of the database to select. 16 | 'database' => '{{name}}', 17 | 18 | // The database username to connect with. 19 | 'user' => '{{user}}', 20 | 21 | // The database password to connect with. 22 | 'password' => '{{password}}', 23 | 24 | // The prefix to use when naming tables. This can be no more than 5 characters. 25 | 'tablePrefix' => 'craft', 26 | 27 | ); 28 | --------------------------------------------------------------------------------