made --nopatch mode for installExtension.php a bit smarter
[lhc/web/wiklou.git] / maintenance / installExtension.php
1 <?php
2 /**
3 * Copyright (C) 2006 Daniel Kinzler, brightbyte.de
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 * @package MediaWiki
21 * @subpackage Maintenance
22 */
23
24 $optionsWithArgs = array( 'target' );
25
26 require_once( 'commandLine.inc' );
27
28 class ExtensionInstaller {
29 var $source;
30 var $target;
31 var $name;
32 var $dir;
33
34 function ExtensionInstaller( $name, $source, $target ) {
35 $this->name = $name;
36 $this->source = $source;
37 $this->target = realpath( $target );
38 $this->extdir = "$target/extensions";
39 $this->dir = "{$this->extdir}/$name";
40 $this->incpath = "extensions/$name";
41
42 #TODO: allow a subdir different from "extensions"
43 #TODO: allow a config file different from "LocalSettings.php"
44 }
45
46 function note( $msg ) {
47 print "$msg\n";
48 }
49
50 function warn( $msg ) {
51 print "WARNING: $msg\n";
52 }
53
54 function error( $msg ) {
55 print "ERROR: $msg\n";
56 }
57
58 function prompt( $msg ) {
59 if ( function_exists( 'readline' ) ) {
60 $s = readline( $msg );
61 }
62 else {
63 if ( !@$this->stdin ) $this->stdin = fopen( 'php://stdin', 'r' );
64 if ( !$this->stdin ) die( "Failed to open stdin for user interaction!\n" );
65
66 print $msg;
67 flush();
68
69 $s = fgets( $this->stdin );
70 }
71
72 $s = trim( $s );
73 return $s;
74 }
75
76 function confirm( $msg ) {
77 while ( true ) {
78 $s = $this->prompt( $msg . " [yes/no]: ");
79 $s = strtolower( trim($s) );
80
81 if ( $s == 'yes' || $s == 'y' ) return true;
82 else if ( $s == 'no' || $s == 'n' ) return false;
83 else print "bad response: $s\n";
84 }
85 }
86
87 function deleteContents( $dir ) {
88 $ff = glob( $dir . "/*" );
89 if ( !$ff ) return;
90
91 foreach ( $ff as $f ) {
92 if ( is_dir( $f ) ) $this->deleteContents( $f );
93 unlink( $f );
94 }
95 }
96
97 function copyDir( $dir, $tgt ) {
98 $d = $tgt . '/' . basename( $dir );
99
100 if ( !file_exists( $d ) ) {
101 $ok = mkdir( $d );
102 if ( !$ok ) {
103 $this->error( "failed to create director $d" );
104 return false;
105 }
106 }
107
108 $ff = glob( $dir . "/*" );
109 if ( $ff === false || $ff === NULL ) return false;
110
111 foreach ( $ff as $f ) {
112 if ( is_dir( $f ) ) {
113 $ok = $this->copyDir( $f, $d );
114 if ( !$ok ) return false;
115 }
116 else {
117 $t = $d . '/' . basename( $f );
118 $ok = copy( $f, $t );
119
120 if ( !$ok ) {
121 $this->error( "failed to copy $f to $t" );
122 return false;
123 }
124 }
125 }
126
127 return true;
128 }
129
130 function fetchExtension( ) {
131 if ( file_exists( $this->dir ) && glob( $this->dir . "/*" )
132 && realpath( $this->source ) != $this->dir ) {
133
134 if ( $this->confirm( "{$this->dir} exists and is not empty.\nDelete all files in that directory?" ) ) {
135 $this->deleteContents( $this->dir );
136 }
137 else {
138 return false;
139 }
140 }
141
142 preg_match( '!([-\w]+://)?.*?(\.[-\w\d.]+)?$!', $this->source, $m );
143 $proto = @$m[1];
144 $ext = @$m[2];
145 if ( $ext ) $ext = strtolower( $ext );
146
147 $src = $this->source;
148
149 #TODO: check that the required program is available.
150 #may be used: tar, unzip, svn
151
152 if ( $proto && $ext ) { #remote file
153 $tmp = wfTempDir() . '/' . basename( $src );
154
155 $this->note( "fetching {$this->source}..." );
156 $ok = copy( $src, $tmp );
157
158 if ( !$ok ) {
159 $this->error( "failed to download {$src}" );
160 return false;
161 }
162
163 $src = $tmp;
164 $proto = NULL;
165 }
166
167 if ( $proto ) { #assume SVN repository
168 $this->note( "SVN checkout of $src..." );
169 wfShellExec( 'svn co ' . escapeshellarg( $src ) . ' ' . escapeshellarg( $this->dir ), $code );
170
171 if ( $code !== 0 ) {
172 $this->error( "checkout failed for $src!" );
173 return false;
174 }
175 }
176 else { #local file or directory
177 $src = realpath ( $src );
178
179 if ( !file_exists( $src ) ) {
180 $this->error( "file not found: {$this->source}" );
181 return false;
182 }
183
184 if ( $ext === NULL || $ext === '') { #local dir
185 if ( $src == $this->dir ) {
186 $this->note( "files are already in the extension dir" );
187 return true;
188 }
189
190 $this->copyDir( $src, $this->extdir );
191 }
192 else if ( $ext == '.tgz' || $ext == '.tar.gz' ) { #tgz file
193 $this->note( "extracting $src..." );
194 wfShellExec( 'tar zxvf ' . escapeshellarg( $src ) . ' -C ' . escapeshellarg( $this->extdir ), $code );
195
196 if ( $code !== 0 ) {
197 $this->error( "failed to extract $src!" );
198 return false;
199 }
200 }
201 else if ( $ext == '.zip' ) { #zip file
202 $this->note( "extracting $src..." );
203 wfShellExec( 'unzip ' . escapeshellarg( $src ) . ' -d ' . escapeshellarg( $this->extdir ) , $code );
204
205 if ( $code !== 0 ) {
206 $this->error( "failed to extract $src!" );
207 return false;
208 }
209 }
210 else {
211 $this->error( "unknown file extension: $ext" );
212 return false;
213 }
214 }
215
216 if ( !file_exists( $this->dir ) && glob( $this->dir . "/*" ) ) {
217 $this->error( "{$this->dir} does not exist or is empty. Something went wrong, sorry." );
218 return false;
219 }
220
221 #TODO: set permissions.... somehow. Copy from extension dir??
222
223 $this->note( "fetched extension to {$this->dir}" );
224 return true;
225 }
226
227 function patchLocalSettings( $nopatch ) {
228 #NOTE: if we get a better way to hook up extensions, that should be used instead.
229
230 $f = $this->dir . '/install.settings';
231 $t = $this->target . '/LocalSettings.php';
232
233 #TODO: assert version ?!
234 #TODO: allow custom installer scripts + sql patches
235
236 if ( !file_exists( $f ) ) {
237 $this->note( "" );
238 $this->warn( "No install.settings file provided! Please read the instructions and edit LocalSettings.php manually." );
239 $this->note( "" );
240 return '?';
241 }
242
243 $settings = file_get_contents( $f );
244
245 if ( !$settings ) {
246 $this->error( "failed to read settings from $f!" );
247 return false;
248 }
249
250 $settings = str_replace( '{{path}}', $this->incpath, $settings );
251
252 if ( $nopatch ) {
253 $this->note( "" );
254 $this->note( "Automatic patching is off. Please put the following into your LocalSettings.php:" );
255 print " \n$settings\n";
256
257 return true;
258 }
259
260 #NOTE: keep php extension for backup file!
261 $bak = $this->target . '/LocalSettings.install-' . $this->name . '-' . wfTimestamp(TS_MW) . '.bak.php';
262
263 $ok = copy( $t, $bak );
264
265 if ( !$ok ) {
266 $this->warn( "failed to create backup of LocalSettings.php!" );
267 return false;
268 }
269 else {
270 $this->note( "created backup of LocalSettings.php at $bak" );
271 }
272
273 $localsettings = file_get_contents( $t );
274
275 if ( !$settings ) {
276 $this->error( "failed to read $t for patching!" );
277 return false;
278 }
279
280 $marker = "<@< extension {$this->name} >@>";
281 $blockpattern = "/\n\s*#\s*BEGIN\s*$marker.*END\s*$marker\s*/smi";
282
283 if ( preg_match( $blockpattern, $localsettings ) ) {
284 $localsettings = preg_replace( $blockpattern, "\n", $localsettings );
285 $this->warn( "removed old configuration block for extension {$this->name}!" );
286 }
287
288 $newblock= "\n# BEGIN $marker\n$settings\n# END $marker\n";
289
290 $localsettings = preg_replace( "/\?>\s*$/si", "$newblock?>", $localsettings );
291
292 $ok = file_put_contents( $t, $localsettings );
293
294 if ( !$ok ) {
295 $this->error( "failed to patch $t!" );
296 return false;
297 }
298 else {
299 $this->note( "successfully patched LocalSettings.php" );
300 }
301
302 return true;
303 }
304
305 function printNotices( ) {
306 $files = array();
307
308 if ( file_exists( $this->dir . '/README' ) ) $files[] = 'README';
309 if ( file_exists( $this->dir . '/INSTALL' ) ) $files[] = 'INSTALL';
310
311 if ( !$files ) {
312 $this->note( "no information files found in {$this->dir}" );
313 }
314 else {
315 $this->note( "" );
316
317 $this->note( "Please have a look at the following files in {$this->dir}," );
318 $this->note( "they may contain important information about {$this->name}." );
319
320 $this->note( "" );
321
322 foreach ( $files as $f ) {
323 $this->note ( "\t* $f" );
324 }
325
326 $this->note( "" );
327 }
328
329 return true;
330 }
331
332 /* static */ function listRepository( $repos ) {
333 preg_match( '!([-\w]+://)?.*?(\.[-\w\d.]+)?$!', $repos, $m );
334 $proto = @$m[1];
335
336 #TODO: right now, this basically lists filenames, so it's not terribly useful.
337 #In future, there should be a "repository + logical name" scheme
338
339 if ( $proto == 'http://' ) { #HTML directory listing
340 ExtensionInstaller::note( "listing index from $repos..." );
341
342 $txt = file_get_contents( $repos );
343
344 $ok = preg_match_all( '!<a\s[^>]*href\s*=\s*['."'".'"]([^/'."'".'"]+)['."'".'"][^>]*>.*?</a>!si', $txt, $m, PREG_SET_ORDER );
345 if ( !$ok ) {
346 ExtensionInstaller::error( "listing index from $repos failed!" );
347 print ( $txt );
348 return false;
349 }
350
351 foreach ( $m as $l ) {
352 $n = $l[1];
353
354 if ( preg_match('!^[./?]!', $n) ) continue;
355
356 ExtensionInstaller::note( "\t$n" );
357 }
358 }
359 else if ( !$proto ) { #local directory
360 ExtensionInstaller::note( "listing directory $repos..." );
361
362 $ff = glob( "$repos/*" );
363 if ( $ff === false || $ff === NULL ) {
364 ExtensionInstaller::error( "listing directory $repos failed!" );
365 return false;
366 }
367
368 foreach ( $ff as $f ) {
369 $n = basename($f);
370
371 ExtensionInstaller::note( "\t$n" );
372 }
373 }
374 else { #assume svn
375 ExtensionInstaller::note( "SVN list $repos..." );
376 $txt = wfShellExec( 'svn ls ' . escapeshellarg( $repos ), $code );
377
378 if ( $code !== 0 ) {
379 ExtensionInstaller::error( "svn list for $repos failed!" );
380 return false;
381 }
382
383 $ll = preg_split('/(\s*[\r\n]\s*)+/', $txt);
384
385 foreach ( $ll as $line ) {
386 if ( !preg_match('!^(.*)/$!', $line, $m) ) continue;
387
388 ExtensionInstaller::note( "\t{$m[1]}" );
389 }
390 }
391 }
392 }
393
394 if ( isset( $options['list'] ) ) {
395 $repos = $options['list'];
396 if ( $repos === true || $repos === 1 ) {
397 # Default to SVN trunk. Perhaps change that to use the version of the present install,
398 # and/or use bundles at an official download location.
399 $repos = 'http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions/';
400 }
401
402 ExtensionInstaller::listRepository( $repos );
403
404 exit(0);
405 }
406
407 if( !isset( $args[0] ) ) {
408 die( "USAGE: installExtension.php [options] name [source]\n" .
409 "OPTIONS: \n" .
410 " --target=<dir> mediawiki installation directory\n" .
411 " --nopatch don't touch LocalSettings.php\n" .
412 "SOURCE: \n" .
413 " May be a local file (tgz or zip) or directory.\n" .
414 " May be the URL of a remote file (tgz or zip).\n" .
415 " May be a SVN repository\n"
416 );
417 }
418
419 $name = $args[0];
420
421 # Default to SVN trunk. Perhaps change that to use the version of the present install,
422 # and/or use bundles at an official download location.
423 $defsrc = "http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions/" . urlencode($name);
424
425 $src = isset ( $args[1] ) ? $args[1] : $defsrc;
426
427 $tgt = isset ( $options['target'] ) ? $options['target'] : $IP;
428
429 $nopatch = isset( $options['nopatch'] ) || @$wgExtensionInstallerNoPatch;
430
431 if ( !file_exists( "$tgt/LocalSettings.php" ) ) {
432 die("can't find $tgt/LocalSettings.php\n");
433 }
434
435 if ( !$nopatch && !is_writable( "$tgt/LocalSettings.php" ) ) {
436 die("can't write to $tgt/LocalSettings.php\n");
437 }
438
439 if ( !file_exists( "$tgt/extensions" ) ) {
440 die("can't find $tgt/extensions\n");
441 }
442
443 if ( !is_writable( "$tgt/extensions" ) ) {
444 die("can't write to $tgt/extensions\n");
445 }
446
447 $installer = new ExtensionInstaller( $name, $src, $tgt );
448
449 $installer->note( "Installing extension {$installer->name} from {$installer->source} to {$installer->dir}" );
450
451 print "\n";
452 print "\tTHIS TOOL IS EXPERIMENTAL!\n";
453 print "\tEXPECT THE UNEXPECTED!\n";
454 print "\n";
455
456 if ( !$installer->confirm("continue") ) die("aborted\n");
457
458 $ok = $installer->fetchExtension();
459
460 if ( $ok ) $ok = $installer->patchLocalSettings( $nopatch );
461
462 $ok = $installer->printNotices();
463
464 if ( $ok ) $installer->note( "$name extension was installed successfully" );
465 ?>