Table of Contents

Building PHP Forms within Dokuwiki

(Sept 4, 2006) This website no longer uses this code. While this was useful in the original implementation, it created a large number of problems trying to upgrade later as new places to apply all the changes were required. Now we use the Include form plugin.

As distributed, Dokuwiki is very wide open. We've used ACL settings to tighten it considerably, which helps. The problem is that our previous site design used a lot of server side code for things like Word lists, base finders, publication indexes, and some other interactive tools.

Allowing regular users to build PHP code into a website is simply way too risky. Nor is it fair to simply try and recast all the tools back into a CGI format such as Perl since it appears that running a CGI application behind a DokuWiki page would be as difficult, if not more. Unless we were willing to accept leaving the Dokiwiki layouts when we retrieve results.

The goals are:

This modification seems to allow me that capability with low impact on the DokuWiki performance.


In my inc/template.php file, I added two functions. First, a function modelled after tpl_page which imports the form content after replacing all the <?php ... ?> blocks with the eval() result of the block. I merged two models for this:

function tpl_form() {
  global $ACT;
  global $REV;
  global $ID;
  //global $TEXT;
  //global $PRE;
  //global $SUF;
  //global $SUM;
  //global $IDX;

  //echo "<!-- ACT: $ACT -->\n";
    case 'show':
	    //echo "<!-- ID: $ID -->\n";
		$path =  formFN($ID,$REV);  
		if(file_exists(DOKU_INC . $path)) {
		    //echo "<!-- form was found -->\n";
			$text = io_readFile($path); 
			$pattern = "/(<\?php)(.*?)(\?>)/is";
			while(preg_match($pattern, $text)) {
				$text = preg_replace_callback(
                                          $pattern,    // Matches php block
                                          "eval_php",  // function in this file
                                          $text);      // the php form file
			echo $text;
		} //else {
		    //echo "<!-- form was not found -->\n";
    case 'preview':
    case 'edit':
    case 'wordblock':
    case 'search':
    case 'revisions':
    case 'diff':
    case 'recent':
    case 'index':
    case 'backlink':
    case 'conflict':
    case 'locked':
    case 'login':
    case 'register':
    case 'denied':
    case 'admin':
    case 'profile';     // Profile Manager modification
       break;           // Unless we're showing the page, we do nothing.
       msg("Failed to handle command: ".hsc($ACT),-1); 

Secondly, I added a small function to wrap the calls to eval() for preg_replace_callback(). This is where I incorporated the important code found in handling <php> tags when you have $conf[“phpok”] = 1 in your configuration. Using preg_replace_callback() eliminates the while() loop in the original interpreter – yes, my code still has the while( ;-) ) but I'm not done yet.

// Callback function 
function eval_php($arr) {
	// echo "<!-- $arr[2] !! -->\n";
	$content = ob_get_contents();
	// echo "<!-- Content: $content -->\n";
	return $content;


In my lib/tpl/{template}/main.php, I added a new <div class="form"> block.

  <?php html_sidebar()        // SIDEBAR modificaction ?>  

  <div class="form">
    <!-- wikiform start -->
    <?php tpl_form() ?>
    <!-- wikiform stop -->

  <div class="page">         
    <!-- wikipage start -->
    <?php tpl_content() ?>
    <!-- wikipage stop -->

The template name isn't that important. We're using template derived from the Sidebar code which is derived from the default. This allows us to use our own favorites icon, the League's name, and our Sphinx mascot on the header as well as implementing our form mod.

Like all things, the reason for classing the <div> is so that your cascading stylsheet can format it better. I don't actually have any explicit formats using the <div> class, but I'm not done yet.


I added a value conf["formdir"] where I store my forms path, and then implemented a function modelled after wikiFN() in the same file.

function formFN($id,$rev=''){
  global $conf;
     $id = cleanID($id);
     $id = str_replace(':','/',$id);
     $fn = $conf['formdir'].'/'.utf8_encodeFN($id).'.php';
  return $fn;

Changed some code in resolve_pageid which, essentially, checks both the pages and forms directory to determine if a file exists and returns TRUE if either exists. Maintains the “check singular and plurals” logic.

  // Does the file exist as a page or a form?
  // checks for forms are new -- edh
  if(!@file_exists($file) && !@file_exists(formFN($page))) {
    //check alternative plural/nonplural form
    if( $conf['autoplural'] ){
      if(substr($page,-1) == 's'){
        $try = substr($page,0,-1);
        $try = $page.'s';
      if(@file_exists(wikiFN($try)) && !@file_exists(formFN($try))){
        $page   = $try;
        $exists = true;
    $exists = true;


A new function, scan_dir(), gets a lot of the body from the function search():

function scan_dir(&$dirs, &$files, $base, $dir) {
  //read in directories and files
  $dh = @opendir($base.'/'.$dir);
  if(!$dh) return;
  while(($file = readdir($dh)) !== false){
    if(preg_match('/^[\._]/',$file)) continue; //skip hidden files and upper dirs
      $dirs[] = $dir.'/'.$file;
    }elseif(substr($file,-5) == '.lock'){
      //skip lockfiles
    $files[] = $dir.'/'.$file;

And the search function has numerous changes to allow parallel searches:

 * recurse direcory
 * This function recurses into a given base directory
 * and calls the supplied function for each file and directory
 * @author  Andreas Gohr <>
function search(&$data,$base,$func,$opts,$dir='',$lvl=1){
  global $conf;                            // new forms edh
  echo "<!-- \$base= $base -->\n";
  $page_dirs   = array();
  $page_files  = array();

  scan_dir(&$page_dirs, &$page_files, $base, $dir);

  // ========================================================== new forms edh

  $form_dirs   = array();
  $form_files  = array();

  $mask = "%^". $conf['datadir'] ."(.*)$%";
  //echo "<!-- \$mask= $mask -->\n";
  if(preg_match($mask, $base)) {
      $form_base = preg_replace($mask, $conf['formdir'] .'\1', $base);
	  //echo "\t<!-- \$form_base= $form_base -->\n";
      scan_dir(&$form_dirs, &$form_files, $form_base, $dir);

  // ========================================================== new forms edh

  //give directories to userfunction then recurse
  foreach($page_dirs as $dir){
    if ($func($data,$base,$dir,'d',$lvl,$opts)){
  // ========================================================== new forms edh
  //give directories to userfunction then recurse
  foreach($form_dirs as $dir){
    if ($func($data,$base,$dir,'d',$lvl,$opts)){
  //now handle the files
  foreach($page_files as $file){
  // ========================================================== new forms edh
  foreach($form_files as $file){

search_index() has several changes; first to make the PHP files in ./forms equivalent to TXT files in ./pages and to avoid duplicates in the result array.

  if($type == 'd' && !preg_match('#^'.$file.'(/|$)#','/'.$opts['ns'])){
    //add but don't recurse
    $return = false;
  }elseif($type == 'f' && !preg_match('#\.txt$#',$file) 
                       && !preg_match('#\.php$#',$file)){  // forms edh
    //don't add
    return false;


  // forms edh
  // try to avoid duplicate entries for forms and pages
  foreach($data as $item) {
      if( ($item['id'] == $id) 
	   && ($item['type'] == $type) 
	   && ($item['level'] == $lvl)) {
	     // This item already has a member in the array
  	     return $return;
  $data[]=array( 'id'    => $id,
                 'type'  => $type,
                 'level' => $lvl,
                 'open'  => $return );
  return $return;

in search_pagename(), again make .php as valid as .txt:

     || preg_match('#\.php$#',$file) ) return true;   // forms EDH

In search_list, again make .php as valid as .txt:

function search_list(&$data,$base,$file,$type,$lvl,$opts){
  //we do nothing with directories
  if($type == 'd') return false;
  || preg_match('#\.php$#',$file)){    // forms EDH
    //check ACL
    $id = pathID($file);
    if(auth_quickaclcheck($id) < AUTH_READ){
      return false;
    $data[]['id'] = $id;;
  return false;

in pathID(), remove the txt and php extensions from files:

  if(!$keeptxt) $id = preg_replace(
       array('#\.txt$#', '#\.php$#'),         // forms EDH
       array(''        , ''        ),   $id); // forms EDH

File System

Finally, I created a forms directory tree where each directory is a namespace, just as in the other DokuWiki data folders. By placing a file into these folders with the same name + .php, my forms code interprets it and injects the result imediately above the formatted text file result


One of the problems I've had is integrating the form pages with Wiki pages in indexing; the sidebar and indexing pages give me a list of files, but not of forms. I've played with a few ideas around the searching function but I get duplicate namespaces because, for example, Dictionary Grep/Search is a page namespace and a form namespace. The same problem is even worse for Base Finding Tools since none of the forms tend to have a page … makes them hard to locate.

My changes to inc/search.php have improved the problem above immensely. I don't like how a page which is also a namespace is listed as a different item in the list… hard to make them out without explicitly looking.

I'd love to find a way to make some pages “invisible” or “default” like index.* pages are; if one is in a folder and you access that folder, you get that page, not an index of the folder. This also resolves my “page is a namespace” gripe, I think. If I could make a defaut page in a namespace (ie. _default.txt), then I could create :my:name:space:_default … and when I have page parameters like ?id=&idx=my:name:space, I'd see my _default.txt file.