'Releases',
'page callback' => 'project_release_project_edit_releases',
'page arguments' => array(1),
'access callback' => 'project_release_project_settings_form_access',
'access arguments' => array(1),
'type' => MENU_LOCAL_TASK,
'file' => 'includes/project_edit_releases.inc',
);
$items['node/add/project-release/%'] = array(
'page callback' => 'node_add',
'page arguments' => array('project-release'),
'access callback' => 'node_access',
'access arguments' => array('create', 'project_release'),
'file' => 'node.pages.inc',
'file path' => drupal_get_path('module', 'node'),
'type' => MENU_CALLBACK,
);
$items['admin/project/project-release-settings'] = array(
'description' => 'Configure the default version string for releases and other settings for the Project release module.',
'title' => 'Project release settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('project_release_settings_form'),
'access arguments' => array('administer projects'),
'weight' => 1,
'type' => MENU_NORMAL_ITEM,
'file' => 'includes/admin.settings.inc',
);
// Redirect node/add/project_release/* to node/add/project-release.
$items['node/add/project_release'] = array(
'page callback' => 'project_release_add_redirect_page',
'access callback' => 'node_access',
'access arguments' => array('create', 'project_release'),
'type' => MENU_CALLBACK,
'file' => 'includes/release_node_form.inc',
);
return $items;
}
/**
* Implementation of hook_menu_alter().
*/
function project_release_menu_alter(&$callbacks) {
$callbacks['node/add/project-release']['page callback'] = 'drupal_get_form';
$callbacks['node/add/project-release']['page arguments'] = array('project_release_pick_project_form');
$callbacks['node/add/project-release']['file'] = 'release_node_form.inc';
$callbacks['node/add/project-release']['file path'] = drupal_get_path('module', 'project_release') . '/includes';
}
/**
* Access callback for node/%project_node/edit/releases subtab.
*/
function project_release_project_settings_form_access($node) {
if (!variable_get('project_release_sandbox_allow_release', TRUE) && $node->project['sandbox'] && $node->project_release['releases'] == 0) {
return FALSE;
}
else {
return project_user_access($node, 'administer releases');
}
}
/**
* @defgroup project_release_node Drupal node-type related hooks
*/
/**
* Implementation of hook_access().
* @ingroup project_release_node
*
* TODO: Maybe we should add new permissions for accessing release
* nodes, but for now, we're just using the existing project perms.
*/
function project_release_access($op, $node, $account) {
switch ($op) {
case 'view':
// We want to use the identical logic for viewing projects,
// so we call that method directly.
return project_project_access($op, $node, $account);
case 'create':
// Due to how node_menu() works, we have to allow anyone with
// permission to maintain a project to be able to create a
// release node, or else you can have a faulty entry added to
// the {cache_menu} table that thinks you're not allowed to
// create *any* releases. So, we are more relaxed here, and
// enforce more closely in project_release_form(). As with the
// 'view' case above, we want the identical logic as project
// nodes, so we call that hook, instead of duplicating code.
return project_project_access($op, $node, $account);
case 'update':
// We can't just use project_project_access() here, since we
// need to check access to the project itself, not the release
// node, so we use the helper method and pass the project id.
return project_user_access($node->project_release['pid'], 'administer releases');
case 'delete':
// No one should ever delete a release node, only unpublish it.
return FALSE;
}
}
/**
* Implementation of hook_node_info().
* @ingroup project_release_node
*/
function project_release_node_info() {
return array(
'project_release' => array(
'name' => t('Project release'),
'module' => 'project_release',
'description' => t('A release of a project with a specific version number.'),
),
);
}
/**
* Implement hook_project_permission_info()
*
* This advertises an 'Administer releases' permission if the site is
* configured to allow sandboxes to have releases, if the project is not a
* sandbox, or if the project already has releases enabled.
*/
function project_release_project_permission_info($project = NULL) {
if (variable_get('project_release_sandbox_allow_release', TRUE) || empty($project->project['sandbox']) || !empty($project->project_release['releases'])) {
return array(
'administer releases' => array(
'title' => t('Administer releases'),
'description' => t('Allows a user to create and update releases, and to control which branches are recommended or supported.'),
),
);
}
}
/**
* Implement hook_project_maintainer_save()
*/
function project_release_project_maintainer_save($nid, $uid, $permissions = array()) {
db_query("UPDATE {project_release_project_maintainer} SET administer_releases = %d WHERE nid = %d AND uid = %d", !empty($permissions['administer releases']), $nid, $uid);
if (!db_affected_rows()) {
// If we didn't have a record to update, add this as a new maintainer.
db_query("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_releases) VALUES (%d, %d, %d)", $nid, $uid, !empty($permissions['administer releases']));
}
}
/**
* Implement hook_project_maintainer_remove()
*/
function project_release_project_maintainer_remove($nid, $uid) {
db_query("DELETE FROM {project_release_project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid);
}
/**
* Implement hook_project_maintainer_project_load()
*/
function project_release_project_maintainer_project_load($nid, &$maintainers) {
$query = db_query('SELECT u.uid, u.name, prpm.administer_releases FROM {project_release_project_maintainer} prpm INNER JOIN {users} u ON prpm.uid = u.uid WHERE prpm.nid = %d', $nid);
while ($maintainer = db_fetch_object($query)) {
if (empty($maintainers[$maintainer->uid])) {
$maintainers[$maintainer->uid]['name'] = $maintainer->name;
}
$maintainers[$maintainer->uid]['permissions']['administer releases'] = $maintainer->administer_releases;
}
}
/**
* Implement of hook_form() for project_release nodes.
*/
function project_release_form(&$release, &$form_state) {
module_load_include('inc', 'project_release', 'includes/release_node_form');
return _project_release_form($release, $form_state);
}
/**
* Validation callback for project release node forms.
*
* @see _project_release_node_form_validate()
*/
function project_release_node_form_validate(&$form, &$form_state) {
module_load_include('inc', 'project_release', 'includes/release_node_form');
return _project_release_node_form_validate($form, $form_state);
}
/**
* Implementation of hook_load().
* @ingroup project_release_node
*/
function project_release_load($node) {
$additions = db_fetch_array(db_query("SELECT * FROM {project_release_nodes} WHERE nid = %d", $node->nid));
// Add in file info.
$file_info = db_query("SELECT f.*, prf.filehash, prf.weight FROM {project_release_file} prf INNER JOIN {files} f ON prf.fid = f.fid WHERE prf.nid = %d", $node->nid);
$files = array();
while ($file = db_fetch_object($file_info)) {
$files[$file->fid] = $file;
}
$additions['files'] = $files;
$release = new stdClass;
$release->project_release = $additions;
return $release;
}
/**
* Implementation of hook_insert().
*
* @param $node
* Object containing form values from the project_release node form. Even
* though this is NOT a fully loaded $node object, the release-related
* values are in the $node->project_release array due to manual #tree and
* #parents hacking in project_release_form().
*
* @ingroup project_release_node
*/
function project_release_insert($node) {
module_load_include('inc', 'project_release', 'includes/release_node_form');
project_release_db_save($node, true);
}
/**
* Implementation of hook_update().
*
* @param $node
* Object containing form values from the project_release node form. Even
* though this is NOT a fully loaded $node object, the release-related
* values are in the $node->project_release array due to manual #tree and
* #parents hacking in project_release_form().
*
* @ingroup project_release_node
*/
function project_release_update($node) {
module_load_include('inc', 'project_release', 'includes/release_node_form');
project_release_db_save($node, false);
}
/**
* Verifies the data for supported release versions, and updates if necessary.
*
* @param $pid
* The project ID.
* @param $tid
* The API compatibility term ID.
* @param $major
* The major version of the new/modified/deleted release.
* @param $delete
* Boolean to indicate if we're deleting a release of this major or not.
*
* @return
* TRUE if we updated a record in {project_release_supported_versions},
* otherwise FALSE (e.g. if there were no published releases on the
* requested branch).
*/
function project_release_check_supported_versions($pid, $tid, $major, $delete) {
// Remember if we updated {project_release_supported_versions} so we can
// return the value to our caller.
$did_update = FALSE;
// If we're being called as a release node is being edited and saved, and
// the site we're running on is using DB replication, we need to make sure
// we're talking to the primary DB so that all of this works.
if (function_exists('db_set_ignore_slave')) {
db_set_ignore_slave();
}
// Regardless of if we're deleting, adding, or editing, we need to know the
// latest and recommended releases (if any) from the given branch. If
// there's no published release, these values will be 0.
list($latest_release, $recommended_release, $latest_security_release) = project_release_find_latest_releases($pid, $tid, $major);
if ($delete) {
// Make sure this isn't the last release node for the given major.
if (!empty($latest_release)) {
// Since the node we just deleted might have been the latest or
// recommended on the branch, update our record with the real values.
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);
$did_update = TRUE;
}
else {
// No latest release -- remove the bogus record for this branch.
db_query("DELETE FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d AND major = %d", $pid, $tid, $major);
$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));
if ($num_recommended > 1) {
// Something seriously bogus, clear out the values and start over.
db_query("UPDATE {project_release_supported_versions} SET recommended = %d WHERE nid = %d AND tid = %d", 0, $pid, $tid);
$num_recommended = 0;
}
}
}
else {
// Adding or editing a release.
if (!empty($latest_release)) {
// We have at least 1 published release, so make sure we have an entry
// for this major version in {project_release_supported_versions}.
$current_branches = db_query("SELECT major FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d", $pid, $tid);
$have_current_branch = FALSE;
$num_branches = 0;
while (($branch = db_fetch_object($current_branches)) !== FALSE) {
$num_branches++;
if ($branch->major == $major) {
$have_current_branch = TRUE;
break;
}
}
if ($num_branches == 0 || !$have_current_branch) {
// First entry for this API tid/major version pair, so add a new
// record to the table as supported but not recommended.
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);
}
else {
// We already have this branch in the table, but the latest_release
// and recommended_release fields might be stale based on whatever
// node was just added or edited.
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);
}
$did_update = TRUE;
}
}
// Regardless of insert/edit/delete, we want to go through and recompute
// {project_release_nodes}.update_status for all records on this branch.
// Note: we end up doing the same query in here that we performed in
// project_release_find_latest_releases(), we just need to process the
// results differently. However, to keep the code sane, we invoke the query
// again. If this becomes a performance problem, we can always refactor.
project_release_compute_update_status($pid, $tid, $major);
// Either way, clear the cache for the release table, since what we want to
// display might have changed, too.
$cid = 'table:'. $pid .':';
cache_clear_all($cid, 'cache_project_release', TRUE);
return $did_update;
}
/**
* Compute the {project_release_nodes}.update_status values for a given branch.
*
* For any given release node, there are three possible status values for if
* if the release needs an update or not:
* - 'current' (PROJECT_RELEASE_UPDATE_STATUS_CURRENT): It's the currently
* recommended release (without extra), or the latest possible release
* (including betas, rcs, etc). There is no need to upgrade this release at
* this time, it's the most up-to-date available.
* - 'not-current' (PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT): Any release
* older than the recommended release, or any older release with extra from
* the same major/minor/patch as the latest release.
* - 'not-secure' (PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE): Any release
* older than the latest security update on this branch is considered not
* secure. Releases are only marked 'not-secure' on sites that define the
* 'project_release_security_update_tid' variable.
*
* For example, if 1.2.2 is the recommended release, 1.2.1 was a security
* update, and 1.2.2-beta2 is the latest release, here would be the following
* update status values for various releases:
* - 1.2.2-beta2: 'current' (since it's the latest release)
* - 1.2.2-beta1: 'not-current' (since beta2 is available)
* - 1.2.2: 'current' (recommended release, latest without "extra")
* - 1.2.2-rc1: 'not-current' (since 1.2.2 official is out)
* - 1.2.1: 'not-current'
* - 1.2.1-beta1: 'not-secure' (since 1.2.1 official was a security update)
* - 1.2.0: 'not-secure'
*
* This status is recorded in the {project_release_nodes}.update_status column
* in the database. Whenever a release is created, updated, or deleted, we
* need to inspect all the other releases on the same branch to potentially
* modify the update_status column as needed.
*
* This function walks through all the records in the {project_release_nodes}
* table matching the given branch (API compatibility term ID and major
* version) for a specified project in version order (as determined by
* project_release_query_releases_by_branch() which sorts by version_minor,
* version_patch, version_extra_weight and finally version_extra), and
* compares them with that branch's latest release, recommended release, and
* latest security release to compute their update status. If the release is
* the latest or recommended, it's 'current'. Otherwise, it's 'not-current'
* if we haven't passed a security update yet, or 'not-secure' once we find a
* security update.
*
* @param $pid
* The project ID.
* @param $api_tid
* The API compatibility term ID.
* @param $major
* The major version of the new/modified/deleted release.
*
* @return
* Void. This function directly updates the {project_release_nodes} table
* with the appropriate values.
*
* @see project_release_check_supported_versions()
* @see project_release_query_releases_by_branch()
* @see project_release_release_nodeapi()
*/
function project_release_compute_update_status($pid, $api_tid, $major) {
$latest_release = $recommended_release = $latest_security_release = 0;
$nid_update_map = array();
$query = project_release_query_releases_by_branch($pid, $api_tid, $major);
while ($release = db_fetch_object($query)) {
// Clear out the status so we always start fresh with each release.
unset($update_status);
if (empty($latest_release)) {
$latest_release = $release->nid;
// If this is the latest release, it's current.
$update_status = PROJECT_RELEASE_UPDATE_STATUS_CURRENT;
}
if (empty($recommended_release) && empty($release->version_extra)) {
$recommended_release = $release->nid;
// If this is the recommended release, it's current.
$update_status = PROJECT_RELEASE_UPDATE_STATUS_CURRENT;
}
if (empty($latest_security_release) && !empty($release->security_update)) {
$latest_security_release = $release->nid;
}
// Based on what we've already seen, figure out the status. The only
// possible releases that can be "CURRENT" are the latest and recommended
// releases, and we already set the status for those. So, if we're here,
// we know it's not current, we just need to know if it's also not secure.
if (!isset($update_status)) {
// If we haven't found a security release yet, or the release we're on
// is the latest security update, this is just 'not_current'.
if (empty($latest_security_release) || $latest_security_release == $release->nid) {
$update_status = PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT;
}
// Otherwise, we're past the latest security release, this is insecure.
else {
$update_status = PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE;
}
}
// If the status is different than what we have in the DB, remember that
// we need to update this nid in the DB.
if ($update_status != $release->update_status) {
$nid_update_map[$update_status][] = $release->nid;
}
}
if (!empty($nid_update_map)) {
foreach ($nid_update_map as $update_status => $nids) {
if (!empty($nids)) {
$placeholders = db_placeholders($nids);
db_query("UPDATE {project_release_nodes} SET update_status = %d WHERE nid IN ($placeholders)", array_merge(array($update_status), $nids));
if ($update_status == PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE && module_exists('project_package')) {
project_package_check_update_status($nids);
}
}
}
}
}
/**
* Implementation of hook_delete().
* @ingroup project_release_node
*/
function project_release_delete($node) {
if (!empty($node->project_release['files'])) {
foreach ($node->project_release['files'] as $fid => $file) {
project_release_file_delete($file);
}
}
db_query("DELETE FROM {project_release_package_errors} WHERE nid = %d", $node->nid);
db_query("DELETE FROM {project_release_nodes} WHERE nid = %d", $node->nid);
}
/**
* Deletes release files.
*
* @param $file
* The file object to delete.
*/
function project_release_file_delete($file) {
db_query("DELETE FROM {files} WHERE fid = %d", $file->fid);
db_query("DELETE FROM {project_release_file} WHERE fid = %d", $file->fid);
file_delete(file_create_path($file->filepath));
}
/**
* @defgroup project_release_api Project release functions that other
* modules might want to use
*/
/**
* Returns the version format string for a given project
* @ingroup project_release_api
*/
function project_release_get_version_format($project) {
if (!empty($project->project_release['version_format'])) {
return $project->project_release['version_format'];
}
$db_format = db_result(db_query("SELECT version_format FROM {project_release_projects} WHERE nid = %d", $project->nid));
if (!empty($db_format)) {
return $db_format;
}
return variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT);
}
/**
* Validates a version format string. Only alphanumeric characters and
* [-_.] are allowed. Calls form_set_error() on error, else returns.
* @param $form_values Array of form values passed to validate hook.
* @param $element The name of the form element for the format string.
* @ingroup project_release_internal
*/
function _project_release_validate_format_string($form_values, $element) {
if (!preg_match('/^[a-zA-Z0-9_\-.!%#]+$/', $form_values[$element])) {
form_set_error($element, PROJECT_RELEASE_VERSION_FORMAT_VALID_MSG);
}
}
/**
* Returns the formatted version string for a given version object.
*
* @param $version
* Object containing the separate version-related fields, such as
* version_major, version_minor, etc.
* @param $project
* Optional project node that the version corresponds with. If not defined,
* the version object should at least include a "pid" field.
*
* @return
* The formatted version string for the given version and project info.
*
* @ingroup project_release_api
*/
function project_release_get_version($version, $project = NULL) {
if (isset($project)) {
$node = $project;
}
else {
$node->nid = $version->pid;
}
$variables = array();
foreach (array('major', 'minor', 'patch', 'extra') as $field) {
$var = "version_$field";
if (isset($version->$var) && $version->$var !== '') {
$variables["!$field"] = $version->$var;
$variables["%$field"] = '.'. $version->$var;
$variables["#$field"] = '-'. $version->$var;
}
else {
$variables["!$field"] = '';
$variables["%$field"] = '';
$variables["#$field"] = '';
}
}
$vid = _project_release_get_api_vid();
if (project_release_get_api_taxonomy() && isset($version->version_api_tid)) {
$term = taxonomy_get_term($version->version_api_tid);
$variables["!api"] = $term->name;
$variables["%api"] = '.'. $term->name;
$variables["#api"] = '-'. $term->name;
}
else {
$variables["!api"] = '';
$variables["%api"] = '';
$variables["#api"] = '';
}
$version_format = project_release_get_version_format($node);
return strtr($version_format, $variables);
}
/**
* Implementation of hook_view().
* @ingroup project_release_node
*/
function project_release_view($node, $teaser = FALSE, $page = FALSE) {
$node = node_prepare($node, $teaser);
$project = node_load($node->project_release['pid']);
if ($page) {
// Breadcrumb navigation
$breadcrumb[] = l($project->title, 'node/'. $project->nid);
$breadcrumb[] = l(t('Releases'), 'node/'. $project->nid .'/release');
project_project_set_breadcrumb($project, $breadcrumb);
}
$output = '';
$max_file_timestamp = 0;
if (!empty($node->project_release['files'])) {
$view_info = variable_get('project_release_files_view', 'project_release_files:default');
list($view_name, $display_name) = split(':', $view_info);
$output .= views_embed_view($view_name, $display_name, $node->nid);
foreach ($node->project_release['files'] as $file) {
$max_file_timestamp = max($max_file_timestamp, $file->timestamp);
}
$node->content['release_file_info'] = array(
'#value' => '
'. $output .'
',
'#weight' => -4,
);
}
$output = '';
if (project_use_cvs($project) && isset($node->project_release['tag'])) {
if (!empty($node->project_release['rebuild'])) {
$output .= t('Nightly development snapshot from CVS branch: @tag', array('@tag' => $node->project_release['tag'])) .'
';
}
else {
$output .= t('Official release from CVS tag: @tag', array('@tag' => $node->project_release['tag'])) .'
';
}
}
if (!empty($max_file_timestamp)) {
$output .= '' . t('Last updated: !changed', array('!changed' => format_date($max_file_timestamp))) . '
';
}
if (module_exists('project_usage') && user_access('view project usage')) {
$output .= ''. l(t('View usage statistics for this release'), 'project/usage/'. $node->nid) .'
';
}
$node->content['release_info'] = array(
'#value' => ''. $output .'
',
'#weight' => -3,
);
if (module_exists('project_package')) {
$output = '';
if (!empty($node->project_package['count'])) {
$view_info = variable_get('project_package_release_items_view', 'project_package_items:default');
list($view_name, $display_name) = split(':', $view_info);
$output .= '' . t('In this package') . '
';
$output .= views_embed_view($view_name, $display_name, $node->nid);
}
if (!empty($output)) {
$node->content['release_package_items'] = array(
'#value' => ''. $output .'
',
'#weight' => -2,
);
}
}
// Display packaging errors to admins.
if (project_user_access($node->project_release['pid'], 'administer releases')) {
$rows = array();
$result = db_query('SELECT * FROM {project_release_package_errors} WHERE nid = %d', $node->nid);
$error = db_fetch_object($result);
if (!empty($error)) {
$rows = unserialize($error->messages);
if (!empty($rows)) {
$node->content['release_errors'] = array(
'#value' => theme('item_list', $rows, t('Packaging error messages')),
'#weight' => -1,
'#prefix' => '',
'#suffix' => '
',
);
}
}
}
return $node;
}
function project_release_load_file($fid) {
return db_fetch_object(db_query("SELECT f.*, prf.filehash, prf.weight FROM {project_release_file} prf INNER JOIN {files} f ON prf.fid = f.fid WHERE f.fid = %d", $fid));
}
function theme_project_release_download_file($file, $download_link = TRUE) {
$output = '';
if ($download_link) {
$output .= ''. t('Download: !file', array('!file' => theme('project_release_download_link', $file->filepath))) .'
';
}
else {
$output .= ''. t('File: @filepath', array('@filepath' => $file->filepath)) .'
';
}
$output .= ''. t('Size: !size', array('!size' => format_size($file->filesize))) .'
';
$output .= ''. t('md5_file hash: !filehash', array('!filehash' => $file->filehash)) .'
';
$output .= ''. t('Last updated: !changed', array('!changed' => format_date($file->timestamp))) .'
';
return $output;
}
/*
@TODO: This function is used by project_issue, so we need to keep it here,
even though we're now creating the list of releases at node/XXX/release using
the views module. however, it might be nice if we could replace this function
with views as well just to use views's query builder. Maybe that's a bad
idea in terms of performance, however.
*/
/**
* Get an array of release nodes
* @ingroup project_release_api
*
* @param $project
* The project node object.
* @param $nodes
* If set, an array of release nodes will be returned.
* Otherwise only the version field will be returned in the array value.
* @param $sort_by
* This can be 'date' or 'version' and determines how the releases
* returned are to be sorted.
* @param $filter_by
* This can be 'all' to include all releases or 'files' to return
* only releases which have a file attached.
* @param $rids
* This is a special parameter that can be used to allow one or more
* releases to be returned even if the node itself is unpublished.
* This is useful when this function is called by the project_issue
* module to allow a user to keep the version of an issue unchanged
* even if the release represented by the version is now unpublished.
* @return
* An array of releases. The keys are the release node nids. The values
* will either be release objects or release version strings, depending
* on the value of the $nodes parameter.
*/
function project_release_get_releases($project, $nodes = TRUE, $sort_by = 'version', $filter_by = 'all', $rids = array()) {
if ($sort_by == 'date') {
$order_by = 'n.created';
}
else {
$order_by = 'r.version';
}
$where = '';
$join = '';
$args = array($project->nid);
if (!project_user_access($project, 'administer releases')) {
if (!empty($rids)) {
$where = "AND (n.status = %d OR n.nid IN (". db_placeholders($rids) ."))";
$args[] = 1;
foreach ($rids as $rid) {
$args[] = $rid;
}
}
else {
$where = 'AND (n.status = %d)';
$args[] = 1;
}
if ($filter_by == 'files') {
$join .= "INNER JOIN {project_release_file} prf ON n.nid = prf.nid";
}
}
$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);
$releases = array();
while ($obj = db_fetch_object($result)) {
if ($nodes) {
$releases[$obj->nid] = node_load($obj->nid);
}
else {
$releases[$obj->nid] = $obj->version;
}
}
return $releases;
}
/**
* @defgroup project_release_callback Menu callback functions
*/
/**
* Returns a listing of all active project release compatibility terms
* in the system.
* @ingroup project_release_api
*/
function project_release_compatibility_list() {
static $terms = array();
if (empty($terms) && $tree = project_release_get_api_taxonomy()) {
$tids = variable_get('project_release_active_compatibility_tids', array());
foreach ($tree as $term) {
if (($tids && !empty($tids[$term->tid])) || !$tids) {
$terms[$term->tid] = $term->name;
}
}
}
return $terms;
}
/**
* @defgroup project_release_fapi Form API hooks
*/
/**
* Implementation of hook_form_alter().
* @ingroup project_release_fapi
*/
function project_release_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'project_project_node_form') {
return project_release_alter_project_form($form, $form_state);
}
if ($form_id == 'project_release_node_form') {
return project_release_alter_release_form($form, $form_state);
}
if ($form_id == 'project_settings_form') {
return project_release_alter_project_settings_form($form, $form_state);
}
if ($form_id == 'project_promote_project_form') {
return project_release_alter_project_promote_form($form, $form_state);
}
if ($form_id == 'project_promote_project_confirm_form') {
return project_release_alter_project_promote_confirm_form($form, $form_state);
}
}
/**
* Alters the project_project node form to add release settings.
* @ingroup project_release_fapi
* @see project_release_form_alter
*/
function project_release_alter_project_form(&$form) {
if (!empty($form['project_node']['project']['uri']['#description'])) {
$form['project_node']['project']['uri']['#description'] .= ' ' . t('This string is also used to generate the name of releases associated with this project.');
}
else {
$form['project_node']['project']['uri']['#description'] = t('This string is used to generate the name of releases associated with this project.');
}
}
/**
* Alters the project_release node form to handle the API taxonomy.
* If the vocabulary is empty, this removes the form elements.
* @ingroup project_release_fapi
* @see project_release_form_alter
*/
function project_release_alter_release_form(&$form, &$form_state) {
global $user;
$node = $form['#node'];
$tid = '';
if (!empty($node->project_release['version_api_tid'])) {
$tid = $node->project_release['version_api_tid'];
}
$vid = _project_release_get_api_vid();
if (!project_release_get_api_taxonomy() && isset($form['taxonomy'][$vid])) {
unset($form['taxonomy'][$vid]);
}
else {
if (!user_access('administer projects')) {
// The user doesn't have 'administer projects' permission, so
// we restrict their options for the compatibility taxonomy.
if (!empty($tid)) {
// If we already have the term, we want to force it to stay.
$indexes = form_get_options($form['taxonomy'][$vid], $tid);
if ($indexes !== FALSE) {
foreach ($indexes as $index) {
$options[] = $form['taxonomy'][$vid]['#options'][$index];
}
}
$form['taxonomy'][$vid]['#default_value'] = $tid;
}
elseif ($tids = variable_get('project_release_active_compatibility_tids', array())) {
// We don't have the term since we're adding a new release.
// Restrict to the active terms (if any).
foreach (array_filter($tids) as $tid) {
$indexes = form_get_options($form['taxonomy'][$vid], $tid);
if ($indexes !== FALSE) {
foreach ($indexes as $index) {
$options[$index] = $form['taxonomy'][$vid]['#options'][$index];
}
}
}
}
if (!empty($options)) {
$form['taxonomy'][$vid]['#options'] = $options;
}
else {
unset($form['taxonomy'][$vid]);
}
// If they're not project admins, remove the delete button (if any).
unset($form['delete']);
}
}
// If there are no children elements, we should unset the entire
// thing so we don't end up with an empty fieldset.
if (isset($form['taxonomy']) && !element_children($form['taxonomy'])) {
unset($form['taxonomy']);
}
$form['buttons']['submit']['#submit'][] = 'project_release_node_submit';
}
/**
* Alters the project settings form.
*/
function project_release_alter_project_settings_form(&$form, &$form_state) {
$form['sandbox']['project_release_sandbox_allow_release'] = array(
'#title' => t('Enable releases for sandboxed projects'),
'#type' => 'checkbox',
'#default_value' => variable_get('project_release_sandbox_allow_release', TRUE),
'#description' => t('If checked, projects marked as sandboxes will be permitted to have releases associated with them.'),
);
}
/**
* Alters the form for promoting a project from sandbox to a full project.
*/
function project_release_alter_project_promote_form(&$form, &$form_state) {
// Unset this in case the user cancels on the confirm form so we don't leak
// this into the $_SESSION permanently. Also ensures we start clean.
unset($_SESSION['project_promote_project_releases']);
if (!variable_get('project_release_sandbox_allow_release', TRUE)) {
$node = node_load($form['pid']['#value']);
$form['releases'] = array(
'#type' => 'checkbox',
'#title' => t('Enable releases'),
'#return_value' => 1,
'#weight' => 10,
'#default_value' => $node->project_release['releases'],
'#description' => t('Allow releases of this project with specific versions. This can be changed later.'),
);
$form['#submit'][] = 'project_release_project_promote_form_submit';
}
}
/**
* Submit handler for project promote form.
*
* This just saves the value of the 'Enable releases' checkbox into the
* SESSION so it can persist when building the confirm form.
*/
function project_release_project_promote_form_submit($form, &$form_state) {
if (isset($form_state['values']['releases'])) {
$_SESSION['project_promote_project_releases'] = $form_state['values']['releases'];
}
}
/**
* Alters the confirm form for promoting sandbox into a full project.
*/
function project_release_alter_project_promote_confirm_form(&$form, &$form_state) {
if (isset($_SESSION['project_promote_project_releases'])) {
$form['releases'] = array(
'#type' => 'value',
'#value' => $_SESSION['project_promote_project_releases'],
);
// Note: we can't unset this value from the $_SEESSION here, or this form
// element won't be included when building the form during submission. So,
// we'll unset once we actually submit the confirm form.
}
$form['#submit'][] = 'project_release_project_promote_confirm_form_submit';
}
/**
* Submit handler for project promote confirm form.
*/
function project_release_project_promote_confirm_form_submit($form, &$form_state) {
db_query("UPDATE {project_release_projects} SET releases = %d WHERE nid = %d", $form_state['values']['releases'], $form_state['values']['nid']);
unset($_SESSION['project_promote_project_releases']);
}
/**
* @defgroup project_release_nodeapi Node API hooks
*/
/**
* hook_nodeapi() implementation. This just decides what type of node
* is being passed, and calls the appropriate type-specific hook.
* @ingroup project_release_nodeapi
* @see project_release_project_nodeapi().
*/
function project_release_nodeapi(&$node, $op, $arg) {
switch ($node->type) {
case 'project_project':
project_release_project_nodeapi($node, $op, $arg);
break;
case 'project_release':
project_release_release_nodeapi($node, $op, $arg);
break;
}
}
/**
* hook_nodeapi implementation specific to "project_project" nodes
* (from the project.module)
* @ingroup project_release_nodeapi
* @see project_release_nodeapi().
*/
function project_release_project_nodeapi(&$node, $op, $arg) {
switch ($op) {
case 'load':
project_release_project_nodeapi_load($node);
break;
case 'insert':
project_release_project_nodeapi_insert($node);
break;
case 'delete':
project_release_project_nodeapi_delete($node);
}
}
/**
* Loads project_release fields into the project node object.
*/
function project_release_project_nodeapi_load(&$node) {
$project = db_fetch_object(db_query('SELECT * FROM {project_release_projects} WHERE nid = %d', $node->nid));
if (!empty($project)) {
$fields = array('releases', 'version_format');
foreach ($fields as $field) {
$node->project_release[$field] = $project->$field;
}
$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));
if (isset($wants_snapshots)) {
$node->project_release['project_release_show_snapshots'] = TRUE;
}
}
}
/**
* Insert release information about a project node.
*/
function project_release_project_nodeapi_insert(&$node) {
$releases = (!variable_get('project_release_sandbox_allow_release', TRUE) && $node->project['sandbox']) ? 0 : 1;
db_query("INSERT INTO {project_release_projects} (nid, releases, version_format) VALUES (%d, %d, '%s')", $node->nid, $releases, '');
}
/**
* Deletes release information when a project is deleted.
*/
function project_release_project_nodeapi_delete(&$node) {
// TODO: unpublish (delete?) all release nodes associated with
// this project, too.
db_query('DELETE FROM {project_release_projects} WHERE nid = %d', $node->nid);
}
/**
* hook_nodeapi implementation specific to "project_release" nodes.
*
* We use hook_nodeapi() for our own node type to trigger some code that has
* to happen after taxonomy_nodeapi() runs. project_release already has to be
* weighted heavier than taxonomy for other things to work.
*
* @ingroup project_release_nodeapi
* @see project_release_nodeapi().
*/
function project_release_release_nodeapi(&$node, $op, $arg) {
switch ($op) {
case 'insert':
case 'update':
case 'delete':
// Since release nodes can be unpublished, we need to make sure that the
// recommended branch information is still up to date.
if (module_exists('taxonomy')) {
if (isset($node->project_release['version_api_tid'])) {
$tid = $node->project_release['version_api_tid'];
}
else {
$vid = _project_release_get_api_vid();
if (isset($node->taxonomy[$vid])) {
$tid = $node->taxonomy[$vid];
}
}
if (isset($tid)) {
project_release_check_supported_versions($node->project_release['pid'], $tid, $node->project_release['version_major'], ($op == 'delete' ? TRUE : FALSE));
}
}
break;
case 'rss item':
// Prepend the table of release info whenever a release is in a feed.
if (isset($node->body)) {
$node->body = $node->content['release_info']['#value'] . $node->body;
}
if (isset($node->teaser)) {
$node->teaser = $node->content['release_info']['#value'] . $node->teaser;
}
// If the release node has a file, include an enclosure attribute for it.
if (!empty($node->project_release['files'])) {
// RSS will only take the first file.
$file = reset($node->project_release['files']);
$file_link = theme('project_release_download_link', $file->filepath, NULL, TRUE);
return array(
array(
'key' => 'enclosure',
'attributes' => array(
'url' => $file_link['href'],
'length' => $file->filesize,
'type' => 'application/octet-stream',
)
)
);
}
break;
}
}
/**
* Fetch information about the current releases for a given project.
*
* This just queries the {project_release_supported_versions} table for either
* the latest release or the recommended release, and retrieves data about
* that release from the {node} and {project_release_nodes} tables. To
* actually recompute the latest and recommended releases for a given branch,
* you must use project_release_find_latest_releases().
*
* @param $project_nid
* The nid of the project to find the current release for.
* @param $api_tid
* The API compatibility term ID you want to search.
* @param $recommended_major
* An optional major version to search. If not specified, the current
* recommended branch from {project_release_supported_versions} is used.
* @param $type
* String for what kind of release to get ('recommended' or 'latest').
*
* @return
* An object containing all the fields from {project_release_nodes}, along
* with {node}.title and {node}.created, for the appropriate release; or
* FALSE if no published releases exists that the caller can access on the
* requested branch of the desired project.
*/
function project_release_get_current_recommended($project_nid, $api_tid, $recommended_major = NULL, $type = 'recommended') {
// Compute the appropriate JOIN ON clauses based on the arguments.
$prsv_joins[] = 'n.nid = ' . ($type == 'recommended' ? 'prsv.recommended_release' : 'prsv.latest_release');
$prsv_joins[] = 'prsv.nid = %d';
$join_params[] = $project_nid;
$prsv_joins[] = 'prsv.tid = %d';
$join_params[] = $api_tid;
if (!isset($recommended_major)) {
$prsv_joins[] = 'prsv.recommended = %d';
$join_params[] = 1;
}
else {
$prsv_joins[] = 'prsv.major = %d';
$join_params[] = $recommended_major;
}
// Build the actual JOIN ON string by AND'ing all the clauses together.
$prsv_join = implode(' AND ', $prsv_joins);
$result = db_query(db_rewrite_sql(
"SELECT n.nid, n.title, n.created, r.* FROM {node} n ".
"INNER JOIN {project_release_nodes} r ON r.nid = n.nid ".
"INNER JOIN {project_release_supported_versions} prsv ON $prsv_join "),
$join_params);
return db_fetch_object($result);
}
/**
* Finds the latest and recommended releases for a given project and branch.
*
* The "latest" release just means the published release node with the highest
* version string. The "recommended" release is the published release node
* with the highest version string that doesn't have a "version_extra" field
* (e.g. "beta1"). If all releases on the given branch have "extra", then the
* recommended release will be the same as the latest release.
*
* @param $project_nid
* The node ID of the project to find the latest and recommended releases of.
* @param $api_tid
* The API compatibility term ID to search.
* @param $major
* The {project_release_nodes}.version_major field of the branch to search.
* @param $access
* Optional boolean to indicate if node access checks should be enforced.
* Defaults to FALSE since the caller might not actually have access to all
* the releases or projects. However, this function usually has to compute
* the accurate values regardless of access, and consumers of this data are
* responsible for ensuring access.
*
* @return
* An array containing the node ID (nid) of the latest and recommended
* releases, and latest security update (if any) from the given branch.
*
* @see project_release_query_releases_by_branch()
*/
function project_release_find_latest_releases($project_nid, $api_tid, $major, $access = FALSE) {
$latest_release = $recommended_release = $latest_security_release = 0;
$query = project_release_query_releases_by_branch($project_nid, $api_tid, $major, $access);
while ($release = db_fetch_object($query)) {
if (empty($latest_release)) {
$latest_release = $release->nid;
}
if (empty($recommended_release) && empty($release->version_extra)) {
$recommended_release = $release->nid;
}
if (empty($latest_security_release) && !empty($release->security_update)) {
$latest_security_release = $release->nid;
}
// If we've found everything we're looking for, break out of the loop and
// stop inspecting release from this branch. $latest_release can't
// possibly be empty here, so don't bother testing for it.
if (!empty($recommended_release) && !empty($latest_security_release)) {
break;
}
}
// If we found no releases without extra (e.g. a new branch that only has
// betas), just call the latest release the recommended one).
if (empty($recommended_release)) {
$recommended_release = $latest_release;
}
return array(
$latest_release,
$recommended_release,
$latest_security_release,
);
}
/**
* Build a query for releases on a given branch, ordered by version.
*
* @param $project_nid
* The project node ID.
* @param $api_tid
* The API compatibility term ID.
* @param $major
* The major version that defines the branch for the project and API term.
* @param $access
* Optional boolean to indicate if node access checks should be enforced.
* Defaults to FALSE since the caller might not actually have access to all
* the releases or projects. However, this function usually has to compute
* the accurate values regardless of access, and consumers of this data are
* responsible for ensuring access.
*
* @return
* A database query result resource, as returned by db_query().
*
* @see db_query()
* @see project_release_find_latest_releases()
*/
function project_release_query_releases_by_branch($project_nid, $api_tid, $major, $access = FALSE) {
$wheres = $params = $order_bys = array();
$wheres[] = '(r.pid = %d)';
$params[] = $project_nid;
$wheres[] = '(r.version_api_tid = %d)';
$params[] = $api_tid;
$wheres[] = '(r.version_major = %d)';
$params[] = $major;
$wheres[] = '(n.status = %d)';
$params[] = 1;
$where = 'WHERE ' . implode(' AND ', $wheres);
// We always want the dev snapshots to show up last.
$order_bys[] = 'r.rebuild';
// Sort by the obvious integer values along the branch (minor and patch).
$order_bys[] = 'r.version_minor DESC';
$order_bys[] = 'r.version_patch DESC';
// To reliably sort release with version_extra, use version_extra_weight.
$order_bys[] = 'r.version_extra_weight DESC';
// Within releases of the same version_extra_weight (e.g. rc1 vs. rc2),
// sort by version_extra_delta.
$order_bys[] = 'r.version_extra_delta DESC';
// Within releases of the same version_extra_weight and version_extra_delta,
// sort alphabetically. This shouldn't normally happen, but just in case you
// have multiple releases with the same delta (e.g. "alpha-one", "alpha-two"
// etc), at least you'll get deterministic results.
$order_bys[] = 'r.version_extra DESC';
$order_by = 'ORDER BY '. implode(', ', $order_bys);
$sql = "SELECT n.nid, n.title, n.created, r.* FROM {node} n ".
"INNER JOIN {project_release_nodes} r ON r.nid = n.nid ".
"$where $order_by";
// Only enforce node access via db_rewrite_sql() if the caller specifically
// requested that behavior.
if ($access) {
$sql = db_rewrite_sql($sql);
}
return db_query($sql, $params);
}
/**
* Theme the appropriate release download table for a project node.
*/
function theme_project_release_project_download_table($node) {
if (empty($node->project_release['releases'])) {
return;
}
$output = ''. t('Downloads') .'
';
$view_args = array($node->nid);
$displays = array(
'attachment_1' => array(
'class' => 'ok',
'header' => t('Recommended releases'),
),
'attachment_2' => array(
'class' => 'warning',
'header' => t('Other releases'),
),
'attachment_3' => array(
'class' => 'error',
'header' => t('Development releases'),
),
);
$number_of_tables = 0;
$views_output = array();
foreach ($displays as $display => $info) {
$view = views_get_view('project_release_download_table');
$view_output = $view->preview($display, $view_args);
if (!empty($view->result)) {
$views_output[$display] = $view_output;
$number_of_tables++;
}
}
if ($number_of_tables > 0) {
foreach ($displays as $display => $info) {
if (!empty($views_output[$display])) {
$classes = 'download-table download-table-' . $info['class'];
$output .= '';
if ($number_of_tables > 1) {
$output .= '
' . $info['header'] . "
\n";
}
$output .= $views_output[$display];
$output .= " \n";
}
}
}
return $output;
}
/**
* Implemenation of hook_project_page_link_alter().
*
* Note: This is *not* an implementation of hook_link_alter().
*/
function project_release_project_page_link_alter(&$links, $node) {
if (empty($node->project_release['releases'])) {
return;
}
$links['project_release'] = array(
// NOTE: The 'name' element of this array is not defined here because
// it's actually printed as part of the output of the
// theme_project_release_project_download_table() function above.
'weight' => 2,
'clear' => TRUE,
'links' => array(
'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'))
),
);
if (project_user_access($node->nid, 'administer releases')) {
$links['project_release']['links']['add_new_release'] = l(t('Add new release'), 'node/add/project_release/'. $node->nid);
$links['project_release']['links']['administer_releases'] = l(t('Administer releases'), 'node/'. $node->nid .'/edit/releases');
}
}
/**
* Theme function that calls project_release_table().
*
* The main purpose of this theme wrapper function is to make it easier
* to display a different kind of table (for example, $tabel_type=all)
* from the project_page_overview() function in project.module.
*
* The parameters are described at project_release_table().
*
* @see project_page_overview()
* @see project_release_table()
*/
function theme_project_release_table_overview($project, $table_type, $release_type, $title, $print_size) {
return project_release_table($project, $table_type, $release_type, $title, $print_size);
}
/**
* Generate a table of releases for a given project.
*
* @param $project
* The project object (as returned by node_load(), for example).
*
* @param $table_type
* Indicates what kind of table should be generated. Possible options:
* 'recommended': Only show the current recommended versions.
* 'supported': Only show the latest release from each supported branch.
* 'all': Include all releases.
*
* @param $release_type
* Filter what kinds of releases are visible in the table. Possible options:
* 'official': Only include offical releases.
* 'snapshot': Only include development snapshots.
* 'all': Include all releases.
*
* @param $title
* The title of the first column in the table. Defaults to "Version" if NULL.
*
* @param $print_size
* Should the table include the filesize of each release?
*
* @param $check_edit
* Should the table check for and include edit links to user with access?
*/
function project_release_table($project, $table_type = 'recommended', $release_type = 'all', $title = NULL, $print_size = TRUE, $check_edit = TRUE) {
if (empty($title)) {
$title = t('Version');
}
// Can the current user edit releases for this project?
$can_edit = $check_edit ? node_access('update', $project) : FALSE;
// Generate the cache ID.
$cid = 'table:'. $project->nid .':'. $table_type .':'. $release_type .':'. $title .':'. (int)$print_size .':'. (int)$can_edit;
if ($cached = cache_get($cid, 'cache_project_release')) {
return $cached->data;
}
$selects = array();
$join = $where = $order_by = '';
$args = array();
$tids = project_release_compatibility_list();
if (!empty($tids)) {
$join = ' INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid in ('
. db_placeholders($tids) .') '
.' INNER JOIN {term_data} td ON td.tid = tn.tid ';
$args = array_keys($tids);
$selects[] = 'tn.tid';
$selects[] = 'td.name as api_term_name';
$orderby[] = 'td.weight';
$orderby[] = 'td.name';
}
if ($tids) {
$selects[] = 'prsv.supported';
$selects[] = 'prsv.recommended';
$selects[] = 'prsv.snapshot';
$join .= ' INNER JOIN {project_release_supported_versions} prsv ON prsv.nid = r.pid AND prsv.tid = tn.tid AND prsv.major = r.version_major ';
if ($table_type == 'recommended') {
$join .= 'AND prsv.recommended = %d ';
$args[] = 1;
}
elseif ($table_type == 'supported') {
$join .= 'AND prsv.supported = %d ';
$args[] = 1;
}
}
else {
// TODO: someday (never?) when project_release doesn't require taxonomy.
}
$args[] = $project->nid; // Account for r.pid.
$args[] = 1; // Account for n.status = 1.
switch ($release_type) {
case 'official':
$where = 'AND r.rebuild <> %d';
$args[] = 1;
break;
case 'snapshot':
// For snapshot tables, restrict to snapshot nodes from branches where
// the maintainer wants the snapshot visible.
$where = 'AND r.rebuild = %d';
$args[] = 1;
if ($tids) {
$where .= ' AND prsv.snapshot = %d';
$args[] = 1;
}
break;
case 'all':
// If we're generating the default releases table, we want the
// dev snapshots to be last in the query results, so that we
// only show them if there's nothing else.
if ($table_type == 'recommended') {
$orderby[] = 'r.rebuild ASC';
}
break;
}
$orderby[] = 'r.version_major DESC';
$orderby[] = 'r.version_minor DESC';
$orderby[] = 'r.version_patch DESC';
$orderby[] = 'f.timestamp DESC';
$order_by = !empty($orderby) ? (' ORDER BY '. implode(', ', $orderby)) : '';
$select = !empty($selects) ? (implode(', ', $selects) .',') : '';
// TODO: we MUST rewrite this query when multiple files attachments
// per release node lands, as it will return a non-unique result set.
$result = db_query(db_rewrite_sql(
"SELECT n.nid, n.created, f.filename, f.filepath, f.timestamp, ".
"f.filesize, $select r.* FROM {node} n ".
"INNER JOIN {project_release_nodes} r ON r.nid = n.nid ".
"INNER JOIN {project_release_file} prf ON n.nid = prf.nid ".
"INNER JOIN {files} f ON prf.fid = f.fid$join ".
"WHERE (r.pid = %d) AND (n.status = %d) $where $order_by"),
$args);
$rows = array(); // Rows for the download table.
$seen = array(); // Keeps track of which versions we already saw.
while ($release = db_fetch_object($result)) {
$tid = $release->tid;
$major = $release->version_major;
$recommended = false;
if ($table_type == 'supported') {
// Supported version can be multiple majors per tid.
if (empty($seen[$tid])) {
$seen[$tid] = array();
}
if (empty($seen[$tid][$major])) {
$seen[$tid][$major] = 1;
if ($release->recommended) {
$recommended = true;
}
}
else {
// We already know the supported release for this tid/major, go on.
continue;
}
}
else {
if (empty($seen[$tid])) {
// Only one major per tid, so the row lives here.
$seen[$tid] = 1;
if ($release->recommended) {
$recommended = true;
}
}
elseif ($table_type == 'recommended') {
// We already know the recommended release for this tid and that's all
// we want in the table, so skip this release.
continue;
}
}
// If we're still here, we need to add the row to the table.
$rows[] = theme('project_release_download_table_row', $release, $recommended, $can_edit, $print_size);
}
$header = array(
array(
'class' => 'release-title',
'data' => $title,
),
array(
'class' => 'release-date',
'data' => t('Date'),
),
);
if ($print_size) {
$header[] = array(
'class' => 'release-size',
'data' => t('Size'),
);
}
$header[] = array(
'class' => 'release-links',
'data' => t('Links'),
);
$header[] = array(
'class' => 'release-status',
'data' => t('Status'),
'colspan' => 2,
);
$output = '';
if (!empty($rows)) {
$output = theme('table', $header, $rows, array('class' => 'releases'));
}
// Default cache time is 12 hours - will be cleared by the packaging script
cache_set($cid, $output, 'cache_project_release', time() + 43200);
return $output;
}
/**
* Helper function to return an individual row for the download table.
*
* @param $release
* The release object queried from the database. Since this is NOT a
* fully-loaded $node object, so the release-related fields are not in a
* 'project_release' sub-array.
* @param $recommended
* Boolean indicating if this release is the currently recommended one.
* @param $can_edit
* Boolean indicating if the current user can edit the release.
* @param $print_size
* Boolean indicating if the size of the download should be printed.
*/
function theme_project_release_download_table_row($release, $recommended = false, $can_edit = false, $print_size = true) {
static $icons = array();
if (empty($icons)) {
$icons = array(
'ok' => 'misc/watchdog-ok.png',
'warning' => 'misc/watchdog-warning.png',
'error' => 'misc/watchdog-error.png',
);
}
$links = array();
if (!empty($release->filepath)) {
$links['project_release_download'] = theme('project_release_download_link', $release->filepath, t('Download'), TRUE);
}
$links['project_release_notes'] = array(
'title' => t('Release notes'),
'href' => "node/$release->nid",
);
if ($can_edit) {
$links['project_release_edit'] = array(
'title' => t('Edit'),
'href' => "node/$release->nid/edit",
);
}
// Figure out the class for the table row
$row_class = $release->rebuild ? 'release-dev' : 'release';
// Now, set the row color and help text, based on the release attributes.
if (!$release->supported) {
$text = theme('project_release_download_text_unsupported', $release, 'summary');
$message = theme('project_release_download_text_unsupported', $release, 'message');
$classification = 'error';
}
elseif ($release->rebuild) {
$reason = theme('project_release_download_text_snapshot', $release, 'summary');
$message = theme('project_release_download_text_snapshot', $release, 'message');
$classification = 'error';
}
elseif ($recommended) {
$reason = theme('project_release_download_text_recommended', $release, 'summary');
$message = theme('project_release_download_text_recommended', $release, 'message');
$classification = 'ok';
}
else {
// Supported, but not recommened, official release.
$reason = theme('project_release_download_text_supported', $release, 'summary');
$message = theme('project_release_download_text_supported', $release, 'message');
$classification = 'warning';
}
$row = array(
// class of
'class' => $row_class .' '. $classification,
'data' => array(
array(
'class' => 'release-title',
'data' => l($release->version, "node/$release->nid"),
),
array(
'class' => 'release-date',
'data' => !empty($release->filepath) ? format_date($release->timestamp, 'custom', 'Y-M-d') : format_date($release->created, 'custom', 'Y-M-d'),
),
),
);
if ($print_size) {
$row['data'][] = array(
'class' => 'release-size',
'data' => !empty($release->filepath) ? format_size($release->filesize) : t('n/a'),
);
}
$row['data'][] = array(
'class' => 'release-links',
'data' => theme('links', $links),
);
$row['data'][] = array(
'class' => 'release-reason',
'data' => $reason,
);
$row['data'][] = array(
'class' => 'release-icon',
'data' => theme('image', $icons[$classification], $message, $message),
);
return $row;
}
/**
* Return the message text for recommended releases in the download table.
*
* @param $release
* The release object queried from the database. Since this is NOT a
* fully-loaded $node object, so the release-related fields are not in a
* 'project_release' sub-array.
* @param $text_type
* What kind of text to render. Can be either 'summary' for the summary
* text to include directly on the project node, or 'message' for the text
* to put in the title and alt attributes of the icon.
*/
function theme_project_release_download_text_recommended($release, $text_type) {
if ($text_type == 'summary') {
return t('Recommended for %api_term_name', array('%api_term_name' => $release->api_term_name));
}
return t('This is currently the recommended release for @api_term_name.', array('@api_term_name' => $release->api_term_name));
}
/**
* Return the message text for supported releases in the download table.
*
* @see theme_project_release_download_text_recommended
*/
function theme_project_release_download_text_supported($release, $text_type) {
if ($text_type == 'summary') {
return t('Supported for %api_term_name', array('%api_term_name' => $release->api_term_name));
}
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));
}
/**
* Return the message text for snapshot releases in the download table.
*
* @see theme_project_release_download_text_recommended
*/
function theme_project_release_download_text_snapshot($release, $text_type) {
if ($text_type == 'summary') {
return t('Development snapshot');
}
return t('Development snapshots are automatically regenerated and their contents can frequently change, so they are not recommended for production use.');
}
/**
* Return the message text for snapshot releases in the download table.
*
* @see theme_project_release_download_text_recommended
*/
function theme_project_release_download_text_unsupported($release, $text_type) {
if ($text_type == 'summary') {
return t('Unsupported');
}
return t('This release is not supported and may no longer work.');
}
/**
* Implementation of hook_taxonomy().
*/
function project_release_taxonomy($op, $type, $array = NULL) {
if ($op == 'delete' && $type == 'vocabulary') {
if ($array['vid'] == _project_release_get_api_vid()) {
variable_del('project_release_api_vocabulary');
}
elseif ($array['vid'] == _project_release_get_release_type_vid()) {
variable_del('project_release_release_type_vid');
}
}
elseif ($type == 'term' && $array['vid'] == _project_release_get_api_vid()) {
menu_rebuild();
}
}
/**
* If taxonomy is enabled, returns the taxonomy tree for the
* API compatibility vocabulary, otherwise, it returns false.
*/
function project_release_get_api_taxonomy() {
if (!module_exists('taxonomy')) {
return false;
}
static $tree = NULL;
if (!isset($tree)) {
$tree = taxonomy_get_tree(_project_release_get_api_vid());
}
return $tree;
}
/**
* Returns the vocabulary id for project release API
*/
function _project_release_get_api_vid() {
return variable_get('project_release_api_vocabulary', '');
}
/**
* Return the taxonomy tree for the release type vocabulary (if any).
*
* If taxonomy is disabled, this returns false.
*/
function project_release_get_release_type_vocabulary() {
if (!module_exists('taxonomy')) {
return false;
}
static $tree = NULL;
if (!isset($tree)) {
$tree = taxonomy_get_tree(_project_release_get_release_type_vid());
}
return $tree;
}
/**
* Return the vocabulary id for project release type.
*/
function _project_release_get_release_type_vid() {
return variable_get('project_release_release_type_vid', '');
}
/**
* Determine if a release already exists with the given version.
*
* @param stdClass $version
* An object containing fields that define the version for a release. Must
* include 'pid', the project node ID. Can also include 'version_api_tid',
* 'version_major', 'version_minor', 'version_patch', 'version_extra',
* and/or 'version' (the full version string itself).
*
* @return integer
* The node ID of an existing release with the given version information, or
* FALSE if no such release already exists.
*/
function project_release_exists($version) {
$values = array();
$fields = array('version_major', 'version_minor', 'version_patch', 'version_api_tid');
foreach ($fields as $field) {
if (isset($version->$field) && is_numeric($version->$field)) {
$types[$field] = "%d";
$values[$field] = $version->$field;
$foo = $version->$field;
}
else {
$null_types[] = $field;
}
}
$fields = array('version', 'version_extra');
foreach ($fields as $field) {
if (isset($version->$field) && $version->$field !== '') {
$types[$field] = "'%s'";
$values[$field] = $version->$field;
$str = $version->$field;
}
elseif ($field == 'version_extra') {
$null_types[] = $field;
}
}
if (empty($types) && empty($null_types)) {
// We have nothing to query, yet...
return false;
}
$sql = 'SELECT nid FROM {project_release_nodes} WHERE pid = %d';
if (!empty($types)) {
foreach ($types as $field => $type) {
$sql .= " AND $field = $type";
}
}
if (!empty($null_types)) {
foreach ($null_types as $field) {
$sql .= " AND $field IS NULL";
}
}
// we put pid as the first WHERE, so stick it on the front
$values = array_merge(array('pid' => $version->pid), $values);
return db_result(db_query($sql, $values));
}
/**
* Generates the appropriate download link for a give file path. This
* function takes the 'project_release_download_base' setting into
* account, so it should be used everywhere a download link is made.
*
* @param $filepath
* The path to the download file, as stored in the database.
* @param $link_text
* The text to use for the download link. If NULL, the basename
* of the given $filepath is used.
* @param $as_array
* Should the link be returned as a structured array, or as raw HTML?
* @return
* The link itself, as a structured array.
*/
function theme_project_release_download_link($filepath, $link_text = NULL, $as_array = FALSE) {
if (empty($link_text)) {
$link_text = basename($filepath);
}
$download_base = variable_get('project_release_download_base', '');
if (!empty($download_base)) {
$link_path = $download_base . $filepath;
}
else {
$link_path = file_create_url($filepath);
}
if ($as_array) {
return array(
'title' => $link_text,
'href' => $link_path,
);
}
else {
return l($link_text, $link_path);
}
}
/**
* Implementation of hook_file_download().
*
* @param $filename
* The name of the file to download.
* @return
* An array of header fields for the download.
*/
function project_release_file_download($filename) {
$filepath = file_create_path($filename);
$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);
if ($nid = db_result($result)) {
$node = node_load($nid);
if (node_access('view', $node)) {
return array(
'Content-Type: application/octet-stream',
'Content-Length: '. filesize($filepath),
'Content-Disposition: attachment; filename="'. mime_header_encode($filename) .'"',
);
}
return -1;
}
}
/**
* Implementation of hook_flush_caches().
*/
function project_release_flush_caches() {
return array('cache_project_release');
}
/**
* Menu callback to select a project when creating a new release.
*/
function project_release_pick_project_page($type_name) {
drupal_set_title(t('Submit @name', array('@name' => $type_name)));
$project = arg(3);
if (!empty($project)) {
// If there's any argument at all and we hit this form, it's from a
// non-numeric project id, which by definition is invalid. No one's ever
// going to hit this code from clicking around in the normal UI, only if
// they type in a URL manually.
drupal_set_message(t('Specified argument (%project) is not a valid project ID number.', array('%project' => $project)), 'error');
return drupal_goto('/node/add/project-release');
}
return drupal_get_form('project_release_pick_project_form');
}
/**
* Implementation of hook_theme().
*/
function project_release_theme() {
return array(
'project_release_download_file' => array(
'arguments' => array(
'file' => NULL,
'download_link' => TRUE,
),
),
'project_release_download_link' => array(
'arguments' => array(
'filepath' => NULL,
'link_text' => NULL,
'as_array' => FALSE,
),
),
'project_release_download_table_row' => array(
'arguments' => array(
'release' => NULL,
'recommended' => FALSE,
'can_edit' => FALSE,
'print_size' => TRUE,
),
),
'project_release_download_text_recommended' => array(
'arguments' => array(
'release' => NULL,
'text_type' => NULL,
),
),
'project_release_download_text_snapshot' => array(
'arguments' => array(
'release' => NULL,
'text_type' => NULL,
),
),
'project_release_download_text_supported' => array(
'arguments' => array(
'release' => NULL,
'text_type' => NULL,
),
),
'project_release_download_text_unsupported' => array(
'arguments' => array(
'release' => NULL,
'text_type' => NULL,
),
),
'project_release_form_value' => array(
'file' => 'includes/release_node_form.inc',
'arguments' => array(
'element' => NULL,
),
),
'project_release_project_download_table' => array(
'arguments' => array(
'node' => NULL,
),
),
'project_release_project_edit_form' => array(
'file' => 'includes/release_node_form.inc',
'arguments' => array(
'form' => NULL,
),
),
'project_release_table_overview' => array(
'arguments' => array(
'project' => NULL,
'table_type' => NULL,
'release_type' => NULL,
'title' => NULL,
'print_size' => NULL,
),
),
'project_release_node_form_version_elements' => array(
'arguments' => array(
'form' => NULL,
),
),
'project_release_update_status_icon' => array(
'arguments' => array(
'status' => NULL,
),
),
);
}
function theme_project_release_node_form_version_elements($form) {
$output = '';
$output .= drupal_render($form);
$output .= '
';
return $output;
}
/**
* Implement hook_token_list() (from token.module)
*/
function project_release_token_list($type) {
if ($type == 'node') {
$tokens['node'] = array(
'project_release_pid' => t("A release's project nid"),
'project_release_project_title' => t("A release's project title"),
'project_release_project_title-raw' => t("A release's project title raw"),
'project_release_project_shortname' => t("A release's project short name"),
'project_release_version' => t("A release's version string"),
'project_release_version_major' => t("A release's major version number"),
'project_release_version_minor' => t("A release's minor version number"),
'project_release_version_patch' => t("A release's patch version number"),
'project_release_version_extra' => t("A release's extra version identifier"),
);
if (project_release_compatibility_list()) {
$vocab = taxonomy_vocabulary_load(_project_release_get_api_vid());
$tokens['node']['project_release_version_api_tid'] = t("A release's %api_compatibility term ID", array('%api_compatibility' => $vocab->name));
$tokens['node']['project_release_version_api_term'] = t("A release's %api_compatibility term name", array('%api_compatibility' => $vocab->name));
}
return $tokens;
}
}
/**
* Implement hook_token_values() (from token.module).
*/
function project_release_token_values($type = 'all', $object = NULL) {
if ($type == 'node') {
// Defaults in case it's not a release or we run into other problems.
$values = array(
'project_release_pid' => '',
'project_release_project_title' => '',
'project_release_project_title-raw' => '',
'project_release_project_shortname' => '',
'project_release_version' => '',
'project_release_version_major' => '',
'project_release_version_minor' => '',
'project_release_version_patch' => '',
'project_release_version_extra' => '',
'project_release_version_api_tid' => '',
'project_release_version_api_term' => '',
);
if ($object->type == 'project_release') {
if ($project = node_load($object->project_release['pid'])) {
$values['project_release_pid'] = intval($object->project_release['pid']);
$values['project_release_project_title'] = check_plain($project->title);
$values['project_release_project_title-raw'] = $project->title;
$values['project_release_project_shortname'] = check_plain($project->project['uri']);
}
$values['project_release_version'] = check_plain($object->project_release['version']);
$values['project_release_version_major'] = check_plain($object->project_release['version_major']);
$values['project_release_version_minor'] = check_plain($object->project_release['version_minor']);
$values['project_release_version_patch'] = check_plain($object->project_release['version_patch']);
$values['project_release_version_extra'] = check_plain($object->project_release['version_extra']);
if (!empty($object->project_release['version_api_tid'])) {
$term = taxonomy_get_term($object->project_release['version_api_tid']);
$values['project_release_version_api_tid'] = check_plain($term->tid);
$values['project_release_version_api_term'] = check_plain($term->name);
}
}
return $values;
}
}
/**
* Determines taxonomy-specific functionality for releases.
*/
function project_release_use_taxonomy() {
return module_exists('taxonomy') && _project_release_get_api_vid();
}
/**
* Implementation of hook_help().
*/
function project_release_help($section) {
switch ($section) {
case 'admin/project/project-release-settings':
if (project_release_use_taxonomy()) {
return _project_release_taxonomy_help();
}
break;
}
if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'taxonomy') {
$vid = _project_release_get_api_vid();
if (arg(3) == $vid) {
return _project_release_taxonomy_help($vid, FALSE);
}
}
}
/**
* Prints help message for release compatibility vocabulary configuration.
*
* @param $vid
* Vocabulary ID of the project taxonomy.
* @param $vocab_link
* Boolean that controls if a link to the vocabulary admin page is added.
*/
function _project_release_taxonomy_help($vid = 0, $vocab_link = TRUE) {
if (!$vid) {
$vid = _project_release_get_api_vid();
}
if (empty($vid)) {
return;
}
$vocabulary = taxonomy_vocabulary_load($vid);
$text = ''. 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)) .'
';
$text .= ''. 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".') .'
';
$text .=''. t('For more information, please see !url.', array('!url' => l('http://drupal.org/node/116544', 'http://drupal.org/node/116544'))) .'
';
if ($vocab_link) {
$text .= ''. t('Use the vocabulary admininistration page to view and add terms.', array('@taxonomy-admin' => url('admin/content/taxonomy/'. $vid))) .'
';
}
return $text;
}
/**
* Implementation of hook_views_api().
*/
function project_release_views_api() {
return array(
'api' => 2,
'path' => drupal_get_path('module', 'project_release') .'/views',
);
}
/**
* Return the mapping of version_extra prefixes to version_extra_weight values.
*
* This mapping allows project_release to use SQL to sort releases by version,
* even though direct string comparison doesn't work for the kinds of version
* strings people might use (for example "1.0-unstable1" should be lower than
* "1.0-alpha3", even though "u" comes higher in the alphabet than "a"). This
* is similar to the logic version_compare() performs, only using this weight
* field, we can do the comparison in SQL instead of in PHP.
*
* @return
* Associative array mapping version_extra prefixes into weights. The
* prefixes should be lowercase, since the query uses LOWER(version_extra)
* inside _project_release_update_version_extra_weights(). The special-case
* is the record with the key 'NULL' (should be uppercase) which doesn't
* correspond to a literal version_extra field, but is used for releases
* that do not define version_extra where the value is NULL in the database.
*
* @see version_compare()
* @see _project_release_update_version_extra_weights()
*/
function project_release_get_version_extra_weight_map() {
$default_map = array(
'NULL' => 10, // Official releases without extra are always highest.
'rc' => 4,
'beta' => 3,
'alpha' => 2,
'unstable' => 1,
// Anything that doesn't match will remain at weight 0, the default.
);
return variable_get('project_release_version_extra_weights', $default_map);
}
/**
* Get the human-readable update status string, or an array of all statuses.
*
* @param $status
* Optional status code to get the human-readable string for. If NULL, the
* whole mapping of status codes to strings is returned.
*
* @return
* If $status is defined, the human-readable string for that status,
* otherwise, an associative array of status strings keyed by status code.
*/
function project_release_update_status($status = NULL) {
$status_map = array(
PROJECT_RELEASE_UPDATE_STATUS_CURRENT => t('Up to date'),
PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT => t('Update available'),
PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE => t('Not secure'),
);
return isset($status) ? $status_map[$status] : $status_map;
}
/**
* Render HTML for an icon approrpriate for the given release update status.
*
* @param $status
* Update status code to get the icon for.
*
* @return
* Icon to use for the given update status code.
*/
function theme_project_release_update_status_icon($status) {
$label = project_release_update_status($status);
$icon = '';
switch ($status) {
case PROJECT_RELEASE_UPDATE_STATUS_CURRENT:
$icon = theme('image', 'misc/watchdog-ok.png', $label, $label);
break;
case PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT:
$icon = theme('image', 'misc/watchdog-warning.png', $label, $label);
break;
case PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE:
$icon = theme('image', 'misc/watchdog-error.png', $label, $label);
break;
}
return $icon;
}
/**
* Implement hook_preprocess_views_view_table().
*
* Handles the logic for conditionally adding row classes based on release
* update_status, and has a hack for hiding the update_status column entirely
* on the project_release_download_table view if there's nothing to see.
*/
function project_release_preprocess_views_view_table($variables) {
$view = $variables['view'];
if ($view->plugin_name == 'project_release_table') {
// TODO: this is a hack, we want something more flexible.
$needs_status_column = FALSE;
foreach ($view->result as $num => $result) {
$variables['row_classes'][$num][] = "release-update-status-$result->project_release_nodes_update_status";
if (!empty($variables['rows'][$num]['update_status'])) {
$needs_status_column = TRUE;
}
}
if ($view->name == 'project_release_download_table' && !$needs_status_column) {
unset($variables['header']['update_status']);
foreach ($variables['rows'] as &$row) {
unset($row['update_status']);
}
}
$variables['class'] .= " project-release";
}
}
/**
* Implement hook_views_default_views_alter().
*/
function project_release_views_default_views_alter(&$views) {
$path = drupal_get_path('module', 'project_release');
require_once("$path/views/project_release.views_default.inc");
_project_release_views_default_views_alter($views);
}