[ Index ]

PHP Cross Reference of Drupal 6 (gatewave)

title

Body

[close]

/sites/all/modules/project/release/ -> project_release.module (source)

   1  <?php
   2  // $Id: project_release.module,v 1.152 2010/01/30 02:43:24 dww Exp $
   3  
   4  define('PROJECT_RELEASE_DEFAULT_VERSION_FORMAT', '!major%minor%patch#extra');
   5  define('PROJECT_RELEASE_FILE_EXTENSIONS', 'zip gz tar bz2 rar tgz tar.gz dmg rpm deb');
   6  
   7  /**
   8   * Constants for the possible values of {project_release_nodes}.update_status.
   9   */
  10  define('PROJECT_RELEASE_UPDATE_STATUS_CURRENT', 0);
  11  define('PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT', 1);
  12  define('PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE', 2);
  13  
  14  /**
  15   * @defgroup project_release_core Core Drupal hooks
  16   */
  17  
  18  /**
  19   * Implementation of hook_init().
  20   */
  21  function project_release_init() {
  22    drupal_add_css(drupal_get_path('module', 'project_release') .'/project_release.css');
  23    project_release_get_api_taxonomy();
  24  
  25    // These constants are defined here since they use t() and the
  26    // global $locale variable needs to be initialized before calling
  27    // t() or you suffer a big performance hit.
  28    define('PROJECT_RELEASE_VERSION_FORMAT_VALID_MSG', t('The version format string can only contain letters, numbers, and the characters . _ and - (in addition to the special characters used for identifying variables: % ! and #).'));
  29    define('PROJECT_RELEASE_VERSION_FORMAT_HELP', t("Available variables are: %api, %major, %minor, %patch, %extra. The percent sign ('%') at the front of the variable name indicates that a period ('.') should be inserted as a delimiter before the value of the variable. The '%' can be replaced with a hash mark ('#') to use a hyphen ('-') delimiter, or with an exclaimation point ('!') to have the value printed without a delimiter. Any variable in the format string that has no value will be removed entirely from the final string.") .' '. PROJECT_RELEASE_VERSION_FORMAT_VALID_MSG);
  30  }
  31  
  32  /**
  33   * Implementation of hook_menu()
  34   * @ingroup project_release_core
  35   */
  36  function project_release_menu() {
  37    $items = array();
  38  
  39    $items['node/%project_node/edit/releases'] = array(
  40      'title' => 'Releases',
  41      'page callback' => 'project_release_project_edit_releases',
  42      'page arguments' => array(1),
  43      'access callback' => 'node_access',
  44      'access arguments' => array('update', 1),
  45      'type' => MENU_LOCAL_TASK,
  46      'file' => 'includes/project_edit_releases.inc',
  47    );
  48  
  49    $items['node/add/project-release/%'] = array(
  50      'page callback' => 'node_add',
  51      'page arguments' => array('project-release'),
  52      'access callback' => 'node_access',
  53      'access arguments' => array('create', 'project_release'),
  54      'file' => 'node.pages.inc',
  55      'file path' => drupal_get_path('module', 'node'),
  56      'type' => MENU_CALLBACK,
  57    );
  58  
  59    $items['admin/project/project-release-settings'] = array(
  60      'description' => 'Configure the default version string for releases and other settings for the Project release module.',
  61      'title' => 'Project release settings',
  62      'page callback' => 'drupal_get_form',
  63      'page arguments' => array('project_release_settings_form'),
  64      'access arguments' => array('administer projects'),
  65      'weight' => 1,
  66      'type' => MENU_NORMAL_ITEM,
  67      'file' => 'includes/admin.settings.inc',
  68    );
  69  
  70    // Redirect node/add/project_release/* to node/add/project-release.
  71    $items['node/add/project_release'] = array(
  72      'page callback' => 'project_release_add_redirect_page',
  73      'access callback' => 'node_access',
  74      'access arguments' => array('create', 'project_release'),
  75      'type' => MENU_CALLBACK,
  76      'file' => 'includes/release_node_form.inc',
  77    );
  78  
  79    return $items;
  80  }
  81  
  82  /**
  83   * Implementation of hook_menu_alter().
  84   */
  85  function project_release_menu_alter(&$callbacks) {
  86    $callbacks['node/add/project-release']['page callback'] = 'drupal_get_form';
  87    $callbacks['node/add/project-release']['page arguments'] = array('project_release_pick_project_form');
  88    $callbacks['node/add/project-release']['file'] = 'release_node_form.inc';
  89    $callbacks['node/add/project-release']['file path'] = drupal_get_path('module', 'project_release') . '/includes';
  90  }
  91  
  92  /**
  93   * @defgroup project_release_node Drupal node-type related hooks
  94   */
  95  
  96  /**
  97   * Implementation of hook_access().
  98   * @ingroup project_release_node
  99   *
 100   * TODO: Maybe we should add new permissions for accessing release
 101   * nodes, but for now, we're just using the existing project perms.
 102   */
 103  function project_release_access($op, $node, $account) {
 104    switch ($op) {
 105      case 'view':
 106        // We want to use the identical logic for viewing projects,
 107        // so we call that method directly.
 108        return project_project_access($op, $node, $account);
 109      case 'create':
 110        // Due to how node_menu() works, we have to allow anyone with
 111        // permission to maintain a project to be able to create a
 112        // release node, or else you can have a faulty entry added to
 113        // the {cache_menu} table that thinks you're not allowed to
 114        // create *any* releases. So, we are more relaxed here, and
 115        // enforce more closely in project_release_form(). As with the
 116        // 'view' case above, we want the identical logic as project
 117        // nodes, so we call that hook, instead of duplicating code.
 118        return project_project_access($op, $node, $account);
 119      case 'update':
 120        // We can't just use project_project_access() here, since we
 121        // need to check access to the project itself, not the release
 122        // node, so we use the helper method and pass the project id.
 123        return project_check_admin_access($node->project_release['pid']);
 124      case 'delete':
 125        // No one should ever delete a release node, only unpublish it.
 126        return FALSE;
 127    }
 128  }
 129  
 130  /**
 131   * Implementation of hook_node_info().
 132   * @ingroup project_release_node
 133   */
 134  function project_release_node_info() {
 135    return array(
 136      'project_release' => array(
 137        'name' => t('Project release'),
 138        'module' => 'project_release',
 139        'description' => t('A release of a project with a specific version number.'),
 140       ),
 141    );
 142  }
 143  
 144  /**
 145   * Implement of hook_form() for project_release nodes.
 146   */
 147  function project_release_form(&$release, &$form_state) {
 148    module_load_include('inc', 'project_release', 'includes/release_node_form');
 149    return _project_release_form($release, $form_state);
 150  }
 151  
 152  /**
 153   * Validation callback for project release node forms.
 154   *
 155   * @see _project_release_node_form_validate()
 156   */
 157  function project_release_node_form_validate(&$form, &$form_state) {
 158    module_load_include('inc', 'project_release', 'includes/release_node_form');
 159    return _project_release_node_form_validate($form, $form_state);
 160  }
 161  
 162  
 163  /**
 164   * Implementation of hook_load().
 165   * @ingroup project_release_node
 166   */
 167  function project_release_load($node) {
 168    $additions = db_fetch_array(db_query("SELECT * FROM {project_release_nodes} WHERE nid = %d", $node->nid));
 169    // Add in file info.
 170    $file_info = db_query("SELECT f.*, prf.filehash FROM {project_release_file} prf INNER JOIN {files} f ON prf.fid = f.fid WHERE prf.nid = %d", $node->nid);
 171    $files = array();
 172    while ($file = db_fetch_object($file_info)) {
 173      $files[$file->fid] = $file;
 174    }
 175    $additions['files'] = $files;
 176  
 177    $release = new stdClass;
 178    $release->project_release = $additions;
 179    return $release;
 180  }
 181  
 182  /**
 183   * Implementation of hook_insert().
 184   *
 185   * @param $node
 186   *   Object containing form values from the project_release node form.  Even
 187   *   though this is NOT a fully loaded $node object, the release-related
 188   *   values are in the $node->project_release array due to manual #tree and
 189   *   #parents hacking in project_release_form().
 190   *
 191   * @ingroup project_release_node
 192   */
 193  function project_release_insert($node) {
 194    module_load_include('inc', 'project_release', 'includes/release_node_form');
 195    project_release_db_save($node, true);
 196  }
 197  
 198  /**
 199   * Implementation of hook_update().
 200   *
 201   * @param $node
 202   *   Object containing form values from the project_release node form.  Even
 203   *   though this is NOT a fully loaded $node object, the release-related
 204   *   values are in the $node->project_release array due to manual #tree and
 205   *   #parents hacking in project_release_form().
 206   *
 207   * @ingroup project_release_node
 208   */
 209  function project_release_update($node) {
 210    module_load_include('inc', 'project_release', 'includes/release_node_form');
 211    project_release_db_save($node, false);
 212  }
 213  
 214  /**
 215   * Verifies the data for supported release versions, and updates if necessary.
 216   *
 217   * @param $pid
 218   *   The project ID.
 219   * @param $tid
 220   *   The API compatibility term ID.
 221   * @param $major
 222   *   The major version of the new/modified/deleted release.
 223   * @param $delete
 224   *   Boolean to indicate if we're deleting a release of this major or not.
 225   *
 226   * @return
 227   *   TRUE if we updated a record in {project_release_supported_versions},
 228   *   otherwise FALSE (e.g. if there were no published releases on the
 229   *   requested branch).
 230   */
 231  function project_release_check_supported_versions($pid, $tid, $major, $delete) {
 232    // Remember if we updated {project_release_supported_versions} so we can
 233    // return the value to our caller.
 234    $did_update = FALSE;
 235  
 236    // If we're being called as a release node is being edited and saved, and
 237    // the site we're running on is using DB replication, we need to make sure
 238    // we're talking to the primary DB so that all of this works.
 239    if (function_exists('db_set_ignore_slave')) {
 240      db_set_ignore_slave();
 241    }
 242  
 243    // Regardless of if we're deleting, adding, or editing, we need to know the
 244    // latest and recommended releases (if any) from the given branch. If
 245    // there's no published release, these values will be 0.
 246    list($latest_release, $recommended_release, $latest_security_release) = project_release_find_latest_releases($pid, $tid, $major);
 247  
 248    if ($delete) {
 249      // Make sure this isn't the last release node for the given major.
 250      if (!empty($latest_release)) {
 251        // Since the node we just deleted might have been the latest or
 252        // recommended on the branch, update our record with the real values.
 253        db_query("UPDATE {project_release_supported_versions} SET recommended_release = %d, latest_release = %d, latest_security_release = %d WHERE nid = %d AND tid = %d AND major = %d", $recommended_release, $latest_release, $latest_security_release, $pid, $tid, $major);
 254        $did_update = TRUE;
 255      }
 256      else {
 257        // No latest release -- remove the bogus record for this branch.
 258        db_query("DELETE FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d AND major = %d", $pid, $tid, $major);
 259  
 260        $num_recommended = db_result(db_query("SELECT COUNT(*) FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d AND supported = %d AND recommended = %d", $pid, $tid, 1, 1));
 261        if ($num_recommended > 1) {
 262          // Something seriously bogus, clear out the values and start over.
 263          db_query("UPDATE {project_release_supported_versions} SET recommended = %d WHERE nid = %d AND tid = %d", 0, $pid, $tid);
 264          $num_recommended = 0;
 265        }
 266      }
 267    }
 268    else {
 269      // Adding or editing a release.
 270      if (!empty($latest_release)) {
 271        // We have at least 1 published release, so make sure we have an entry
 272        // for this major version in {project_release_supported_versions}.
 273        $current_branches = db_query("SELECT major FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d", $pid, $tid);
 274        $have_current_branch = FALSE;
 275        $num_branches = 0;
 276        while (($branch = db_fetch_object($current_branches)) !== FALSE) {
 277          $num_branches++;
 278          if ($branch->major == $major) {
 279            $have_current_branch = TRUE;
 280            break;
 281          }
 282        }
 283        if ($num_branches == 0 || !$have_current_branch) {
 284          // First entry for this API tid/major version pair, so add a new
 285          // record to the table as supported but not recommended.
 286          db_query("INSERT INTO {project_release_supported_versions} (nid, tid, major, supported, recommended, snapshot, recommended_release, latest_release, latest_security_release) VALUES (%d, %d, %d, %d, %d, %d, %d, %d, %d)", $pid, $tid, $major, 1, 0, 0, $recommended_release, $latest_release, $latest_security_release);
 287        }
 288        else {
 289          // We already have this branch in the table, but the latest_release
 290          // and recommended_release fields might be stale based on whatever
 291          // node was just added or edited.
 292          db_query("UPDATE {project_release_supported_versions} SET recommended_release = %d, latest_release = %d, latest_security_release = %d WHERE nid = %d AND tid = %d AND major = %d", $recommended_release, $latest_release, $latest_security_release, $pid, $tid, $major);
 293        }
 294        $did_update = TRUE;
 295      }
 296    }
 297  
 298    // Regardless of insert/edit/delete, we want to go through and recompute
 299    // {project_release_nodes}.update_status for all records on this branch.
 300    // Note: we end up doing the same query in here that we performed in
 301    // project_release_find_latest_releases(), we just need to process the
 302    // results differently. However, to keep the code sane, we invoke the query
 303    // again. If this becomes a performance problem, we can always refactor.
 304    project_release_compute_update_status($pid, $tid, $major);
 305  
 306    // Either way, clear the cache for the release table, since what we want to
 307    // display might have changed, too.
 308    $cid = 'table:'. $pid .':';
 309    cache_clear_all($cid, 'cache_project_release', TRUE);
 310  
 311    return $did_update;
 312  }
 313  
 314  /**
 315   * Compute the {project_release_nodes}.update_status values for a given branch.
 316   *
 317   * For any given release node, there are three possible status values for if
 318   * if the release needs an update or not:
 319   * - 'current' (PROJECT_RELEASE_UPDATE_STATUS_CURRENT): It's the currently
 320   *   recommended release (without extra), or the latest possible release
 321   *   (including betas, rcs, etc). There is no need to upgrade this release at
 322   *   this time, it's the most up-to-date available.
 323   * - 'not-current' (PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT): Any release
 324   *   older than the recommended release, or any older release with extra from
 325   *   the same major/minor/patch as the latest release.
 326   * - 'not-secure' (PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE): Any release
 327   *   older than the latest security update on this branch is considered not
 328   *   secure. Releases are only marked 'not-secure' on sites that define the
 329   *   'project_release_security_update_tid' variable.
 330   *
 331   *  For example, if 1.2.2 is the recommended release, 1.2.1 was a security
 332   *  update, and 1.2.2-beta2 is the latest release, here would be the following
 333   *  update status values for various releases:
 334   *  - 1.2.2-beta2: 'current'     (since it's the latest release)
 335   *  - 1.2.2-beta1: 'not-current' (since beta2 is available)
 336   *  - 1.2.2: 'current'           (recommended release, latest without "extra")
 337   *  - 1.2.2-rc1: 'not-current'   (since 1.2.2 official is out)
 338   *  - 1.2.1: 'not-current'
 339   *  - 1.2.1-beta1: 'not-secure'  (since 1.2.1 official was a security update)
 340   *  - 1.2.0: 'not-secure'
 341   *
 342   * This status is recorded in the {project_release_nodes}.update_status column
 343   * in the database. Whenever a release is created, updated, or deleted, we
 344   * need to inspect all the other releases on the same branch to potentially
 345   * modify the update_status column as needed.
 346   *
 347   * This function walks through all the records in the {project_release_nodes}
 348   * table matching the given branch (API compatibility term ID and major
 349   * version) for a specified project in version order (as determined by
 350   * project_release_query_releases_by_branch() which sorts by version_minor,
 351   * version_patch, version_extra_weight and finally version_extra), and
 352   * compares them with that branch's latest release, recommended release, and
 353   * latest security release to compute their update status. If the release is
 354   * the latest or recommended, it's 'current'. Otherwise, it's 'not-current'
 355   * if we haven't passed a security update yet, or 'not-secure' once we find a
 356   * security update.
 357   *
 358   * @param $pid
 359   *   The project ID.
 360   * @param $api_tid
 361   *   The API compatibility term ID.
 362   * @param $major
 363   *   The major version of the new/modified/deleted release.
 364   *
 365   * @return
 366   *   Void. This function directly updates the {project_release_nodes} table
 367   *   with the appropriate values.
 368   *
 369   * @see project_release_check_supported_versions()
 370   * @see project_release_query_releases_by_branch()
 371   * @see project_release_release_nodeapi()
 372   */
 373  function project_release_compute_update_status($pid, $api_tid, $major) {
 374    $latest_release = $recommended_release = $latest_security_release = 0;  
 375    $nid_update_map = array();
 376    $query = project_release_query_releases_by_branch($pid, $api_tid, $major);
 377    while ($release = db_fetch_object($query)) {
 378      // Clear out the status so we always start fresh with each release.
 379      unset($update_status);
 380      if (empty($latest_release)) {
 381        $latest_release = $release->nid;
 382        // If this is the latest release, it's current.
 383        $update_status = PROJECT_RELEASE_UPDATE_STATUS_CURRENT;
 384      }
 385      if (empty($recommended_release) && empty($release->version_extra)) {
 386        $recommended_release = $release->nid;
 387        // If this is the recommended release, it's current.
 388        $update_status = PROJECT_RELEASE_UPDATE_STATUS_CURRENT;
 389      }
 390      if (empty($latest_security_release) && !empty($release->security_update)) {
 391        $latest_security_release = $release->nid;
 392      }
 393  
 394      // Based on what we've already seen, figure out the status. The only
 395      // possible releases that can be "CURRENT" are the latest and recommended
 396      // releases, and we already set the status for those. So, if we're here,
 397      // we know it's not current, we just need to know if it's also not secure.
 398      if (!isset($update_status)) {
 399        // If we haven't found a security release yet, or the release we're on
 400        // is the latest security update, this is just 'not_current'.
 401        if (empty($latest_security_release) || $latest_security_release == $release->nid) {
 402          $update_status = PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT;
 403        }
 404        // Otherwise, we're past the latest security release, this is insecure.
 405        else {
 406          $update_status = PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE;
 407        }
 408      }
 409  
 410      // If the status is different than what we have in the DB, remember that
 411      // we need to update this nid in the DB.
 412      if ($update_status != $release->update_status) {
 413        $nid_update_map[$update_status][] = $release->nid;
 414      }
 415    }
 416  
 417    if (!empty($nid_update_map)) {
 418      foreach ($nid_update_map as $update_status => $nids) {
 419        if (!empty($nids)) {
 420          $placeholders = db_placeholders($nids);
 421          db_query("UPDATE {project_release_nodes} SET update_status = %d WHERE nid IN ($placeholders)", array_merge(array($update_status), $nids));
 422          if ($update_status == PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE && module_exists('project_package')) {
 423            project_package_check_update_status($nids);
 424          }
 425        }
 426      }
 427    }
 428  }
 429  
 430  /**
 431   * Implementation of hook_delete().
 432   * @ingroup project_release_node
 433   */
 434  function project_release_delete($node) {
 435    if (!empty($node->project_release['files'])) {
 436      foreach ($node->project_release['files'] as $fid => $file) {
 437        project_release_file_delete($file);
 438      }
 439    }
 440    db_query("DELETE FROM {project_release_package_errors} WHERE nid = %d", $node->nid);
 441    db_query("DELETE FROM {project_release_nodes} WHERE nid = %d", $node->nid);
 442  }
 443  
 444  /**
 445   * Deletes release files.
 446   *
 447   * @param $file
 448   *   The file object to delete.
 449   */
 450  function project_release_file_delete($file) {
 451    db_query("DELETE FROM {files} WHERE fid = %d", $file->fid);
 452    db_query("DELETE FROM {project_release_file} WHERE fid = %d", $file->fid);
 453    file_delete(file_create_path($file->filepath));
 454  }
 455  
 456  /**
 457   * @defgroup project_release_api Project release functions that other
 458   * modules might want to use
 459   */
 460  
 461  /**
 462   * Returns the version format string for a given project
 463   * @ingroup project_release_api
 464   */
 465  function project_release_get_version_format($project) {
 466    if (!empty($project->project_release['version_format'])) {
 467      return $project->project_release['version_format'];
 468    }
 469  
 470    $db_format = db_result(db_query("SELECT version_format FROM {project_release_projects} WHERE nid = %d", $project->nid));
 471    if (!empty($db_format)) {
 472      return $db_format;
 473    }
 474  
 475    return variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT);
 476  }
 477  
 478  /**
 479   * Validates a version format string. Only alphanumeric characters and
 480   * [-_.] are allowed. Calls form_set_error() on error, else returns.
 481   * @param $form_values Array of form values passed to validate hook.
 482   * @param $element The name of the form element for the format string.
 483   * @ingroup project_release_internal
 484   */
 485  function _project_release_validate_format_string($form_values, $element) {
 486    if (!preg_match('/^[a-zA-Z0-9_\-.!%#]+$/', $form_values[$element])) {
 487      form_set_error($element, PROJECT_RELEASE_VERSION_FORMAT_VALID_MSG);
 488    }
 489  }
 490  
 491  /**
 492   * Returns the formatted version string for a given version object.
 493   *
 494   * @param $version
 495   *   Object containing the separate version-related fields, such as
 496   *   version_major, version_minor, etc.
 497   * @param $project
 498   *   Optional project node that the version corresponds with.  If not defined,
 499   *   the version object should at least include a "pid" field.
 500   *
 501   * @return
 502   *   The formatted version string for the given version and project info.
 503   *
 504   * @ingroup project_release_api
 505   */
 506  function project_release_get_version($version, $project = NULL) {
 507    if (isset($project)) {
 508      $node = $project;
 509    }
 510    else {
 511      $node->nid = $version->pid;
 512    }
 513    $variables = array();
 514    foreach (array('major', 'minor', 'patch', 'extra') as $field) {
 515      $var = "version_$field";
 516      if (isset($version->$var) && $version->$var !== '') {
 517        $variables["!$field"] = $version->$var;
 518        $variables["%$field"] = '.'. $version->$var;
 519        $variables["#$field"] = '-'. $version->$var;
 520      }
 521      else {
 522        $variables["!$field"] = '';
 523        $variables["%$field"] = '';
 524        $variables["#$field"] = '';
 525      }
 526    }
 527    $vid = _project_release_get_api_vid();
 528    if (project_release_get_api_taxonomy() && isset($version->version_api_tid)) {
 529      $term = taxonomy_get_term($version->version_api_tid);
 530      $variables["!api"] = $term->name;
 531      $variables["%api"] = '.'. $term->name;
 532      $variables["#api"] = '-'. $term->name;
 533    }
 534    else {
 535      $variables["!api"] = '';
 536      $variables["%api"] = '';
 537      $variables["#api"] = '';
 538    }
 539    $version_format = project_release_get_version_format($node);
 540    return strtr($version_format, $variables);
 541  }
 542  
 543  /**
 544   * Implementation of hook_view().
 545   * @ingroup project_release_node
 546   */
 547  function project_release_view($node, $teaser = FALSE, $page = FALSE) {
 548    $node = node_prepare($node, $teaser);
 549    $project = node_load($node->project_release['pid']);
 550  
 551    if ($page) {
 552      // Breadcrumb navigation
 553      $breadcrumb[] = l($project->title, 'node/'. $project->nid);
 554      $breadcrumb[] = l(t('Releases'), 'node/'. $project->nid .'/release');
 555      project_project_set_breadcrumb($project, $breadcrumb);
 556    }
 557  
 558    $output = '';
 559    $max_file_timestamp = 0;
 560    if (!empty($node->project_release['files'])) {
 561      $view_info = variable_get('project_release_files_view', 'project_release_files:default');
 562      list($view_name, $display_name) = split(':', $view_info);
 563      $output .= views_embed_view($view_name, $display_name, $node->nid);
 564      foreach ($node->project_release['files'] as $file) {
 565        $max_file_timestamp = max($max_file_timestamp, $file->timestamp);
 566      }
 567      $node->content['release_file_info'] = array(
 568        '#value' => '<div class="project-release-files">'. $output .'</div>',
 569        '#weight' => -4,
 570      );
 571    }
 572  
 573    $output = '';
 574    if (project_use_cvs($project) && isset($node->project_release['tag'])) {
 575      if (!empty($node->project_release['rebuild'])) {
 576        $output .= t('Nightly development snapshot from CVS branch: @tag', array('@tag' => $node->project_release['tag'])) .'<br />';
 577      }
 578      else {
 579        $output .= t('Official release from CVS tag: @tag', array('@tag' => $node->project_release['tag'])) .'<br />';
 580      }
 581    }
 582  
 583    if (!empty($max_file_timestamp)) {
 584      $output .= '<div class="last-updated">' . t('Last updated: !changed', array('!changed' => format_date($max_file_timestamp))) . '</div>';
 585    }
 586  
 587    if (module_exists('project_usage') && user_access('view project usage')) {
 588      $output .= '<div class="usage-statistics-link">'. l(t('View usage statistics for this release'), 'project/usage/'. $node->nid) .'</div>';
 589    }
 590    $node->content['release_info'] = array(
 591      '#value' => '<div class="project-release-info">'. $output .'</div>',
 592      '#weight' => -3,
 593    );
 594  
 595    if (module_exists('project_package')) {
 596      $output = '';
 597      if (!empty($node->project_package['count'])) {
 598        $view_info = variable_get('project_package_release_items_view', 'project_package_items:default');
 599        list($view_name, $display_name) = split(':', $view_info);
 600        $output .= '<h3>' . t('In this package') . '</h3>';
 601        $output .= views_embed_view($view_name, $display_name, $node->nid);
 602      }
 603      if (!empty($output)) {
 604        $node->content['release_package_items'] = array(
 605          '#value' => '<div class="project-release-package-items">'. $output .'</div>',
 606          '#weight' => -2,
 607        );
 608      }
 609    }
 610  
 611    // Display packaging errors to admins.
 612    if (project_check_admin_access($node->project_release['pid'])) {
 613      $rows = array();
 614      $result = db_query('SELECT * FROM {project_release_package_errors} WHERE nid = %d', $node->nid);
 615      $error = db_fetch_object($result);
 616      if (!empty($error)) {
 617        $rows = unserialize($error->messages);
 618        if (!empty($rows)) {
 619          $node->content['release_errors'] = array(
 620            '#value' => theme('item_list', $rows, t('Packaging error messages')),
 621            '#weight' => -1,
 622            '#prefix' => '<div class="messages error">',
 623            '#suffix' => '</div>',
 624          );
 625        }
 626      }
 627    }
 628  
 629    return $node;
 630  }
 631  
 632  function project_release_load_file($fid) {
 633    return db_fetch_object(db_query("SELECT f.*, prf.filehash FROM {project_release_file} prf INNER JOIN {files} f ON prf.fid = f.fid WHERE f.fid = %d", $fid));
 634  }
 635  
 636  function theme_project_release_download_file($file, $download_link = TRUE) {
 637    $output = '';
 638    if ($download_link) {
 639      $output .= '<small>'. t('Download: !file', array('!file' => theme('project_release_download_link', $file->filepath))) .'</small><br />';
 640    }
 641    else {
 642      $output .= '<small>'. t('File: @filepath', array('@filepath' => $file->filepath)) .'</small><br />';
 643    }
 644    $output .= '<small>'. t('Size: !size', array('!size' => format_size($file->filesize))) .'</small><br />';
 645    $output .= '<small>'. t('md5_file hash: !filehash', array('!filehash' => $file->filehash)) .'</small><br />';
 646    $output .= '<small>'. t('Last updated: !changed', array('!changed' => format_date($file->timestamp))) .'</small><br />';
 647    return $output;
 648  }
 649  
 650  /*
 651   @TODO: This function is used by project_issue, so we need to keep it here,
 652   even though we're now creating the list of releases at node/XXX/release using
 653   the views module. however, it might be nice if we could replace this function
 654   with views as well just to use views's query builder.  Maybe that's a bad
 655   idea in terms of performance, however.
 656  */
 657  /**
 658   * Get an array of release nodes
 659   * @ingroup project_release_api
 660   *
 661   * @param $project
 662   *   The project node object.
 663   * @param $nodes
 664   *   If set, an array of release nodes will be returned.
 665   *   Otherwise only the version field will be returned in the array value.
 666   * @param $sort_by
 667   *   This can be 'date' or 'version' and determines how the releases
 668   *   returned are to be sorted.
 669   * @param $filter_by
 670   *   This can be 'all' to include all releases or 'files' to return
 671   *   only releases which have a file attached.
 672   * @param $rids
 673   *   This is a special parameter that can be used to allow one or more
 674   *   releases to be returned even if the node itself is unpublished.
 675   *   This is useful when this function is called by the project_issue
 676   *   module to allow a user to keep the version of an issue unchanged
 677   *   even if the release represented by the version is now unpublished.
 678    * @return
 679   *   An array of releases.  The keys are the release node nids.  The values
 680   *   will either be release objects or release version strings, depending
 681   *   on the value of the $nodes parameter.
 682   */
 683  function project_release_get_releases($project, $nodes = TRUE, $sort_by = 'version', $filter_by = 'all', $rids = array()) {
 684    if ($sort_by == 'date') {
 685      $order_by = 'n.created';
 686    }
 687    else {
 688      $order_by = 'r.version';
 689    }
 690    $where = '';
 691    $join = '';
 692    $args = array($project->nid);
 693    if (!project_check_admin_access($project)) {
 694      if (!empty($rids)) {
 695        $where = "AND (n.status = %d OR n.nid IN (". db_placeholders($rids) ."))";
 696        $args[] = 1;
 697        foreach ($rids as $rid) {
 698          $args[] = $rid;
 699        }
 700      }
 701      else {
 702        $where = 'AND (n.status = %d)';
 703        $args[] = 1;
 704      }
 705      if ($filter_by == 'files') {
 706        $join .= "INNER JOIN {project_release_file} prf ON n.nid = prf.nid";
 707      }
 708    }
 709  
 710    $result = db_query(db_rewrite_sql("SELECT n.nid, r.* FROM {node} n INNER JOIN {project_release_nodes} r $join ON r.nid = n.nid WHERE (r.pid = %d) $where ORDER BY $order_by DESC"), $args);
 711    $releases = array();
 712    while ($obj = db_fetch_object($result)) {
 713      if ($nodes) {
 714        $releases[$obj->nid] = node_load($obj->nid);
 715      }
 716      else {
 717        $releases[$obj->nid] = $obj->version;
 718      }
 719    }
 720    return $releases;
 721  }
 722  
 723  
 724  /**
 725   * @defgroup project_release_callback Menu callback functions
 726   */
 727  
 728  /**
 729   * Returns a listing of all active project release compatibility terms
 730   * in the system.
 731   * @ingroup project_release_api
 732   */
 733  function project_release_compatibility_list() {
 734    static $terms = array();
 735    if (empty($terms) && $tree = project_release_get_api_taxonomy()) {
 736      $tids = variable_get('project_release_active_compatibility_tids', array());
 737      foreach ($tree as $term) {
 738        if (($tids && !empty($tids[$term->tid])) || !$tids) {
 739          $terms[$term->tid] = $term->name;
 740        }
 741      }
 742    }
 743    return $terms;
 744  }
 745  
 746  /**
 747   * @defgroup project_release_fapi Form API hooks
 748   */
 749  
 750  /**
 751   * Implementation of hook_form_alter().
 752   * @ingroup project_release_fapi
 753   */
 754  function project_release_form_alter(&$form, &$form_state, $form_id) {
 755    if ($form_id == 'project_project_node_form') {
 756      return project_release_alter_project_form($form, $form_state);
 757    }
 758    if ($form_id == 'project_release_node_form') {
 759      return project_release_alter_release_form($form, $form_state);
 760    }
 761  }
 762  
 763  /**
 764   * Alters the project_project node form to add release settings.
 765   * @ingroup project_release_fapi
 766   * @see project_release_form_alter
 767   */
 768  function project_release_alter_project_form(&$form) {
 769    if (!empty($form['project_node']['project']['uri']['#description'])) {
 770      $form['project_node']['project']['uri']['#description'] .= ' ' . t('This string is also used to generate the name of releases associated with this project.');
 771    }
 772    else {
 773      $form['project_node']['project']['uri']['#description'] = t('This string is used to generate the name of releases associated with this project.');
 774    }
 775  }
 776  
 777  /**
 778   * Alters the project_release node form to handle the API taxonomy.
 779   * If the vocabulary is empty, this removes the form elements.
 780   * @ingroup project_release_fapi
 781   * @see project_release_form_alter
 782   */
 783  function project_release_alter_release_form(&$form, &$form_state) {
 784    global $user;
 785    $node = $form['#node'];
 786    $tid = '';
 787    if (!empty($node->project_release['version_api_tid'])) {
 788      $tid = $node->project_release['version_api_tid'];
 789    }
 790    $vid = _project_release_get_api_vid();
 791    if (!project_release_get_api_taxonomy() && isset($form['taxonomy'][$vid])) {
 792      unset($form['taxonomy'][$vid]);
 793    }
 794    else {
 795      if (!user_access('administer projects')) {
 796        // The user doesn't have 'administer projects' permission, so
 797        // we restrict their options for the compatibility taxonomy.
 798        if (!empty($tid)) {
 799          // If we already have the term, we want to force it to stay.
 800          $indexes = form_get_options($form['taxonomy'][$vid], $tid);
 801          if ($indexes !== FALSE) {
 802            foreach ($indexes as $index) {
 803              $options[] = $form['taxonomy'][$vid]['#options'][$index];
 804            }
 805          }
 806          $form['taxonomy'][$vid]['#default_value'] = $tid;
 807        }
 808        elseif ($tids = variable_get('project_release_active_compatibility_tids', array())) {
 809          // We don't have the term since we're adding a new release.
 810          // Restrict to the active terms (if any).
 811          foreach (array_filter($tids) as $tid) {
 812            $indexes = form_get_options($form['taxonomy'][$vid], $tid);
 813  
 814            if ($indexes !== FALSE) {
 815              foreach ($indexes as $index) {
 816                $options[$index] = $form['taxonomy'][$vid]['#options'][$index];
 817              }
 818            }
 819          }
 820        }
 821        if (!empty($options)) {
 822          $form['taxonomy'][$vid]['#options'] = $options;
 823        }
 824        else {
 825          unset($form['taxonomy'][$vid]);
 826        }
 827        // If they're not project admins, remove the delete button (if any).
 828        unset($form['delete']);
 829      }
 830    }
 831    // If there are no children elements, we should unset the entire
 832    // thing so we don't end up with an empty fieldset.
 833    if (isset($form['taxonomy']) && !element_children($form['taxonomy'])) {
 834      unset($form['taxonomy']);
 835    }
 836  
 837    $form['buttons']['submit']['#submit'][] = 'project_release_node_submit';
 838  }
 839  
 840  
 841  /**
 842   * @defgroup project_release_nodeapi Node API hooks
 843   */
 844  
 845  /**
 846   * hook_nodeapi() implementation. This just decides what type of node
 847   * is being passed, and calls the appropriate type-specific hook.
 848   * @ingroup project_release_nodeapi
 849   * @see project_release_project_nodeapi().
 850   */
 851  function project_release_nodeapi(&$node, $op, $arg) {
 852    switch ($node->type) {
 853      case 'project_project':
 854        project_release_project_nodeapi($node, $op, $arg);
 855        break;
 856      case 'project_release':
 857        project_release_release_nodeapi($node, $op, $arg);
 858        break;
 859    }
 860  }
 861  
 862  /**
 863   * hook_nodeapi implementation specific to "project_project" nodes
 864   * (from the project.module)
 865   * @ingroup project_release_nodeapi
 866   * @see project_release_nodeapi().
 867   */
 868  function project_release_project_nodeapi(&$node, $op, $arg) {
 869    switch ($op) {
 870      case 'load':
 871        project_release_project_nodeapi_load($node);
 872        break;
 873  
 874      case 'insert':
 875        project_release_project_nodeapi_insert($node);
 876        break;
 877  
 878      case 'delete':
 879        project_release_project_nodeapi_delete($node);
 880    }
 881  }
 882  
 883  /**
 884   * Loads project_release fields into the project node object.
 885   */
 886  function project_release_project_nodeapi_load(&$node) {
 887    $project = db_fetch_object(db_query('SELECT * FROM {project_release_projects} WHERE nid = %d', $node->nid));
 888    if (!empty($project)) {
 889      $fields = array('releases', 'version_format');
 890      foreach ($fields as $field) {
 891        $node->project_release[$field] = $project->$field;
 892      }
 893      $wants_snapshots = db_result(db_query('SELECT tid FROM {project_release_supported_versions} WHERE nid = %d AND snapshot = %d LIMIT %d', $node->nid, 1, 1));
 894      if (isset($wants_snapshots)) {
 895        $node->project_release['project_release_show_snapshots'] = TRUE;
 896      }
 897    }
 898  }
 899  
 900  /**
 901   * Insert release information about a project node.
 902   */
 903  function project_release_project_nodeapi_insert(&$node) {
 904    db_query("INSERT INTO {project_release_projects} (nid, releases, version_format) VALUES (%d, %d, '%s')", $node->nid, 1, '');
 905  }
 906  
 907  /**
 908   * Deletes release information when a project is deleted.
 909   */
 910  function project_release_project_nodeapi_delete(&$node) {
 911    // TODO: unpublish (delete?) all release nodes associated with
 912    // this project, too.
 913    db_query('DELETE FROM {project_release_projects} WHERE nid = %d', $node->nid);
 914  
 915  }
 916  
 917  /**
 918   * hook_nodeapi implementation specific to "project_release" nodes.
 919   *
 920   * We use hook_nodeapi() for our own node type to trigger some code that has
 921   * to happen after taxonomy_nodeapi() runs.  project_release already has to be
 922   * weighted heavier than taxonomy for other things to work.
 923   *
 924   * @ingroup project_release_nodeapi
 925   * @see project_release_nodeapi().
 926   */
 927  function project_release_release_nodeapi(&$node, $op, $arg) {
 928    switch ($op) {
 929      case 'insert':
 930      case 'update':
 931      case 'delete':
 932        // Since release nodes can be unpublished, we need to make sure that the
 933        // recommended branch information is still up to date.
 934        if (module_exists('taxonomy')) {
 935          if (isset($node->project_release['version_api_tid'])) {
 936            $tid = $node->project_release['version_api_tid'];
 937          }
 938          else {
 939            $vid = _project_release_get_api_vid();
 940            if (isset($node->taxonomy[$vid])) {
 941              $tid = $node->taxonomy[$vid];
 942            }
 943          }
 944          if (isset($tid)) {
 945            project_release_check_supported_versions($node->project_release['pid'], $tid, $node->project_release['version_major'], ($op == 'delete' ? TRUE : FALSE));
 946          }
 947        }
 948        break;
 949  
 950      case 'rss item':
 951        // Prepend the table of release info whenever a release is in a feed.
 952        if (isset($node->body)) {
 953          $node->body = $node->content['release_info']['#value'] . $node->body;
 954        }
 955        if (isset($node->teaser)) {
 956          $node->teaser = $node->content['release_info']['#value'] . $node->teaser;
 957        }
 958        // If the release node has a file, include an enclosure attribute for it.
 959        if (!empty($node->project_release['files'])) {
 960          // RSS will only take the first file.
 961          $file = reset($node->project_release['files']);
 962          $file_link = theme('project_release_download_link', $file->filepath, NULL, TRUE);
 963          return array(
 964            array(
 965              'key' => 'enclosure',
 966              'attributes' => array(
 967                'url' => $file_link['href'],
 968                'length' => $file->filesize,
 969                'type' => 'application/octet-stream',
 970              )
 971            )
 972          );
 973        }
 974        break;
 975    }
 976  }
 977  
 978  /**
 979   * Fetch information about the current releases for a given project.
 980   *
 981   * This just queries the {project_release_supported_versions} table for either
 982   * the latest release or the recommended release, and retrieves data about
 983   * that release from the {node} and {project_release_nodes} tables. To
 984   * actually recompute the latest and recommended releases for a given branch,
 985   * you must use project_release_find_latest_releases().
 986   *
 987   * @param $project_nid
 988   *   The nid of the project to find the current release for.
 989   * @param $api_tid
 990   *   The API compatibility term ID you want to search.
 991   * @param $recommended_major
 992   *   An optional major version to search. If not specified, the current
 993   *   recommended branch from {project_release_supported_versions} is used.
 994   * @param $type
 995   *   String for what kind of release to get ('recommended' or 'latest').
 996   *
 997   * @return
 998   *   An object containing all the fields from {project_release_nodes}, along
 999   *   with {node}.title and {node}.created, for the appropriate release; or
1000   *   FALSE if no published releases exists that the caller can access on the
1001   *   requested branch of the desired project.
1002   */
1003  function project_release_get_current_recommended($project_nid, $api_tid, $recommended_major = NULL, $type = 'recommended') {
1004    // Compute the appropriate JOIN ON clauses based on the arguments.
1005    $prsv_joins[] = 'n.nid = ' . ($type == 'recommended' ? 'prsv.recommended_release' : 'prsv.latest_release');
1006    $prsv_joins[] = 'prsv.nid = %d';
1007    $join_params[] = $project_nid;
1008    $prsv_joins[] = 'prsv.tid = %d';
1009    $join_params[] = $api_tid;
1010    if (!isset($recommended_major)) {
1011      $prsv_joins[] = 'prsv.recommended = %d';
1012      $join_params[] = 1;
1013    }
1014    else {
1015      $prsv_joins[] = 'prsv.major = %d';
1016      $join_params[] = $recommended_major;
1017    }
1018    // Build the actual JOIN ON string by AND'ing all the clauses together.
1019    $prsv_join = implode(' AND ', $prsv_joins);
1020    $result = db_query(db_rewrite_sql(
1021      "SELECT n.nid, n.title, n.created, r.* FROM {node} n ".
1022      "INNER JOIN {project_release_nodes} r ON r.nid = n.nid ".
1023      "INNER JOIN {project_release_supported_versions} prsv ON $prsv_join "),
1024      $join_params);
1025    return db_fetch_object($result);
1026  }
1027  
1028  /**
1029   * Finds the latest and recommended releases for a given project and branch.
1030   *
1031   * The "latest" release just means the published release node with the highest
1032   * version string. The "recommended" release is the published release node
1033   * with the highest version string that doesn't have a "version_extra" field
1034   * (e.g. "beta1"). If all releases on the given branch have "extra", then the
1035   * recommended release will be the same as the latest release.
1036   * 
1037   * @param $project_nid
1038   *   The node ID of the project to find the latest and recommended releases of.
1039   * @param $api_tid
1040   *   The API compatibility term ID to search.
1041   * @param $major
1042   *   The {project_release_nodes}.version_major field of the branch to search.
1043   * @param $access
1044   *   Optional boolean to indicate if node access checks should be enforced.
1045   *   Defaults to FALSE since the caller might not actually have access to all
1046   *   the releases or projects. However, this function usually has to compute
1047   *   the accurate values regardless of access, and consumers of this data are
1048   *   responsible for ensuring access.
1049   *
1050   * @return
1051   *  An array containing the node ID (nid) of the latest and recommended
1052   *  releases, and latest security update (if any) from the given branch.
1053   *
1054   * @see project_release_query_releases_by_branch()
1055   */
1056  function project_release_find_latest_releases($project_nid, $api_tid, $major, $access = FALSE) {
1057    $latest_release = $recommended_release = $latest_security_release = 0;
1058  
1059    $query = project_release_query_releases_by_branch($project_nid, $api_tid, $major, $access);
1060    while ($release = db_fetch_object($query)) {
1061      if (empty($latest_release)) {
1062        $latest_release = $release->nid;
1063      }
1064      if (empty($recommended_release) && empty($release->version_extra)) {
1065        $recommended_release = $release->nid;
1066      }
1067      if (empty($latest_security_release) && !empty($release->security_update)) {
1068        $latest_security_release = $release->nid;
1069      }
1070  
1071      // If we've found everything we're looking for, break out of the loop and
1072      // stop inspecting release from this branch. $latest_release can't
1073      // possibly be empty here, so don't bother testing for it.
1074      if (!empty($recommended_release) && !empty($latest_security_release)) {
1075        break;
1076      }
1077    }
1078  
1079    // If we found no releases without extra (e.g. a new branch that only has
1080    // betas), just call the latest release the recommended one).
1081    if (empty($recommended_release)) {
1082      $recommended_release = $latest_release;
1083    }
1084  
1085    return array(
1086      $latest_release,
1087      $recommended_release,
1088      $latest_security_release,
1089    );
1090  }
1091  
1092  /**
1093   * Build a query for releases on a given branch, ordered by version.
1094   *
1095   * @param $project_nid
1096   *   The project node ID.
1097   * @param $api_tid
1098   *   The API compatibility term ID.
1099   * @param $major
1100   *   The major version that defines the branch for the project and API term.
1101   * @param $access
1102   *   Optional boolean to indicate if node access checks should be enforced.
1103   *   Defaults to FALSE since the caller might not actually have access to all
1104   *   the releases or projects. However, this function usually has to compute
1105   *   the accurate values regardless of access, and consumers of this data are
1106   *   responsible for ensuring access.
1107   *
1108   * @return
1109   *   A database query result resource, as returned by db_query().
1110   *
1111   * @see db_query()
1112   * @see project_release_find_latest_releases()
1113   */
1114  function project_release_query_releases_by_branch($project_nid, $api_tid, $major, $access = FALSE) {
1115    $wheres = $params = $order_bys = array();
1116  
1117    $wheres[] = '(r.pid = %d)';
1118    $params[] = $project_nid;
1119  
1120    $wheres[] = '(r.version_api_tid = %d)';
1121    $params[] = $api_tid;
1122  
1123    $wheres[] = '(r.version_major = %d)';
1124    $params[] = $major;
1125  
1126    $wheres[] = '(n.status = %d)';
1127    $params[] = 1;
1128  
1129    $where = 'WHERE ' . implode(' AND ', $wheres);
1130  
1131    // We always want the dev snapshots to show up last.
1132    $order_bys[] = 'r.rebuild';
1133    // Sort by the obvious integer values along the branch (minor and patch).
1134    $order_bys[] = 'r.version_minor DESC';
1135    $order_bys[] = 'r.version_patch DESC';
1136    // To reliably sort release with version_extra, use version_extra_weight.
1137    $order_bys[] = 'r.version_extra_weight DESC';
1138    // Within releases of the same version_extra_weight (e.g. rc1 vs. rc2),
1139    // sort by version_extra_delta.
1140    $order_bys[] = 'r.version_extra_delta DESC';
1141    // Within releases of the same version_extra_weight and version_extra_delta,
1142    // sort alphabetically. This shouldn't normally happen, but just in case you
1143    // have multiple releases with the same delta (e.g. "alpha-one", "alpha-two"
1144    // etc), at least you'll get deterministic results.
1145    $order_bys[] = 'r.version_extra DESC';
1146  
1147    $order_by = 'ORDER BY '. implode(', ', $order_bys);
1148  
1149    $sql = "SELECT n.nid, n.title, n.created, r.* FROM {node} n ".
1150      "INNER JOIN {project_release_nodes} r ON r.nid = n.nid ".
1151      "$where $order_by";
1152  
1153    // Only enforce node access via db_rewrite_sql() if the caller specifically
1154    // requested that behavior.
1155    if ($access) {
1156      $sql = db_rewrite_sql($sql);
1157    }
1158  
1159    return db_query($sql, $params);
1160  }
1161  
1162  /**
1163   * Theme the appropriate release download table for a project node.
1164   */
1165  function theme_project_release_project_download_table($node) {
1166    if (empty($node->project_release['releases'])) {
1167      return;
1168    }
1169    $output = '<h3 id="downloads">'. t('Downloads') .'</h3>';
1170    $view_args = array($node->nid);
1171    $displays = array(
1172      'attachment_1' => array(
1173        'class' => 'ok',
1174        'header' => t('Recommended releases'),
1175      ),
1176      'attachment_2' => array(
1177        'class' => 'warning',
1178        'header' => t('Other releases'),
1179      ),
1180      'attachment_3' => array(
1181        'class' => 'error',
1182        'header' => t('Development releases'),
1183      ),
1184    );
1185    $number_of_tables = 0;
1186    $views_output = array();
1187    foreach ($displays as $display => $info) {
1188      $view = views_get_view('project_release_download_table');
1189      $view_output = $view->preview($display, $view_args);
1190      if (!empty($view->result)) {
1191        $views_output[$display] = $view_output;
1192        $number_of_tables++;
1193      }
1194    }
1195  
1196    if ($number_of_tables > 0) {
1197      foreach ($displays as $display => $info) {
1198        if (!empty($views_output[$display])) {
1199          $classes = 'download-table download-table-' . $info['class'];
1200          $output .= '<div class="' . $classes . '">';
1201          if ($number_of_tables > 1) {
1202            $output .= '<h4>' . $info['header'] . "</h4>\n";
1203          }
1204          $output .= $views_output[$display];
1205          $output .= "</div> <!-- .download-table -->\n";
1206        }
1207      }
1208    }
1209  
1210    return $output;
1211  }
1212  
1213  /**
1214   * Implemenation of hook_project_page_link_alter().
1215   *
1216   * Note:  This is *not* an implementation of hook_link_alter().
1217   */
1218  function project_release_project_page_link_alter(&$links, $node) {
1219    if (empty($node->project_release['releases'])) {
1220      return;
1221    }
1222    $links['project_release'] = array(
1223      // NOTE:  The 'name' element of this array is not defined here because
1224      // it's actually printed as part of the output of the
1225      // theme_project_release_project_download_table() function above.
1226      'weight' => 2,
1227      'clear' => TRUE,
1228      'links' => array(
1229        'view_all_releases' => l(t('View all releases'), 'node/'. $node->nid .'/release') . theme('project_feed_icon', url('node/'. $node->nid .'/release/feed'), t('RSS feed of all releases'))
1230      ),
1231    );
1232  
1233    if (project_check_admin_access($node->nid)) {
1234      $links['project_release']['links']['add_new_release'] = l(t('Add new release'), 'node/add/project_release/'. $node->nid);
1235      $links['project_release']['links']['administer_releases'] = l(t('Administer releases'), 'node/'. $node->nid .'/edit/releases');
1236    }
1237  }
1238  
1239  /**
1240   * Theme function that calls project_release_table().
1241   *
1242   * The main purpose of this theme wrapper function is to make it easier
1243   * to display a different kind of table (for example, $tabel_type=all)
1244   * from the project_page_overview() function in project.module.
1245   *
1246   * The parameters are described at project_release_table().
1247   *
1248   * @see project_page_overview()
1249   * @see project_release_table()
1250   */
1251  function theme_project_release_table_overview($project, $table_type, $release_type, $title, $print_size) {
1252    return project_release_table($project, $table_type, $release_type, $title, $print_size);
1253  }
1254  
1255  /**
1256   * Generate a table of releases for a given project.
1257   *
1258   * @param $project
1259   *   The project object (as returned by node_load(), for example).
1260   *
1261   * @param $table_type
1262   *   Indicates what kind of table should be generated. Possible options:
1263   *    'recommended': Only show the current recommended versions.
1264   *    'supported': Only show the latest release from each supported branch.
1265   *    'all': Include all releases.
1266   *
1267   * @param $release_type
1268   *   Filter what kinds of releases are visible in the table. Possible options:
1269   *    'official': Only include offical releases.
1270   *    'snapshot': Only include development snapshots.
1271   *    'all': Include all releases.
1272   *
1273   * @param $title
1274   *   The title of the first column in the table. Defaults to "Version" if NULL.
1275   *
1276   * @param $print_size
1277   *   Should the table include the filesize of each release?
1278   *
1279   * @param $check_edit
1280   *   Should the table check for and include edit links to user with access?
1281   */
1282  function project_release_table($project, $table_type = 'recommended', $release_type = 'all', $title = NULL, $print_size = TRUE, $check_edit = TRUE) {
1283    if (empty($title)) {
1284      $title = t('Version');
1285    }
1286  
1287    // Can the current user edit releases for this project?
1288    $can_edit = $check_edit ? node_access('update', $project) : FALSE;
1289  
1290    // Generate the cache ID.
1291    $cid = 'table:'. $project->nid .':'. $table_type .':'. $release_type .':'. $title .':'. (int)$print_size .':'. (int)$can_edit;
1292    if ($cached = cache_get($cid, 'cache_project_release')) {
1293      return $cached->data;
1294    }
1295  
1296    $selects = array();
1297    $join = $where = $order_by = '';
1298    $args = array();
1299    $tids = project_release_compatibility_list();
1300    if (!empty($tids)) {
1301      $join = ' INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid in ('
1302        . db_placeholders($tids) .') '
1303        .' INNER JOIN {term_data} td ON td.tid = tn.tid ';
1304      $args = array_keys($tids);
1305      $selects[] = 'tn.tid';
1306      $selects[] = 'td.name as api_term_name';
1307      $orderby[] = 'td.weight';
1308      $orderby[] = 'td.name';
1309    }
1310  
1311    if ($tids) {
1312      $selects[] = 'prsv.supported';
1313      $selects[] = 'prsv.recommended';
1314      $selects[] = 'prsv.snapshot';
1315      $join .= ' INNER JOIN {project_release_supported_versions} prsv ON prsv.nid = r.pid AND prsv.tid = tn.tid AND prsv.major = r.version_major ';
1316      if ($table_type == 'recommended') {
1317        $join .= 'AND prsv.recommended = %d ';
1318        $args[] = 1;
1319      }
1320      elseif ($table_type == 'supported') {
1321        $join .= 'AND prsv.supported = %d ';
1322        $args[] = 1;
1323      }
1324    }
1325    else {
1326      // TODO: someday (never?) when project_release doesn't require taxonomy.
1327    }
1328    $args[] = $project->nid;  // Account for r.pid.
1329    $args[] = 1;  // Account for n.status = 1.
1330  
1331    switch ($release_type) {
1332      case 'official':
1333        $where = 'AND r.rebuild <> %d';
1334        $args[] = 1;
1335        break;
1336  
1337      case 'snapshot':
1338        // For snapshot tables, restrict to snapshot nodes from branches where
1339        // the maintainer wants the snapshot visible.
1340        $where = 'AND r.rebuild = %d';
1341        $args[] = 1;
1342        if ($tids) {
1343          $where .= ' AND prsv.snapshot = %d';
1344          $args[] = 1;
1345        }
1346        break;
1347  
1348      case 'all':
1349        // If we're generating the default releases table, we want the
1350        // dev snapshots to be last in the query results, so that we
1351        // only show them if there's nothing else.
1352        if ($table_type == 'recommended') {
1353          $orderby[] = 'r.rebuild ASC';
1354        }
1355        break;
1356    }
1357  
1358    $orderby[] = 'r.version_major DESC';
1359    $orderby[] = 'r.version_minor DESC';
1360    $orderby[] = 'r.version_patch DESC';
1361    $orderby[] = 'f.timestamp DESC';
1362  
1363    $order_by = !empty($orderby) ? (' ORDER BY '. implode(', ', $orderby)) : '';
1364    $select = !empty($selects) ? (implode(', ', $selects) .',') : '';
1365  
1366    // TODO: we MUST rewrite this query when multiple files attachments
1367    // per release node lands, as it will return a non-unique result set.
1368    $result = db_query(db_rewrite_sql(
1369      "SELECT n.nid, n.created, f.filename, f.filepath, f.timestamp, ".
1370      "f.filesize, $select r.* FROM {node} n ".
1371      "INNER JOIN {project_release_nodes} r ON r.nid = n.nid ".
1372      "INNER JOIN {project_release_file} prf ON n.nid = prf.nid ".
1373      "INNER JOIN {files} f ON prf.fid = f.fid$join ".
1374      "WHERE (r.pid = %d) AND (n.status = %d) $where $order_by"),
1375      $args);
1376  
1377    $rows = array();  // Rows for the download table.
1378    $seen = array();  // Keeps track of which versions we already saw.
1379    while ($release = db_fetch_object($result)) {
1380      $tid = $release->tid;
1381      $major = $release->version_major;
1382      $recommended = false;
1383      if ($table_type == 'supported') {
1384        // Supported version can be multiple majors per tid.
1385        if (empty($seen[$tid])) {
1386          $seen[$tid] = array();
1387        }
1388        if (empty($seen[$tid][$major])) {
1389          $seen[$tid][$major] = 1;
1390          if ($release->recommended) {
1391            $recommended = true;
1392          }
1393        }
1394        else {
1395          // We already know the supported release for this tid/major, go on.
1396          continue;
1397        }
1398      }
1399      else {
1400        if (empty($seen[$tid])) {
1401          // Only one major per tid, so the row lives here.
1402          $seen[$tid] = 1;
1403          if ($release->recommended) {
1404            $recommended = true;
1405          }
1406        }
1407        elseif ($table_type == 'recommended') {
1408          // We already know the recommended release for this tid and that's all
1409          // we want in the table, so skip this release.
1410          continue;
1411        }
1412      }
1413      // If we're still here, we need to add the row to the table.
1414      $rows[] = theme('project_release_download_table_row', $release, $recommended, $can_edit, $print_size);
1415    }
1416  
1417    $header = array(
1418      array(
1419        'class' => 'release-title',
1420        'data' => $title,
1421      ),
1422      array(
1423        'class' => 'release-date',
1424        'data' => t('Date'),
1425      ),
1426    );
1427    if ($print_size) {
1428      $header[] = array(
1429        'class' => 'release-size',
1430        'data' => t('Size'),
1431      );
1432    }
1433    $header[] = array(
1434      'class' => 'release-links',
1435      'data' => t('Links'),
1436    );
1437    $header[] = array(
1438      'class' => 'release-status',
1439      'data' => t('Status'),
1440      'colspan' => 2,
1441    );
1442  
1443    $output = '';
1444    if (!empty($rows)) {
1445      $output = theme('table', $header, $rows, array('class' => 'releases'));
1446    }
1447    // Default cache time is 12 hours - will be cleared by the packaging script
1448    cache_set($cid, $output, 'cache_project_release', time() + 43200);
1449    return $output;
1450  }
1451  
1452  /**
1453   * Helper function to return an individual row for the download table.
1454   *
1455   * @param $release
1456   *   The release object queried from the database. Since this is NOT a
1457   *   fully-loaded $node object, so the release-related fields are not in a
1458   *   'project_release' sub-array.
1459   * @param $recommended
1460   *   Boolean indicating if this release is the currently recommended one.
1461   * @param $can_edit
1462   *   Boolean indicating if the current user can edit the release.
1463   * @param $print_size
1464   *   Boolean indicating if the size of the download should be printed.
1465   */
1466  function theme_project_release_download_table_row($release, $recommended = false, $can_edit = false, $print_size = true) {
1467    static $icons = array();
1468    if (empty($icons)) {
1469      $icons = array(
1470        'ok' => 'misc/watchdog-ok.png',
1471        'warning' => 'misc/watchdog-warning.png',
1472        'error' => 'misc/watchdog-error.png',
1473      );
1474    }
1475    $links = array();
1476    if (!empty($release->filepath)) {
1477      $links['project_release_download'] = theme('project_release_download_link', $release->filepath, t('Download'), TRUE);
1478    }
1479    $links['project_release_notes'] = array(
1480      'title' => t('Release notes'),
1481      'href' => "node/$release->nid",
1482    );
1483    if ($can_edit) {
1484      $links['project_release_edit'] = array(
1485        'title' => t('Edit'),
1486        'href' => "node/$release->nid/edit",
1487      );
1488    }
1489    // Figure out the class for the table row
1490    $row_class = $release->rebuild ? 'release-dev' : 'release';
1491    // Now, set the row color and help text, based on the release attributes.
1492    if (!$release->supported) {
1493      $text = theme('project_release_download_text_unsupported', $release, 'summary');
1494      $message = theme('project_release_download_text_unsupported', $release, 'message');
1495      $classification = 'error';
1496    }
1497    elseif ($release->rebuild) {
1498      $reason = theme('project_release_download_text_snapshot', $release, 'summary');
1499      $message = theme('project_release_download_text_snapshot', $release, 'message');
1500      $classification = 'error';
1501    }
1502    elseif ($recommended) {
1503      $reason = theme('project_release_download_text_recommended', $release, 'summary');
1504      $message = theme('project_release_download_text_recommended', $release, 'message');
1505      $classification = 'ok';
1506    }
1507    else {
1508      // Supported, but not recommened, official release.
1509      $reason = theme('project_release_download_text_supported', $release, 'summary');
1510      $message = theme('project_release_download_text_supported', $release, 'message');
1511      $classification = 'warning';
1512    }
1513  
1514    $row = array(
1515      // class of <tr>
1516      'class' => $row_class .' '. $classification,
1517      'data' => array(
1518        array(
1519          'class' => 'release-title',
1520          'data' => l($release->version, "node/$release->nid"),
1521        ),
1522        array(
1523          'class' => 'release-date',
1524          'data' => !empty($release->filepath) ? format_date($release->timestamp, 'custom', 'Y-M-d') : format_date($release->created, 'custom', 'Y-M-d'),
1525        ),
1526      ),
1527    );
1528    if ($print_size) {
1529      $row['data'][] = array(
1530        'class' => 'release-size',
1531        'data' => !empty($release->filepath) ? format_size($release->filesize) : t('n/a'),
1532        );
1533    }
1534    $row['data'][] = array(
1535      'class' => 'release-links',
1536      'data' => theme('links', $links),
1537    );
1538    $row['data'][] = array(
1539      'class' => 'release-reason',
1540      'data' => $reason,
1541    );
1542    $row['data'][] = array(
1543      'class' => 'release-icon',
1544      'data' => theme('image', $icons[$classification], $message, $message),
1545    );
1546    return $row;
1547  }
1548  
1549  /**
1550   * Return the message text for recommended releases in the download table.
1551   *
1552   * @param $release
1553   *   The release object queried from the database. Since this is NOT a
1554   *   fully-loaded $node object, so the release-related fields are not in a
1555   *   'project_release' sub-array.
1556   * @param $text_type
1557   *   What kind of text to render.  Can be either 'summary' for the summary
1558   *   text to include directly on the project node, or 'message' for the text
1559   *   to put in the title and alt attributes of the icon.
1560   */
1561  function theme_project_release_download_text_recommended($release, $text_type) {
1562    if ($text_type == 'summary') {
1563      return t('Recommended for %api_term_name', array('%api_term_name' => $release->api_term_name));
1564    }
1565    return t('This is currently the recommended release for @api_term_name.', array('@api_term_name' => $release->api_term_name));
1566  }
1567  
1568  /**
1569   * Return the message text for supported releases in the download table.
1570   *
1571   * @see theme_project_release_download_text_recommended
1572   */
1573  function theme_project_release_download_text_supported($release, $text_type) {
1574    if ($text_type == 'summary') {
1575      return t('Supported for %api_term_name', array('%api_term_name' => $release->api_term_name));
1576    }
1577    return t('This release is supported but is not currently the recommended release for @api_term_name.', array('@api_term_name' => $release->api_term_name));
1578  }
1579  
1580  /**
1581   * Return the message text for snapshot releases in the download table.
1582   *
1583   * @see theme_project_release_download_text_recommended
1584   */
1585  function theme_project_release_download_text_snapshot($release, $text_type) {
1586    if ($text_type == 'summary') {
1587      return t('Development snapshot');
1588    }
1589    return t('Development snapshots are automatically regenerated and their contents can frequently change, so they are not recommended for production use.');
1590  }
1591  
1592  /**
1593   * Return the message text for snapshot releases in the download table.
1594   *
1595   * @see theme_project_release_download_text_recommended
1596   */
1597  function theme_project_release_download_text_unsupported($release, $text_type) {
1598    if ($text_type == 'summary') {
1599      return t('Unsupported');
1600    }
1601    return t('This release is not supported and may no longer work.');
1602  }
1603  
1604  /**
1605   * Implementation of hook_taxonomy().
1606   */
1607  function project_release_taxonomy($op, $type, $array = NULL) {
1608    if ($op == 'delete' && $type == 'vocabulary') {
1609      if ($array['vid'] == _project_release_get_api_vid()) {
1610        variable_del('project_release_api_vocabulary');
1611      }
1612      elseif ($array['vid'] == _project_release_get_release_type_vid()) {
1613        variable_del('project_release_release_type_vid');
1614      }
1615    }
1616    elseif ($type == 'term' && $array['vid'] == _project_release_get_api_vid()) {
1617      menu_rebuild();
1618    }
1619  }
1620  
1621  /**
1622   * If taxonomy is enabled, returns the taxonomy tree for the
1623   * API compatibility vocabulary, otherwise, it returns false.
1624   */
1625  function project_release_get_api_taxonomy() {
1626    if (!module_exists('taxonomy')) {
1627      return false;
1628    }
1629    static $tree = NULL;
1630    if (!isset($tree)) {
1631      $tree = taxonomy_get_tree(_project_release_get_api_vid());
1632    }
1633    return $tree;
1634  }
1635  
1636  /**
1637   * Returns the vocabulary id for project release API
1638   */
1639  function _project_release_get_api_vid() {
1640    return variable_get('project_release_api_vocabulary', '');
1641  }
1642  
1643  /**
1644   * Return the taxonomy tree for the release type vocabulary (if any).
1645   *
1646   * If taxonomy is disabled, this returns false.
1647   */
1648  function project_release_get_release_type_vocabulary() {
1649    if (!module_exists('taxonomy')) {
1650      return false;
1651    }
1652    static $tree = NULL;
1653    if (!isset($tree)) {
1654      $tree = taxonomy_get_tree(_project_release_get_release_type_vid());
1655    }
1656    return $tree;
1657  }
1658  
1659  /**
1660   * Return the vocabulary id for project release type.
1661   */
1662  function _project_release_get_release_type_vid() {
1663    return variable_get('project_release_release_type_vid', '');
1664  }
1665  
1666  function project_release_exists($version) {
1667    $fields = array('version_major', 'version_minor', 'version_patch', 'version_api_tid');
1668    foreach ($fields as $field) {
1669      if (isset($version->$field) && is_numeric($version->$field)) {
1670        $types[$field] = "%d";
1671        $values[$field] = $version->$field;
1672        $foo = $version->$field;
1673      }
1674      else {
1675        $null_types[] = $field;
1676      }
1677    }
1678    $fields = array('version', 'version_extra');
1679    foreach ($fields as $field) {
1680      if (isset($version->$field) && $version->$field !== '') {
1681        $types[$field] = "'%s'";
1682        $values[$field] = $version->$field;
1683        $str = $version->$field;
1684      }
1685      elseif ($field == 'version_extra') {
1686        $null_types[] = $field;
1687      }
1688    }
1689    if (empty($types) && empty($null_types)) {
1690      // We have nothing to query, yet...
1691      return false;
1692    }
1693  
1694    $sql = 'SELECT COUNT(*) FROM {project_release_nodes} WHERE pid = %d';
1695    if (!empty($types)) {
1696      foreach ($types as $field => $type) {
1697        $sql .= " AND $field = $type";
1698      }
1699    }
1700    if (!empty($null_types)) {
1701      foreach ($null_types as $field) {
1702        $sql .= " AND $field IS NULL";
1703      }
1704    }
1705    // we put pid as the first WHERE, so stick it on the front
1706    $values = array_merge(array('pid' => $version->pid), $values);
1707    return db_result(db_query($sql, $values));
1708  }
1709  
1710  /**
1711   * Generates the appropriate download link for a give file path. This
1712   * function takes the 'project_release_download_base' setting into
1713   * account, so it should be used everywhere a download link is made.
1714   *
1715   * @param $filepath
1716   *   The path to the download file, as stored in the database.
1717   * @param $link_text
1718   *   The text to use for the download link. If NULL, the basename
1719   *   of the given $filepath is used.
1720   * @param $as_array
1721   *   Should the link be returned as a structured array, or as raw HTML?
1722   * @return
1723   *   The link itself, as a structured array.
1724   */
1725  function theme_project_release_download_link($filepath, $link_text = NULL, $as_array = FALSE) {
1726    if (empty($link_text)) {
1727      $link_text = basename($filepath);
1728    }
1729    $download_base = variable_get('project_release_download_base', '');
1730    if (!empty($download_base)) {
1731      $link_path = $download_base . $filepath;
1732    }
1733    else {
1734      $link_path = file_create_url($filepath);
1735    }
1736    if ($as_array) {
1737      return array(
1738        'title' => $link_text,
1739        'href' => $link_path,
1740      );
1741    }
1742    else {
1743      return l($link_text, $link_path);
1744    }
1745  }
1746  
1747  /**
1748   * Implementation of hook_file_download().
1749   *
1750   * @param $filename
1751   *   The name of the file to download.
1752   * @return
1753   *   An array of header fields for the download.
1754   */
1755  function project_release_file_download($filename) {
1756    $filepath = file_create_path($filename);
1757    $result = db_query("SELECT prf.nid FROM {project_release_file} prf INNER JOIN {files} f ON prf.fid = f.fid WHERE f.filepath = '%s'", $filepath);
1758    if ($nid = db_result($result)) {
1759      $node = node_load($nid);
1760      if (node_access('view', $node)) {
1761        return array(
1762          'Content-Type: application/octet-stream',
1763          'Content-Length: '. filesize($filepath),
1764          'Content-Disposition: attachment; filename="'. mime_header_encode($filename) .'"',
1765        );
1766      }
1767      return -1;
1768    }
1769  }
1770  
1771  /**
1772   * Implementation of hook_flush_caches().
1773   */
1774  function project_release_flush_caches() {
1775    return array('cache_project_release');
1776  }
1777  
1778  /**
1779   * Menu callback to select a project when creating a new release.
1780   */
1781  function project_release_pick_project_page($type_name) {
1782    drupal_set_title(t('Submit @name', array('@name' => $type_name)));
1783    $project = arg(3);
1784    if (!empty($project)) {
1785      // If there's any argument at all and we hit this form, it's from a
1786      // non-numeric project id, which by definition is invalid.  No one's ever
1787      // going to hit this code from clicking around in the normal UI, only if
1788      // they type in a URL manually.
1789      drupal_set_message(t('Specified argument (%project) is not a valid project ID number.', array('%project' => $project)), 'error');
1790      return drupal_goto('/node/add/project-release');
1791    }
1792    return drupal_get_form('project_release_pick_project_form');
1793  }
1794  
1795  /**
1796   * Implementation of hook_theme().
1797   */
1798  function project_release_theme() {
1799    return array(
1800      'project_release_download_file' => array(
1801        'arguments' => array(
1802          'file' => NULL,
1803          'download_link' => TRUE,
1804        ),
1805      ),
1806      'project_release_download_link' => array(
1807        'arguments' => array(
1808          'filepath' => NULL,
1809          'link_text' => NULL,
1810          'as_array' => FALSE,
1811        ),
1812      ),
1813      'project_release_download_table_row' => array(
1814        'arguments' => array(
1815          'release' => NULL,
1816          'recommended' => FALSE,
1817          'can_edit' => FALSE,
1818          'print_size' => TRUE,
1819        ),
1820      ),
1821      'project_release_download_text_recommended' => array(
1822        'arguments' => array(
1823          'release' => NULL,
1824          'text_type' => NULL,
1825        ),
1826      ),
1827      'project_release_download_text_snapshot' => array(
1828        'arguments' => array(
1829          'release' => NULL,
1830          'text_type' => NULL,
1831        ),
1832      ),
1833      'project_release_download_text_supported' => array(
1834        'arguments' => array(
1835          'release' => NULL,
1836          'text_type' => NULL,
1837        ),
1838      ),
1839      'project_release_download_text_unsupported' => array(
1840        'arguments' => array(
1841          'release' => NULL,
1842          'text_type' => NULL,
1843        ),
1844      ),
1845      'project_release_form_value' => array(
1846        'file' => 'includes/release_node_form.inc',
1847        'arguments' => array(
1848          'element' => NULL,
1849        ),
1850      ),
1851      'project_release_project_download_table' => array(
1852        'arguments' => array(
1853          'node' => NULL,
1854        ),
1855      ),
1856      'project_release_project_edit_form' => array(
1857        'file' => 'includes/release_node_form.inc',
1858        'arguments' => array(
1859          'form' => NULL,
1860        ),
1861      ),
1862      'project_release_table_overview' => array(
1863        'arguments' => array(
1864          'project' => NULL,
1865          'table_type' => NULL,
1866          'release_type' => NULL,
1867          'title' => NULL,
1868          'print_size' => NULL,
1869        ),
1870      ),
1871      'project_release_node_form_version_elements' => array(
1872        'arguments' => array(
1873          'form' => NULL,
1874        ),
1875      ),
1876      'project_release_update_status_icon' => array(
1877        'arguments' => array(
1878          'status' => NULL,
1879        ),
1880      ),
1881    );
1882  }
1883  
1884  function theme_project_release_node_form_version_elements($form) {
1885    $output = '<div class="version-elements">';
1886    $output .= drupal_render($form);
1887    $output .= '</div>';
1888    return $output;
1889  }
1890  
1891  /**
1892   * Implement hook_token_list() (from token.module)
1893   */
1894  function project_release_token_list($type) {
1895    if ($type == 'node') {
1896      $tokens['node'] = array(
1897        'project_release_pid' => t("A release's project nid"),
1898        'project_release_project_title' => t("A release's project title"),
1899        'project_release_project_title-raw' => t("A release's project title raw"),
1900        'project_release_project_shortname' => t("A release's project short name"),
1901        'project_release_version' => t("A release's version string"),
1902        'project_release_version_major' => t("A release's major version number"),
1903        'project_release_version_minor' => t("A release's minor version number"),
1904        'project_release_version_patch' => t("A release's patch version number"),
1905        'project_release_version_extra' => t("A release's extra version identifier"),
1906      );
1907      if (project_release_compatibility_list()) {
1908        $vocab = taxonomy_vocabulary_load(_project_release_get_api_vid());
1909        $tokens['node']['project_release_version_api_tid'] = t("A release's %api_compatibility term ID", array('%api_compatibility' => $vocab->name));
1910        $tokens['node']['project_release_version_api_term'] = t("A release's %api_compatibility term name", array('%api_compatibility' => $vocab->name));
1911      }
1912      return $tokens;
1913    }
1914  }
1915  
1916  /**
1917   * Implement hook_token_values() (from token.module).
1918   */
1919  function project_release_token_values($type = 'all', $object = NULL) {
1920    if ($type == 'node') {
1921      // Defaults in case it's not a release or we run into other problems.
1922      $values = array(
1923        'project_release_pid' => '',
1924        'project_release_project_title' => '',
1925        'project_release_project_title-raw' => '',
1926        'project_release_project_shortname' => '',
1927        'project_release_version' => '',
1928        'project_release_version_major' => '',
1929        'project_release_version_minor' => '',
1930        'project_release_version_patch' => '',
1931        'project_release_version_extra' => '',
1932        'project_release_version_api_tid' => '',
1933        'project_release_version_api_term' => '',
1934      );
1935      if ($object->type == 'project_release') {
1936        if ($project = node_load($object->project_release['pid'])) {
1937          $values['project_release_pid'] = intval($object->project_release['pid']);
1938          $values['project_release_project_title'] = check_plain($project->title);
1939          $values['project_release_project_title-raw'] = $project->title;
1940          $values['project_release_project_shortname'] = check_plain($project->project['uri']);
1941        }
1942        $values['project_release_version'] = check_plain($object->project_release['version']);
1943        $values['project_release_version_major'] = check_plain($object->project_release['version_major']);
1944        $values['project_release_version_minor'] = check_plain($object->project_release['version_minor']);
1945        $values['project_release_version_patch'] = check_plain($object->project_release['version_patch']);
1946        $values['project_release_version_extra'] = check_plain($object->project_release['version_extra']);
1947        if (!empty($object->project_release['version_api_tid'])) {
1948          $term = taxonomy_get_term($object->project_release['version_api_tid']);
1949          $values['project_release_version_api_tid'] = check_plain($term->tid);
1950          $values['project_release_version_api_term'] = check_plain($term->name);
1951        }
1952      }
1953      return $values;
1954    }
1955  }
1956  
1957  /**
1958   * Determines taxonomy-specific functionality for releases.
1959   */
1960  function project_release_use_taxonomy() {
1961    return module_exists('taxonomy') && _project_release_get_api_vid();
1962  }
1963  
1964  /**
1965   * Implementation of hook_help().
1966   */
1967  function project_release_help($section) {
1968    switch ($section) {
1969      case 'admin/project/project-release-settings':
1970        if (project_release_use_taxonomy()) {
1971          return _project_release_taxonomy_help();
1972        }
1973        break;
1974    }
1975    if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'taxonomy') {
1976      $vid = _project_release_get_api_vid();
1977      if (arg(3) == $vid) {
1978        return _project_release_taxonomy_help($vid, FALSE);
1979      }
1980    }
1981  }
1982  
1983  /**
1984   * Prints help message for release compatibility vocabulary configuration.
1985   *
1986   * @param $vid
1987   *   Vocabulary ID of the project taxonomy.
1988   * @param $vocab_link
1989   *   Boolean that controls if a link to the vocabulary admin page is added.
1990   */
1991  function _project_release_taxonomy_help($vid = 0, $vocab_link = TRUE) {
1992    if (!$vid) {
1993      $vid = _project_release_get_api_vid();
1994    }
1995    if (empty($vid)) {
1996      return;
1997    }
1998    $vocabulary = taxonomy_vocabulary_load($vid);
1999    $text = '<p>'. t('The Project release module makes special use of the taxonomy (category) system. A special vocabulary, %vocabulary_name, has been created automatically.', array('%vocabulary_name' => $vocabulary->name)) .'</p>';
2000    $text .= '<p>'. t('To categorize project releases by their compatibility with a version of some outside software (eg. a library or API of some sort), add at least one term to this vocabulary. For example, you might add the following terms: "5.x", "6.x", "7.x".') .'</p>';
2001    $text .='<p>'. t('For more information, please see !url.', array('!url' => l('http://drupal.org/node/116544', 'http://drupal.org/node/116544'))) .'</p>';
2002    if ($vocab_link) {
2003      $text .= '<p>'. t('Use the <a href="@taxonomy-admin">vocabulary admininistration page</a> to view and add terms.', array('@taxonomy-admin' => url('admin/content/taxonomy/'. $vid))) .'</p>';
2004    }
2005    return $text;
2006  }
2007  
2008  /**
2009   * Implementation of hook_views_api().
2010   */
2011  function project_release_views_api() {
2012    return array(
2013      'api' => 2,
2014      'path' => drupal_get_path('module', 'project_release') .'/views',
2015    );
2016  }
2017  
2018  /**
2019   * Return the mapping of version_extra prefixes to version_extra_weight values.
2020   *
2021   * This mapping allows project_release to use SQL to sort releases by version,
2022   * even though direct string comparison doesn't work for the kinds of version
2023   * strings people might use (for example "1.0-unstable1" should be lower than
2024   * "1.0-alpha3", even though "u" comes higher in the alphabet than "a"). This
2025   * is similar to the logic version_compare() performs, only using this weight
2026   * field, we can do the comparison in SQL instead of in PHP.
2027   *
2028   * @return
2029   *   Associative array mapping version_extra prefixes into weights. The
2030   *   prefixes should be lowercase, since the query uses LOWER(version_extra)
2031   *   inside _project_release_update_version_extra_weights(). The special-case
2032   *   is the record with the key 'NULL' (should be uppercase) which doesn't
2033   *   correspond to a literal version_extra field, but is used for releases
2034   *   that do not define version_extra where the value is NULL in the database.
2035   *
2036   * @see version_compare()
2037   * @see _project_release_update_version_extra_weights()
2038   */
2039  function project_release_get_version_extra_weight_map() {
2040    $default_map = array(
2041      'NULL' => 10, // Official releases without extra are always highest.
2042      'rc' => 4,
2043      'beta' => 3,
2044      'alpha' => 2,
2045      'unstable' => 1,
2046      // Anything that doesn't match will remain at weight 0, the default.
2047    );
2048    return variable_get('project_release_version_extra_weights', $default_map);
2049  }
2050  
2051  /**
2052   * Get the human-readable update status string, or an array of all statuses.
2053   *
2054   * @param $status
2055   *   Optional status code to get the human-readable string for. If NULL, the
2056   *   whole mapping of status codes to strings is returned.
2057   *
2058   * @return
2059   *   If $status is defined, the human-readable string for that status,
2060   *   otherwise, an associative array of status strings keyed by status code.
2061   */
2062  function project_release_update_status($status = NULL) {
2063    $status_map = array(
2064      PROJECT_RELEASE_UPDATE_STATUS_CURRENT => t('Up to date'),
2065      PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT => t('Update available'),
2066      PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE => t('Not secure'),
2067    );
2068    return isset($status) ? $status_map[$status] : $status_map;
2069  }
2070  
2071  /**
2072   * Render HTML for an icon approrpriate for the given release update status.
2073   *
2074   * @param $status
2075   *   Update status code to get the icon for.
2076   *
2077   * @return
2078   *   Icon to use for the given update status code.
2079   */
2080  function theme_project_release_update_status_icon($status) {
2081    $label = project_release_update_status($status);
2082    $icon = '';
2083    switch ($status) {
2084      case PROJECT_RELEASE_UPDATE_STATUS_CURRENT:
2085        $icon = theme('image', 'misc/watchdog-ok.png', $label, $label);
2086        break;
2087  
2088      case PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT:
2089        $icon = theme('image', 'misc/watchdog-warning.png', $label, $label);
2090        break;
2091  
2092      case PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE:
2093        $icon = theme('image', 'misc/watchdog-error.png', $label, $label);
2094        break;
2095    }
2096  
2097    return $icon;
2098  }
2099  
2100  /**
2101   * Implement hook_preprocess_views_view_table().
2102   *
2103   * Handles the logic for conditionally adding row classes based on release
2104   * update_status, and has a hack for hiding the update_status column entirely
2105   * on the project_release_download_table view if there's nothing to see.
2106   */
2107  function project_release_preprocess_views_view_table($variables) {
2108    $view = $variables['view'];
2109    if ($view->plugin_name == 'project_release_table') {  
2110      // TODO: this is a hack, we want something more flexible.
2111      $needs_status_column = FALSE;
2112      foreach ($view->result as $num => $result) {
2113        $variables['row_classes'][$num][] = "release-update-status-$result->project_release_nodes_update_status";
2114        if (!empty($variables['rows'][$num]['update_status'])) {
2115          $needs_status_column = TRUE;
2116        }
2117      }
2118      if ($view->name == 'project_release_download_table' && !$needs_status_column) {
2119        unset($variables['header']['update_status']);
2120        foreach ($variables['rows'] as &$row) {
2121          unset($row['update_status']);
2122        }
2123      }
2124      $variables['class'] .= " project-release";
2125    }
2126  }
2127  


Generated: Thu Mar 24 11:18:33 2011 Cross-referenced by PHPXref 0.7