Merge "ChangeTag::createTagWithChecks calls ChangeTag::canCreateTag()"
[lhc/web/wiklou.git] / maintenance / resources / manageForeignResources.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Maintenance
20 */
21
22 require_once __DIR__ . '/../Maintenance.php';
23
24 /**
25 * Manage foreign resources registered with ResourceLoader.
26 *
27 * @ingroup Maintenance
28 * @since 1.32
29 */
30 class ManageForeignResources extends Maintenance {
31 private $defaultAlgo = 'sha384';
32 private $tmpParentDir;
33
34 public function __construct() {
35 global $IP;
36 parent::__construct();
37 $this->addDescription( <<<TEXT
38 Manage foreign resources registered with ResourceLoader.
39
40 This helps developers to download, verify and update local copies of upstream
41 libraries registered as ResourceLoader modules. See also foreign-resources.yaml.
42
43 For sources that don't publish an integrity hash, leave the value empty at
44 first, and run this script with --make-sri to compute the hashes.
45
46 This script runs in dry mode by default. Use --update to actually change, remove,
47 or add files to /resources/lib/.
48 TEXT
49 );
50 $this->addArg( 'module', 'Name of a single module (Default: all)', false );
51 $this->addOption( 'update', ' resources/lib/ missing integrity metadata' );
52 $this->addOption( 'make-sri', 'Compute missing integrity metadata' );
53 $this->addOption( 'verbose', 'Be verbose' );
54
55 // Use a directory in $IP instead of wfTempDir() because
56 // PHP's rename() does not work across file systems.
57 $this->tmpParentDir = "{$IP}/resources/tmp";
58 }
59
60 public function execute() {
61 global $IP;
62 $module = $this->getArg();
63 $makeSRI = $this->hasOption( 'make-sri' );
64
65 $registry = $this->parseBasicYaml(
66 file_get_contents( __DIR__ . '/foreign-resources.yaml' )
67 );
68 foreach ( $registry as $moduleName => $info ) {
69 if ( $module !== null && $moduleName !== $module ) {
70 continue;
71 }
72 $this->verbose( "\n### {$moduleName}\n\n" );
73
74 // Validate required keys
75 $info += [ 'src' => null, 'integrity' => null, 'dest' => null ];
76 if ( $info['src'] === null ) {
77 $this->fatalError( "Module '$moduleName' must have a 'src' key." );
78 }
79 $integrity = is_string( $info['integrity'] ) ? $info['integrity'] : $makeSRI;
80 if ( $integrity === false ) {
81 $this->fatalError( "Module '$moduleName' must have an 'integrity' key." );
82 }
83
84 // Download the resource
85 $data = Http::get( $info['src'], [ 'followRedirects' => false ] );
86 if ( $data === false ) {
87 $this->fatalError( "Failed to download resource for '$moduleName'." );
88 }
89
90 // Validate integrity metadata
91 $this->output( "... checking integrity of '{$moduleName}'\n" );
92 $algo = $integrity === true ? $this->defaultAlgo : explode( '-', $integrity )[0];
93 $actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
94 if ( $integrity === true ) {
95 $this->output( "Integrity for '{$moduleName}':\n\t${actualIntegrity}\n" );
96 continue;
97 } elseif ( $integrity !== $actualIntegrity ) {
98 $this->fatalError( "Integrity check failed for '{$moduleName}:\n" .
99 "Expected: {$integrity}\n" .
100 "Actual: {$actualIntegrity}"
101 );
102 }
103
104 // Determine destination
105 $destDir = "{$IP}/resources/lib/$moduleName";
106 $this->output( "... extracting files for '{$moduleName}'\n" );
107 $this->handleTypeTar( $moduleName, $data, $destDir, $info );
108 }
109
110 // Clean up
111 wfRecursiveRemoveDir( $this->tmpParentDir );
112 $this->output( "\nDone!\n" );
113 }
114
115 private function handleTypeTar( $moduleName, $data, $destDir, array $info ) {
116 global $IP;
117 wfRecursiveRemoveDir( $this->tmpParentDir );
118 if ( !wfMkdirParents( $this->tmpParentDir ) ) {
119 $this->fatalError( "Unable to create {$this->tmpParentDir}" );
120 }
121
122 // Write resource to temporary file and open it
123 $tmpFile = "{$this->tmpParentDir}/$moduleName.tar";
124 $this->verbose( "... writing '$moduleName' src to $tmpFile\n" );
125 file_put_contents( $tmpFile, $data );
126 $p = new PharData( $tmpFile );
127 $tmpDir = "{$this->tmpParentDir}/$moduleName";
128 $p->extractTo( $tmpDir );
129 unset( $data, $p );
130
131 if ( $info['dest'] === null ) {
132 // Replace the entire directory as-is
133 if ( !$this->hasOption( 'update' ) ) {
134 $this->output( "[dry run] Would replace /resources/lib/$moduleName\n" );
135 } else {
136 wfRecursiveRemoveDir( $destDir );
137 if ( !rename( $tmpDir, $destDir ) ) {
138 $this->fatalError( "Could not move $destDir to $tmpDir." );
139 }
140 }
141 return;
142 }
143
144 // Create and/or empty the destination
145 if ( !$this->hasOption( 'update' ) ) {
146 $this->output( "... [dry run] would empty /resources/lib/$moduleName\n" );
147 } else {
148 wfRecursiveRemoveDir( $destDir );
149 wfMkdirParents( $destDir );
150 }
151
152 // Expand and normalise the 'dest' entries
153 $toCopy = [];
154 foreach ( $info['dest'] as $fromSubPath => $toSubPath ) {
155 // Use glob() to expand wildcards and check existence
156 $fromPaths = glob( "{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
157 if ( !$fromPaths ) {
158 $this->fatalError( "Path '$fromSubPath' of '$moduleName' not found." );
159 }
160 foreach ( $fromPaths as $fromPath ) {
161 $toCopy[$fromPath] = $toSubPath === null
162 ? "$destDir/" . basename( $fromPath )
163 : "$destDir/$toSubPath/" . basename( $fromPath );
164 }
165 }
166 foreach ( $toCopy as $from => $to ) {
167 if ( !$this->hasOption( 'update' ) ) {
168 $shortFrom = strtr( $from, [ "$tmpDir/" => '' ] );
169 $shortTo = strtr( $to, [ "$IP/" => '' ] );
170 $this->output( "... [dry run] would move $shortFrom to $shortTo\n" );
171 } else {
172 $this->verbose( "... moving $from to $to\n" );
173 wfMkdirParents( dirname( $to ) );
174 if ( !rename( $from, $to ) ) {
175 $this->fatalError( "Could not move $from to $to." );
176 }
177 }
178 }
179 }
180
181 private function verbose( $text ) {
182 if ( $this->hasOption( 'verbose' ) ) {
183 $this->output( $text );
184 }
185 }
186
187 /**
188 * Basic YAML parser.
189 *
190 * Supports only string or object values, and 2 spaces indentation.
191 *
192 * @todo Just ship symfony/yaml.
193 * @param string $input
194 * @return array
195 */
196 private function parseBasicYaml( $input ) {
197 $lines = explode( "\n", $input );
198 $root = [];
199 $stack = [ &$root ];
200 $prev = 0;
201 foreach ( $lines as $i => $text ) {
202 $line = $i + 1;
203 $trimmed = ltrim( $text, ' ' );
204 if ( $trimmed === '' || $trimmed[0] === '#' ) {
205 continue;
206 }
207 $indent = strlen( $text ) - strlen( $trimmed );
208 if ( $indent % 2 !== 0 ) {
209 throw new Exception( __METHOD__ . ": Odd indentation on line $line." );
210 }
211 $depth = $indent === 0 ? 0 : ( $indent / 2 );
212 if ( $depth < $prev ) {
213 // Close previous branches we can't re-enter
214 array_splice( $stack, $depth + 1 );
215 }
216 if ( !array_key_exists( $depth, $stack ) ) {
217 throw new Exception( __METHOD__ . ": Too much indentation on line $line." );
218 }
219 if ( strpos( $trimmed, ':' ) === false ) {
220 throw new Exception( __METHOD__ . ": Missing colon on line $line." );
221 }
222 $dest =& $stack[ $depth ];
223 if ( $dest === null ) {
224 // Promote from null to object
225 $dest = [];
226 }
227 list( $key, $val ) = explode( ':', $trimmed, 2 );
228 $val = ltrim( $val, ' ' );
229 if ( $val !== '' ) {
230 // Add string
231 $dest[ $key ] = $val;
232 } else {
233 // Add null (may become an object later)
234 $val = null;
235 $stack[] = &$val;
236 $dest[ $key ] = &$val;
237 }
238 $prev = $depth;
239 unset( $dest, $val );
240 }
241 return $root;
242 }
243 }
244
245 $maintClass = ManageForeignResources::class;
246 require_once RUN_MAINTENANCE_IF_MAIN;