/[drupal]/contributions/modules/checkout/checkout.module
ViewVC logotype

Contents of /contributions/modules/checkout/checkout.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.16 - (show annotations) (download) (as text)
Fri Apr 24 14:11:38 2009 UTC (7 months ago) by smk
Branch: MAIN
CVS Tags: HEAD
Changes since 1.15: +4 -4 lines
File MIME type: text/x-php
Fixed watchdog message.
1 <?php
2 // $Id: checkout.module,v 1.15 2008/08/11 13:40:13 smk Exp $
3
4 /**
5 * @file
6 * Allows users to lock documents for modification.
7 */
8
9 /**
10 * Implementation of hook_perm().
11 */
12 function checkout_perm() {
13 return array('check out documents', 'keep documents checked out', 'administer checked out documents');
14 }
15
16 /**
17 * Implementation of hook_help().
18 */
19 function checkout_help($path, $arg) {
20 switch ($path) {
21 case 'admin/help#checkout':
22 $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>';
23 $output .= '<p>'. t('The Checkout 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>';
24 $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>';
25 return $output;
26
27 case 'admin/content/node/checkout':
28 return '<p>'. t('Below is a list of all locked documents. Click on <em>check in</em> to release a lock.') .'</p>';
29
30 case 'user/%user/checkout':
31 return '<p>'. t('Below is a list of all documents locked by you. Click on <em>check in</em> to release a lock.') .'</p>';
32 }
33 }
34
35 /**
36 * Implementation of hook_init().
37 */
38 function checkout_init() {
39 global $user;
40 if ($user->uid && user_access('check out documents')) {
41 // Avoid AJAX requests unlocking a node. This header is automatically set
42 // when doing AJAX requests through jQuery.
43 if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') {
44 checkout_handle_request($user->uid);
45 }
46 }
47 }
48
49 /**
50 * Implementation of hook_menu().
51 */
52 function checkout_menu() {
53 $items['admin/content/node/checkout'] = array(
54 'title' => 'Locked documents',
55 'page callback' => 'checkout_overview',
56 'access callback' => 'user_access',
57 'access arguments' => array('administer checked out documents'),
58 'weight' => 5,
59 'type' => MENU_LOCAL_TASK,
60 );
61 $items['admin/content/node/checkout/release'] = array(
62 'page callback' => 'checkout_release_item',
63 'page arguments' => array(5, NULL),
64 'access arguments' => array('administer checked out documents'),
65 'type' => MENU_CALLBACK,
66 );
67 $items['user/%user/checkout'] = array(
68 'title' => 'Locked documents',
69 'page callback' => 'checkout_overview',
70 'page arguments' => array(1),
71 'access callback' => 'user_access',
72 'access arguments' => array('check out documents'),
73 'weight' => 5,
74 'type' => MENU_LOCAL_TASK
75 );
76 $items['user/%user/checkout/release'] = array(
77 'page callback' => 'checkout_release_item',
78 'page arguments' => array(4, 1),
79 'access arguments' => array('check out documents'),
80 'type' => MENU_CALLBACK
81 );
82
83 return $items;
84 }
85
86 /**
87 * Implementation of hook_form_alter().
88 */
89 function checkout_form_alter(&$form, $form_state, $form_id) {
90 if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
91 if (user_access('check out documents') && user_access('keep documents checked out')) {
92 $form['checkout'] = array(
93 '#type' => 'checkbox',
94 '#title' => t('Keep document locked'),
95 '#return_value' => 1,
96 '#weight' => 21, // Place immediately after log message.
97 '#default_value' => FALSE,
98 '#description' => t('Check this box if you want to keep this document locked in your name after submitting it.'),
99 );
100 }
101 }
102 else if ($form_id == 'node_configure') {
103 // Make sure our element appears before the submit buttons.
104 $form['buttons']['#weight'] = 10;
105
106 $period = array(0 => t('Disabled')) + drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
107 $form['checkout_clear'] = array(
108 '#type' => 'select',
109 '#title' => t('Automatic check-in'),
110 '#default_value' => variable_get('checkout_clear', 0),
111 '#options' => $period,
112 '#description' => t('The period after which locked documents will be automatically released.'),
113 );
114 }
115 }
116
117 /**
118 * Implementation of hook_nodeapi().
119 */
120 function checkout_nodeapi(&$node, $op, $teaser, $page) {
121 global $user;
122
123 switch ($op) {
124 case 'validate':
125 if (isset($node->nid) && user_access('check out documents')) {
126 // Existing node. Check if we still own the lock.
127 if ($lock = checkout_fetch_lock($node->nid)) {
128 if ($lock->uid != $user->uid) {
129 // Lock is no longer ours.
130 form_set_error('changed', t('Your lock has been removed!') .'<br />'. checkout_lock_owner($lock) .'<br />'. t('You can still save the content if this user aborts the edit operation without saving changes.'));
131 }
132 }
133 else {
134 // Node is not locked. Try to re-lock if node is unchanged.
135 if (node_last_changed($node->nid) > $node->changed || !checkout_node($node->nid, $user->uid)) {
136 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.'));
137 }
138 }
139 }
140 break;
141
142 case 'insert':
143 case 'update':
144 if (!empty($node->checkout)) {
145 checkout_persistent($node->nid);
146 }
147 else if ($op == 'update') {
148 checkout_release($node->nid, $user->uid, TRUE);
149 }
150 break;
151
152 case 'delete':
153 checkout_release($node->nid, NULL, TRUE);
154 break;
155 }
156 }
157
158 /**
159 * Implementation of hook_cron().
160 *
161 * Release nodes that have been locked longer than the configured period.
162 */
163 function checkout_cron() {
164 $checkout_clear = variable_get('checkout_clear', 0);
165 if ($checkout_clear > 0) {
166 $result = db_query('DELETE FROM {checkout} WHERE timestamp < %d', time() - $checkout_clear);
167 if ($num = db_affected_rows($result)) {
168 $period = format_interval($checkout_clear);
169 drupal_set_message(format_plural($num, 'Released one document locked for more than @period.', 'Released @count documents locked for more than @period.', array('@period' => $period)));
170 watchdog('checkout', 'Released @count document(s) locked for more than @period.', array('@count' => $num, '@period' => $period));
171 }
172 }
173 }
174
175 /**
176 * Handle node locking.
177 *
178 * When landing on a node edit page the current node needs to be locked.
179 * When coming from an edit page the previous node needs to be unlocked.
180 *
181 * @param $uid
182 * The user id to (un)lock nodes for.
183 */
184 function checkout_handle_request($uid) {
185 global $base_path;
186
187 // Build referer path
188 $referer_uri = parse_url(referer_uri());
189 if (variable_get('clean_url', 0)) {
190 $referer = substr($referer_uri['path'], strlen($base_path));
191 }
192 else {
193 $vars = array();
194 if (isset($referer_uri['query'])) {
195 parse_str($referer_uri['query'], $vars);
196 }
197 $referer = isset($vars['q']) ? $vars['q'] : '';
198 }
199 if ($referer = rtrim($referer, '/')) {
200 $referer = drupal_get_normal_path($referer);
201 }
202
203 // If refering and current paths match we can abort, since there can't be any
204 // locking action involved.
205 if ($_GET['q'] == $referer) {
206 return;
207 }
208
209 // Otherwise try to extract nid from path.
210 $previous_nid = checkout_get_nid($referer);
211 $current_nid = checkout_get_nid($_GET['q']);
212
213 // Check whether to release a previously edited node.
214 if ($previous_nid && (!$current_nid || $current_nid != $previous_nid)) {
215 checkout_release($previous_nid, $uid);
216 }
217
218 // Check whether to lock the current node.
219 if ($current_nid && (!$previous_nid || $previous_nid != $current_nid)) {
220 // Try to lock the node.
221 if (!checkout_node($current_nid, $uid)) {
222 // Node already locked: send back to refering page.
223 drupal_goto(referer_uri());
224 }
225 }
226 }
227
228 /**
229 * Extract the node id from a node edit path.
230 *
231 * @param $path
232 * The path to match.
233 * @return
234 * The node id extracted from the path.
235 */
236 function checkout_get_nid($path) {
237 static $regexp;
238
239 if (!isset($regexp)) {
240 $patterns = variable_get('checkout_edit_paths', "edit\nrevisions\nrevisions/*\noutline\nclassify");
241 $regexp = '@^node/(\d+)/(?:'. preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/'), array('|', '.*'), preg_quote($patterns, '@')) .')$@';
242 }
243 if (preg_match($regexp, $path, $match)) {
244 return $match[1];
245 }
246 return FALSE;
247 }
248
249 /**
250 * Fetch the lock for a node.
251 *
252 * @param $nid
253 * A node id.
254 * @return
255 * The lock for the node. FALSE, if the document is not locked.
256 */
257 function checkout_fetch_lock($nid) {
258 return db_fetch_object(db_query("SELECT c.*, u.name FROM {checkout} c LEFT JOIN {users} u ON u.uid = c.uid WHERE c.nid = %d", $nid));
259 }
260
261 /**
262 * Tell who has locked node.
263 *
264 * @param $lock
265 * The lock for a node.
266 * @return
267 * String with the message.
268 */
269 function checkout_lock_owner($lock) {
270 $username = theme('username', $lock);
271 $date = format_date($lock->timestamp, 'medium');
272 return t('This document is locked for editing by !name since @date.', array('!name' => $username, '@date' => $date));
273 }
274
275 /**
276 * Try to lock a document for editing.
277 *
278 * @param $nid
279 * A node id.
280 * @param $uid
281 * The user id to lock the node for.
282 * @return
283 * FALSE, if a document has already been locked by someone else.
284 */
285 function checkout_node($nid, $uid) {
286 if ($lock = checkout_fetch_lock($nid)) {
287 // Node is already locked.
288
289 // Deny editing this node even if the node is locked by the same user.
290 // The only exception to this rule is when the user had previously acquired
291 // a persistent lock.
292 if ($lock->uid != $uid || !$lock->persistent) {
293 $message = checkout_lock_owner($lock);
294
295 if ($lock->uid == $uid) {
296 $url = "user/$uid/checkout/release/$nid";
297 }
298 else if (user_access('administer checked out documents')) {
299 $url = "admin/content/node/checkout/release/$nid";
300 }
301 if (isset($url)) {
302 $message .= '<br />'. t('Click <a href="!release-url">here</a> to check back in now.', array('!release-url' => url($url, array('query' => 'destination='. $_GET['q']))));
303 }
304
305 drupal_set_message($message, 'error');
306 return FALSE;
307 }
308 }
309 else {
310 // Lock node.
311 db_query("INSERT INTO {checkout} (nid, uid, timestamp) VALUES (%d, %d, %d)", $nid, $uid, time());
312
313 drupal_set_message(t('This document is now locked against simultaneous editing. It will unlock when you navigate elsewhere.'));
314 }
315
316 return TRUE;
317 }
318
319 /**
320 * Set a persistent document lock.
321 *
322 * @param $nid
323 * The node id to lock persistently.
324 */
325 function checkout_persistent($nid) {
326 db_query("UPDATE {checkout} SET persistent = 1 WHERE nid = %d", $nid);
327 }
328
329 /**
330 * Release a locked node.
331 *
332 * @param $nid
333 * The node id to release the edit lock for.
334 * @param $uid
335 * If set, verify that a lock belongs to this user prior to release.
336 * @param $break
337 * Break persistent locks.
338 */
339 function checkout_release($nid, $uid = NULL, $break = FALSE) {
340 $add_sql = '';
341 $args = array($nid);
342 if (isset($uid)) {
343 $add_sql = " AND uid = %d";
344 $args[] = $uid;
345 }
346 if ($break || !db_result(db_query_range("SELECT persistent FROM {checkout} WHERE nid = %d". $add_sql, $args, 0, 1))) {
347 db_query("DELETE FROM {checkout} WHERE nid = %d". $add_sql, $args);
348 }
349 }
350
351 /**
352 * Build an overview of locked documents.
353 *
354 * @param $account
355 * A user object.
356 */
357 function checkout_overview($account = NULL) {
358 $header = array(array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'));
359 if (!$account) {
360 $header[] = array('data' => t('Username'), 'field' => 'u.name');
361 $uid = NULL;
362 }
363 else {
364 $uid = $account->uid;
365 }
366 $header[] = array('data' => t('Locked since'), 'field' => 'c.timestamp');
367 $header[] = array('data' => t('Persistent lock'), 'field' => 'c.persistent');
368 $header[] = t('Operations');
369
370 $rows = array();
371 $add_sql = $uid ? " WHERE c.uid = %d" : '';
372 $result = pager_query('SELECT c.*, n.title, u.name FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid INNER JOIN {users} u ON u.uid = c.uid'. $add_sql . tablesort_sql($header), 50, 0, NULL, $uid);
373 $url = $uid ? "user/$uid/checkout/release" : 'admin/content/node/checkout/release';
374
375 while ($data = db_fetch_object($result)) {
376 $row = array();
377 $row[] = l($data->title, "node/$data->nid");
378 if (!$uid) {
379 $row[] = theme('username', user_load(array('uid' => $data->uid)));
380 }
381 $row[] = format_date($data->timestamp, 'small');
382 $row[] = $data->persistent ? t('yes') : '&mdash;';
383 $row[] = l(t('check in'), "$url/$data->nid");
384 $rows[] = $row;
385 }
386
387 $output = theme('table', $header, $rows, array('id' => 'checkout'));
388 if (!$rows) {
389 $output .= t('No locked documents.');
390 }
391 else if ($pager = theme('pager', array(), 50, 0)) {
392 $output .= $pager;
393 }
394
395 return $output;
396 }
397
398 /**
399 * Menu callback; release a locked node for all users or a specific user.
400 *
401 * @param $nid
402 * A node id.
403 * @param $account
404 * A user object.
405 * @return
406 * This function will execute a redirect and doesn't return.
407 */
408 function checkout_release_item($nid, $account = NULL) {
409 checkout_release($nid, $account ? $account->uid : NULL, TRUE);
410 drupal_set_message(t('The editing lock has been released.'));
411 drupal_goto($account ? "user/$account->uid/checkout" : 'admin/content/node/checkout');
412 }
413

  ViewVC Help
Powered by ViewVC 1.1.2