1 | <?php |
||
2 | /** |
||
3 | * Copyright © Vaimo Group. All rights reserved. |
||
4 | * See LICENSE_VAIMO.txt for license details. |
||
5 | */ |
||
6 | namespace Vaimo\ComposerPatches\Patch\File; |
||
7 | |||
8 | use Vaimo\ComposerPatches\Config as PluginConfig; |
||
9 | use Vaimo\ComposerPatches\Repository\PatchesApplier\Operation as PatcherOperation; |
||
10 | |||
11 | class Applier |
||
12 | { |
||
13 | /** |
||
14 | * @var \Vaimo\ComposerPatches\Logger |
||
15 | */ |
||
16 | private $logger; |
||
17 | |||
18 | /** |
||
19 | * @var \Vaimo\ComposerPatches\Shell |
||
20 | */ |
||
21 | private $shell; |
||
22 | |||
23 | /** |
||
24 | * @var array |
||
25 | */ |
||
26 | private $config; |
||
27 | |||
28 | /** |
||
29 | * @var \Vaimo\ComposerPatches\Utils\ConfigUtils |
||
30 | */ |
||
31 | private $applierUtils; |
||
32 | |||
33 | /** |
||
34 | * @var \Vaimo\ComposerPatches\Utils\TemplateUtils |
||
35 | */ |
||
36 | private $templateUtils; |
||
37 | |||
38 | /** |
||
39 | * @var \Vaimo\ComposerPatches\Utils\DataUtils |
||
40 | */ |
||
41 | private $dataUtils; |
||
42 | |||
43 | /** |
||
44 | * @var array |
||
45 | */ |
||
46 | private $resultCache; |
||
47 | |||
48 | /** |
||
49 | * @param \Vaimo\ComposerPatches\Logger $logger |
||
50 | * @param array $config |
||
51 | */ |
||
52 | public function __construct( |
||
53 | \Vaimo\ComposerPatches\Logger $logger, |
||
54 | array $config |
||
55 | ) { |
||
56 | $this->logger = $logger; |
||
57 | $this->config = $config; |
||
58 | |||
59 | $this->shell = new \Vaimo\ComposerPatches\Shell($logger); |
||
60 | $this->applierUtils = new \Vaimo\ComposerPatches\Utils\ConfigUtils(); |
||
61 | $this->templateUtils = new \Vaimo\ComposerPatches\Utils\TemplateUtils(); |
||
62 | $this->dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils(); |
||
63 | } |
||
64 | |||
65 | public function applyFile($filename, $cwd, array $config = array()) |
||
66 | { |
||
67 | $applierConfig = $this->applierUtils->mergeApplierConfig($this->config, array_filter($config)); |
||
68 | |||
69 | $applierConfig = $this->applierUtils->sortApplierConfig($applierConfig); |
||
70 | |||
71 | $patchers = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_APPLIERS); |
||
72 | $operations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_OPERATIONS); |
||
73 | $levels = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_LEVELS); |
||
74 | $failureMessages = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_FAILURES); |
||
75 | |||
76 | $sanityOperations = $this->extractArrayValue($applierConfig, PluginConfig::PATCHER_SANITY); |
||
77 | |||
78 | try { |
||
79 | $this->applierUtils->validateConfig($applierConfig); |
||
80 | } catch (\Vaimo\ComposerPatches\Exceptions\ConfigValidationException $exception) { |
||
81 | $this->logger->writeVerbose('error', $exception->getMessage()); |
||
82 | } |
||
83 | |||
84 | $arguments = array( |
||
85 | PluginConfig::PATCHER_ARG_FILE => $filename, |
||
86 | PluginConfig::PATCHER_ARG_CWD => $cwd |
||
87 | ); |
||
88 | |||
89 | $patcherName = $this->executeOperations($patchers, $sanityOperations, $arguments); |
||
90 | |||
91 | if (!$patcherName) { |
||
92 | $message = sprintf( |
||
93 | 'None of the patch appliers seem to be available (tried: %s)', |
||
94 | implode(', ', array_keys($patchers)) |
||
95 | ); |
||
96 | |||
97 | throw new \Vaimo\ComposerPatches\Exceptions\RuntimeException($message); |
||
98 | } |
||
99 | |||
100 | $errorGroups = array(); |
||
101 | |||
102 | foreach ($levels as $patchLevel) { |
||
103 | $arguments = array_replace( |
||
104 | $arguments, |
||
105 | array(PluginConfig::PATCHER_ARG_LEVEL => $patchLevel) |
||
106 | ); |
||
107 | |||
108 | try { |
||
109 | $patcherName = $this->executeOperations($patchers, $operations, $arguments, $failureMessages); |
||
110 | } catch (\Vaimo\ComposerPatches\Exceptions\ApplierFailure $exception) { |
||
111 | $errorGroups[] = $exception->getErrors(); |
||
112 | continue; |
||
113 | } |
||
114 | |||
115 | $this->logger->writeVerbose( |
||
116 | 'info', |
||
117 | 'SUCCESS with type=%s (p=%s)', |
||
118 | array($patcherName, $patchLevel) |
||
119 | ); |
||
120 | |||
121 | return; |
||
122 | } |
||
123 | |||
124 | $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure( |
||
125 | sprintf('Cannot apply patch %s', $filename) |
||
126 | ); |
||
127 | |||
128 | $failure->setErrors( |
||
129 | array_reduce($errorGroups, 'array_merge_recursive', array()) |
||
130 | ); |
||
131 | |||
132 | throw $failure; |
||
133 | } |
||
134 | |||
135 | private function executeOperations( |
||
136 | $patchers, |
||
137 | array $operations, |
||
138 | array $args = array(), |
||
139 | array $failures = array() |
||
140 | ) { |
||
141 | $outputRecords = array(); |
||
142 | |||
143 | foreach ($patchers as $type => $patcher) { |
||
144 | if (!$patcher) { |
||
145 | continue; |
||
146 | } |
||
147 | |||
148 | try { |
||
149 | return $this->processOperationItems($patcher, $operations, $args, $failures); |
||
150 | } catch (\Vaimo\ComposerPatches\Exceptions\OperationFailure $exception) { |
||
151 | $operationReference = is_string($exception->getMessage()) |
||
152 | ? $exception->getMessage() |
||
153 | : PatcherOperation::TYPE_UNKNOWN; |
||
154 | |||
155 | $outputRecords[$type] = $exception->getOutput(); |
||
156 | |||
157 | $messageArgs = array( |
||
158 | strtoupper($operationReference), |
||
159 | $type, |
||
160 | $this->extractStringValue($args, PluginConfig::PATCHER_ARG_LEVEL) |
||
161 | ); |
||
162 | |||
163 | $this->logger->writeVerbose('warning', '%s (type=%s) failed with p=%s', $messageArgs); |
||
164 | } |
||
165 | } |
||
166 | |||
167 | $messages = $this->collectErrors($outputRecords, array( |
||
168 | 'failed', |
||
169 | 'unexpected', |
||
170 | 'malformed', |
||
171 | 'error', |
||
172 | 'corrupt', |
||
173 | 'can\'t find file', |
||
174 | 'patch unexpectedly ends', |
||
175 | 'due to output analysis', |
||
176 | 'no file to patch' |
||
177 | )); |
||
178 | |||
179 | $failure = new \Vaimo\ComposerPatches\Exceptions\ApplierFailure(); |
||
180 | |||
181 | $failure->setErrors($messages); |
||
182 | |||
183 | throw $failure; |
||
184 | } |
||
185 | |||
186 | private function collectErrors(array $outputRecords, array $filters) |
||
187 | { |
||
188 | $pathMarker = '\|\+\+\+\s(.*)'; |
||
189 | |||
190 | $errorMatcher = sprintf( |
||
191 | '/(%s)/i', |
||
192 | implode('|', array_merge($filters, array($pathMarker))) |
||
193 | ); |
||
194 | |||
195 | $pathMatcher = sprintf('/^%s/i', $pathMarker); |
||
196 | |||
197 | $result = array(); |
||
198 | |||
199 | foreach ($outputRecords as $code => $output) { |
||
200 | $result[$code] = $this->dataUtils->listToGroups( |
||
201 | preg_grep($errorMatcher, explode(PHP_EOL, $output)), |
||
202 | $pathMatcher |
||
203 | ); |
||
204 | } |
||
205 | |||
206 | return $result; |
||
207 | } |
||
208 | |||
209 | private function processOperationItems($patcher, $operations, $args, $failures) |
||
210 | { |
||
211 | $operationResults = array_fill_keys(array_keys($operations), ''); |
||
212 | |||
213 | $result = true; |
||
214 | |||
215 | foreach (array_keys($operations) as $operationCode) { |
||
216 | if (!isset($patcher[$operationCode])) { |
||
217 | continue; |
||
218 | } |
||
219 | |||
220 | $args = array_replace($args, $operationResults); |
||
221 | |||
222 | $applierOperations = is_array($patcher[$operationCode]) |
||
223 | ? $patcher[$operationCode] |
||
224 | : array($patcher[$operationCode]); |
||
225 | |||
226 | |||
227 | $operationFailures = $this->extractArrayValue($failures, $operationCode); |
||
228 | |||
229 | list($result, $output) = $this->resolveOperationOutput( |
||
230 | $applierOperations, |
||
231 | $args, |
||
232 | $operationFailures |
||
233 | ); |
||
234 | |||
235 | if ($output !== false) { |
||
236 | $operationResults[$operationCode] = $output; |
||
237 | } |
||
238 | |||
239 | if ($result) { |
||
240 | continue; |
||
241 | } |
||
242 | |||
243 | $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure($operationCode); |
||
244 | |||
245 | $failure->setOutput($output); |
||
246 | |||
247 | throw $failure; |
||
248 | } |
||
249 | |||
250 | return $result; |
||
251 | } |
||
252 | |||
253 | private function resolveOperationOutput($applierOperations, $args, $operationFailures) |
||
254 | { |
||
255 | $variableFormats = array( |
||
256 | '{{%s}}' => array('escapeshellarg'), |
||
257 | '[[%s]]' => array() |
||
258 | ); |
||
259 | |||
260 | $output = ''; |
||
261 | |||
262 | foreach ($applierOperations as $operation) { |
||
263 | $passOnFailure = strpos($operation, '!') === 0; |
||
264 | $operation = ltrim($operation, '!'); |
||
265 | |||
266 | $command = $this->templateUtils->compose($operation, $args, $variableFormats); |
||
267 | |||
268 | $cwd = $this->extractStringValue($args, PluginConfig::PATCHER_ARG_CWD); |
||
269 | |||
270 | $resultKey = sprintf('%s | %s', $cwd, $command); |
||
271 | |||
272 | if ($passOnFailure) { |
||
273 | $this->logger->writeVerbose( |
||
274 | \Vaimo\ComposerPatches\Logger::TYPE_NONE, |
||
275 | '<comment>***</comment> ' |
||
276 | . 'The expected result to execution is a failure' |
||
277 | . '<comment>***</comment>' |
||
278 | ); |
||
279 | } |
||
280 | |||
281 | if (isset($this->resultCache[$resultKey])) { |
||
282 | $this->logger->writeVerbose( |
||
283 | \Vaimo\ComposerPatches\Logger::TYPE_NONE, |
||
284 | sprintf('(using cached result for: %s = %s)', $command, reset($this->resultCache[$resultKey])) |
||
285 | ); |
||
286 | } |
||
287 | |||
288 | if (!isset($this->resultCache[$resultKey])) { |
||
289 | $this->resultCache[$resultKey] = $this->shell->execute($command, $cwd); |
||
290 | } |
||
291 | |||
292 | list($result, $output) = $this->resultCache[$resultKey]; |
||
293 | |||
294 | if ($result) { |
||
295 | $result = $this->scanOutputForFailures($output, $operationFailures); |
||
296 | } |
||
297 | |||
298 | if ($passOnFailure) { |
||
299 | $result = !$result; |
||
300 | } |
||
301 | |||
302 | if (!$result) { |
||
303 | continue; |
||
304 | } |
||
305 | |||
306 | return array($result, $output); |
||
307 | } |
||
308 | |||
309 | return array(false, $output); |
||
310 | } |
||
311 | |||
312 | private function scanOutputForFailures($output, array $failureMessages) |
||
313 | { |
||
314 | $pathMarker = '\|\+\+\+\s(.*)'; |
||
315 | $pathMatcher = sprintf('/^%s/', $pathMarker); |
||
316 | |||
317 | $patternsWithResults = array_filter($failureMessages, function ($pattern) use ($output) { |
||
318 | return $pattern && preg_match($pattern, $output); |
||
319 | }); |
||
320 | |||
321 | if (!$patternsWithResults) { |
||
0 ignored issues
–
show
|
|||
322 | return true; |
||
323 | } |
||
324 | |||
325 | $lines = explode(PHP_EOL, $output); |
||
326 | |||
327 | $matches = array(); |
||
328 | |||
329 | foreach ($lines as $line) { |
||
330 | if (preg_match($pathMatcher, $line)) { |
||
331 | $matches[] = $line; |
||
332 | |||
333 | continue; |
||
334 | } |
||
335 | |||
336 | foreach ($patternsWithResults as $patternCode => $pattern) { |
||
337 | if (!preg_match($pattern, $line)) { |
||
338 | continue; |
||
339 | } |
||
340 | |||
341 | $message = sprintf( |
||
342 | 'Success changed to FAILURE due to output analysis (%s)', |
||
343 | $patternCode |
||
344 | ); |
||
345 | |||
346 | $matches[] = $message; |
||
347 | |||
348 | $this->logger->writeVerbose( |
||
349 | 'warning', |
||
350 | sprintf('%s: %s', $message, $pattern) |
||
351 | ); |
||
352 | } |
||
353 | } |
||
354 | |||
355 | $failure = new \Vaimo\ComposerPatches\Exceptions\OperationFailure('Output analysis failed'); |
||
356 | |||
357 | $failure->setOutput( |
||
358 | implode(PHP_EOL, $matches) |
||
359 | ); |
||
360 | |||
361 | throw $failure; |
||
362 | } |
||
363 | |||
364 | private function extractArrayValue($data, $key) |
||
365 | { |
||
366 | return $this->extractValue($data, $key, array()); |
||
367 | } |
||
368 | |||
369 | private function extractStringValue($data, $key) |
||
370 | { |
||
371 | return $this->extractValue($data, $key, ''); |
||
372 | } |
||
373 | |||
374 | private function extractValue($data, $key, $default) |
||
375 | { |
||
376 | return isset($data[$key]) ? $data[$key] : $default; |
||
377 | } |
||
378 | } |
||
379 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.