workflow.module 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223
  1. <?php
  2. /**
  3. * @file
  4. * Support workflows made up of arbitrary states.
  5. */
  6. define('WORKFLOW_CREATION', 1);
  7. define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
  8. define('WORKFLOW_DELETION', 0);
  9. // Couldn't find a more elegant way to preserve translation.
  10. define('WORKFLOW_CREATION_STATE_NAME', '(' . t('creation') . ')');
  11. // #2657072 brackets are added later to indicate a special role, and distinguish from frequently used 'author' role.
  12. define('WORKFLOW_ROLE_AUTHOR_NAME', 'author');
  13. define('WORKFLOW_ROLE_AUTHOR_RID', '-1');
  14. // The definition of the Field_info property type. Shared between 'workflow_field' and 'workflow_rules'.
  15. define('WORKFLOWFIELD_PROPERTY_TYPE', 'text'); // @todo: 'list', 'text' or 'workflow'?
  16. // Add entity support file.
  17. require_once dirname(__FILE__) . '/workflow.entity.inc';
  18. // Add workflow block (credits to workflow_extensions module).
  19. require_once dirname(__FILE__) . '/workflow.block.inc';
  20. // The type_map is only needed for workflow_node, but the API is used by
  21. // several third-party add-on modules. It's a small file, so just add it.
  22. require_once dirname(__FILE__) . '/workflow.node.type_map.inc';
  23. // Split the rather long list of hooks for the form with action buttons.
  24. require_once dirname(__FILE__) . '/workflow.form.inc';
  25. /**
  26. * Implements hook_help().
  27. */
  28. function workflow_help($path, $arg) {
  29. $output = '';
  30. switch ($path) {
  31. case 'admin/help#workflow':
  32. $output .= '<h3>' . t('About') . '</h3>';
  33. $output .= '<p>' . t('The Workflow module adds a field to Entities to
  34. store field values as Workflow states. You can control "state transitions"
  35. and add action to specific transitions.') . '</p>';
  36. }
  37. return $output;
  38. }
  39. /**
  40. * Implements hook_permission().
  41. */
  42. function workflow_permission() {
  43. return array(
  44. 'schedule workflow transitions' => array(
  45. 'title' => t('Schedule workflow transitions'),
  46. 'description' => t('Schedule workflow transitions.'),
  47. ),
  48. 'show workflow state form' => array(
  49. 'title' => t('Show workflow state change on node view'),
  50. 'description' => t('Show workflow state change form on node viewing.'),
  51. ),
  52. 'participate in workflow' => array(
  53. 'title' => t('Participate in workflows'),
  54. 'description' => t('Role is enabled for transitions on the workflow admin pages.'),
  55. ),
  56. 'edit workflow comment' => array(
  57. 'title' => t('Edit comment in workflow transitions'),
  58. 'description' => t('Edit comment of Logged transitions via a Views link.'),
  59. ),
  60. );
  61. }
  62. /**
  63. * Implements hook_menu_alter().
  64. *
  65. * hook_menu() in workflownode sets a '/workflow' menu item for entity type 'node'.
  66. * hook_menu_alter() in workflowfield sets a '/workflow' menu item for each relevant entity type.
  67. */
  68. function workflow_menu_alter(&$items) {
  69. // @todo: Move menu-items to a UI Controller class via workflow.entity.inc:
  70. $items['workflow_transition/%workflow_transition/edit'] = array(
  71. // %workflow_transition maps to function workflow_transition_load()
  72. 'title' => 'Edit workflow log comment',
  73. 'description' => 'Edit workflow transition comment.',
  74. // 'page callback' => 'drupal_get_form',
  75. // 'page arguments' => array('workflow_transition_form_wrapper', 1),
  76. 'page callback' => 'entity_ui_get_form',
  77. // @todo: below parameter should be the machine_name of the entity type.
  78. 'page arguments' => array('WorkflowTransition', 1),
  79. 'access arguments' => array('edit workflow comment'),
  80. // 'file' => 'workflow.transition.page.inc',
  81. 'menu wildcard' => '%workflow_transition',
  82. );
  83. if (module_exists('workflownode')) {
  84. $type = 'node';
  85. $items['node/%node/workflow'] = array(
  86. 'title' => 'Workflow',
  87. 'page callback' => 'workflow_tab_page',
  88. 'page arguments' => array($type, 1),
  89. 'access callback' => 'workflow_tab_access',
  90. 'access arguments' => array($type, 1),
  91. 'file' => 'workflow.pages.inc',
  92. 'file path' => drupal_get_path('module', 'workflow'),
  93. 'weight' => 2,
  94. 'type' => MENU_LOCAL_TASK,
  95. 'module' => 'workflow',
  96. );
  97. }
  98. if (!module_exists('workflowfield')) {
  99. return;
  100. }
  101. $menu_item = array(
  102. 'title' => 'Workflow',
  103. 'page callback' => 'workflow_tab_page',
  104. 'access callback' => 'workflow_tab_access',
  105. 'file' => 'workflow.pages.inc',
  106. 'file path' => drupal_get_path('module', 'workflow'),
  107. 'weight' => 2,
  108. 'type' => MENU_LOCAL_TASK,
  109. 'module' => 'workflow',
  110. );
  111. // Get a cross-bundle map of all workflow fields so we can add the workflow
  112. // tab to all entities with a workflow field.
  113. foreach (_workflow_info_fields() as $field_info) {
  114. if (TRUE) {
  115. // Loop over the entity types that have this field.
  116. foreach ($field_info['bundles'] as $type => $bundles) {
  117. $entity_info = entity_get_info($type);
  118. // Add the workflow tab in the Entity Admin UI.
  119. if (!empty($entity_info['admin ui']['path'])) {
  120. $entity_position = substr_count($entity_info['admin ui']['path'], '/') + 2;
  121. $wildcard = (isset($entity_info['admin ui']['menu wildcard']) ? $entity_info['admin ui']['menu wildcard'] : '%entity_object');
  122. $items[$entity_info['admin ui']['path'] . '/manage/' . $wildcard . '/workflow'] = $menu_item + array(
  123. 'page arguments' => array($type, $entity_position),
  124. 'access arguments' => array($type, $entity_position),
  125. 'load arguments' => array($type),
  126. );
  127. }
  128. // We can only continue if the entity relies on a ENTITY_TYPE_load() load hook.
  129. if ($entity_info['load hook'] == $type . '_load') {
  130. try {
  131. foreach ($bundles as $bundle) {
  132. // Get the default entity values.
  133. $values = array($entity_info['entity keys']['id'] => '%' . $type);
  134. if ($entity_info['entity keys']['bundle']) {
  135. $values[$entity_info['entity keys']['bundle']] = $bundle;
  136. }
  137. // Create a dummy entity and get the URI.
  138. $entity = @entity_create($type, $values);
  139. if (!$entity) {
  140. // Some entities (entity_example.module, ECK) are not complete.
  141. $entity = new stdClass($values);
  142. foreach ($values as $key => $value) {
  143. $entity->{$key} = $value;
  144. }
  145. }
  146. $uri = entity_uri($type, $entity);
  147. if (isset($uri['path'])) {
  148. $uri = $uri['path'];
  149. // Add the workflow tab if possible.
  150. if (isset($items[$uri]) && !isset($items[$uri . '/workflow'])) {
  151. $entity_position = array_search('%' . $type, explode('/', $uri));
  152. if ($entity_position) {
  153. $items[$uri . '/workflow'] = $menu_item + array(
  154. 'page arguments' => array($type, $entity_position),
  155. 'access arguments' => array($type, $entity_position),
  156. );
  157. }
  158. }
  159. }
  160. }
  161. }
  162. catch (Exception $ex) {
  163. // The $type entity could not be created or the URI building failed.
  164. // workflow_debug( __FILE__, __FUNCTION__, __LINE__, $ex->getMessage(), '');
  165. }
  166. }
  167. }
  168. }
  169. }
  170. }
  171. /**
  172. * Implements hook_admin_paths_alter().
  173. *
  174. * If node edits are done in admin mode, then workflow history tab will be too.
  175. *
  176. * @todo: add support for every $entity_type.
  177. */
  178. function workflow_admin_paths_alter(&$paths) {
  179. if (isset($paths['node/*/edit'])) {
  180. $paths['node/*/workflow'] = $paths['node/*/edit'];
  181. }
  182. if (isset($paths['user/*/edit'])) {
  183. $paths['user/*/workflow'] = $paths['user/*/edit'];
  184. }
  185. }
  186. /**
  187. * Menu access control callback. Determine access to Workflow tab.
  188. *
  189. * The History tab should not be used with multiple workflows per node.
  190. * Use the dedicated view for this use case.
  191. *
  192. * @todo D8: remove this in favour of View 'Workflow history per entity'.
  193. */
  194. function workflow_tab_access($entity_type, $entity) {
  195. global $user;
  196. static $access = array();
  197. // $figure out the $entity's bundle and id.
  198. list($entity_id, , $entity_bundle) = entity_extract_ids($entity_type, $entity);
  199. if (isset($access[$user->uid][$entity_type][$entity_id])) {
  200. return $access[$user->uid][$entity_type][$entity_id];
  201. }
  202. // When having multiple workflows per bundle, use Views display
  203. // 'Workflow history per entity' instead!
  204. if (!is_null($field_name = workflow_get_field_name($entity, $entity_type, NULL, $entity_id))) {
  205. // Get the role IDs of the user. Workflow only stores Ids, not role names.
  206. $roles = array_keys($user->roles);
  207. // Some entities (e.g., taxonomy_term) do not have a uid.
  208. $entity_uid = isset($entity->uid) ? $entity->uid : 0;
  209. // If this is a new page, give the authorship role.
  210. if (!$entity_id) {
  211. $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles);
  212. }
  213. // Add 'author' role to user if user is author of this entity.
  214. // N.B.1: Some entities (e.g, taxonomy_term) do not have a uid.
  215. // N.B.2: If 'anonymous' is the author, don't allow access to History Tab,
  216. // since anyone can access it, and it will be published in Search engines.
  217. elseif (($entity_uid > 0) && ($user->uid > 0) && ($entity_uid == $user->uid)) {
  218. $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles);
  219. }
  220. // Get the permissions from the workflow settings.
  221. // @todo: workflow_tab_access(): what to do with multiple workflow_fields per bundle? Use Views instead!
  222. $tab_roles = array();
  223. $history_tab_show = FALSE;
  224. $fields = _workflow_info_fields($entity, $entity_type, $entity_bundle);
  225. foreach ($fields as $field) {
  226. $tab_roles += $field['settings']['history']['roles'];
  227. $history_tab_show |= $field['settings']['history']['history_tab_show'];
  228. }
  229. if ($history_tab_show == FALSE) {
  230. $access[$user->uid][$entity_type][$entity_id] = FALSE;
  231. }
  232. elseif (user_access('administer nodes') || array_intersect($roles, $tab_roles)) {
  233. $access[$user->uid][$entity_type][$entity_id] = TRUE;
  234. }
  235. else {
  236. $access[$user->uid][$entity_type][$entity_id] = FALSE;
  237. }
  238. return $access[$user->uid][$entity_type][$entity_id];
  239. }
  240. return FALSE;
  241. }
  242. /**
  243. * Implements hook_hook_info().
  244. *
  245. * Allow adopters to place their hook implementations in either
  246. * their main module or in a module.workflow.inc file.
  247. */
  248. function workflow_hook_info() {
  249. $hooks['workflow'] = array('group' => 'workflow');
  250. return $hooks;
  251. }
  252. /**
  253. * Implements hook_features_api().
  254. */
  255. function workflow_features_api() {
  256. return array(
  257. 'workflow' => array(
  258. 'name' => t('Workflow'),
  259. 'file' => drupal_get_path('module', 'workflow') . '/workflow.features.inc',
  260. 'default_hook' => 'workflow_default_workflows',
  261. 'feature_source' => TRUE,
  262. ),
  263. );
  264. }
  265. /**
  266. * Implements hook_theme().
  267. */
  268. function workflow_theme() {
  269. return array(
  270. 'workflow_history_table_row' => array(
  271. 'variables' => array(
  272. 'history' => NULL,
  273. 'old_state_name' => NULL,
  274. 'state_name' => NULL,
  275. ),
  276. ),
  277. 'workflow_history_table' => array(
  278. 'variables' => array(
  279. 'header' => array(),
  280. 'rows' => array(),
  281. 'footer' => NULL,
  282. ),
  283. ),
  284. 'workflow_history_current_state' => array(
  285. 'variables' => array(
  286. 'state_name' => NULL,
  287. 'state_system_name' => NULL,
  288. 'sid' => NULL,
  289. ),
  290. ),
  291. 'workflow_current_state' => array(
  292. 'variables' => array(
  293. 'state' => NULL,
  294. 'state_system_name' => NULL,
  295. 'sid' => NULL,
  296. ),
  297. ),
  298. 'workflow_deleted_state' => array(
  299. 'variables' => array(
  300. 'state_name' => NULL,
  301. 'state_system_name' => NULL,
  302. 'sid' => NULL,
  303. ),
  304. ),
  305. );
  306. }
  307. /**
  308. * Implements hook_cron().
  309. */
  310. function workflow_cron() {
  311. $clear_cache = FALSE;
  312. // If the time now is greater than the time to execute a transition, do it.
  313. foreach (WorkflowScheduledTransition::loadBetween(0, REQUEST_TIME) as $scheduled_transition) {
  314. /* @var $scheduled_transition WorkflowScheduledTransition */
  315. $entity_type = $scheduled_transition->entity_type;
  316. $entity = $scheduled_transition->getEntity();
  317. $field_name = $scheduled_transition->field_name;
  318. // If user didn't give a comment, create one.
  319. if (empty($scheduled_transition->comment)) {
  320. $scheduled_transition->addDefaultComment();
  321. }
  322. $current_sid = workflow_node_current_state($entity, $entity_type, $field_name);
  323. // Make sure transition is still valid: the node must still be in the state
  324. // it was in, when the transition was scheduled.
  325. if ($current_sid == $scheduled_transition->old_sid) {
  326. // Do transition. Force it because user who scheduled was checked.
  327. // The scheduled transition is not scheduled anymore, and is also deleted from DB.
  328. // A watchdog message is created with the result.
  329. $scheduled_transition->schedule(FALSE);
  330. workflow_execute_transition($entity_type, $entity, $field_name, $scheduled_transition, TRUE);
  331. if (!$field_name) {
  332. $clear_cache = TRUE;
  333. }
  334. }
  335. else {
  336. // Node is not in the same state it was when the transition
  337. // was scheduled. Defer to the node's current state and
  338. // abandon the scheduled transition.
  339. $scheduled_transition->delete();
  340. }
  341. }
  342. if ($clear_cache) {
  343. // Clear the cache so that if the transition resulted in a node
  344. // being published, the anonymous user can see it.
  345. cache_clear_all();
  346. }
  347. }
  348. /**
  349. * Implements hook_user_delete().
  350. */
  351. function workflow_user_delete($account) {
  352. // Update tables for deleted account, move account to user 0 (anon.)
  353. // ALERT: This may cause previously non-anon posts to suddenly be accessible to anon.
  354. workflow_update_workflow_node_uid($account->uid, 0);
  355. workflow_update_workflow_node_history_uid($account->uid, 0);
  356. }
  357. /**
  358. * Implements hook_user_role_insert().
  359. *
  360. * Make sure new roles are allowed to participate in workflows by default.
  361. * @see https://www.drupal.org/node/2484431
  362. */
  363. //function workflow_user_role_insert($role) {
  364. // user_role_change_permissions($role->rid, array('participate in workflow' => 1));
  365. //}
  366. /**
  367. * Business related functions, the API.
  368. */
  369. /**
  370. * Implements hook_forms().
  371. *
  372. * Allows the workflow tab form to be repeated multiple times on a page.
  373. * See http://drupal.org/node/1970846.
  374. */
  375. function workflow_forms($form_id, $args) {
  376. $forms = array();
  377. if (strpos($form_id, 'workflow_transition_form_') !== FALSE) {
  378. $forms[$form_id] = array('callback' => 'workflow_transition_form');
  379. }
  380. // For the 'edit a comment' form.
  381. if (strpos($form_id, 'WorkflowTransition_edit_') !== FALSE) {
  382. $forms[$form_id] = array('callback' => 'workflow_transition_wrapper_form');
  383. }
  384. return $forms;
  385. }
  386. /**
  387. * Creates a form element to show the current value of a Workflow state.
  388. *
  389. * @params
  390. * Like a normal Field API function.
  391. * @param int $default_value
  392. * Extra param for performance and edge cases.
  393. *
  394. * @return array
  395. * Form element, resembling the formatter of List module.
  396. * If state 0 is given, return an empty form element.
  397. */
  398. function workflow_state_formatter($entity_type, $entity, $field = array(), $instance = array(), $default_value = NULL) {
  399. $list_element = array();
  400. $field_name = isset($field['field_name']) ? $field['field_name'] : '';
  401. $current_sid = workflow_node_current_state($entity, $entity_type, $field_name);
  402. if (!$current_sid && !$default_value) {
  403. $list_element = array();
  404. }
  405. elseif ($field_name) {
  406. // This is a Workflow Field workflow. Use the Field API field view.
  407. $field_name = $field['field_name'];
  408. // Add the 'current value' formatter for this field.
  409. $list_display = $instance['display']['default'];
  410. $list_display['type'] = 'list_default';
  411. // Clone the entity and restore old value, in case you want to show an
  412. // executed transition.
  413. if ($default_value != $current_sid) {
  414. $entity = clone $entity;
  415. $entity->{$field_name}[LANGUAGE_NONE][0]['value'] = $default_value;
  416. }
  417. // Generate a renderable array for the field. Use default language determination ($langcode = NULL).
  418. $list_element = field_view_field($entity_type, $entity, $field_name, $list_display);
  419. // Make sure the current value is before the form. (which has weight = 0.005)
  420. $list_element['#weight'] = 0;
  421. }
  422. else {
  423. // This is a Workflow Node workflow.
  424. $current_sid = ($default_value == NULL) ? $current_sid : $default_value;
  425. $current_state = workflow_state_load_single($current_sid);
  426. $args = array(
  427. 'state' => $current_state ? workflow_get_sid_label($current_sid) : 'unknown state',
  428. 'state_system_name' => $current_state ? $current_state->getName() : 'unknown state',
  429. 'sid' => $current_sid,
  430. );
  431. $list_element = array(
  432. '#type' => 'item',
  433. // '#title' => t('Current state'),
  434. '#markup' => theme('workflow_current_state', $args),
  435. );
  436. }
  437. return $list_element;
  438. }
  439. /**
  440. * Saves the workflow field, rather then the whole entity.
  441. *
  442. * This is especially important when adding a new entity, and having an extra
  443. * activity:
  444. * - a Rules action after adding, cloning an entity (#2425453, #2550719)
  445. * - revisions are expected after each update. (#2563125)
  446. *
  447. * @param $entity_type
  448. * @param $entity
  449. * @param $field_name
  450. * @param $langcode
  451. * @param $value
  452. */
  453. function workflow_entity_field_save($entity_type, $entity, $field_name, $langcode, $value) {
  454. if ($value !== FALSE) {
  455. $entity->{$field_name}[$langcode][0]['workflow'] = $value;
  456. }
  457. // entity_save($entity_type, $entity);
  458. field_attach_presave($entity_type, $entity);
  459. field_attach_update($entity_type, $entity);
  460. if ($entity_type == 'node') {
  461. // Rebuild node access - necessary if using workflow access.
  462. node_access_acquire_grants($entity);
  463. // Manually clearing entity cache.
  464. entity_get_controller($entity_type)->resetCache(array($entity->nid));
  465. }
  466. }
  467. /**
  468. * Executes a transition (change state of a node), from outside the node, e.g., workflow_cron().
  469. *
  470. * Serves as a wrapper function to hide differences between Node API and Field API.
  471. * Use workflow_execute_transition($transition) to start a State Change from outside an entity.
  472. * Use $transition->execute() to start a State Change from within an enetity.
  473. *
  474. * @param string $entity_type
  475. * Entity type of target entity.
  476. * @param object $entity
  477. * Target entity.
  478. * @param string $field_name
  479. * A field name, used when changing a Workflow Field.
  480. * @param object $transition
  481. * A WorkflowTransition or WorkflowScheduledTransition.
  482. * @param bool $force
  483. * If set to TRUE, workflow permissions will be ignored.
  484. *
  485. * @return int
  486. * The new state ID.
  487. */
  488. function workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force = FALSE) {
  489. // $todo D8: Remove first 3 parameters - they can be extracted from $transition.
  490. // Make sure $force is set in the transition, too.
  491. if ($force) {
  492. $transition->force($force);
  493. }
  494. $force = $transition->isForced();
  495. if ($field_name) {
  496. // @todo: use $new_sid = $transition->execute() without generating infinite loops.
  497. $langcode = $transition->language;
  498. // Do a separate update to update the field (Workflow Field API)
  499. // This will call hook_field_update() and WorkflowFieldDefaultWidget::submit().
  500. $entity->{$field_name}[$langcode][0]['transition'] = $transition;
  501. $entity->{$field_name}[$langcode][0]['value'] = $transition->new_sid;
  502. // Save only the field, not the complete entity.
  503. workflow_entity_field_save($entity_type, $entity, $field_name, $langcode, FALSE);
  504. $new_sid = workflow_node_current_state($entity, $entity_type, $field_name);
  505. }
  506. else {
  507. // For Node API, the node is not saved, since all fields are custom.
  508. // Force = TRUE for backwards compatibility with version 7.x-1.2
  509. $new_sid = $transition->execute($force = TRUE);
  510. }
  511. return $new_sid;
  512. }
  513. /**
  514. * Get a list of roles.
  515. *
  516. * @return array
  517. * Array of role names keyed by role ID, including the 'author' role.
  518. */
  519. function workflow_get_roles($permission = 'participate in workflow') {
  520. static $roles = NULL;
  521. if (!$roles[$permission]) {
  522. $roles[$permission][WORKFLOW_ROLE_AUTHOR_RID] = '(' . t(WORKFLOW_ROLE_AUTHOR_NAME) . ')';
  523. foreach (user_roles(FALSE, $permission) as $rid => $role_name) {
  524. $roles[$permission][$rid] = check_plain(t($role_name));
  525. }
  526. }
  527. return $roles[$permission];
  528. }
  529. /**
  530. * Functions to be used in non-OO modules, like workflow_rules, workflow_views.
  531. */
  532. /**
  533. * Get an options list for workflow states (to show in a widget).
  534. *
  535. * To be used in non-OO modules, like workflow_rules.
  536. *
  537. * @param mixed $wid
  538. * The Workflow ID.
  539. * @param bool $grouped
  540. * Indicates if the value must be grouped per workflow.
  541. * This influence the rendering of the select_list options.
  542. * @param bool $all
  543. * Indicates to return all (TRUE) or active (FALSE) states of a workflow.
  544. *
  545. * @return array $options
  546. * An array of $sid => state->label(), grouped per Workflow.
  547. */
  548. function workflow_get_workflow_state_names($wid = 0, $grouped = FALSE, $all = FALSE) {
  549. $options = array();
  550. // Get the (user-dependent) options.
  551. // Since this function is only used in UI, it is save to use the global $user.
  552. global $user;
  553. foreach (workflow_load_multiple($wid ? array($wid) : FALSE) as $workflow) {
  554. $state = new WorkflowState(array('wid' => $workflow->wid));
  555. $workflow_options = $state->getOptions('', NULL, '', $user, FALSE);
  556. if (!$grouped) {
  557. $options += $workflow_options;
  558. }
  559. else {
  560. // Make a group for each Workflow.
  561. $options[$workflow->label()] = $workflow_options;
  562. }
  563. }
  564. return $options;
  565. }
  566. /**
  567. * Get an options list for workflows (to show in a widget).
  568. *
  569. * To be used in non-OO modules.
  570. *
  571. * @return array $options
  572. * An array of $wid => workflow->label().
  573. */
  574. function workflow_get_workflow_names() {
  575. $options = array();
  576. foreach (workflow_load_multiple() as $workflow) {
  577. $options[$workflow->wid] = html_entity_decode(t($workflow->label()));
  578. }
  579. return $options;
  580. }
  581. /**
  582. * Helper function, to get the label of a given state.
  583. */
  584. function workflow_get_sid_label($sid) {
  585. if (empty($sid)) {
  586. $label = 'No state';
  587. }
  588. elseif ($state = workflow_state_load_single($sid)) {
  589. $label = $state->label();
  590. }
  591. else {
  592. $label = 'Unknown state';
  593. }
  594. return html_entity_decode(t($label));
  595. }
  596. /**
  597. * Gets the current state ID of a given entity.
  598. *
  599. * There is no need to use a page cache.
  600. * The performance is OK, and the cache gives problems when using Rules.
  601. *
  602. * @param object $entity
  603. * The entity to check. May be an EntityDrupalWrapper.
  604. * @param string $entity_type
  605. * The entity_type of the entity to check.
  606. * May be empty in case of an EntityDrupalWrapper.
  607. * @param string $field_name
  608. * The name of the field of the entity to check.
  609. * If NULL, the field_name is determined on the spot. This must be avoided,
  610. * making multiple workflows per entity unpredictable.
  611. * The found field_name will be returned in the param.
  612. * If '', we have a workflow_node mode.
  613. *
  614. * @return mixed $sid
  615. * The ID of the current state.
  616. */
  617. function workflow_node_current_state($entity, $entity_type = 'node', &$field_name = NULL) {
  618. $sid = FALSE;
  619. if (!$entity) {
  620. return $sid; // <-- exit !!!
  621. }
  622. // If $field_name is not known, yet, determine it.
  623. $field_name = workflow_get_field_name($entity, $entity_type, $field_name);
  624. if (is_null($field_name)) {
  625. // This entity has no workflow.
  626. return $sid; // <-- exit !!!
  627. }
  628. if ($field_name === '') {
  629. // Workflow Node API: Get current/previous state for a Workflow Node.
  630. // Multi-language not supported.
  631. // N.B. Do not use a page cache. This gives problems with Rules.
  632. $sid = isset($entity->workflow) ? $entity->workflow : FALSE;
  633. }
  634. elseif ($field_name) {
  635. // Get State ID for existing nodes (A new node has no sid - will be fetched later.)
  636. // and normal node, on Node view page / Workflow history tab.
  637. $wrapper = entity_metadata_wrapper($entity_type, $entity);
  638. $sid = $wrapper->{$field_name}->value();
  639. }
  640. else {
  641. // Not possible. All options are covered.
  642. }
  643. // Entity is new or in preview or there is no current state. Use previous state.
  644. if ( !$sid || !empty($entity->is_new) || !empty($entity->in_preview) ) {
  645. $sid = workflow_node_previous_state($entity, $entity_type, $field_name);
  646. }
  647. return $sid;
  648. }
  649. /**
  650. * Gets the previous state ID of a given entity.
  651. */
  652. function workflow_node_previous_state($entity, $entity_type, $field_name) {
  653. $sid = FALSE;
  654. $langcode = LANGUAGE_NONE;
  655. if (!$entity) {
  656. return $sid; // <-- exit !!!
  657. }
  658. // If $field_name is not known, yet, determine it.
  659. $field_name = workflow_get_field_name($entity, $entity_type, $field_name);
  660. if (is_null($field_name)) {
  661. // This entity has no workflow.
  662. return $sid; // <-- exit !!!
  663. }
  664. $previous_entity = NULL;
  665. if (isset($entity->old_vid) && ($entity->vid - $entity->old_vid) <= 1) {
  666. // Using the Revisioning module, get the old revision from DB,
  667. // if it is NOT the previous version.
  668. // The old revision from which to get our state, if it is not the revision
  669. // to which we want to switch.
  670. $previous_entity = entity_revision_load($entity_type, $entity->old_vid);
  671. }
  672. elseif (isset($entity->{$field_name}) && isset($entity->{$field_name}[$langcode][0]['workflow']['workflow_entity'])) {
  673. // Still using the Revisioning module, get the old revision from DB.
  674. $previous_entity = $entity->{$field_name}[$langcode][0]['workflow']['workflow_entity'];
  675. }
  676. elseif (isset($entity->original)) {
  677. $previous_entity = $entity->original;
  678. }
  679. if ($field_name === '') {
  680. // Workflow Node API: Get current/previous state for a Workflow Node.
  681. // Multi-language not supported.
  682. // N.B. Do not use a page cache. This gives problems with Rules.
  683. // Todo D7: support for Revisioning module.
  684. $sid = isset($entity->workflow) ? $entity->workflow : FALSE;
  685. }
  686. elseif ($field_name) {
  687. // Workflow Field API.
  688. if (isset($previous_entity)) {
  689. // A changed node.
  690. $wrapper = entity_metadata_wrapper($entity_type, $previous_entity);
  691. $sid = $wrapper->{$field_name}->value();
  692. // Get language. Multi-language is not supported for Workflow Node.
  693. $langcode = _workflow_metadata_workflow_get_properties($previous_entity, array(), 'langcode', $entity_type, $field_name);
  694. }
  695. elseif (isset($entity->workflow_transitions[$field_name]->sid)) {
  696. // A new node. Upon save with Workflow Access enabled, the sid is needed
  697. // in workflow_access_node_access_records.
  698. $sid = $entity->workflow_transitions[$field_name]->sid;
  699. }
  700. }
  701. else {
  702. // Not possible. All options are covered.
  703. }
  704. if (!$sid) {
  705. if (!empty($entity->is_new)) {
  706. // A new Node. $is_new is not set when saving terms, etc.
  707. $sid = _workflow_get_workflow_creation_sid($entity_type, $entity, $field_name);
  708. }
  709. // Get Id. Is empty when creating a node.
  710. $entity_id = 0;
  711. if (!$sid) {
  712. $entity_id = entity_id($entity_type, $entity);
  713. }
  714. if (!$sid && $entity_id) {
  715. // Read the history with an explicit langcode.
  716. if ($last_transition = workflow_transition_load_single($entity_type, $entity_id, $field_name, $langcode)) {
  717. $sid = $last_transition->new_sid;
  718. }
  719. }
  720. }
  721. if (!$sid) {
  722. // No history found on an existing entity.
  723. $sid = _workflow_get_workflow_creation_sid($entity_type, $entity, $field_name);
  724. }
  725. return $sid;
  726. }
  727. /**
  728. * DB functions.
  729. *
  730. * All SQL in workflow.module should be put into its own function and placed
  731. * here. This encourages good separation of code and reuse of SQL statements.
  732. * It *also* makes it easy to make schema updates and changes without rummaging
  733. * through every single inch of code looking for SQL. Sure it's a little
  734. * type A, granted. But it's useful in the long run.
  735. */
  736. /**
  737. * Functions related to table workflows.
  738. */
  739. /**
  740. * Get a specific workflow, given a Node type. Only one workflow is possible per node type.
  741. *
  742. * @param string $entity_bundle
  743. * A node type (a.k.a. entity bundle).
  744. * @param string $entity_type
  745. * An entity type. This is passed when also the Field API must be checked.
  746. *
  747. * @return
  748. * A Workflow object, or FALSE if no workflow is retrieved.
  749. *
  750. * Caveat: gives undefined results with multiple workflows per entity.
  751. *
  752. * @todo: support multiple workflows per entity.
  753. */
  754. function workflow_get_workflows_by_type($entity_bundle, $entity_type = 'node') {
  755. static $map = array();
  756. if (!isset($map[$entity_type][$entity_bundle])) {
  757. $wid = FALSE;
  758. $map[$entity_type][$entity_bundle] = FALSE;
  759. // Check the Node API first: Get $wid.
  760. if (module_exists('workflownode') && $type_map = workflow_get_workflow_type_map_by_type($entity_bundle)) {
  761. // Get the workflow by wid.
  762. $wid = $type_map->wid;
  763. }
  764. // If $entity_type is set, we must check Field API. Data is already cached by core.
  765. if (!$wid && isset($entity_type)) {
  766. foreach (_workflow_info_fields(NULL, $entity_type, $entity_bundle) as $field_name => $field_info) {
  767. $wid = $field_info['settings']['wid'];
  768. }
  769. }
  770. // Set the cache with a workflow object.
  771. if ($wid) {
  772. // $wid can be numeric or named.
  773. $workflow = workflow_load_single($wid);
  774. $map[$entity_type][$entity_bundle] = $workflow;
  775. }
  776. }
  777. return $map[$entity_type][$entity_bundle];
  778. }
  779. /**
  780. * Functions related to table workflow_node_history.
  781. */
  782. /**
  783. * Given a user id, re-assign history to the new user account. Called by user_delete().
  784. */
  785. function workflow_update_workflow_node_history_uid($uid, $new_value) {
  786. return db_update('workflow_node_history')->fields(array('uid' => $new_value))->condition('uid', $uid, '=')->execute();
  787. }
  788. /**
  789. * Functions related to table workflow_node.
  790. */
  791. /**
  792. * Given a node id, find out what it's current state is. Unique (for now).
  793. *
  794. * @param mixed $nid
  795. * A Node ID or an array of node ID's.
  796. *
  797. * @deprecated: workflow_get_workflow_node_by_nid --> workflow_node_current_state().
  798. */
  799. function workflow_get_workflow_node_by_nid($nid) {
  800. $query = db_select('workflow_node', 'wn')->fields('wn')->condition('wn.nid', $nid)->execute();
  801. if (is_array($nid)) {
  802. $result = array();
  803. foreach ($query->fetchAll() as $workflow_node) {
  804. $result[$workflow_node->nid] = $workflow_node;
  805. }
  806. }
  807. else {
  808. $result = $query->fetchObject();
  809. }
  810. return $result;
  811. }
  812. /**
  813. * Given a sid, find out the nodes associated.
  814. */
  815. function workflow_get_workflow_node_by_sid($sid) {
  816. return db_select('workflow_node', 'wn')->fields('wn')->condition('wn.sid', $sid)->execute()->fetchAll();
  817. }
  818. /**
  819. * Given data, update the new user account. Called by user_delete().
  820. */
  821. function workflow_update_workflow_node_uid($uid, $new_uid) {
  822. return db_update('workflow_node')->fields(array('uid' => $new_uid))->condition('uid', $uid, '=')->execute();
  823. }
  824. /**
  825. * Given nid, delete associated workflow data.
  826. */
  827. function workflow_delete_workflow_node_by_nid($nid) {
  828. return db_delete('workflow_node')->condition('nid', $nid)->execute();
  829. }
  830. /**
  831. * Given sid, delete associated workflow data.
  832. */
  833. function workflow_delete_workflow_node_by_sid($sid) {
  834. return db_delete('workflow_node')->condition('sid', $sid)->execute();
  835. }
  836. /**
  837. * Given data, insert the node association.
  838. */
  839. function workflow_update_workflow_node($data) {
  840. $data = (object) $data;
  841. if (isset($data->nid) && workflow_get_workflow_node_by_nid($data->nid)) {
  842. drupal_write_record('workflow_node', $data, 'nid');
  843. }
  844. else {
  845. drupal_write_record('workflow_node', $data);
  846. }
  847. }
  848. /**
  849. * Get a single value from an Field API $items array.
  850. *
  851. * @param array $items
  852. * Array with values, as passed in the hook_field_<op> functions.
  853. * Although we are parsing an array,
  854. * the Workflow Field settings ensure that the cardinality is set to 1.
  855. *
  856. * @return int $sid
  857. * A State ID.
  858. */
  859. function _workflow_get_sid_by_items(array $items) {
  860. // On a normal widget:
  861. $sid = isset($items[0]['value']) ? $items[0]['value'] : 0;
  862. // On a workflow form widget:
  863. $sid = isset($items[0]['workflow']['workflow_sid']) ? $items[0]['workflow']['workflow_sid'] : $sid;
  864. return $sid;
  865. }
  866. /**
  867. * Gets the creation sid for a given $entity and $field_name.
  868. */
  869. function _workflow_get_workflow_creation_sid($entity_type, $entity, $field_name) {
  870. $sid = 0;
  871. $wid = 0;
  872. if ($field_name) {
  873. // A new Node with Workflow Field.
  874. $field = field_info_field($field_name);
  875. // $field['settings']['wid'] can be numeric or named.
  876. $wid = $field['settings']['wid'];
  877. $workflow = workflow_load_single($wid);
  878. }
  879. else {
  880. // A new Node with Workflow Node.
  881. list(, , $entity_bundle) = entity_extract_ids($entity_type, $entity);
  882. $workflow = workflow_get_workflows_by_type($entity_bundle, $entity_type);
  883. }
  884. if ($workflow) {
  885. $sid = $workflow->getCreationSid();
  886. }
  887. else {
  888. drupal_set_message(t('Workflow !wid cannot be loaded. Contact your system administrator.', array('!wid' => $wid)), 'error');
  889. }
  890. return $sid;
  891. }
  892. /**
  893. * Determines the Workflow field_name of an entity.
  894. * If an entity has more workflows, only returns the first one.
  895. *
  896. * Usage
  897. * if (is_null($field_name = workflow_get_field_name($entity, $entity_type))) {
  898. * return; // No workflow on this entity
  899. * }
  900. * else {
  901. * ... // WorkflowField or WorkflowNode on this entity
  902. * }
  903. *
  904. * @param $entity
  905. * The entity at hand.
  906. * @param $entity_type
  907. * @param string $field_name (optional)
  908. * The field name. If given, will be passed as return value.
  909. * @param $entity_id (optional)
  910. *
  911. * @return string
  912. */
  913. function workflow_get_field_name($entity, $entity_type, $field_name = NULL, $entity_id = NULL) {
  914. if (!$entity) {
  915. // $entity may be empty on Entity Add page.
  916. return NULL;
  917. }
  918. if (!is_null($field_name)) {
  919. // $field_name is already known.
  920. return $field_name;
  921. }
  922. // If $field_name is not known, yet, determine it.
  923. if (!$entity_id) {
  924. list($entity_id, , ) = entity_extract_ids($entity_type, $entity);
  925. }
  926. $field_names = &drupal_static(__FUNCTION__);
  927. if (isset($field_names[$entity_type][$entity_id])) {
  928. $field_name = $field_names[$entity_type][$entity_id]['field_name'];
  929. }
  930. else {
  931. $fields = _workflow_info_fields($entity, $entity_type);
  932. if (count($fields)) {
  933. // Get the first field.
  934. // Workflow Field API: return a field name.
  935. // Workflow Node API: return ''.
  936. $field = reset($fields);
  937. $field_name = $field['field_name'];
  938. $field_names[$entity_type][$entity_id]['field_name'] = $field_name;
  939. }
  940. else {
  941. // No workflow at all on this entity.
  942. $field_name = NULL;
  943. // Use special sub-array, or it won't work for NULL.
  944. $field_names[$entity_type][$entity_id]['field_name'] = $field_name;
  945. }
  946. }
  947. return $field_name;
  948. }
  949. /**
  950. * Gets the workflow field names, if not known already.
  951. *
  952. * For workflow_field, multiple workflows per bundle are supported.
  953. * For workflow_node, only one 'field' structure is returned.
  954. *
  955. * @param $entity
  956. * Object to work with. May be empty, e.g., on menu build.
  957. * @param string $entity_type
  958. * Entity type of object. Optional, but required if $entity provided.
  959. * @param string $entity_bundle
  960. * Bundle of entity. Optional.
  961. *
  962. * @return array $field_info
  963. * An array of field_info structures.
  964. */
  965. function _workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '') {
  966. $field_info = array();
  967. // Unwrap the entity.
  968. if ($entity instanceof EntityDrupalWrapper) {
  969. $entity_type = $entity->type();
  970. $entity = $entity->value();
  971. list(, , $entity_bundle) = entity_extract_ids($entity_type, $entity);
  972. }
  973. // Check if this is a workflow_node sid.
  974. $workflow_node_sid = isset($entity->workflow) ? $entity->workflow : FALSE;
  975. if ($workflow_node_sid) {
  976. $field_name = '';
  977. $workflow = NULL;
  978. if ($state = workflow_state_load($workflow_node_sid)) {
  979. $workflow = workflow_load($state->wid);
  980. }
  981. // Call field_info_field().
  982. // Generates pseudo data for workflow_node to re-use Field API.
  983. $field = _workflow_info_field($field_name, $workflow);
  984. $field_info[$field_name] = $field;
  985. }
  986. else {
  987. // In Drupal 7.22, function field_info_field_map() was added, which is more
  988. // memory-efficient in certain cases than field_info_fields().
  989. // @see https://drupal.org/node/1915646
  990. $field_map_available = version_compare(VERSION, '7.22', '>=');
  991. $field_list = $field_map_available ? field_info_field_map() : field_info_fields();
  992. // Get the bundle, if not provided yet.
  993. if ($entity && !$entity_bundle) {
  994. list(, , $entity_bundle) = entity_extract_ids($entity_type, $entity);
  995. }
  996. foreach ($field_list as $field_name => $data) {
  997. if (($data['type'] == 'workflow')
  998. && (!$entity_type || array_key_exists($entity_type, $data['bundles']))
  999. && (!$entity_bundle || in_array($entity_bundle, $data['bundles'][$entity_type]))) {
  1000. $field_info[$field_name] = $field_map_available ? field_info_field($field_name) : $data;
  1001. }
  1002. }
  1003. }
  1004. return $field_info;
  1005. }
  1006. /**
  1007. * A wrapper around field_info_field.
  1008. *
  1009. * This is to hide implementation details of workflow_node.
  1010. *
  1011. * @param string $field_name
  1012. * The name of a Workflow Field. Can be empty if fetching Workflow Node.
  1013. * @param Workflow $workflow
  1014. * Workflow object. Can be NULL.
  1015. * For a workflow_field, no $workflow is needed, since info is in field itself.
  1016. * For a workflow_node, $workflow provides additional data in return.
  1017. *
  1018. * @return array
  1019. * Field info structure. Pseudo data for workflow_node.
  1020. */
  1021. function _workflow_info_field($field_name, $workflow = NULL) {
  1022. // @todo D8: remove this function when we only use workflow_field.
  1023. $field = array();
  1024. if ($field_name) {
  1025. $field = field_info_field($field_name);
  1026. }
  1027. else {
  1028. $field['field_name'] = '';
  1029. $field['id'] = 0;
  1030. $field['settings']['wid'] = 0;
  1031. $field['settings']['widget'] = array();
  1032. if ($workflow != NULL) {
  1033. // $field['settings']['wid'] can be both: numeric or named.
  1034. $field['settings']['wid'] = $workflow->wid; // @todo: to make this exportable: use machine_name??
  1035. $field['settings']['widget'] = $workflow->options;
  1036. $field['settings']['history']['roles'] = $workflow->tab_roles;
  1037. $field['settings']['history']['history_tab_show'] = TRUE; // @todo: add a setting for this in workflow_node.
  1038. }
  1039. // Add default values.
  1040. $field['settings']['widget'] += array(
  1041. 'name_as_title' => TRUE,
  1042. 'fieldset' => 0,
  1043. 'options' => 'radios',
  1044. 'schedule' => TRUE,
  1045. 'schedule_timezone' => TRUE,
  1046. 'comment_log_node' => TRUE,
  1047. 'comment_log_tab' => TRUE,
  1048. 'watchdog_log' => TRUE,
  1049. 'history_tab_show' => TRUE,
  1050. );
  1051. }
  1052. return $field;
  1053. }
  1054. /**
  1055. * Get features defaults for workflows.
  1056. */
  1057. function workflow_get_defaults($module) {
  1058. $funcname = $module . '_default_Workflow';
  1059. return $funcname();
  1060. }
  1061. /**
  1062. * Revert a single workflow.
  1063. */
  1064. function workflow_revert($defaults, $name) {
  1065. $workflow = $defaults[$name];
  1066. $old = workflow_load_by_name($name);
  1067. if ($old) {
  1068. $workflow->wid = $old->wid;
  1069. $workflow->is_new = FALSE;
  1070. $workflow->is_reverted = TRUE;
  1071. }
  1072. $workflow->save();
  1073. }
  1074. /**
  1075. * Helper function for D8-port: Get some info on screen.
  1076. * @see workflow_devel module
  1077. *
  1078. * Usage:
  1079. * workflow_debug( __FILE__, __FUNCTION__, __LINE__, '', ''); // @todo: still test this snippet.
  1080. *
  1081. * @param string $class_name
  1082. * @param string $function_name
  1083. * @param string $line
  1084. * @param string $value1
  1085. * @param string $value2
  1086. *
  1087. */
  1088. function workflow_debug($class_name, $function_name, $line = '', $value1 = '', $value2 = '') {
  1089. $debug_switch = FALSE;
  1090. // $debug_switch = TRUE;
  1091. if (!$debug_switch) {
  1092. return;
  1093. }
  1094. $class_name_elements = explode( "\\" , $class_name);
  1095. $output = 'Testing... function ' . end($class_name_elements) . '::' . $function_name . '/' . $line;
  1096. if ($value1) {
  1097. $output .= ' = ' . $value1;
  1098. }
  1099. if ($value2) {
  1100. $output .= ' > ' . $value2;
  1101. }
  1102. drupal_set_message($output, 'warning');
  1103. }