1<?php 2/** 3 * Core installer command line interface. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Installer 22 */ 23 24use MediaWiki\Installer\InstallException; 25use MediaWiki\MediaWikiServices; 26 27/** 28 * Class for the core installer command line interface. 29 * 30 * @ingroup Installer 31 * @since 1.17 32 */ 33class CliInstaller extends Installer { 34 private $specifiedScriptPath = false; 35 36 private $optionMap = [ 37 'dbtype' => 'wgDBtype', 38 'dbserver' => 'wgDBserver', 39 'dbname' => 'wgDBname', 40 'dbuser' => 'wgDBuser', 41 'dbpass' => 'wgDBpassword', 42 'dbprefix' => 'wgDBprefix', 43 'dbtableoptions' => 'wgDBTableOptions', 44 'dbport' => 'wgDBport', 45 'dbschema' => 'wgDBmwschema', 46 'dbpath' => 'wgSQLiteDataDir', 47 'server' => 'wgServer', 48 'scriptpath' => 'wgScriptPath', 49 ]; 50 51 /** 52 * @param string $siteName 53 * @param string|null $admin 54 * @param array $options 55 * @throws InstallException 56 */ 57 public function __construct( $siteName, $admin = null, array $options = [] ) { 58 global $wgContLang, $wgPasswordPolicy; 59 60 parent::__construct(); 61 62 if ( isset( $options['scriptpath'] ) ) { 63 $this->specifiedScriptPath = true; 64 } 65 66 foreach ( $this->optionMap as $opt => $global ) { 67 if ( isset( $options[$opt] ) ) { 68 $GLOBALS[$global] = $options[$opt]; 69 $this->setVar( $global, $options[$opt] ); 70 } 71 } 72 73 if ( isset( $options['lang'] ) ) { 74 global $wgLang, $wgLanguageCode; 75 $this->setVar( '_UserLang', $options['lang'] ); 76 $wgLanguageCode = $options['lang']; 77 $this->setVar( 'wgLanguageCode', $wgLanguageCode ); 78 $wgContLang = MediaWikiServices::getInstance()->getContentLanguage(); 79 $wgLang = MediaWikiServices::getInstance()->getLanguageFactory() 80 ->getLanguage( $options['lang'] ); 81 RequestContext::getMain()->setLanguage( $wgLang ); 82 } 83 84 $this->setVar( 'wgSitename', $siteName ); 85 86 $metaNS = $wgContLang->ucfirst( str_replace( ' ', '_', $siteName ) ); 87 if ( $metaNS == 'MediaWiki' ) { 88 $metaNS = 'Project'; 89 } 90 $this->setVar( 'wgMetaNamespace', $metaNS ); 91 92 if ( !isset( $options['installdbuser'] ) ) { 93 $this->setVar( '_InstallUser', 94 $this->getVar( 'wgDBuser' ) ); 95 $this->setVar( '_InstallPassword', 96 $this->getVar( 'wgDBpassword' ) ); 97 } else { 98 $this->setVar( '_InstallUser', 99 $options['installdbuser'] ); 100 $this->setVar( '_InstallPassword', 101 $options['installdbpass'] ?? "" ); 102 103 // Assume that if we're given the installer user, we'll create the account. 104 $this->setVar( '_CreateDBAccount', true ); 105 } 106 107 if ( $admin ) { 108 $this->setVar( '_AdminName', $admin ); 109 if ( isset( $options['pass'] ) ) { 110 $adminUser = User::newFromName( $admin ); 111 if ( !$adminUser ) { 112 throw new InstallException( Status::newFatal( 'config-admin-name-invalid' ) ); 113 } 114 $upp = new UserPasswordPolicy( 115 $wgPasswordPolicy['policies'], 116 $wgPasswordPolicy['checks'] 117 ); 118 $status = $upp->checkUserPasswordForGroups( $adminUser, $options['pass'], 119 [ 'bureaucrat', 'sysop', 'interface-admin' ] ); // per Installer::createSysop() 120 if ( !$status->isGood() ) { 121 throw new InstallException( Status::newFatal( 122 $status->getMessage( 'config-admin-error-password-invalid' ) ) ); 123 } 124 $this->setVar( '_AdminPassword', $options['pass'] ); 125 } 126 } 127 128 // Detect and inject any extension found 129 if ( isset( $options['extensions'] ) ) { 130 $status = $this->validateExtensions( 131 'extension', 'extensions', $options['extensions'] ); 132 if ( !$status->isOK() ) { 133 throw new InstallException( $status ); 134 } 135 $this->setVar( '_Extensions', $status->value ); 136 } elseif ( isset( $options['with-extensions'] ) ) { 137 $status = $this->findExtensions(); 138 if ( !$status->isOK() ) { 139 throw new InstallException( $status ); 140 } 141 $this->setVar( '_Extensions', array_keys( $status->value ) ); 142 } 143 144 // Set up the default skins 145 if ( isset( $options['skins'] ) ) { 146 $status = $this->validateExtensions( 'skin', 'skins', $options['skins'] ); 147 if ( !$status->isOK() ) { 148 throw new InstallException( $status ); 149 } 150 $skins = $status->value; 151 } else { 152 $status = $this->findExtensions( 'skins' ); 153 if ( !$status->isOK() ) { 154 throw new InstallException( $status ); 155 } 156 $skins = array_keys( $status->value ); 157 } 158 $this->setVar( '_Skins', $skins ); 159 160 if ( $skins ) { 161 $skinNames = array_map( 'strtolower', $skins ); 162 $this->setVar( 'wgDefaultSkin', $this->getDefaultSkin( $skinNames ) ); 163 } 164 } 165 166 private function validateExtensions( $type, $directory, $nameLists ) { 167 $extensions = []; 168 $status = new Status; 169 foreach ( (array)$nameLists as $nameList ) { 170 foreach ( explode( ',', $nameList ) as $name ) { 171 $name = trim( $name ); 172 if ( $name === '' ) { 173 continue; 174 } 175 $extStatus = $this->getExtensionInfo( $type, $directory, $name ); 176 if ( $extStatus->isOK() ) { 177 $extensions[] = $name; 178 } else { 179 $status->merge( $extStatus ); 180 } 181 } 182 } 183 $extensions = array_unique( $extensions ); 184 $status->value = $extensions; 185 return $status; 186 } 187 188 /** 189 * Main entry point. 190 * @return Status 191 */ 192 public function execute() { 193 // If APC is available, use that as the MainCacheType, instead of nothing. 194 // This is hacky and should be consolidated with WebInstallerOptions. 195 // This is here instead of in __construct(), because it should run run after 196 // doEnvironmentChecks(), which populates '_Caches'. 197 if ( count( $this->getVar( '_Caches' ) ) ) { 198 // We detected a CACHE_ACCEL implementation, use it. 199 $this->setVar( '_MainCacheType', 'accel' ); 200 } 201 202 $vars = Installer::getExistingLocalSettings(); 203 if ( $vars ) { 204 $status = Status::newFatal( "config-localsettings-cli-upgrade" ); 205 $this->showStatusMessage( $status ); 206 return $status; 207 } 208 209 $result = $this->performInstallation( 210 [ $this, 'startStage' ], 211 [ $this, 'endStage' ] 212 ); 213 // PerformInstallation bails on a fatal, so make sure the last item 214 // completed before giving 'next.' Likewise, only provide back on failure 215 $lastStepStatus = end( $result ); 216 if ( $lastStepStatus->isOK() ) { 217 return Status::newGood(); 218 } else { 219 return $lastStepStatus; 220 } 221 } 222 223 /** 224 * Write LocalSettings.php to a given path 225 * 226 * @param string $path Full path to write LocalSettings.php to 227 */ 228 public function writeConfigurationFile( $path ) { 229 $ls = InstallerOverrides::getLocalSettingsGenerator( $this ); 230 $ls->writeFile( "$path/LocalSettings.php" ); 231 } 232 233 public function startStage( $step ) { 234 // Messages: config-install-database, config-install-tables, config-install-interwiki, 235 // config-install-stats, config-install-keys, config-install-sysop, config-install-mainpage, 236 // config-install-extensions 237 $this->showMessage( "config-install-$step" ); 238 } 239 240 public function endStage( $step, $status ) { 241 $this->showStatusMessage( $status ); 242 $this->showMessage( 'config-install-step-done' ); 243 } 244 245 public function showMessage( $msg, ...$params ) { 246 echo $this->getMessageText( $msg, $params ) . "\n"; 247 flush(); 248 } 249 250 public function showError( $msg, ...$params ) { 251 echo "***{$this->getMessageText( $msg, $params )}***\n"; 252 flush(); 253 } 254 255 /** 256 * @param string $msg 257 * @param array $params 258 * 259 * @return string 260 */ 261 protected function getMessageText( $msg, $params ) { 262 $text = wfMessage( $msg, $params )->parse(); 263 264 $text = preg_replace( '/<a href="(.*?)".*?>(.*?)<\/a>/', '$2 <$1>', $text ); 265 266 return Sanitizer::stripAllTags( $text ); 267 } 268 269 /** 270 * Dummy 271 * @param string $msg Key for wfMessage() 272 * @param mixed ...$params 273 */ 274 public function showHelpBox( $msg, ...$params ) { 275 } 276 277 public function showStatusMessage( Status $status ) { 278 $warnings = array_merge( $status->getWarningsArray(), 279 $status->getErrorsArray() ); 280 281 if ( count( $warnings ) !== 0 ) { 282 foreach ( $warnings as $w ) { 283 $this->showMessage( ...$w ); 284 } 285 } 286 } 287 288 public function envCheckPath() { 289 if ( !$this->specifiedScriptPath ) { 290 $this->showMessage( 'config-no-cli-uri', $this->getVar( "wgScriptPath" ) ); 291 } 292 293 return parent::envCheckPath(); 294 } 295 296 protected function envGetDefaultServer() { 297 // Use a basic value if the user didn't pass in --server 298 return 'http://localhost'; 299 } 300 301 public function dirIsExecutable( $dir, $url ) { 302 $this->showMessage( 'config-no-cli-uploads-check', $dir ); 303 304 return false; 305 } 306} 307