* $project = hackedProject('context'); * $project->compute_differences(); * * * Which is quite nice I think. */ class hackedProject { use StringTranslationTrait; var $name = ''; var $project_info = array(); var $remote_files_downloader; /* @var hackedFileGroup $remote_files */ var $remote_files; /* @var hackedFileGroup $local_files */ var $local_files; var $project_type = ''; var $existing_version = ''; var $result = array(); var $project_identified = FALSE; var $remote_downloaded = FALSE; var $remote_hashed = FALSE; var $local_hashed = FALSE; /** * Constructor. */ function __construct($name) { $this->name = $name; $this->remote_files_downloader = new hackedProjectWebFilesDownloader($this); } /** * Get the Human readable title of this project. */ function title() { $this->identify_project(); return isset($this->project_info['title']) ? $this->project_info['title'] : $this->name; } /** * Identify the project from the name we've been created with. * * We leverage the update (status) module to get the data we require about * projects. We just pull the information in, and make descisions about this * project being from CVS or not. */ function identify_project() { // Only do this once, no matter how many times we're called. if (!empty($this->project_identified)) { return; } // Fetch the required data from the update (status) module. // TODO: clean this up. $available = update_get_available(TRUE); $data = update_calculate_project_data($available); $releases = \Drupal::keyValueExpirable('update_available_releases') ->getAll(); foreach ($data as $key => $project) { if ($key == $this->name) { $this->project_info = $project; if (!isset($this->project_info['releases']) || !is_array($this->project_info['releases'])) { $this->project_info['releases'] = array(); } if (isset($releases[$key]['releases']) && is_array($releases[$key]['releases'])) { $this->project_info['releases'] += $releases[$key]['releases']; } // Add in the additional info that update module strips out. // This is a really naff way of doing this, but update (status) module // ripped out a lot of useful stuff in issue: // http://drupal.org/node/669554 $this->project_identified = TRUE; $this->existing_version = $this->project_info['existing_version']; $this->project_type = $this->project_info['project_type']; break; } } // Logging. if (!$this->project_identified) { $message = $this->t('Could not identify project: @name', array('@name' => $this->name)); \Drupal::logger('hacked')->warning($message->render()); } } /** * Downloads the remote project to be hashed later. */ function download_remote_project() { // Only do this once, no matter how many times we're called. if (!empty($this->remote_downloaded)) { return; } $this->identify_project(); $this->remote_downloaded = (bool) $this->remote_files_downloader->download(); // Logging. if (!$this->remote_downloaded) { $message = $this->t('Could not download project: @title', array('@title' => $this->title())); \Drupal::logger('hacked')->error($message->render()); } } /** * Hashes the remote project downloaded earlier. */ function hash_remote_project() { // Only do this once, no matter how many times we're called. if (!empty($this->remote_hashed)) { return; } // Ensure that the remote project has actually been downloaded. $this->download_remote_project(); // Set up the remote file group. $base_path = $this->remote_files_downloader->get_final_destination(); $this->remote_files = hackedFileGroup::fromDirectory($base_path); $this->remote_files->compute_hashes(); $this->remote_hashed = !empty($this->remote_files->files); // Logging. if (!$this->remote_hashed) { $message = $this->t('Could not hash remote project: @title', array('@title' => $this->title())); \Drupal::logger('hacked')->error($message->render()); } } /** * Locate the base directory of the local project. */ function locate_local_project() { // we need a remote project to do this :( $this->hash_remote_project(); // Do we have at least some modules to check for: if (!is_array($this->project_info['includes']) || !count($this->project_info['includes'])) { return FALSE; } // If this project is drupal it, we need to handle it specially if ($this->project_type != 'core') { $includes = array_keys($this->project_info['includes']); $include = array_shift($includes); $include_type = $this->project_info['project_type']; } else { // Just use the system module to find where we've installed drupal $include = 'system'; $include_type = 'module'; } //$include = 'image_captcha'; $path = drupal_get_path($include_type, $include); // Now we need to find the path of the info file in the downloaded package: $temp = ''; foreach ($this->remote_files->files as $file) { if (preg_match('@(^|.*/)' . $include . '.info.yml$@', $file)) { $temp = $file; break; } } // How many '/' were in that path: $slash_count = substr_count($temp, '/'); $back_track = str_repeat('/..', $slash_count); return realpath($path . $back_track); } /** * Hash the local version of the project. */ function hash_local_project() { // Only do this once, no matter how many times we're called. if (!empty($this->local_hashed)) { return; } $location = $this->locate_local_project(); $this->local_files = hackedFileGroup::fromList($location, $this->remote_files->files); $this->local_files->compute_hashes(); $this->local_hashed = !empty($this->local_files->files); // Logging. if (!$this->local_hashed) { $message = $this->t('Could not hash local project: @title', ['@title' => $this->title()]); \Drupal::logger('hacked')->error($message->render()); } } /** * Compute the differences between our version and the canonical version of the project. */ function compute_differences() { // Make sure we've hashed both remote and local files. $this->hash_remote_project(); $this->hash_local_project(); $results = [ 'same' => [], 'different' => [], 'missing' => [], 'access_denied' => [], ]; // Now compare the two file groups. foreach ($this->remote_files->files as $file) { if ($this->remote_files->files_hashes[$file] == $this->local_files->files_hashes[$file]) { $results['same'][] = $file; } elseif (!$this->local_files->file_exists($file)) { $results['missing'][] = $file; } elseif (!$this->local_files->is_readable($file)) { $results['access_denied'][] = $file; } else { $results['different'][] = $file; } } $this->result = $results; } /** * Return a nice report, a simple overview of the status of this project. */ function compute_report() { // Ensure we know the differences. $this->compute_differences(); // Do some counting $report = [ 'project_name' => $this->name, 'status' => HACKED_STATUS_UNCHECKED, 'counts' => [ 'same' => count($this->result['same']), 'different' => count($this->result['different']), 'missing' => count($this->result['missing']), 'access_denied' => count($this->result['access_denied']), ], 'title' => $this->title(), ]; // Add more details into the report result (if we can). $details = array( 'link', 'name', 'existing_version', 'install_type', 'datestamp', 'project_type', 'includes', ); foreach ($details as $item) { if (isset($this->project_info[$item])) { $report[$item] = $this->project_info[$item]; } } if ($report['counts']['access_denied'] > 0) { $report['status'] = HACKED_STATUS_PERMISSION_DENIED; } elseif ($report['counts']['missing'] > 0) { $report['status'] = HACKED_STATUS_HACKED; } elseif ($report['counts']['different'] > 0) { $report['status'] = HACKED_STATUS_HACKED; } elseif ($report['counts']['same'] > 0) { $report['status'] = HACKED_STATUS_UNHACKED; } return $report; } /** * Return a nice detailed report. */ function compute_details() { // Ensure we know the differences. $report = $this->compute_report(); $report['files'] = array(); // Add extra details about every file. $states = array( 'access_denied' => HACKED_STATUS_PERMISSION_DENIED, 'missing' => HACKED_STATUS_DELETED, 'different' => HACKED_STATUS_HACKED, 'same' => HACKED_STATUS_UNHACKED, ); foreach ($states as $state => $status) { foreach ($this->result[$state] as $file) { $report['files'][$file] = $status; $report['diffable'][$file] = $this->file_is_diffable($file); } } return $report; } function file_is_diffable($file) { $this->hash_remote_project(); $this->hash_local_project(); return $this->remote_files->is_not_binary($file) && $this->local_files->is_not_binary($file); } function file_get_location($storage = 'local', $file) { switch ($storage) { case 'remote': $this->download_remote_project(); return $this->remote_files->file_get_location($file); case 'local': $this->hash_local_project(); return $this->local_files->file_get_location($file); } return FALSE; } }