content_lock.module

Tracking 7.x-1.x branch
  1. drupal
    1. 6 contributions/content_lock/content_lock.module
    2. 7 contributions/content_lock/content_lock.module

Allows users to lock documents for modification.

Functions & methods

NameDescription
content_lock_cancel_submitCallback for a cancel request on a form
content_lock_content_lock_node_lockableImplement our own hook_content_lock_node_lockable().
content_lock_content_lock_skip_locking
content_lock_fetch_lockFetch the lock for a node.
content_lock_form_alterImplementation of hook_form_alter().
content_lock_get_release_tokenCalculate the token required to unlock a node.
content_lock_helpImplementation of hook_help().
content_lock_lock_ownerTell who has locked node.
content_lock_menuImplementation of hook_menu().
content_lock_nodeTry to lock a document for editing.
content_lock_node_deleteImplement hook_node_delete() to remove references to deleted nodes from the lock tables.
content_lock_node_updateImplement hook_node_update() to unlock a node after it's saved.
content_lock_node_validateImplement hook_node_validate() to check that the user is maintaining his lock.
content_lock_node_viewImplement hook_node_view() to inform user if he's holding locks on other nodes.
content_lock_overviewBuild an overview of locked documents.
content_lock_permissionImplementation of hook_permission().
content_lock_releaseRelease a locked node.
content_lock_release_itemMenu callback; release a locked node for all users or a specific user.
content_lock_release_own_itemRelease the lock of a node. We are using the current users uid, so the user only can delete his own locks. We never fail, as if the lock does not exist, the node is unlocked anyway
content_lock_user_logoutImplement hook_user_logout().
content_lock_views_api
content_lock_warn_pending_locksFor every lock a user current have on any nodes, print a warning messagt with an link to release this node.
_content_lock_add_cancelbutton
_content_lock_add_unload_js
_content_lock_is_lockable_nodeCheck whether a node is configured to be protected by content_lock.
_content_lock_release_all_user_locks
_content_lock_save_lock_warning
_content_lock_show_warnings
_content_lock_still_locked
_content_lock_verbose

File

View source
  1. <?php /* -*- mode: php; indent-tabs-mode: nil; tab-width: 2; -*- */
  2. /**
  3. * @file
  4. * Allows users to lock documents for modification.
  5. */
  6. /**
  7. * Implementation of hook_permission().
  8. */
  9. function content_lock_permission() {
  10. return array(
  11. 'check out documents' => array(
  12. 'title' => t('Check Out/Lock Documents'),
  13. 'description' => t('Enables users to lock documents and requires them to respect locks others have made.'),
  14. ),
  15. 'administer checked out documents' => array(
  16. 'title' => t('Administer Checked Out Documents'),
  17. 'description' => t('Enables administrators to view and break locks made by other users.'),
  18. 'restrict access' => TRUE,
  19. ),
  20. );
  21. }
  22. /**
  23. * Implementation of hook_help().
  24. */
  25. function content_lock_help($path, $arg) {
  26. switch ($path) {
  27. case 'admin/help#content_lock':
  28. $output = '<p>'. t("Drupal's default content locking strategy is optimistic, that is, two users may start to edit the same content and the one who is hitting the save button first wins the race, while the other is displayed a message stating <em>this content has been modified by another user, changes cannot be saved</em>. Depending on the number of editors in your organization this might not be an acceptable solution.") .'</p>';
  29. $output .= '<p>'. t('The Content locking module implements pessimistic locking, which means that content will be exclusively locked whenever a user starts editing it. The lock will be automatically released when the user submits the form or navigates away from the edit page.') .'</p>';
  30. $output .= '<p>'. t('Users may also permanently lock content, to prevent others from editing it. Content locks that have been "forgotten" can be automatically released after a configurable time span.') .'</p>';
  31. return $output;
  32. case 'admin/content/content_lock':
  33. return '<p>'. t('Below is a list of all locked documents. Click on <em>!checkin</em> to release a lock.', array('!checkin' => t('release lock'))) .'</p>';
  34. case 'user/%/content_lock':
  35. return '<p>'. t('Below is a list of all documents locked by you. Click on <em>!checkin</em> to release a lock.', array('!checkin' => t('release lock'))) .'</p>';
  36. }
  37. }
  38. /**
  39. * Implementation of hook_menu().
  40. */
  41. function content_lock_menu() {
  42. $items['admin/content/content_lock'] = array(
  43. 'title' => 'Locked documents',
  44. 'page callback' => 'content_lock_overview',
  45. 'access callback' => 'user_access',
  46. 'access arguments' => array('administer checked out documents'),
  47. 'weight' => 5,
  48. 'type' => MENU_LOCAL_TASK,
  49. );
  50. $items['admin/content/content_lock/release'] = array(
  51. 'page callback' => 'content_lock_release_item',
  52. 'page arguments' => array(4, NULL),
  53. 'access arguments' => array('administer checked out documents'),
  54. 'type' => MENU_CALLBACK,
  55. );
  56. $items['admin/content/%/content_lock/releaseown'] = array(
  57. 'page callback' => 'content_lock_release_own_item',
  58. 'page arguments' => array(2, TRUE, FALSE),
  59. 'access arguments' => array('check out documents'),
  60. 'type' => MENU_CALLBACK,
  61. );
  62. $items['user/%user/content_lock'] = array(
  63. 'title' => 'Locked documents',
  64. 'page callback' => 'content_lock_overview',
  65. 'page arguments' => array(1),
  66. 'access callback' => 'user_access',
  67. 'access arguments' => array('check out documents'),
  68. 'weight' => 5,
  69. 'type' => MENU_LOCAL_TASK
  70. );
  71. $items['ajax/content_lock/%/canceledit'] = array (
  72. 'page callback' => 'content_lock_release_own_item',
  73. 'page arguments' => array(2, FALSE, FALSE),
  74. 'access callback' => true
  75. );
  76. $items['admin/config/content/content_lock'] = array (
  77. 'type' => MENU_NORMAL_ITEM,
  78. 'title' => 'Content lock',
  79. 'description' => 'Configuration options for the Content lock module',
  80. 'page callback' => 'drupal_get_form',
  81. 'page arguments' => array('content_lock_admin_settings'),
  82. 'access arguments' => array('administer site configuration'),
  83. 'file' => 'content_lock.admin.inc'
  84. );
  85. return $items;
  86. }
  87. /**
  88. * Implement hook_node_validate() to check that the user is
  89. * maintaining his lock.
  90. *
  91. * @param $node
  92. * The node whose lock to validate.
  93. * @param $form
  94. * The node's form.
  95. * @param $form_state
  96. * Form state.
  97. */
  98. function content_lock_node_validate($node, $form, &$form_state) {
  99. global $user;
  100. if (isset($node->nid) && _content_lock_is_lockable_node($node) && user_access('check out documents')) {
  101. // Existing node. Check if we still own the lock.
  102. if ($lock = content_lock_fetch_lock($node->nid)) {
  103. if ($lock->uid != $user->uid) {
  104. // Lock is no longer ours.
  105. form_set_error('changed', t('Your lock has been removed!') .'<br />'. content_lock_lock_owner($lock) .'<br />'. t('You can still save the content if this user aborts the edit operation without saving changes.'));
  106. }
  107. }
  108. else {
  109. // Node is not locked. Try to re-lock if node is unchanged.
  110. if (node_last_changed($node->nid) > $node->changed || !content_lock_node($node->nid, $user->uid)) {
  111. form_set_error('alsochanged', t('Your lock has been removed due to inactivity or by an administrator. Failed to regain the lock since the document has been changed since. Please !discard.',
  112. array('!discard' => l('discard your changes', 'node/' . $node->nid))));
  113. }
  114. }
  115. }
  116. }
  117. /**
  118. * Implement hook_node_update() to unlock a node after it's saved.
  119. *
  120. * This hook is invoked after hook_node_save() is invoked and after
  121. * the changes to the node are actually saved to the database, so it's
  122. * the right place to unlock a modified node.
  123. *
  124. * @param $node
  125. * The node which was saved.
  126. */
  127. function content_lock_node_update($node) {
  128. global $user;
  129. if (_content_lock_is_lockable_node($node)) {
  130. content_lock_release($node->nid, $user->uid);
  131. }
  132. }
  133. /**
  134. * Implement hook_node_delete() to remove references to deleted nodes
  135. * from the lock tables.
  136. *
  137. * @param $node
  138. * The node being deleted.
  139. */
  140. function content_lock_node_delete($node) {
  141. content_lock_release($node->nid, NULL);
  142. }
  143. /**
  144. * Implement hook_node_view() to inform user if he's holding locks on
  145. * other nodes.
  146. *
  147. * @param $node
  148. * The node being viewed.
  149. * @param $view_mode
  150. * The type of output being generated for the node.
  151. * @param $langcode
  152. * The language code of the user.
  153. */
  154. function content_lock_node_view($node, $view_mode, $langcode) {
  155. global $user;
  156. static $messages_shown = FALSE;
  157. if ($view_mode != 'full'
  158. || !_content_lock_is_lockable_node($node)) {
  159. return;
  160. }
  161. if (!$messages_shown) {
  162. _content_lock_show_warnings();
  163. $messages_shown = TRUE;
  164. }
  165. if (empty($node->in_preview)) {
  166. /* Check if the user has pending locks and warn him. */
  167. content_lock_warn_pending_locks($user->uid);
  168. }
  169. }
  170. /**
  171. * Implementation of hook_form_alter().
  172. */
  173. function content_lock_form_alter(&$form, &$form_state, $form_id) {
  174. global $user;
  175. $node = empty($form['#node']) ? NULL : $form['#node'];
  176. $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  177. $destination = 'node/' . $nid;
  178. /* Ensure that users acquire a lock when reverting a node to an older revision. */
  179. if (!empty($form['#node_revision'])) {
  180. $node = $form['#node_revision'];
  181. $nid = $node->nid;
  182. $destination = 'node/' . $nid . '/revisions';
  183. }
  184. /* **************** Restore the node format ****************************** */
  185. // _content_lock_is_lockable_node() needs to know the original
  186. // node format. We either dig up a stashed content_lock_old_format or
  187. // initialize it here.
  188. // Only touch node edit forms and then only if the form is `normal' and has a body with a format (#1183678):
  189. if (is_object($node) && is_numeric($nid)
  190. && ($form_id == $node->type . '_node_form' || $form_id == 'node_revision_revert_confirm')
  191. && !empty($node->body[$node->language][0]['format'])) {
  192. $old_format = $node->body[$node->language][0]['format'];
  193. if (!empty($node->content_lock_old_format)) {
  194. $old_format = $node->content_lock_old_format;
  195. }
  196. if (!empty($form_state['values']['content_lock_old_format'])) {
  197. $old_format = $form_state['values']['content_lock_old_format'];
  198. }
  199. // Needs to be manually set before first form submission.
  200. // We set this in the $node-> namespace because content_lock_nodeapi()
  201. // doesn't see $form_state['values'].
  202. $node->content_lock_old_format = $old_format;
  203. $form['content_lock_old_format'] = array(
  204. '#type' => 'hidden',
  205. '#value' => $node->content_lock_old_format,
  206. );
  207. }
  208. /** ******************* General preconditions for locking ***************** */
  209. // Veto-API. Let other modules veto the locking - so force skipping out of any conditions they want.
  210. // We will use || logic, so if any module denies locking then we deny locking.
  211. // Be sure to notice that content_lock also implements this api for his own vetos!
  212. $skip_lock = FALSE; // no veto yet
  213. $result = module_invoke_all('content_lock_skip_locking', $node, $form_id, $form, $form_state);
  214. foreach($result as $bool) {
  215. if (is_bool($bool)) {
  216. $skip_lock = $skip_lock || $bool;
  217. }
  218. }
  219. if ($skip_lock == FALSE) {
  220. // if we should lock or already have been locked, load the unload js. Dont use
  221. // form alter but rather after build, so it works even for previews
  222. if(variable_get('content_lock_unload_js', true)) {
  223. $form['#after_build'][] = '_content_lock_add_unload_js';
  224. }
  225. // Adding cancel button, if configured
  226. if(variable_get('content_lock_admin_cancelbutton', true)) {
  227. _content_lock_add_cancelbutton($form, $form_state, $form_id);
  228. }
  229. // If we are handling a preview, skip locking
  230. if(!empty($form_state['rebuild']) && $form_state['rebuild'] == TRUE) {
  231. // We dont need anything here right now
  232. }
  233. // If the form did not get submitted we show it the first time
  234. // so try to get the lock if possible
  235. else if ($form_state['submitted'] === FALSE) {
  236. // Finally set the lock if everthing passed.
  237. if(content_lock_node($nid, $user->uid) == false) {
  238. // could not lock node, it's locked by someone else
  239. drupal_goto($destination);
  240. }
  241. }
  242. // else if($form_state['submitted'] === TRUE)
  243. // if it is a submission, we would not need to lock once again, as we had before.
  244. // as nodeapi insert/update are not called on preview, the node should stay locked until saved or canceled.
  245. }
  246. }
  247. /*
  248. * Implementation of our own skip_locking api to implement our logic to skip locks
  249. */
  250. function content_lock_content_lock_skip_locking($node, $form_id, $form, $form_state) {
  251. global $user;
  252. $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  253. /* support the node revision form by not requiring the form to have an 'nid' key */
  254. if (empty($nid) && $form_id == 'node_revision_revert_confirm' && !empty($form['#node_revision'])) {
  255. $nid = $node->nid;
  256. }
  257. // Locked node types. Dont mix this up with the content_types you can chose on the admin form of content lock
  258. // this types are forced due to disfunctionality
  259. $node_type_blacklist = array(
  260. 'user' => TRUE // we are not allowed to lock on users form edit, as it always returns to the edit form..
  261. );
  262. // Form ids listed here will not be locked
  263. $form_id_blacklist = array(
  264. 'comment_form' => TRUE, // dont lock on comment forms
  265. );
  266. if($node != NULL) {
  267. $form_id_blacklist['node_type_form'] = TRUE; // add the node-type administration
  268. }
  269. // Let other modules modify our blacklist
  270. drupal_alter('content_lock_form_id_blacklist', $form_id_blacklist, $node);
  271. if($node == NULL // If we somehow have no node, no need to lock at all
  272. || empty($nid)
  273. || !empty($node_type_blacklist[$node->type])// If this node is blacklisted, don't lock.
  274. || !empty($form_id_blacklist[$form_id]) // If this form is blacklisted, don't lock.
  275. || $user->uid <= 0 // A valid user is needed for locking
  276. || !user_access('check out documents') // The user must have this permission to be able to lock.
  277. || ($form_id != $node->type . '_node_form' // See node_forms(). Don't lock custom forms just because
  278. && $form_id != 'node_revision_revert_confirm') // they have $form['nid'] and $form['#node'].
  279. ) {
  280. // Preconditions failed, skip the lock
  281. return TRUE;
  282. }
  283. // Check if the current node type and format type is configured to be locked
  284. // $node->content_lock_old_format has been set in content_lock_form_alter().
  285. if (!_content_lock_is_lockable_node($node)) {
  286. // It should not be locked, so skip the lock
  287. return TRUE;
  288. }
  289. // we have no veto, so lock the node
  290. return FALSE;
  291. }
  292. /**
  293. * Calculate the token required to unlock a node.
  294. *
  295. * Tokens are required because they prevent CSRF,
  296. * https://security.drupal.org/node/2429.
  297. */
  298. function content_lock_get_release_token($nid) {
  299. return drupal_get_token("content_lock/release/$nid");
  300. }
  301. function _content_lock_add_unload_js(&$form, $form_state) {
  302. $m = drupal_get_path('module','content_lock');
  303. drupal_add_js("$m/js/jquery.url.packed.js", array('group' => JS_LIBRARY));
  304. drupal_add_js("$m/js/onUserExit.js", array('group' => JS_LIBRARY));
  305. drupal_add_js("$m/js/content_lock_init.js");
  306. $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  307. $internal_urls = array();
  308. $internal_form_selectors = array();
  309. /* We're on a locked revision reversion page... */
  310. if (!empty($form['#node_revision']->nid)) {
  311. $nid = $form['#node_revision']->nid;
  312. /* Don't ask the user if he wants to leave the page when cancelling a reversion */
  313. $internal_urls[] = $form['actions']['cancel']['#href'];
  314. $internal_form_selectors[] = '.confirmation';
  315. }
  316. $internal_urls[] = 'node/' . $nid . '/edit';
  317. $internal_form_selectors[] = 'form.node-form';
  318. $lock = content_lock_fetch_lock($nid);
  319. $token = content_lock_get_release_token($nid);
  320. $settings = array(
  321. 'nid' => $nid,
  322. 'ajax_key' => $lock->ajax_key, 'token' => $token,
  323. 'unload_js_message_enable' => variable_get('content_lock_unload_js_message_enable', TRUE),
  324. 'internal_urls' => implode('|', $internal_urls),
  325. 'internal_forms' => implode(', ', $internal_form_selectors),
  326. );
  327. if ($settings['unload_js_message_enable']) {
  328. $settings['unload_js_message'] = variable_get('content_lock_unload_js_message', 'If you proceed, ALL of your changes will be lost.');
  329. }
  330. /*
  331. * Workaround for http://drupal.org/node/1525784 where this function
  332. * is called multiple times when doing a file field AJAX upload and
  333. * array_merge_recursive() is used instead of
  334. * drupal_array_merge_deep_array() to construct the Drupal.settings
  335. * value. Not calling drupal_add_js() multiple times deprives
  336. * file_ajax_upload() of the ability to mess up here ;-).
  337. */
  338. $called =& drupal_static(__FUNCTION__ . '__called');
  339. if (!empty($called)) {
  340. $called++;
  341. return $form;
  342. }
  343. $called = 1;
  344. drupal_add_js(array('content_lock' => $settings), 'setting');
  345. return $form;
  346. }
  347. function _content_lock_verbose() {
  348. return variable_get('content_lock_admin_verbose', true);
  349. }
  350. function _content_lock_add_cancelbutton(&$form, $form_state, $form_id) {
  351. // If we're on the node form
  352. $node = empty($form['#node']) ? NULL : $form['#node'];
  353. $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  354. if (!empty($form['#node_revision'])) {
  355. $node = $form['#node_revision'];
  356. $nid = $node->nid;
  357. }
  358. if (!empty($node) && !empty($nid)
  359. && ($form_id == $node->type . '_node_form' || $form_id == 'node_revision_revert_confirm')) {
  360. // revert node
  361. if ($form_id == 'node_revision_revert_confirm') {
  362. /* hijack the default cancel link to become a lock-releasing link */
  363. $destination = 'node/' . $nid . '/revisions';
  364. if (!empty($form['actions']['cancel']['#href'])) {
  365. $destination = $form['actions']['cancel']['#href'];
  366. }
  367. $form['actions']['cancel']['#href'] = 'admin/content/' . $nid . '/content_lock/releaseown';
  368. $form['actions']['cancel'] += array('#options' => array());
  369. $form['actions']['cancel']['#options'] += array('query' => array('token' => content_lock_get_release_token($nid),
  370. 'destination' => $destination));
  371. }
  372. // If we're editing a node (not adding)
  373. else if ($node->nid) {
  374. $form['actions']['cancel'] = array(
  375. '#type' => 'button',
  376. '#weight' => 2000,
  377. '#value' => t('Cancel'),
  378. '#validate' => array('content_lock_cancel_submit'),
  379. );
  380. if (isset($form['actions']['delete'])) {
  381. $form['actions']['delete']['#weight'] = 2001;
  382. }
  383. }
  384. }
  385. }
  386. /**
  387. * Callback for a cancel request on a form
  388. */
  389. function content_lock_cancel_submit(&$form, &$form_state) {
  390. // Release the node
  391. content_lock_release_own_item($form['#node']->nid, TRUE, TRUE);
  392. }
  393. /**
  394. * Fetch the lock for a node.
  395. *
  396. * @param $nid
  397. * A node id.
  398. * @return
  399. * The lock for the node. FALSE, if the document is not locked.
  400. */
  401. function content_lock_fetch_lock($nid) {
  402. $query = db_select('content_lock', 'c')
  403. ->fields('c')
  404. ->condition('c.nid', $nid);
  405. $u = $query->leftJoin('users', 'u', '%alias.uid = c.uid');
  406. $query->fields($u, array('name'));
  407. return $query->execute()->fetchObject();
  408. }
  409. /**
  410. * Tell who has locked node.
  411. *
  412. * @param $lock
  413. * The lock for a node.
  414. * @return
  415. * String with the message.
  416. */
  417. function content_lock_lock_owner($lock) {
  418. $username = theme('username', array('account' => user_load($lock->uid)));
  419. $date = format_date($lock->timestamp, 'medium');
  420. return t('This document is locked for editing by !name since @date.', array('!name' => $username, '@date' => $date));
  421. }
  422. /**
  423. * Implement hook_user_logout().
  424. */
  425. function content_lock_user_logout($account) {
  426. // removing all locks, as the user logs out
  427. _content_lock_release_all_user_locks($account->uid);
  428. }
  429. /**
  430. * Try to lock a document for editing.
  431. *
  432. * If the lock exists, a new AJAX unlock key is created to combat AJAX
  433. * unlocks during page reloads. See http://drupal.org/node/1049708.
  434. *
  435. * @param $nid
  436. * A node id.
  437. * @param $uid
  438. * The user id to lock the node for.
  439. * @param $quiet
  440. * Suppress any normal user messages.
  441. * @return
  442. * FALSE, if a document has already been locked by someone else.
  443. */
  444. function content_lock_node($nid, $uid, $quiet = FALSE) {
  445. $lock = content_lock_fetch_lock($nid);
  446. if ($lock != FALSE && $lock->uid != $uid) {
  447. $message = content_lock_lock_owner($lock);
  448. if (user_access('administer checked out documents')) {
  449. $url = "admin/content/content_lock/release/$nid";
  450. }
  451. if (isset($url)) {
  452. $token = content_lock_get_release_token($nid);
  453. $message .= '<br />'. t('Click !here to check back in now.', array('!here' => l(t('here'), $url, array('query' => array('token' => $token, 'destination' => $_GET['q'])))));
  454. }
  455. if(!empty($message)) {
  456. drupal_set_message($message, 'warning', FALSE);
  457. }
  458. return FALSE;
  459. }
  460. else {
  461. // no lock yet, create one
  462. if($lock == false) {
  463. // Lock node.
  464. $data = array(
  465. 'nid' => $nid,
  466. 'uid' => $uid,
  467. 'timestamp' => time(),
  468. 'ajax_key' => rand(),
  469. );
  470. drupal_write_record(
  471. 'content_lock',
  472. $data
  473. );
  474. if(_content_lock_verbose() && !$quiet) {
  475. drupal_set_message(t('This document is now locked against simultaneous editing. It will unlock when you navigate elsewhere.'), 'status', FALSE);
  476. }
  477. module_invoke_all('content_lock_locked', $nid, $uid);
  478. } else {
  479. /* A lock already exists: update its AJAX key */
  480. $lock->ajax_key = rand();
  481. if (!drupal_write_record('content_lock', $lock, array('nid'))) {
  482. /*
  483. * we encountered a race condition where the lock was deleted
  484. * between when we loaded it and when we tried to update it
  485. * with a new key. Recreate the lock then:
  486. */
  487. drupal_write_record('content_lock', $lock);
  488. }
  489. }
  490. }
  491. return TRUE;
  492. }
  493. /**
  494. * Release a locked node.
  495. *
  496. * @param $nid
  497. * The node id to release the edit lock for.
  498. * @param $uid
  499. * If set, verify that a lock belongs to this user prior to release.
  500. */
  501. function content_lock_release($nid, $uid = NULL) {
  502. $query = db_delete('content_lock')
  503. ->condition('nid', $nid);
  504. if (!empty($uid))
  505. $query->condition('uid', $uid);
  506. $query->execute();
  507. module_invoke_all('content_lock_released', $nid);
  508. }
  509. function _content_lock_release_all_user_locks($uid) {
  510. db_delete('content_lock')
  511. ->condition('uid', $uid)
  512. ->execute();
  513. }
  514. /**
  515. * Build an overview of locked documents.
  516. *
  517. * @param $account
  518. * A user object.
  519. */
  520. function content_lock_overview($account = NULL) {
  521. global $user;
  522. // TODO: old checkout code, review
  523. $header = array(array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'));
  524. // in the case of an admin, we dont have uid, as he sees all locks
  525. if (!$account) {
  526. $header[] = array('data' => t('Username'), 'field' => 'u.name');
  527. $uid = NULL;
  528. }
  529. // otherwise we have the account of the user just beeing views as argument
  530. else {
  531. $uid = $account->uid;
  532. }
  533. $header[] = array('data' => t('Locked since'), 'field' => 'c.timestamp');
  534. if($uid == $user->uid || user_access('administer checked out documents')) {
  535. $header[] = t('Operations');
  536. }
  537. $query = db_select('content_lock', 'c')
  538. ->extend('TableSort')
  539. ->fields('c');
  540. $n = $query->join('node', 'n', '%alias.nid = c.nid');
  541. $query->fields($n, array('title'));
  542. $u = $query->join('users', 'u', '%alias.uid = c.uid');
  543. $query->fields($u, array('name'));
  544. if ($uid)
  545. $query->condition('c.uid', $uid);
  546. $query->orderByHeader($header);
  547. $rows = array();
  548. foreach ($query->execute() as $data) {
  549. $url = $uid ? "admin/content/".$data->nid."/content_lock/releaseown" : 'admin/content/content_lock/release/'.$data->nid;
  550. $row = array();
  551. $row[] = l($data->title, "node/$data->nid");
  552. if (!$uid) {
  553. $row[] = theme('username', array('account' => user_load($data->uid)));
  554. }
  555. $row[] = format_date($data->timestamp, 'small');
  556. if($uid == $user->uid || user_access('administer checked out documents')) {
  557. $row[] = l(t('release lock'), $url, array('query' => array('token' => content_lock_get_release_token($data->nid))));
  558. }
  559. $rows[] = $row;
  560. }
  561. $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'content_lock'),
  562. 'empty' => t('No locked documents.')));
  563. $output .= theme('pager', array('quantity' => 50));
  564. return $output;
  565. }
  566. /**
  567. * Menu callback; release a locked node for all users or a specific user.
  568. *
  569. * @param $nid
  570. * A node id.
  571. * @param $account
  572. * A user object. If passed, the lock will only be released if this
  573. * user owned it.
  574. * @return
  575. * This function will execute a redirect and doesn't return.
  576. */
  577. function content_lock_release_item($nid, $account = NULL) {
  578. global $user;
  579. if (empty($_GET['token']) || !drupal_valid_token($_GET['token'], "content_lock/release/$nid")) {
  580. return MENU_ACCESS_DENIED;
  581. }
  582. if (!$account && _content_lock_verbose()) {
  583. /*
  584. * Enable our "lock released" message to inform the user who
  585. * likely owned the lock which is to be broken.
  586. */
  587. $lock = content_lock_fetch_lock($nid);
  588. }
  589. content_lock_release($nid, $account ? $account->uid : NULL);
  590. if(_content_lock_verbose()) {
  591. if (!empty($lock) && !$account && $user->uid != $lock->uid) {
  592. $lock_account = user_load($lock->uid);
  593. drupal_set_message(t('The editing lock held by !user has been released.', array('!user' => theme('username', array('account' => $lock_account)))), 'status', FALSE);
  594. } else {
  595. drupal_set_message(t('The editing lock has been released.'),'status', FALSE);
  596. }
  597. }
  598. drupal_goto($account ? "user/{$account->uid}/content_lock" : 'admin/content/content_lock');
  599. }
  600. /**
  601. * For every lock a user current have on any nodes, print a warning messagt
  602. * with an link to release this node.
  603. *
  604. */
  605. function content_lock_warn_pending_locks($uid) {
  606. // cache
  607. static $warned_nodes = array();
  608. static $content_lock_messages_printed = false;
  609. if($content_lock_messages_printed) {
  610. return;
  611. }
  612. if(array_key_exists($uid,$warned_nodes)){
  613. // do nothing
  614. }
  615. else {
  616. // load form db
  617. $warned_nodes[$uid] = array();
  618. $query = db_select('content_lock', 'c')
  619. ->fields('c', array('nid'))
  620. ->condition('c.uid', $uid);
  621. $n = $query->leftJoin('node', 'n', '%alias.nid = c.nid');
  622. $query->fields($n, array('title'));
  623. foreach ($query->execute() as $lock) {
  624. $warned_nodes[$uid][] = $lock;
  625. }
  626. }
  627. foreach($warned_nodes[$uid] as $lock) {
  628. $nodetitle_link = l($lock->title,"node/{$lock->nid}");
  629. $token = content_lock_get_release_token($lock->nid);
  630. $releasethelock_link = l(t('release the lock'),"admin/content/{$lock->nid}/content_lock/releaseown", array('query' => array('token' => $token)));
  631. _content_lock_save_lock_warning(t("The node '!nodetitle_link' is locked by you. You may want to '!releasethelock_link' in order to allow others to edit.", array ('!nodetitle_link' => $nodetitle_link, '!releasethelock_link' => $releasethelock_link)),$lock->nid);
  632. }
  633. $content_lock_messages_printed = true;
  634. }
  635. function _content_lock_save_lock_warning($message, $nid) {
  636. if(empty($_SESSION['content_lock'])) {
  637. $_SESSION['content_lock'] = '';
  638. }
  639. $data = unserialize($_SESSION['content_lock']);
  640. if(!is_array($data)) {
  641. $data = array();
  642. }
  643. if(array_key_exists($nid,$data)) {
  644. return;
  645. }
  646. $data[$nid] = $message;
  647. $_SESSION['content_lock'] = serialize($data);
  648. }
  649. function _content_lock_show_warnings() {
  650. global $user;
  651. if(empty($_SESSION['content_lock'])) {
  652. return;
  653. }
  654. $data = unserialize($_SESSION['content_lock']);
  655. if(!is_array($data) || count($data) == 0) {
  656. return;
  657. }
  658. foreach($data as $nid => $messsage) {
  659. if(_content_lock_still_locked($user->uid,$nid) > 0){
  660. drupal_set_message($messsage,'warning', FALSE);
  661. }
  662. }
  663. $_SESSION['content_lock'] = '';
  664. }
  665. function _content_lock_still_locked($uid,$nid) {
  666. $query = db_select('content_lock', 'c')
  667. ->condition('nid', $nid)
  668. ->condition('uid', $uid)
  669. ->countQuery();
  670. $result = $query->execute();
  671. return (bool)$result->fetchCol();
  672. }
  673. /**
  674. * Release the lock of a node. We are using the current users uid, so the user only can delete
  675. * his own locks. We never fail, as if the lock does not exist, the node is unlocked anyway
  676. *
  677. * @param $response
  678. * When set to FALSE, indicates that the request was made through
  679. * ajax. This means that we shouldn't talk to the user. It also
  680. * means that we should compare the ajax_key to fix the page Reload
  681. * bug (http://drupal.org/node/1049708). In the page reload bug, the
  682. * browser sends a request to load the edit page and simultaneously
  683. * sends an AJAX request asking for the node to be unlocked. By
  684. * changing the ajax_key when responding to the browser, we can
  685. * detect that the soon-to-come ajax request is from the previous
  686. * page load and that it should be ignored.
  687. * @param $ignore_token
  688. * Use this to disable the anti-CSRF token check. This should only
  689. * be disabled when some other means is being used to prevent
  690. * CSRF. Drupal forms, for example, are already protected by the
  691. * equivalent of a token—we need not and may not go adding tokens to
  692. * the node forms we hijack.
  693. */
  694. function content_lock_release_own_item($nid, $response = TRUE, $ignore_token = FALSE) {
  695. global $user;
  696. if (!$ignore_token) {
  697. if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], "content_lock/release/$nid")) {
  698. return MENU_ACCESS_DENIED;
  699. }
  700. }
  701. if($nid != NULL) {
  702. /*
  703. * Imply that this is an AJAX request if we aren't expected to
  704. * interface with a human.
  705. */
  706. if (!$response) {
  707. $lock = content_lock_fetch_lock($nid);
  708. if (strcmp($_GET['k'], $lock->ajax_key)) {
  709. /* the key doesn't match, don't unlock the node */
  710. if ($response) {
  711. drupal_set_message('Trounced AJAX unlock request.', 'status', FALSE);
  712. }
  713. exit();
  714. }
  715. }
  716. content_lock_release($nid,$user->uid);
  717. // drupal_get_messages();
  718. if($response) {
  719. drupal_goto("node/$nid");
  720. }
  721. else {
  722. exit();
  723. }
  724. }
  725. else { // thats what we do, if a user was creating a node and canceled
  726. if($response) {
  727. drupal_goto();
  728. }
  729. else {
  730. exit();
  731. }
  732. }
  733. }
  734. /**
  735. * Check whether a node is configured to be protected by content_lock.
  736. */
  737. function _content_lock_is_lockable_node($node) {
  738. return !in_array(FALSE, module_invoke_all('content_lock_node_lockable', $node));
  739. }
  740. /**
  741. * Implement our own hook_content_lock_node_lockable().
  742. */
  743. function content_lock_content_lock_node_lockable($node) {
  744. static $lockable = array();
  745. // To catch the case where the user is changing the input format,
  746. // we store the original input format. Remember that not all nodes
  747. // store formats in the same way nor even have formats (#1183678).
  748. $format = '';
  749. if (!empty($node->body[$node->language][0]['format'])) {
  750. $format = $node->body[$node->language][0]['format'];
  751. }
  752. if (!empty($node->content_lock_old_format)) {
  753. $format = $node->content_lock_old_format;
  754. }
  755. // Check for a cache hit
  756. if (isset($lockable[$format][$node->nid])) {
  757. return $lockable[$format][$node->nid];
  758. }
  759. $types = array_filter(variable_get('content_lock_allowed_node_types', array()));
  760. // Let other modules modify our blacklist
  761. drupal_alter('content_lock_node_type_blacklist', $types, $node);
  762. $formats = array_filter(variable_get('content_lock_allowed_formats', array()));
  763. $lockable[$format][$node->nid] = FALSE;
  764. // Determine if the node is of a lockable content type or text format.
  765. if ((empty($types) || in_array($node->type, $types))
  766. && (empty($formats) || in_array($format, $formats))) {
  767. $lockable[$format][$node->nid] = TRUE;
  768. }
  769. return $lockable[$format][$node->nid];
  770. }
  771. function content_lock_views_api() {
  772. return array(
  773. 'api' => 2.0,
  774. 'path' => drupal_get_path('module', 'content_lock') . '/views',
  775. );
  776. }