diff --git includes/project_maintainers.inc includes/project_maintainers.inc
new file mode 100644
index 0000000..237f92f
--- /dev/null
+++ includes/project_maintainers.inc
@@ -0,0 +1,228 @@
+ t('User'));
+ foreach ($project_perms as $perm_name => $perm_info) {
+ $form['#header'][$perm_name] = array('data' => $perm_info['title']);
+ }
+ $form['#header']['operations'] = array('data' => t('Operations'));
+
+ if (!empty($project->project['maintainers'])) {
+ foreach ($project->project['maintainers'] as $uid => $maintainer) {
+ $form['maintainers'][$uid] = array();
+ $form['maintainers'][$uid]['name'] = array(
+ '#type' => 'value',
+ '#value' => $maintainer['name'],
+ );
+ foreach ($project_perms as $perm_name => $perm_info) {
+ $form['maintainers'][$uid]['permissions'][$perm_name] = array(
+ '#type' => 'checkbox',
+ '#default_value' => !empty($maintainer['permissions'][$perm_name]),
+ );
+ }
+ $form['maintainers'][$uid]['operations'] = array();
+ if ($uid == $project->uid) {
+ // We special-case the project owner with disabled checkboxes.
+ foreach ($project_perms as $perm_name => $perm_info) {
+ $form['maintainers'][$uid]['permissions'][$perm_name]['#disabled'] = TRUE;
+ }
+ $form['maintainers'][$uid]['operations']['delete'] = array(
+ '#value' => t('locked'),
+ );
+ }
+ else {
+ $form['maintainers'][$uid]['operations']['delete'] = array(
+ '#value' => l(t('delete'), "node/$project->nid/maintainers/delete/$uid"),
+ );
+ }
+ }
+ }
+
+ $form['new_maintainer'] = array();
+ $form['new_maintainer']['user'] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#maxlength' => 40,
+ '#autocomplete_path' => 'user/autocomplete',
+ );
+ // we'll fill this in with a real value during validate()
+ $form['new_maintainer']['uid'] = array(
+ '#type' => 'value',
+ '#value' => 0,
+ );
+ foreach ($project_perms as $perm_name => $perm_info) {
+ $form['new_maintainer']['permissions'][$perm_name] = array(
+ '#type' => 'checkbox',
+ );
+ }
+
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Update'));
+
+ return $form;
+}
+
+function theme_project_maintainers_form($form) {
+ $output = '';
+
+ $header = $form['#header'];
+ $rows = array();
+
+ // Render all the existing maintainers.
+ if (is_array($form['maintainers'])) {
+ foreach (element_children($form['maintainers']) as $uid) {
+ $row = array();
+ $account = new stdClass;
+ $account->uid = $uid;
+ $account->name = $form['maintainers'][$uid]['name']['#value'];
+ $row[] = theme('username', $account);
+ foreach (element_children($form['maintainers'][$uid]['permissions']) as $perm) {
+ $row[] = drupal_render($form['maintainers'][$uid]['permissions'][$perm]);
+ }
+ $row[] = drupal_render($form['maintainers'][$uid]['operations']);
+ if ($form['#project']->uid == $uid) {
+ $owner_row = $row;
+ }
+ else {
+ $rows[] = $row;
+ }
+ }
+ }
+
+ // Create the final row for adding a new maintainer.
+ $row = array();
+ $row[] = drupal_render($form['new_maintainer']['user']);
+ foreach (element_children($form['new_maintainer']['permissions']) as $perm) {
+ $row[] = drupal_render($form['new_maintainer']['permissions'][$perm]);
+ }
+ $row[] = ''; // Empty cell for the 'Operations' column on new maintainers.
+ $rows[] = $row;
+
+ // Always put the owner row at the top of the table.
+ $rows = array_merge(array($owner_row), $rows);
+
+ // Although using named keys in the $header array makes this form easier to
+ // alter, theme_table() freaks out if the $header array has non-numeric
+ // keys. So we ditch the keys at this point to avoid notices.
+ $output .= theme('table', array_values($header), $rows);
+
+ $project_perms = project_permission_load();
+ $output .= '
';
+ foreach ($project_perms as $perm => $perm_info) {
+ $output .= ' - ' . $perm_info['title'] . '
';
+ $output .= ' - ' . $perm_info['description'] . '
';
+ }
+ $output .= "
\n";
+
+ $output .= drupal_render($form);
+ return $output;
+}
+
+/**
+ * Validation callback for the project maintainers form.
+ */
+function project_maintainers_form_validate($form, &$form_state) {
+ $new_maintainer = $form_state['values']['new_maintainer'];
+ if (!empty($new_maintainer['user'])) {
+ $user_result = db_fetch_object(db_query("SELECT name, uid FROM {users} WHERE name = '%s'", $new_maintainer['user']));
+ if (empty($user_result->uid)) {
+ form_set_error('new_maintainer][user', t('%user is not a valid user on this site.', array('%user' => $new_maintainer['user'])), 'error');
+ return;
+ }
+ if (!empty($form['#project']->project['maintainers'][$user_result->uid])) {
+ form_set_error('new_maintainer][user', t('%user is already a maintainer of this project.', array('%user' => $new_maintainer['user'])), 'error');
+ return;
+ }
+ // Save the uid in the form so we don't have to look it up again at submit.
+ form_set_value($form['new_maintainer']['uid'], $user_result->uid, $form_state);
+ }
+ else {
+ foreach ($new_maintainer['permissions'] as $name => $value) {
+ if (!empty($value)) {
+ form_set_error('new_maintainer][user', t('You must specify a valid user name to grant permissions.'));
+ }
+ }
+ }
+}
+
+/**
+ * Submit callback for the project maintainers form.
+ */
+function project_maintainers_form_submit($form, &$form_state) {
+ $project_nid = $form['#project']->nid;
+
+ // Loop over all the maintainers and update their permissions accordingly.
+ if (!empty($form_state['values']['maintainers'])) {
+ foreach ($form_state['values']['maintainers'] as $uid => $maintainer) {
+ // Just to be extra safe, always give the project owner full permissions.
+ if ($uid == $form['#project']->uid) {
+ $perms = array_fill_keys(array_keys(project_permission_load()), 1);
+ }
+ else {
+ $perms = $maintainer['permissions'];
+ }
+ project_maintainer_save($project_nid, $uid, $perms);
+ }
+ }
+
+ // See if we need to insert a record for a new maintainer.
+ if (!empty($form_state['values']['new_maintainer']['uid'])) {
+ project_maintainer_save($project_nid, $form_state['values']['new_maintainer']['uid'], $form_state['values']['new_maintainer']['permissions']);
+ }
+
+}
+
+/**
+ * Confirm form for removing a uid as a cvs maintainer from a given project.
+ */
+function project_maintainer_delete_confirm($form_state, $project, $user) {
+ if ($user->uid == $project->uid) {
+ drupal_set_message(t('You can not delete the project owner (!user) as a maintainer.', array('!user' => theme('username', $user))), 'error');
+ return drupal_goto("node/$project->nid/maintainers/");
+ }
+
+ $form['nid'] = array('#type' => 'value', '#value' => $project->nid);
+ $form['uid'] = array('#type' => 'value', '#value' => $user->uid);
+
+ return confirm_form($form,
+ t('Are you sure you want to delete !user as a maintainer of !project?',
+ array(
+ '!user' => theme('username', $user),
+ '!project' => l($project->title, "node/$project->nid"),
+ )),
+ "node/$project->nid/maintainers",
+ t('This action cannot be undone.'),
+ t('Delete'),
+ t('Cancel'));
+}
+
+/**
+ * Delete the requested user as a maintainer.
+ *
+ * Invoked when the delete button on the confirm_form() page is pressed.
+ */
+function project_maintainer_delete_confirm_submit($form, &$form_state) {
+ $nid = $form_state['values']['nid'];
+ $uid = $form_state['values']['uid'];
+ $user = user_load(array('uid' => $uid));
+
+ project_maintainer_remove($nid, $uid);
+
+ drupal_set_message(t('Removed !user as a maintainer.', array('!user' => theme('username', $user))));
+ $form_state['redirect'] = "node/$nid/maintainers";
+}
diff --git project.api.php project.api.php
new file mode 100644
index 0000000..0effd7b
--- /dev/null
+++ project.api.php
@@ -0,0 +1,115 @@
+ array(
+ 'title' => t('Name of this permission'),
+ 'description' => t('Description of what this this permission allows project maintainers to do.'),
+ ),
+ );
+}
+
+/**
+ * Alter hook for per-project permissions supported by a module.
+ *
+ * @param $permissions
+ * Reference to an array of all the permissions defined via
+ * hook_project_permission_info().
+ *
+ * @see hook_project_permission_info().
+ * @see project_permission_load().
+ * @see drupal_alter()
+ */
+function hook_project_permission_alter(&$permissions) {
+ // I can't yet fathom why we need an alter hook here, but we might need it
+ // and it was free to include it, so why not? ;)
+}
+
+/**
+ * Invoked whenever a project maintainer is added or updated.
+ *
+ * This gives any modules that are providing their own per-project permissions
+ * a chance to store the data about a maintainer's permissions whenever the
+ * record for that maintainer is being saved.
+ *
+ * @param $nid
+ * The Project NID to save the maintainer information for.
+ * @param $uid
+ * The user ID of the maintainer to save.
+ * @param array $permissions
+ * Associative array of which project-level permissions the maintainer
+ * should have. The keys are permission names, and the values are if the
+ * permission should be granted or not.
+ *
+ * @see hook_project_permission_info()
+ */
+function hook_project_maintainer_save($nid, $uid, $permissions) {
+ // Try to update an existing record for this maintainer for our permission.
+ db_query("UPDATE {example_project_maintainer} SET some_project_permission = %d WHERE nid = %d AND uid = %d", !empty($permissions['some project permission']), $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 {example_project_maintainer} (nid, uid, some_project_permission) VALUES (%d, %d, %d)", $nid, $uid, !empty($permissions['some project permission']));
+ }
+}
+
+/**
+ * Invoked whenever a maintainer is removed from a given project.
+ *
+ * @param $nid
+ * The Project NID to remove the maintainer from.
+ * @param $uid
+ * The user ID of the maintainer to remove.
+ *
+ * @see project_maintainer_remove()
+ */
+function hook_project_maintainer_remove($nid, $uid) {
+ db_query("DELETE FROM {example_project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid);
+}
+
+/**
+ * Populate the maintainer information for a given project.
+ *
+ * Whenever a project node is being loaded, this hook is invoked to give any
+ * modules providing per-project permissions a chance to update the maintainer
+ * array. This array is stored in the project as $node->project['maintainers'].
+ *
+ * The maintainers array is keyed by the UID of each maintainer. Each value is
+ * itself a nested array of information about the maintainer. These arrays
+ * have the keys 'name' for the username and 'permissions', which is an array
+ * of per-project permissions associated with the maintainer. This
+ * 'permissions' subarray is keyed by permission name, and the values are 0 or
+ * 1 to indicate if the maintainer should have access to that permission.
+ *
+ * @param $nid
+ * The Project NID to populate maintainer information about.
+ * @param $maintainers
+ * Reference to a nested array of maintainers.
+ */
+function hook_project_maintainer_project_load($nid, &$maintainers) {
+ $query = db_query('SELECT u.uid, u.name, epm.some_project_permission FROM {example_project_maintainer} epm INNER JOIN {users} u ON epm.uid = u.uid WHERE epm.nid = %d', $nid);
+ while ($maintainer = db_fetch_object($query)) {
+ if (empty($maintainers[$maintainer->uid])) {
+ $maintainers[$maintainer->uid]['name'] = $maintainer->name;
+ }
+ $maintainers[$maintainer->uid]['permissions']['some project permission'] = $maintainer->some_project_permission;
+ }
+}
diff --git project.inc project.inc
index 7ea06d5..c771c37 100644
--- project.inc
+++ project.inc
@@ -425,11 +425,15 @@ function project_project_nodeapi(&$node, $op, $arg) {
function project_project_insert($node) {
db_query("INSERT INTO {project_projects} (nid, uri, homepage, changelog, cvs, demo, screenshots, documentation, license) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", $node->nid, $node->project['uri'], $node->project['homepage'], $node->project['changelog'], $node->project['cvs'], $node->project['demo'], $node->project['screenshots'], $node->project['documentation'], $node->project['license']);
// project_release_scan_directory($node->project['uri']);
+ $perms = array_fill_keys(array_keys(project_permission_load()), 1);
+ project_maintainer_save($node->nid, $node->uid, $perms);
}
function project_project_update($node) {
db_query("UPDATE {project_projects} SET uri = '%s', homepage = '%s', changelog = '%s', cvs = '%s', demo = '%s', screenshots = '%s', documentation = '%s', license = '%s' WHERE nid = %d", $node->project['uri'], $node->project['homepage'], $node->project['changelog'], $node->project['cvs'], $node->project['demo'], $node->project['screenshots'], $node->project['documentation'], $node->project['license'], $node->nid);
// project_release_scan_directory($node->project['uri']);
+ $perms = array_fill_keys(array_keys(project_permission_load()), 1);
+ project_maintainer_save($node->nid, $node->uid, $perms);
}
function project_project_delete($node) {
diff --git project.install project.install
index 6e951a5..ecb4078 100644
--- project.install
+++ project.install
@@ -104,6 +104,41 @@ function project_schema() {
'project_projects_uri' => array(array('uri', 8)),
),
);
+ $schema['project_maintainer'] = array(
+ 'description' => t('Users who have various per-project maintainer permissions.'),
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => t('Foreign key: {project_projects}.nid of the project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'uid' => array(
+ 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'administer_settings' => array(
+ 'description' => t('Can this user edit the given project and modify its settings.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'administer_maintainers' => array(
+ 'description' => t('Can this user manipulate the maintainers for the given project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('nid', 'uid'),
+ );
+
return $schema;
}
@@ -137,3 +172,58 @@ function project_update_6001() {
return $ret;
}
+/**
+ * Add the {project_maintainer} table.
+ */
+function project_update_6002() {
+ $ret = array();
+ $table = array(
+ 'description' => t('Users who have various per-project maintainer permissions.'),
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => t('Foreign key: {project_projects}.nid of the project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'uid' => array(
+ 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'administer_settings' => array(
+ 'description' => t('Can this user edit the given project and modify its settings.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'administer_maintainers' => array(
+ 'description' => t('Can this user manipulate the maintainers for the given project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('nid', 'uid'),
+ );
+ db_create_table($ret, 'project_maintainer', $table);
+
+ // Initially populate the table so that every project owner has full
+ // powers on their own projects.
+ $ret[] = update_sql("INSERT INTO {project_maintainer} (nid, uid, administer_settings, administer_maintainers) SELECT nid, uid, 1, 1 FROM {node} WHERE type = 'project_project'");
+
+ // If CVS module is enabled, also populate the table from the
+ // {cvs_project_maintainers} table so that anyone with CVS access
+ // who is not the project owner can administer the project but not
+ // manipulate the per-project permissions.
+ if (module_exists('cvs')) {
+ $ret[] = update_sql("INSERT INTO {project_maintainer} (nid, uid, administer_settings, administer_maintainers) SELECT cpm.nid, cpm.uid, 1, 0 FROM {cvs_project_maintainers} cpm INNER JOIN {node} n ON cpm.nid = n.nid WHERE cpm.uid != n.uid");
+ }
+
+ return $ret;
+}
diff --git project.module project.module
index 79a9e88..3aee50b 100644
--- project.module
+++ project.module
@@ -518,6 +518,25 @@ function project_menu() {
'type' => MENU_NORMAL_ITEM,
);
+ $items['node/%project_node/maintainers'] = array(
+ 'title' => 'Maintainers',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('project_maintainers_form', 1),
+ 'access callback' => 'project_user_access',
+ 'access arguments' => array(1, 'administer maintainers'),
+ 'file' => 'includes/project_maintainers.inc',
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 4,
+ );
+ $items['node/%project_node/maintainers/delete/%user'] = array(
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('project_maintainer_delete_confirm', 1, 4),
+ 'access callback' => 'project_user_access',
+ 'access arguments' => array(1, 'administer maintainers'),
+ 'file' => 'includes/project_maintainers.inc',
+ 'type' => MENU_CALLBACK,
+ );
+
$items['node/%project_edit_project/edit/project'] = array(
'title' => 'Project',
'page callback' => 'node_page',
@@ -567,7 +586,16 @@ function project_edit_project_load($nid) {
return project_node_load($nid);
}
-function project_check_admin_access($project, $cvs_access = NULL) {
+/**
+ * See if the current user has the given permission on a given project.
+ *
+ * @param $project
+ * The project to check access against. Can be either a numeric node ID
+ * (nid) or a fully-loaded $node object.
+ * @param $permission
+ * The string representing the permission to check access for.
+ */
+function project_user_access($project, $permission) {
global $user;
if (empty($user->uid)) {
return FALSE;
@@ -578,31 +606,22 @@ function project_check_admin_access($project, $cvs_access = NULL) {
return FALSE;
}
+ // If the user has the site-wide admin permission, always grant access.
if (user_access('administer projects')) {
return TRUE;
}
- // If $cvs_access is not defined, check to make sure the user has cvs access
- // and that the user's cvs account is approved.
- if (project_use_cvs($project_obj) && !isset($cvs_access)) {
- if (db_result(db_query("SELECT COUNT(*) FROM {cvs_accounts} WHERE uid = %d AND status = %d", $user->uid, CVS_APPROVED))) {
- $cvs_access = TRUE;
- }
- else {
- $cvs_access = FALSE;
- }
- }
-
if (user_access('maintain projects')) {
+ // Project owners are treated as super users and can always access.
if ($user->uid == $project_obj->uid) {
return TRUE;
}
- if (project_use_cvs($project_obj) && $cvs_access) {
- if (db_result(db_query("SELECT COUNT(*) FROM {cvs_project_maintainers} WHERE uid = %d AND nid = %d", $user->uid, $project_obj->nid))) {
- return TRUE;
- }
- }
+
+ // Otherwise, see if the user has the right permission for this project.
+ return !empty($project_obj->project['maintainers'][$user->uid]['permissions'][$permission]);
}
+
+ // If we haven't granted access yet, deny it.
return FALSE;
}
@@ -693,11 +712,11 @@ function project_project_access($op, $node, $account) {
switch ($op) {
case 'view':
// Since this function is shared for project_release nodes, we have to
- // be careful what node we pass to project_check_admin_access().
+ // be careful what node we pass to project_user_access().
if ($node->type == 'project_release') {
$node = node_load($node->project_release['pid']);
}
- if (project_check_admin_access($node)) {
+ if (project_user_access($node, 'administer settings')) {
return TRUE;
}
if (!user_access('access projects')) {
@@ -717,7 +736,7 @@ function project_project_access($op, $node, $account) {
}
break;
case 'update':
- if (project_check_admin_access($node)) {
+ if (project_user_access($node, 'administer settings')) {
return TRUE;
}
break;
@@ -730,12 +749,136 @@ function project_project_access($op, $node, $account) {
}
/**
+ * Load all per-project permission information and return it.
+ *
+ * This invokes hook_project_permission_info() and
+ * hook_project_permission_alter(), and caches the results in RAM.
+ *
+ * @see hook_project_permission_info()
+ * @see hook_project_permission_alter()
+ * @see drupal_alter()
+ */
+function project_permission_load() {
+ static $project_permissions = array();
+ if (empty($project_permissions)) {
+ $project_permissions = module_invoke_all('project_permission_info');
+ drupal_alter('project_permission', $project_permissions);
+ }
+ return $project_permissions;
+}
+
+/**
+ * Implement hook_project_permission_info()
+ */
+function project_project_permission_info() {
+ return array(
+ 'administer settings' => array(
+ 'title' => t('Administer settings'),
+ 'description' => t('Allows a user to edit a project and modify its settings.'),
+ ),
+ 'administer maintainers' => array(
+ 'title' => t('Administer maintainers'),
+ 'description' => t('Allows a user to add and remove other project maintainers and to modify their permissions.'),
+ ),
+ );
+}
+
+/**
+ * Save the permissions associated with a maintainer for a given project.
+ *
+ * This creates a new maintainer record if none currently exists. Furthermore,
+ * it invokes hook_project_maintainer_save() to give other modules a chance to
+ * act on the fact that a maintainer is being saved.
+ *
+ * @param $nid
+ * The Project NID to update the maintainer for.
+ * @param $uid
+ * The user ID of the maintainer to update.
+ * @param array $permissions
+ * Associative array of which project-level permissions the maintainer
+ * should have. The keys are permission names, and the values are if the
+ * permission should be granted or not.
+ *
+ * @see hook_project_maintainer_save()
+ * @see hook_project_permission_info()
+ */
+function project_maintainer_save($nid, $uid, $permissions = array()) {
+ // Try to update an existing record, if any.
+ db_query("UPDATE {project_maintainer} SET administer_settings = %d, administer_maintainers = %d WHERE nid = %d AND uid = %d", !empty($permissions['administer settings']), !empty($permissions['administer maintainers']), $nid, $uid);
+ if (!db_affected_rows()) {
+ // Didn't update anything, add this as a new maintainer, instead.
+ db_query("INSERT INTO {project_maintainer} (nid, uid, administer_settings, administer_maintainers) VALUES (%d, %d, %d, %d)", $nid, $uid, !empty($permissions['administer settings']), !empty($permissions['administer maintainers']));
+ }
+
+ // Invoke hook_project_maintainer_save() to let other modules know this
+ // maintainer is being saved so they can take any actions or record any
+ // data they need to.
+ module_invoke_all('project_maintainer_save', $nid, $uid, $permissions);
+}
+
+/**
+ * Remove a maintainer from a given project.
+ *
+ * @param $nid
+ * The Project NID to remove the maintainer from.
+ * @param $uid
+ * The user ID of the maintainer to remove.
+ */
+function project_maintainer_remove($nid, $uid) {
+ db_query("DELETE FROM {project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid);
+
+ // Invoke hook_project_maintainer_remove() to let other modules know this
+ // maintainer is being removed so they can take any actions or record any
+ // data they need to.
+ module_invoke_all('project_maintainer_remove', $nid, $uid);
+}
+
+/**
+ * Load all the per-project maintainer info for a given project.
+ *
+ * @param $nid
+ * Node ID of the project to load maintainer info about.
+ *
+ * @return
+ * Array of maintainer info for the given project.
+ *
+ * @see hook_project_maintainer_project_load().
+ */
+function project_maintainer_project_load($nid) {
+ $maintainers = array();
+
+ // We don't want to load all the permissions here, just the ones that
+ // Project itself is responsible for, so we use our implementation of the
+ // hook, instead of the global load function.
+ $project_perms = project_project_permission_info();
+ $query = db_query('SELECT u.name, pm.* FROM {project_maintainer} pm INNER JOIN {users} u ON pm.uid = u.uid WHERE pm.nid = %d ORDER BY u.name', $nid);
+ while ($maintainer = db_fetch_object($query)) {
+ $maintainers[$maintainer->uid]['name'] = $maintainer->name;
+ foreach ($project_perms as $perm_name => $perm_info) {
+ $db_field = str_replace(' ', '_', $perm_name);
+ $maintainers[$maintainer->uid]['permissions'][$perm_name] = $maintainer->$db_field;
+ }
+ }
+
+ // Invoke hook_project_maintainer_project_load(). We can't use
+ // module_invoke_all() since we want a reference to the $maintainers array.
+ foreach (module_implements('project_maintainer_project_load') as $module) {
+ $function_name = $module . '_project_maintainer_project_load';
+ $function_name($nid, $maintainers);
+ }
+
+ return $maintainers;
+}
+
+/**
* Implement hook_load().
*/
function project_project_load($node) {
$additions = db_fetch_array(db_query('SELECT * FROM {project_projects} WHERE nid = %d', $node->nid));
$project = new stdClass;
$project->project = $additions;
+ $project->project['maintainers'] = project_maintainer_project_load($node->nid);
+
return $project;
}
@@ -996,6 +1139,12 @@ function project_theme() {
'title' => NULL,
),
),
+ 'project_maintainers_form' => array(
+ 'file' => 'includes/project_maintainers.inc',
+ 'arguments' => array(
+ 'form' => NULL,
+ ),
+ ),
'project_project_node_form_taxonomy' => array(
'file' => 'project.inc',
'arguments' => array(
diff --git project.test project.test
index bee2cd5..46a0652 100644
--- project.test
+++ project.test
@@ -12,6 +12,75 @@ class ProjectWebTestCase extends DrupalWebTestCase {
// We can't call parent::setUp() with a single array argument, so we need
// this ugly call_user_func_array().
call_user_func_array(array($this, 'parent::setUp'), $modules);
+
+ $perms = array('maintain projects', 'access user profiles', 'access projects');
+
+ $this->owner = $this->drupalCreateUser($perms);
+ $this->drupalLogin($this->owner);
+
+ $this->maintainer = $this->drupalCreateUser($perms);
+ }
+
+ /**
+ * Assert that a field in the current page is enabled.
+ * @TODO Remove this when http://drupal.org/node/882564 is committed.
+ *
+ * @param $name
+ * name of field to assert.
+ * @param $message
+ * Message to display.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertFieldEnabled($name, $message = '') {
+ $elements = $this->xpath('//input[@name="' . $name . '"]');
+ return $this->assertTrue(isset($elements[0]) && empty($elements[0]['disabled']), $message ? $message : t('Field @name is enabled.', array('@name' => $name)));
+ }
+
+ /**
+ * Assert that a field in the current page is disabled.
+ * @TODO Remove this when http://drupal.org/node/882564 is committed.
+ *
+ * @param $name
+ * name of field to assert.
+ * @param $message
+ * Message to display.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertFieldDisabled($name, $message = '') {
+ $elements = $this->xpath('//input[@name="' . $name . '"]');
+ return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['disabled']), $message ? $message : t('Field @name is disabled.', array('@name' => $name)));
+ }
+
+ /**
+ * Assert that a checkbox field in the current page is not checked.
+ *
+ * @param $name
+ * name of field to assert.
+ * @param $message
+ * Message to display.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ protected function assertNoFieldCheckedByName($name, $message = '') {
+ $elements = $this->xpath('//input[@name="' . $name . '"]');
+ return $this->assertTrue(isset($elements[0]) && empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is not checked.', array('@id' => $name)), t('Browser'));
+ }
+
+ /**
+ * Assert that a checkbox field in the current page is checked.
+ *
+ * @param $name
+ * name of field to assert.
+ * @param $message
+ * Message to display.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ protected function assertFieldCheckedByName($name, $message = '') {
+ $elements = $this->xpath('//input[@name="' . $name . '"]');
+ return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is checked.', array('@id' => $name)), t('Browser'));
}
/**
@@ -310,3 +379,109 @@ class ProjectDrupalOrgWebTestCase extends ProjectWebTestCase {
}
}
}
+
+class ProjectMaintainersTestCase extends ProjectWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Project maintainers functionality',
+ 'description' => 'Test Project module maintainers access control system.',
+ 'group' => 'Project'
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * Test maintainer permissions.
+ */
+ function testProjectMaintainerPermissions() {
+ // Create project, make sure Maintainers link is shown
+ $project = $this->createProject();
+
+ // Check that owner can access
+ $this->drupalGet("node/$project->nid/edit");
+ $this->assertResponse(200, 'Project owner can edit project.');
+
+ // Check the maintainers tab is shown and owner is included correctly
+ $this->drupalGet("node/$project->nid");
+ $this->assertLink(t('Maintainers'), 0, ('Maintainers tab is shown.'));
+ $this->drupalGet("node/$project->nid/maintainers");
+ $this->assertLink($this->owner->name, 0, ('Project owner is displayed on form.'));
+ $this->assertFieldDisabled("maintainers[{$this->owner->uid}][permissions][administer settings]", 'Checkbox is disabled for project owner');
+ $this->assertFieldDisabled("maintainers[{$this->owner->uid}][permissions][administer maintainers]", 'Checkbox is disabled for project owner');
+ $this->assertFieldCheckedByName("maintainers[{$this->owner->uid}][permissions][administer settings]", 'Owners permissions are automatically granted');
+ $this->assertFieldCheckedByName("maintainers[{$this->owner->uid}][permissions][administer maintainers]", 'Owners permissions are automatically granted');
+ $this->assertNoRaw("node/$project->nid/maintainers/delete/{$this->owner->uid}", 'No delete link is displayed for the project owner.');
+
+ // Try to delete the owner anyway and make sure it fails.
+ $this->drupalGet("node/$project->nid/maintainers/delete/{$this->owner->uid}");
+ $this->assertText("You can not delete the project owner ({$this->owner->name}) as a maintainer.", 'Project owner can not be deleted as a maintainer.');
+
+ // Verify that other users do not have access
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid/edit");
+ $this->assertResponse(403, 'Project edit form is protected.');
+ $this->drupalGet("node/$project->nid/maintainers");
+ $this->assertResponse(403, 'Project maintainers form is protected.');
+ $this->drupalGet("node/$project->nid/maintainers/delete/{$this->maintainer->uid}");
+ $this->assertResponse(403, 'Project delete maintainer form is protected.');
+
+ // Add a new user and verify that they are added:
+ // Login as owner
+ $this->drupalLogin($this->owner);
+ // Add new user
+ $edit = array();
+ $edit['new_maintainer[user]'] = $this->maintainer->name;
+ $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update'));
+ $this->assertLink($this->maintainer->name, 0, 'New user is displayed on form correctly.');
+ $this->assertNoFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer settings]", 'Permissions not explicitly granted.');
+ $this->assertNoFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer maintainers]", 'Permissions not explicitly granted.');
+
+ // Test validation for adding a duplicate maintainer
+ $edit = array();
+ $edit['new_maintainer[user]'] = $this->maintainer->name;
+ $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update'));
+ $this->assertText("{$this->maintainer->name} is already a maintainer of this project.", 'Duplicate maintainers are not permitted.');
+
+ // Add permissions to user
+ $edit = array();
+ $edit["maintainers[{$this->maintainer->uid}][permissions][administer settings]"] = TRUE;
+ $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update'));
+ $this->assertFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer settings]", 'Permissions are displayed correctly on maintainers form.');
+ // Login as maintainer and check access
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid/edit");
+ $this->assertResponse(200, 'User is correctly granted access to project edit form.');
+ $this->drupalGet("node/$project->nid/maintainers");
+ $this->assertResponse(403, 'Project maintainers form is protected.');
+ $this->drupalGet("node/$project->nid/maintainers/delete/{$this->maintainer->uid}");
+ $this->assertResponse(403, 'Project delete maintainer form is protected.');
+
+ // Have owner grant administer maintainers permission
+ $this->drupalLogin($this->owner);
+ // Add permissions to user
+ $edit = array();
+ $edit["maintainers[{$this->maintainer->uid}][permissions][administer maintainers]"] = TRUE;
+ $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update'));
+ $this->assertFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer maintainers]", 'Permissions are displayed correctly on maintainers form.');
+ // Login as maintainer and check access
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid/maintainers");
+ $this->assertResponse(200, 'User is correctly granted access to project edit form.');
+
+ // Remove the user from the project
+ $this->drupalLogin($this->owner);
+ $this->drupalGet("node/$project->nid/maintainers/delete/{$this->maintainer->uid}");
+ $this->assertText("Are you sure you want to delete {$this->maintainer->name} as a maintainer of {$project->title}?", 'Deletion page is displayed properly.');
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->assertText("Removed {$this->maintainer->name} as a maintainer.", 'Project maintainer successfully deleted.');
+ // Verify that access has been removed
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid/edit");
+ $this->assertResponse(403, 'Project edit form is protected.');
+ $this->drupalGet("node/$project->nid/maintainers");
+ $this->assertResponse(403, 'Project maintainers form is protected.');
+ }
+}
diff --git release/includes/release_node_form.inc release/includes/release_node_form.inc
index 810cc35..e663881 100644
--- release/includes/release_node_form.inc
+++ release/includes/release_node_form.inc
@@ -24,7 +24,7 @@ function _project_release_form(&$release, &$form_state) {
}
// Make sure this user should have permissions to add releases for
// the requested project
- if (!project_check_admin_access($project)) {
+ if (!project_user_access($project, 'administer releases')) {
drupal_access_denied();
module_invoke_all('exit');
exit;
diff --git release/project_release.install release/project_release.install
index b96b174..435629c 100644
--- release/project_release.install
+++ release/project_release.install
@@ -351,6 +351,35 @@ function project_release_schema() {
'expire' => array('expire')
),
);
+
+ $schema['project_release_project_maintainer'] = array(
+ 'description' => t('Users who have various per-project maintainer permissions.'),
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => t('Foreign key: {project_projects}.nid of the project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'uid' => array(
+ 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'administer_releases' => array(
+ 'description' => t('Can this user create and administer releases for the given project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('nid', 'uid'),
+ );
+
return $schema;
}
@@ -706,3 +735,52 @@ function project_release_update_6009() {
}
return $ret;
}
+
+/**
+ * Add the {project_release_project_maintainer} table.
+ */
+function project_release_update_6010() {
+ $ret = array();
+
+ $table = array(
+ 'description' => t('Users who have various per-project maintainer permissions.'),
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => t('Foreign key: {project_projects}.nid of the project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'uid' => array(
+ 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'administer_releases' => array(
+ 'description' => t('Can this user create and administer releases for the given project.'),
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('nid', 'uid'),
+ );
+ db_create_table($ret, 'project_release_project_maintainer', $table);
+
+ // Initially populate the table so that every project owner has full
+ // powers on their own projects.
+ $ret[] = update_sql("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_releases) SELECT nid, uid, 1 FROM {node} WHERE type = 'project_project'");
+
+ // If CVS module is enabled, also populate the table from the
+ // {cvs_project_maintainers} table so that anyone with CVS access
+ // who is not the project owner can administer releases.
+ if (module_exists('cvs')) {
+ $ret[] = update_sql("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_releases) SELECT cpm.nid, cpm.uid, 1 FROM {cvs_project_maintainers} cpm INNER JOIN {node} n ON cpm.nid = n.nid WHERE cpm.uid != n.uid");
+ }
+
+ return $ret;
+}
diff --git release/project_release.module release/project_release.module
index 71b26d3..2df45cd 100644
--- release/project_release.module
+++ release/project_release.module
@@ -40,8 +40,8 @@ function project_release_menu() {
'title' => 'Releases',
'page callback' => 'project_release_project_edit_releases',
'page arguments' => array(1),
- 'access callback' => 'node_access',
- 'access arguments' => array('update', 1),
+ 'access callback' => 'project_user_access',
+ 'access arguments' => array(1, 'administer releases'),
'type' => MENU_LOCAL_TASK,
'file' => 'includes/project_edit_releases.inc',
);
@@ -120,7 +120,7 @@ function project_release_access($op, $node, $account) {
// 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->project_release['pid']);
+ 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;
@@ -142,6 +142,49 @@ function project_release_node_info() {
}
/**
+ * Implement hook_project_permission_info()
+ */
+function project_release_project_permission_info() {
+ 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) {
@@ -609,7 +652,7 @@ function project_release_view($node, $teaser = FALSE, $page = FALSE) {
}
// Display packaging errors to admins.
- if (project_check_admin_access($node->project_release['pid'])) {
+ 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);
@@ -690,7 +733,7 @@ function project_release_get_releases($project, $nodes = TRUE, $sort_by = 'versi
$where = '';
$join = '';
$args = array($project->nid);
- if (!project_check_admin_access($project)) {
+ if (!project_user_access($project, 'administer releases')) {
if (!empty($rids)) {
$where = "AND (n.status = %d OR n.nid IN (". db_placeholders($rids) ."))";
$args[] = 1;
@@ -1230,7 +1273,7 @@ function project_release_project_page_link_alter(&$links, $node) {
),
);
- if (project_check_admin_access($node->nid)) {
+ 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');
}
diff --git release/project_release.test release/project_release.test
new file mode 100644
index 0000000..29ad39b
--- /dev/null
+++ release/project_release.test
@@ -0,0 +1,97 @@
+ 'Project release maintainers functionality',
+ 'description' => 'Test Project release maintainers access control system.',
+ 'group' => 'Project Release'
+ );
+ }
+
+ function setUp() {
+ parent::setUp('project_release', 'views');
+ }
+
+ /**
+ * Test maintainer permissions.
+ */
+ function testProjectMaintainerPermissions() {
+ // Create project, make sure Maintainers link is shown
+ $project = $this->createProject();
+
+ // Check that project_release permissions show up correctly
+ $this->drupalGet("node/$project->nid/edit");
+ $this->assertResponse(200, 'Project owner can edit project.');
+
+ // Check the project release permissions appear correctly
+ $this->drupalGet("node/$project->nid/maintainers");
+ $this->assertFieldCheckedByName("maintainers[{$this->owner->uid}][permissions][administer releases]", 'Checkbox is checked for project owner.');
+ $this->assertFieldDisabled("maintainers[{$this->owner->uid}][permissions][administer releases]", 'Checkbox is disabled for project owner.');
+
+ // Make sure access is properly denied to start with
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid");
+ $this->assertNoLink(t('Add new release'));
+ $this->assertNoLink(t('Administer releases'));
+ $this->drupalGet("node/$project->nid/edit/releases");
+ $this->assertResponse(403, 'Administer releases form is properly protected.');
+ $this->drupalGet("node/add/project-release/$project->nid");
+ $this->assertResponse(403, 'Add new release form is properly protected.');
+
+ // Add permissions and check admin and new release pages again
+ $this->drupalLogin($this->owner);
+ $edit = array();
+ $edit['new_maintainer[user]'] = $this->maintainer->name;
+ $edit['new_maintainer[permissions][administer releases]'] = TRUE;
+ $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update'));
+ $this->assertFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer releases]", 'Permissions are displayed correctly on maintainers form.');
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid");
+ $this->assertLink(t('Add new release'));
+ $this->assertLink(t('Administer releases'));
+ $this->drupalGet("node/$project->nid/edit/releases");
+ $this->assertResponse(200, 'User is correctly granted access to project releases administration form');
+ $this->drupalGet("node/add/project-release/$project->nid");
+ $this->assertResponse(200, 'User is correctly granted access to add new project releases.');
+
+ // Create a project release and check update access
+ $edit = array();
+ $edit['project_release[version_major]'] = '1';
+ $edit['project_release[version_patch]'] = '2';
+ $edit['project_release[version_extra]'] = 'beta';
+ $edit['body'] = $this->randomString(128);
+ $this->drupalPost("node/add/project-release/$project->nid", $edit, t('Save'));
+ $release = $this->drupalGetNodeByTitle("{$project->project['uri']} 1.2-beta");
+ $this->drupalGet("node/$release->nid/edit");
+ $this->assertResponse(200, 'User is correctly granted permission to edit release.');
+
+ // Add errors to project_release_package_errors and check that they are shown
+ $errors = new stdClass;
+ $errors->nid = $release->nid;
+ $errors->messages = serialize(array($this->randomName(), $this->randomName(), $this->randomName()));
+ $success = drupal_write_record('project_release_package_errors', $errors);
+ $this->drupalGet("node/$release->nid");
+ $this->assertText(t('Packaging error messages'), 'Packaging error messages shown correctly.');
+ foreach ($errors->message as $message) {
+ $this->assertText($message);
+ }
+
+ // Remove the permissions and check access
+ $this->drupalLogin($this->owner);
+ $edit = array();
+ $edit["maintainers[{$this->maintainer->uid}][permissions][administer releases]"] = FALSE;
+ $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update'));
+ $this->assertNoFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer releases]", 'Permissions are displayed correctly on maintainers form.');
+ $this->drupalLogin($this->maintainer);
+ $this->drupalGet("node/$project->nid/edit/releases");
+ $this->assertResponse(403, 'Administer releases form is properly protected.');
+ $this->drupalGet("node/add/project-release/$project->nid");
+ $this->assertResponse(403, 'Add new release form is properly protected.');
+ $this->drupalGet("node/$release->nid/edit");
+ $this->assertResponse(403, 'User is correctly denied permission to edit release.');
+ $this->drupalGet("node/$release->nid");
+ $this->assertNoText(t('Packaging error messages'), 'Packaging error messages not shown.');
+ }
+}