workflow.module 40 KB

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