1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Drush Cerbere command line tools. |
5
|
|
|
* Copyright (C) 2015 - Sebastien Malot <[email protected]> |
6
|
|
|
* |
7
|
|
|
* This program is free software; you can redistribute it and/or modify |
8
|
|
|
* it under the terms of the GNU General Public License as published by |
9
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
10
|
|
|
* (at your option) any later version. |
11
|
|
|
* |
12
|
|
|
* This program is distributed in the hope that it will be useful, |
13
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
14
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15
|
|
|
* GNU General Public License for more details. |
16
|
|
|
* |
17
|
|
|
* You should have received a copy of the GNU General Public License along |
18
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc., |
19
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
20
|
|
|
*/ |
21
|
|
|
|
22
|
|
|
namespace Cerbere\Model; |
23
|
|
|
|
24
|
|
|
use Doctrine\Common\Cache\CacheProvider; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Class ReleaseHistory |
28
|
|
|
* @package Cerbere\Action |
29
|
|
|
*/ |
30
|
|
|
class ReleaseHistory |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* Project is up to date. |
34
|
|
|
*/ |
35
|
|
|
const UPDATE_CURRENT = 5; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Project has a new release available, but it is not a security release. |
39
|
|
|
*/ |
40
|
|
|
const UPDATE_NOT_CURRENT = 4; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Current release is no longer supported by the project maintainer. |
44
|
|
|
*/ |
45
|
|
|
const UPDATE_NOT_SUPPORTED = 3; |
46
|
|
|
/** |
47
|
|
|
* Current release has been unpublished and is no longer available. |
48
|
|
|
*/ |
49
|
|
|
const UPDATE_REVOKED = 2; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Project is missing security update(s). |
53
|
|
|
*/ |
54
|
|
|
const UPDATE_NOT_SECURE = 1; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Project's status cannot be checked. |
58
|
|
|
*/ |
59
|
|
|
const UPDATE_NOT_CHECKED = -1; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* No available update data was found for project. |
63
|
|
|
*/ |
64
|
|
|
const UPDATE_UNKNOWN = -2; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* There was a failure fetching available update data for this project. |
68
|
|
|
*/ |
69
|
|
|
const UPDATE_NOT_FETCHED = -3; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* We need to (re)fetch available update data for this project. |
73
|
|
|
*/ |
74
|
|
|
const UPDATE_FETCH_PENDING = -4; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var string |
78
|
|
|
*/ |
79
|
|
|
protected $url; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var array |
83
|
|
|
*/ |
84
|
|
|
protected $data; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @var CacheProvider |
88
|
|
|
*/ |
89
|
|
|
protected $cache; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param CacheProvider $cache |
93
|
|
|
* @param string $url |
94
|
|
|
*/ |
95
|
|
|
public function __construct(CacheProvider $cache = null, $url = null) |
96
|
|
|
{ |
97
|
|
|
$this->cache = $cache; |
98
|
|
|
$this->url = $url; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @param Project $project |
103
|
|
|
*/ |
104
|
|
|
public function compare(Project $project) |
105
|
|
|
{ |
106
|
|
|
// If the project status is marked as something bad, there's nothing else |
107
|
|
|
// to consider. |
108
|
|
|
if ($this->getProjectStatus()) { |
109
|
|
|
switch ($this->getProjectStatus()) { |
110
|
|
|
case 'insecure': |
111
|
|
|
$project->setStatus(self::UPDATE_NOT_SECURE); |
112
|
|
|
break; |
113
|
|
|
case 'unpublished': |
114
|
|
|
case 'revoked': |
115
|
|
|
$project->setStatus(self::UPDATE_REVOKED); |
116
|
|
|
break; |
117
|
|
|
case 'unsupported': |
118
|
|
|
$project->setStatus(self::UPDATE_NOT_SUPPORTED); |
119
|
|
|
break; |
120
|
|
|
case 'not-fetched': |
121
|
|
|
$project->setStatus(self::UPDATE_NOT_FETCHED); |
122
|
|
|
break; |
123
|
|
|
|
124
|
|
|
default: |
125
|
|
|
// Assume anything else (e.g. 'published') is valid and we should |
126
|
|
|
// perform the rest of the logic in this function. |
127
|
|
|
break; |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
if ($project->getStatus()) { |
132
|
|
|
// We already know the status for this project, so there's nothing else to |
133
|
|
|
// compute. Record the project status into $project_data and we're done. |
134
|
|
|
$project->setProjectStatus($this->getProjectStatus()); |
135
|
|
|
|
136
|
|
|
return; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
// Figure out the target major version. |
140
|
|
|
$existing_major = $project->getExistingMajor(); |
141
|
|
|
$supported_majors = array(); |
142
|
|
|
if ($this->getSupportedMajors()) { |
143
|
|
|
$supported_majors = explode(',', $this->getSupportedMajors()); |
144
|
|
|
} elseif ($this->getDefaultMajor()) { |
145
|
|
|
// Older release history XML file without supported or recommended. |
146
|
|
|
$supported_majors[] = $this->getDefaultMajor(); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
if (in_array($existing_major, $supported_majors)) { |
150
|
|
|
// Still supported, stay at the current major version. |
151
|
|
|
$target_major = $existing_major; |
152
|
|
|
} elseif ($this->getRecommendedMajor()) { |
153
|
|
|
// Since 'recommended_major' is defined, we know this is the new XML |
154
|
|
|
// format. Therefore, we know the current release is unsupported since |
155
|
|
|
// its major version was not in the 'supported_majors' list. We should |
156
|
|
|
// find the best release from the recommended major version. |
157
|
|
|
$target_major = $this->getRecommendedMajor(); |
158
|
|
|
$project->setStatus(self::UPDATE_NOT_SUPPORTED); |
159
|
|
|
} elseif ($this->getDefaultMajor()) { |
160
|
|
|
// Older release history XML file without recommended, so recommend |
161
|
|
|
// the currently defined "default_major" version. |
162
|
|
|
$target_major = $this->getDefaultMajor(); |
163
|
|
|
} else { |
164
|
|
|
// Malformed XML file? Stick with the current version. |
165
|
|
|
$target_major = $existing_major; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
// Make sure we never tell the admin to downgrade. If we recommended an |
169
|
|
|
// earlier version than the one they're running, they'd face an |
170
|
|
|
// impossible data migration problem, since Drupal never supports a DB |
171
|
|
|
// downgrade path. In the unfortunate case that what they're running is |
172
|
|
|
// unsupported, and there's nothing newer for them to upgrade to, we |
173
|
|
|
// can't print out a "Recommended version", but just have to tell them |
174
|
|
|
// what they have is unsupported and let them figure it out. |
175
|
|
|
$target_major = max($existing_major, $target_major); |
176
|
|
|
|
177
|
|
|
$release_patch_changed = null; |
178
|
|
|
$patch = ''; |
179
|
|
|
|
180
|
|
|
// If the project is marked as UPDATE_FETCH_PENDING, it means that the |
181
|
|
|
// data we currently have (if any) is stale, and we've got a task queued |
182
|
|
|
// up to (re)fetch the data. In that case, we mark it as such, merge in |
183
|
|
|
// whatever data we have (e.g. project title and link), and move on. |
184
|
|
|
if ($this->getFetchStatus() == self::UPDATE_FETCH_PENDING) { |
185
|
|
|
$project->setStatus(self::UPDATE_FETCH_PENDING); |
186
|
|
|
$project->setReason('No available update data'); |
187
|
|
|
$project->setFetchStatus($this->getFetchStatus()); |
188
|
|
|
|
189
|
|
|
return; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
// Defend ourselves from XML history files that contain no releases. |
193
|
|
|
if (!$this->getReleases()) { |
194
|
|
|
$project->setStatus(self::UPDATE_UNKNOWN); |
195
|
|
|
$project->setReason('No available releases found'); |
196
|
|
|
|
197
|
|
|
return; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
foreach ($this->getReleases() as $version => $release) { |
201
|
|
|
// First, if this is the existing release, check a few conditions. |
202
|
|
|
if ($project->getExistingVersion() == $version) { |
203
|
|
|
if ($release->hasTerm('Release type') && |
204
|
|
|
in_array('Insecure', $release->getTerm('Release type')) |
205
|
|
|
) { |
206
|
|
|
$project->setStatus(self::UPDATE_NOT_SECURE); |
207
|
|
|
} elseif ($release->getStatus() == 'unpublished') { |
208
|
|
|
$project->setStatus(self::UPDATE_REVOKED); |
209
|
|
|
} elseif ($release->hasTerm('Release type') && |
210
|
|
|
in_array('Unsupported', $release->getTerm('Release type')) |
211
|
|
|
) { |
212
|
|
|
$project->setStatus(self::UPDATE_NOT_SUPPORTED); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
// Otherwise, ignore unpublished, insecure, or unsupported releases. |
217
|
|
|
if ($release->getStatus() == 'unpublished' || |
218
|
|
|
($release->hasTerm('Release type') && |
219
|
|
|
(in_array('Insecure', $release->getTerm('Release type')) || |
220
|
|
|
in_array('Unsupported', $release->getTerm('Release type')))) |
221
|
|
|
) { |
222
|
|
|
continue; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
// See if this is a higher major version than our target and yet still |
226
|
|
|
// supported. If so, record it as an "Also available" release. |
227
|
|
|
// Note: some projects have a HEAD release from CVS days, which could |
228
|
|
|
// be one of those being compared. They would not have version_major |
229
|
|
|
// set, so we must call isset first. |
230
|
|
|
if ($release->getVersionMajor() > $target_major) { |
231
|
|
|
// if (in_array($release['version_major'], $supported_majors)) { |
|
|
|
|
232
|
|
|
// if (!isset($project_data['also'][$release['version_major']])) { |
233
|
|
|
// $project_data['also'][$release['version_major']] = $version; |
234
|
|
|
// $project_data['releases'][$version] = $release; |
235
|
|
|
// |
236
|
|
|
// $project->addAlsoAvailable($version, $release); |
237
|
|
|
// } |
238
|
|
|
// } |
239
|
|
|
// Otherwise, this release can't matter to us, since it's neither |
240
|
|
|
// from the release series we're currently using nor the recommended |
241
|
|
|
// release. We don't even care about security updates for this |
242
|
|
|
// branch, since if a project maintainer puts out a security release |
243
|
|
|
// at a higher major version and not at the lower major version, |
244
|
|
|
// they must remove the lower version from the supported major |
245
|
|
|
// versions at the same time, in which case we won't hit this code. |
246
|
|
|
continue; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
// Look for the 'latest version' if we haven't found it yet. Latest is |
250
|
|
|
// defined as the most recent version for the target major version. |
251
|
|
|
if (!$project->getLatestVersion() && $release->getVersionMajor() == $target_major) { |
252
|
|
|
$project->setLatestVersion($version); |
253
|
|
|
$project->setRelease($version, $release); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
// Look for the development snapshot release for this branch. |
257
|
|
|
if (!$project->getDevVersion() |
258
|
|
|
&& $release->getVersionMajor() == $target_major |
259
|
|
|
&& $release->getVersionExtra() == Project::INSTALL_TYPE_DEV |
260
|
|
|
) { |
261
|
|
|
$project->setDevVersion($version); |
262
|
|
|
$project->setRelease($version, $release); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
// Look for the 'recommended' version if we haven't found it yet (see |
266
|
|
|
// phpdoc at the top of this function for the definition). |
267
|
|
|
if (!$project->getRecommended() |
268
|
|
|
&& $release->getVersionMajor() == $target_major |
269
|
|
|
&& $release->getVersionPatch() |
270
|
|
|
) { |
271
|
|
|
if ($patch != $release->getVersionPatch()) { |
272
|
|
|
$patch = $release->getVersionPatch(); |
273
|
|
|
$release_patch_changed = $release; |
274
|
|
|
} |
275
|
|
|
if (!$release->getVersionExtra() && $patch == $release->getVersionPatch()) { |
276
|
|
|
$project->setRecommended($release_patch_changed->getVersion()); |
277
|
|
|
if ($release_patch_changed instanceof Release) { |
278
|
|
|
$project->setRelease($release_patch_changed->getVersion(), $release_patch_changed); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
// Stop searching once we hit the currently installed version. |
284
|
|
|
if ($project->getExistingVersion() == $version) { |
285
|
|
|
break; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
// If we're running a dev snapshot and have a timestamp, stop |
289
|
|
|
// searching for security updates once we hit an official release |
290
|
|
|
// older than what we've got. Allow 100 seconds of leeway to handle |
291
|
|
|
// differences between the datestamp in the .info file and the |
292
|
|
|
// timestamp of the tarball itself (which are usually off by 1 or 2 |
293
|
|
|
// seconds) so that we don't flag that as a new release. |
294
|
|
|
if ($project->getInstallType() == Project::INSTALL_TYPE_DEV) { |
295
|
|
|
if (!$project->getDatestamp()) { |
296
|
|
|
// We don't have current timestamp info, so we can't know. |
297
|
|
|
continue; |
298
|
|
|
} elseif ($release->getDate() && ($project->getDatestamp() + 100 > $release->getDate())) { |
299
|
|
|
// We're newer than this, so we can skip it. |
300
|
|
|
continue; |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
// See if this release is a security update. |
305
|
|
|
if ($release->hasTerm('Release type') && in_array('Security update', $release->getTerm('Release type'))) { |
306
|
|
|
$project->addSecurityUpdate($release->getVersion(), $release); |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
// If we were unable to find a recommended version, then make the latest |
311
|
|
|
// version the recommended version if possible. |
312
|
|
|
if (!$project->getRecommended() && $project->getLatestVersion()) { |
313
|
|
|
$project->setRecommended($project->getLatestVersion()); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
// Check to see if we need an update or not. |
317
|
|
|
if ($project->hasSecurityUpdates()) { |
318
|
|
|
// If we found security updates, that always trumps any other status. |
319
|
|
|
$project->setStatus(self::UPDATE_NOT_SECURE); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
if ($project->getStatus()) { |
323
|
|
|
// If we already know the status, we're done. |
324
|
|
|
return; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
// If we don't know what to recommend, there's nothing we can report. |
328
|
|
|
// Bail out early. |
329
|
|
|
if (!$project->getRecommended()) { |
330
|
|
|
$project->setStatus(self::UPDATE_UNKNOWN); |
331
|
|
|
$project->setReason('No available releases found'); |
332
|
|
|
|
333
|
|
|
return; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
// If we're running a dev snapshot, compare the date of the dev snapshot |
337
|
|
|
// with the latest official version, and record the absolute latest in |
338
|
|
|
// 'latest_dev' so we can correctly decide if there's a newer release |
339
|
|
|
// than our current snapshot. |
340
|
|
|
if ($project->getInstallType() == Project::INSTALL_TYPE_DEV) { |
341
|
|
|
if ($project->getDevVersion() && $this->getRelease($project->getDevVersion())->getDate( |
342
|
|
|
) > $this->getRelease($project->getLatestVersion())->getDate() |
343
|
|
|
) { |
344
|
|
|
$project->setLatestDev($project->getDevVersion()); |
345
|
|
|
} else { |
346
|
|
|
$project->setLatestDev($project->getLatestVersion()); |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
// Figure out the status, based on what we've seen and the install type. |
351
|
|
|
switch ($project->getInstallType()) { |
352
|
|
|
case Project::INSTALL_TYPE_OFFICIAL: |
353
|
|
|
if ($project->getExistingVersion() == $project->getRecommended() || |
354
|
|
|
$project->getExistingVersion() == $project->getLatestVersion() |
355
|
|
|
) { |
356
|
|
|
$project->setStatus(self::UPDATE_CURRENT); |
357
|
|
|
} else { |
358
|
|
|
$project->setStatus(self::UPDATE_NOT_CURRENT); |
359
|
|
|
} |
360
|
|
|
break; |
361
|
|
|
|
362
|
|
|
case Project::INSTALL_TYPE_DEV: |
363
|
|
|
$latest = $this->getRelease($project->getLatestDev()); |
364
|
|
|
|
365
|
|
|
if (!$project->getDatestamp()) { |
366
|
|
|
$project->setStatus(self::UPDATE_NOT_CHECKED); |
367
|
|
|
$project->setReason('Unknown release date'); |
368
|
|
|
} elseif (($project->getDatestamp() + 100 > $latest->getDate())) { |
369
|
|
|
$project->setStatus(self::UPDATE_CURRENT); |
370
|
|
|
} else { |
371
|
|
|
$project->setStatus(self::UPDATE_NOT_CURRENT); |
372
|
|
|
} |
373
|
|
|
break; |
374
|
|
|
|
375
|
|
|
default: |
376
|
|
|
$project->setStatus(self::UPDATE_UNKNOWN); |
377
|
|
|
$project->setReason('Invalid info'); |
378
|
|
|
} |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* @return string |
383
|
|
|
*/ |
384
|
|
|
public function getProjectStatus() |
385
|
|
|
{ |
386
|
|
|
return $this->data['project_status']; |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* @return string |
391
|
|
|
*/ |
392
|
|
|
public function getSupportedMajors() |
393
|
|
|
{ |
394
|
|
|
return $this->data['supported_majors']; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* @return int |
399
|
|
|
*/ |
400
|
|
|
public function getDefaultMajor() |
401
|
|
|
{ |
402
|
|
|
return $this->data['default_major']; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* @return int |
407
|
|
|
*/ |
408
|
|
|
public function getRecommendedMajor() |
409
|
|
|
{ |
410
|
|
|
return $this->data['recommended_major']; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* @return mixed |
415
|
|
|
*/ |
416
|
|
|
public function getFetchStatus() |
417
|
|
|
{ |
418
|
|
|
return isset($this->data['fetch_status']) ? $this->data['fetch_status'] : 0; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* @return Release[] |
423
|
|
|
*/ |
424
|
|
|
public function getReleases() |
425
|
|
|
{ |
426
|
|
|
return $this->data['releases']; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* @param string $release |
431
|
|
|
* |
432
|
|
|
* @return Release|null |
433
|
|
|
*/ |
434
|
|
|
public function getRelease($release) |
435
|
|
|
{ |
436
|
|
|
if (isset($this->data['releases'][$release])) { |
437
|
|
|
return $this->data['releases'][$release]; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
return null; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* @return string |
445
|
|
|
*/ |
446
|
|
|
public function getApiVersion() |
447
|
|
|
{ |
448
|
|
|
return $this->data['api_version']; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* @return array |
453
|
|
|
*/ |
454
|
|
|
public function getData() |
455
|
|
|
{ |
456
|
|
|
return $this->data; |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* @return Release |
461
|
|
|
*/ |
462
|
|
|
public function getLastRelease() |
463
|
|
|
{ |
464
|
|
|
$release = reset($this->data['releases']); |
465
|
|
|
|
466
|
|
|
return $release; |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* @return string |
471
|
|
|
*/ |
472
|
|
|
public function getLink() |
473
|
|
|
{ |
474
|
|
|
return $this->data['link']; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* @return mixed |
479
|
|
|
*/ |
480
|
|
|
public function getShortName() |
481
|
|
|
{ |
482
|
|
|
return $this->data['short_name']; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* @param int $status |
487
|
|
|
* |
488
|
|
|
* @return string |
489
|
|
|
*/ |
490
|
|
|
public static function getStatusLabel($status) |
491
|
|
|
{ |
492
|
|
|
switch ($status) { |
493
|
|
|
case self::UPDATE_NOT_SECURE: |
494
|
|
|
return 'SECURITY UPDATE available'; |
495
|
|
|
case self::UPDATE_REVOKED: |
496
|
|
|
return 'Installed version REVOKED'; |
497
|
|
|
case self::UPDATE_NOT_SUPPORTED: |
498
|
|
|
return 'Installed version not supported'; |
499
|
|
|
case self::UPDATE_NOT_CURRENT: |
500
|
|
|
return 'Update available'; |
501
|
|
|
case self::UPDATE_CURRENT: |
502
|
|
|
return 'Up to date'; |
503
|
|
|
case self::UPDATE_NOT_CHECKED: |
504
|
|
|
case self::UPDATE_NOT_FETCHED: |
505
|
|
|
case self::UPDATE_FETCH_PENDING: |
506
|
|
|
return 'Unable to check status'; |
507
|
|
|
case self::UPDATE_UNKNOWN: |
508
|
|
|
default: |
509
|
|
|
return 'Unknown'; |
510
|
|
|
} |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* @return string |
515
|
|
|
*/ |
516
|
|
|
public function getTerms() |
517
|
|
|
{ |
518
|
|
|
return trim($this->data['terms']); |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
/** |
522
|
|
|
* @return string |
523
|
|
|
*/ |
524
|
|
|
public function getTitle() |
525
|
|
|
{ |
526
|
|
|
return $this->data['title']; |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
/** |
530
|
|
|
* @return string |
531
|
|
|
*/ |
532
|
|
|
public function getType() |
533
|
|
|
{ |
534
|
|
|
return $this->data['type']; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/** |
538
|
|
|
* @return string |
539
|
|
|
*/ |
540
|
|
|
public function getUrl() |
541
|
|
|
{ |
542
|
|
|
return $this->url; |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* @param string $url |
547
|
|
|
*/ |
548
|
|
|
public function setUrl($url) |
549
|
|
|
{ |
550
|
|
|
$this->url = $url; |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
/** |
554
|
|
|
* @param Project $project |
555
|
|
|
* @param bool|false $reset |
556
|
|
|
*/ |
557
|
|
|
public function prepare(Project $project, $reset = false) |
558
|
|
|
{ |
559
|
|
|
$cid_parts = array( |
560
|
|
|
'release_history', |
561
|
|
|
$project->getProject(), |
562
|
|
|
$project->getCore(), |
563
|
|
|
); |
564
|
|
|
|
565
|
|
|
$cid = implode(':', $cid_parts); |
566
|
|
|
$data = false; |
567
|
|
|
|
568
|
|
|
if ($this->cache && !$reset) { |
569
|
|
|
$data = $this->cache->fetch($cid); |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
// If not in cache, load from remote. |
573
|
|
|
if ($data === false) { |
574
|
|
|
$url = $project->getStatusUrl() . '/' . |
575
|
|
|
$project->getProject() . '/' . |
576
|
|
|
$project->getCore(); |
577
|
|
|
|
578
|
|
|
// Todo: use guzzle library. |
579
|
|
|
$content = file_get_contents($url); |
580
|
|
|
|
581
|
|
|
$data = $this->parseUpdateXml($content); |
582
|
|
|
|
583
|
|
|
// If data, store into cache. |
584
|
|
|
if ($this->cache && !empty($data)) { |
585
|
|
|
$this->cache->save($cid, $data, 1800); |
586
|
|
|
} |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
$data += array( |
590
|
|
|
'project_status' => '', |
591
|
|
|
'default_major' => '', |
592
|
|
|
'recommended_major' => '', |
593
|
|
|
'supported_majors' => '', |
594
|
|
|
); |
595
|
|
|
|
596
|
|
|
// Hydrate release objects. |
597
|
|
|
if (isset($data['releases']) && is_array($data['releases'])) { |
598
|
|
|
foreach ($data['releases'] as $key => $value) { |
599
|
|
|
$data['releases'][$key] = new Release($value); |
600
|
|
|
} |
601
|
|
|
$project->setReleases($data['releases']); |
602
|
|
|
} else { |
603
|
|
|
$data['releases'] = array(); |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
if (!empty($data['type'])) { |
607
|
|
|
$project->setProjectType($data['type']); |
608
|
|
|
} else { |
609
|
|
|
$project->setProjectType(Project::TYPE_UNKNOWN); |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
$this->data = (array) $data; |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
/** |
616
|
|
|
* Parses the XML of the Drupal release history info files. |
617
|
|
|
* |
618
|
|
|
* @param string $raw_xml |
619
|
|
|
* A raw XML string of available release data for a given project. |
620
|
|
|
* |
621
|
|
|
* @return array |
622
|
|
|
* Array of parsed data about releases for a given project, or NULL if there |
623
|
|
|
* was an error parsing the string. |
624
|
|
|
*/ |
625
|
|
|
protected function parseUpdateXml($raw_xml) |
626
|
|
|
{ |
627
|
|
|
try { |
628
|
|
|
$xml = new \SimpleXMLElement($raw_xml); |
629
|
|
|
} catch (\Exception $e) { |
630
|
|
|
// SimpleXMLElement::__construct produces an E_WARNING error message for |
631
|
|
|
// each error found in the XML data and throws an exception if errors |
632
|
|
|
// were detected. Catch any exception and return failure (NULL). |
633
|
|
|
return array(); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
// If there is no valid project data, the XML is invalid, so return failure. |
637
|
|
|
if (!isset($xml->short_name)) { |
638
|
|
|
return array(); |
639
|
|
|
} |
640
|
|
|
|
641
|
|
|
$data = array(); |
642
|
|
|
foreach ($xml as $k => $v) { |
643
|
|
|
$data[$k] = (string) $v; |
644
|
|
|
} |
645
|
|
|
$data['releases'] = array(); |
646
|
|
|
|
647
|
|
|
if (isset($xml->releases)) { |
648
|
|
|
foreach ($xml->releases->children() as $release) { |
649
|
|
|
$version = (string) $release->version; |
650
|
|
|
$data['releases'][$version] = array(); |
651
|
|
|
foreach ($release->children() as $k => $v) { |
652
|
|
|
$data['releases'][$version][$k] = (string) $v; |
653
|
|
|
} |
654
|
|
|
$data['releases'][$version]['terms'] = array(); |
655
|
|
|
if ($release->terms) { |
656
|
|
|
foreach ($release->terms->children() as $term) { |
657
|
|
|
if (!isset($data['releases'][$version]['terms'][(string) $term->name])) { |
658
|
|
|
$data['releases'][$version]['terms'][(string) $term->name] = array(); |
659
|
|
|
} |
660
|
|
|
$data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value; |
661
|
|
|
} |
662
|
|
|
} |
663
|
|
|
} |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
return $data; |
667
|
|
|
} |
668
|
|
|
} |
669
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.