type == 'project_project') { $items[] = array( 'path' => 'node/'. arg(1) .'/release', 'title' => t('Releases'), 'callback' => 'project_release_project_releases', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK, ); if (node_access('update', $node)) { $items[] = array( 'path' => 'node/'. arg(1) .'/release/overview', 'title' => t('Overview'), 'callback' => 'project_release_project_releases', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK, ); $items[] = array( 'path' => 'node/'. arg(1) .'/edit/releases', 'title' => t('Releases'), 'callback' => 'project_release_project_edit_releases', 'type' => MENU_LOCAL_TASK, ); } } } if (arg(0) == 'node' && arg(1) == 'add' && arg(2) == 'project-release' && is_numeric(arg(3))) { $items[] = array( 'path' => 'node/add/project-release/'. arg(3), 'callback' => 'node_add', 'callback arguments' => array('project-release'), 'access' => $access_create, 'type' => MENU_CALLBACK, ); } drupal_add_css(drupal_get_path('module', 'project_release') .'/project_release.css'); project_release_get_api_taxonomy(); } else { // $may_cache $items[] = array( 'path' => 'admin/project/project-release-settings', 'description' => t('Configure the default version string for releases and other settings for the Project release module.'), 'title' => t('Project release settings'), 'callback' => 'drupal_get_form', 'callback arguments' => 'project_release_settings_form', 'access' => user_access('administer projects'), 'weight' => 1, 'type' => MENU_NORMAL_ITEM, ); // Special menu item for the "first page" of submitting a new release. // Instead of the treachery of a true multipage form, we just have // a simple form at node/add/project-release that provides a project // selector which redirects to node/add/project-release/[project-nid]. $items[] = array( 'path' => 'node/add/project-release', 'title' => node_get_types('name', 'project_release'), 'callback' => 'project_release_pick_project_page', 'callback arguments' => array($types['project_release']->name), 'access' => $access_create, 'type' => MENU_NORMAL_ITEM, ); // Redirect node/add/project_release/* to node/add/project-release. $items[] = array( 'path' => 'node/add/project_release', 'callback' => 'project_release_add_redirect_page', 'access' => $access_create, 'type' => MENU_CALLBACK, ); } return $items; } /** * Redirect node/add/project_release/* to node/add/project-release/*. */ function project_release_add_redirect_page() { $arg = arg(3); drupal_goto('node/add/project-release/' . (empty($arg) ? '' : $arg)); } /** * Callback for the main settings page. * @ingroup project_release_core */ function project_release_settings_form() { if ($rel_dir = variable_get('project_release_directory', '')) { $form['project_release_directory'] = array( '#type' => 'textfield', '#title' => t('Release directory'), '#default_value' => $rel_dir, '#size' => 50, '#maxlength' => 255, '#description' => t('This setting has been temporarily deprecated. If your site depends on scanning for releases generated by an external tool, you should wait to upgrade until a future version of the project_release.module is available that restores this functionality. Set the value blank to acknowlege that you do not need this behavior, and this error will disappear.'), ); } $form['project_release_default_version_format'] = array( '#type' => 'textfield', '#title' => t('Default version format string'), '#default_value' => variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT), '#size' => 50, '#maxlength' => 255, '#description' => t('Customize the default format of the version strings for releases of projects on this site. Users with "administer projects" permissions can override this setting for each project.') .' '. PROJECT_RELEASE_VERSION_FORMAT_HELP, ); $form['project_release_file_extensions'] = array( '#type' => 'textfield', '#title' => t('Permitted file extensions'), '#default_value' => variable_get('project_release_file_extensions', PROJECT_RELEASE_FILE_EXTENSIONS), '#size' => 50, '#maxlength' => 255, '#description' => t('Files uploaded to release nodes will only be allowed if they have an extension that is included in this list.'), ); if ($tree = project_release_get_api_taxonomy()) { foreach ($tree as $term) { $terms[$term->tid] = check_plain($term->name); } $vocab = taxonomy_get_vocabulary(_project_release_get_api_vid()); $tids = variable_get('project_release_active_compatibility_tids', array()); $form['project_release_active_compatibility_tids'] = array( '#type' => 'checkboxes', '#title' => t('Active @vocab terms', array('@vocab' => $vocab->name)), '#default_value' => $tids, '#options' => $terms, '#description' => t('Terms from the %vocab vocabulary that should be visibile to end users and project maintainers.', array('%vocab' => $vocab->name)), ); // TODO: put these 2 in a fieldset? $terms = array(-1 => t('all')) + $terms; $form['project_release_browse_versions'] = array( '#type' => 'checkbox', '#title' => t('Browse projects by release versions'), '#default_value' => variable_get('project_release_browse_versions', 0), '#description' => t('Checking this box will cause the project browsing page to have a version select.'), ); $form['project_release_overview'] = array( '#type' => 'radios', '#title' => t('Default release overview'), '#default_value' => variable_get('project_release_overview', -1), '#options' => $terms, '#required' => TRUE, '#description' => t('Default release version to list on the overview page.'), ); } $form['project_release_download_base'] = array( '#type' => 'textfield', '#title' => t('Download link base URL'), '#default_value' => variable_get('project_release_download_base', ''), '#size' => 50, '#maxlength' => 255, '#description' => t("By default, all download links to releases will use the standard download path for the site. However, if you wish to host the downloads at a different location, you can specify the base of the URL that should be used for download links. For example, if you stored releases in %files_path and you want to have download links pointing to %ftp_url, you would set this to %ftp_setting. Note that if you define this, the value should end with a slash ('/').", array('%files_path' => 'files/projects/foo.tar.gz', '%ftp_url' => 'ftp://ftp.example.com/files/projects/foo.tar.gz', '%ftp_setting' => 'ftp://ftp.example.com/')), ); return system_settings_form($form); } function project_release_settings_form_validate($form_id, $form_values, $form) { if (!empty($form_values['project_release_directory'])) { form_set_error('project_release_directory', t('Release directory setting has been deprecated.')); } $tids = $form_values['project_release_active_compatibility_tids']; $default_tid = $form_values['project_release_overview']; if ($default_tid != -1 && !$tids[$default_tid]) { $vocab = taxonomy_get_vocabulary(_project_release_get_api_vid()); form_set_error('project_release_overview', t('Project release overview must be one of the active @vocab terms.', array('@vocab' => $vocab->name))); } // Make sure the default version format has no bad characters. _project_release_validate_format_string($form_values, 'project_release_default_version_format'); // If set, the project_release_download_base must end with a '/' if (!empty($form_values['project_release_download_base'])) { if (substr($form_values['project_release_download_base'], -1) != '/') { form_set_error('project_release_download_base', t('The Download link base URL should end with a slash.')); } } } /** * @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) { global $user; 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); 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); 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_check_admin_access($node->pid); 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.'), ), ); } /** * Implementation of hook_form(). * @ingroup project_release_node */ function project_release_form($release) { if (arg(1) == 'add') { $release->pid = (integer) arg(3); $project = node_load($release->pid); project_project_set_breadcrumb($project, TRUE); if (!isset($project) || $project->type != 'project_project') { drupal_set_message(t('Node %nid is not a valid project.', array('%nid' => $release->pid)), 'error'); drupal_goto('node/add/project-release'); } // Make sure this user should have permissions to add releases for // the requested project if (!project_check_admin_access($project)) { drupal_access_denied(); module_invoke_all('exit'); exit; } $form['project'] = array( '#type' => 'value', '#value' => $project, ); $format = project_release_get_version_format($project); } else { global $user; $admin = user_access('administer projects'); $is_edit = true; $project->nid = $release->pid; $project = node_load($project); $breadcrumb[] = l($project->title, 'node/'. $project->nid); $breadcrumb[] = l(t('Releases'), 'node/'. $project->nid . '/release'); project_project_set_breadcrumb($project, $breadcrumb); $format = project_release_get_version_format($project); } $form['#attributes'] = array("enctype" => "multipart/form-data"); $form['#prefix'] = '
'; $form['#suffix'] = '
'; if ($is_edit) { $form['rel_id'] = array( '#type' => 'fieldset', '#title' => t('Release identification'), '#prefix' => '
', '#suffix' => '
', '#weight' => -4, '#collapsible' => TRUE, ); } _project_release_form_add_text_element($form['rel_id']['title'], t('Title'), $release->title, $is_edit, $admin, TRUE, 40, 128); _project_release_form_add_text_element($form['rel_id']['version'], t('Version string'), $release->version, $is_edit, $admin, TRUE, 20, 255); $form['pid'] = array( '#type' => 'value', '#value' => $release->pid, ); $form['version']['num'] = array( '#type' => 'fieldset', '#title' => t('Version number elements'), '#collapsible' => TRUE, '#prefix' => '
', '#suffix' => '
', ); $modify = $admin || !$is_edit; $form['validate_version'] = array('#type' => 'hidden', '#value' => 1); _project_release_form_add_version_element($form, $release, $modify, $format, 'major', t('Major')); _project_release_form_add_version_element($form, $release, $modify, $format, 'minor', t('Minor')); _project_release_form_add_version_element($form, $release, $modify, $format, 'patch', t('Patch-level')); _project_release_form_add_version_element($form, $release, $modify, $format, 'extra', t('Extra identifier'), t('Optionally specify other identifying information for this version, for example "beta-1", "rc-1" or "dev". In most cases, this should be left blank.'), 40); $form['file'] = array( '#type' => 'fieldset', '#title' => t('File information'), '#collapsible' => TRUE, ); if (empty($release->file_path)) { $file = file_check_upload('file'); $form['file']['file'] = array( '#title' => t('File'), '#type' => 'file', '#description' => ($file ? t('A file named %filename has already been uploaded. If you upload another file %filename will be replaced.', array('%filename' => $file->filename)) : t('Choose the file that will be associated with this release.')), ); } $form['body_filter'] = array( '#type' => 'fieldset', '#title' => t('Description'), '#collapsible' => TRUE, ); $form['body_filter']['body'] = array( '#title' => t('Body'), '#type' => 'textarea', '#default_value' => $release->body, '#rows' => 10, '#cols' => 40, '#required' => $admin ? FALSE : TRUE, '#description' => t('Enter a description of this release, such as a list of the major changes or updates.'), ); $form['body_filter']['format'] = filter_form($release->format); _project_release_form_add_text_element($form['tag']['tag'], t('Tag'), $release->tag, $is_edit, $admin, TRUE, 40, 255); $form['tag']['rebuild'] = array( '#type' => 'value', '#value' => $release->rebuild, ); _project_release_form_add_text_element($form['file']['file_path'], t('File path'), $release->file_path, $is_edit, $admin, FALSE, 40, 255); _project_release_form_add_text_element($form['file']['file_hash'], t('File md5 hash'), $release->file_hash, $is_edit, FALSE); _project_release_form_add_text_element($form['file']['file_date'], t('File date'), $release->file_date, $is_edit, FALSE); return $form; } /** * @defgroup project_release_internal Internal module functions */ /** * Modifies the given $form array to add the appropriate form element * for the requested version field. Since the 20+ lines of code in * here have to be duplicated 6 times in project_release_form(), this * function exists so we can reuse the code. * @see project_release_form * @ingroup project_release_internal * * @param $form Form array to modify * @param $release Relase node the form is for * @param $modify Boolean indicating if we should allow modifications * @param $format Version format string for this project * @param $name Name of this version element * @param $title Translatable title of the form element * @param $description Translatable description of the form element. * @param $size Size of the form element * @param $required Boolean for if the form element should be required */ function _project_release_form_add_version_element(&$form, $release, $modify, $format, $name, $title, $description = '', $size = 10, $required = FALSE) { $var_name = 'version_'. $name; $regexp = "@.*[!#%]$name.*@"; if (preg_match($regexp, $format)) { $form['version']['num'][$var_name] = array( '#type' => 'textfield', '#title' => $title, '#default_value' => $release->$var_name, '#size' => $size, '#maxlength' => $size+10, '#attributes' => array('style' => 'width:auto'), ); if ($required) { // TODO: handle this more flexibly for sites not using CVS // perhaps if the format variable is UPPERCASE it's required, // and lowercase is optional or something crazy? $form[$var_name]['#required'] = TRUE; } if ($description) { $form['version']['num'][$var_name]['#description'] = $description; } if (!$modify) { $form['version']['num'][$var_name]['#attributes'] = array('disabled' => 'disabled'); } } else { $form['version']['num'][$var_name] = array( '#type' => 'value', '#value' => $release->$var_name, ); } } /** * Modifies the given $form array to add the appropriate form element * for the desired text field. Since the 16+ lines of code in here * have to be duplicated 5 times in project_release_form(), this * function exists so we can reuse the code. * @see project_release_form * @ingroup project_release_internal * * @param $form * Reference to form element to add. * @param $title * Translatable title of the form element. * @param $value * The value to use in the form. * @param $is_edit * Boolean indicating if we're editing or creating. * @param $admin * Boolean for if the edit is by a project administrator. * @param $required * Boolean for if the field should be required. * @param $size * Value to use for the '#size' property. * @param $maxlength * Value to use for the '#maxlength' property. */ function _project_release_form_add_text_element(&$form, $title, $value, $is_edit, $admin, $required = TRUE, $size = 40, $maxlength = 50) { if ($is_edit && !empty($value)) { $form = array( '#type' => 'textfield', '#title' => $title, '#default_value' => $value, '#required' => $required, '#size' => $size, '#maxlength' => $maxlength, ); if(!$admin) { $form['#attributes']['disabled'] = 'disabled'; } } else { $form = array( '#type' => 'value', '#value' => $value, ); } } /** * Implementation of hook_validate(). * @ingroup project_release_node */ function project_release_validate(&$edit, $form) { global $user; global $form_values; if ($_POST['validate_version']) { if (!isset($edit->version_major) && !isset($edit->version_minor) && !isset($edit->version_patch) && (!($edit->version_extra) || $edit->version_extra === '')) { form_set_error('version_major', t('You must fill in some version information.')); // TODO: find a better form value to mark as the error? } foreach (array('version_major' => t('Major version number'), 'version_minor' => t('Minor version number')) as $field => $name) { $val = $edit->$field; if (isset($val) && $val !== '' && !is_numeric($val)) { form_set_error($field, t('%name must be a number.', array('%name' => $name))); } } $val = $edit->version_patch; if (isset($val) && $val !== '' && !is_numeric($val) && $val != 'x') { form_set_error('version_patch', t("Patch-level version number must be numeric or the letter 'x'.")); } } if ($file = file_check_upload('file')) { // Make sure that the extension on the file is one of the allowed extensions for release files. // Most of this validation code was modified from the code in file_check_upload(). $extensions = variable_get('project_release_file_extensions', PROJECT_RELEASE_FILE_EXTENSIONS); $regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i'; if (!preg_match($regex, $file->filename)) { form_set_error('file', t('The selected file %name can not be attached to this post, because it is only possible to attach files with the following extensions: %files-allowed.', array('%name' => $file->filename, '%files-allowed' => $extensions))); file_delete($file->filepath); } else { if ($user->uid != 1) { // Here something.php.pps becomes something.php_.pps $file->filename = upload_munge_filename($file->filename, $extensions, 0); $file->description = $file->filename; } if ($file = file_save_upload($file)) { // Using $form_values here is an ugly hack, but the caching mechanism // in file_check_upload() causes problems if we can't pass this object // to the file_save_upload() call in the submit function which actually // saves the file. $form_values['release_file'] = $file; $filepath = file_create_path($file->filepath); form_set_value($form['file']['file_path'], $file->filepath); form_set_value($form['file']['file_date'], filemtime($filepath)); form_set_value($form['file']['file_hash'], md5_file($filepath)); } else { form_set_error('file', t('There was a problem uploading the specified file.')); } } } if (project_release_get_api_taxonomy()) { $vid = _project_release_get_api_vid(); if (isset($edit->taxonomy)) { $tid = $edit->taxonomy[$vid]; } elseif (isset($edit->$vid)) { $tid = $edit->$vid; } if (isset($tid) && is_numeric($tid)) { $edit->version_api_tid = $tid; } } // With cvs.module installed, this validation is already handled. // We only want to do it here if we're *not* doing the N-page form... if (!project_use_cvs($edit) && !isset($edit->nid) && project_release_exists($edit)) { // TODO: is there a better form element to mark with this error? form_set_error('version_patch', t('This version already exists for this project.')); } // TODO: it'd be nice to automagically reset the version string and // title based on changes to the version elements on an edit, but we // have to be careful not to break the fancy N-page form when // cvs_form_alter() is involved... if (isset($edit->project->uri)) { $project_name = $edit->project->uri; } elseif (isset($edit->pid)) { $project_name = db_result(db_query("SELECT uri FROM {project_projects} WHERE nid = %d", $edit->pid)); } if (isset($edit->title)) { // TODO: Magic re-setting to "%project_name %version" ?? } elseif (isset($edit->version) && $edit->version !== '') { form_set_value($form['title'], "$project_name $edit->version"); } elseif (isset($edit->project)) { $version = project_release_get_version((object)$edit, $edit->project); form_set_value(array('#parents' => array('version')), $version); $title = $edit->project->title; form_set_value($form['title'], "$title $version"); } } /** * Implementation of hook_load(). * @ingroup project_release_node */ function project_release_load($node) { $additions = db_fetch_object(db_query("SELECT * FROM {project_release_nodes} WHERE nid = %d", $node->nid)); // Add the API term associated with the release. $api_vid = _project_release_get_api_vid(); $api_tid = db_result(db_query("SELECT tn.tid FROM {term_node} tn INNER JOIN {term_data} td ON tn.tid = td.tid WHERE td.vid = %d AND tn.nid = %d", $api_vid, $node->nid)); $additions->version_api_tid = $api_tid; return $additions; } /** * Implementation of hook_insert(). * @ingroup project_release_node */ function project_release_insert($node) { project_release_db_save($node, true); if (project_use_cvs($node->project)) { if ($node->rebuild) { $msg = variable_get('cvs_message_new_release_branch', ''); } else { $msg = variable_get('cvs_message_new_release_tag', ''); } if (!empty($msg)) { drupal_set_message($msg); } } } /** * Implementation of hook_update(). * @ingroup project_release_node */ function project_release_update($node) { project_release_db_save($node, false); } /** * Helper method to take data out of a $node object and store it into * the DB as necessary. Sadly, db_query() doesn't let us store NULL in * the DB, since those get cast to 0. Therefore, we have to do some * manual effort to dynamically create the appropriate SQL depending * on which version fields are set in the release node. * @see project_release_insert * @see project_release_update * @see db_query * @ingroup project_release_internal * * @param $node Node object to save * @param $is_new Is this a new release node, or are we updating? */ function project_release_db_save($node, $is_new) { global $form_values; // If the patch field is set to a non-numeric value, we just want to // keep it as a NULL in the DB, instead of casting it to a 0. if (isset($node->version_patch) && !is_numeric($node->version_patch)) { unset($node->version_patch); } // Handle file upload data. $file_data = new stdClass(); if (isset($form_values['release_file'])) { $file_data = file_save_upload($form_values['release_file'], file_directory_path()); // If a file was uploaded, this is what we need to use. $file_path = $file_data->filepath; } else { // If there's no upload, save whatever value is already in $node. $file_path = $node->file_path; } $types = array( 'pid' => "%d", 'version' => "'%s'", 'tag' => "'%s'", 'file_path' => "'%s'", 'file_date' => "%d", 'file_hash' => "'%s'", 'rebuild' => "%d", ); $values = array( 'pid' => $node->pid, 'version' => $node->version, 'tag' => $node->tag, 'file_path' => $file_path, 'file_date' => $node->file_date, 'file_hash' => $node->file_hash, 'rebuild' => $node->rebuild, ); $fields = array('version_major', 'version_minor', 'version_patch'); foreach ($fields as $field) { if (isset($node->$field) && is_numeric($node->$field)) { $types[$field] = "%d"; $values[$field] = $node->$field; } } if (!empty($node->version_extra)) { $types['version_extra'] = "'%s'"; $values['version_extra'] = $node->version_extra; } if ($is_new) { $types['nid'] = "%d"; $sql = 'INSERT INTO {project_release_nodes} ('. implode(', ', array_keys($types)) .') VALUES ('. implode(', ', $types) .')'; } else { $arr = array(); foreach ($types as $key => $value) { $arr[] = $key .' = '. $value; } $sql = 'UPDATE {project_release_nodes} SET '. implode(',', $arr) .' WHERE nid = %d'; } $values['nid'] = $node->nid; db_query($sql, $values); } /** * 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. */ function project_release_check_supported_versions($pid, $tid, $major, $delete) { if ($delete) { // Make sure this isn't the last release node for the given major. $active_branch_releases = db_result(db_query("SELECT COUNT(DISTINCT(r.nid)) FROM {project_release_nodes} r INNER JOIN {node} n ON n.nid = r.nid INNER JOIN {term_node} tn ON tn.nid = n.nid WHERE r.pid = %d AND tn.tid = %d AND n.status = 1 AND r.version_major = %d", $pid, $tid, $major)); if (empty($active_branch_releases)) { // Remove the bogus record. 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 = 1 AND recommended = 1", $pid, $tid)); if ($num_recommended > 1) { // Something seriously bogus, clear out the values and start over. db_query("UPDATE {project_release_supported_versions} SET recommended = 0 WHERE nid = %d AND tid = %d", $pid, $tid); $num_recommended = 0; } if ($num_recommended == 0) { // We no longer have a recommended major for this API/tid, so select a // sensible default, if possible. Find the lowest supported major // number that has a published release. $lowest_major = db_result(db_query("SELECT MIN(r.version_major) FROM {project_release_nodes} r INNER JOIN {project_release_supported_versions} prsv ON r.pid = prsv.nid INNER JOIN {node} n ON n.nid = r.nid INNER JOIN {term_node} tn ON tn.nid = n.nid WHERE r.pid = %d AND tn.tid = %d AND prsv.supported = 1 AND n.status = 1", $pid, $tid)); if ($lowest_major !== NULL && $lowest_major !== FALSE) { db_query("UPDATE {project_release_supported_versions} SET supported = 1, recommended = 1 WHERE nid = %d AND tid = %d AND major = %d", $pid, $tid, $lowest_major); } } } } else { // Adding or editing a release. // First make sure this branch has at least 1 published release node $published_releases = db_result(db_query("SELECT COUNT(*) FROM {project_release_nodes} r INNER JOIN {node} n ON n.nid = r.nid INNER JOIN {term_node} tn ON tn.nid = n.nid WHERE r.pid = %d AND r.version_major = %d AND tn.tid = %d AND n.status = 1", $pid, $major, $tid)); if ($published_releases) { // 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) { // First entry for this API tid. Add it as supported and recommended. $recommended = 1; } elseif (!$have_current_branch) { // We've already got some branches (num_branches > 0), but not the // current branch, so add it as supported but not recommended. $recommended = 0; } if (isset($recommended)) { db_query("INSERT INTO {project_release_supported_versions} (nid, tid, major, supported, recommended, snapshot) VALUES (%d, %d, %d, 1, %d, 0)", $pid, $tid, $major, $recommended); } } } // 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); } /** * Implementation of hook_delete(). * @ingroup project_release_node */ function project_release_delete($node) { if ($node->file_path) { file_delete(file_create_path($node->file_path)); } 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); } /** * @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->version_format)) { return $project->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 release node. * @ingroup project_release_api */ function project_release_get_version($release, $project = NULL) { if (isset($project)) { $node = $project; } else { $node->nid = $release->pid; } $variables = array(); foreach (array('major', 'minor', 'patch', 'extra') as $field) { $var = "version_$field"; if (isset($release->$var) && $release->$var !== '') { $variables["!$field"] = $release->$var; $variables["%$field"] = '.'. $release->$var; $variables["#$field"] = '-'. $release->$var; } else { $variables["!$field"] = ''; $variables["%$field"] = ''; $variables["#$field"] = ''; } } $variables["!api"] = ''; $variables["%api"] = ''; $variables["#api"] = ''; $vid = _project_release_get_api_vid(); if (project_release_get_api_taxonomy()) { if (isset($release->version_api_tid)) { $tid = $release->version_api_tid; } elseif (isset($release->$vid)) { $tid = $release->$vid; } if (isset($tid)) { $term = taxonomy_get_term($tid); $variables["!api"] = $term->name; $variables["%api"] = '.'. $term->name; $variables["#api"] = '-'. $term->name; } } $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->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 = ''; if (project_use_cvs($project) && isset($node->tag)) { if ($node->rebuild) { $output .= t('Nightly development snapshot from CVS branch: @tag', array('@tag' => $node->tag)) .'
'; } else { $output .= t('Official release from CVS tag: @tag', array('@tag' => $node->tag)) .'
'; } } if ($node->file_path) { $output .= ''. t('Download: !file', array('!file' => theme('project_release_download_link', $node->file_path))) .'
'; $output .= ''. t('Size: !size', array('!size' => format_size(filesize(file_create_path($node->file_path))))) .'
'; $output .= ''. t('md5_file hash: !file_hash', array('!file_hash' => $node->file_hash)) .'
'; } if ($node->created) { $output .= ''. t('First released: !created', array('!created' => format_date($node->created))) .'
'; } if ($node->file_date && ($node->file_date != $node->created)) { $output .= ''. t('Last updated: !changed', array('!changed' => format_date($node->file_date))) .'
'; } 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' => -2, ); // Display packaging errors to admins. if (project_check_admin_access($node->pid)) { $rows = array(); $result = db_query('SELECT * FROM {project_release_package_errors} WHERE nid = %d', $node->nid); $error = db_fetch_object($result); $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; } /** * Display a list of releases for a given project * @ingroup project_release_api */ function project_release_project_releases($op = 'page') { $nid = arg(1); $node = node_load($nid); switch ($op) { case 'feed': return project_release_feed($node); case 'page': default: // Breadcrumb navigation $breadcrumb[] = l($node->title, 'node/'. $node->nid); project_project_set_breadcrumb($node, $breadcrumb); drupal_set_title(t('Releases for %project', array('%project' => $node->title))); drupal_add_feed(url('node/'. $nid .'/release/feed'), t('RSS - releases for @project_title', array('@project_title' => $node->title))); $output = project_release_list($node); return $output; } } /** * 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 = ''; $args = array($project->nid); if (!project_check_admin_access($project)) { if (!empty($rids)) { $where = "AND (n.status = 1 OR n.nid IN (". implode(',', array_fill(0, count($rids), '%d')) ."))"; foreach ($rids as $rid) { $args[] = $rid; } } else { $where = 'AND (n.status = 1)'; } if ($filter_by == 'files') { $where .= " AND (r.file_path <> '')"; } } $result = db_query(db_rewrite_sql("SELECT n.nid, r.* FROM {node} n INNER JOIN {project_release_nodes} r 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 project release nodes * @ingroup project_release_callback */ function project_release_list($project) { if ($releases = project_release_get_releases($project, 1, 'date', 'files')) { $output = ''; foreach ($releases as $release) { $output .= node_view($release); } } else { $output = t('There are no published releases for this project.'); } return $output; } /** * Prints an RSS feed of project release nodes * * @ingroup project_release_callback */ function project_release_feed($project) { $project = node_invoke($project, 'view', FALSE, FALSE); $channel['link'] = url('node/'. $project->nid .'/release', NULL, NULL, TRUE); $channel['title'] = t('Releases for @project_title', array('@project_title' => $project->title)); $channel['description'] = $project->body; $order_by = 'n.created'; $where = "AND (n.status = 1) AND (r.file_path <> '')"; $result = db_query_range(db_rewrite_sql("SELECT n.nid FROM {node} n INNER JOIN {project_release_nodes} r ON r.nid = n.nid WHERE (r.pid = %d) $where ORDER BY $order_by DESC"), $project->nid, 0, variable_get('feed_default_items', 10)); node_feed($result, $channel); } /** * 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()); $terms[-1] = '<'. t('all') .'>'; foreach ($tree as $term) { if (($tids && !empty($tids[$term->tid])) || !$tids) { $terms[$term->tid] = $term->name; } } } return $terms; } /** * Creates a form array for the "filter by version" selector when browsing * projects on a site with 'project_release_browse_versions' enabled. * @see project_page_overview */ function project_release_version_filter_form($version = NULL) { global $user; if ($terms = project_release_compatibility_list()) { if ($user->uid) { if (is_null($version)) { $version = isset($_SESSION['project_version']) ? $_SESSION['project_version'] : variable_get('project_release_overview', -1); } $form['version_tid'] = array( '#type' => 'select', '#default_value' => $version, '#options' => $terms, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Filter'), ); } else { $destination = drupal_get_destination(); $default_version = variable_get('project_release_overview', -1); $form['version_tid'] = array( '#type' => 'select', '#default_value' => $default_version, '#options' => array($default_version => $terms[$default_version]), '#disabled' => TRUE, ); if (variable_get('user_register', 1)) { // Users can register themselves. $form['version_login'] = array('#value' => t('Login or register to modify the filter.', array('@login' => url('user/login', $destination), '@register' => url('user/register', $destination)))); } else { // Only admins can add new users, no public registration. $form['version_login'] = array('#value' => t('Login to modify the filter.', array('@login' => url('user/login', $destination)))); } } $form['#redirect'] = FALSE; } return isset($form) ? $form : ''; } function project_release_version_filter_form_submit($form_id, $form_values) { if (isset($form_values['version_tid'])) { $_SESSION['project_version'] = $form_values['version_tid']; } } function project_release_version_filter_form_validate($form_id, $form_values, $form) { global $user; if (!$user->uid && $form_values['version_tid'] != variable_get('project_release_overview', -1)) { $vocab = taxonomy_get_vocabulary(_project_release_get_api_vid()); form_set_error('version_tid', t('You must login or register to modify the @vocab filter.', array('@vocab' => $vocab->name))); } } function theme_project_release_version_filter_form($form) { if (project_release_get_api_taxonomy()) { $vocab = taxonomy_get_vocabulary(_project_release_get_api_vid()); $label = check_plain($vocab->name); } else { $label = t('version'); } $output = '
'. t('Filter by !label:', array('!label' => $label)) . drupal_render($form) .'
'; return $output; } /** * @defgroup project_release_fapi Form API hooks */ /** * Implementation of hook_form_alter(). * @ingroup project_release_fapi */ function project_release_form_alter($form_id, &$form) { if ($form_id == 'project_project_node_form') { return project_release_alter_project_form($form); } if ($form_id == 'project_release_node_form') { return project_release_alter_release_form($form); } } /** * 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) { $form['project']['uri']['#description'] .= ' '. t('This string is also used to generate the name of releases associated with this project.'); } /** * Releases subtab for the project edit tab. */ function project_release_project_edit_releases() { $node = node_load(arg(1)); project_project_set_breadcrumb($node); drupal_set_title(check_plain($node->title)); return drupal_get_form('project_release_project_edit_form', $node); } function project_release_project_edit_form($node) { $active_tids = project_release_compatibility_list(); // Make sure there is more than 1 active tid because // project_release_compatibility_list() will always // add $terms[-1] = 'all'. if (count($active_tids) > 1) { // Get all the data about major versions for this project. $data = _project_release_get_version_major_data($node); // Build all the form elements for supported and recommended major versions. $form = _project_release_edit_version_major_form($data); // Now, add a header and some help text for those elements. $form['header'] = array( '#type' => 'markup', '#value' => t('Supported versions'), ); $vocab = taxonomy_get_vocabulary(_project_release_get_api_vid()); $form['help'] = array( '#type' => 'markup', '#value' => t('For each term in the %api_vocabulary_name vocabulary, the following tables allow you to define which major versions are supported. If there are releases from more than one major version number, you can select which major version should be recommended for new users to download. You can also control if the development snapshot releases should be displayed on the front page of the project. Finally, for each term in the %api_vocabulary_name vocabulary, the current recommended release is shown. If no official releases have been made, the development snapshot from the corresponding branch will be listed. Otherwise, the most recent official release will be listed.', array('%api_vocabulary_name' => $vocab->name)), ); } // Finally, add everything else (currently, the "Advanced options"). $form['advanced'] = array( '#type' => 'fieldset', '#title' => t('Advanced options'), '#collapsible' => TRUE, '#collapsed' => $node->releases, '#weight' => 5, ); $form['advanced']['releases'] = array( '#type' => 'checkbox', '#title' => t('Enable releases'), '#return_value' => 1, '#weight' => -10, '#default_value' => isset($node->releases) ? $node->releases : 1, '#description' => t('Allow releases of this project with specific versions.'), ); if (user_access('administer projects')) { $form['advanced']['version_format'] = array( '#type' => 'textfield', '#title' => t('Version format string'), '#default_value' => $node->version_format, '#size' => 50, '#maxlength' => 255, '#description' => t('Customize the format of the version strings for releases of this project.') .' '. PROJECT_RELEASE_VERSION_FORMAT_HELP .' '. t('If blank, this project will use the site-wide default (currently set to: %default)', array('%default' => variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT))), ); } $form['nid'] = array('#type' => 'value', '#value' => $node->nid); $form['submit'] = array( '#type' => 'submit', '#value' => t('Update'), '#weight' => 45, ); return $form; } function _project_release_get_version_major_data($node) { $data = array(); $data['node'] = $node; $params = array(); $params[] = $node->nid; $placeholders = array(); $active_tids = project_release_compatibility_list(); // Get rid of -1 => 'all' element inserted by project_release_compatibility_list(). if (isset($active_tids[-1])) { unset($active_tids[-1]); } foreach ($active_tids as $tid => $api_term) { $placeholders[] = '%d'; $params[] = $tid; } $tid_where = ''; if (!empty($active_tids)) { $tid_where = 'prsv.tid IN ('. implode(',', $placeholders) .')'; } $result = db_query("SELECT prsv.*, td.name AS term_name FROM {project_release_supported_versions} prsv INNER JOIN {term_data} td ON prsv.tid = td.tid WHERE prsv.nid = %d AND $tid_where ORDER BY td.weight, td.name", $params); while ($obj = db_fetch_object($result)) { $tid = $obj->tid; if (empty($data[$tid])) { $data[$tid] = array( 'name' => $obj->term_name, 'majors' => array(), ); } $data[$tid]['majors'][$obj->major] = array( '#snapshot' => $obj->snapshot ? TRUE : FALSE, '#supported' => $obj->supported ? TRUE : FALSE, '#recommended' => $obj->recommended ? TRUE : FALSE, ); } return $data; } function _project_release_edit_version_major_form($data) { drupal_add_js(drupal_get_path('module', 'project_release') .'/project_release.js'); $form = array(); $node = $data['node']; unset($data['node']); $form['api'] = array( '#tree' => TRUE, ); foreach ($data as $api_tid => $api_data) { $form['api'][$api_tid] = array( '#api_term_name' => $api_data['name'], 'major' => array(), ); $recommended_version = -1; $recommended_options = array(); foreach ($api_data['majors'] as $major_version => $major_data) { if ($major_data['#recommended'] == TRUE) { $recommended_version = $major_version; } $recommended_options[$major_version] = ''; $current = project_release_get_current_recommended($node->nid, $api_tid, $major_version); $version_name = (!empty($current)) ? $current->version : t('n/a'); $form['api'][$api_tid]['major'][$major_version] = array( 'supported' => array( '#type' => 'checkbox', '#title' => t('Supported'), '#default_value' => $major_data['#supported'], '#attributes' => array('class' => 'supported'), ), 'snapshot' => array( '#type' => 'checkbox', '#title' => t('Show snapshot release'), '#default_value' => $major_data['#snapshot'], '#attributes' => array('class' => 'snapshot'), ), 'version_name' => array( '#type' => 'hidden', '#value' => $version_name, '#attributes' => array('class' => 'version-name'), ), ); } $form['api'][$api_tid]['recommended'] = array( '#type' => 'radios', '#title' => t('Recommended'), '#options' => $recommended_options, '#default_value' => $recommended_version, '#attributes' => array('class'=>'recommended'), ); $recommended_major = db_result(db_query("SELECT major FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d AND recommended = 1", $node->nid, $api_tid)); if ($recommended_major !== FALSE) { $current_recommended = $form['api'][$api_tid]['major'][$recommended_major]['version_name']['#value']; } else { $current_recommended = t('n/a'); } $form['api'][$api_tid]['currently_recommended'] = array( '#type' => 'markup', '#value' => ' '. $current_recommended .'', ); } return $form; } function theme_project_release_form_value($element) { return check_plain($element['#value']); } function theme_project_release_project_edit_form($form) { $output = ''; if (!empty($form['api'])) { $output .= '

'. drupal_render($form['header']) ."

\n"; $output .= '

'. drupal_render($form['help']) ."

\n"; $header = array( t('Major version'), t('Supported'), t('Recommended'), array( 'data' => t('Show snapshot release'), 'colspan' => 2, ), ); foreach (element_children($form['api']) as $tid) { $output .= '

'. $form['api'][$tid]['#api_term_name'] .'

'; $rows = array(); krsort($form['api'][$tid]['major']); foreach (element_children($form['api'][$tid]['major']) as $major) { $row = array(); $row[] = $major; // We want to unset the titles for each element, since we already have // table headers to label each column. unset($form['api'][$tid]['major'][$major]['supported']['#title']); $row[] = drupal_render($form['api'][$tid]['major'][$major]['supported']); unset($form['api'][$tid]['recommended'][$major]['#title']); $row[] = drupal_render($form['api'][$tid]['recommended'][$major]); unset($form['api'][$tid]['major'][$major]['snapshot']['#title']); $row[] = drupal_render($form['api'][$tid]['major'][$major]['snapshot']). drupal_render($form['api'][$tid]['major'][$major]['version_name']); $rows[] = $row; } // Finally, add a row for the currently recommended version. $row = array(); $row[] = array( 'data' => drupal_render($form['api'][$tid]['currently_recommended']), 'colspan' => 5, ); $rows[] = $row; $output .= theme('table', $header, $rows); } unset($form['api']); } $output .= drupal_render($form['advanced']); $output .= drupal_render($form); return $output; } /** * Validates the project form regarding release-specific fields. * Ensures that the version format string doesn't contain bad characters. * @ingroup project_release_fapi * @see project_release_project_edit_releases */ function project_release_project_edit_form_validate($form_id, $form_values, $form) { if (!empty($form_values['version_format'])) { _project_release_validate_format_string($form_values, 'version_format'); } if (isset($form_values['api'])) { foreach ($form_values['api'] as $tid => $api_info) { $supported = FALSE; // First, we just iterate through to see if *any* majors are supported. foreach ($api_info['major'] as $major => $flags) { if (!empty($flags['supported'])) { $supported = TRUE; break; } } if ($supported) { // At least 1 major is supported, so validate the settings. foreach ($api_info['major'] as $major => $flags) { if (empty($flags['supported']) && !empty($flags['snapshot'])) { $element = 'api]['. $tid .'][major]['. $major .'][snapshot'; form_set_error($element, t('You can not show a snapshot release for a major version that is not supported for %api_term_name.', array('%api_term_name' => $form['api'][$tid]['#api_term_name']))); } } $recommended = $api_info['recommended']; if ($recommended < 0) { form_set_error("api][$tid][recommended", t('You must select a recommended major version for %api_term_name.', array('%api_term_name' => $form['api'][$tid]['#api_term_name']))); } elseif (empty($api_info['major'][$recommended]['supported'])) { form_set_error("api][$tid][recommended", t('You can not recommend a major version that is not supported for %api_term_name.', array('%api_term_name' => $form['api'][$tid]['#api_term_name']))); } } } } } /** * Submit handler when project admins use the releases subtab. * @ingroup project_release_fapi * @see project_release_project_edit_releases */ function project_release_project_edit_form_submit($form_id, $form_values) { $nid = $form_values['nid']; db_query("UPDATE {project_release_projects} SET releases = %d, version_format = '%s' WHERE nid = %d", $form_values['releases'], $form_values['version_format'], $nid); if (!db_affected_rows()) { // It's possible there's no record in {project_release_projects} if this // particular project was created before project_issue.module was enabled. db_query("INSERT INTO {project_release_projects} (nid, releases, version_format) VALUES (%d, %d, '%s')", $nid, $form_values['releases'], $form_values['version_format']); } if (!empty($form_values['api'])) { foreach ($form_values['api'] as $tid => $values) { if (isset($values['recommended'])) { $recommended_major = $values['recommended']; } if (!empty($values['major'])) { foreach ($values['major'] as $major => $major_values) { $major_values['recommended'] = ($major_values['supported'] && $recommended_major == $major) ? 1 : 0; $major_values['snapshot'] = ($major_values['supported'] && $major_values['snapshot']) ? 1 : 0; if ($obj = db_fetch_object(db_query("SELECT * FROM {project_release_supported_versions} WHERE nid = %d AND tid = %d AND major = %d", $nid, $tid, $major))) { if ($obj->supported != $major_values['supported'] || $obj->recommended != $major_values['recommended'] || $obj->snapshot != $major_values['snapshot']) { db_query("UPDATE {project_release_supported_versions} SET supported = %d, recommended = %d, snapshot = %d WHERE nid = %d AND tid = %d AND major = %d", $major_values['supported'], $major_values['recommended'], $major_values['snapshot'], $nid, $tid, $major); } } else { db_query("INSERT INTO {project_release_supported_versions} (nid, tid, major, supported, recommended, snapshot) VALUES (%d, %d, %d, %d, %d, %d)", $nid, $tid, $major, $major_values['supported'], $major_values['recommended'], $major_values['snapshot']); } } } } } db_query("UPDATE {node} SET changed = %d WHERE nid = %d", time(), $form_values['nid']); $cid = 'table:'. $form_values['nid'] .':'; cache_clear_all($cid, 'cache_project_release', TRUE); drupal_set_message(t('Release settings have been saved.')); } /** * 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) { global $user; $node = $form['#node']; $tid = $node->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 (isset($node->pid)) { $project->nid = $node->pid; } elseif (arg(1) == 'add' && is_numeric(arg(3))) { $project->nid = arg(3); } if (isset($project->nid)) { */ if (!user_access('administer projects')) { // The user doesn't have 'administer projects' permission, so // we restrict their options for the compatibility taxonomy. if (isset($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 (!element_children($form['taxonomy'])) { unset($form['taxonomy']); } } /** * @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 = db_fetch_object(db_query('SELECT * FROM {project_release_projects} WHERE nid = %d', $node->nid)); $fields = array('releases', 'version_format'); foreach ($fields as $field) { $node->$field = $project->$field; } $wants_snapshots = db_result(db_query('SELECT tid FROM {project_release_supported_versions} WHERE nid = %d AND snapshot = 1 LIMIT 1', $node->nid)); if (isset($wants_snapshots)) { $node->project_release_show_snapshots = TRUE; } break; case 'insert': db_query("INSERT INTO {project_release_projects} (nid, releases, version_format) VALUES (%d, %d, '%s')", $node->nid, 1, ''); break; case 'delete': // 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->version_api_tid)) { $tid = $node->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->pid, $tid, $node->version_major, ($op == 'delete' ? TRUE : FALSE)); } } break; case 'rss item': // Prepend the table of release info whenever a release is in a feed. $node->body = $node->content['release_info']['#value'] . $node->body; $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->file_path)) { $file_link = theme('project_release_download_link', $node->file_path, NULL, TRUE); return array( array( 'key' => 'enclosure', 'attributes' => array( 'url' => $file_link['href'], 'length' => filesize(file_create_path($node->file_path)), 'type' => 'application/octet-stream', ) ) ); } break; } } /** * Finds the currently recommended release for a given project. * * @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 * (Optional) a specific major version to search. If not specified, the * current value from the {project_release_supported_versions} table is used. * * @return * An object containing all the fields from {project_release_nodes}, along * with {node}.title, for the appropriate release. */ function project_release_get_current_recommended($project_nid, $api_tid, $recommended_major = NULL) { static $current = array(); $major = isset($recommended_major) ? $recommended_major : 'current'; if (isset($current_major[$project_nid][$api_tid][$major])) { return $current[$project_nid][$api_tid][$major]; } $join = $where = ''; $params = $orderby = array(); $join = ' INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid = %d'; $params[] = $api_tid; $where = 'WHERE (r.pid = %d) AND (n.status = 1)'; $params[] = $project_nid; if (isset($recommended_major)) { $where .= ' AND (r.version_major = %d)'; $params[] = $recommended_major; } else { $join .= ' INNER JOIN {project_release_supported_versions} prsv ON prsv.nid = r.pid AND prsv.tid = tn.tid AND prsv.major = r.version_major AND prsv.recommended = 1 '; } // We always want the dev snapshots to show up last. $orderby[] = 'r.rebuild'; $orderby[] = 'r.version_major DESC'; $orderby[] = 'r.version_minor DESC'; $orderby[] = 'r.version_patch DESC'; $orderby[] = 'r.file_date DESC'; $order_by = 'ORDER BY '. implode(', ', $orderby); $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 $join ". "$where $order_by LIMIT 1"), $params); $release = db_fetch_object($result); $current_major[$project_nid][$api_tid][$major] = $release; return $release; } /** * Theme the appropriate release download table for a project node. */ function theme_project_release_project_download_table($node) { if (!$node->releases) { return; } $output = '

'. t('Releases') .'

'; $output .= project_release_table($node, 'supported', 'official', t('Official releases')); if ($node->project_release_show_snapshots) { $output .= project_release_table($node, 'supported', 'snapshot', t('Development snapshots')); } return $output; } /** * Implemenation of hook_project_page_link_alter(). */ function project_release_project_page_link_alter($node, &$all_links) { if (empty($node->releases)) { return; } $all_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_check_admin_access($node->nid)) { $all_links['project_release']['links']['add_new_release'] = l(t('Add new release'), 'node/add/project_release/'. $node->nid); $all_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? */ function project_release_table($project, $table_type = 'recommended', $release_type = 'all', $title = NULL, $print_size = TRUE) { if (empty($title)) { $title = t('Version'); } // Can the current user edit releases for this project? $can_edit = node_access('update', $project); // 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 = ''; $tids = project_release_compatibility_list(); if ($tids) { unset($tids[-1]); // get rid of that one. if (!empty($tids)) { $join = ' INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid in (' . implode(',', array_keys($tids)) .') ' .' INNER JOIN {term_data} td ON td.tid = tn.tid '; $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 = 1 '; } elseif ($table_type == 'supported') { $join .= 'AND prsv.supported = 1 '; } } else { // TODO: someday (never?) when project_release doesn't require taxonomy. } switch ($release_type) { case 'official': $where = 'AND r.rebuild <> 1'; break; case 'snapshot': // For snapshot tables, restrict to snapshot nodes from branches where // the maintainer wants the snapshot visible. $where = 'AND r.rebuild = 1'; if ($tids) { $where .= ' AND prsv.snapshot = 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[] = 'r.file_date DESC'; $order_by = !empty($orderby) ? (' ORDER BY '. implode(', ', $orderby)) : ''; $select = !empty($selects) ? (implode(', ', $selects) .',') : ''; $result = db_query(db_rewrite_sql( "SELECT n.nid, n.created, $select r.* FROM {node} n ". "INNER JOIN {project_release_nodes} r ON r.nid = n.nid $join ". "WHERE (r.pid = %d) AND (n.status = 1) $where $order_by"), $project->nid); $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, 'cache_project_release', $output, time() + 43200); return $output; } /** * Helper function to return an individual row for the download table. * * @param $release * The release object queried from the database. * @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->file_path)) { $links['project_release_download'] = theme('project_release_download_link', $release->file_path, 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->file_path) ? format_date($release->file_date, 'custom', 'Y-M-d') : format_date($release->created, 'custom', 'Y-M-d'), ), ), ); if ($print_size) { $row['data'][] = array( 'class' => 'release-size', 'data' => !empty($release->file_path) ? format_size(filesize(file_create_path($release->file_path))) : 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 * Object with data about the release. * @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, $object = NULL) { if ($op == 'delete' && $type == 'vocabulary' && $object->vid == _project_release_get_api_vid()) { variable_del('project_release_api_vocabulary'); } elseif ($type == 'term' && $object->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; } return taxonomy_get_tree(_project_release_get_api_vid()); } /** * Returns the vocabulary id for project release API */ function _project_release_get_api_vid() { $vid = variable_get('project_release_api_vocabulary', ''); if (empty($vid)) { // Check to see if a project release module vocabulary exists. $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module='%s'", 'project_release')); if (!$vid) { $edit = array('name' => t('Project release API compatibility'), 'multiple' => 0, 'hierarchy' => 1, 'relations' => 0, 'module' => 'project_release', 'nodes' => array('project_release' => 1)); // If there is already a vocabulary assigned to 'project_project' nodes, use it. $vocabularies = taxonomy_get_vocabularies('project_release'); if (count($vocabularies)) { $vocabulary = reset($vocabularies); $edit['vid'] = $vocabulary->vid; } taxonomy_save_vocabulary($edit); $vid = $edit['vid']; } variable_set('project_release_api_vocabulary', $vid); } return $vid; } function project_release_exists($version) { $fields = array('version_major', 'version_minor', 'version_patch'); foreach ($fields as $field) { if (isset($version->$field) && is_numeric($version->$field)) { $types[$field] = "%d"; $values[$field] = $version->$field; $foo = $version->$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; } } if (!isset($types)) { // We have nothing to query, yet... return false; } if (isset($version->version_api_tid)) { $taxo_join = ' INNER JOIN {term_node} t ON p.nid = t.nid'; $taxo_where = ' AND t.tid = %d'; $values['tid'] = $version->version_api_tid; $tid = $version->version_api_tid; } $sql = 'SELECT * FROM {project_release_nodes} p'. $taxo_join .' WHERE p.pid = %d'; foreach ($types as $field => $type) { $sql .= " AND p.$field = $type"; } $sql .= $taxo_where; // we put pid as the first WHERE, so stick it on the front $values = array_merge(array('pid' => $version->pid), $values); return db_num_rows(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 $file_path * 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 $file_path 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($file_path, $link_text = NULL, $as_array = FALSE) { if (empty($link_text)) { $link_text = basename($file_path); } $download_base = variable_get('project_release_download_base', ''); if (!empty($download_base)) { $link_path = $download_base . $file_path; } else { $link_path = file_create_url($file_path); } if ($as_array) { return array( 'title' => $link_text, 'href' => $link_path, ); } else { return l($link_text, $link_path); } } /** * Implementation of hook_file_download(). * * @param $filepath * 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 f.nid FROM {project_release_nodes} f WHERE file_path = '%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 devel_caches(). */ function project_release_devel_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'); } /** * Form builder for a simple form to select a project when creating a new * release (as the first "page", but this is not really a multi-page form). */ function project_release_pick_project_form() { $form = array(); drupal_set_title(t('Submit @name', array('@name' => node_get_types('name', 'project_release')))); // Fetch a list of all projects. $uris = NULL; $projects = array(0 => t('- Select a project -')) + project_projects_select_options($uris); if (count($projects) == 1) { drupal_set_message(t('You do not have access to any projects.'), 'error'); } $form['pid'] = array( '#type' => 'select', '#title' => t('Project'), '#options' => $projects, '#required' => TRUE, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Next'), ); return $form; } function project_release_pick_project_form_validate($form_id, $form_values) { if (empty($form_values['pid'])) { form_set_error('pid', t('You must select a project.')); } $node = node_load($form_values['pid']); if (empty($node) || $node->type != 'project_project') { form_set_error('pid', t('Invalid project selected.')); } } function project_release_pick_project_form_submit($form_id, $form_values) { return 'node/add/project-release/'. $form_values['pid']; } /** * 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(); } $vocabulary = taxonomy_get_vocabulary($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; }