From 3d9f66c2f86612e83a9380eee905795280659450 Mon Sep 17 00:00:00 2001
From: Colan Schwartz <colan@58704.no-reply.drupal.org>
Date: Thu, 25 Feb 2016 17:18:01 -0500
Subject: [PATCH] Issue #2066371 by FluxSauce, codi, colan: Added Drush support
 without installation & integrated with Site Audit.

---
 hacked.drush.inc      | 196 ++++++++++++++++++++++++++++++++------------------
 hacked.module         |  12 +++-
 hacked.report.inc     |  11 ++-
 hacked.site_audit.inc | 110 ++++++++++++++++++++++++++++
 4 files changed, 255 insertions(+), 74 deletions(-)
 create mode 100644 hacked.site_audit.inc

diff --git a/hacked.drush.inc b/hacked.drush.inc
index 640127b..d04e516 100644
--- a/hacked.drush.inc
+++ b/hacked.drush.inc
@@ -1,22 +1,43 @@
 <?php
+/**
+ * @file
+ * Hacked! drush support.
+ */
 
+if (function_exists('hacked_load_drush_dependencies')) {
+  return;
+}
 
 /**
- * @file
- *   Hacked drush command.
- *
- *   Enables drush support for the Hacked! module.
+ * Allow Hacked! to be used without enabling the module.
  */
+function hacked_load_drush_dependencies() {
+  if (!module_exists('hacked')) {
+    drush_log(dt('Loading Hacked! dependencies.'));
+    require_once __DIR__ . '/hacked.module';
+    require_once __DIR__ . '/includes/hackedFileGroup.inc';
+    require_once __DIR__ . '/includes/hackedFileHasher.inc';
+    require_once __DIR__ . '/includes/hackedFileIgnoreEndingsHasher.inc';
+    require_once __DIR__ . '/includes/hackedFileIncludeEndingsHasher.inc';
+    require_once __DIR__ . '/includes/hackedProject.inc';
+    require_once __DIR__ . '/includes/hackedProjectWebDownloader.inc';
+    require_once __DIR__ . '/includes/hackedProjectWebCVSDownloader.inc';
+    require_once __DIR__ . '/includes/hackedProjectWebFilesDownloader.inc';
+    require_once __DIR__ . '/hacked.report.inc';
+  }
+}
 
 /**
- * Implementation of hook_drush_help().
+ * Implements hook_drush_help().
  */
 function hacked_drush_help($section) {
   switch ($section) {
     case 'drush:hacked-list-projects':
       return dt('List projects and their hacked/unhacked status.');
+
     case 'drush:hacked-details':
       return dt('Show details of the files in one project, and the hacked/unhacked status of those files.');
+
     case 'drush:hacked-diff':
       return dt('Output a unified diff of the specified project.');
 
@@ -24,50 +45,41 @@ function hacked_drush_help($section) {
 }
 
 /**
- * Implementation of hook_drush_command().
- *
- * @See drush_parse_command() for a list of recognized keys.
- *
- * @return
- *   An associative array describing your command(s).
+ * Implements hook_drush_command().
  */
 function hacked_drush_command() {
   $items = array();
 
   $items['hacked-list-projects'] = array(
-    'description' => "List all projects that can be analysed by Hacked! ",
-    'drupal dependencies' => array('hacked'),
+    'description' => dt('List all projects that can be analysed by Hacked!'),
     'options' => array(
-      'force-rebuild' => 'Rebuild the Hacked! report instead of getting a cached version.'
+      'force-rebuild' => dt('Rebuild the Hacked! report instead of getting a cached version.'),
     ),
     'aliases' => array('hlp'),
   );
 
   $items['hacked-lock-modified'] = array(
-    'description' => "Lock all projects that Hacked! detects are modified, so that drush pm-updatecode will not touch them. (drush-4.x+ only)",
-    'drupal dependencies' => array('hacked'),
+    'description' => dt('Lock all projects that Hacked! detects are modified so pm-updatecode will not change them.'),
   );
 
   $items['hacked-details'] = array(
-    'description' => "Show the Hacked! report about a specific project.",
-    'drupal dependencies' => array('hacked'),
+    'description' => dt('Show the Hacked! report about a specific project.'),
     'arguments' => array(
-      'project' => 'The machine name of the project to report on.',
+      'project' => dt('The machine name of the project to report on.'),
     ),
     'options' => array(
-      'include-unchanged' => 'Show the files that are unchanged too.',
+      'include-unchanged' => dt('List unchanged files.'),
     ),
     'aliases' => array('hd'),
   );
 
   $items['hacked-diff'] = array(
-    'description' => "Output a unified diff of the project specified.",
-    'drupal dependencies' => array('hacked'),
+    'description' => dt('Output a unified diff of the project specified.'),
     'arguments' => array(
-      'project' => 'The machine name of the project to report on.',
+      'project' => dt('The machine name of the project to report on.'),
     ),
     'options' => array(
-      'diff-options' => 'Command line options to pass through to the diff command.'
+      'diff-options' => dt('Command line options to pass through to the diff command.'),
     ),
   );
 
@@ -81,10 +93,10 @@ function hacked_drush_command() {
  * So you'll want to be very careful if you call this!
  */
 function hacked_calculate_project_data_drush($projects, $force = FALSE, $redirect = NULL) {
-  include_once DRUPAL_ROOT . '/includes/batch.inc';
+  hacked_load_drush_dependencies();
 
   // Try to get the report form cache if we can.
-  $cache = cache_get('hacked:drush:full-report', HACKED_CACHE_TABLE);
+  $cache = drush_cache_get('hacked:drush:full-report', HACKED_CACHE_TABLE);
   if (!empty($cache->data) && !$force) {
     return $cache->data;
   }
@@ -93,7 +105,7 @@ function hacked_calculate_project_data_drush($projects, $force = FALSE, $redirec
   $operations = array();
   foreach ($projects as $project) {
     $operations[] = array(
-      'hacked_build_report_batch',
+      'hacked_build_report_batch_drush',
       array($project['name']),
     );
   }
@@ -102,7 +114,7 @@ function hacked_calculate_project_data_drush($projects, $force = FALSE, $redirec
     'operations' => $operations,
     'finished' => 'hacked_build_report_batch_finished_drush',
     'file' => drupal_get_path('module', 'hacked') . '/hacked.report.inc',
-    'title' => t('Building report'),
+    'title' => dt('Building report'),
   );
 
   drush_print('Rebuilding Hacked! report');
@@ -113,13 +125,29 @@ function hacked_calculate_project_data_drush($projects, $force = FALSE, $redirec
   drush_print('Done.');
 
   // Now we can get the data from the cache.
-  $cache = cache_get('hacked:drush:full-report', HACKED_CACHE_TABLE);
+  $cache = drush_cache_get('hacked:drush:full-report', HACKED_CACHE_TABLE);
   if (!empty($cache->data)) {
     return $cache->data;
   }
 }
 
 /**
+ * Batch callback to build the hacked report.
+ */
+function hacked_build_report_batch_drush($project_name, &$context) {
+  hacked_load_drush_dependencies();
+  if (!isset($context['results']['report'])) {
+    $context['results']['report'] = array();
+  }
+
+  $project = new hackedProject($project_name);
+  $context['results']['report'][$project_name] = $project->compute_report();
+  $context['message'] = dt('Finished processing: @name', array(
+    '@name' => $project->title(),
+  ));
+}
+
+/**
  * Completion callback for the report batch.
  */
 function hacked_build_report_batch_finished_drush($success, $results, $operations) {
@@ -127,22 +155,32 @@ function hacked_build_report_batch_finished_drush($success, $results, $operation
     // Sort the results.
     usort($results['report'], '_hacked_project_report_sort_by_status');
     // Store them.
-    cache_set('hacked:drush:full-report', $results['report'], HACKED_CACHE_TABLE, strtotime('+1 day'));
+    drush_cache_set('hacked:drush:full-report', $results['report'], HACKED_CACHE_TABLE, strtotime('+1 day'));
   }
 }
 
 /**
- * Drush command callback that shows the listing of changed/unchanged projects.
+ * Validation callback for drush hacked_list_projects.
  */
-function drush_hacked_list_projects() {
+function drush_hacked_list_projects_validate() {
+  if (!function_exists('update_get_available')) {
+    return drush_set_error('HACKED_UPDATE_DISABLED', 'Cannot list projects without the Drupal core update module enabled.');
+  }
+}
 
-  // Go get the data:
+/**
+ * Command callback for drush hacked_list_projects.
+ */
+function drush_hacked_list_projects() {
+  hacked_load_drush_dependencies();
+  // Get the data.
   module_load_include('inc', 'update', 'update.report');
   if ($available = update_get_available(TRUE)) {
     module_load_include('inc', 'update', 'update.compare');
     $data = update_calculate_project_data($available);
     $force_rebuild = drush_get_option('force-rebuild', FALSE);
     $projects = hacked_calculate_project_data_drush($data, $force_rebuild);
+
     // Now print the data using drush:
     $rows[] = array(
       dt('Title'),
@@ -152,11 +190,12 @@ function drush_hacked_list_projects() {
       dt('Changed'),
       dt('Deleted'),
     );
+
     foreach ($projects as $project) {
       $row = array(
         $project['title'],
         $project['name'],
-        $project['existing_version']
+        $project['existing_version'],
       );
 
       // Now add the status:
@@ -164,19 +203,21 @@ function drush_hacked_list_projects() {
         case HACKED_STATUS_UNHACKED:
           $row[] = dt('Unchanged');
           break;
+
         case HACKED_STATUS_HACKED:
-          $row[] = t('Changed');
+          $row[] = dt('Changed');
           break;
+
         case HACKED_STATUS_UNCHECKED:
         default:
-          $row[] = t('Unchecked');
+          $row[] = dt('Unchecked');
           break;
+
       }
 
       $row[] = $project['counts']['different'];
       $row[] = $project['counts']['missing'];
 
-
       $rows[] = $row;
     }
     drush_print_table($rows, TRUE);
@@ -186,8 +227,7 @@ function drush_hacked_list_projects() {
 }
 
 /**
- * Lock all of the modified files so that pm-updatecode will not
- * touch them.
+ * Lock modified files so that pm-updatecode won't touch them.
  */
 function drush_hacked_lock_modified() {
   $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT');
@@ -201,7 +241,7 @@ function drush_hacked_lock_modified() {
       $row = array(
         empty($project['title']) ? $project['name'] : $project['title'],
         $project['name'],
-        $project['existing_version']
+        $project['existing_version'],
       );
 
       // Lock the file if it is not already locked.
@@ -211,24 +251,30 @@ function drush_hacked_lock_modified() {
           $project_ob = hacked_project_load($project['project_name']);
           $lockfile = $project_ob->file_get_location('local', '.drush-lock-update');
           if (!file_exists($lockfile)) {
-            drush_op('file_put_contents', $lockfile, dt("Locked: modified."));
-            drush_print(dt("Locked: @project", array('@project' => $project['name'])));
+            drush_op('file_put_contents', $lockfile, dt('Locked: modified.'));
+            drush_print(dt('Locked: @project', array(
+              '@project' => $project['name'],
+            )));
           }
           else {
-            drush_print(dt("@project is modified and already locked", array('@project' => $project['name'])));
+            drush_print(dt('@project is modified and already locked', array(
+              '@project' => $project['name'],
+            )));
           }
           break;
+
         case HACKED_STATUS_UNHACKED:
         case HACKED_STATUS_UNCHECKED:
         default:
           break;
+
       }
     }
   }
 }
 
 /**
- * Add a --lock-modified flag to pm-updatecode
+ * Add a --lock-modified flag to pm-updatecode.
  */
 function drush_hacked_pre_pm_updatecode() {
   if (drush_get_option('lock-modified')) {
@@ -239,11 +285,11 @@ function drush_hacked_pre_pm_updatecode() {
 }
 
 /**
- * Add --lock-modified to the pm-updatecode and pm-update help
+ * Add --lock-modified to the pm-updatecode and pm-update help.
  */
 function hacked_drush_help_alter(&$command) {
   if (($command['command'] == 'pm-updatecode') || ($command['command'] == 'pm-update')) {
-    $command['sub-options']['--lock']['--lock-modified'] = "Lock any project that Hacked! determines is modified.";
+    $command['sub-options']['--lock']['--lock-modified'] = dt('Lock any project that Hacked! determines is modified.');
   }
 }
 
@@ -255,26 +301,31 @@ function drush_hacked_details_validate($short_name = '') {
 }
 
 /**
- * Validate hook for the hacked drush commands that need a project.
+ * Generic validator for hacked drush commands.
+ *
+ * @param string $short_name
+ *   The name of the project in question.
+ *
+ * @return null|boolean
+ *   Return FALSE if there's an error.
  */
 function drush_hacked_drush_command_validate($short_name = '') {
+  hacked_load_drush_dependencies();
   if (empty($short_name)) {
-    return drush_set_error('HACKED_PROJECT_NOT_FOUND', dt('A valid project must be specified', array('@project' => $short_name)));
+    return drush_set_error('HACKED_PROJECT_NOT_FOUND', dt('You must specify the name of a project.'));
   }
 
   $project = hacked_project_load($short_name);
   $project->identify_project();
   if (!$project->project_identified) {
-    return drush_set_error('HACKED_PROJECT_NOT_FOUND', dt('Could not find project: @project', array('@project' => $short_name)));
+    return drush_set_error('HACKED_PROJECT_NOT_FOUND', dt('Unable to find "@project".', array(
+      '@project' => $short_name,
+    )));
   }
-  $project = NULL;
 }
 
 /**
- * Drush command callback that shows the list of changes/unchanged files in a project.
- *
- * You may specify the --include-unchanged option to show unchanged files too,
- * otherwise just the changed and deleted files are shown.
+ * Command callback for drush hacked_details.
  */
 function drush_hacked_details($short_name) {
   $project = hacked_project_load($short_name);
@@ -301,48 +352,44 @@ function drush_hacked_details($short_name) {
     if (!$show_unchanged && $status == HACKED_STATUS_UNHACKED) {
       continue;
     }
-    $row = array(
-    );
+    $row = array();
 
     // Now add the status:
     switch ($status) {
       case HACKED_STATUS_UNHACKED:
         $row[] = dt('Unchanged');
         break;
+
       case HACKED_STATUS_HACKED:
-        $row[] = t('Changed');
+        $row[] = dt('Changed');
         break;
+
       case HACKED_STATUS_DELETED:
-        $row[] = t('Deleted');
+        $row[] = dt('Deleted');
         break;
+
       case HACKED_STATUS_UNCHECKED:
       default:
-        $row[] = t('Unchecked');
+        $row[] = dt('Unchecked');
         break;
+
     }
 
     $row[] = $file;
-
-
     $rows[] = $row;
   }
   drush_print_table($rows, TRUE);
-
-
 }
 
 /**
- * Validate hook for the hacked_diff drush command.
+ * Validation callback for drush hacked_diff.
  */
 function drush_hacked_diff_validate($short_name = '') {
   return drush_hacked_drush_command_validate($short_name);
 }
 
 /**
- * Drush command callback that shows the list of changes/unchanged files in a project.
- *
- * You may specify the --include-unchanged option to show unchanged files too,
- * otherwise just the changed and deleted files are shown.
+ * Command callback for drush hacked_diff.
  */
 function drush_hacked_diff($short_name) {
   $project = hacked_project_load($short_name);
@@ -375,9 +422,18 @@ function drush_hacked_diff($short_name) {
       $line = str_replace($clean_location_trim, 'a/', $line);
       $line = str_replace($local_location_trim, 'b/', $line);
     }
-
     drush_print($line);
-
   }
+}
 
+/**
+ * Implements hook_drush_command_alter().
+ */
+function hacked_drush_command_alter(&$command) {
+  if ($command['command'] == 'audit_security') {
+    $command['checks'][] = array(
+      'name' => 'Hacked',
+      'location' => __DIR__ . '/hacked.site_audit.inc',
+    );
+  }
 }
diff --git a/hacked.module b/hacked.module
index d51f97f..8ccbad5 100644
--- a/hacked.module
+++ b/hacked.module
@@ -19,6 +19,10 @@ define('HACKED_STATUS_UNHACKED', 5);
 
 define('HACKED_DEFAULT_FILE_HASHER', 'hacked_ignore_line_endings');
 
+if (function_exists('hacked_menu')) {
+  return;
+}
+
 /**
  * Implementation of hook_menu().
  */
@@ -153,7 +157,6 @@ function hacked_permission() {
  * So you'll want to be very careful if you call this!
  */
 function hacked_calculate_project_data($projects, $force = FALSE, $redirect = NULL) {
-
   // Try to get the report form cache if we can.
   $cache = cache_get('hacked:full-report', HACKED_CACHE_TABLE);
   if (!empty($cache->data) && !$force) {
@@ -339,7 +342,12 @@ function hacked_get_file_hasher($name = NULL) {
   if (is_null($name)) {
     $name = variable_get('hacked_selected_file_hasher', HACKED_DEFAULT_FILE_HASHER);
   }
-  $hashers = hacked_get_file_hashers();
+  if (!module_exists('hacked')) {
+    $hashers = hacked_hacked_file_hashers_info();
+  }
+  else {
+    $hashers = hacked_get_file_hashers();
+  }
   $class_name = $hashers[$name]['class'];
   return new $class_name;
 }
diff --git a/hacked.report.inc b/hacked.report.inc
index 20659a8..4117617 100644
--- a/hacked.report.inc
+++ b/hacked.report.inc
@@ -1,5 +1,8 @@
 <?php
-
+/**
+ * @file
+ * Report building utilities.
+ */
 
 /**
  * Page callback to build up a full report.
@@ -35,10 +38,14 @@ function hacked_reports_rebuild() {
  * Batch callback to build the hacked report.
  */
 function hacked_build_report_batch($project_name, &$context) {
+  // Add missing dependencies if the module isn't installed.
+  if (function_exists('drush_verify_cli') && drush_verify_cli()) {
+    hacked_load_drush_dependencies();
+  }
+
   if (!isset($context['results']['report'])) {
     $context['results']['report'] = array();
   }
-  module_load_include('inc', 'hacked', 'includes/hacked_project');
   $project = new hackedProject($project_name);
   $context['results']['report'][$project_name] = $project->compute_report();
   $context['message'] = t('Finished processing: @name', array('@name' => $project->title()));
diff --git a/hacked.site_audit.inc b/hacked.site_audit.inc
new file mode 100644
index 0000000..0d934e0
--- /dev/null
+++ b/hacked.site_audit.inc
@@ -0,0 +1,110 @@
+<?php
+/**
+ * @file
+ * Contains \SiteAudit\Check\Codebase\Hacked.
+ */
+
+class SiteAuditCheckSecurityHacked extends SiteAuditCheckAbstract {
+  /**
+   * Implements \SiteAudit\Check\Abstract\getLabel().
+   */
+  public function getLabel() {
+    return dt('Hacked');
+  }
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\getDescription().
+   */
+  public function getDescription() {
+    return dt('Determine whether core or contrib modules have been modified.');
+  }
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\getResultFail().
+   */
+  public function getResultFail() {
+    return dt('Unable to determine whether core or contrib modules have been modified!');
+  }
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\getResultInfo().
+   */
+  public function getResultInfo() {
+    return dt('Hacked! module not available to drush, either as part of site to be audited or in drush commands.');
+  }
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\getResultPass().
+   */
+  public function getResultPass() {
+    return dt('No modifications were found.');
+  }
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\getResultWarn().
+   */
+  public function getResultWarn() {
+    $ret_val = dt('The following extension(s) have been modified:');
+
+    if (drush_get_option('html')) {
+      $ret_val = '<p>' . $ret_val . '</p>';
+      $ret_val .= '<table class="table table-condensed">';
+      $ret_val .= '<thead><tr><th>Name</th><th>Title</th><th>Version</th><th>Changed</th></thead>';
+      $ret_val .= '<tbody>';
+      foreach ($this->registry['hacked'] as $row) {
+        $ret_val .= '<tr><td>' . implode('</td><td>', $row) . '</td></tr>';
+      }
+      $ret_val .= '</tbody>';
+      $ret_val .= '</table>';
+    }
+    else {
+      foreach ($this->registry['hacked'] as $row) {
+        $ret_val .= PHP_EOL . str_repeat(' ', 6);
+        $ret_val .= $row['project_name'] . ' v' . $row['project_version'];
+        $ret_val .= ': ' . $row['lines_different'] . ' line(s)';
+      }
+    }
+    return $ret_val;
+  }
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\getAction().
+   */
+  public function getAction() {}
+
+  /**
+   * Implements \SiteAudit\Check\Abstract\calculateScore().
+   */
+  public function calculateScore() {
+    $result = drush_invoke_process('@self', 'hacked-list-projects', array(), array('--include-unchanged=0', '--strict=0'), FALSE);
+    if ($result === FALSE) {
+      return SiteAuditCheckAbstract::AUDIT_CHECK_SCORE_INFO;
+    }
+
+    $rows = array();
+    foreach ($result['object'] as $info) {
+      if ($info['counts']['different'] != 0) {
+        $rows[] = array(
+          'project_name' => $info['project_name'],
+          'project_title' => $info['title'],
+          'project_version' => $info['existing_version'],
+          'lines_different' => $info['counts']['different'],
+        );
+      }
+    }
+
+    $this->registry['hacked'] = $result;
+    if (empty($result['object'])) {
+      return SiteAuditCheckAbstract::AUDIT_CHECK_SCORE_FAIL;
+    }
+    else {
+      if (!empty($rows)) {
+        $this->registry['hacked'] = $rows;
+        return SiteAuditCheckAbstract::AUDIT_CHECK_SCORE_WARN;
+      }
+      else {
+        return SiteAuditCheckAbstract::AUDIT_CHECK_SCORE_PASS;
+      }
+    }
+  }
+}
-- 
2.5.0

