3 namespace Drupal\hacked;
5 use Drupal\Core\StringTranslation\StringTranslationTrait;
8 * Encapsulates a Hacked! project.
10 * This class should handle all the complexity for you, and so you should be able to do:
12 * $project = hackedProject('context');
13 * $project->compute_differences();
16 * Which is quite nice I think.
19 use StringTranslationTrait;
23 var $project_info = array();
25 var $remote_files_downloader;
27 /* @var hackedFileGroup $remote_files */
30 /* @var hackedFileGroup $local_files */
33 var $project_type = '';
34 var $existing_version = '';
36 var $result = array();
38 var $project_identified = FALSE;
39 var $remote_downloaded = FALSE;
40 var $remote_hashed = FALSE;
41 var $local_hashed = FALSE;
46 function __construct($name) {
47 // Identify the project.
49 $this->identify_project();
51 // Choose an appropriate downloader.
52 if ($this->isDevVersion()) {
53 $this->remote_files_downloader = new hackedProjectWebDevDownloader($this);
56 $this->remote_files_downloader = new hackedProjectWebFilesDownloader($this);
61 * Get the Human readable title of this project.
64 $this->identify_project();
65 return isset($this->project_info['title']) ? $this->project_info['title'] : $this->name;
69 * Identify the project from the name we've been created with.
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.
75 function identify_project() {
76 // Only do this once, no matter how many times we're called.
77 if (!empty($this->project_identified)) {
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')
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();
94 if (isset($releases[$key]['releases']) && is_array($releases[$key]['releases'])) {
95 $this->project_info['releases'] += $releases[$key]['releases'];
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
103 $this->project_identified = TRUE;
104 $this->existing_version = $this->project_info['existing_version'];
105 $this->project_type = $this->project_info['project_type'];
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());
118 * Determines if the project is a development version or has an explicit release.
121 * TRUE if the project is a dev release; FALSE otherwise.
123 function isDevVersion() {
124 // Grab the version string.
125 $version = $this->existing_version;
127 // Assume we have a dev version if the string ends with "-dev".
128 return (substr_compare($version, '-dev', -4, 4) === 0) ? TRUE : FALSE;
132 * Downloads the remote project to be hashed later.
134 function download_remote_project() {
135 // Only do this once, no matter how many times we're called.
136 if (!empty($this->remote_downloaded)) {
140 $this->identify_project();
141 $this->remote_downloaded = (bool) $this->remote_files_downloader->download();
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());
151 * Hashes the remote project downloaded earlier.
153 function hash_remote_project() {
154 // Only do this once, no matter how many times we're called.
155 if (!empty($this->remote_hashed)) {
159 // Ensure that the remote project has actually been downloaded.
160 $this->download_remote_project();
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();
167 $this->remote_hashed = !empty($this->remote_files->files);
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());
177 * Locate the base directory of the local project.
179 function locate_local_project() {
180 // we need a remote project to do this :(
181 $this->hash_remote_project();
183 // Do we have at least some modules to check for:
184 if (!is_array($this->project_info['includes']) || !count($this->project_info['includes'])) {
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'];
195 // Just use the system module to find where we've installed drupal
197 $include_type = 'module';
200 //$include = 'image_captcha';
202 $path = drupal_get_path($include_type, $include);
204 // Now we need to find the path of the info file in the downloaded package:
206 foreach ($this->remote_files->files as $file) {
207 if (preg_match('@(^|.*/)' . $include . '.info.yml$@', $file)) {
213 // How many '/' were in that path:
214 $slash_count = substr_count($temp, '/');
215 $back_track = str_repeat('/..', $slash_count);
217 return realpath($path . $back_track);
221 * Hash the local version of the project.
223 function hash_local_project() {
224 // Only do this once, no matter how many times we're called.
225 if (!empty($this->local_hashed)) {
229 $location = $this->locate_local_project();
231 $this->local_files = hackedFileGroup::fromList($location, $this->remote_files->files);
232 $this->local_files->compute_hashes();
234 $this->local_hashed = !empty($this->local_files->files);
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());
244 * Compute the differences between our version and the canonical version of the project.
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();
255 'access_denied' => [],
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;
263 elseif (!$this->local_files->file_exists($file)) {
264 $results['missing'][] = $file;
266 elseif (!$this->local_files->is_readable($file)) {
267 $results['access_denied'][] = $file;
270 $results['different'][] = $file;
274 $this->result = $results;
278 * Return a nice report, a simple overview of the status of this project.
280 function compute_report() {
281 // Ensure we know the differences.
282 $this->compute_differences();
287 'project_name' => $this->name,
288 'status' => HACKED_STATUS_UNCHECKED,
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']),
295 'title' => $this->title(),
298 // Add more details into the report result (if we can).
308 foreach ($details as $item) {
309 if (isset($this->project_info[$item])) {
310 $report[$item] = $this->project_info[$item];
315 if ($report['counts']['access_denied'] > 0) {
316 $report['status'] = HACKED_STATUS_PERMISSION_DENIED;
318 elseif ($report['counts']['missing'] > 0) {
319 $report['status'] = HACKED_STATUS_HACKED;
321 elseif ($report['counts']['different'] > 0) {
322 $report['status'] = HACKED_STATUS_HACKED;
324 elseif ($report['counts']['same'] > 0) {
325 $report['status'] = HACKED_STATUS_UNHACKED;
332 * Return a nice detailed report.
334 function compute_details() {
335 // Ensure we know the differences.
336 $report = $this->compute_report();
338 $report['files'] = array();
340 // Add extra details about every file.
342 'access_denied' => HACKED_STATUS_PERMISSION_DENIED,
343 'missing' => HACKED_STATUS_DELETED,
344 'different' => HACKED_STATUS_HACKED,
345 'same' => HACKED_STATUS_UNHACKED,
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);
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);
364 function file_get_location($storage = 'local', $file) {
367 $this->download_remote_project();
368 return $this->remote_files->file_get_location($file);
370 $this->hash_local_project();
371 return $this->local_files->file_get_location($file);