$_SESSION['user'], 'filename' => $_POST['filename']); $entry = getDB()->select($query, 'registered'); if ($entry != null) { $entry->remove(); echo formatJSEND('success'); } else { // Should only be enabled when testing //echo formatJSEND('success', 'Not registered as collaborator for ' . $_POST['filename']); echo formatJSEND('success'); } break; case 'unregisterFromAllFiles': /* Find all the files for which the current user is registered as * collaborator and unregister him. */ unregisterFromAllFiles($_SESSION['user']); echo formatJSEND('success'); break; case 'removeSelectionAndChangesForAllFiles': $query = array('user' => $_SESSION['user'], 'filename' => '*'); $entries = getDB()->select($query, 'selection'); foreach($entries as $entry) { $entry->remove(); } $entries = getDB()->select($query, 'change'); foreach($entries as $entry) { $entry->remove(); } echo formatJSEND('success'); break; case 'removeServerTextForAllFiles': $entries = getDB()->select_group('text'); foreach($entries as $entry) $entry->remove(); echo formatJSEND('success'); break; case 'sendSelectionChange': /* Push the current selection to the server. */ if(!isset($_POST['filename']) || empty($_POST['filename'])) { exit(formatJSEND('error', 'No Filename Specified in sendSelectionChange')); } if(!isset($_POST['selection']) || empty($_POST['selection'])) { exit(formatJSEND('error', 'No selection specified in sendSelectionChange')); } /* If user is not already registerd for the given file, register him. */ if (!isUserRegisteredForFile($_POST['filename'], $_SESSION['user'])) { $isRegistered = registerToFile($_POST['filename'], $_SESSION['user']); if (!$isRegistered) { // Should only be enabled when testing //echo formatJSEND('success', 'Not registered as collaborator for ' . $_POST['filename']); exit; } } $selection = json_decode($_POST['selection']); $query = array('user' => $_SESSION['user'], 'filename' => $_POST['filename']); $entry = getDB()->create($query, 'selection'); $entry->put_value($selection); echo formatJSEND('success'); break; case 'getUsersAndSelectionsForFile': /* Get an object containing all the users registered to the given file * and their associated selections. The data corresponding to the * current user is omitted. */ if(!isset($_POST['filename']) || empty($_POST['filename'])) { exit(formatJSEND('error', 'No filename specified in getUsersAndSelectionsForFile')); } $filename = $_POST['filename']; $usersAndSelections = array(); $users = getRegisteredUsersForFile($filename); foreach ($users as $user) { if ($user !== $_SESSION['user']) { $selection = getSelection($filename, $user); if (!empty($selection)) { $data = array( "selection" => $selection, "color" => getColorForUser($user) ); $usersAndSelections[$user] = $data; } } } echo formatJSEND('success', $usersAndSelections); break; case 'sendShadow': if(!isset($_POST['filename']) || empty($_POST['filename'])) { exit(formatJSEND('error', 'No filename specified in sendShadow')); } if(!isset($_POST['shadow'])) { exit(formatJSEND('error', 'No shadow specified in sendShadow')); } $filename = $_POST['filename']; $clientShadow = $_POST['shadow']; setShadow($filename, $_SESSION['user'], $clientShadow); /* If there is no server text for $filename or if there is still no or * only one user registered for $filename, set the server text equal * to the shadow. */ $registeredUsersForFileCount = count(getRegisteredUsersForFile($filename)); if (!existsServerText($filename) || $registeredUsersForFileCount == 0) { setServerText($filename, $clientShadow); } echo formatJSEND('success'); break; case 'synchronizeText': if(!isset($_POST['filename']) || empty($_POST['filename'])) { exit(formatJSEND('error', 'No filename specified in synchronizeText')); } if(!isset($_POST['patch'])) { exit(formatJSEND('error', 'No patch specified in synchronizeText')); } $query = array('filename' => $_POST['filename']); $serverTextEntry = getDB()->select($query, 'text'); if ($serverTextEntry == null) { exit(formatJSEND('error', 'Inconsistent sever text filename in synchronizeText: ' . $serverTextEntry)); } $query = array('user' => $_SESSION['user'], 'filename' => $_POST['filename']); $shadowTextEntry = getDB()->select($query, 'shadow'); if ($shadowTextEntry == null) { exit(formatJSEND('error', 'Inconsistent sever text filename in synchronizeText: ' . $shadowTextEntry)); } /* First acquire a lock or wait until a lock can be acquired for server * text and shadow. */ $serverTextEntry->lock(); $shadowTextEntry->lock(); $serverText = $serverTextEntry->get_value(); $shadowText = $shadowTextEntry->get_value(); $patchFromClient = $_POST['patch']; /* Patch the shadow and server texts with the edits from the client. */ $dmp = new diff_match_patch(); $patchedServerText = $dmp->patch_apply($dmp->patch_fromText($patchFromClient), $serverText); $serverTextEntry->put_value($patchedServerText[0]); $patchedShadowText = $dmp->patch_apply($dmp->patch_fromText($patchFromClient), $shadowText); /* Make a diff between server text and shadow to get the edits to send * back to the client. */ $patchFromServer = $dmp->patch_toText($dmp->patch_make($patchedShadowText[0], $patchedServerText[0])); /* Apply it to the shadow. */ $patchedShadowText = $dmp->patch_apply($dmp->patch_fromText($patchFromServer), $patchedShadowText[0]); $shadowTextEntry->put_value($patchedShadowText[0]); /* Release locks. */ $serverTextEntry->unlock(); $shadowTextEntry->unlock(); echo formatJSEND('success', $patchFromServer); break; case 'sendHeartbeat': /* Hard coded heartbeat time interval. Beware to keep this value here * twice the value on client side. */ $maxHeartbeatInterval = 5; $currentTime = time(); /* Check if the user is a new user, or if it is just an update of * his heartbeat. */ $isUserNewlyConnected = true; $query = array('user' => $_SESSION['user']); $entry = getDB()->select($query, 'heartbeat'); if($entry != null) { $heartbeatTime = $entry->get_value(); $heartbeatInterval = $currentTime - $heartbeatTime; $isUserNewlyConnected = ($heartbeatInterval > 1.5*$maxHeartbeatInterval); /* If the user is newly connected and if the heartbeat file * exits, that mean that the user was the latest in the previous * collaborative session. We need to call the disconnect method * to clear the data relatives to the user before calling the * connect method. */ if($isUserNewlyConnected) { onCollaboratorDisconnect($_SESSION['user']); } } updateHeartbeatMarker($_SESSION['user']); /* If the user is newly connected, we fire the * corresponding method. */ if($isUserNewlyConnected) { onCollaboratorConnect($_SESSION['user']); } $usersAndHeartbeatTime = getUsersAndHeartbeatTime(); foreach ($usersAndHeartbeatTime as $user => $heartbeatTime) { if (($currentTime - $heartbeatTime) > $maxHeartbeatInterval) { /* The $user heartbeat time is too old, consider him dead and * remove his 'registered' and 'heartbeat' marker files. */ unregisterFromAllFiles($user); removeHeartbeatMarker($user); onCollaboratorDisconnect($user); } } /* Return the number of connected collaborators. */ $collaboratorCount = count(getUsersAndHeartbeatTime()); $data = array(); $data['collaboratorCount'] = $collaboratorCount; echo formatJSEND('success', $data); break; default: exit(formatJSEND('error', 'Unknown Action ' . $_POST['action'])); } // -------------------- /* $filename must contain only the basename of the file. */ function isUserRegisteredForFile($filename, $user) { $query = array('user' => $user, 'filename' => $filename); $entry = getDB()->select($query, 'registered'); return ($entry != null); } /* Unregister the given user from all the files by removing his * 'registered' marker file. */ function unregisterFromAllFiles($user) { $query = array('user' => $user, 'filename' => '*'); $entries = getDB()->select($query, 'registered'); foreach($entries as $entry) { $entry->remove(); } } /* Register as a collaborator for the given filename. Return false if * failed. */ function registerToFile($user, $filename) { $query = array('user' => $user, 'filename' => $filename); $entry = getDB()->select($query, 'registered'); if ($entry != null) { debug('Warning: already registered as collaborator for ' . $filename); return true; } else { $entry = getDB()->create($query, 'registered'); if ($entry != null) { return true; } else { debug('Error: unable to register as collaborator for ' . $filename); return false; } } } /* Touch the heartbeat marker file for the given user. Return true on * success, false on failure. */ function updateHeartbeatMarker($user) { $query = array('user' => $user); $entry = getDB()->create($query, 'heartbeat'); if($entry == null) return false; $entry->put_value(time()); return true; } function removeHeartbeatMarker($user) { $query = array('user' => $user); $entry = getDB()->select($query, 'heartbeat'); if($entry != null) $entry->remove(); } /* Return an array containing the user as key and his last heartbeat time * as value. */ function &getUsersAndHeartbeatTime() { $usersAndHeartbeatTime = array(); $query = array('user' => '*'); $entries = getDB()->select($query, 'heartbeat'); foreach($entries as $entry) { $user = $entry->get_field('user'); $usersAndHeartbeatTime[$user] = $entry->get_value(); } return $usersAndHeartbeatTime; } /* $filename must contain only the basename of the file. */ function &getRegisteredUsersForFile($filename) { $usernames = array(); $query = array('user' => '*', 'filename' => $filename); $entries = getDB()->select($query, 'registered'); foreach($entries as $entry) { $user = $entry->get_field('user'); $usernames[] = $user; } return $usernames; } /* Return the selection object, if any, for the given filename and user. * $filename must contain only the basename of the file. */ function getSelection($filename, $user) { $query = array('user' => $user, 'filename' => $filename); $entry = getDB()->select($query, 'selection'); if($entry == null) return null; return $entry->get_value(); } /* Return the list of changes, if any, for the given filename, user and * from the given revision number. * $filename must contain only the basename of the file. */ function getChanges($filename, $user, $fromRevision) { $query = array('user' => $user, 'filename' => $filename); $entry = getDB()->select($query, 'change'); if($entry == null) return null; return array_slice($entry->get_value(), $fromRevision, NULL, true); } /* Set the server shadow acquiring an exclusive lock on the file. $shadow * is a string. */ function setShadow($filename, $user, $shadow) { $query = array('user' => $user, 'filename' => $filename); $entry = getDB()->create($query, 'shadow'); if($entry == null) return null; $entry->put_value($shadow); } /* Return the shadow for the given filename as a string or an empty string * if no shadow exists. */ function getShadow($filename, $user) { $query = array('user' => $user, 'filename' => $filename); $entry = getDB()->select($query, 'shadow'); if($entry == null) return null; return $entry->get_value(); } function existsServerText($filename) { $query = array('filename' => $filename); $entry = getDB()->select($query, 'text'); return ($entry != null); } /* Set the server text acquiring an exclusive lock on the file. $serverText * is a string. */ function setServerText($filename, $serverText) { $query = array('filename' => $filename); $entry = getDB()->create($query, 'text'); if($entry == null) return null; $entry->put_value($serverText); } /* Return the server text for the given filename as a string or an empty string * if no server text exists. */ function getServerText($filename) { $query = array('filename' => $filename); $entry = getDB()->select($query, 'text'); if($entry == null) return null; return $entry->get_value(); } /* Return the color of the given user. */ function getColorForUser($user) { /* Check if the color is already defined for the * user. */ $query = array('user' => $user); $entry = getDB()->select($query, 'color'); if ($entry != null) { return $entry->get_value(); } /* If the color is not defined for the given user, * we pick an unused color. */ $colors = array( "#0000FF", "#FF0000", "#00FF00", "#FF00FF", "#0F00F0", "#F0000F", "#0F0F0F", "#F0F0F0" ); /* Retreive all used colors. */ $query = array('user' => '*'); $entries = getDB()->select($query, 'color'); $usedColors = array(); foreach ($entries as $entry) { $usedColors[] = $entry->get_value(); } $colors = array_diff($colors, $usedColors); if(count($colors) > 0) { $color = array_shift($colors); } else { $color = "#FFFFFF"; } /* Save the picked color. */ $query = array('user' => $user); $entry = getDB()->create($query, 'color'); $entry->put_value($color); return $color; } /* Remove the color file for the given user. */ function resetColorForUser($user) { $query = array('user' => $user); $entry = getDB()->create($query, 'color'); if($entry != null) $entry->remove(); } /* This function is called when a new collaborator /* is connected. */ function onCollaboratorConnect($user) { //debug('User connected: '.$user); } /* This function is called when a collaborator is * disconnected. */ function onCollaboratorDisconnect($user) { //debug('User disconnected: '.$user); resetColorForUser($user); } ?>