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	18 Jan 2010 19:17:58 -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	18 Jan 2010 19:17:58 -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	19 Jan 2010 08:15:17 -0000
@@ -0,0 +1,97 @@
+<?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 term ID that a release node must have in order to require verification.
+define('PROJECT_VERIFY_PACKAGE_PROJECT_TYPE_TID', 96);
+
+// NOTE: Other configuration settings need to be defined at the top of the
+// project_verify_package.pages.inc file.
+
+// -----------------
+// END CONFIGURATION
+// -----------------
+
+/**
+ * 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 drupal-org.make 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,
+    'file' => 'project_verify_package.pages.inc',
+  );
+
+  // 'Convert .make files' link on profile project pages.
+  $items['node/%project_node/convert-make-file'] = array(
+    'title' => 'Convert .make files to drupal-org.make 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,
+    'file' => 'project_verify_package.pages.inc',
+  );
+
+  return $items;
+}
+
+/**
+ * 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");
+  }
+}
+
+/**
+ * Verify if a drupalorg.make file in a release has the right format.
+ *
+ * This function must be included in the main module file, as hook_form_alter()
+ * is not called on cached forms. It's provided here as a simple wrapper for
+ * the internal validation function.
+ */
+function project_verify_package_verify_release_node($form, &$form_state) {
+  require_once(drupal_get_path('module', 'project_verify_package') . '/project_verify_package.pages.inc');
+  _project_verify_package_verify_release_node($form, $form_state);
+}
Index: project_verify_package/project_verify_package.pages.inc
===================================================================
RCS file: project_verify_package/project_verify_package.pages.inc
diff -N project_verify_package/project_verify_package.pages.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ project_verify_package/project_verify_package.pages.inc	19 Jan 2010 08:24:09 -0000
@@ -0,0 +1,364 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Page callbacks for drupal.org specific verification helpers for the project
+ * packaging system.
+ */
+
+// -------------------
+// 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 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.
+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 name of the .make file to convert to or verify.
+define('PROJECT_VERIFY_PACKAGE_MAKE_FILE', 'drupal-org.make');
+
+// -----------------
+// END CONFIGURATION
+// -----------------
+
+
+/**
+ * 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');
+      $output_title = t('Verification messages');
+      $makefile_title = t('Paste the contents of the .make file to be verified');
+      $help_text = 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));
+      break;
+
+    case 'convert':
+      $button_text = t('Convert');
+      $output_title = t('Conversion messages');
+      $makefile_title = t('Paste the contents of the .make file to be converted');
+      $help_text = 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));
+      break;
+
+  }
+
+  $form = array();
+
+  $form['operation'] = array(
+    '#type' => 'value',
+    '#value' => $operation,
+  );
+  $form['help'] = array(
+    '#type' => 'markup',
+    '#value' => $help_text,
+  );
+  // The form is being redisplayed after running a drush command.
+  if (isset($form_state['storage']['return'])) {
+    // Special case for successful conversions -- put them in a textarea to aid
+    // copy/paste.
+    if ($operation == 'convert' && $form_state['storage']['return'] == '0') {
+      $form['converted_file_display'] = array(
+        '#title' => t('Converted makefile'),
+        '#type' => 'textarea',
+        '#default_value' => $form_state['storage']['output'],
+        '#rows' => count(explode("\n", $form_state['storage']['output'])),
+      );
+    }
+    else {
+      $output_array = array();
+      if (!empty($form_state['storage']['errors'])) {
+        $output_array[] = nl2br($form_state['storage']['errors']);
+      }
+      if (!empty($form_state['storage']['escaped_output'])) {
+        $output_array[] = nl2br($form_state['storage']['output']);
+      }
+      $form['output_container'] = array(
+        '#type' => 'fieldset',
+        '#title' => $output_title,
+      );
+      $form['output_container']['output'] = array(
+        '#type' => 'markup',
+        '#value' => implode("<br /><br />", $output_array),
+      );
+    }
+  }
+  $form['makefile'] = array(
+    '#title' => $makefile_title,
+    '#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'], $form_state['storage']['escaped_output'], $form_state['storage']['errors'], $form_state['storage']['return']);
+
+  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, $errors, $return) = project_verify_package_run_drush_via_pipe($command, $makefile);
+
+    list($raw_output, $escaped_output) = project_verify_package_clean_output($output);
+    list($raw_errors, $escaped_errors) = project_verify_package_clean_output($errors);
+
+    // Store the command output, errors, return value and the original makefile
+    // for display on form reload.
+    $form_state['storage']['makefile'] = $makefile;
+    $form_state['storage']['output'] = $raw_output;
+    $form_state['storage']['escaped_output'] = $escaped_output;
+    $form_state['storage']['errors'] = $escaped_errors;
+    $form_state['storage']['return'] = $return;
+
+
+    // 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);
+  }
+
+}
+
+/**
+ * Runs a drush command via pipes, so that nothing touches the I/O subsystem.
+ *
+ * @param $command
+ *   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, the second element is
+ *   any error output, and the third element is the command return value.
+ */
+function project_verify_package_run_drush_via_pipe($command, $input) {
+
+  $descriptorspec = array(
+     0 => array("pipe", "r"),  // STDIN
+     1 => array("pipe", "w"),  // STDOUT
+     2 => array("pipe", "w"),  // STDERR
+  );
+
+  // drush expects a terminal, so give it one.
+  $env = array('TERM' => 'vt100');
+
+  $process = proc_open(PROJECT_VERIFY_PACKAGE_DRUSH_BIN . ' --include=' . PROJECT_VERIFY_PACKAGE_DRUSH_MAKE_PATH . " $command -", $descriptorspec, $pipes, NULL, $env);
+
+  if (is_resource($process)) {
+      // $pipes now looks like this:
+      // 0 => writeable handle connected to child STDIN.
+      // 1 => readable handle connected to child STDOUT.
+      // 2 => readable handle connected to child STDERR.
+      fwrite($pipes[0], $input);
+      fclose($pipes[0]);
+
+      $output = stream_get_contents($pipes[1]);
+      fclose($pipes[1]);
+
+      $errors = stream_get_contents($pipes[2]);
+      fclose($pipes[2]);
+
+      // It is important that you close any pipes before calling
+      // proc_close in order to avoid a deadlock.
+      $return_value = proc_close($process);
+  }
+  else {
+    $output = '';
+    $errors = t('Unable to open drush process.');
+    $return_value = 1;
+  }
+
+  return array($output, $errors, $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
+ *   A string of output data.
+ * @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_clean_output($output) {
+  $escaped_output_array = array();
+  $raw_output_array = array();
+  $lines = explode("\n", $output);
+  foreach ($lines 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);
+}
+
+/**
+ * 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,
+      );
+      // 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, $errors, $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_clean_output($output);
+          $token_args['%project_title'] = $project->title;
+          $token_args['!output'] = nl2br($escaped_output);
+          if (preg_match(PROJECT_VERIFY_PACKAGE_RELEASE_TAG_REGEX, $cvs_tag)) {
+            $message = project_verify_package_cvs_error_messages('tag', $token_args);
+          }
+          else {
+            $message = project_verify_package_cvs_error_messages('branch', $token_args);
+          }
+          form_set_error('title', $message);
+        }
+      }
+      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)));
+      }
+    }
+  }
+}
+
+/**
+ * Formats an error message for failed packaging validation for a release node.
+ *
+ * @param $cvs_tag_type
+ *   The type of CVS tag to generate the error message for. Should be 'branch'
+ *   or 'tag'.
+ * @param $t_args
+ *   An associative array of string substitutions for the error message.
+ *   Key/value pairs should be as follows:
+ *
+ *     %cvs_tag => The CVS tag associated with the release.
+ *     %project_title => The full name of the project associated with the
+ *                       release node.
+ *     !output => Escaped output from the drush call.
+ *
+ * @return
+ *   The formatted error message.
+ */
+function project_verify_package_cvs_error_messages($cvs_tag_type, $t_args) {
+  $t_args['%makefile'] = PROJECT_VERIFY_PACKAGE_MAKE_FILE;
+  $t_args['!doc_link'] = DOCUMENTATION_LINK;
+
+  switch ($cvs_tag_type) {
+    case 'branch':
+      return t("<p>The %makefile file for project %project_title failed verification for CVS branch %cvs_tag.</p><p><a href=\"!doc_link\">!doc_link</a> -- to learn more about correcting these errors.</p><p>!output</p><p>Once these errors are fixed, commit them to the branch, then resubmit the release.</p>", $t_args);
+    case 'tag':
+      return t("<p>The %makefile file for project %project_title failed verification for CVS tag %cvs_tag.</p><p><a href=\"!doc_link\">!doc_link</a> -- to learn more about correcting these errors.</p><p>!output</p><p>Once these errors are fixed, commit the changes to your %makefile, move the release tag for your project, and submit the release node again.</p>", $t_args);
+  }
+}
