workflow_access.module 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. /**
  3. * @file
  4. * Provides node access permissions based on workflow states.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function workflow_access_menu() {
  10. $items = array();
  11. $items["admin/config/workflow/access/%workflow"] = array(
  12. 'title' => 'Access',
  13. 'access arguments' => array('administer workflow'),
  14. 'page callback' => 'drupal_get_form',
  15. 'page arguments' => array('workflow_access_form', 4),
  16. 'type' => MENU_CALLBACK,
  17. );
  18. return $items;
  19. }
  20. /**
  21. * Implements hook_node_grants().
  22. *
  23. * Supply the workflow access grants. We are simply using
  24. * roles as access lists, so rids translate directly to gids.
  25. */
  26. function workflow_access_node_grants($account, $op) {
  27. return array(
  28. 'workflow_access' => array_keys($account->roles),
  29. 'workflow_access_owner' => array($account->uid),
  30. );
  31. }
  32. /**
  33. * Implements hook_node_access_records().
  34. *
  35. * Returns a list of grant records for the passed in node object.
  36. */
  37. function workflow_access_node_access_records($node) {
  38. $grants = array();
  39. if ($state = workflow_get_workflow_node_by_nid($node->nid)) {
  40. $state = workflow_get_workflow_states_by_sid($state->sid);
  41. foreach (workflow_access_get_workflow_access_by_sid($state->sid) as $grant) {
  42. $grants[] = array(
  43. 'realm' => ($grant->rid == -1) ? 'workflow_access_owner' : 'workflow_access',
  44. 'gid' => ($grant->rid == -1) ? $node->uid : $grant->rid,
  45. 'grant_view' => $grant->grant_view,
  46. 'grant_update' => $grant->grant_update,
  47. 'grant_delete' => $grant->grant_delete,
  48. 'priority' => variable_get('workflow_access_priority', 0),
  49. );
  50. }
  51. }
  52. return $grants;
  53. }
  54. /**
  55. * Implements hook_node_access_explain().
  56. * This is a Devel Node Access hook.
  57. */
  58. function workflow_access_node_access_explain($row) {
  59. static $interpretations = array();
  60. switch ($row->realm) {
  61. case 'workflow_access_owner':
  62. $interpretations[$row->gid] = t('Workflow access: author of the content may access');
  63. break;
  64. case 'workflow_access':
  65. $roles = user_roles();
  66. $interpretations[$row->gid] = t('Workflow access: %role may access', array('%role' => $roles[$row->gid]));
  67. break;
  68. }
  69. return (!empty($interpretations[$row->gid]) ? $interpretations[$row->gid] : NULL);
  70. }
  71. /**
  72. * Implements hook_workflow_operations().
  73. * Create action link for access form.
  74. */
  75. function workflow_access_workflow_operations($op, $workflow = NULL, $state = NULL) {
  76. switch ($op) {
  77. case 'workflow':
  78. $alt = t('Control content access for @wf', array('@wf' => $workflow->name));
  79. $actions = array(
  80. 'workflow_access_form' => array(
  81. 'title' => t('Access'),
  82. 'href' => "admin/config/workflow/access/$workflow->wid",
  83. 'attributes' => array('alt' => $alt, 'title' => $alt),
  84. ),
  85. );
  86. return $actions;
  87. }
  88. }
  89. /**
  90. * Implements hook_form().
  91. *
  92. * Add a "three dimensional" (state, role, permission type) configuration
  93. * interface to the workflow edit form.
  94. */
  95. function workflow_access_form($form, $form_state, $workflow) {
  96. if ($workflow) {
  97. drupal_set_title(t('@name Access', array('@name' => $workflow->name)));
  98. }
  99. else {
  100. drupal_set_message(t('Unknown workflow'));
  101. drupal_goto('admin/config/workflow/workflow');
  102. }
  103. $bc = array(l(t('Home'), '<front>'));
  104. $bc[] = l(t('Configuration'), 'admin/config');
  105. $bc[] = l(t('Workflow'), 'admin/config/workflow');
  106. $bc[] = l(t('Workflow'), 'admin/config/workflow/workflow');
  107. $bc[] = l(t($workflow->name), "admin/config/workflow/workflow/$workflow->wid");
  108. // $bc[] = l(t('Access'), "admin/config/workflow/access/$workflow->wid");
  109. drupal_set_breadcrumb($bc);
  110. $form = array('#tree' => TRUE);
  111. $form['#wid'] = $workflow->wid;
  112. // A list of roles available on the site and our
  113. // special -1 role used to represent the node author.
  114. $rids = user_roles(FALSE, 'participate in workflow');
  115. $rids['-1'] = t('author');
  116. // Add a table for every workflow state.
  117. $options = array('status' => 1);
  118. foreach (workflow_get_workflow_states_by_wid($workflow->wid, $options) as $state) {
  119. if ($state->state == t('(creation)')) {
  120. // No need to set perms on creation.
  121. continue;
  122. }
  123. $view = $update = $delete = array();
  124. $count = 0;
  125. foreach (workflow_access_get_workflow_access_by_sid($state->sid) as $access) {
  126. $count++;
  127. if ($access->grant_view) {
  128. $view[] = $access->rid;
  129. }
  130. if ($access->grant_update) {
  131. $update[] = $access->rid;
  132. }
  133. if ($access->grant_delete) {
  134. $delete[] = $access->rid;
  135. }
  136. }
  137. // Allow view grants by default for anonymous and authenticated users,
  138. // if no grants were set up earlier.
  139. if (!$count) {
  140. $view = array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID);
  141. }
  142. // TODO better tables using a #theme function instead of direct #prefixing
  143. $form[$state->sid] = array(
  144. '#type' => 'fieldset',
  145. '#title' => t('@state', array('@state' => $state->state)),
  146. '#collapsible' => TRUE,
  147. '#collapsed' => FALSE,
  148. '#tree' => TRUE,
  149. );
  150. $form[$state->sid]['view'] = array(
  151. '#type' => 'checkboxes',
  152. '#options' => $rids,
  153. '#default_value' => $view,
  154. '#title' => t('Roles who can view posts in this state'),
  155. '#prefix' => '<table width="100%" style="border: 0;"><tbody style="border: 0;"><tr><td>',
  156. );
  157. $form[$state->sid]['update'] = array(
  158. '#type' => 'checkboxes',
  159. '#options' => $rids,
  160. '#default_value' => $update,
  161. '#title' => t('Roles who can edit posts in this state'),
  162. '#prefix' => "</td><td>",
  163. );
  164. $form[$state->sid]['delete'] = array(
  165. '#type' => 'checkboxes',
  166. '#options' => $rids,
  167. '#default_value' => $delete,
  168. '#title' => t('Roles who can delete posts in this state'),
  169. '#prefix' => "</td><td>",
  170. '#suffix' => "</td></tr></tbody></table>",
  171. );
  172. }
  173. $form['warning'] = array(
  174. '#type' => 'markup',
  175. '#markup' => '<p><strong>'
  176. . t('WARNING:')
  177. . '</strong> '
  178. . t('Use of the "Edit any," "Edit own," and even "View published content" permissions
  179. for the content type may override these access settings.')
  180. . '</p>',
  181. );
  182. $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
  183. return $form;
  184. // Place our block comfortably down the page.
  185. $form['submit']['#weight'] = 10;
  186. $form['#submit'][] = 'workflow_access_form_submit';
  187. }
  188. /**
  189. * Store permission settings for workflow states.
  190. */
  191. function workflow_access_form_submit($form, &$form_state) {
  192. foreach ($form_state['values'] as $sid => $access) {
  193. // Ignore irrelevant keys.
  194. if (!is_numeric($sid)) {
  195. continue;
  196. }
  197. $grants = array();
  198. foreach ($access['view'] as $rid => $checked) {
  199. $data = array(
  200. 'sid' => $sid,
  201. 'rid' => $rid,
  202. 'grant_view' => (!empty($checked)) ? (bool) $checked : 0,
  203. 'grant_update' => (!empty($access['update'][$rid])) ? (bool) $access['update'][$rid] : 0,
  204. 'grant_delete' => (!empty($access['delete'][$rid])) ? (bool) $access['delete'][$rid] : 0,
  205. );
  206. $id = workflow_access_insert_workflow_access_by_sid($data);
  207. }
  208. // Update all nodes having same workflow state to reflect new settings.
  209. foreach (workflow_get_workflow_node_by_sid($sid) as $data) {
  210. // Instead of trying to construct what the grants should be per node as we save.
  211. // Let's fall back on existing node grant systems that will find it for us.
  212. $node = node_load($data->nid);
  213. node_access_acquire_grants($node);
  214. }
  215. }
  216. drupal_set_message(t('Workflow access permissions updated.'));
  217. $form_state['redirect'] = 'admin/config/workflow/workflow/' . $form['#wid'];
  218. }
  219. /**
  220. * Implements hook_workflow().
  221. *
  222. * Update grants when a node changes workflow state.
  223. * This is already called when node_save is called.
  224. */
  225. function workflow_access_workflow($op, $old_sid, $sid, $node) {
  226. // ALERT:
  227. // This is a tricky spot when called on node_insert as part of the transition from create to state1.
  228. // node_save invokes this function as a hook before calling node_access_acquire_grants.
  229. // But when it calls node_access_acquire_grants later, it does so without deleting the access
  230. // set when calling workflow_node_insert because it is an insert and no prior grants are expected.
  231. // This leads to a SQL error of duplicate grants which causes a rollback of all changes.
  232. // Unfortunately, setting access rights isn't the only thing we're doing on node_insert so we
  233. // can't skip the whole thing. So we need to fix it further downstream in order to get this to work.
  234. // Here we don't want to run this in the case of (and ONLY in the case of) a brand new node.
  235. // Node access grants will be run as part of node_save's own granting after this.
  236. //
  237. // NOTE: Any module that sets node access rights on insert will hit this situation.
  238. //
  239. if ($old_state = workflow_get_workflow_states_by_sid($old_sid)) {
  240. if ($op == 'transition post' && $old_sid != $sid && (empty($node->is_new) || !$node->is_new)) {
  241. node_access_acquire_grants($node);
  242. }
  243. }
  244. }
  245. /**
  246. * DB functions - all DB interactions are isolated here to make for easy updating should our schema change.
  247. */
  248. /**
  249. * Given a sid, retrieve the access information and return the row(s).
  250. */
  251. function workflow_access_get_workflow_access_by_sid($sid) {
  252. $results = db_query('SELECT * from {workflow_access} where sid = :sid', array(':sid' => $sid));
  253. return $results->fetchAll();
  254. }
  255. /**
  256. * Given a sid and a rid (the unique key), delete all access data for this state.
  257. */
  258. function workflow_access_delete_workflow_access_by_sid_rid($sid, $rid) {
  259. db_delete('workflow_access')->condition('sid', $sid)->condition('rid', $rid)->execute();
  260. }
  261. /**
  262. * Given a sid, delete all access data for this state.
  263. */
  264. function workflow_access_delete_workflow_access_by_sid($sid) {
  265. db_delete('workflow_access')->condition('sid', $sid)->execute();
  266. }
  267. /**
  268. * Given data, insert into workflow access - we never update.
  269. */
  270. function workflow_access_insert_workflow_access_by_sid(&$data) {
  271. $data = (object) $data;
  272. workflow_access_delete_workflow_access_by_sid_rid($data->sid, $data->rid);
  273. drupal_write_record('workflow_access', $data);
  274. }
  275. /**
  276. * Implements hook_form_alter().
  277. *
  278. * Tell the Features module that we intend to provide one exportable component.
  279. */
  280. function workflow_access_form_alter(&$form, &$form_state, $form_id) {
  281. switch ($form_id) {
  282. case 'workflow_admin_ui_types_form':
  283. $form['workflow_access'] = array(
  284. '#type' => 'fieldset',
  285. '#title' => t('Workflow Access'),
  286. '#collapsible' => TRUE,
  287. '#collapsed' => TRUE,
  288. );
  289. $form['workflow_access']['workflow_access_priority'] = array(
  290. '#type' => 'weight',
  291. '#delta' => 10,
  292. '#title' => t('Workflow Access Priority'),
  293. '#default_value' => variable_get('workflow_access_priority', 0),
  294. '#description' => t('This sets the node access priority. This can be dangerous. If there is any doubt,
  295. leave it at 0.')
  296. . ' '
  297. . l(t('Read the manual.'), 'https://api.drupal.org/api/drupal/modules!node!node.api.php/function/hook_node_access_records/7'),
  298. );
  299. $form['#submit'][] = 'workflow_access_priority_submit';
  300. return;
  301. }
  302. }
  303. /**
  304. * Submit handler.
  305. */
  306. function workflow_access_priority_submit($form, &$form_state) {
  307. variable_set('workflow_access_priority', $form_state['values']['workflow_access']['workflow_access_priority']);
  308. }
  309. /**
  310. * Implements hook_features_api().
  311. *
  312. * Tell the Features module that we intend to provide one exportable component.
  313. */
  314. function workflow_access_features_api() {
  315. return array(
  316. 'workflow_access' => array(
  317. 'name' => t('Workflow access'),
  318. 'file' => drupal_get_path('module', 'workflow_access') . '/workflow_access.features.inc',
  319. 'default_hook' => 'workflow_access_features_default_settings',
  320. 'default_file' => FEATURES_DEFAULTS_INCLUDED,
  321. 'feature_source' => TRUE,
  322. ),
  323. );
  324. }