1<?php
2/**
3 * Merge $wgExtensionMessagesFiles from various extensions to produce a
4 * single array containing all message files.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Maintenance
23 */
24
25// NO_AUTOLOAD -- file-scope define() used to modify behaviour
26
27# Start from scratch
28define( 'MW_NO_EXTENSION_MESSAGES', 1 );
29
30require_once __DIR__ . '/Maintenance.php';
31$maintClass = MergeMessageFileList::class;
32$mmfl = false;
33
34/**
35 * Maintenance script that merges $wgExtensionMessagesFiles from various
36 * extensions to produce a single array containing all message files.
37 *
38 * @ingroup Maintenance
39 */
40class MergeMessageFileList extends Maintenance {
41	public function __construct() {
42		parent::__construct();
43		$this->addOption(
44			'list-file',
45			'A file containing a list of extension setup files, one per line.',
46			false,
47			true
48		);
49		$this->addOption( 'extensions-dir', 'Path where extensions can be found.', false, true );
50		$this->addOption( 'output', 'Send output to this file (omit for stdout)', false, true );
51		$this->addDescription( 'Merge $wgExtensionMessagesFiles and $wgMessagesDirs from ' .
52			' various extensions to produce a single file listing all message files and dirs.'
53		);
54	}
55
56	public function execute() {
57		global $mmfl;
58		global $wgExtensionEntryPointListFiles;
59
60		if ( !count( $wgExtensionEntryPointListFiles )
61			&& !$this->hasOption( 'list-file' )
62			&& !$this->hasOption( 'extensions-dir' )
63		) {
64			$this->fatalError( "Either --list-file or --extensions-dir must be provided if " .
65				"\$wgExtensionEntryPointListFiles is not set" );
66		}
67
68		$mmfl = [ 'setupFiles' => [] ];
69
70		# Add setup files contained in file passed to --list-file
71		if ( $this->hasOption( 'list-file' ) ) {
72			$extensionPaths = $this->readFile( $this->getOption( 'list-file' ) );
73			$mmfl['setupFiles'] = array_merge( $mmfl['setupFiles'], $extensionPaths );
74		}
75
76		# Now find out files in a directory
77		if ( $this->hasOption( 'extensions-dir' ) ) {
78			$extdir = $this->getOption( 'extensions-dir' );
79			# Allow multiple directories to be passed with ":" as delimiter
80			$extdirs = explode( ':', $extdir );
81			foreach ( $extdirs as $extdir ) {
82				$entries = scandir( $extdir );
83				foreach ( $entries as $extname ) {
84					if ( $extname == '.' || $extname == '..' || !is_dir( "$extdir/$extname" ) ) {
85						continue;
86					}
87					$possibilities = [
88						"$extdir/$extname/extension.json",
89						"$extdir/$extname/skin.json",
90						"$extdir/$extname/$extname.php"
91					];
92					$found = false;
93					foreach ( $possibilities as $extfile ) {
94						if ( file_exists( $extfile ) ) {
95							$mmfl['setupFiles'][] = $extfile;
96							$found = true;
97							break;
98						}
99					}
100
101					if ( !$found ) {
102						$this->error( "Extension {$extname} in {$extdir} lacks expected entry point: " .
103							"extension.json, skin.json, or {$extname}.php." );
104					}
105				}
106			}
107		}
108
109		# Add setup files defined via configuration
110		foreach ( $wgExtensionEntryPointListFiles as $points ) {
111			$extensionPaths = $this->readFile( $points );
112			$mmfl['setupFiles'] = array_merge( $mmfl['setupFiles'], $extensionPaths );
113		}
114
115		if ( $this->hasOption( 'output' ) ) {
116			$mmfl['output'] = $this->getOption( 'output' );
117		}
118		if ( $this->hasOption( 'quiet' ) ) {
119			$mmfl['quiet'] = true;
120		}
121	}
122
123	public function finalSetup() {
124		# This script commonly needs to be run before the l10n cache. But if
125		# $wgLanguageCode is not 'en', it won't be able to run because there is
126		# no l10n cache. Break the cycle by forcing $wgLanguageCode = 'en'.
127		global $wgLanguageCode;
128		$wgLanguageCode = 'en';
129		parent::finalSetup();
130	}
131
132	/**
133	 * Database access is not needed.
134	 *
135	 * @return int DB constant
136	 */
137	public function getDbType() {
138		return Maintenance::DB_NONE;
139	}
140
141	/**
142	 * @param string $fileName
143	 * @return array List of absolute extension paths
144	 */
145	private function readFile( $fileName ) {
146		global $IP;
147
148		$files = [];
149		$fileLines = file( $fileName );
150		if ( $fileLines === false ) {
151			$this->error( "Unable to open list file $fileName." );
152
153			return $files;
154		}
155		# Strip comments, discard empty lines, and trim leading and trailing
156		# whitespace. Comments start with '#' and extend to the end of the line.
157		foreach ( $fileLines as $extension ) {
158			$extension = trim( preg_replace( '/#.*/', '', $extension ) );
159			if ( $extension !== '' ) {
160				# Paths may use the string $IP to be substituted by the actual value
161				$extension = str_replace( '$IP', $IP, $extension );
162				if ( file_exists( $extension ) ) {
163					$files[] = $extension;
164				} else {
165					$this->error( "Extension {$extension} doesn't exist" );
166				}
167			}
168		}
169
170		return $files;
171	}
172}
173
174require_once RUN_MAINTENANCE_IF_MAIN;
175
176$queue = [];
177'@phan-var string[][] $mmfl';
178foreach ( $mmfl['setupFiles'] as $fileName ) {
179	if ( strval( $fileName ) === '' ) {
180		continue;
181	}
182	if ( empty( $mmfl['quiet'] ) ) {
183		fwrite( STDERR, "Loading data from $fileName\n" );
184	}
185	// Using extension.json or skin.json
186	if ( substr( $fileName, -strlen( '.json' ) ) === '.json' ) {
187		$queue[$fileName] = 1;
188	} else {
189		require_once $fileName;
190	}
191}
192
193if ( $queue ) {
194	$registry = new ExtensionRegistry();
195	$data = $registry->readFromQueue( $queue );
196	foreach ( [ 'wgExtensionMessagesFiles', 'wgMessagesDirs' ] as $var ) {
197		if ( isset( $data['globals'][$var] ) ) {
198			$GLOBALS[$var] = array_merge( $data['globals'][$var], $GLOBALS[$var] );
199		}
200	}
201}
202
203if ( empty( $mmfl['quiet'] ) ) {
204	fwrite( STDERR, "\n" );
205}
206$s =
207	"<?php\n" .
208	"## This file is generated by mergeMessageFileList.php. Do not edit it directly.\n\n" .
209	"if ( defined( 'MW_NO_EXTENSION_MESSAGES' ) ) return;\n\n" .
210	'$wgExtensionMessagesFiles = ' . var_export( $wgExtensionMessagesFiles, true ) . ";\n\n" .
211	'$wgMessagesDirs = ' . var_export( $wgMessagesDirs, true ) . ";\n\n";
212
213$dirs = [
214	$IP,
215	dirname( __DIR__ ),
216	realpath( $IP )
217];
218
219foreach ( $dirs as $dir ) {
220	$s = preg_replace( "/'" . preg_quote( $dir, '/' ) . "([^']*)'/", '"$IP\1"', $s );
221}
222
223if ( isset( $mmfl['output'] ) ) {
224	$outputFile = $mmfl['output'];
225	$res = file_put_contents( $outputFile, $s );
226	if ( $res === false ) {
227		fwrite( STDERR, "Failed to write to $outputFile\n" );
228		exit( 1 );
229	}
230} else {
231	echo $s;
232}
233