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