Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / vendor / drush / drush / src / Commands / pm / SecurityUpdateCommands.php
1 <?php
2 namespace Drush\Commands\pm;
3
4 use Composer\Semver\Comparator;
5 use Consolidation\AnnotatedCommand\CommandData;
6 use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
7 use Drush\Commands\DrushCommands;
8 use Drush\Drush;
9 use Exception;
10 use Webmozart\PathUtil\Path;
11
12 /**
13  * Check Drupal Composer packages for security updates.
14  */
15 class SecurityUpdateCommands extends DrushCommands
16 {
17
18     /**
19      * @var array
20      */
21     protected $securityUpdates;
22
23     /**
24      * Check Drupal Composer packages for pending security updates.
25      *
26      * This uses the Drupal security advisories package to determine if updates
27      * are available.
28      *
29      * @see https://github.com/drupal-composer/drupal-security-advisories
30      *
31      * @command pm:security
32      * @aliases sec,pm-security
33      * @bootstrap none
34      * @table-style default
35      * @field-labels
36      *   name: Name
37      *   version: Installed Version
38      *   min-version: Suggested version
39      * @default-fields name,version,min-version
40      *
41      * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
42      *
43      * @throws \Exception
44      */
45     public function security()
46     {
47         $this->securityUpdates = [];
48         $security_advisories_composer_json = $this->fetchAdvisoryComposerJson();
49         $composer_lock_data = $this->loadSiteComposerLock();
50         $this->registerAllSecurityUpdates($composer_lock_data, $security_advisories_composer_json);
51         if ($this->securityUpdates) {
52             // @todo Modernize.
53             drush_set_context('DRUSH_EXIT_CODE', DRUSH_FRAMEWORK_ERROR);
54             $result = new RowsOfFields($this->securityUpdates);
55             return $result;
56         } else {
57             $this->logger()->success("<info>There are no outstanding security updates for Drupal projects.</info>");
58         }
59     }
60
61     /**
62      * Emit suggested Composer command for security updates.
63      *
64      * @hook post-command pm:security
65      */
66     public function suggestComposerCommand($result, CommandData $commandData)
67     {
68         if (!empty($this->securityUpdates)) {
69             $suggested_command = 'composer require ';
70             foreach ($this->securityUpdates as $package) {
71                 $suggested_command .= $package['name'] . ':^' . $package['min-version'] . ' ';
72             }
73             $suggested_command .= '--update-with-dependencies';
74             $this->logger()->warning("One or more of your dependencies has an outstanding security update. Please apply update(s) immediately.");
75             $this->logger()->notice("Try running: <comment>$suggested_command</comment>");
76             $this->logger()->notice("If that fails due to a conflict then you must update one or more root dependencies.");
77         }
78     }
79
80     /**
81      * Fetches the generated composer.json from drupal-security-advisories.
82      *
83      * @return mixed
84      *
85      * @throws \Exception
86      */
87     protected function fetchAdvisoryComposerJson()
88     {
89         try {
90             $response_body = file_get_contents('https://raw.githubusercontent.com/drupal-composer/drupal-security-advisories/8.x/composer.json');
91         } catch (Exception $e) {
92             throw new Exception("Unable to fetch drupal-security-advisories information.");
93         }
94         $security_advisories_composer_json = json_decode($response_body, true);
95         return $security_advisories_composer_json;
96     }
97
98     /**
99      * Loads the contents of the local Drupal application's composer.lock file.
100      *
101      * @return array
102      *
103      * @throws \Exception
104      */
105     protected function loadSiteComposerLock()
106     {
107         $composer_root = Drush::bootstrapManager()->getComposerRoot();
108         $composer_lock_file_name = getenv('COMPOSER') ? str_replace(
109             '.json',
110             '',
111             getenv('COMPOSER')
112         ) : 'composer';
113         $composer_lock_file_name .= '.lock';
114         $composer_lock_file_path = Path::join(
115             $composer_root,
116             $composer_lock_file_name
117         );
118         if (!file_exists($composer_lock_file_path)) {
119             throw new Exception("Cannot find $composer_lock_file_path!");
120         }
121         $composer_lock_contents = file_get_contents($composer_lock_file_path);
122         $composer_lock_data = json_decode($composer_lock_contents, true);
123         if (!array_key_exists('packages', $composer_lock_data)) {
124             throw new Exception("No packages were found in $composer_lock_file_path! Contents:\n $composer_lock_contents");
125         }
126         return $composer_lock_data;
127     }
128
129     /**
130      * Register all available security updates in $this->securityUpdates.
131      * @param array $composer_lock_data
132      *   The contents of the local Drupal application's composer.lock file.
133      * @param array $security_advisories_composer_json
134      *   The composer.json array from drupal-security-advisories.
135      */
136     protected function registerAllSecurityUpdates($composer_lock_data, $security_advisories_composer_json)
137     {
138         $both = $composer_lock_data['packages-dev'] + $composer_lock_data['packages'];
139         foreach ($both as $package) {
140             $name = $package['name'];
141             $this->registerPackageSecurityUpdates($security_advisories_composer_json, $name, $package);
142         }
143     }
144
145     /**
146      * Determines if update is available based on a conflict constraint.
147      *
148      * @param string $conflict_constraint
149      *   The constraint for the conflicting, insecure package version.
150      *   E.g., <1.0.0.
151      * @param array $package
152      *   The package to be evaluated.
153      * @param string $name
154      *   The human readable display name for the package.
155      *
156      * @return array
157      *   An associative array containing name, version, and min-version keys.
158      */
159     public static function determineUpdatesFromConstraint(
160         $conflict_constraint,
161         $package,
162         $name
163     ) {
164         // Only parse constraints that follow pattern like "<1.0.0".
165         if (substr($conflict_constraint, 0, 1) == '<') {
166             $min_version = substr($conflict_constraint, 1);
167             if (Comparator::lessThan(
168                 $package['version'],
169                 $min_version
170             )) {
171                 return [
172                     'name' => $name,
173                     'version' => $package['version'],
174                     // Assume that conflict constraint of <1.0.0 indicates that
175                     // 1.0.0 is the available, secure version.
176                     'min-version' => $min_version,
177                 ];
178             }
179         } // Compare exact versions that are insecure.
180         elseif (preg_match(
181             '/^[[:digit:]](?![-*><=~ ])/',
182             $conflict_constraint
183         )) {
184             $exact_version = $conflict_constraint;
185             if (Comparator::equalTo(
186                 $package['version'],
187                 $exact_version
188             )) {
189                 $version_parts = explode('.', $package['version']);
190                 if (count($version_parts) == 3) {
191                     $version_parts[2]++;
192                     $min_version = implode('.', $version_parts);
193                     return [
194                         'name' => $name,
195                         'version' => $package['version'],
196                         // Assume that conflict constraint of 1.0.0 indicates that
197                         // 1.0.1 is the available, secure version.
198                         'min-version' => $min_version,
199                     ];
200                 }
201             }
202         }
203         return [];
204     }
205
206     /**
207      * Registers available security updates for a given package.
208      *
209      * @param array $security_advisories_composer_json
210      *   The composer.json array from drupal-security-advisories.
211      * @param string $name
212      *   The human readable display name for the package.
213      * @param array $package
214      *   The package to be evaluated.
215      */
216     protected function registerPackageSecurityUpdates(
217         $security_advisories_composer_json,
218         $name,
219         $package
220     ) {
221         if (empty($this->securityUpdates[$name]) &&
222             !empty($security_advisories_composer_json['conflict'][$name])) {
223             $conflict_constraints = explode(
224                 ',',
225                 $security_advisories_composer_json['conflict'][$name]
226             );
227             foreach ($conflict_constraints as $conflict_constraint) {
228                 $available_update = $this->determineUpdatesFromConstraint(
229                     $conflict_constraint,
230                     $package,
231                     $name
232                 );
233                 if ($available_update) {
234                     $this->securityUpdates[$name] = $available_update;
235                 }
236             }
237         }
238     }
239 }