(bug 28868) Include the number of pages in the default getLongDesc for multipaged...
[lhc/web/wiklou.git] / includes / installer / PostgresInstaller.php
1 <?php
2 /**
3 * PostgreSQL-specific installer.
4 *
5 * @file
6 * @ingroup Deployment
7 */
8
9 /**
10 * Class for setting up the MediaWiki database using Postgres.
11 *
12 * @ingroup Deployment
13 * @since 1.17
14 */
15 class PostgresInstaller extends DatabaseInstaller {
16
17 protected $globalNames = array(
18 'wgDBserver',
19 'wgDBport',
20 'wgDBname',
21 'wgDBuser',
22 'wgDBpassword',
23 'wgDBmwschema',
24 );
25
26 var $minimumVersion = '8.3';
27 private $useAdmin = false;
28
29 function getName() {
30 return 'postgres';
31 }
32
33 public function isCompiled() {
34 return self::checkExtension( 'pgsql' );
35 }
36
37 function getConnectForm() {
38 // If this is our first time here, switch the default user presented in the form
39 if ( ! $this->getVar('_switchedInstallUser') ) {
40 $this->setVar('_InstallUser', 'postgres');
41 $this->setVar('_switchedInstallUser', true);
42 }
43 return
44 $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
45 $this->getTextBox( 'wgDBport', 'config-db-port' ) .
46 Html::openElement( 'fieldset' ) .
47 Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) .
48 $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-db-name-help' ) ) .
49 $this->getTextBox( 'wgDBmwschema', 'config-db-schema', array(), $this->parent->getHelpBox( 'config-db-schema-help' ) ) .
50 Html::closeElement( 'fieldset' ) .
51 $this->getInstallUserBox();
52 }
53
54 function submitConnectForm() {
55 // Get variables from the request
56 $newValues = $this->setVarsFromRequest( array( 'wgDBserver', 'wgDBport',
57 'wgDBname', 'wgDBmwschema' ) );
58
59 // Validate them
60 $status = Status::newGood();
61 if ( !strlen( $newValues['wgDBname'] ) ) {
62 $status->fatal( 'config-missing-db-name' );
63 } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
64 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
65 }
66 if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
67 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
68 }
69
70 // Submit user box
71 if ( $status->isOK() ) {
72 $status->merge( $this->submitInstallUserBox() );
73 }
74 if ( !$status->isOK() ) {
75 return $status;
76 }
77
78 $this->useAdmin = true;
79 // Try to connect
80 $status->merge( $this->getConnection() );
81 if ( !$status->isOK() ) {
82 return $status;
83 }
84
85 //Make sure install user can create
86 if( !$this->canCreateAccounts() ) {
87 $status->fatal( 'config-pg-no-create-privs' );
88 }
89 if ( !$status->isOK() ) {
90 return $status;
91 }
92
93 // Check version
94 $version = $this->db->getServerVersion();
95 if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
96 return Status::newFatal( 'config-postgres-old', $this->minimumVersion, $version );
97 }
98
99 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
100 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
101 return $status;
102 }
103
104 public function openConnection() {
105 $status = Status::newGood();
106 try {
107 if ( $this->useAdmin ) {
108 $db = new DatabasePostgres(
109 $this->getVar( 'wgDBserver' ),
110 $this->getVar( '_InstallUser' ),
111 $this->getVar( '_InstallPassword' ),
112 'postgres' );
113 } else {
114 $db = new DatabasePostgres(
115 $this->getVar( 'wgDBserver' ),
116 $this->getVar( 'wgDBuser' ),
117 $this->getVar( 'wgDBpassword' ),
118 $this->getVar( 'wgDBname' ) );
119 }
120 $status->value = $db;
121 } catch ( DBConnectionError $e ) {
122 $status->fatal( 'config-connection-error', $e->getMessage() );
123 }
124 return $status;
125 }
126
127 protected function canCreateAccounts() {
128 $this->useAdmin = true;
129 $status = $this->getConnection();
130 if ( !$status->isOK() ) {
131 return false;
132 }
133 $conn = $status->value;
134
135 $superuser = $this->getVar( '_InstallUser' );
136
137 $rights = $conn->selectField( 'pg_catalog.pg_user',
138 'CASE WHEN usesuper IS TRUE THEN
139 CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
140 ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
141 END AS rights',
142 array( 'usename' => $superuser ), __METHOD__
143 );
144
145 if( !$rights || ( $rights != 1 && $rights != 3 ) ) {
146 return false;
147 }
148
149 return true;
150 }
151
152 public function getSettingsForm() {
153 if ( $this->canCreateAccounts() ) {
154 $noCreateMsg = false;
155 } else {
156 $noCreateMsg = 'config-db-web-no-create-privs';
157 }
158 $s = $this->getWebUserBox( $noCreateMsg );
159
160 return $s;
161 }
162
163 public function submitSettingsForm() {
164 $status = $this->submitWebUserBox();
165 if ( !$status->isOK() ) {
166 return $status;
167 }
168
169 // Validate the create checkbox
170 if ( !$this->canCreateAccounts() ) {
171 $this->setVar( '_CreateDBAccount', false );
172 $create = false;
173 } else {
174 $create = $this->getVar( '_CreateDBAccount' );
175 }
176
177 // Don't test the web account if it is the same as the admin.
178 if ( !$create && $this->getVar( 'wgDBuser' ) != $this->getVar( '_InstallUser' ) ) {
179 // Test the web account
180 try {
181 $this->useAdmin = false;
182 return $this->openConnection();
183 } catch ( DBConnectionError $e ) {
184 return Status::newFatal( 'config-connection-error', $e->getMessage() );
185 }
186 }
187
188 return Status::newGood();
189 }
190
191 public function preInstall() {
192 $commitCB = array(
193 'name' => 'pg-commit',
194 'callback' => array( $this, 'commitChanges' ),
195 );
196 $plpgCB = array(
197 'name' => 'pg-plpgsql',
198 'callback' => array( $this, 'setupPLpgSQL' ),
199 );
200 $this->parent->addInstallStep( $commitCB, 'interwiki' );
201 $this->parent->addInstallStep( $plpgCB, 'database' );
202 if( $this->getVar( '_CreateDBAccount' ) ) {
203 $this->parent->addInstallStep( array(
204 'name' => 'user',
205 'callback' => array( $this, 'setupUser' ),
206 ) );
207 }
208 }
209
210 function setupDatabase() {
211 $this->useAdmin = true;
212 $status = $this->getConnection();
213 if ( !$status->isOK() ) {
214 return $status;
215 }
216 $this->setupSchemaVars();
217 $conn = $status->value;
218
219 $dbName = $this->getVar( 'wgDBname' );
220 $schema = $this->getVar( 'wgDBmwschema' );
221 $user = $this->getVar( 'wgDBuser' );
222 $safeschema = $conn->addIdentifierQuotes( $schema );
223 $safeuser = $conn->addIdentifierQuotes( $user );
224
225 $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $conn->addQuotes( $dbName );
226 $rows = $conn->numRows( $conn->query( $SQL ) );
227 $safedb = $conn->addIdentifierQuotes( $dbName );
228 if( !$rows ) {
229 $conn->query( "CREATE DATABASE $safedb OWNER $safeuser", __METHOD__ );
230 } else {
231 $conn->query( "ALTER DATABASE $safedb OWNER TO $safeuser", __METHOD__ );
232 }
233
234 // Now that we've established the real database exists, connect to it
235 // Because we do not want the same connection, forcibly expire the existing conn
236 $this->db = null;
237 $this->useAdmin = false;
238 $status = $this->getConnection();
239 if ( !$status->isOK() ) {
240 return $status;
241 }
242 $conn = $status->value;
243
244 if( !$conn->schemaExists( $schema ) ) {
245 $result = $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
246 if( !$result ) {
247 $status->fatal( 'config-install-pg-schema-failed', $user, $schema );
248 }
249 } else {
250 $safeschema2 = $conn->addQuotes( $schema );
251 $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
252 "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n" .
253 "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n" .
254 "AND p.relkind IN ('r','S','v')\n";
255 $SQL .= "UNION\n";
256 $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
257 "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n" .
258 "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n" .
259 "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
260 $conn->query( "SET search_path = $safeschema" );
261 $res = $conn->query( $SQL );
262 }
263 return $status;
264 }
265
266 function commitChanges() {
267 $this->db->query( 'COMMIT' );
268 return Status::newGood();
269 }
270
271 function setupUser() {
272 if ( !$this->getVar( '_CreateDBAccount' ) ) {
273 return Status::newGood();
274 }
275
276 $this->useAdmin = true;
277 $status = $this->getConnection();
278
279 if ( !$status->isOK() ) {
280 return $status;
281 }
282
283 $schema = $this->getVar( 'wgDBmwschema' );
284 $safeuser = $this->db->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
285 $safeusercheck = $this->db->addQuotes( $this->getVar( 'wgDBuser' ) );
286 $safepass = $this->db->addQuotes( $this->getVar( 'wgDBpassword' ) );
287 $safeschema = $this->db->addIdentifierQuotes( $schema );
288
289 $rows = $this->db->numRows(
290 $this->db->query( "SELECT 1 FROM pg_catalog.pg_shadow WHERE usename = $safeusercheck" )
291 );
292 if ( $rows < 1 ) {
293 $res = $this->db->query( "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass", __METHOD__ );
294 if ( $res !== true && !( $res instanceOf ResultWrapper ) ) {
295 $status->fatal( 'config-install-user-failed', $this->getVar( 'wgDBuser' ), $res );
296 }
297 if( $status->isOK() ) {
298 $this->db->query("ALTER USER $safeuser SET search_path = $safeschema");
299 }
300 }
301
302 return $status;
303 }
304
305 function getLocalSettings() {
306 $port = $this->getVar( 'wgDBport' );
307 $schema = $this->getVar( 'wgDBmwschema' );
308 return
309 "# Postgres specific settings
310 \$wgDBport = \"{$port}\";
311 \$wgDBmwschema = \"{$schema}\";";
312 }
313
314 public function preUpgrade() {
315 global $wgDBuser, $wgDBpassword;
316
317 # Normal user and password are selected after this step, so for now
318 # just copy these two
319 $wgDBuser = $this->getVar( '_InstallUser' );
320 $wgDBpassword = $this->getVar( '_InstallPassword' );
321 }
322
323 public function createTables() {
324 $schema = $this->getVar( 'wgDBmwschema' );
325
326 $this->db = null;
327 $this->useAdmin = false;
328 $status = $this->getConnection();
329 if ( !$status->isOK() ) {
330 return $status;
331 }
332
333 if( $this->db->tableExists( 'user' ) ) {
334 $status->warning( 'config-install-tables-exist' );
335 return $status;
336 }
337
338 $this->db->begin( __METHOD__ );
339
340 if( !$this->db->schemaExists( $schema ) ) {
341 $status->error( 'config-install-pg-schema-not-exist' );
342 return $status;
343 }
344 $safeschema = $this->db->addIdentifierQuotes( $schema );
345 $this->db->query( "SET search_path = $safeschema" );
346 $error = $this->db->sourceFile( $this->db->getSchema() );
347 if( $error !== true ) {
348 $this->db->reportQueryError( $error, 0, '', __METHOD__ );
349 $this->db->rollback( __METHOD__ );
350 $status->fatal( 'config-install-tables-failed', $error );
351 } else {
352 $this->db->commit( __METHOD__ );
353 }
354 // Resume normal operations
355 if( $status->isOk() ) {
356 $this->enableLB();
357 }
358 return $status;
359 }
360
361 public function setupPLpgSQL() {
362 $this->useAdmin = true;
363 $status = $this->getConnection();
364 if ( !$status->isOK() ) {
365 return $status;
366 }
367
368 $rows = $this->db->numRows(
369 $this->db->query( "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'" )
370 );
371 if ( $rows < 1 ) {
372 // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
373 $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
374 "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
375 $rows = $this->db->numRows( $this->db->query( $SQL ) );
376 $dbName = $this->getVar( 'wgDBname' );
377 if ( $rows >= 1 ) {
378 $result = $this->db->query( 'CREATE LANGUAGE plpgsql' );
379 if ( !$result ) {
380 return Status::newFatal( 'config-pg-no-plpgsql', $dbName );
381 }
382 } else {
383 return Status::newFatal( 'config-pg-no-plpgsql', $dbName );
384 }
385 }
386 return Status::newGood();
387 }
388 }