PageRenderTime 44ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/mediawiki-integration/source/php/mediawiki/maintenance/installExtension.php

https://code.google.com/
PHP | 642 lines | 253 code | 61 blank | 328 comment | 76 complexity | c8003ec4ed309d2ab9f6d23ebd738281 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0
  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. $optionsWithArgs = array( 'target', 'repository', 'repos' );
  24. require_once( 'commandLine.inc' );
  25. define('EXTINST_NOPATCH', 0);
  26. define('EXTINST_WRITEPATCH', 6);
  27. define('EXTINST_HOTPATCH', 10);
  28. class InstallerRepository {
  29. var $path;
  30. function InstallerRepository( $path ) {
  31. $this->path = $path;
  32. }
  33. function printListing( ) {
  34. trigger_error( 'override InstallerRepository::printListing()', E_USER_ERROR );
  35. }
  36. function getResource( $name ) {
  37. trigger_error( 'override InstallerRepository::getResource()', E_USER_ERROR );
  38. }
  39. /*static*/ function makeRepository( $path, $type = NULL ) {
  40. if ( !$type ) {
  41. preg_match( '!(([-+\w]+)://)?.*?(\.[-\w\d.]+)?$!', $path, $m );
  42. $proto = @$m[2];
  43. if( !$proto ) $type = 'dir';
  44. else if ( ( $proto == 'http' || $proto == 'https' )
  45. && preg_match( '!([^\w]svn|svn[^\w])!i', $path) ) $type = 'svn'; #HACK!
  46. else $type = $proto;
  47. }
  48. if ( $type == 'dir' || $type == 'file' ) return new LocalInstallerRepository( $path );
  49. else if ( $type == 'http' || $type == 'http' ) return new WebInstallerRepository( $path );
  50. else return new SVNInstallerRepository( $path );
  51. }
  52. }
  53. class LocalInstallerRepository extends InstallerRepository {
  54. function LocalInstallerRepository ( $path ) {
  55. InstallerRepository::InstallerRepository( $path );
  56. }
  57. function printListing( ) {
  58. $ff = glob( "{$this->path}/*" );
  59. if ( $ff === false || $ff === NULL ) {
  60. ExtensionInstaller::error( "listing directory $repos failed!" );
  61. return false;
  62. }
  63. foreach ( $ff as $f ) {
  64. $n = basename($f);
  65. if ( !is_dir( $f ) ) {
  66. if ( !preg_match( '/(.*)\.(tgz|tar\.gz|zip)/', $n, $m ) ) continue;
  67. $n = $m[1];
  68. }
  69. print "\t$n\n";
  70. }
  71. }
  72. function getResource( $name ) {
  73. $path = $this->path . '/' . $name;
  74. if ( !file_exists( $path ) || !is_dir( $path ) ) $path = $this->path . '/' . $name . '.tgz';
  75. if ( !file_exists( $path ) ) $path = $this->path . '/' . $name . '.tar.gz';
  76. if ( !file_exists( $path ) ) $path = $this->path . '/' . $name . '.zip';
  77. return new LocalInstallerResource( $path );
  78. }
  79. }
  80. class WebInstallerRepository extends InstallerRepository {
  81. function WebInstallerRepository ( $path ) {
  82. InstallerRepository::InstallerRepository( $path );
  83. }
  84. function printListing( ) {
  85. ExtensionInstaller::note( "listing index from {$this->path}..." );
  86. $txt = @file_get_contents( $this->path . '/index.txt' );
  87. if ( $txt ) {
  88. print $txt;
  89. print "\n";
  90. }
  91. else {
  92. $txt = file_get_contents( $this->path );
  93. if ( !$txt ) {
  94. ExtensionInstaller::error( "listing index from {$this->path} failed!" );
  95. print ( $txt );
  96. return false;
  97. }
  98. $ok = preg_match_all( '!<a\s[^>]*href\s*=\s*['."'".'"]([^/'."'".'"]+)\.tgz['."'".'"][^>]*>.*?</a>!si', $txt, $m, PREG_SET_ORDER );
  99. if ( !$ok ) {
  100. ExtensionInstaller::error( "listing index from {$this->path} does not match!" );
  101. print ( $txt );
  102. return false;
  103. }
  104. foreach ( $m as $l ) {
  105. $n = $l[1];
  106. print "\t$n\n";
  107. }
  108. }
  109. }
  110. function getResource( $name ) {
  111. $path = $this->path . '/' . $name . '.tgz';
  112. return new WebInstallerResource( $path );
  113. }
  114. }
  115. class SVNInstallerRepository extends InstallerRepository {
  116. function SVNInstallerRepository ( $path ) {
  117. InstallerRepository::InstallerRepository( $path );
  118. }
  119. function printListing( ) {
  120. ExtensionInstaller::note( "SVN list {$this->path}..." );
  121. $txt = wfShellExec( 'svn ls ' . escapeshellarg( $this->path ), $code );
  122. if ( $code !== 0 ) {
  123. ExtensionInstaller::error( "svn list for {$this->path} failed!" );
  124. return false;
  125. }
  126. $ll = preg_split('/(\s*[\r\n]\s*)+/', $txt);
  127. foreach ( $ll as $line ) {
  128. if ( !preg_match('!^(.*)/$!', $line, $m) ) continue;
  129. $n = $m[1];
  130. print "\t$n\n";
  131. }
  132. }
  133. function getResource( $name ) {
  134. $path = $this->path . '/' . $name;
  135. return new SVNInstallerResource( $path );
  136. }
  137. }
  138. class InstallerResource {
  139. var $path;
  140. var $isdir;
  141. var $islocal;
  142. function InstallerResource( $path, $isdir, $islocal ) {
  143. $this->path = $path;
  144. $this->isdir= $isdir;
  145. $this->islocal = $islocal;
  146. preg_match( '!([-+\w]+://)?.*?(\.[-\w\d.]+)?$!', $path, $m );
  147. $this->protocol = @$m[1];
  148. $this->extensions = @$m[2];
  149. if ( $this->extensions ) $this->extensions = strtolower( $this->extensions );
  150. }
  151. function fetch( $target ) {
  152. trigger_error( 'override InstallerResource::fetch()', E_USER_ERROR );
  153. }
  154. function extract( $file, $target ) {
  155. if ( $this->extensions == '.tgz' || $this->extensions == '.tar.gz' ) { #tgz file
  156. ExtensionInstaller::note( "extracting $file..." );
  157. wfShellExec( 'tar zxvf ' . escapeshellarg( $file ) . ' -C ' . escapeshellarg( $target ), $code );
  158. if ( $code !== 0 ) {
  159. ExtensionInstaller::error( "failed to extract $file!" );
  160. return false;
  161. }
  162. }
  163. else if ( $this->extensions == '.zip' ) { #zip file
  164. ExtensionInstaller::note( "extracting $file..." );
  165. wfShellExec( 'unzip ' . escapeshellarg( $file ) . ' -d ' . escapeshellarg( $target ) , $code );
  166. if ( $code !== 0 ) {
  167. ExtensionInstaller::error( "failed to extract $file!" );
  168. return false;
  169. }
  170. }
  171. else {
  172. ExtensionInstaller::error( "unknown extension {$this->extensions}!" );
  173. return false;
  174. }
  175. return true;
  176. }
  177. /*static*/ function makeResource( $url ) {
  178. preg_match( '!(([-+\w]+)://)?.*?(\.[-\w\d.]+)?$!', $url, $m );
  179. $proto = @$m[2];
  180. $ext = @$m[3];
  181. if ( $ext ) $ext = strtolower( $ext );
  182. if ( !$proto ) return new LocalInstallerResource( $url, $ext ? false : true );
  183. else if ( $ext && ( $proto == 'http' || $proto == 'http' || $proto == 'ftp' ) ) return new WebInstallerResource( $url );
  184. else return new SVNInstallerResource( $url );
  185. }
  186. }
  187. class LocalInstallerResource extends InstallerResource {
  188. function LocalInstallerResource( $path ) {
  189. InstallerResource::InstallerResource( $path, is_dir( $path ), true );
  190. }
  191. function fetch( $target ) {
  192. if ( $this->isdir ) return ExtensionInstaller::copyDir( $this->path, dirname( $target ) );
  193. else return $this->extract( $this->path, dirname( $target ) );
  194. }
  195. }
  196. class WebInstallerResource extends InstallerResource {
  197. function WebInstallerResource( $path ) {
  198. InstallerResource::InstallerResource( $path, false, false );
  199. }
  200. function fetch( $target ) {
  201. $tmp = wfTempDir() . '/' . basename( $this->path );
  202. ExtensionInstaller::note( "downloading {$this->path}..." );
  203. $ok = copy( $this->path, $tmp );
  204. if ( !$ok ) {
  205. ExtensionInstaller::error( "failed to download {$this->path}" );
  206. return false;
  207. }
  208. $this->extract( $tmp, dirname( $target ) );
  209. unlink($tmp);
  210. return true;
  211. }
  212. }
  213. class SVNInstallerResource extends InstallerResource {
  214. function SVNInstallerResource( $path ) {
  215. InstallerResource::InstallerResource( $path, true, false );
  216. }
  217. function fetch( $target ) {
  218. ExtensionInstaller::note( "SVN checkout of {$this->path}..." );
  219. wfShellExec( 'svn co ' . escapeshellarg( $this->path ) . ' ' . escapeshellarg( $target ), $code );
  220. if ( $code !== 0 ) {
  221. ExtensionInstaller::error( "checkout failed for {$this->path}!" );
  222. return false;
  223. }
  224. return true;
  225. }
  226. }
  227. class ExtensionInstaller {
  228. var $source;
  229. var $target;
  230. var $name;
  231. var $dir;
  232. var $tasks;
  233. function ExtensionInstaller( $name, $source, $target ) {
  234. if ( !is_object( $source ) ) $source = InstallerResource::makeResource( $source );
  235. $this->name = $name;
  236. $this->source = $source;
  237. $this->target = realpath( $target );
  238. $this->extdir = "$target/extensions";
  239. $this->dir = "{$this->extdir}/$name";
  240. $this->incpath = "extensions/$name";
  241. $this->tasks = array();
  242. #TODO: allow a subdir different from "extensions"
  243. #TODO: allow a config file different from "LocalSettings.php"
  244. }
  245. function note( $msg ) {
  246. print "$msg\n";
  247. }
  248. function warn( $msg ) {
  249. print "WARNING: $msg\n";
  250. }
  251. function error( $msg ) {
  252. print "ERROR: $msg\n";
  253. }
  254. function prompt( $msg ) {
  255. if ( function_exists( 'readline' ) ) {
  256. $s = readline( $msg );
  257. }
  258. else {
  259. if ( !@$this->stdin ) $this->stdin = fopen( 'php://stdin', 'r' );
  260. if ( !$this->stdin ) die( "Failed to open stdin for user interaction!\n" );
  261. print $msg;
  262. flush();
  263. $s = fgets( $this->stdin );
  264. }
  265. $s = trim( $s );
  266. return $s;
  267. }
  268. function confirm( $msg ) {
  269. while ( true ) {
  270. $s = $this->prompt( $msg . " [yes/no]: ");
  271. $s = strtolower( trim($s) );
  272. if ( $s == 'yes' || $s == 'y' ) return true;
  273. else if ( $s == 'no' || $s == 'n' ) return false;
  274. else print "bad response: $s\n";
  275. }
  276. }
  277. function deleteContents( $dir ) {
  278. $ff = glob( $dir . "/*" );
  279. if ( !$ff ) return;
  280. foreach ( $ff as $f ) {
  281. if ( is_dir( $f ) && !is_link( $f ) ) $this->deleteContents( $f );
  282. unlink( $f );
  283. }
  284. }
  285. function copyDir( $dir, $tgt ) {
  286. $d = $tgt . '/' . basename( $dir );
  287. if ( !file_exists( $d ) ) {
  288. $ok = mkdir( $d );
  289. if ( !$ok ) {
  290. ExtensionInstaller::error( "failed to create director $d" );
  291. return false;
  292. }
  293. }
  294. $ff = glob( $dir . "/*" );
  295. if ( $ff === false || $ff === NULL ) return false;
  296. foreach ( $ff as $f ) {
  297. if ( is_dir( $f ) && !is_link( $f ) ) {
  298. $ok = ExtensionInstaller::copyDir( $f, $d );
  299. if ( !$ok ) return false;
  300. }
  301. else {
  302. $t = $d . '/' . basename( $f );
  303. $ok = copy( $f, $t );
  304. if ( !$ok ) {
  305. ExtensionInstaller::error( "failed to copy $f to $t" );
  306. return false;
  307. }
  308. }
  309. }
  310. return true;
  311. }
  312. function setPermissions( $dir, $dirbits, $filebits ) {
  313. if ( !chmod( $dir, $dirbits ) ) ExtensionInstaller::warn( "faield to set permissions for $dir" );
  314. $ff = glob( $dir . "/*" );
  315. if ( $ff === false || $ff === NULL ) return false;
  316. foreach ( $ff as $f ) {
  317. $n= basename( $f );
  318. if ( $n{0} == '.' ) continue; #HACK: skip dot files
  319. if ( is_link( $f ) ) continue; #skip link
  320. if ( is_dir( $f ) ) {
  321. ExtensionInstaller::setPermissions( $f, $dirbits, $filebits );
  322. }
  323. else {
  324. if ( !chmod( $f, $filebits ) ) ExtensionInstaller::warn( "faield to set permissions for $f" );
  325. }
  326. }
  327. return true;
  328. }
  329. function fetchExtension( ) {
  330. if ( $this->source->islocal && $this->source->isdir && realpath( $this->source->path ) === $this->dir ) {
  331. $this->note( "files are already in the extension dir" );
  332. return true;
  333. }
  334. if ( file_exists( $this->dir ) && glob( $this->dir . "/*" ) ) {
  335. if ( $this->confirm( "{$this->dir} exists and is not empty.\nDelete all files in that directory?" ) ) {
  336. $this->deleteContents( $this->dir );
  337. }
  338. else {
  339. return false;
  340. }
  341. }
  342. $ok = $this->source->fetch( $this->dir );
  343. if ( !$ok ) return false;
  344. if ( !file_exists( $this->dir ) && glob( $this->dir . "/*" ) ) {
  345. $this->error( "{$this->dir} does not exist or is empty. Something went wrong, sorry." );
  346. return false;
  347. }
  348. if ( file_exists( $this->dir . '/README' ) ) $this->tasks[] = "read the README file in {$this->dir}";
  349. if ( file_exists( $this->dir . '/INSTALL' ) ) $this->tasks[] = "read the INSTALL file in {$this->dir}";
  350. if ( file_exists( $this->dir . '/RELEASE-NOTES' ) ) $this->tasks[] = "read the RELEASE-NOTES file in {$this->dir}";
  351. #TODO: configure this smartly...?
  352. $this->setPermissions( $this->dir, 0755, 0644 );
  353. $this->note( "fetched extension to {$this->dir}" );
  354. return true;
  355. }
  356. function patchLocalSettings( $mode ) {
  357. #NOTE: if we get a better way to hook up extensions, that should be used instead.
  358. $f = $this->dir . '/install.settings';
  359. $t = $this->target . '/LocalSettings.php';
  360. #TODO: assert version ?!
  361. #TODO: allow custom installer scripts + sql patches
  362. if ( !file_exists( $f ) ) {
  363. $this->warn( "No install.settings file provided!" );
  364. $this->tasks[] = "Please read the instructions and edit LocalSettings.php manually to activate the extension.";
  365. return '?';
  366. }
  367. else {
  368. $this->note( "applying settings patch..." );
  369. }
  370. $settings = file_get_contents( $f );
  371. if ( !$settings ) {
  372. $this->error( "failed to read settings from $f!" );
  373. return false;
  374. }
  375. $settings = str_replace( '{{path}}', $this->incpath, $settings );
  376. if ( $mode == EXTINST_NOPATCH ) {
  377. $this->tasks[] = "Please put the following into your LocalSettings.php:" . "\n$settings\n";
  378. $this->note( "Skipping patch phase, automatic patching is off." );
  379. return true;
  380. }
  381. if ( $mode == EXTINST_HOTPATCH ) {
  382. #NOTE: keep php extension for backup file!
  383. $bak = $this->target . '/LocalSettings.install-' . $this->name . '-' . wfTimestamp(TS_MW) . '.bak.php';
  384. $ok = copy( $t, $bak );
  385. if ( !$ok ) {
  386. $this->warn( "failed to create backup of LocalSettings.php!" );
  387. return false;
  388. }
  389. else {
  390. $this->note( "created backup of LocalSettings.php at $bak" );
  391. }
  392. }
  393. $localsettings = file_get_contents( $t );
  394. if ( !$settings ) {
  395. $this->error( "failed to read $t for patching!" );
  396. return false;
  397. }
  398. $marker = "<@< extension {$this->name} >@>";
  399. $blockpattern = "/\n\s*#\s*BEGIN\s*$marker.*END\s*$marker\s*/smi";
  400. if ( preg_match( $blockpattern, $localsettings ) ) {
  401. $localsettings = preg_replace( $blockpattern, "\n", $localsettings );
  402. $this->warn( "removed old configuration block for extension {$this->name}!" );
  403. }
  404. $newblock= "\n# BEGIN $marker\n$settings\n# END $marker\n";
  405. $localsettings = preg_replace( "/\?>\s*$/si", "$newblock?>", $localsettings );
  406. if ( $mode != EXTINST_HOTPATCH ) {
  407. $t = $this->target . '/LocalSettings.install-' . $this->name . '-' . wfTimestamp(TS_MW) . '.php';
  408. }
  409. $ok = file_put_contents( $t, $localsettings );
  410. if ( !$ok ) {
  411. $this->error( "failed to patch $t!" );
  412. return false;
  413. }
  414. else if ( $mode == EXTINST_HOTPATCH ) {
  415. $this->note( "successfully patched $t" );
  416. }
  417. else {
  418. $this->note( "created patched settings file $t" );
  419. $this->tasks[] = "Replace your current LocalSettings.php with ".basename($t);
  420. }
  421. return true;
  422. }
  423. function printNotices( ) {
  424. if ( !$this->tasks ) {
  425. $this->note( "Installation is complete, no pending tasks" );
  426. }
  427. else {
  428. $this->note( "" );
  429. $this->note( "PENDING TASKS:" );
  430. $this->note( "" );
  431. foreach ( $this->tasks as $t ) {
  432. $this->note ( "* " . $t );
  433. }
  434. $this->note( "" );
  435. }
  436. return true;
  437. }
  438. }
  439. $tgt = isset ( $options['target'] ) ? $options['target'] : $IP;
  440. $repos = @$options['repository'];
  441. if ( !$repos ) $repos = @$options['repos'];
  442. if ( !$repos ) $repos = @$wgExtensionInstallerRepository;
  443. if ( !$repos && file_exists("$tgt/.svn") && is_dir("$tgt/.svn") ) {
  444. $svn = file_get_contents( "$tgt/.svn/entries" );
  445. if ( preg_match( '!url="(.*?)"!', $svn, $m ) ) {
  446. $repos = dirname( $m[1] ) . '/extensions';
  447. }
  448. }
  449. if ( !$repos ) $repos = 'http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions';
  450. if( !isset( $args[0] ) && !@$options['list'] ) {
  451. die( "USAGE: installExtension.php [options] <name> [source]\n" .
  452. "OPTIONS: \n" .
  453. " --list list available extensions. <name> is ignored / may be omitted.\n" .
  454. " --repository <n> repository to fetch extensions from. May be a local directoy,\n" .
  455. " an SVN repository or a HTTP directory\n" .
  456. " --target <dir> mediawiki installation directory to use\n" .
  457. " --nopatch don't create a patched LocalSettings.php\n" .
  458. " --hotpatch patched LocalSettings.php directly (creates a backup)\n" .
  459. "SOURCE: specifies the package source directly. If given, the repository is ignored.\n" .
  460. " The source my be a local file (tgz or zip) or directory, the URL of a\n" .
  461. " remote file (tgz or zip), or a SVN path.\n"
  462. );
  463. }
  464. $repository = InstallerRepository::makeRepository( $repos );
  465. if ( isset( $options['list'] ) ) {
  466. $repository->printListing();
  467. exit(0);
  468. }
  469. $name = $args[0];
  470. $src = isset( $args[1] ) ? $args[1] : $repository->getResource( $name );
  471. #TODO: detect $source mismatching $name !!
  472. $mode = EXTINST_WRITEPATCH;
  473. if ( isset( $options['nopatch'] ) || @$wgExtensionInstallerNoPatch ) $mode = EXTINST_NOPATCH;
  474. else if ( isset( $options['hotpatch'] ) || @$wgExtensionInstallerHotPatch ) $mode = EXTINST_HOTPATCH;
  475. if ( !file_exists( "$tgt/LocalSettings.php" ) ) {
  476. die("can't find $tgt/LocalSettings.php\n");
  477. }
  478. if ( $mode == EXTINST_HOTPATCH && !is_writable( "$tgt/LocalSettings.php" ) ) {
  479. die("can't write to $tgt/LocalSettings.php\n");
  480. }
  481. if ( !file_exists( "$tgt/extensions" ) ) {
  482. die("can't find $tgt/extensions\n");
  483. }
  484. if ( !is_writable( "$tgt/extensions" ) ) {
  485. die("can't write to $tgt/extensions\n");
  486. }
  487. $installer = new ExtensionInstaller( $name, $src, $tgt );
  488. $installer->note( "Installing extension {$installer->name} from {$installer->source->path} to {$installer->dir}" );
  489. print "\n";
  490. print "\tTHIS TOOL IS EXPERIMENTAL!\n";
  491. print "\tEXPECT THE UNEXPECTED!\n";
  492. print "\n";
  493. if ( !$installer->confirm("continue") ) die("aborted\n");
  494. $ok = $installer->fetchExtension();
  495. if ( $ok ) $ok = $installer->patchLocalSettings( $mode );
  496. if ( $ok ) $ok = $installer->printNotices();
  497. if ( $ok ) $installer->note( "$name extension installed." );
  498. ?>