From ee4c4a1b5a5840c1b9d2d8c74b3f4298dd52928b Mon Sep 17 00:00:00 2001
From: Andreas Gohr <andi@splitbrain.org>
Date: Sat, 11 Mar 2006 21:01:48 +0100
Subject: [PATCH] Automatic draft saving

DokuWiki now automatically creates a draft file of the currently edited
page. In case of an editing interuption (eg. Browsercrash) the draftfile
can be continued later.

darcs-hash:20060311200148-7ad00-919337a51e001136178d175a1755cd26122e9726.gz
---
 conf/dokuwiki.php          |  1 +
 inc/actions.php            | 77 +++++++++++++++++++++++++++++++++-----
 inc/common.php             | 14 +++++++
 inc/html.php               | 47 ++++++++++++++++++++---
 inc/io.php                 |  3 ++
 inc/lang/en/lang.php       | 10 +++--
 inc/template.php           | 14 +++++--
 lib/exe/ajax.php           | 38 ++++++++++++++++++-
 lib/exe/js.php             |  4 +-
 lib/scripts/edit.js        | 41 +++++++++++++++++---
 lib/scripts/script.js      | 10 +++++
 lib/tpl/default/design.css | 14 +++++++
 12 files changed, 241 insertions(+), 32 deletions(-)

diff --git a/conf/dokuwiki.php b/conf/dokuwiki.php
index 539da08e8..411d2be7d 100644
--- a/conf/dokuwiki.php
+++ b/conf/dokuwiki.php
@@ -65,6 +65,7 @@ $conf['profileconfirm'] = '1';           //Require current password to confirm c
 /* Advanced Options */
 $conf['userewrite']  = 0;                //this makes nice URLs: 0: off 1: .htaccess 2: internal
 $conf['useslash']    = 0;                //use slash instead of colon? only when rewrite is on
+$conf['usedraft']    = 1;                //automatically save a draft while editing (0|1)
 $conf['sepchar']     = '_';              //word separator character in page names; may be a
                                          //  letter, a digit, '_', '-', or '.'.
 $conf['canonical']   = 0;                //Should all URLs use full canonical http://... style?
diff --git a/inc/actions.php b/inc/actions.php
index e92e366d0..963be86a9 100644
--- a/inc/actions.php
+++ b/inc/actions.php
@@ -62,6 +62,14 @@ function act_dispatch(){
   if($ACT == 'save')
     $ACT = act_save($ACT);
 
+  //draft deletion
+  if($ACT == 'draftdel')
+    $ACT = act_draftdel($ACT);
+
+  //draft saving on preview
+  if($ACT == 'preview')
+    $ACT = act_draftsave($ACT);
+
   //edit
   if(($ACT == 'edit' || $ACT == 'preview') && $INFO['editable']){
     $ACT = act_edit($ACT);
@@ -116,10 +124,18 @@ function act_clean($act){
   global $lang;
   global $conf;
 
+  // check if the action was given as array key
+  if(is_array($act)){
+    list($act) = array_keys($act);
+  }
+
   //handle localized buttons
-  if($act == $lang['btn_save']) $act = 'save';
-  if($act == $lang['btn_preview']) $act = 'preview';
-  if($act == $lang['btn_cancel']) $act = 'show';
+  if($act == $lang['btn_save'])     $act = 'save';
+  if($act == $lang['btn_preview'])  $act = 'preview';
+  if($act == $lang['btn_cancel'])   $act = 'show';
+  if($act == $lang['btn_recover'])  $act = 'recover';
+  if($act == $lang['btn_draftdel']) $act = 'draftdel';
+
 
   //remove all bad chars
   $act = strtolower($act);
@@ -136,12 +152,12 @@ function act_clean($act){
     return 'show';
   }
 
-  if(array_search($act,array('login','logout','register','save','edit',
-                             'preview','search','show','check','index','revisions',
-                             'diff','recent','backlink','admin','subscribe',
-                             'unsubscribe','profile','resendpwd',)) === false
-     && substr($act,0,7) != 'export_' ) {
-    msg('Unknown command: '.htmlspecialchars($act),-1);
+  if(!in_array($act,array('login','logout','register','save','edit','draft',
+                          'preview','search','show','check','index','revisions',
+                          'diff','recent','backlink','admin','subscribe',
+                          'unsubscribe','profile','resendpwd','recover',
+                          'draftdel',)) && substr($act,0,7) != 'export_' ) {
+    msg('Command unknown: '.htmlspecialchars($act),-1);
     return 'show';
   }
   return $act;
@@ -156,7 +172,7 @@ function act_permcheck($act){
   global $INFO;
   global $conf;
 
-  if(in_array($act,array('save','preview','edit'))){
+  if(in_array($act,array('save','preview','edit','recover'))){
     if($INFO['exists']){
       if($act == 'edit'){
         //the edit function will check again and do a source show
@@ -186,6 +202,43 @@ function act_permcheck($act){
   return 'denied';
 }
 
+/**
+ * Handle 'draftdel'
+ *
+ * Deletes the draft for the current page and user
+ */
+function act_draftdel($act){
+  global $INFO;
+  @unlink($INFO['draft']);
+  $INFO['draft'] = null;
+  return 'show';
+}
+
+/**
+ * Saves a draft on preview
+ *
+ * @todo this currently duplicates code from ajax.php :-/
+ */
+function act_draftsave($act){
+  global $INFO;
+  global $ID;
+  global $conf;
+  if($conf['usedraft'] && $_POST['wikitext']){
+    $draft = array('id'     => $ID,
+                   'prefix' => $_POST['prefix'],
+                   'text'   => $_POST['wikitext'],
+                   'suffix' => $_POST['suffix'],
+                   'date'   => $_POST['date'],
+                   'client' => $INFO['client'],
+                  );
+    $cname = getCacheName($draft['client'].$ID,'.draft');
+    if(io_saveFile($cname,serialize($draft))){
+      $INFO['draft'] = $cname;
+    }
+  }
+  return $act;
+}
+
 /**
  * Handle 'save'
  *
@@ -215,6 +268,9 @@ function act_save($act){
   //unlock it
   unlock($ID);
 
+  //delete draft
+  act_draftdel($act);
+
   //show it
   session_write_close();
   header("Location: ".wl($ID,'',true));
@@ -259,6 +315,7 @@ function act_auth($act){
  */
 function act_edit($act){
   global $ID;
+  global $INFO;
 
   //check if locked by anyone - if not lock for my self
   $lockedby = checklock($ID);
diff --git a/inc/common.php b/inc/common.php
index ffb78f432..a2facee90 100644
--- a/inc/common.php
+++ b/inc/common.php
@@ -36,14 +36,17 @@ function pageinfo(){
     $info['userinfo']   = $USERINFO;
     $info['perm']       = auth_quickaclcheck($ID);
     $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER']);
+    $info['client']     = $_SERVER['REMOTE_USER'];
 
     // if some outside auth were used only REMOTE_USER is set
     if(!$info['userinfo']['name']){
       $info['userinfo']['name'] = $_SERVER['REMOTE_USER'];
     }
+
   }else{
     $info['perm']       = auth_aclcheck($ID,'',null);
     $info['subscribed'] = false;
+    $info['client']     = clientIP(true);
   }
 
   $info['namespace'] = getNS($ID);
@@ -86,6 +89,17 @@ function pageinfo(){
     $info['editor'] = $revinfo['ip'];
   }
 
+  // draft
+  $draft = getCacheName($info['client'].$ID,'.draft');
+  if(@file_exists($draft)){
+    if(@filemtime($draft) < @filemtime(wikiFN($ID))){
+      // remove stale draft
+      @unlink($draft);
+    }else{
+      $info['draft'] = $draft;
+    }
+  }
+
   return $info;
 }
 
diff --git a/inc/html.php b/inc/html.php
index c0ce8527f..0cc52fbd1 100644
--- a/inc/html.php
+++ b/inc/html.php
@@ -98,7 +98,7 @@ function html_login(){
 }
 
 /**
- * shows the edit/source/show button dependent on current mode
+ * shows the edit/source/show/draft button dependent on current mode
  *
  * @author Andreas Gohr <andi@splitbrain.org>
  */
@@ -110,10 +110,14 @@ function html_editbutton(){
 
   if($ACT == 'show' || $ACT == 'search'){
     if($INFO['writable']){
-      if($INFO['exists']){
-        $r = html_btn('edit',$ID,'e',array('do' => 'edit','rev' => $REV),'post');
+      if($INFO['draft']){
+          $r = html_btn('draft',$ID,'e',array('do' => 'draft'),'post');
       }else{
-        $r = html_btn('create',$ID,'e',array('do' => 'edit','rev' => $REV),'post');
+        if($INFO['exists']){
+          $r = html_btn('edit',$ID,'e',array('do' => 'edit','rev' => $REV),'post');
+        }else{
+          $r = html_btn('create',$ID,'e',array('do' => 'edit','rev' => $REV),'post');
+        }
       }
     }else{
       $r = html_btn('source',$ID,'v',array('do' => 'edit','rev' => $REV),'post');
@@ -286,6 +290,36 @@ function html_show($txt=''){
   }
 }
 
+/**
+ * ask the user about how to handle an exisiting draft
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_draft(){
+  global $INFO;
+  global $ID;
+  global $lang;
+  global $conf;
+  $draft = unserialize(io_readFile($INFO['draft'],false));
+  $text  = cleanText(con($draft['prefix'],$draft['text'],$draft['suffix'],true));
+
+  echo p_locale_xhtml('draft');
+  ?>
+  <form id="dw__editform" method="post" action="<?php echo script()?>"
+   accept-charset="<?php echo $lang['encoding']?>"><div class="no">
+    <input type="hidden" name="id"   value="<?php echo $ID?>" />
+    <input type="hidden" name="date" value="<?php echo $draft['date']?>" /></div>
+    <textarea name="wikitext" id="wiki__text" readonly="readonly" cols="80" rows="10" class="edit"><?php echo "\n".formText($text)?></textarea>
+
+    <div id="draft__status"><?php echo $lang['draftdate'].' '.date($conf['dformat'],filemtime($INFO['draft']))?></div>
+
+    <input class="button" type="submit" name="do" value="<?php echo $lang['btn_recover']?>" tabindex="1" />
+    <input class="button" type="submit" name="do" value="<?php echo $lang['btn_draftdel']?>" tabindex="2" />
+    <input class="button" type="submit" name="do" value="<?php echo $lang['btn_cancel']?>" tabindex="3" />
+  </form>
+  <?php
+}
+
 /**
  * Highlights searchqueries in HTML code
  *
@@ -999,6 +1033,7 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed?
   <div style="width:99%;">
   <form id="dw__editform" method="post" action="<?php echo script()?>" accept-charset="<?php echo $lang['encoding']?>"><div class="no">
      <div class="toolbar">
+        <div id="draft__status"><?php if($INFO['draft']) echo $lang['draftdate'].' '.date($conf['dformat']);?></div>
         <div id="tool__bar"></div>
         <input type="hidden" name="id"   value="<?php echo $ID?>" />
         <input type="hidden" name="rev"  value="<?php echo $REV?>" />
@@ -1012,8 +1047,8 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed?
           textChanged = <?php ($pr) ? print 'true' : print 'false' ?>;
         </script>
         <span id="spell__action"></span>
-        <?php } ?>
         <div id="spell__suggest"></div>
+        <?php } ?>
     </div>
     <div id="spell__result"></div>
 
@@ -1025,7 +1060,7 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed?
          <div class="editButtons">
             <input class="button" id="edbtn__save" type="submit" name="do" value="<?php echo $lang['btn_save']?>" accesskey="s" title="[ALT+S]" tabindex="4" />
             <input class="button" id="edbtn__preview" type="submit" name="do" value="<?php echo $lang['btn_preview']?>" accesskey="p" title="[ALT+P]" tabindex="5" />
-            <input class="button" type="submit" name="do" value="<?php echo $lang['btn_cancel']?>" tabindex="5" />
+            <input class="button" type="submit" name="do[draftdel]" value="<?php echo $lang['btn_cancel']?>" tabindex="5" />
          </div>
       <?php } ?>
       <?php if($wr){ ?>
diff --git a/inc/io.php b/inc/io.php
index 1d458ace9..ae1a3611c 100644
--- a/inc/io.php
+++ b/inc/io.php
@@ -33,6 +33,9 @@ function io_sweepNS($id,$basedir='datadir'){
  *
  * Uses gzip if extension is .gz
  *
+ * If you want to use the returned value in unserialize
+ * be sure to set $clean to false!
+ *
  * @author  Andreas Gohr <andi@splitbrain.org>
  */
 function io_readFile($file,$clean=true){
diff --git a/inc/lang/en/lang.php b/inc/lang/en/lang.php
index 1a4308125..c1aa6bd6c 100644
--- a/inc/lang/en/lang.php
+++ b/inc/lang/en/lang.php
@@ -36,9 +36,12 @@ $lang['btn_backlink']    = "Backlinks";
 $lang['btn_backtomedia'] = 'Back to Mediafile Selection';
 $lang['btn_subscribe']   = 'Subscribe Changes';
 $lang['btn_unsubscribe'] = 'Unsubscribe Changes';
-$lang['btn_profile']    = 'Update Profile';
-$lang['btn_reset']     = 'Reset';
-$lang['btn_resendpwd'] = 'Send new password';
+$lang['btn_profile']     = 'Update Profile';
+$lang['btn_reset']       = 'Reset';
+$lang['btn_resendpwd']   = 'Send new password';
+$lang['btn_draft']    = 'Edit draft';
+$lang['btn_recover']  = 'Recover draft';
+$lang['btn_draftdel'] = 'Delete draft';
 
 $lang['loggedinas'] = 'Logged in as';
 $lang['user']       = 'Username';
@@ -53,6 +56,7 @@ $lang['register']   = 'Register';
 $lang['profile']    = 'User Profile';
 $lang['badlogin']   = 'Sorry, username or password was wrong.';
 $lang['minoredit']  = 'Minor Changes';
+$lang['draftdate']  = 'Draft autosaved on'; // full dformat date will be added
 
 $lang['regmissing'] = 'Sorry, you must fill in all fields.';
 $lang['reguexists'] = 'Sorry, a user with this login already exists.';
diff --git a/inc/template.php b/inc/template.php
index 091cd2e56..a74067f8b 100644
--- a/inc/template.php
+++ b/inc/template.php
@@ -53,8 +53,8 @@ function template($tpl){
  * (defined by the global $ACT var) by calling the appropriate
  * outputfunction(s) from html.php
  *
- * Everything that doesn't use the default template isn't
- * handled by this function. ACL stuff is not done either.
+ * Everything that doesn't use the main template file isn't
+ * handled by this function. ACL stuff is not done here either.
  *
  * @author Andreas Gohr <andi@splitbrain.org>
  */
@@ -74,9 +74,15 @@ function tpl_content(){
       html_edit($TEXT);
       html_show($TEXT);
       break;
+    case 'recover':
+      html_edit($TEXT);
+      break;
     case 'edit':
       html_edit();
       break;
+    case 'draft':
+      html_draft();
+      break;
     case 'wordblock':
       html_edit($TEXT,'wordblock');
       break;
@@ -203,7 +209,7 @@ function tpl_metaheaders($alt=true){
   ptln('<link rel="stylesheet" media="print" type="text/css" href="'.DOKU_BASE.'lib/exe/css.php?print=1" />',$it);
 
   // load javascript
-  $js_edit  = ($ACT=='edit' || $ACT=='preview') ? 1 : 0;
+  $js_edit  = ($ACT=='edit' || $ACT=='preview' || $ACT=='recover') ? 1 : 0;
   $js_write = ($INFO['writable']) ? 1 : 0;
   if($js_edit && $js_write){
     ptln('<script type="text/javascript" charset="utf-8">',$it);
@@ -283,7 +289,7 @@ function tpl_getparent($ID){
  *
  * Available Buttons are
  *
- *  edit        - edit/create/show button
+ *  edit        - edit/create/show/draft button
  *  history     - old revisions
  *  recent      - recent changes
  *  login       - login/logout button - if ACL enabled
diff --git a/lib/exe/ajax.php b/lib/exe/ajax.php
index e52d5d378..886e9829d 100644
--- a/lib/exe/ajax.php
+++ b/lib/exe/ajax.php
@@ -61,18 +61,52 @@ function ajax_qsearch(){
 }
 
 /**
- * Refresh a page lock
+ * Refresh a page lock and save draft
  *
  * Andreas Gohr <andi@splitbrain.org>
  */
 function ajax_lock(){
+  global $conf;
+  global $lang;
   $id = cleanID($_POST['id']);
   if(empty($id)) return;
 
   if(!checklock($id)){
     lock($id);
-    print 1;
+    echo 1;
   }
+
+  if($conf['usedraft'] && $_POST['wikitext']){
+    $client = $_SERVER['REMOTE_USER'];
+    if(!$client) $client = clientIP(true);
+
+    $draft = array('id'     => $ID,
+                   'prefix' => $_POST['prefix'],
+                   'text'   => $_POST['wikitext'],
+                   'suffix' => $_POST['suffix'],
+                   'date'   => $_POST['date'],
+                   'client' => $client,
+                  );
+    $cname = getCacheName($draft['client'].$id,'.draft');
+    if(io_saveFile($cname,serialize($draft))){
+      echo $lang['draftdate'].' '.date($conf['dformat']);
+    }
+  }
+
+}
+
+/**
+ * Delete a draft
+ */
+function ajax_draftdel(){
+  $id = cleanID($_POST['id']);
+  if(empty($id)) return;
+
+  $client = $_SERVER['REMOTE_USER'];
+  if(!$client) $client = clientIP(true);
+
+  $cname = getCacheName($client.$id,'.draft');
+  @unlink($cname);
 }
 
 //Setup VIM: ex: et ts=2 enc=utf-8 :
diff --git a/lib/exe/js.php b/lib/exe/js.php
index 5147f1be3..9cc4a863c 100644
--- a/lib/exe/js.php
+++ b/lib/exe/js.php
@@ -93,7 +93,7 @@ function js_out(){
             js_runonstart("initChangeCheck('".js_escape($lang['notsavedyet'])."')");
 
             // add lock timer
-            js_runonstart("locktimer.init(".($conf['locktime'] - 60).",'".js_escape($lang['willexpire'])."')");
+            js_runonstart("locktimer.init(".($conf['locktime'] - 60).",'".js_escape($lang['willexpire'])."',".$conf['usedraft'].")");
 
             // load spell checker
             if($conf['spellchecker']){
@@ -195,7 +195,7 @@ function js_escape($string){
  * @author Andreas Gohr <andi@splitbrain.org>
  */
 function js_runonstart($func){
-    echo "addInitEvent(function(){ $func; });";
+    echo "addInitEvent(function(){ $func; });\n";
 }
 
 /**
diff --git a/lib/scripts/edit.js b/lib/scripts/edit.js
index d39835526..48acc542a 100644
--- a/lib/scripts/edit.js
+++ b/lib/scripts/edit.js
@@ -328,7 +328,24 @@ var textChanged = false;
  */
 function changeCheck(msg){
   if(textChanged){
-    return confirm(msg);
+    var ok = confirm(msg);
+    if(ok){
+        // remove a possibly saved draft using ajax
+        var dwform = $('dw__editform');
+        if(dwform){
+            var params = 'call=draftdel';
+            params += '&id='+dwform.elements.id.value;
+            params += '&user='+encodeURI(USERNAME);
+
+            var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
+            sackobj.AjaxFailedAlert = '';
+            sackobj.encodeURIString = false;
+            sackobj.runAJAX(params);
+            // we send this request blind without waiting for
+            // and handling the returned data
+        }
+    }
+    return ok;
   }else{
     return true;
   }
@@ -408,10 +425,11 @@ function locktimer_class(){
         this.pageid   = '';
 };
 var locktimer = new locktimer_class();
-    locktimer.init = function(timeout,msg){
+    locktimer.init = function(timeout,msg,draft){
         // init values
         locktimer.timeout  = timeout*1000;
         locktimer.msg      = msg;
+        locktimer.draft    = draft;
         locktimer.lasttime = new Date();
 
         if(!$('dw__editform')) return;
@@ -465,8 +483,16 @@ var locktimer = new locktimer_class();
     locktimer.refresh = function(){
         var now = new Date();
         // refresh every minute only
-        if(now.getTime() - locktimer.lasttime.getTime() > 60*1000){
-            locktimer.sack.runAJAX('call=lock&id='+encodeURI(locktimer.pageid));
+        if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
+            var params = 'call=lock&id='+encodeURI(locktimer.pageid);
+            if(locktimer.draft){
+                var dwform = $('dw__editform');
+                params += '&prefix='+encodeURI(dwform.elements.prefix.value);
+                params += '&wikitext='+encodeURI(dwform.elements.wikitext.value);
+                params += '&suffix='+encodeURI(dwform.elements.suffix.value);
+                params += '&date='+encodeURI(dwform.elements.date.value);
+            }
+            locktimer.sack.runAJAX(params);
             locktimer.lasttime = now;
         }
     };
@@ -476,7 +502,12 @@ var locktimer = new locktimer_class();
      * Callback. Resets the warning timer
      */
     locktimer.refreshed = function(){
-        if(this.response != '1') return; // locking failed
+        var data  = this.response;
+        var error = data.charAt(0);
+            data  = data.substring(1);
+
+        $('draft__status').innerHTML=data;
+        if(error != '1') return; // locking failed
         locktimer.reset();
     };
 // end of locktimer class functions
diff --git a/lib/scripts/script.js b/lib/scripts/script.js
index e05aeb0fe..87fd8e503 100644
--- a/lib/scripts/script.js
+++ b/lib/scripts/script.js
@@ -40,6 +40,16 @@ function $() {
   return elements;
 }
 
+/**
+ * Simple function to check if a global var is defined
+ *
+ * @author Kae Verens
+ * @link http://verens.com/archives/2005/07/25/isset-for-javascript/#comment-2835
+ */
+function isset(varname){
+  return(typeof(window[varname])!='undefined');
+}
+
 /**
  * Get the X offset of the top left corner of the given object
  *
diff --git a/lib/tpl/default/design.css b/lib/tpl/default/design.css
index 0f3502c98..70b429bc3 100644
--- a/lib/tpl/default/design.css
+++ b/lib/tpl/default/design.css
@@ -115,6 +115,16 @@ div.dokuwiki input.missing {
   display: inline;
 }
 
+/* disabled style - not understood by IE */
+div.dokuwiki textarea.edit[disabled],
+div.dokuwiki textarea.edit[readonly],
+div.dokuwiki input.edit[disabled],
+div.dokuwiki input.edit[readonly],
+div.dokuwiki select.edit[disabled] {
+  background-color: #f5f5f5!important;
+  color: #666!important;
+}
+
 /* edit form */
 div.dokuwiki div.toolbar, div.dokuwiki div#wiki__editbar {
    margin:2px 0;
@@ -138,6 +148,10 @@ div.dokuwiki div#wiki__editbar div.summary {
 div.dokuwiki .nowrap {
    white-space:nowrap;
 }
+div.dokuwiki div#draft__status {
+    float: right;
+    color: __dark__;
+}
 
 /* --------- buttons ------------------- */
 
-- 
GitLab