'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 .= ''; } $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); }