Edit Handler


See also:
Documentation: EditHandlerInfo
This is the development page for the edit handler.
 

Since there are a number of issues with the edit handler in Wikka version 1.1.6.0 (one actually introduced with that version) I'm creating this development page to tackle them. --JavaWoman

Current Edit Handler


For reference, the code of the current (version 1.1.6.0) edit handler is as follows:
  1. <div class="page">
  2. <?php
  3. if (!(preg_match("/^[A-Za-zÄÖÜßäöü]+[A-Za-z0-9ÄÖÜßäöü]*$/s", $this->tag))) {
  4.     echo '<em>The page name is invalid. Valid page names must start with a letter and contain only letters and numbers.</em>';
  5. }
  6. elseif ($this->HasAccess("write") && $this->HasAccess("read"))
  7. {
  8.     if ($newtag = $_POST['newtag']) $this->Redirect($this->Href('edit', $newtag));
  9.     if ($_POST)
  10.     {
  11.         // strip CRLF line endings down to LF to achieve consistency ... plus it saves database space.
  12.         // Note: these codes must remain enclosed in double-quotes to work! -- JsnX
  13.         $body = str_replace("\r\n", "\n", $_POST['body']);
  14.  
  15.         $body = preg_replace("/\n[ ]{4}/", "\n\t", $body);                      # @@@ FIXME: misses first line and multiple sets of four spaces - JW 2005-01-16
  16.  
  17.         // we don't need to escape here, we do that just before display (i.e., treat note just like body!)
  18.         $note = trim($_POST['note']);
  19.  
  20.         // only if saving:
  21.         if ($_POST['submit'] == 'Store')
  22.         {
  23.             // check for overwriting
  24.             if ($this->page)
  25.             {
  26.                 if ($this->page['id'] != $_POST['previous'])
  27.                 {
  28.                     $error = 'OVERWRITE ALERT: This page was modified by someone else while you were editing it.<br />'."\n".'Please copy your changes and re-edit this page.';
  29.                 }
  30.             }
  31.             // store
  32.             if (!$error)
  33.             {
  34.                 // only save if new body differs from old body
  35.                 if ($body != $this->page['body']) {
  36.  
  37.                     // add page (revisions)
  38.                     $this->SavePage($this->tag, $body, $note);
  39.  
  40.                     // now we render it internally so we can write the updated link table.
  41.                     $this->ClearLinkTable();
  42.                     $this->StartLinkTracking();
  43.                     $dummy = $this->Header();
  44.                     $dummy .= $this->Format($body);
  45.                     $dummy .= $this->Footer();
  46.                     $this->StopLinkTracking();
  47.                     $this->WriteLinkTable();
  48.                     $this->ClearLinkTable();
  49.                 }
  50.  
  51.                 // forward
  52.                 $this->Redirect($this->Href());
  53.             }
  54.         }
  55.     }
  56.  
  57.     // fetch fields
  58.     if (!$previous = $_POST['previous']) $previous = $this->page['id'];
  59.     if (!$body) $body = $this->page['body'];
  60.     $body = preg_replace("/\n[ ]{4}/", "\n\t", $body);                      # @@@ FIXME: misses first line and multiple sets of four spaces - JW 2005-01-16
  61.  
  62.  
  63.     if ($result = mysql_query("describe ".$this->config['table_prefix']."pages tag")) {
  64.         $field = mysql_fetch_assoc($result);
  65.         if (preg_match("/varchar\((\d+)\)/", $field['Type'], $matches)) $maxtaglen = $matches[1];
  66.     }
  67.     else
  68.     {
  69.         $maxtaglen = 75;
  70.     }
  71.  
  72.     // preview?
  73.     if ($_POST['submit'] == 'Preview')                                      # preview page
  74.     {
  75.         $previewButtons =
  76.             "<hr />\n".
  77.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  78.             // so we use htmlspecialchars on the edit note (as on the body)
  79.             '<input size="50" type="text" name="note" value="'.htmlspecialchars($note).'"/> Note on your edit.<br />'."\n".
  80.             '<input name="submit" type="submit" value="Store" accesskey="s" />'."\n".
  81.             '<input name="submit" type="submit" value="Re-Edit" accesskey="p" />'."\n".
  82.             '<input type="button" value="Cancel" onclick="document.location=\''.$this->href('').'\';" />'."\n";
  83.  
  84.         $output .= '<div class="previewhead">Preview</div>'."\n";
  85.  
  86.         $output .= $this->Format($body);
  87.  
  88.         $output .=
  89.             $this->FormOpen('edit')."\n".
  90.             '<input type="hidden" name="previous" value="'.$previous.'" />'."\n".
  91.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  92.             // hence htmlspecialchars() instead of htmlspecialchars_ent() which UNescapes entities!
  93.             '<input type="hidden" name="body" value="'.htmlspecialchars($body).'" />'."\n";
  94.  
  95.  
  96.         $output .=
  97.             "<br />\n".
  98.             $previewButtons.
  99.             $this->FormClose()."\n";
  100.     }
  101.     elseif (!$this->page && strlen($this->tag) > $maxtaglen)                # rename page
  102.     {
  103.         $this->tag = substr($this->tag, 0, $maxtaglen); // truncate tag to feed a backlinks-handler with the correct value. may be omited. it only works if the link to a backlinks-handler is built in the footer.
  104.         $output  = '<div class="error">Tag too long! $maxtaglen characters max.</div><br />'."\n";
  105.         $output .= 'FYI: Clicking on Rename will automatically truncate the tag to the correct size.<br /><br />'."\n";
  106.         $output .= $this->FormOpen('edit');
  107.         $output .= '<input name="newtag" size="75" value="'.$this->htmlspecialchars_ent($this->tag).'" />';
  108.         $output .= '<input name="submit" type="submit" value="Rename" />'."\n";
  109.         $output .= $this->FormClose();
  110.     }
  111.     else                                                                    # edit page
  112.     {
  113.         // display form
  114.         if ($error)
  115.         {
  116.             $output .= '<div class="error">'.$error.'</div>'."\n";
  117.         }
  118.  
  119.         // append a comment?
  120.         if ($_REQUEST['appendcomment'])
  121.         {
  122.             $body = trim($body)."\n\n----\n\n--".$this->GetUserName().' ('.strftime("%c").')';
  123.         }
  124.  
  125.         $output .=
  126.             $this->FormOpen('edit').
  127.             '<input type="hidden" name="previous" value="'.$previous.'" />'."\n".
  128.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  129.             // hence htmlspecialchars() instead of htmlspecialchars_ent() which UNescapes entities!
  130.             '<textarea id="body" name="body" style="width: 100%; height: 500px">'.htmlspecialchars($body).'</textarea><br />'."\n".
  131.             //note add Edit
  132.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  133.             // so we use htmlspecialchars on the edit note (as on the body)
  134.             '<input size="40" type="text" name="note" value="'.htmlspecialchars($note).'" /> Please add a note on your edit.<br />'."\n".
  135.             //finish
  136.             '<input name="submit" type="submit" value="Store" accesskey="s" /> <input name="submit" type="submit" value="Preview" accesskey="p" /> <input type="button" value="Cancel" onclick="document.location=\''.$this->Href('').'\';" />'."\n".
  137.             $this->FormClose();
  138.  
  139.         if ($this->GetConfigValue('gui_editor') == 1) {
  140.             $output .=
  141.                     '<script language="JavaScript" src="3rdparty/plugins/wikiedit/protoedit.js"></script>'."\n".
  142.                     '<script language="JavaScript" src="3rdparty/plugins/wikiedit/wikiedit2.js"></script>'."\n";
  143.             $output .= '<script type="text/javascript">'."  wE = new WikiEdit(); wE.init('body','WikiEdit','editornamecss');".'</script>'."\n";
  144.         }
  145.     }
  146.  
  147.     echo $output;
  148. }
  149. else
  150. {
  151.     $message'<em>You don\'t have write access to this page. You might need to register an account to get write access.</em><br />'."\n".
  152.             "<br />\n".
  153.             '<a href="'.$this->Href('showcode').'" title="Click to view page formatting code">View formatting code for this page</a>'.
  154.             "<br />\n";
  155.     echo $message;
  156. }
  157. ?>
  158. </div>


As TimoK stated on his user page, "I found the code quite hard to read". Clearly, it's not very well-structured, which makes it also hard to tackle the various problems with it.

Current issues


In no particular order, but numbered to make it easier to refer to them:
  1. Bad structure makes the code hard to understand and fix (see TimoK)
  2. The function of some of the code is unclear - possibly it is never used and could (should) be eliminated
  3. Various statements lead to notices because of reference to undefined variables
  4. Indents consisting of (4) spaces are translated into tabs in only a limited number of cases (only the first group of 4 spaces at the start of a line, and not on the first line of the page (see Indenting not working properly in handlers/page/edit.php on WikkaBugs)
  5. The code generated is not actually valid XHTML; in particular the <script> elements are not valid.
  6. Unicode characters that were entered as Unicode characters (rather than as character or numeric entity references) are rendered as numeric entity references in the textarea when editing the page, making the text very hard to edit since it becomes essentially unreadable (this bug was actually introduced in version 1.1.6.0; see 1.1.6.0beta4: Simplified Chinese (or Unicode relative) in WikiEdit: on WikkaBugs)
  7. There is also an issue with Unicode characters in code blocks (such as in comments!) being converted to numeric entity references but it's unclear for now whether this is caused by the edit handler or by the GeSHi code highlighting. (An example of this is (still) found in the SandBox - see also Missing language-support in the code formatter on WikkaBugs)
  8. For some sites it's desirable to have the edit note be a required field; this is currently not supported (see Note on edit, mandatory field on SuggestionBox)
  9. While registered users do have an option to set whether or not to use double-click editing, they cannot set a preference for using the WikiEdit toolbar. Since some people don't actually use the toolbar while this slows down editing, this really should be an option.
  10. There is no way to indicate whether a page edit is "minor" (for instance, correcting a typo or a small change to layout without effectively changing the page content); minor changes should not be displayed in RecentChanges, and not broadcast in the RecentChanges RSS feed or WikiPing. Especially RSS feeds and WikiPing will be more valuable if they are not cluttered up with with irrelevant changes.

Tackling the issues


Issue 3: Notices

On Alan (my development system) I'm running with error_reporting(E_ALL); so all errors, warnings and notices get reported - instead of the (Wikka) default of error_reporting (E_ALL ^ E_NOTICE); which suppresses notices. This is to catch any notices (mostly caused by using uninitialized variables which can lead to hard-to-track bugs) and squash them where I find them.

Since the edit handler invariably caused a number of notices, this is the first thing I tackled. The fix for such notices is always easy: make sure a variable is actually assigned an (initial) value before trying to evaluate it. (Ideally Wikka should be completely notice-free but we can get there by squashing them where we find them.)

Issue 4: Indenting not working properly

The code that attempts to change (leading) groups of four space into tabs actually occurs twice in the code (see issue 1!) on lines 15 and 60:
    $body = preg_replace("/\n[ ]{4}/", "\n\t", $body);                      # @@@ FIXME: misses first line and multiple sets of four spaces - JW 2005-01-16


As indicated on WikkaBugs, I came up with the following solution:
            # JW FIXED 2005-07-12
            $pattern = '/^(\t*) {4}/m';                 # m modifier: match ^ at start of line *and* at start of string;
            $replace = "$1\t";
            while (preg_match($pattern,$body))
            {
                $body = preg_replace($pattern,$replace,$body);
            }
            # @@@ NOTE: This could be easily extended to also change a '~' into a tab. But should we? '~' is easy to type and edit


Issue 5: Invalid XHTML generated


Simply solved by ensuring that all <script> elements have the required type="text/javascript" attribute.

More later...


The code with the solutions for issues 3, 4 and 5 is now implemented as beta on this site. Other issues to be tackled later.

This beta code also contains an experimental anti-spam feature: link throttling (to be documented later). For obvious reasons the exact number used in the link throttling is not revealed here; besides, it should become a configuration option rather than be hard-coded as it is for now.

The current beta code now looks as follows (replace XXX with whatever number you wish for link throttling):
  1. <div class="page">
  2. <?php
  3. // various minor changes to prevent NOTICES (due to uninitialized variables) - JW 2005-06-07
  4. // added type="text/javascript" to script elements (valid XHTML) - JW 2005-07-01
  5. if (!(preg_match("/^[A-Za-zÄÖÜßäöü]+[A-Za-z0-9ÄÖÜßäöü]*$/s", $this->tag))) {
  6.     echo '<em>The page name is invalid. Valid page names must start with a letter and contain only letters and numbers.</em>';
  7. }
  8. elseif ($this->HasAccess("write") && $this->HasAccess("read"))
  9. {
  10.     #if ($newtag = $_POST['newtag']) $this->Redirect($this->Href('edit', $newtag));
  11.     if (isset($_POST['newtag']))
  12.     {
  13.         $newtag = $_POST['newtag'];
  14.         $this->Redirect($this->Href('edit', $newtag));
  15.     }
  16.     // init fields
  17.     $body = '';
  18.     $note = '';
  19.     $submit = '';
  20.     $output = '';
  21.  
  22.     if ($_POST)
  23.     {
  24.         if (isset($_POST['body']))
  25.         {
  26.             // strip CRLF line endings down to LF to achieve consistency ... plus it saves database space.
  27.             // Note: these codes must remain enclosed in double-quotes to work! -- JsnX
  28.             $body = str_replace("\r\n", "\n", $_POST['body']);
  29.             // replace each 4 consecutive spaces at the start of a line with a tab
  30.             #$body = preg_replace("/\n[ ]{4}/", "\n\t", $body);                     # @@@ FIXME: misses first line and multiple sets of four spaces - JW 2005-01-16
  31.             # JW FIXED 2005-07-12
  32.             $pattern = '/^(\t*) {4}/m';                 # m modifier: match ^ at start of line *and* at start of string;
  33.             $replace = "$1\t";
  34.             while (preg_match($pattern,$body))
  35.             {
  36.                 $body = preg_replace($pattern,$replace,$body);
  37.             }
  38.             # @@@ NOTE: This could be easily extended to also change a '~' into a tab. But should we? '~' is easy to type and edit
  39.         }
  40.  
  41.         // we don't need to escape here, we do that just before display (i.e., treat note just like body!)
  42.         if (isset($_POST['note']))
  43.         {
  44.             $note = trim($_POST['note']);
  45.         }
  46.  
  47.         if (isset($_POST['submit']))
  48.         {
  49.             $submit = $_POST['submit'];
  50.         }
  51.  
  52.         // only if saving:
  53.         if ($submit == 'Store')
  54.         {
  55.             // check for overwriting
  56.             if ($this->page)
  57.             {
  58.                 if ($this->page['id'] != $_POST['previous'])
  59.                 {
  60.                     $error = 'OVERWRITE ALERT: This page was modified by someone else while you were editing it.<br />'."\n".'Please copy your changes and re-edit this page.';
  61.                 }
  62.                 // check for spam - antispam measure added by JsnX 2005-03-21; modified 2005-03-25 (BETA)
  63.                 $existing_link_count = substr_count($this->page['body'], "http://");
  64.                 $new_link_count = substr_count($body, "http://");
  65.                 if ($new_link_count >= $existing_link_count + XXX) # @@@ replace XXX by your own number!
  66.                 {
  67.                     $error = 'SPAM ALERT: It appears that you are trying to spam.<br />'."\n".$this->Format('If you are not spamming, please report the situation on Wikka:WikkaBugs.');
  68.                 }
  69.                 // end anti-spam BETA
  70.             }
  71.             // store
  72.             if (!isset($error))
  73.             {
  74.                 // only save if new body differs from old body
  75.                 if ($body != $this->page['body']) {
  76.  
  77.                     // add page (revisions)
  78.                     $this->SavePage($this->tag, $body, $note);
  79.  
  80.                     // now we render it internally so we can write the updated link table.
  81.                     $this->ClearLinkTable();
  82.                     $this->StartLinkTracking();
  83.                     $dummy = $this->Header();
  84.                     $dummy .= $this->Format($body);
  85.                     $dummy .= $this->Footer();
  86.                     $this->StopLinkTracking();
  87.                     $this->WriteLinkTable();
  88.                     $this->ClearLinkTable();
  89.                 }
  90.  
  91.                 // forward
  92.                 $this->Redirect($this->Href());
  93.             }
  94.         }
  95.     }
  96.  
  97.     // fetch fields
  98.     #if (!$previous = $_POST['previous']) $previous = $this->page['id'];
  99.     $previous = (isset($_POST['previous'])) ? $_POST['previous'] : $this->page['id'];
  100.     if (!$body) $body = $this->page['body'];                                # @@@ ??
  101.     // replace each 4 consecutive spaces at the start of a line with a tab
  102.     #$body = preg_replace("/\n[ ]{4}/", "\n\t", $body);                     # @@@ FIXME: misses first line and multiple sets of four spaces - JW 2005-01-16
  103.     # JW FIXED 2005-07-12
  104.     $pattern = '/^(\t*) {4}/m';                 # m modifier: match ^ at start of line *and* at start of string;
  105.     $replace = "$1\t";
  106.     while (preg_match($pattern,$body))
  107.     {
  108.         $body = preg_replace($pattern,$replace,$body);
  109.     }
  110.  
  111.  
  112.     // derive maximum length for a page name from the table structure
  113.     if ($result = mysql_query("describe ".$this->config['table_prefix']."pages tag"))
  114.     {
  115.         $field = mysql_fetch_assoc($result);
  116.         if (preg_match("/varchar\((\d+)\)/", $field['Type'], $matches)) $maxtaglen = $matches[1];
  117.     }
  118.     else
  119.     {
  120.         $maxtaglen = 75;
  121.     }
  122.  
  123.     // preview?
  124.     if ($submit == 'Preview')                                               # preview page
  125.     {
  126.         $previewButtons =
  127.             "<hr />\n".
  128.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  129.             // so we use htmlspecialchars on the edit note (as on the body)
  130.             '<input size="50" type="text" name="note" value="'.htmlspecialchars($note).'"/> Note on your edit.<br />'."\n".
  131.             '<input name="submit" type="submit" value="Store" accesskey="s" />'."\n".
  132.             '<input name="submit" type="submit" value="Re-Edit" accesskey="p" />'."\n".
  133.             '<input type="button" value="Cancel" onclick="document.location=\''.$this->href('').'\';" />'."\n";
  134.  
  135.         $output .= '<div class="previewhead">Preview</div>'."\n";
  136.  
  137.         $output .= $this->Format($body);
  138.  
  139.         $output .=
  140.             $this->FormOpen('edit')."\n".
  141.             '<input type="hidden" name="previous" value="'.$previous.'" />'."\n".
  142.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  143.             // hence htmlspecialchars() instead of htmlspecialchars_ent() which UNescapes entities!
  144.             '<input type="hidden" name="body" value="'.htmlspecialchars($body).'" />'."\n";
  145.  
  146.  
  147.         $output .=
  148.             "<br />\n".
  149.             $previewButtons.
  150.             $this->FormClose()."\n";
  151.     }
  152.     elseif (!$this->page && strlen($this->tag) > $maxtaglen)                # rename page
  153.     {
  154.         // truncate tag to feed a backlinks-handler with the correct value. may be omited. it only works if the link to a backlinks-handler is built in the footer.
  155.         // @@@ backlinks handler ???
  156.         $this->tag = substr($this->tag, 0, $maxtaglen);
  157.         $output  = '<div class="error">Tag too long! '.$maxtaglen.' characters max.</div><br />'."\n";
  158.         $output .= 'FYI: Clicking on Rename will automatically truncate the tag to the correct size.<br /><br />'."\n";
  159.         $output .= $this->FormOpen('edit');
  160.         $output .= '<input name="newtag" size="75" value="'.$this->htmlspecialchars_ent($this->tag).'" />';
  161.         $output .= '<input name="submit" type="submit" value="Rename" />'."\n";
  162.         $output .= $this->FormClose();
  163.     }
  164.     else                                                                    # edit page
  165.     {
  166.         // display form
  167.         if (isset($error))
  168.         {
  169.             $output .= '<div class="error">'.$error.'</div>'."\n";
  170.         }
  171.  
  172.         // append a comment?
  173.         if ($_REQUEST['appendcomment'])
  174.         {
  175.             $body = trim($body)."\n\n----\n\n--".$this->GetUserName().' ('.strftime("%c").')';
  176.         }
  177.  
  178.         $output .=
  179.             $this->FormOpen('edit').
  180.             '<input type="hidden" name="previous" value="'.$previous.'" />'."\n".
  181.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  182.             // hence htmlspecialchars() instead of htmlspecialchars_ent() which UNescapes entities!
  183.             '<textarea id="body" name="body" style="width: 100%; height: 500px">'.htmlspecialchars($body).'</textarea><br />'."\n".
  184.             //note add Edit
  185.             // We need to escape ALL entity refs before display so we display them _as_ entities instead of interpreting them
  186.             // so we use htmlspecialchars on the edit note (as on the body)
  187.             '<input size="40" type="text" name="note" value="'.htmlspecialchars($note).'" /> Please add a note on your edit.<br />'."\n".
  188.             //finish
  189.             '<input name="submit" type="submit" value="Store" accesskey="s" /> <input name="submit" type="submit" value="Preview" accesskey="p" /> <input type="button" value="Cancel" onclick="document.location=\''.$this->Href('').'\';" />'."\n".
  190.             $this->FormClose();
  191.  
  192.         if ($this->GetConfigValue('gui_editor') == 1) {
  193.             $output .=
  194.                 '<script language="JavaScript" type="text/javascript" src="3rdparty/plugins/wikiedit/protoedit.js"></script>'."\n".
  195.                 '<script language="JavaScript" type="text/javascript" src="3rdparty/plugins/wikiedit/wikiedit2.js"></script>'."\n".
  196.                 '<script language="JavaScript" type="text/javascript">'."  wE = new WikiEdit(); wE.init('body','WikiEdit','editornamecss');".'</script>'."\n";
  197.         }
  198.     }
  199.  
  200.     echo $output;
  201. }
  202. else
  203. {
  204.     $message'<em>You don\'t have write access to this page. You might need to register an account to get write access.</em><br />'."\n".
  205.             "<br />\n".
  206.             '<a href="'.$this->Href('showcode').'" title="Click to view page formatting code">View formatting code for this page</a>'.
  207.             "<br />\n";
  208.     echo $message;
  209. }
  210. ?>
  211. </div>


CategoryDevelopmentHandlers
There is one comment on this page. [Display comment]
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki