33f82e3d20b548b528b8fa82eb7438732cd1388c
[yaffs-website] / web / modules / contrib / hacked / src / hackedProject.php
1 <?php
2
3 namespace Drupal\hacked;
4
5 use Drupal\Core\StringTranslation\StringTranslationTrait;
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   use StringTranslationTrait;
20
21   var $name = '';
22
23   var $project_info = array();
24
25   var $remote_files_downloader;
26
27   /* @var hackedFileGroup $remote_files */
28   var $remote_files;
29
30   /* @var hackedFileGroup $local_files */
31   var $local_files;
32
33   var $project_type = '';
34   var $existing_version = '';
35
36   var $result = array();
37
38   var $project_identified = FALSE;
39   var $remote_downloaded = FALSE;
40   var $remote_hashed = FALSE;
41   var $local_hashed = FALSE;
42
43   /**
44    * Constructor.
45    */
46   function __construct($name) {
47     // Identify the project.
48     $this->name = $name;
49     $this->identify_project();
50
51     // Choose an appropriate downloader.
52     if ($this->isDevVersion()) {
53       $this->remote_files_downloader = new hackedProjectWebDevDownloader($this);
54     }
55     else {
56       $this->remote_files_downloader = new hackedProjectWebFilesDownloader($this);
57     }
58   }
59
60   /**
61    * Get the Human readable title of this project.
62    */
63   function title() {
64     $this->identify_project();
65     return isset($this->project_info['title']) ? $this->project_info['title'] : $this->name;
66   }
67
68   /**
69    * Identify the project from the name we've been created with.
70    *
71    * We leverage the update (status) module to get the data we require about
72    * projects. We just pull the information in, and make descisions about this
73    * project being from CVS or not.
74    */
75   function identify_project() {
76     // Only do this once, no matter how many times we're called.
77     if (!empty($this->project_identified)) {
78       return;
79     }
80
81     // Fetch the required data from the update (status) module.
82     // TODO: clean this up.
83     $available = update_get_available(TRUE);
84     $data = update_calculate_project_data($available);
85     $releases = \Drupal::keyValueExpirable('update_available_releases')
86       ->getAll();
87
88     foreach ($data as $key => $project) {
89       if ($key == $this->name) {
90         $this->project_info = $project;
91         if (!isset($this->project_info['releases']) || !is_array($this->project_info['releases'])) {
92           $this->project_info['releases'] = array();
93         }
94         if (isset($releases[$key]['releases']) && is_array($releases[$key]['releases'])) {
95           $this->project_info['releases'] += $releases[$key]['releases'];
96         }
97
98         // Add in the additional info that update module strips out.
99         // This is a really naff way of doing this, but update (status) module
100         // ripped out a lot of useful stuff in issue:
101         // http://drupal.org/node/669554
102
103         $this->project_identified = TRUE;
104         $this->existing_version = $this->project_info['existing_version'];
105         $this->project_type = $this->project_info['project_type'];
106         break;
107       }
108     }
109
110     // Logging.
111     if (!$this->project_identified) {
112       $message = $this->t('Could not identify project: @name', array('@name' => $this->name));
113       \Drupal::logger('hacked')->warning($message->render());
114     }
115   }
116
117   /**
118    * Determines if the project is a development version or has an explicit release.
119    *
120    * @return boolean
121    *   TRUE if the project is a dev release; FALSE otherwise.
122    */
123   function isDevVersion() {
124     // Grab the version string.
125     $version = $this->existing_version;
126
127     // Assume we have a dev version if the string ends with "-dev".
128     return (substr_compare($version, '-dev', -4, 4) === 0) ? TRUE : FALSE;
129   }
130
131   /**
132    * Downloads the remote project to be hashed later.
133    */
134   function download_remote_project() {
135     // Only do this once, no matter how many times we're called.
136     if (!empty($this->remote_downloaded)) {
137       return;
138     }
139
140     $this->identify_project();
141     $this->remote_downloaded = (bool) $this->remote_files_downloader->download();
142
143     // Logging.
144     if (!$this->remote_downloaded) {
145       $message = $this->t('Could not download project: @title', array('@title' => $this->title()));
146       \Drupal::logger('hacked')->error($message->render());
147     }
148   }
149
150   /**
151    * Hashes the remote project downloaded earlier.
152    */
153   function hash_remote_project() {
154     // Only do this once, no matter how many times we're called.
155     if (!empty($this->remote_hashed)) {
156       return;
157     }
158
159     // Ensure that the remote project has actually been downloaded.
160     $this->download_remote_project();
161
162     // Set up the remote file group.
163     $base_path = $this->remote_files_downloader->get_final_destination();
164     $this->remote_files = hackedFileGroup::fromDirectory($base_path);
165     $this->remote_files->compute_hashes();
166
167     $this->remote_hashed = !empty($this->remote_files->files);
168
169     // Logging.
170     if (!$this->remote_hashed) {
171       $message = $this->t('Could not hash remote project: @title', array('@title' => $this->title()));
172       \Drupal::logger('hacked')->error($message->render());
173     }
174   }
175
176   /**
177    * Locate the base directory of the local project.
178    */
179   function locate_local_project() {
180     // we need a remote project to do this :(
181     $this->hash_remote_project();
182
183     // Do we have at least some modules to check for:
184     if (!is_array($this->project_info['includes']) || !count($this->project_info['includes'])) {
185       return FALSE;
186     }
187
188     // If this project is drupal it, we need to handle it specially
189     if ($this->project_type != 'core') {
190       $includes = array_keys($this->project_info['includes']);
191       $include = array_shift($includes);
192       $include_type = $this->project_info['project_type'];
193     }
194     else {
195       // Just use the system module to find where we've installed drupal
196       $include = 'system';
197       $include_type = 'module';
198     }
199
200     //$include = 'image_captcha';
201
202     $path = drupal_get_path($include_type, $include);
203
204     // Now we need to find the path of the info file in the downloaded package:
205     $temp = '';
206     foreach ($this->remote_files->files as $file) {
207       if (preg_match('@(^|.*/)' . $include . '.info.yml$@', $file)) {
208         $temp = $file;
209         break;
210       }
211     }
212
213     // How many '/' were in that path:
214     $slash_count = substr_count($temp, '/');
215     $back_track = str_repeat('/..', $slash_count);
216
217     return realpath($path . $back_track);
218   }
219
220   /**
221    * Hash the local version of the project.
222    */
223   function hash_local_project() {
224     // Only do this once, no matter how many times we're called.
225     if (!empty($this->local_hashed)) {
226       return;
227     }
228
229     $location = $this->locate_local_project();
230
231     $this->local_files = hackedFileGroup::fromList($location, $this->remote_files->files);
232     $this->local_files->compute_hashes();
233
234     $this->local_hashed = !empty($this->local_files->files);
235
236     // Logging.
237     if (!$this->local_hashed) {
238       $message = $this->t('Could not hash local project: @title', ['@title' => $this->title()]);
239       \Drupal::logger('hacked')->error($message->render());
240     }
241   }
242
243   /**
244    * Compute the differences between our version and the canonical version of the project.
245    */
246   function compute_differences() {
247     // Make sure we've hashed both remote and local files.
248     $this->hash_remote_project();
249     $this->hash_local_project();
250
251     $results = [
252       'same'          => [],
253       'different'     => [],
254       'missing'       => [],
255       'access_denied' => [],
256     ];
257
258     // Now compare the two file groups.
259     foreach ($this->remote_files->files as $file) {
260       if ($this->remote_files->files_hashes[$file] == $this->local_files->files_hashes[$file]) {
261         $results['same'][] = $file;
262       }
263       elseif (!$this->local_files->file_exists($file)) {
264         $results['missing'][] = $file;
265       }
266       elseif (!$this->local_files->is_readable($file)) {
267         $results['access_denied'][] = $file;
268       }
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   function compute_report() {
281     // Ensure we know the differences.
282     $this->compute_differences();
283
284     // Do some counting
285
286     $report = [
287       'project_name' => $this->name,
288       'status'       => HACKED_STATUS_UNCHECKED,
289       'counts'       => [
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->title(),
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     foreach ($details as $item) {
309       if (isset($this->project_info[$item])) {
310         $report[$item] = $this->project_info[$item];
311       }
312     }
313
314
315     if ($report['counts']['access_denied'] > 0) {
316       $report['status'] = HACKED_STATUS_PERMISSION_DENIED;
317     }
318     elseif ($report['counts']['missing'] > 0) {
319       $report['status'] = HACKED_STATUS_HACKED;
320     }
321     elseif ($report['counts']['different'] > 0) {
322       $report['status'] = HACKED_STATUS_HACKED;
323     }
324     elseif ($report['counts']['same'] > 0) {
325       $report['status'] = HACKED_STATUS_UNHACKED;
326     }
327
328     return $report;
329   }
330
331   /**
332    * Return a nice detailed report.
333    */
334   function compute_details() {
335     // Ensure we know the differences.
336     $report = $this->compute_report();
337
338     $report['files'] = array();
339
340     // Add extra details about every file.
341     $states = array(
342       'access_denied' => HACKED_STATUS_PERMISSION_DENIED,
343       'missing'       => HACKED_STATUS_DELETED,
344       'different'     => HACKED_STATUS_HACKED,
345       'same'          => HACKED_STATUS_UNHACKED,
346     );
347
348     foreach ($states as $state => $status) {
349       foreach ($this->result[$state] as $file) {
350         $report['files'][$file] = $status;
351         $report['diffable'][$file] = $this->file_is_diffable($file);
352       }
353     }
354
355     return $report;
356   }
357
358   function file_is_diffable($file) {
359     $this->hash_remote_project();
360     $this->hash_local_project();
361     return $this->remote_files->is_not_binary($file) && $this->local_files->is_not_binary($file);
362   }
363
364   function file_get_location($storage = 'local', $file) {
365     switch ($storage) {
366       case 'remote':
367         $this->download_remote_project();
368         return $this->remote_files->file_get_location($file);
369       case 'local':
370         $this->hash_local_project();
371         return $this->local_files->file_get_location($file);
372     }
373     return FALSE;
374   }
375
376 }