commit_restrictions.module

Tracking 5.x-2.x branch
  1. drupal
    1. 5 contributions/versioncontrol/commit_restrictions/commit_restrictions.module

Commit Restrictions - Restrict commits, branches and tags based on item path or branch/tag name.

Copyright 2007, 2008 by Jakob Petsovits ("jpetso", http://drupal.org/user/56020)

Functions & methods

NameDescription
commit_restrictions_form_alterImplementation of hook_form_alter(): Add commit restriction settings to the add/edit repository form of the Version Control API module.
commit_restrictions_versioncontrol_extract_repository_dataImplementation of hook_versioncontrol_extract_repository_data(): Extract commit restriction repository additions from the repository editing/adding form's submitted values.
commit_restrictions_versioncontrol_repositoryImplementation of hook_versioncontrol_repository(): Manage (insert, update or delete) additional repository data in the database.
commit_restrictions_versioncontrol_write_accessImplementation 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.
_commit_restrictions_branch_tag_item_access
_commit_restrictions_commit_item_accessImplementation of hook_versioncontrol_write_access() for commit operations.
_commit_restrictions_contains_only_delete_labels
_commit_restrictions_item_error_message
_commit_restrictions_label_accessDetermine if the operation labels may be created or modified.
_commit_restrictions_loadRetrieve a structured array with the database values of the {commit_restrictions} table as array elements. The allowed/forbidden lists already appear as arrays, not as space-separated strings.

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Commit Restrictions - Restrict commits, branches and tags
  5. * based on item path or branch/tag name.
  6. *
  7. * Copyright 2007, 2008 by Jakob Petsovits ("jpetso", http://drupal.org/user/56020)
  8. */
  9. /**
  10. * Implementation of hook_form_alter(): Add commit restriction settings
  11. * to the add/edit repository form of the Version Control API module.
  12. */
  13. function commit_restrictions_form_alter($form_id, &$form) {
  14. if ($form['#id'] == 'versioncontrol-repository-form') {
  15. $backends = versioncontrol_get_backends();
  16. $backend_capabilities = $backends[$form['#vcs']]['capabilities'];
  17. $repo_id = $form['repo_id']['#value'];
  18. $restrictions = _commit_restrictions_load($repo_id);
  19. if (in_array(VERSIONCONTROL_CAPABILITY_COMMIT_RESTRICTIONS, $backend_capabilities)) {
  20. $form['directory_restrictions'] = array(
  21. '#type' => 'fieldset',
  22. '#title' => t('Commit restrictions'),
  23. '#collapsible' => TRUE,
  24. '#collapsed' => TRUE,
  25. '#weight' => 6,
  26. );
  27. $form['directory_restrictions']['allowed_paths'] = array(
  28. '#type' => 'textfield',
  29. '#title' => t('Freely accessible paths'),
  30. '#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)/@"'),
  31. '#default_value' => implode(' ', $restrictions['allowed_paths']),
  32. '#size' => 60,
  33. );
  34. $form['directory_restrictions']['deny_undefined_paths'] = array(
  35. '#type' => 'checkbox',
  36. '#title' => t('Deny access to all other paths'),
  37. '#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.'),
  38. '#default_value' => $restrictions['deny_undefined_paths'],
  39. );
  40. $form['directory_restrictions']['forbidden_paths'] = array(
  41. '#type' => 'textfield',
  42. '#title' => t('Forbidden paths'),
  43. '#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.*(?&lt;!\.profile|\.txt)$@ @^.*\.(gz|tgz|tar|zip)$@"'),
  44. '#default_value' => implode(' ', $restrictions['forbidden_paths']),
  45. '#size' => 60,
  46. );
  47. }
  48. if (in_array(VERSIONCONTROL_CAPABILITY_BRANCH_TAG_RESTRICTIONS, $backend_capabilities)) {
  49. $form['branch_tag_restrictions'] = array(
  50. '#type' => 'fieldset',
  51. '#title' => t('Branch and tag restrictions'),
  52. '#collapsible' => TRUE,
  53. '#collapsed' => TRUE,
  54. '#weight' => 7,
  55. );
  56. $form['branch_tag_restrictions']['valid_branch_tag_paths'] = array(
  57. '#type' => 'textfield',
  58. '#title' => t('Allowed paths for branches and tags'),
  59. '#description' => t('A space-separated list of PHP regular expressions for directories or files where it will be possible to create branches and tags. Example: "@^(/[^/]+)?/(modules|themes|theme-engines|docs|translations)/@"'),
  60. '#default_value' => implode(' ', $restrictions['valid_branch_tag_paths']),
  61. '#size' => 60,
  62. );
  63. $form['branch_tag_restrictions']['valid_branches'] = array(
  64. '#type' => 'textfield',
  65. '#title' => t('Valid branches'),
  66. '#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]$@"'),
  67. '#default_value' => implode(' ', $restrictions['valid_branches']),
  68. '#size' => 60,
  69. );
  70. $form['branch_tag_restrictions']['valid_tags'] = array(
  71. '#type' => 'textfield',
  72. '#title' => t('Valid tags'),
  73. '#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]+)?$@"'),
  74. '#default_value' => implode(' ', $restrictions['valid_tags']),
  75. '#size' => 60,
  76. );
  77. }
  78. }
  79. }
  80. /**
  81. * Implementation of hook_versioncontrol_extract_repository_data():
  82. * Extract commit restriction repository additions from the repository
  83. * editing/adding form's submitted values.
  84. */
  85. function commit_restrictions_versioncontrol_extract_repository_data($form_values) {
  86. $allowed_paths = empty($form_values['allowed_paths'])
  87. ? array() : array_filter(explode(' ', $form_values['allowed_paths']));
  88. $forbidden_paths = empty($form_values['forbidden_paths'])
  89. ? array() : array_filter(explode(' ', $form_values['forbidden_paths']));
  90. $deny_undefined_paths = isset($form_values['deny_undefined_paths'])
  91. ? FALSE : $form_values['deny_undefined_paths'];
  92. $valid_branch_tag_paths = empty($form_values['valid_branch_tag_paths'])
  93. ? array() : array_filter(explode(' ', $form_values['valid_branch_tag_paths']));
  94. $valid_branches = empty($form_values['valid_branches'])
  95. ? array() : array_filter(explode(' ', $form_values['valid_branches']));
  96. $valid_tags = empty($form_values['valid_tags'])
  97. ? array() : array_filter(explode(' ', $form_values['valid_tags']));
  98. return array(
  99. 'commit_restrictions' => array(
  100. 'allowed_paths' => $form_values['allowed_paths'],
  101. 'forbidden_paths' => $form_values['forbidden_paths'],
  102. 'deny_undefined_paths' => $form_values['deny_undefined_paths'],
  103. 'valid_branch_tag_paths' => $form_values['valid_branch_tag_paths'],
  104. 'valid_branches' => $form_values['valid_branches'],
  105. 'valid_tags' => $form_values['valid_tags'],
  106. ),
  107. );
  108. }
  109. /**
  110. * Implementation of hook_versioncontrol_repository():
  111. * Manage (insert, update or delete) additional repository data in the database.
  112. *
  113. * @param $op
  114. * Either 'insert' when the repository has just been created, or 'update'
  115. * when repository name, root, URL backend or module specific data change,
  116. * or 'delete' if it will be deleted after this function has been called.
  117. *
  118. * @param $repository
  119. * The repository array containing the repository. It's a single
  120. * repository array like the one returned by versioncontrol_get_repository(),
  121. * so it consists of the following elements:
  122. *
  123. * - 'repo_id': The unique repository id.
  124. * - 'name': The user-visible name of the repository.
  125. * - 'vcs': The unique string identifier of the version control system
  126. * that powers this repository.
  127. * - 'root': The root directory of the repository. In most cases,
  128. * this will be a local directory (e.g. '/var/repos/drupal'),
  129. * but it may also be some specialized string for remote repository
  130. * access. How this string may look like depends on the backend.
  131. * - 'authorization_method': The string identifier of the repository's
  132. * authorization method, that is, how users may register accounts
  133. * in this repository. Modules can provide their own methods
  134. * by implementing hook_versioncontrol_authorization_methods().
  135. * - 'url_backend': The prefix (excluding the trailing underscore)
  136. * for URL backend retrieval functions.
  137. * - '[xxx]_specific': An array of VCS specific additional repository
  138. * information. How this array looks like is defined by the
  139. * corresponding backend module (versioncontrol_[xxx]).
  140. * - '???': Any other additions that modules added by implementing
  141. * versioncontrol_extract_repository_data().
  142. */
  143. function commit_restrictions_versioncontrol_repository($op, $repository) {
  144. $restrictions = $repository['commit_restrictions'];
  145. switch ($op) {
  146. case 'update':
  147. db_query('DELETE FROM {commit_restrictions} WHERE repo_id = %d',
  148. $repository['repo_id']);
  149. // fall through
  150. case 'insert':
  151. if (isset($restrictions)) {
  152. db_query("INSERT INTO {commit_restrictions}
  153. (repo_id, allowed_paths, forbidden_paths,
  154. deny_undefined_paths, valid_branch_tag_paths,
  155. valid_branches, valid_tags)
  156. VALUES (%d, '%s', '%s', %d, '%s', '%s', '%s')",
  157. $repository['repo_id'], $restrictions['allowed_paths'],
  158. $restrictions['forbidden_paths'], $restrictions['deny_undefined_paths'],
  159. $restrictions['valid_branch_tag_paths'],
  160. $restrictions['valid_branches'], $restrictions['valid_tags']);
  161. }
  162. break;
  163. case 'delete':
  164. db_query('DELETE FROM {commit_restrictions} WHERE repo_id = %d',
  165. $repository['repo_id']);
  166. break;
  167. }
  168. }
  169. /**
  170. * Retrieve a structured array with the database values of the
  171. * {commit_restrictions} table as array elements. The allowed/forbidden lists
  172. * already appear as arrays, not as space-separated strings.
  173. *
  174. * @param $repo_id
  175. * A valid repository id of the repository for which the restrictions
  176. * should be retrieved, or 0 if a default array should be returned instead.
  177. *
  178. * @return
  179. * The mentioned restrictions array, or a default array if no restrictions
  180. * could be found for the given repository.
  181. */
  182. function _commit_restrictions_load($repo_id) {
  183. if ($repo_id) {
  184. $result = db_query('SELECT allowed_paths, forbidden_paths,
  185. deny_undefined_paths, valid_branch_tag_paths,
  186. valid_branches, valid_tags
  187. FROM {commit_restrictions}
  188. WHERE repo_id = %d', $repo_id);
  189. while ($restrictions = db_fetch_object($result)) {
  190. return array(
  191. 'allowed_paths' => empty($restrictions->allowed_paths)
  192. ? array() : explode(' ', $restrictions->allowed_paths),
  193. 'forbidden_paths' => empty($restrictions->forbidden_paths)
  194. ? array() : explode(' ', $restrictions->forbidden_paths),
  195. 'valid_branch_tag_paths' => empty($restrictions->valid_branch_tag_paths)
  196. ? array() : explode(' ', $restrictions->valid_branch_tag_paths),
  197. 'valid_branches' => empty($restrictions->valid_branches)
  198. ? array() : explode(' ', $restrictions->valid_branches),
  199. 'valid_tags' => empty($restrictions->valid_tags)
  200. ? array() : explode(' ', $restrictions->valid_tags),
  201. 'deny_undefined_paths' => ($restrictions->deny_undefined_paths > 0)
  202. ? TRUE : FALSE,
  203. );
  204. }
  205. }
  206. // If $repo_id == 0 or the query didn't return any results,
  207. // return a default array.
  208. return array(
  209. 'allowed_paths' => array(),
  210. 'forbidden_paths' => array(),
  211. 'deny_undefined_paths' => FALSE,
  212. 'valid_branch_tag_paths' => array(),
  213. 'valid_branches' => array(),
  214. 'valid_tags' => array(),
  215. );
  216. }
  217. /**
  218. * Implementation of hook_versioncontrol_write_access():
  219. * Restrict, ignore or explicitly allow a commit, branch or tag operation
  220. * for a repository that is connected to the Version Control API
  221. * by VCS specific hook scripts.
  222. *
  223. * @return
  224. * An array with error messages (without trailing newlines) if the operation
  225. * should not be allowed, or an empty array if you're indifferent,
  226. * or TRUE if the operation should be allowed no matter what other
  227. * write access callbacks say.
  228. */
  229. function commit_restrictions_versioncontrol_write_access($operation, $operation_items) {
  230. // Allow the committer to delete branches and labels (also invalid ones),
  231. // provided that nothing else is done in this operation.
  232. if (_commit_restrictions_contains_only_delete_labels($operation)) {
  233. return array();
  234. }
  235. $restrictions = _commit_restrictions_load($operation['repository']['repo_id']);
  236. $error_messages = _commit_restrictions_label_access($operation, $restrictions);
  237. if (!empty($error_messages)) {
  238. return $error_messages;
  239. }
  240. switch ($operation['type']) {
  241. case VERSIONCONTROL_OPERATION_COMMIT:
  242. return _commit_restrictions_commit_item_access($operation_items, $restrictions);
  243. case VERSIONCONTROL_OPERATION_BRANCH:
  244. case VERSIONCONTROL_OPERATION_TAG:
  245. // Make sure that branches may be created at all for all of these items.
  246. return _commit_restrictions_branch_tag_item_access($operation_items, $restrictions);
  247. }
  248. }
  249. function _commit_restrictions_contains_only_delete_labels($operation) {
  250. if (empty($operation['labels'])) {
  251. return FALSE; // "only delete labels" != "no delete labels"
  252. }
  253. foreach ($operation['labels'] as $label) {
  254. if ($label['action'] != VERSIONCONTROL_ACTION_DELETED) {
  255. return FALSE;
  256. }
  257. }
  258. return TRUE;
  259. }
  260. /**
  261. * Implementation of hook_versioncontrol_write_access() for commit operations.
  262. *
  263. * @return
  264. * An empty array if the all items are allowed to be committed, or an array
  265. * with error messages if at least one item may not be committed.
  266. */
  267. function _commit_restrictions_commit_item_access($operation_items, $restrictions) {
  268. if (empty($operation_items)) {
  269. return array(); // no idea if this is ever going to happen, but let's be prepared
  270. }
  271. $error_messages = array();
  272. // Paths where it is always allowed to commit.
  273. if (!empty($restrictions['allowed_paths'])) {
  274. foreach ($operation_items as $item) {
  275. $always_allow = FALSE;
  276. foreach ($restrictions['allowed_paths'] as $allowed_path_regexp) {
  277. if (versioncontrol_preg_item_match($allowed_path_regexp, $item)) {
  278. $always_allow = TRUE;
  279. break; // ok, this item is fine, next one
  280. }
  281. }
  282. // If only one single item is not always allowed,
  283. // we won't always allow the commit. Makes sense, right?
  284. if (!$always_allow) {
  285. // Store error messages for the 'deny_undefined_paths' case below.
  286. $error_messages[] = _commit_restrictions_item_error_message($item, 'commit');
  287. break;
  288. }
  289. }
  290. if ($always_allow) {
  291. return TRUE;
  292. }
  293. }
  294. // The repository admin can choose to disallow everything that is not
  295. // explicitely allowed.
  296. if ($restrictions['deny_undefined_paths']) {
  297. return $error_messages;
  298. }
  299. // Reset error messages, we only disallow explicitely forbidden paths.
  300. $error_messages = array();
  301. // Paths where it is explicitely forbidden to commit.
  302. if (!empty($restrictions['forbidden_paths'])) {
  303. foreach ($operation_items as $item) {
  304. foreach ($restrictions['forbidden_paths'] as $forbidden_path_regexp) {
  305. if (!versioncontrol_preg_item_match($forbidden_path_regexp, $item)) {
  306. $error_messages[] = _commit_restrictions_item_error_message($item, 'commit');
  307. }
  308. }
  309. }
  310. }
  311. return $error_messages;
  312. }
  313. /**
  314. * Determine if the operation labels may be created or modified.
  315. *
  316. * @return
  317. * An empty array if the each of the labels matches at least one of the
  318. * valid label regexps (or if there are no regexps to be matched),
  319. * or an array filled with error messages if at least one label doesn't.
  320. */
  321. function _commit_restrictions_label_access($operation, $restrictions) {
  322. $error_messages = array();
  323. // This code will work for both branches and tags, given some preset values.
  324. $labelinfos = array(
  325. VERSIONCONTROL_OPERATION_BRANCH => array(
  326. 'valid_restrictions' => $restrictions['valid_branches'],
  327. 'other_restrictions' => $restrictions['valid_tags'],
  328. 'simple_error' => t('** ERROR: the !labelname branch is not allowed in this repository.'),
  329. 'confusion_error' => t(
  330. '** ERROR: "!labelname" is a valid name for a tag, but not for a branch.
  331. ** You must either create a tag with this name, or choose a valid branch name.'),
  332. ),
  333. VERSIONCONTROL_OPERATION_TAG => array(
  334. 'valid_restrictions' => $restrictions['valid_tags'],
  335. 'other_restrictions' => $restrictions['valid_branches'],
  336. 'simple_error' => '** ERROR: the !labelname tag is not allowed in this repository.',
  337. 'confusion_error' => t(
  338. '** ERROR: "!labelname" is a valid name for a branch, but not for a tag.
  339. ** You must either create a branch with this name, or choose a valid tag name.'),
  340. ),
  341. );
  342. foreach ($operation['labels'] as $label) {
  343. if ($label['action'] == VERSIONCONTROL_ACTION_DELETED) {
  344. continue; // we don't want no errors for deleted labels, skip those
  345. }
  346. $labelinfo = $labelinfos[$label['type']];
  347. // Make sure that the assigned branch name is allowed.
  348. if (!empty($labelinfo['valid_restrictions'])) {
  349. $allowed = FALSE;
  350. foreach ($labelinfo['valid_restrictions'] as $valid_regexp) {
  351. if (preg_match($valid_regexp, $label['name'])) {
  352. $allowed = TRUE;
  353. break;
  354. }
  355. }
  356. if (!$allowed) {
  357. // no branch regexps match this branch, so deny access
  358. $error = strtr($labelinfo['simple_error'], array('!labelname' => $label['name']));
  359. // The user might have mistaken tags for branches -
  360. // in that case, we should explain how it actually works.
  361. if (!empty($labelinfo['other_restrictions'])) {
  362. foreach ($labelinfo['other_restrictions'] as $valid_other_regexp) {
  363. if (preg_match($valid_other_regexp, $label['name'])) {
  364. $error = strtr($labelinfo['confusion_error'],
  365. array('!labelname' => $label['name']));
  366. }
  367. }
  368. }
  369. $error_messages[] = $error;
  370. } // end of if (!$allowed)
  371. } // end of if (!empty($restrictions[$valid_restriction]))
  372. } // end of foreach ($operation['labels'])
  373. return $error_messages;
  374. }
  375. /**
  376. * Determine if the items that are being branched or tagged are matching
  377. * at least one of the valid branch/tag paths regexps, and return
  378. * an appropriate error message array.
  379. *
  380. * @return
  381. * An empty array if the each of the items matches at least one of the
  382. * valid path regexps (or if there are no regexps to be matched),
  383. * or an array filled with error messages if at least one item doesn't.
  384. */
  385. // FIXME: ideally we should be doing this per label (if a commit operation has
  386. // multiple labels) but we don't know which items belong to which label.
  387. // That would need an adaptation of the operation/items format. Bummer.
  388. function _commit_restrictions_branch_tag_item_access($items, $restrictions) {
  389. if (empty($items)) {
  390. // Tagging the whole repository (== empty $items array) should be caught
  391. // by general branch/tag restrictions (_commit_restrictions_label_access())
  392. // rather than with the item path restrictions in here. So let's pass
  393. // operations without items through here. Consequently, the regexps for
  394. // allowed branch/tag paths won't work in version control systems like
  395. // Git or Mercurial that tend to always tag the whole repository.
  396. return array();
  397. }
  398. $error_messages = array();
  399. if (!empty($restrictions['valid_branch_tag_paths'])) {
  400. foreach ($items as $item) {
  401. $valid = FALSE;
  402. foreach ($restrictions['valid_branch_tag_paths'] as $valid_path_regexp) {
  403. if (versioncontrol_preg_item_match($valid_path_regexp, $item)) {
  404. $valid = TRUE;
  405. break;
  406. }
  407. }
  408. if (!$valid) {
  409. $error_messages[] = _commit_restrictions_item_error_message($item, 'branch/tag');
  410. }
  411. }
  412. }
  413. return $error_messages;
  414. }
  415. function _commit_restrictions_item_error_message($item, $message_type) {
  416. $itemtype = versioncontrol_is_file_item($item) ? t('file') : t('directory');
  417. $params = array('!itemtype' => $itemtype, '!path' => $item['path']);
  418. switch ($message_type) {
  419. case 'commit':
  420. return t(
  421. '** Access denied: committing to this !itemtype is not allowed:
  422. ** !path', $params);
  423. case 'branch/tag':
  424. return t(
  425. '** Access denied: creating branches or tags for this !itemtype is not allowed:
  426. ** !path', $params);
  427. default:
  428. return t('Access denied: Internal error in _commit_restrictions_item_error_message().');
  429. }
  430. }