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

Contents of /contributions/modules/collaborative_editor/collaborative_editor.module

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


Revision 1.15 - (show annotations) (download) (as text)
Sat May 12 08:12:37 2007 UTC (2 years, 6 months ago) by ernest
Branch: MAIN
CVS Tags: HEAD
Changes since 1.14: +50 -214 lines
File MIME type: text/x-php
Bug http://drupal.org/node/78553 and other IE visual elements fixed
1 <?php
2 // $Id: collaborative_editor.module,v 0.10
3
4 /**
5 * @file
6 * Collaborative Editor Module
7 *
8 * This module allows concurrent edition when content is created in Drupal
9 *
10 *
11 * IMPORTANT: Read INSTALL.txt first
12 *
13 *
14 */
15
16
17 /**
18 * Implementation of hook_help().
19 */
20
21 function collaborative_editor_help($section) {
22 switch ($section) {
23 case 'admin/modules#description':
24 return('Allows concurrent edition when content is created');
25 }
26 }
27
28
29 /**
30 * Implementation of hook_perm().
31 */
32 function collaborative_editor_perm() {
33 return array('create content', 'edit own content');
34 }
35
36
37 /**
38 * Implementation of hook_access().
39 */
40 function collaborative_editor_access($op, $node) {
41 global $user;
42
43 if ($op == 'create') {
44 return user_access('create content');
45 }
46
47 if ($op == 'update' || $op == 'delete') {
48 if (user_access('create content') ) {
49 return TRUE;
50 }
51 }
52 }
53
54 /**
55 * Implementation of hook_menu
56 */
57 function collaborative_editor_menu($may_cache) {
58 $items = array();
59 if ($may_cache) {
60 /* only used to remove change the user status when they leave the document
61 $items[] = array('path' => 'collaborative_editor/removeUserStatus',
62 'title' => t('collaborative_editor'),
63 'callback' => 'remove_user_status',
64 'type' => MENU_CALLBACK,
65 'access' => user_access('create content'));
66 */
67 }
68 else {
69 // this path will be called in each async call
70 $items[] = array('path' => 'collaborative_editor/'. arg(1) .'/getAsyncContent',
71 'title' => t('collaborative_editor'),
72 'callback' => 'collaborative_editor_async_content',
73 'type' => MENU_CALLBACK,
74 'access' => user_access('create content'));
75 }
76 return $items;
77 }
78
79
80
81
82 /**
83 * Implementation of hook_form_alter
84 */
85 function collaborative_editor_form_alter($form_id, &$form) {
86 global $user;
87 if (is_numeric(arg(1))) {
88 $result = db_query('SELECT * FROM {ce_content} WHERE cid = %d',arg(1));
89 // if we already have the ce content created already
90 if (db_num_rows($result)) {
91 $content_from_db = db_fetch_object($result);
92 $init_content = $content_from_db->body;
93 $revision = $content_from_db->rid;
94 }
95 else {
96 $revision = 0;
97 $init_content = '';
98 }
99
100 if (isset($form['type'])) {
101 $path = drupal_get_path('module', 'collaborative_editor');
102 // here we insert the js files we need
103 drupal_add_js($path . '/drupal.collaborative_editor_ui.js');
104 drupal_add_js($path . '/drupal.collaborative_editor.js');
105
106 // this fieldset is used to insert ui elements via DOM
107 $form['track'] = array(
108 '#type' => 'fieldset',
109 '#title' => t('Collaborative editor'),
110 );
111 $form['cid'] = array('#type' => 'hidden', '#default_value' => arg(1));
112 // the initial revision id should be present in the initial form as well
113 $form['rev'] = array('#type' => 'hidden', '#default_value' => $revision);
114 $form['init'] = array('#type' => 'hidden', '#default_value' => $init_content);
115 // this field is only to show the debug values
116 $form['debug'] = array('#type' => 'textarea', '#title' => t('Debug'), '#rows' => 10);
117 }
118 }
119 }
120
121
122
123
124
125 // this function handles the use cases of each async call
126 function collaborative_editor_async_content() {
127 global $user;
128
129 $cid = arg(1);
130
131 // remove those user status whose users are not currently editing
132 check_user_status();
133
134 //This variable is used to store the revision id the server will send back to the client
135 $return_rev = '';
136
137 // get post params:
138 $new_content = $_POST['diffText']; // the new content the user has added
139 $client_rev = $_POST['clientRev']; // should be the same as the server give them last time
140
141 $result = db_query("SELECT * FROM {ce_content} WHERE cid = %d", $cid);
142 // if we already have the ce content created already
143 if (db_num_rows($result)) {
144 $content_from_db = db_fetch_object($result);
145
146 $insert_start = $_POST['prefixLength']; // in what initial position has been added this content
147 $replacement_length = $_POST['replacementLength']; // if there has been a replacement or content deleted, how long it is
148
149 // this checks if the client has modified the content
150 $is_diff = strlen($new_content) > 0 || $replacement_length != 0;
151
152 // insert the values we upload into an array to send it later as pending changes for other users
153 // The aim is to avoid other users work on outdated versions of the content
154 // This changes will be applied to other users so that they can insert their content in the right place
155 $pchanges = array();
156 $pchanges['start'] = $insert_start;
157 $pchanges['replacement'] = $replacement_length;
158 $pchanges['content_length'] = strlen($new_content); // here we dont need to know the content, only its length
159
160 // the revision id of the current content on the server. It changes everytime someone uploads new content
161 $server_rev = $content_from_db->rid;
162
163 }
164 else {
165 // if there is no ce content we create a new one
166 db_query("INSERT INTO {ce_content} (cid,rid) VALUES (%d,'%s')",$cid,$client_rev);
167 $server_rev = $client_rev;
168 $new_doc = true;
169 }
170
171
172 /* implement use cases
173 # case 1: client uploads diff. no changes in server
174 # case 2: client checks but no diff to upload. no changes in server
175 # case 3: client uploads diff. there are changes in server (modifications by others)
176 # case 4: client checks but no diff to upload. there are changes in server (modifications by others)
177 # case 5: client uploads diff but there is a collision
178 # case 6: user still typing, whatever other cases are happening. If there are new changes from other users they will be updated when user stops typing
179 */
180 if ($server_rev != $client_rev) { // if the revision in the server is different than ours means that somebody else has done changes in the document
181 //we set the flag to know there has been changes from others
182 $otherschange = 1;
183 if ($is_diff) { // if we have done any changes
184
185 // debug var
186 $phase = "#3 - changes done. also changes from others";
187
188 // first check if we have pending changes
189 $result = db_query("SELECT pchange FROM {ce_pchanges} WHERE uid = %d AND cid = %d", $user->uid, $cid);
190 // if so, we use them to know where we will have to insert our content
191 while ($row = db_fetch_object($result)) {
192 $offset = unserialize($row->pchange); // the values are extracted from the array
193 $offset_start = $offset['start'];
194 $offset_length = $offset['content_length'];
195 $offset_content = $offset['replacement'];
196
197 // Note: the more tests we do the more bugs we can find in the lines below of collision detection
198 // Dont get confused with the bug # in IE which can make fail the following lines
199 // Here we bother the others inserts happening before our caret position. We dont bother those after our caret position
200 if ($offset_start <= $insert_start) {
201 // check if there is collision with others' changes
202 // This is true if our caret is in between others replacements or deletions
203 if (($offset_start + $offset_content) > $insert_start) {
204 $phase = "#5 - collision";
205 $collision = true;
206 }
207 // update the caret position where we will insert content
208 $insert_start += $offset_length - $offset_content;
209 }
210
211 // there is a second possible collision to check. Others' inserts happen after our caret position
212 // This is true if we replace or delete where others' caret is
213 elseif (($insert_start + $replacement_length) > $offset_start) {
214 $phase = "#5 - collision";
215 $collision = true;
216 }
217 }
218
219 // if there's no collision we continue with the use case #3
220 if (!$collision) {
221 // everytime we store new content the revision id is updated
222 $new_rev = rand(10000,99999);
223 db_query("UPDATE {ce_content} SET rid = '%s' WHERE cid = %d", $new_rev, $cid);
224
225 // put the changes to others' pending list
226 // we take the active users from the ce_users table
227 $result = db_query("SELECT uid FROM {ce_users} WHERE cid = %d", $cid);
228 while ($row = db_fetch_object($result)) {
229 if ($row->uid != $user->uid) {
230 db_query("INSERT INTO {ce_pchanges} (cid,uid,pchange) VALUES ('%s',%d,'%s')",$cid,$row->uid,serialize($pchanges));
231 }
232 }
233
234 // do the insert or replacement and update the database content
235 $display = substr_replace($content_from_db->body, $new_content, $insert_start, $replacement_length); // ( mixed string, string replacement, int start [, int length] )
236 db_query("UPDATE {ce_content} SET body = '%s' WHERE cid = %d", $display, $cid);
237
238 // delete our pending changes
239 delete_pending_changes(arg(1));
240 $return_rev = $new_rev;
241 }
242 // here we handle the collision case. Since the last change is not valid we only return the server content as it is
243 else {
244 $display = $content_from_db->body;
245 $return_rev = $server_rev;
246
247 delete_pending_changes(arg(1));
248 }
249
250 }
251 else {
252 $phase = "#4 - no changes. but changes from others";
253
254 // we return the content of the server
255 $display = $content_from_db->body;
256 $return_rev = $server_rev;
257
258 delete_pending_changes(arg(1));
259 }
260
261 }
262 // If we get this point is because the revision id in the server is the same as the revision we upload. Therefore there is
263 // no change from others in the server content. Two use cases are possible here depending on whether we upload new content (case 1)
264 // or not (case 2: periodic check -> when this case happens and we are editing alone the heartbeat refresh ought to be set
265 // to 30 seconds or so)
266 elseif ($is_diff && !$new_doc) { // #1
267 $phase = "#1 - changes done. nobody else changes";
268
269 // create a new revision since there is a new version of the content
270 $new_rev = rand(10000,99999);
271 db_query("UPDATE {ce_content} SET rid = '%s' WHERE cid = %d", $new_rev, $cid);
272
273 // put the changes to others' pending list
274 // we take the active users from the ce_users table
275 $result = db_query("SELECT uid FROM {ce_users} WHERE cid = %d", $cid);
276 while ($row = db_fetch_object($result)) {
277 if ($row->uid != $user->uid) {
278 db_query("INSERT INTO {ce_pchanges} (cid,uid,pchange) VALUES ('%s',%d,'%s')",$cid,$row->uid,serialize($pchanges));
279 }
280 }
281
282 // do changes and update to db
283 $display = substr_replace($content_from_db->body, $new_content, $insert_start, $replacement_length); // ( mixed string, string replacement, int start [, int length] )
284 db_query("UPDATE {ce_content} SET body = '%s' WHERE cid = %d", $display, $cid);
285
286 // delete our pending changes
287 delete_pending_changes(arg(1));
288 // the revision id returned will be the one we created before
289 $return_rev = $new_rev;
290 // set the flag since there are no changes from others
291 $otherschange = 0;
292
293 }
294 else {
295 // here we dont set the return_rev variable, so this will be blank in the returned hash. The uploaded_rev will be used in the client side instead
296 // since there has been no change in the content the uploaded revision id by the client is valid
297 $phase = "#2 - no movement";
298 $display = $content_from_db->body; // Note: I have to test if I can avoid to send this back
299
300 $otherschange = 0;
301 }
302
303 // set response . json object
304 $display = rawurlencode($display); // we encode line breaks since so that json object doesnt show an error
305 if ($collision) $collision = 1; // not possible send back boolean types ???
306
307
308 // we store the current users in an array to show them in the list below the textarea
309 $result = db_query("SELECT n.name FROM {users} n, {ce_users} c WHERE c.cid = %d AND c.uid = n.uid", $cid);
310 $currentusers = array();
311 while ($row = db_fetch_object($result)) {
312 array_push($currentusers, "'".$row->name."'");
313 }
314 $sendusers = implode(",",$currentusers);
315
316 // if the user is editing the content for the first time the user status is set
317 $result = db_query('SELECT * FROM {ce_users} WHERE cid = %d AND uid = %d',arg(1),$user->uid);
318 if (!db_num_rows($result)) {
319 db_query("INSERT INTO {ce_users} (cid,uid) VALUES (%d,%d)",arg(1),$user->uid);
320 }
321 // if it is not the first time but still editing we update the checked field
322 db_query("UPDATE {ce_users} SET checked = %d, refresh = %d WHERE uid = %d AND cid = %d", time(), 20, $user->uid, $cid);
323
324 echo "{content: '$display',
325 return_rev: '$return_rev',
326 uploaded_rev: '$client_rev',
327 phase: '$phase',
328 otherschange: '$otherschange',
329 collision: '$collision',
330 currentusers: [ $sendusers ]
331 }";
332
333
334 }
335
336
337 function check_user_status() {
338 // remove those users who havent edited in the last 20 seconds
339 $result = db_query('SELECT * FROM {ce_users} WHERE checked + %d < %d', 20, time());
340 while ($row = db_fetch_object($result)) {
341 remove_user_status($row->cid, $row->uid);
342 }
343 }
344
345
346 function delete_pending_changes($cid) {
347 global $user;
348
349 db_query("DELETE FROM {ce_pchanges} WHERE uid = %d AND cid = %d", $user->uid, $cid);
350
351 }
352
353
354 function remove_user_status($cid, $uid) {
355 global $user;
356
357 db_query("DELETE FROM {ce_users} WHERE uid = %d AND cid = %d", $uid, $cid);
358 db_query("DELETE FROM {ce_pchanges} WHERE uid = %d AND cid = %d", $uid, $cid);
359 }
360
361 ?>

  ViewVC Help
Powered by ViewVC 1.1.2