cvs diff: Diffing project_verify_package
Index: project_verify_package/project_verify_package.info
===================================================================
RCS file: project_verify_package/project_verify_package.info
diff -N project_verify_package/project_verify_package.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ project_verify_package/project_verify_package.info	17 Jan 2010 23:02:32 -0000
@@ -0,0 +1,8 @@
+; $Id$
+name = Project verify package
+description = drupal.org specific verification helpers for the project packaging system.
+package = Project
+dependencies[] = project
+dependencies[] = project_release
+dependencies[] = project_package
+core = 6.x
Index: project_verify_package/project_verify_package.install
===================================================================
RCS file: project_verify_package/project_verify_package.install
diff -N project_verify_package/project_verify_package.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ project_verify_package/project_verify_package.install	17 Jan 2010 23:02:25 -0000
@@ -0,0 +1,13 @@
+<?php
+// $Id$
+
+/**
+* @file
+* Install functions for project_verify_package module.
+*/
+
+function project_verify_package_enable() {
+  // Make this module heavier than project_release module.
+  $weight = db_result(db_query("SELECT weight FROM {system} WHERE name = 'project_release'"));
+  db_query("UPDATE {system} SET weight = %d WHERE name = 'project_verify_package'", $weight + 1);
+}
Index: project_verify_package/project_verify_package.module
===================================================================
RCS file: project_verify_package/project_verify_package.module
diff -N project_verify_package/project_verify_package.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ project_verify_package/project_verify_package.module	17 Jan 2010 23:03:01 -0000
@@ -0,0 +1,357 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * drupal.org specific verification helpers for the project packaging system.
+ *
+ * This module was written to provide drupal.org-specific conversion/validation
+ * functions for .make files used on drupal.org, and it would most likely
+ * require some custom hacking to work in any other environment.
+ */
+
+// --------------------
+// BEGIN CONFIGURATION.
+// --------------------
+
+// The full path of the drush executable.
+define('PROJECT_VERIFY_PACKAGE_DRUSH_BIN', '');
+
+// The full path of the directory that contains the drush_make
+// extensions.
+define('PROJECT_VERIFY_PACKAGE_DRUSH_MAKE_PATH', '');
+
+// The name of the .make file to convert to or verify.
+define('PROJECT_VERIFY_PACKAGE_MAKE_FILE', 'drupal-org.make');
+
+// The term ID that a release node must have in order to require verification.
+define('PROJECT_VERIFY_PACKAGE_PROJECT_TYPE_TID', 96);
+
+// The URL to retrieve the package's .info file. The following tokens are
+// available:
+//   %project_directory - The project_type/project_short_name path (ie.
+//                        profiles/foo).
+//   %makefile - The value of PROJECT_VERIFY_PACKAGE_MAKE_FILE.
+//   %cvs_tag - The CVS tag associated with the release.
+//   %project_title - The full name of the project associated with the release
+//                    node.
+define('PROJECT_VERIFY_PACKAGE_MAKE_FILE_URI', "http://drupalcode.org/viewvc/drupal/contributions/%project_directory/%makefile?view=co&pathrev=%cvs_tag");
+
+// The regex used to test if the CVS tag associated with the release is an
+// official release.
+define('PROJECT_VERIFY_PACKAGE_RELEASE_TAG_REGEX', '/^DRUPAL-(\d+)--(\d+)-(\d+)(-[A-Z0-9]+)?$/');
+
+// URI of the 'How to package a profile' handbook page.
+define('DOCUMENTATION_LINK', 'http://drupal.org/node/642116');
+
+// The error message to display when verification fails for a branch. Uses the
+// same tokens as PROJECT_VERIFY_PACKAGE_MAKE_FILE_URI, plus the following:
+//   !doc_link: Link defined in DOCUMENTATION_LINK.
+//   !output: Escaped output from the drush call.
+define('PROJECT_VERIFY_PACKAGE_BRANCH_ERROR_MESSAGE', "The %makefile file for project %project_title failed verification for CVS branch %cvs_tag.\n\n<a href=\"!doc_link\">!doc_link</a> -- to learn more about correcting these errors.\n\n!output\n\nOnce these errors are fixed, commit them to the branch, then resubmit the release.");
+
+// The error message to display when verification fails for a tag.  Uses the
+// same tokens as PROJECT_VERIFY_PACKAGE_MAKE_FILE_URI, plus the following:
+//   !doc_link: Link defined in DOCUMENTATION_LINK.
+//   !output: Escaped output from the drush call.
+define('PROJECT_VERIFY_PACKAGE_TAG_ERROR_MESSAGE', "The %makefile file for project %project_title failed verification for CVS tag %cvs_tag.\n\n<a href=\"!doc_link\">!doc_link</a> -- to learn more about correcting these errors.\n\n!output\n\nOnce these errors are fixed, commit the changes to your %makefile, move the release tag for your project, and submit the release node again.");
+
+// ------------------
+// END CONFIGURATION.
+// ------------------
+
+
+/**
+ * Implement hook_help().
+ */
+function project_verify_package_help($path, $arg) {
+  switch ($path) {
+    case 'node/%/verify-make-file':
+      return t("This form allows you to verify that your %makefile file is in the correct format. Paste the contents of the file below and click 'Verify'. To learn more about building a compatible .make file, visit <a href=\"!doc_link\">How to package a profile</a>.", array('%makefile' => PROJECT_VERIFY_PACKAGE_MAKE_FILE, '!doc_link' => DOCUMENTATION_LINK));
+    case 'node/%/convert-make-file':
+      return t("This form allows you to convert an existing .make file into the %makefile file format. Disallowed .make file attributes will be automatically removed, and the most up to date official releases of all projects will be output in the result. Paste the contents of the file below and click 'Convert'. To learn more about building a compatible .make file, visit <a href=\"!doc_link\">How to package a profile</a>.", array('%makefile' => PROJECT_VERIFY_PACKAGE_MAKE_FILE, '!doc_link' => DOCUMENTATION_LINK));
+  }
+}
+
+/**
+ * Implement hook_form_alter().
+ */
+function project_verify_package_form_alter(&$form, $form_state, $form_id) {
+  switch ($form_id) {
+    case 'project_release_node_form':
+      // Can't conditionally add the validation to the final release form,
+      // as the form is cached and we don't get back to the alter hook. So,
+      // just always add the validator and check for the final form there.
+      $form['#validate'][] = 'project_verify_package_verify_release_node';
+      break;
+  }
+}
+
+/**
+ * Implement hook_menu().
+ */
+function project_verify_package_menu() {
+  $items = array();
+
+  // 'Verify .make files' link on profile project pages.
+  $items['node/%project_node/verify-make-file'] = array(
+    'title' => 'Verify ' . PROJECT_VERIFY_PACKAGE_MAKE_FILE . ' files',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_verify_package_convert_verify_make_file_form', 1, 'verify'),
+    'access callback' => 'node_access',
+    'access arguments' => array('create', 'project_release'),
+    'type' => MENU_CALLBACK,
+  );
+
+  // 'Convert .make files' link on profile project pages.
+  $items['node/%project_node/convert-make-file'] = array(
+    'title' => 'Convert .make files to ' . PROJECT_VERIFY_PACKAGE_MAKE_FILE . ' format',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_verify_package_convert_verify_make_file_form', 1, 'convert'),
+    'access callback' => 'node_access',
+    'access arguments' => array('create', 'project_release'),
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
+ * Verify if a drupalorg.make file in a release has the right format.
+ */
+function project_verify_package_verify_release_node($form, &$form_state) {
+  // It's the final release form, not the CVS tag picker.
+  if (!empty($form_state['values']['project_release']['version'])) {
+    // Check that it's a project category we want to verify.
+    $project = node_load(array('nid' => $form['#node']->project_release['pid']));
+    if (!empty($project->taxonomy[PROJECT_VERIFY_PACKAGE_PROJECT_TYPE_TID])) {
+      $project_diretory = trim($project->cvs['directory'], '/');
+      $cvs_tag = $form_state['values']['project_release']['tag'];
+      $token_args = array(
+        '%project_directory' => $project_diretory,
+        '%makefile' => PROJECT_VERIFY_PACKAGE_MAKE_FILE,
+        '%cvs_tag' => $cvs_tag,
+        '%project_title' => $project->title,
+        '!doc_link' => DOCUMENTATION_LINK,
+      );
+      // Try to grab the .make file to verify.
+      $url = strtr(PROJECT_VERIFY_PACKAGE_MAKE_FILE_URI, $token_args);
+      if ($makefile = project_verify_package_get_remote_file($url)) {
+        // Run the 'verify makefile' drush command. We only diplay a message
+        // for errors.
+        list ($output, $return) = project_verify_package_run_drush_via_pipe('verify makefile', $makefile);
+        if (!($return === 0)) {
+          // Reformat the output.
+          list ($raw_output, $escaped_output) = project_verify_package_format_exec_output($output);
+          $token_args['!output'] = nl2br($escaped_output);
+          if (preg_match(PROJECT_VERIFY_PACKAGE_RELEASE_TAG_REGEX, $cvs_tag)) {
+            $message = PROJECT_VERIFY_PACKAGE_TAG_ERROR_MESSAGE;
+          }
+          else {
+            $message = PROJECT_VERIFY_PACKAGE_BRANCH_ERROR_MESSAGE;
+          }
+          form_set_error('title', t(nl2br($message), $token_args));
+        }
+      }
+      else {
+        form_set_error('title', t("Pre-packaging verification failed -- unable to retrieve %makefile from %url", array('%makefile' => PROJECT_VERIFY_PACKAGE_MAKE_FILE, '%url' => $url)));
+      }
+    }
+  }
+}
+
+/**
+ * Builds a form used to submit .make files to be converted/verified.
+ */
+function project_verify_package_convert_verify_make_file_form(&$form_state, $node, $operation) {
+  project_project_set_breadcrumb($node, TRUE);
+
+  // Set some display text based on the operation.
+  switch ($operation) {
+    case 'verify':
+      $button_text = t('Verify');
+      $command = 'drush verify makefile';  // CLI command -- not translated
+      break;
+    case 'convert':
+      $button_text = t('Convert');
+      $command = 'drush convert makefile';  // CLI command -- not translated
+      break;
+  }
+
+  $form = array();
+
+  $form['operation'] = array(
+    '#type' => 'value',
+    '#value' => $operation,
+  );
+  if (!empty($form_state['storage']['output'])) {
+    $form['output_container'] = array(
+      '#type' => 'fieldset',
+      '#title' => t("Output of %command", array('%command' => $command)),
+    );
+    $form['output_container']['output'] = array(
+      '#type' => 'markup',
+      '#value' => $form_state['storage']['output'],
+    );
+  }
+  $form['makefile'] = array(
+    '#title' => t('Paste the contents of the .make file here'),
+    '#type' => 'textarea',
+    '#default_value' => empty($form_state['storage']['makefile']) ? '' : $form_state['storage']['makefile'],
+    '#rows' => 20,
+    '#required' => TRUE,
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => $button_text,
+  );
+
+  // Get rid of the storage values, so they don't corrupt a future form
+  // submission.
+  unset($form_state['storage']['makefile'], $form_state['storage']['output']);
+
+  return $form;
+}
+
+/**
+ * Submit handler for the convert/verify .make file form.
+ */
+function project_verify_package_convert_verify_make_file_form_submit($form, &$form_state) {
+  $operation = $form_state['values']['operation'];
+  $makefile = $form_state['values']['makefile'];
+  // Could be dangerous to blindly take the operation from the form, so
+  // explicitly look for it.
+  switch ($operation) {
+    case 'convert':
+      $command = 'convert makefile';
+      break;
+    case 'verify':
+      $command = 'verify makefile';
+      break;
+  }
+  if (isset($command)) {
+    // Run the drush command.
+    list ($output, $return) = project_verify_package_run_drush_via_pipe($command, $makefile);
+
+    // Reformat the output.
+    list ($raw_output, $escaped_output) = project_verify_package_format_exec_output($output);
+
+    // Store the output and the original makefile for display on form reload.
+    $form_state['storage']['makefile'] = $makefile;
+    $form_state['storage']['output'] = nl2br($escaped_output);
+
+    // Operation succeeded.
+    if ($return === 0) {
+      drupal_set_message(t('The attempt to %operation the .make file was successful.', array('%operation' => $operation)));
+    }
+    // Operation failed.
+    else {
+      drupal_set_message(t('Errors occured when attempting to %operation the .make file.', array('%operation' => $operation)), 'error');
+    }
+  }
+  // If we made it here, somebody is trying to do something nasty, so log it.
+  else {
+    watchdog('package', t('Malicious attempt to submit command %command to server.', array('%command' => $command)), array(), WATCHDOG_ERROR);
+  }
+
+}
+
+/**
+ * Implemenation of hook_project_page_link_alter().
+ *
+ * Note:  This is *not* an implementation of hook_link_alter().
+ */
+function project_verify_package_project_page_link_alter(&$links, $node) {
+  // Insert the links on all project nodes that:
+  //   1. Have releases enabled.
+  //   2. Have the PROJECT_VERIFY_PACKAGE_PROJECT_TYPE_TID term ID.
+  if (!empty($node->project_release['releases']) && !empty($node->taxonomy[PROJECT_VERIFY_PACKAGE_PROJECT_TYPE_TID])) {
+    $links['project_release']['links']['project_verify_package_verify_make_file_link'] = l(t('Verify release .make files'), "node/$node->nid/verify-make-file");
+    $links['project_release']['links']['project_verify_package_convert_make_file_link'] = l(t('Convert release .make files'), "node/$node->nid/convert-make-file");
+  }
+}
+
+/**
+ * Runs a drush command via pipes, so that nothing touches the I/O subsystem.
+ *
+ * @param $ommand
+ *   The drush command to send.
+ * @param $input
+ *   The input to pipe to the drush command.
+ * @return
+ *   An array, the first element is the command output, and second element is
+ *   the command return value.
+ */
+function project_verify_package_run_drush_via_pipe($command, $input) {
+  // drush expects a terminal, so give it one.
+  putenv("TERM=vt100");
+  // Made sure we grab stderr, too...
+  exec('echo ' . escapeshellarg($input) . ' | ' . escapeshellcmd(PROJECT_VERIFY_PACKAGE_DRUSH_BIN) . ' --include=' . escapeshellarg(PROJECT_VERIFY_PACKAGE_DRUSH_MAKE_PATH) . ' ' . escapeshellarg($command) . " - 2>&1", $output, $return_value);
+  return array($output, $return_value);
+}
+
+/**
+ * Helper to get file contents from a URL using a variety of methods.
+ *
+ * If PHP is configured to allow URLs in fopen(), we use file_get_contents().
+ * Otherwise, if PHP has libcurl loaded, we use that. Finally, we fall back to
+ * attempting to execute wget from a pipe.
+ *
+ * @param $url
+ *   URL for the file you want to fetch.
+ * @return
+ *   String containing the contents of the requested file, or FALSE on error.
+ */
+function project_verify_package_get_remote_file($url) {
+  $file_contents = FALSE;
+
+  // Use fopen if allowed.
+  if (ini_get('allow_url_fopen')) {
+    $file_contents = file_get_contents($url);
+  }
+  // Fallback to cURL if it exists.
+  elseif (function_exists('curl_init')) {
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_TIMEOUT, 50);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
+    $file_contents = curl_exec($ch);
+    curl_close($ch);
+  }
+  // Last try, wget.
+  else {
+    $output = passthru('wget -q -O - ' . escapeshellarg($url), $return_value);
+    if ($return_value === 0) {
+      $file_contents = $output;
+    }
+  }
+
+  return $file_contents;
+}
+
+/**
+ * Format the output returned from an exec() call.
+ *
+ * @param $output
+ *   An array of output data, as returned from exec().
+ * @return
+ *   An array of strings, each string is a formatted version of the $output
+ *   array. The first element is the raw output, the second element is
+ *   escaped, and safe to output to a browser. In both cases, the lines are
+ *   passed through trim() and any blank lines are removed.
+ */
+function project_verify_package_format_exec_output($output) {
+  $escaped_output_array = array();
+  $raw_output_array = array();
+  foreach ($output as $line) {
+    $line = trim($line);
+    if (!empty($line)) {
+      $escaped_output_array[] = check_plain($line);
+      $raw_output_array[] = $line;
+    }
+  }
+  $escaped_output = implode("\n", $escaped_output_array);
+  $raw_output = implode("\n", $raw_output_array);
+
+  return array($raw_output, $escaped_output);
+}
