'textfield', '#title' => t('Freely accessible paths'), '#description' => t('A space-separated list of PHP regular expressions for directories or files that will always be granted commit access to everyone, no matter what other commit restrictions are imposed. Example: "@.*\.(po|pot)$@ @^/contributions/(docs|sandbox|tricks)/@"'), '#default_value' => isset($restrictions['allowed_paths']) ? implode(' ', $restrictions['allowed_paths']) : '', '#size' => 60, '#weight' => 10, ); $form['commit_restrictions']['deny_undefined_paths'] = array( '#type' => 'checkbox', '#title' => t('Deny access to all other paths'), '#description' => t('If this is enabled, no paths other than the ones given above will be granted commit access, except if there is an exception that specifically allows the commit to happen.'), '#default_value' => isset($restrictions['deny_undefined_paths']) ? FALSE : $restrictions['deny_undefined_paths'], '#weight' => 11, ); $form['commit_restrictions']['forbidden_paths'] = array( '#type' => 'textfield', '#title' => t('Forbidden paths'), '#description' => t('A space-separated list of PHP regular expressions for directories or files that will be denied access to everyone, except if there is an exception that specifically allows the commit to happen. Example: "@^/contributions/profiles.*(?<!\.profile|\.txt)$@ @^.*\.(gz|tgz|tar|zip)$@"'), '#default_value' => isset($restrictions['forbidden_paths']) ? implode(' ', $restrictions['forbidden_paths']) : '', '#size' => 60, '#weight' => 12, ); } if (in_array(VERSIONCONTROL_CAPABILITY_BRANCH_TAG_RESTRICTIONS, $backend_capabilities)) { $form['branch_tag_restrictions'] = array( '#type' => 'fieldset', '#title' => t('Branch and tag restrictions'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 7, ); $form['branch_tag_restrictions']['valid_branch_tag_paths'] = array( '#type' => 'textfield', '#title' => t('Allowed paths for branches and tags'), '#description' => t('A space-separated list of PHP regular expressions for directories or files where it will be possible to create branches and tags. If empty, branches and tags may be created anywhere in the repository. Example: "@^(/[^/]+)?/(modules|themes|theme-engines|docs|translations)/@"'), '#default_value' => isset($restrictions['valid_branch_tag_paths']) ? implode(' ', $restrictions['valid_branch_tag_paths']) : '', '#size' => 60, ); $form['branch_tag_restrictions']['valid_branches'] = array( '#type' => 'textfield', '#title' => t('Valid branches'), '#description' => t('A space-separated list of PHP regular expressions for allowed branch names. If empty, all branch names will be allowed. Example: "@^HEAD$@ @^DRUPAL-5(--[2-9])?$@ @^DRUPAL-6--[1-9]$@"'), '#default_value' => isset($restrictions['valid_branches']) ? implode(' ', $restrictions['valid_branches']) : '', '#size' => 60, ); $form['branch_tag_restrictions']['valid_tags'] = array( '#type' => 'textfield', '#title' => t('Valid tags'), '#description' => t('A space-separated list of PHP regular expressions for allowed tag names. If empty, all tag names will be allowed. Example: "@^DRUPAL-[56]--(\d+)-(\d+)(-[A-Z0-9]+)?$@"'), '#default_value' => isset($restrictions['valid_tags']) ? implode(' ', $restrictions['valid_tags']) : '', '#size' => 60, ); } } } /** * Implementation of hook_versioncontrol_repository_submit(): * Extract repository data from the repository editing/adding form's submitted * values, and add it to the @p $repository array for automatic storage. */ function commit_restrictions_versioncontrol_repository_submit(&$repository, $form, $form_state) { $keys = array( 'allowed_paths', 'forbidden_paths', 'valid_branches', 'valid_tags', 'valid_branch_tag_paths', ); $restrictions = array(); // Only fill in those values that are actually set. foreach ($keys as $key) { if (!empty($form_state['values'][$key])) { $restrictions[$key] = array_filter(explode(' ', $form_state['values'][$key])); } } if (!empty($form_state['values']['deny_undefined_paths'])) { $restrictions['deny_undefined_paths'] = (bool) $form_state['values']['deny_undefined_paths']; } if (empty($restrictions)) { unset($repository['data']['commit_restrictions']); } else { $repository['data']['commit_restrictions'] = $restrictions; } } /** * Implementation of hook_versioncontrol_write_access(): * Restrict, ignore or explicitly allow a commit, branch or tag operation * for a repository that is connected to the Version Control API * by VCS specific hook scripts. * * @return * An array with error messages (without trailing newlines) if the operation * should not be allowed, or an empty array if you're indifferent, * or TRUE if the operation should be allowed no matter what other * write access callbacks say. */ function commit_restrictions_versioncontrol_write_access($operation, $operation_items) { // If no commit restrictions are defined, don't deny access. if (empty($operation['repository']['data']['commit_restrictions'])) { return array(); } // Allow the committer to delete branches and labels (also invalid ones), // provided that nothing else is done in this operation. if (_commit_restrictions_contains_only_deleted_labels($operation)) { return array(); } $restrictions = $operation['repository']['data']['commit_restrictions']; $error_messages = _commit_restrictions_label_access($operation, $restrictions); if (!empty($error_messages)) { return $error_messages; } switch ($operation['type']) { case VERSIONCONTROL_OPERATION_COMMIT: return _commit_restrictions_commit_item_access($operation_items, $restrictions); case VERSIONCONTROL_OPERATION_BRANCH: case VERSIONCONTROL_OPERATION_TAG: // Make sure that branches may be created at all for all of these items. return _commit_restrictions_branch_tag_item_access($operation_items, $restrictions); } } function _commit_restrictions_contains_only_deleted_labels($operation) { if (empty($operation['labels'])) { return FALSE; // "only deleted labels" != "no deleted labels" } foreach ($operation['labels'] as $label) { if ($label['action'] != VERSIONCONTROL_ACTION_DELETED) { return FALSE; } } return TRUE; } /** * Implementation of hook_versioncontrol_write_access() for commit operations. * * @return * An empty array if the all items are allowed to be committed, or an array * with error messages if at least one item may not be committed. */ function _commit_restrictions_commit_item_access($operation_items, $restrictions) { if (empty($operation_items)) { return array(); // no idea if this is ever going to happen, but let's be prepared } $error_messages = array(); // Paths where it is always allowed to commit. if (!empty($restrictions['allowed_paths'])) { foreach ($operation_items as $item) { $always_allow = FALSE; foreach ($restrictions['allowed_paths'] as $allowed_path_regexp) { if (versioncontrol_preg_item_match($allowed_path_regexp, $item)) { $always_allow = TRUE; break; // ok, this item is fine, next one } } // If only one single item is not always allowed, // we won't always allow the commit. Makes sense, right? if (!$always_allow) { // Store error messages for the 'deny_undefined_paths' case below. $error_messages[] = _commit_restrictions_item_error_message($item, 'commit'); break; } } if ($always_allow) { return TRUE; } } // The repository admin can choose to deny everything that is not // explicitely allowed. if (!empty($restrictions['deny_undefined_paths'])) { return $error_messages; } // Reset error messages, we only deny explicitely forbidden paths. $error_messages = array(); // Paths where it is explicitely forbidden to commit. if (!empty($restrictions['forbidden_paths'])) { foreach ($operation_items as $item) { foreach ($restrictions['forbidden_paths'] as $forbidden_path_regexp) { if (!versioncontrol_preg_item_match($forbidden_path_regexp, $item)) { $error_messages[] = _commit_restrictions_item_error_message($item, 'commit'); } } } } return $error_messages; } /** * Determine if the operation labels may be created or modified. * * @return * An empty array if the each of the labels matches at least one of the * valid label regexps (or if there are no regexps to be matched), * or an array filled with error messages if at least one label doesn't. */ function _commit_restrictions_label_access($operation, $restrictions) { $error_messages = array(); $valid_branches = empty($restrictions['valid_branches']) ? array() : $restrictions['valid_branches']; $valid_tags = empty($restrictions['valid_tags']) ? array() : $restrictions['valid_tags']; // This code will work for both branches and tags, given some preset values. $labelinfos = array( VERSIONCONTROL_OPERATION_BRANCH => array( 'valid_restrictions' => $valid_branches, 'other_restrictions' => $valid_tags, 'simple_error' => t('** ERROR: the !labelname branch is not allowed in this repository.'), 'confusion_error' => t( '** ERROR: "!labelname" is a valid name for a tag, but not for a branch. ** You must either create a tag with this name, or choose a valid branch name.'), ), VERSIONCONTROL_OPERATION_TAG => array( 'valid_restrictions' => $valid_tags, 'other_restrictions' => $valid_branches, 'simple_error' => '** ERROR: the !labelname tag is not allowed in this repository.', 'confusion_error' => t( '** ERROR: "!labelname" is a valid name for a branch, but not for a tag. ** You must either create a branch with this name, or choose a valid tag name.'), ), ); foreach ($operation['labels'] as $label) { if ($label['action'] == VERSIONCONTROL_ACTION_DELETED) { continue; // we don't want no errors for deleted labels, skip those } $labelinfo = $labelinfos[$label['type']]; // Make sure that the assigned branch name is allowed. if (!empty($labelinfo['valid_restrictions'])) { $allowed = FALSE; foreach ($labelinfo['valid_restrictions'] as $valid_regexp) { if (preg_match($valid_regexp, $label['name'])) { $allowed = TRUE; break; } } if (!$allowed) { // no branch regexps match this branch, so deny access $error = strtr($labelinfo['simple_error'], array('!labelname' => $label['name'])); // The user might have mistaken tags for branches - // in that case, we should explain how it actually works. if (!empty($labelinfo['other_restrictions'])) { foreach ($labelinfo['other_restrictions'] as $valid_other_regexp) { if (preg_match($valid_other_regexp, $label['name'])) { $error = strtr($labelinfo['confusion_error'], array( '!labelname' => $label['name'] )); } } } $error_messages[] = $error; } // end of if (!$allowed) } // end of if (!empty($restrictions['valid_restrictions'])) } // end of foreach ($operation['labels']) return $error_messages; } /** * Determine if the items that are being branched or tagged are matching * at least one of the valid branch/tag paths regexps, and return * an appropriate error message array. * * @return * An empty array if the each of the items matches at least one of the * valid path regexps (or if there are no regexps to be matched), * or an array filled with error messages if at least one item doesn't. */ // FIXME: ideally we should be doing this per label (if a commit operation has // multiple labels) but we don't know which items belong to which label. // That would need an adaptation of the operation/items format. Bummer. function _commit_restrictions_branch_tag_item_access($items, $restrictions) { if (empty($items)) { // Tagging the whole repository (== empty $items array) should be caught // by general branch/tag restrictions (_commit_restrictions_label_access()) // rather than with the item path restrictions in here. So let's pass // operations without items through here. Consequently, the regexps for // allowed branch/tag paths won't work in version control systems like // Git or Mercurial that tend to always tag the whole repository. return array(); } $error_messages = array(); if (!empty($restrictions['valid_branch_tag_paths'])) { foreach ($items as $item) { $valid = FALSE; foreach ($restrictions['valid_branch_tag_paths'] as $valid_path_regexp) { if (versioncontrol_preg_item_match($valid_path_regexp, $item)) { $valid = TRUE; break; } } if (!$valid) { $error_messages[] = _commit_restrictions_item_error_message($item, 'branch/tag'); } } } return $error_messages; } function _commit_restrictions_item_error_message($item, $message_type) { $itemtype = versioncontrol_is_file_item($item) ? t('file') : t('directory'); $params = array('!itemtype' => $itemtype, '!path' => $item['path']); switch ($message_type) { case 'commit': return t( '** Access denied: committing to this !itemtype is not allowed: ** !path', $params); case 'branch/tag': return t( '** Access denied: creating branches or tags for this !itemtype is not allowed: ** !path', $params); default: return t('Access denied: Internal error in _commit_restrictions_item_error_message().'); } }