1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Cerbere\Model\Hacked; |
4
|
|
|
|
5
|
|
|
use Cerbere\Model\Project; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Encapsulates a Hacked! project. |
9
|
|
|
* |
10
|
|
|
* This class should handle all the complexity for you, and so you should be able to do: |
11
|
|
|
* <code> |
12
|
|
|
* $project = hackedProject('context'); |
13
|
|
|
* $project->compute_differences(); |
14
|
|
|
* </code> |
15
|
|
|
* |
16
|
|
|
* Which is quite nice I think. |
17
|
|
|
*/ |
18
|
|
|
class HackedProject |
19
|
|
|
{ |
20
|
|
|
const STATUS_UNCHECKED = 1; |
21
|
|
|
|
22
|
|
|
const STATUS_PERMISSION_DENIED = 2; |
23
|
|
|
|
24
|
|
|
const STATUS_HACKED = 3; |
25
|
|
|
|
26
|
|
|
const STATUS_DELETED = 4; |
27
|
|
|
|
28
|
|
|
const STATUS_UNHACKED = 5; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var Project |
32
|
|
|
*/ |
33
|
|
|
protected $project; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
protected $name = ''; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var array |
42
|
|
|
*/ |
43
|
|
|
protected $project_info = array(); |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var HackedProjectWebDownloader |
47
|
|
|
*/ |
48
|
|
|
protected $remote_files_downloader; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var HackedFileGroup |
52
|
|
|
*/ |
53
|
|
|
protected $remote_files; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var HackedFileGroup |
57
|
|
|
*/ |
58
|
|
|
protected $local_files; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
protected $local_folder; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var string |
67
|
|
|
*/ |
68
|
|
|
protected $project_type = ''; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var string |
72
|
|
|
*/ |
73
|
|
|
protected $existing_version = ''; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @var array |
77
|
|
|
*/ |
78
|
|
|
protected $result = array(); |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @var bool |
82
|
|
|
*/ |
83
|
|
|
protected $project_identified = false; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @var bool |
87
|
|
|
*/ |
88
|
|
|
protected $remote_downloaded = false; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* @var bool |
92
|
|
|
*/ |
93
|
|
|
protected $remote_hashed = false; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @var bool |
97
|
|
|
*/ |
98
|
|
|
protected $local_hashed = false; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Constructor. |
102
|
|
|
* @param Project $project |
103
|
|
|
* @param string $local_folder |
104
|
|
|
*/ |
105
|
|
|
public function __construct(Project $project, $local_folder = null) |
106
|
|
|
{ |
107
|
|
|
if (null === $local_folder) { |
108
|
|
|
$local_folder = getcwd(); |
109
|
|
|
} |
110
|
|
|
$this->project = $project; |
111
|
|
|
$this->local_folder = $local_folder; |
112
|
|
|
$this->name = $project->getProject(); |
113
|
|
|
$this->remote_files_downloader = new HackedProjectWebFilesDownloader($project); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @return Project |
118
|
|
|
*/ |
119
|
|
|
public function getProject() |
120
|
|
|
{ |
121
|
|
|
return $this->project; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Get the Human readable title of this project. |
126
|
|
|
*/ |
127
|
|
|
public function getTitle() |
128
|
|
|
{ |
129
|
|
|
$this->identifyProject(); |
130
|
|
|
|
131
|
|
|
return isset($this->project_info['title']) ? $this->project_info['title'] : $this->name; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Identify the project from the name we've been created with. |
136
|
|
|
* |
137
|
|
|
* We leverage the update (status) module to get the data we require about |
138
|
|
|
* projects. We just pull the information in, and make descisions about this |
139
|
|
|
* project being from CVS or not. |
140
|
|
|
*/ |
141
|
|
|
public function identifyProject() |
142
|
|
|
{ |
143
|
|
|
// Only do this once, no matter how many times we're called. |
144
|
|
|
if (!empty($this->project_identified)) { |
145
|
|
|
return; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
$data = (array) $this->project; |
149
|
|
|
$this->project_info = array(); |
150
|
|
|
|
151
|
|
|
foreach ($data as $key => $value) { |
152
|
|
|
$key = str_replace('*', '', $key); |
153
|
|
|
$this->project_info[$key] = $value; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$this->project_info['releases'] = $this->project->getReleases(); |
157
|
|
|
$this->project_identified = true; |
158
|
|
|
$this->existing_version = $this->project->getExistingVersion(); |
159
|
|
|
$this->project_type = 'module'; |
160
|
|
|
$this->project_info['includes'] = array($this->name . '.module'); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Downloads the remote project to be hashed later. |
165
|
|
|
*/ |
166
|
|
|
public function downloadRemoteProject() |
167
|
|
|
{ |
168
|
|
|
// Only do this once, no matter how many times we're called. |
169
|
|
|
if (!empty($this->remote_downloaded)) { |
170
|
|
|
return; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$this->identifyProject(); |
174
|
|
|
$this->remote_downloaded = (bool) $this->remote_files_downloader->downloadFile(); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Hashes the remote project downloaded earlier. |
179
|
|
|
*/ |
180
|
|
View Code Duplication |
public function hashRemoteProject() |
|
|
|
|
181
|
|
|
{ |
182
|
|
|
// Only do this once, no matter how many times we're called. |
183
|
|
|
if (!empty($this->remote_hashed)) { |
184
|
|
|
return; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
// Ensure that the remote project has actually been downloaded. |
188
|
|
|
$this->downloadRemoteProject(); |
189
|
|
|
|
190
|
|
|
// Set up the remote file group. |
191
|
|
|
$base_path = $this->remote_files_downloader->getFinalDestination(); |
192
|
|
|
$this->remote_files = HackedFileGroup::createFromDirectory($base_path); |
193
|
|
|
$this->remote_files->computeHashes(); |
194
|
|
|
|
195
|
|
|
$this->remote_hashed = count($this->remote_files->getFiles()) > 0; |
196
|
|
|
|
197
|
|
|
// Logging. |
198
|
|
|
if (!$this->remote_hashed) { |
199
|
|
|
//throw new \Exception('Could not hash remote project: ' . $this->getTitle()); |
|
|
|
|
200
|
|
|
echo 'Could not hash remote project: ' . $this->getTitle() . "\n"; |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Locate the base directory of the local project. |
206
|
|
|
*/ |
207
|
|
|
public function locateLocalProject() |
208
|
|
|
{ |
209
|
|
|
// we need a remote project to do this :( |
210
|
|
|
$this->hashRemoteProject(); |
211
|
|
|
|
212
|
|
|
$project_type = $this->project->getProjectType(); |
213
|
|
|
|
214
|
|
|
if ($project_type == Project::TYPE_PROJECT_CORE || $project_type == Project::TYPE_PROJECT_DISTRIBUTION) { |
215
|
|
|
$folder = dirname(dirname($this->local_folder)); |
216
|
|
|
} else { |
217
|
|
|
$folder = $this->local_folder; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return realpath($folder); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Hash the local version of the project. |
225
|
|
|
*/ |
226
|
|
View Code Duplication |
public function hashLocalProject() |
|
|
|
|
227
|
|
|
{ |
228
|
|
|
// Only do this once, no matter how many times we're called. |
229
|
|
|
if (!empty($this->local_hashed)) { |
230
|
|
|
return; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$location = $this->locateLocalProject(); |
234
|
|
|
$this->local_files = HackedFileGroup::createFromList($location, $this->remote_files->getFiles()); |
235
|
|
|
$this->local_files->computeHashes(); |
236
|
|
|
$this->local_hashed = count($this->local_files->getFiles()) > 0; |
237
|
|
|
|
238
|
|
|
// Logging. |
239
|
|
|
if (!$this->local_hashed) { |
240
|
|
|
//throw new \Exception('Could not hash remote project: ' . $this->getTitle()); |
|
|
|
|
241
|
|
|
echo 'Could not hash local project: ' . $this->getTitle() . "\n"; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Compute the differences between our version and the canonical version of the project. |
247
|
|
|
*/ |
248
|
|
|
public function computeDifferences() |
249
|
|
|
{ |
250
|
|
|
// Make sure we've hashed both remote and local files. |
251
|
|
|
$this->hashRemoteProject(); |
252
|
|
|
$this->hashLocalProject(); |
253
|
|
|
|
254
|
|
|
$results = array( |
255
|
|
|
'same' => array(), |
256
|
|
|
'different' => array(), |
257
|
|
|
'missing' => array(), |
258
|
|
|
'access_denied' => array(), |
259
|
|
|
); |
260
|
|
|
|
261
|
|
|
// Now compare the two file groups. |
262
|
|
|
foreach ($this->remote_files->getFiles() as $file) { |
263
|
|
|
if ($this->remote_files->getFileHash($file) == $this->local_files->getFileHash($file)) { |
264
|
|
|
$results['same'][] = $file; |
265
|
|
|
} elseif (!$this->local_files->fileExists($file)) { |
266
|
|
|
$results['missing'][] = $file; |
267
|
|
|
} elseif (!$this->local_files->isReadable($file)) { |
268
|
|
|
$results['access_denied'][] = $file; |
269
|
|
|
} else { |
270
|
|
|
$results['different'][] = $file; |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
$this->result = $results; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Return a nice report, a simple overview of the status of this project. |
279
|
|
|
*/ |
280
|
|
|
public function computeReport() |
281
|
|
|
{ |
282
|
|
|
// Ensure we know the differences. |
283
|
|
|
$this->computeDifferences(); |
284
|
|
|
|
285
|
|
|
// Do some counting |
286
|
|
|
$report = array( |
287
|
|
|
'project_name' => $this->name, |
288
|
|
|
'status' => self::STATUS_UNCHECKED, |
289
|
|
|
'counts' => array( |
290
|
|
|
'same' => count($this->result['same']), |
291
|
|
|
'different' => count($this->result['different']), |
292
|
|
|
'missing' => count($this->result['missing']), |
293
|
|
|
'access_denied' => count($this->result['access_denied']), |
294
|
|
|
), |
295
|
|
|
'title' => $this->getTitle(), |
296
|
|
|
); |
297
|
|
|
|
298
|
|
|
// Add more details into the report result (if we can). |
299
|
|
|
$details = array( |
300
|
|
|
'link', |
301
|
|
|
'name', |
302
|
|
|
'existing_version', |
303
|
|
|
'install_type', |
304
|
|
|
'datestamp', |
305
|
|
|
'project_type', |
306
|
|
|
'includes', |
307
|
|
|
); |
308
|
|
|
|
309
|
|
|
foreach ($details as $item) { |
310
|
|
|
if (isset($this->project_info[$item])) { |
311
|
|
|
$report[$item] = $this->project_info[$item]; |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
if ($report['counts']['access_denied'] > 0) { |
316
|
|
|
$report['status'] = self::STATUS_PERMISSION_DENIED; |
317
|
|
|
} elseif ($report['counts']['missing'] > 0) { |
318
|
|
|
$report['status'] = self::STATUS_HACKED; |
319
|
|
|
} elseif ($report['counts']['different'] > 0) { |
320
|
|
|
$report['status'] = self::STATUS_HACKED; |
321
|
|
|
} elseif ($report['counts']['same'] > 0) { |
322
|
|
|
$report['status'] = self::STATUS_UNHACKED; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
return $report; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Return a nice detailed report. |
330
|
|
|
* @return array |
331
|
|
|
*/ |
332
|
|
|
public function computeDetails() |
333
|
|
|
{ |
334
|
|
|
// Ensure we know the differences. |
335
|
|
|
$report = $this->computeReport(); |
336
|
|
|
|
337
|
|
|
$report['files'] = array(); |
338
|
|
|
|
339
|
|
|
// Add extra details about every file. |
340
|
|
|
$states = array( |
341
|
|
|
'access_denied' => self::STATUS_PERMISSION_DENIED, |
342
|
|
|
'missing' => self::STATUS_DELETED, |
343
|
|
|
'different' => self::STATUS_HACKED, |
344
|
|
|
'same' => self::STATUS_UNHACKED, |
345
|
|
|
); |
346
|
|
|
|
347
|
|
|
foreach ($states as $state => $status) { |
348
|
|
|
foreach ($this->result[$state] as $file) { |
349
|
|
|
$report['files'][$file] = $status; |
350
|
|
|
$report['diffable'][$file] = $this->fileIsDiffable($file); |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
return $report; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param string $file |
359
|
|
|
* |
360
|
|
|
* @return bool |
361
|
|
|
*/ |
362
|
|
|
public function fileIsDiffable($file) |
363
|
|
|
{ |
364
|
|
|
$this->hashRemoteProject(); |
365
|
|
|
$this->hashLocalProject(); |
366
|
|
|
|
367
|
|
|
return $this->remote_files->isNotBinary($file) && $this->local_files->isNotBinary($file); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* @param string $storage |
372
|
|
|
* @param string $file |
373
|
|
|
* |
374
|
|
|
* @return bool|string |
375
|
|
|
*/ |
376
|
|
|
public function getFileLocation($storage = 'local', $file) |
377
|
|
|
{ |
378
|
|
|
switch ($storage) { |
379
|
|
|
case 'remote': |
380
|
|
|
$this->downloadRemoteProject(); |
381
|
|
|
|
382
|
|
|
return $this->remote_files->getFileLocation($file); |
383
|
|
|
case 'local': |
384
|
|
|
$this->hashLocalProject(); |
385
|
|
|
|
386
|
|
|
return $this->local_files->getFileLocation($file); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
return false; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* @param string $status |
394
|
|
|
* @return string |
395
|
|
|
*/ |
396
|
|
|
public static function getStatusLabel($status) |
397
|
|
|
{ |
398
|
|
|
switch ($status) { |
399
|
|
|
case self::STATUS_PERMISSION_DENIED: |
400
|
|
|
return 'Permission denied'; |
401
|
|
|
case self::STATUS_HACKED: |
402
|
|
|
return 'Hacked'; |
403
|
|
|
case self::STATUS_DELETED: |
404
|
|
|
return 'Deleted'; |
405
|
|
|
case self::STATUS_UNHACKED: |
406
|
|
|
return 'Unhacked'; |
407
|
|
|
case self::STATUS_UNCHECKED: |
408
|
|
|
default: |
409
|
|
|
return 'Unchecked'; |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.