PageAdminPrune Action


This is the development page for the PageAdminPrune action.


This action allows you (the owner of a page or the sysadmin) to choose to keep a number of a page's revisions or page revisions from a certain date onwards.

I have tested this with Firefox 1.5, PHP 5.1, IIS6 and Wikka 1.1.6

PLEASE USE CAREFULLY!

I believe I have guarded against SQL injection attacks, however I'd appreciate any comments on the security of this action!

It consists of
- pageadmin.php: Modified so it contains hooks to the new action
- Date.class.php: Class that contains date functions.
- Prune.php: Contains the actual code for pruning. This class currently only manages page revisions. A variation of this could also do page comment pruning.


Save this in actions\pageadmin.php
Please note that I did not write this class. Credit goes to DarTar. (See PageAdminAction)
I merely added hooks for the Prune aspect.

I have marked the lines I changed with (with no spaces between the >)

<?php

/**
* Display a module for page management.
*
* This action allows admins to display information and perform operations
* on wiki pages. Pages can be sorted, searched, paged, filtered. Page-related
* statistics are given, displaying the number of comments, revisions, backlinks
* and referrers. Several handlers allow admins to perform specific operation on
* single pages. If the current user is not an administrator, the pageindex action
* is displayed instead.
*
* @package      Actions
* @name     PageAdmin
*
* @author       {@link http://wikka.jsnx.com/DarTar Dario Taraborelli}
* @author       {@link http://wikka.jsnx.com/JavaWoman JavaWoman} (using getCount(); minor tweaks)
* @version      0.4
* @since        Wikka 1.1.X.X
*
* @input        integer $colcolor  optional: enables color for statistics columns
*               1: enables colored columns;
*               0: disables colored columns;
*               default: 1;
* @input        integer $rowcolor  optional: enables alternate row colors
*               1: enables colored rows;
*               0: disables colored rows;
*               default: 1;
*
* @output       A list of pages available on the current server.
*
* @todo
*           - mass-operations;
*           - handlers: rename handler;
*           - statistics: page hits;
*           - full-text page search;
*           - integrate with other admin modules.
*/


//utilities

/**
* Build an array of numbers consisting of 'ranges' with increasing step size in each 'range'.
*
* A list of numbers like this is useful for instance for a dropdown to choose
* a period expressed in number of days: a difference between 2 and 5 days may
* be significant while that between 92 and 95 may not be.
*
* @author       {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
* @copyright    Copyright (c) 2005, Marjolein Katsma
* @license      http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @version      1.0
*
* @param    mixed   $limits required: single integer or array of integers;
*                   defines the upper limits of the ranges as well as the next step size
* @param    int     $max    required: upper limit for the whole list
*                   (will be included if smaller than the largest limit)
* @param    int     $firstinc optional: increment for the first range; default 1
* @return   array   resulting list of numbers
*/

function optionRanges($limits, $max, $firstinc = 1)
{
    // initializations
    if (is_int($limits))
    {
        $limits = array($limits);
    }
    if ($firstinc < 1)
    {
        $firstinc = 1;
    }

    $opts = array();
    $inc = $firstinc;

    // first element is the first increment
    $opts[] = $inc;
    // each $limit is the upper limit of a 'range'
    foreach ($limits as $limit)
    {
        for ($i = $inc + $inc; $i <= $limit && $i < $max; $i += $inc)
        {
            $opts[] = $i;
        }
        // we quit at $max, even if there are more $limit elements
        if ($limit >= $max)
        {
            // add $max to the list; then break out of the loop
            $opts[] = $max;
            break;
        }
        // when $limit is reached, it becomes the new start and increment for the next 'range'
        $inc = $limit;
    }

    return $opts;
}

// restrict access to admins
if ($this->IsAdmin($this->GetUser())) {

    // -------------------------------------
    // set default values as constants
    define('DEFAULT_RECORDS_LIMIT', '20'); # number of records per page
    define('DEFAULT_MIN_RECORDS_DISPLAY', '5'); # min number of records
    define('DEFAULT_RECORDS_RANGE',serialize(array('10','50','100','500','1000'))); #range array for records pager
    define('DEFAULT_SORT_FIELD', 'time'); # sort field
    define('DEFAULT_SORT_ORDER', 'desc'); # sort order, ascendant or descendant
    define('DEFAULT_START', '0'); # start record
    define('DEFAULT_SEARCH', ''); # keyword to restrict page search
    define('DEFAULT_TAG_LENGTH', '12'); # max. length of displayed pagename
    define('DEFAULT_URL_LENGTH', '15'); # max. length of displayed user host
    define('DEFAULT_TERMINATOR', '&#8230;'); # standard symbol replacing truncated text (ellipsis) JW 2005-07-19
    define('ALTERNATE_ROW_COLOR', '1'); # switch alternate row color
    define('STAT_COLUMN_COLOR', '1'); # switch color for statistics columns

    // -------------------------------------
    // User-interface: icons

    define('HITS_ICON', 'images/icons/16x16/stock_about.png');
    define('REVISIONS_ICON', 'images/icons/16x16/stock_book_open.png');
    define('COMMENTS_ICON', 'images/icons/16x16/stock_help-agent.png');
    define('BACKLINKS_ICON', 'images/icons/16x16/stock_link.png');
    define('REFERRERS_ICON', 'images/icons/16x16/stock_internet.png');


    // -------------------------------------
    // User-interface: strings

    define('PAGE_TITLE','Page Administration');
    define('FORM_LEGEND','Filter view:');
    define('FORM_SEARCH_STRING_LABEL','Search page:');
    define('FORM_SEARCH_STRING_TITLE','Enter a search string');
    define('FORM_SEARCH_SUBMIT','Submit');
    define('FORM_PAGER_LABEL_BEFORE','Show');
    define('FORM_PAGER_TITLE','Select records-per-page limit');
    define('FORM_PAGER_LABEL_AFTER','records per page');
    define('FORM_PAGER_SUBMIT','Apply');
    define('FORM_PAGER_LINK','Show records from %d to %d');
    define('FORM_RESULT_INFO','Records');
    define('FORM_RESULT_SORTED_BY','Sorted by:');
    define('TABLE_HEADING_PAGENAME','Page Name');
    define('TABLE_HEADING_PAGENAME_TITLE','Sort by page name');
    define('TABLE_HEADING_OWNER','Owner');
    define('TABLE_HEADING_OWNER_TITLE','Sort by page owner');
    define('TABLE_HEADING_LASTAUTHOR','Last Author');
    define('TABLE_HEADING_LASTAUTHOR_TITLE','Sort by last author');
    define('TABLE_HEADING_LASTEDIT','Last Edit');
    define('TABLE_HEADING_LASTEDIT_TITLE','Sort by edit time');
    define('TABLE_SUMMARY','List of pages on this server');
    define('TABLE_HEADING_HITS_TITLE','Hits');
    define('TABLE_HEADING_REVISIONS_TITLE','Sort by number of revisions (DEBUG ONLY)');
    define('TABLE_HEADING_COMMENTS_TITLE','Comments');
    define('TABLE_HEADING_BACKLINKS_TITLE','Backlinks');
    define('TABLE_HEADING_REFERRERS_TITLE','Referrers');
    define('TABLE_HEADING_HITS_ALT','Hits');
    define('TABLE_HEADING_REVISIONS_ALT','Revisions');
    define('TABLE_HEADING_COMMENTS_ALT','Comments');
    define('TABLE_HEADING_BACKLINKS_ALT','Backlinks');
    define('TABLE_HEADING_REFERRERS_ALT','Referrers');
    define('TABLE_HEADING_ACTIONS','Actions');
    define('ACTION_EDIT_LINK_TITLE','Edit %s');
    define('ACTION_DELETE_LINK_TITLE','Delete %s');
    define('ACTION_CLONE_LINK_TITLE','Clone %s');
    define('ACTION_RENAME_LINK_TITLE','Rename %s (DISABLED)');
    define('ACTION_ACL_LINK_TITLE','Change Access Control List for %s');
    define('ACTION_INFO_LINK_TITLE','Display information and statistics for %s');
/*>>>*/ define('ACTION_PRUNE_LINK_TITLE','Display misc options for %s');
    define('ACTION_EDIT_LINK','edit');
    define('ACTION_DELETE_LINK','delete');
    define('ACTION_CLONE_LINK','clone');
    define('ACTION_RENAME_LINK','rename');
    define('ACTION_ACL_LINK','acl');
    define('ACTION_INFO_LINK','info');
/*>>>*/     define('ACTION_PRUNE_LINK','prune');
    define('TAKE_OWNERSHIP_LINK','Take ownership of');
    define('NO_OWNER','(Nobody)');
    define('TABLE_CELL_HITS_TITLE','Hits for %s (%d)');
    define('TABLE_CELL_REVISIONS_TITLE','Display revisions for %s (%d)');
    define('TABLE_CELL_COMMENTS_TITLE','Display comments for %s (%d)');
    define('TABLE_CELL_BACKLINKS_TITLE','Display pages linking to %s (%d)');
    define('TABLE_CELL_REFERRERS_TITLE','Display external sites linking to %s (%d)');
    define('SELECT_RECORD_TITLE','Select %s');
    define('NO_EDIT_NOTE','[No edit note]');
    define('CHECK_ALL_TITLE','Check all records');
    define('CHECK_ALL','Check all');
    define('UNCHECK_ALL_TITLE','Uncheck all records');
    define('UNCHECK_ALL','Uncheck all');
    define('FORM_MASSACTION_LEGEND','Mass-action');
    define('FORM_MASSACTION_LABEL','With selected');
    define('FORM_MASSACTION_SELECT_TITLE','Choose action to apply to selected records (DISABLED)');
    define('FORM_MASSACTION_OPT_DELETE','Delete all');
    define('FORM_MASSACTION_OPT_CLONE','Clone all');
    define('FORM_MASSACTION_OPT_RENAME','Rename all');
    define('FORM_MASSACTION_OPT_ACL','Change Access Control List');
    define('FORM_MASSACTION_SUBMIT','Submit');
    define('ERROR_NO_MATCHES','Sorry, there are no pages matching "%s"');


    // -------------------------------------
    // Initialize variables

    $r = 1; #initialize row counter
    $r_color = ALTERNATE_ROW_COLOR; #get alternate row color option
    $c_color = STAT_COLUMN_COLOR; #get column color option
    // record dropdown
    $page_limits = unserialize(DEFAULT_RECORDS_RANGE);
    // pager
    $prev = '';
    $next = '';

    //override defaults with action parameters
    if (is_array($vars))
    {
        foreach ($vars as $param => $value)
        {
            switch ($param)
            {
                case 'colcolor':
                    $c_color = (preg_match('/[01]/',$value))? $value : STAT_COLUMN_COLOR;
                break;

                case 'rowcolor':
                    $r_color = (preg_match('/[01]/',$value))? $value : ALTERNATE_ROW_COLOR;
                break;
            }
        }
    }

    //perform mass-operations if required (forthcoming)
    if (isset($_GET['action']))
    {
        if ($_GET['action'] == 'massdelete')
        {
            echo $this->Action('massdelete');
        }
        elseif ($_GET['action'] == 'massrename')
        {
            echo $this->Action('massrename');
        }
        elseif ($_GET['action'] == 'massacls')
        {
            echo $this->Action('massacls');
        }
    }
    else
    {
        // process URL variables
        # JW 2005-07-19 some modifications to avoid notices but these are still not actually secure

        // number of records per page
        if (isset($_POST['l']))
        {
            $l = $_POST['l'];
        }
        elseif (isset($_GET['l']))
        {
            $l = $_GET['l'];
        }
        else
        {
            $l = DEFAULT_RECORDS_LIMIT;
        }

        // sort field
        $sort = (isset($_GET['sort'])) ? $_GET['sort'] : DEFAULT_SORT_FIELD;
        // sort order
        $d = (isset($_GET['d'])) ? $_GET['d'] : DEFAULT_SORT_ORDER;
        // start record
        $s = (isset($_GET['s'])) ? $_GET['s'] : DEFAULT_START;

        // search string
        if (isset($_POST['q']))
        {
            $q = $_POST['q'];
        }
        elseif (isset($_GET['q']))
        {
            $q = $_GET['q'];
        }
        else
        {
            $q = DEFAULT_SEARCH;
        }

        // select all   added JW 2005-07-19
        $checked = '';
        if (isset($_GET['selectall']))
        {
            $checked = (1 == $_GET['selectall']) ? ' checked="checked"' : '';
        }

        // restrict MySQL query by search string     modified JW 2005-07-19
        $where = ('' == $q) ? "`latest` = 'Y'" : "`tag` LIKE '%".$q."%' AND `latest` = 'Y'";
        // get total number of pages
        $numpages = $this->getCount('pages',$where);

        // print page header
        echo $this->Format('==== '.PAGE_TITLE.' ==== --- ');

        // build pager form
        $form1 = $this->FormOpen('','','post','page_admin_panel');
        $form1 .= '<fieldset><legend>'.FORM_LEGEND.'</legend>'."\n";
        $form1 .= '<label for="q">'.FORM_SEARCH_STRING_LABEL.'</label> <input type ="text" id="q" name="q" title="'.FORM_SEARCH_STRING_TITLE.'" size="20" maxlength="50" value="'.$q.'"/> <input type="submit" value="'.FORM_SEARCH_SUBMIT.'" /><br />'."\n";
        // ranged drop-down
        $pages_opts = optionRanges($page_limits,$numpages,DEFAULT_MIN_RECORDS_DISPLAY);
        $form1 .= '<label for="l">'.FORM_PAGER_LABEL_BEFORE.'</label> '."\n";
        $form1 .= '<select name="l" id="l" title="'.FORM_PAGER_TITLE.'">'."\n";

        // build drop-down
        foreach ($pages_opts as $opt)
        {
            $selected = ($opt == $l) ? ' selected="selected"' : '';
            $form1 .= '<option value="'.$opt.'"'.$selected.'>'.$opt.'</option>'."\n";
        }

        $form1 .=  '</select> <label for="l">'.FORM_PAGER_LABEL_AFTER.'</label> <input type="submit" value="'.FORM_PAGER_SUBMIT.'" /><br />'."\n";

        // build pager links
        if ($s > 0)
        {
            $prev = '<a href="' .$this->Href('','','l='.$l.'&amp;sort='.$sort.'&amp;d='.$d.'&amp;s='.($s-$l)).'&amp;q='.$q.'" title="'.sprintf(FORM_PAGER_LINK, ($s-$l+1), $s).'">'.($s-$l+1).'-'.$s.'</a> |  '."\n";
        }

        if ($numpages > ($s + $l))
        {
            $next = ' | <a href="'.$this->Href('','','l='.$l.'&amp;sort='.$sort.'&amp;d='.$d.'&amp;s='.($s+$l)).'&amp;q='.$q.'" title="'.sprintf(FORM_PAGER_LINK, ($s+$l+1), ($s+2*$l)).'">'.($s+$l+1).'-'.($s+2*$l).'</a>'."\n";
        }

        $form1 .= FORM_RESULT_INFO.' ('.$numpages.'): '.$prev.($s+1).'-'.($s+$l).$next.'<br />'."\n";
        $form1 .= '('.FORM_RESULT_SORTED_BY.'<em>'.$sort.', '.$d.'</em>)'."\n";
        $form1 .= '</fieldset>'.$this->FormClose()."\n";

        // print form
        echo $form1;

        // sort by counted values
        switch($sort)
        {
            case 'edits': #alpha --- 'latest' needs to be disabled
                //sample query:
                //SELECT *, COUNT(*) as edits FROM `wikka1160_pages` GROUP BY tag ORDER BY edits DESC
                $count = ', COUNT(*) as edits';
                $group = 'GROUP BY tag';
                $where = '1';
                //$where = ('' == $q) ? "1" : "`tag` LIKE '%".$q."%'";
                $table = 'pages';
                break;

            case 'comments': #to implement
                /*
                // SELECT wikka1160_pages.tag, COUNT(  *  )  AS comments FROM wikka1160_pages, wikka1160_comments WHERE wikka1160_pages.tag = wikka1160_comments.page_tag GROUP  BY wikka1160_pages.tag ORDER  BY comments DESC
                $count = ', COUNT(*) as edits';
                $group = 'GROUP BY tag';
                $where = '1';
                */

                break;
            default:
                $table = 'pages';
        }

        // get page list
        $pagedata = $this->LoadAll("SELECT *".$count." FROM ".$this->config["table_prefix"].$table." WHERE ".
            $where." ".$group." ORDER BY ".$sort." ".$d." LIMIT ".$s.", ".$l);

        if ($pagedata)
        {
            // build table headers
            $tagheader = '<a href="'.$this->Href('','', (($sort == 'tag' && $d == 'asc')? 'l='.$l.'&amp;sort=tag&amp;d=desc&amp;q='.$q : 'l='.$l.'&amp;sort=tag&amp;d=asc&amp;q='.$q)).'" title="'.TABLE_HEADING_PAGENAME_TITLE.'">'.TABLE_HEADING_PAGENAME.'</a>';
            $ownerheader = '<a href="'.$this->Href('','', (($sort == 'owner' && $d == 'asc')? 'l='.$l.'&amp;sort=owner&amp;d=desc&amp;q='.$q : 'l='.$l.'&amp;sort=owner&amp;d=asc&amp;q='.$q)).'" title="'.TABLE_HEADING_OWNER_TITLE.'">'.TABLE_HEADING_OWNER.'</a>';
            $userheader = '<a href="'.$this->Href('','', (($sort == 'user' && $d == 'asc')? 'l='.$l.'&amp;sort=user&amp;d=desc&amp;q='.$q : 'l='.$l.'&amp;sort=user&amp;d=asc&amp;q='.$q)).'" title="'.TABLE_HEADING_LASTAUTHOR_TITLE.'">'.TABLE_HEADING_LASTAUTHOR.'</a>';
            $lasteditheader = '<a href="'.$this->Href('','', (($sort == 'time' && $d == 'desc')? 'l='.$l.'&amp;sort=time&amp;d=asc&amp;q='.$q : 'l='.$l.'&amp;sort=time&amp;d=desc&amp;q='.$q)).'" title="'.TABLE_HEADING_LASTEDIT_TITLE.'">'.TABLE_HEADING_LASTEDIT.'</a>';
            $revisionsheader = '<a href="'.$this->Href('','', (($sort == 'edits' && $d == 'desc')? 'l='.$l.'&amp;sort=edits&amp;d=asc&amp;q='.$q : 'l='.$l.'&amp;sort=edits&amp;d=desc&amp;q='.$q)).'" title="'.TABLE_HEADING_REVISIONS_TITLE.'"><img src="'.REVISIONS_ICON.'" alt="'.TABLE_HEADING_REVISIONS_ALT.'"/></a>';

            $htmlout = "<table summary=\"".TABLE_SUMMARY."\" border=\"1px\" id=\"admin_table\">\n".
                "<thead>\n<tr>\n".
                "    <th>&nbsp;</th>\n".
                "    <th>".$tagheader."</th>\n".
                "    <th>".$ownerheader."</th>\n".
                "    <th>".$userheader."</th>\n".
                "    <th>".$lasteditheader."</th>\n".
                "    <th class=\"number ".(($c_color == 1)? ' c1' : '')."\" title=\"".TABLE_HEADING_HITS_TITLE."\"><img src=\"".HITS_ICON."\" alt=\"".TABLE_HEADING_HITS_ALT."\"/></th>\n".
                "    <th class=\"number ".(($c_color == 1)? ' c2' : '')."\" title=\"".TABLE_HEADING_REVISIONS_TITLE."\">".$revisionsheader."</th>\n".
                "    <th class=\"number ".(($c_color == 1)? ' c3' : '')."\" title=\"".TABLE_HEADING_COMMENTS_TITLE."\"><img src=\"".COMMENTS_ICON."\" alt=\"".TABLE_HEADING_COMMENTS_ALT."\"/></th>\n".
                "    <th class=\"number ".(($c_color == 1)? ' c4' : '')."\" title=\"".TABLE_HEADING_BACKLINKS_TITLE."\"><img src=\"".BACKLINKS_ICON."\" alt=\"".TABLE_HEADING_BACKLINKS_ALT."\"/></th>\n".
                "    <th class=\"number ".(($c_color == 1)? ' c5' : '')."\" title=\"".TABLE_HEADING_REFERRERS_TITLE."\"><img src=\"".REFERRERS_ICON."\" alt=\"".TABLE_HEADING_REFERRERS_ALT."\"/></th>\n".
                "    <th class=\"center\">".TABLE_HEADING_ACTIONS."</th>\n".
                "  </tr>\n</thead>\n";

            // feed table with data
            foreach($pagedata as $page)
            {
                // truncate long page names
                $pagename = (strlen($page['tag']) > DEFAULT_TAG_LENGTH) ? substr($page['tag'], 0, DEFAULT_TAG_LENGTH).DEFAULT_TERMINATOR : $page['tag'];

                // build handler links
                $lastedit = $page['time'];
                if ($pagename != $page['tag'])
                {
                    $showpage = '<a href="'.$this->Href('',$page['tag'], '').'" title="'.$page['tag'].'">'.$pagename.'</a>';
                }
                else
                {
                    $showpage = '<a href="'.$this->Href('',$page['tag'], '').'">'.$pagename.'</a>';
                }

                $editpage = '<a href="'.$this->Href('edit',$page['tag'], '').'" title="'.sprintf(ACTION_EDIT_LINK_TITLE, $page['tag']).'">'.ACTION_EDIT_LINK.'</a>';
                $deletepage = '<a href="'.$this->Href('delete',$page['tag'], '').'" title="'.sprintf(ACTION_DELETE_LINK_TITLE, $page['tag']).'">'.ACTION_DELETE_LINK.'</a>';
                $clonepage = '<a href="'.$this->Href('clone',$page['tag'], '').'" title="'.sprintf(ACTION_CLONE_LINK_TITLE, $page['tag']).'">'.ACTION_CLONE_LINK.'</a>';
                // renaming disabled
                $renamepage = '<a href="'.$this->Href('rename',$page['tag'], '').'" title="'.sprintf(ACTION_RENAME_LINK_TITLE, $page['tag']).'">'.ACTION_RENAME_LINK.'</a>';
                $aclpage = '<a href="'.$this->Href('acls',$page['tag'], '').'" title="'.sprintf(ACTION_ACL_LINK_TITLE, $page['tag']).'">'.ACTION_ACL_LINK.'</a>';
                $infopage = '<a href="'.$this->Href('info',$page['tag'], '').'" title="'.sprintf(ACTION_INFO_LINK_TITLE, $page['tag']).'">'.ACTION_INFO_LINK.'</a>';
/*>>>*/         $prunepage = '<a href="'.$this->Href('prune',$page['tag'], '').'" title="'.sprintf(ACTION_PRUNE_LINK_TITLE, $page['tag']).'">'.ACTION_PRUNE_LINK.'</a>';

                // get page owner
                if ($page['owner'])
                {
                    // is the owner a registered user?
                    if ($this->LoadUser($page['owner']))
                    {
                        // does user's homepage exist?
                        if ($this->ExistsPage($page['owner']))
                        {
                            $owner = $this->Link($page['owner']);
                        }
                        else
                        {
                            $owner = $page['owner'];
                        }
                    }
                    else
                    {
                        $owner = $page['owner'];
                    }
                }
                else
                {
                    // page has empty owner field: print claim link
                    $owner = $this->Link($page['tag'], 'claim','(Nobody)','','',TAKE_OWNERSHIP_LINK.' '.$page['tag']);
                }
                // get last author
                if ($page['user'])
                {
                    // is the author a registered user?
                    if ($this->LoadUser($page['user']))
                    {
                        // does user's homepage exist?
                        if ($this->ExistsPage($page['user']))
                        {
                            $user = $this->Link($page['user']);
                        }
                        else
                        {
                            $user = $page['user'];
                        }
                    }
                    else
                    {
                        // truncate long host names
                        $user = (strlen($page['user']) > DEFAULT_URL_LENGTH) ? substr($page['user'], 0, DEFAULT_URL_LENGTH).DEFAULT_TERMINATOR : $page['user'];
                        # added  JW 2005-07-19
                        if ($user != $page['user'])
                        {
                            $user = '<span title="'.$page['user'].'">'.$user.'</span>';
                        }
                    }
                }
                else
                {
                    // page has empty user field
                    $user = NO_OWNER;
                }

                // get counts   - JW 2005-07-19
                $whereTag       = "`tag` = '".$page['tag']."'";
                $wherePageTag   = "`page_tag` = '".$page['tag']."'";
                $whereToTag     = "`to_tag` = '".$page['tag']."'";
                $hn = 0;
                $rv = $this->getCount('pages',$whereTag);
                $cn = $this->getCount('comments',$wherePageTag);
                $bn = $this->getCount('links',$whereToTag);
                $rn = $this->getCount('referrers',$wherePageTag);

                // get page hits (forthcoming)
                $hitspage = ($hn > 0) ? '<a href="'.$this->Href('hits',$page['tag'], '').'" title="'.sprintf(TABLE_CELL_HITS_TITLE, $page['tag'], $hn).'">'.$hn.'</a>' : '0';

                // get page revisions and create revision link if needed
                $revpage = ($rv > 0) ? '<a href="'.$this->Href('revisions',$page['tag'], '').'" title="'.sprintf(TABLE_CELL_REVISIONS_TITLE, $page['tag'], $rv).'">'.$rv.'</a>' : '0';

                // get page comments and create comments link if needed
                $commentspage = ($cn > 0) ? '<a href="'.$this->Href('',$page['tag'], 'show_comments=1#comments').'" title="'.sprintf(TABLE_CELL_COMMENTS_TITLE, $page['tag'], $cn).'">'.$cn.'</a>' : '0';

                // get page backlinks and create backlinks link
                $backlinkpage = ($bn > 0) ? '<a href="'.$this->Href('backlinks',$page['tag'], '').'" title="'.sprintf(TABLE_CELL_BACKLINKS_TITLE, $page['tag'], $bn).'">'.$bn.'</a>' : '0';

                // get page referrers and create referrer link
                $refpage = ($rn > 0) ? '<a href="'.$this->Href('referrers',$page['tag'], '').'" title="'.sprintf(TABLE_CELL_REFERRERS_TITLE, $page['tag'], $rn).'">'.$rn.'</a>' : '0';

                // build table body
                $htmlout .= "<tbody>\n";
                if ($r_color == 1)
                {
                    $htmlout .= "<tr ".(($r%2)? '' : 'class="alt"').">\n"; #enable alternate row color
                } else
                {
                    $htmlout .= "<tr>\n"; #disable alternate row color
                }

                $htmlout .="    <td><input type=\"checkbox\" name=\"id_".$page['id']."\"".$checked." title=\"".sprintf(SELECT_RECORD_TITLE, $page['tag'])."\"/></td>\n".    # modified JW 2005-07-19
                    "    <td>".$showpage."</td>\n".
                    "    <td>".$owner."</td>\n".
                    "    <td>".$user."</td>\n".
                    "    <td class=\"time\" ".((strlen($page['note'])>0)? 'title="['.$page['note'].']"' : 'title="'.NO_EDIT_NOTE.'"').">".$lastedit."</td>\n".
                    "    <td class=\"number ".(($c_color == 1)? ' c1' : '')."\">".$hitspage."</td>\n".
                    "    <td class=\"number ".(($c_color == 1)? ' c2' : '')."\">".$revpage."</td>\n".
                    "    <td class=\"number ".(($c_color == 1)? ' c3' : '')."\">".$commentspage."</td>\n".
                    "    <td class=\"number ".(($c_color == 1)? ' c4' : '')."\">".$backlinkpage."</td>\n".
                    "    <td class=\"number ".(($c_color == 1)? ' c5' : '')."\">".$refpage."</td>\n".
/*>>>*/                 "    <td class=\"center \">".$editpage." :: ".$deletepage." :: ".$clonepage." :: "./*$renamepage*." :: ".*/$aclpage." :: ".$infopage." :: ".$prunepage."</td>\n".
                    "  </tr>\n</tbody>\n";

                //increase row counter    ----- alternate row colors
                if ($r_color == 1)
                    $r++;
            }

            $htmlout .= '</table>'."\n";
            // print the table
            echo $this->FormOpen('','','get');
            echo $htmlout;

            // multiple-page operations (forthcoming)       JW 2005-07-19 accesskey removed (causes more problems than it solves)
            echo '<fieldset><legend>'.FORM_MASSACTION_LEGEND.'</legend>';
            echo '[<a href="'.$this->Href('','','l='.$l.'&amp;sort='.$sort.'&amp;d='.$d.'&amp;s='.$s.'&amp;q='.$q.'&amp;selectall=1').'" title="'.CHECK_ALL_TITLE.'">'.CHECK_ALL.'</a> | <a href="'.$this->Href('','','l='.$l.'&amp;sort='.$sort.'&amp;d='.$d.'&amp;s='.$s.'&amp;q='.$q.'&amp;selectall=0').'" title="'.UNCHECK_ALL_TITLE.'">'.UNCHECK_ALL.'</a>]<br />';
            echo '<label for="action" >'.FORM_MASSACTION_LABEL.'</label> <select title="'.FORM_MASSACTION_SELECT_TITLE.'" id="action" name="action">';
            echo '<option value="" selected="selected">---</option>';
            echo '<option value="massdelete">'.FORM_MASSACTION_OPT_DELETE.'</option>';
            echo '<option value="massclone">'.FORM_MASSACTION_OPT_CLONE.'</option>';
            echo '<option value="massrename">'.FORM_MASSACTION_OPT_RENAME.'</option>';
            echo '<option value="massacls">'.FORM_MASSACTION_OPT_ACL.'</option>';
            echo '</select> <input type="submit" value="'.FORM_MASSACTION_SUBMIT.'" />';
            echo '</fieldset>';
            echo $this->FormClose();
        }
        else
        {
            // no records matching the search string: print error message
            echo '<p><span class="error">'.sprintf(ERROR_NO_MATCHES, $q).'</span></p>';
        }
    }
}
else
{
    // current user is not admin: show plain page index
    echo $this->Action('pageindex');
}
?>



Save this as handlers\page\prune.php
<div class="page"
<?php
require 'date.class.php';

/*
    Form to delete a number of revisions from a page.
    This form operates in one of 3 phases:
    1. Initial load: $_POST is not set. Show form to accept revision prune criteria
    2. Confirmation: $_POST is set and contains either of the 2 possible prune criteria (# of revs or date)
    3. Execution: $_POST is set and contains either a GoAhead or Cancel value


NickDamoulakis: 4dec2005, v0.50, Initial version

Comments are most welcome!

- I was using the $_SESSION var to hold variables between invocations. However, I wasn't sure if that is the best way. Should those vars
  stay around till the session end (browser or browser window shut down??).
  I've switched to using the POST vars which only last for the lifetime of the page (right?).
- Initially, I had a function at the end of this source file that did the actual deletion. However, I was getting an error about the function
  LoadSingle() not being found. $this->LoadSingle() obviously didn't work but neither did LoadSingle(). I know I am missing something :-)
- I am unable to return to the calling page via Redirect. I did have it working but I don't understand what made it break. It now goes to
  the page that is being worked on instead. I was forced to specify 'PageAdmin' as the page to return to, which I am not happy with :-(

*/



define('PAGE_TITLE','Prune actions on %s');
define('TABLENAME', $this->config['table_prefix']."pages");

if  (!($this->UserIsOwner() || $this->IsAdmin()))
{
    $this->redirect($this->Href(), "Only the owner of this page can make these changes.");
}
else
{
    // If we are in phase 1, show initial form
    if (! $_POST)
    {
        echo $this->FormOpen("prune");
        echo $this->Format('=== '.sprintf(PAGE_TITLE,'[['.$this->tag.']]').' ===----');
        ?>

        <!-- use this to pass the tag name across to the next phases -->
        <input type="hidden" name="tag" value=<?php echo "\"$this->tag\""; ?> >
        <label for="NumOfRevs2Keep">
            <br>
        </label>
        <table height="64" width="600" border="1">
          <tr><!-- Row 1 -->
            <td width="275">
                No. of revisions to keep for this page
            </td><!-- Col 1 -->
            <td width="115">
                <input maxlength="5" size="5" type="text" name="NumOfRevs2Keep">
            </td><!-- Col 2 -->
            <td>
            </td><!-- Col 3 -->
          </tr>
          <tr><!-- Row 2 -->
            <td width="5">
                Or
            </td><!-- Col 1 -->
          <tr><!-- Row 3 -->
            <td width="275">
                Delete revisions prior to date (inclusive)
            </td><!-- Col 1 -->
            <td width="120">
                <input type="text" maxlength="20" size="20" name="PurgeRevisionsBeforeDate">
            </td><!-- Col 2 -->
            <td>
                <input style="WIDTH: 145px; HEIGHT: 24px" type="submit" size="15" name="Phase2" value="Purge old revisions">
            </td><!-- Col 3 --></tr>
            </table>
            <br><br>
        <br>

        <?php
        echo $this->FormClose();
    }
    // ---------------------------------------------------------------------------------------
    // Phase 2: show what we are about to do and ask for ok to do so.
    elseif ($_POST['Phase2'])
    {
        echo $this->FormOpen("prune");

        $pageRevisions2Keep = trim($_POST["NumOfRevs2Keep"]);
        $purgeRevisionsBeforeDate = trim($_POST["PurgeRevisionsBeforeDate"]);

        // Dummy DO loop that simply allows multiple exits to a single point.
        $ok = false;
        do
        {
            // If both date and # of revs have been specified, use the date in preference of # of revs

            // Has a date been specified?
            if ($purgeRevisionsBeforeDate)
            {
                $dateError = "";
                // Check data, record errors in $dateError and return db-friendly formatted date
                $purgeRevisionsBeforeDate = IsValidDate($purgeRevisionsBeforeDate, $dateError);
                if ($dateError != "")
                {
                    echo "<br>".$dateError;
                    break;
                }

                echo "<br>Keeping page revisions from ".$purgeRevisionsBeforeDate." onwards";

                $ok = true;
                break;
            }

            // User specified (only!) # of revs to keep. Validate number, find what date that number corresponds to
            // in the PAGES table and act as if he specified a date rather than # of revs.
            if ($pageRevisions2Keep)
            {
                if (! is_numeric($pageRevisions2Keep))
                {
                    echo "This is not a valid number (".$pageRevisions2Keep.")";
                    break;
                }

                if ($pageRevisions2Keep < 1)
                {
                    echo "Hmmm. You want me to keep less than 1 revisions, eh? I refuse!";
                    break;
                }

                echo "<br>Keeping only the ".$pageRevisions2Keep." most recent revisions";

                $tag = $_POST["tag"];
                $tag = addslashes($tag);

                $sql = "SELECT time FROM ".TABLENAME." WHERE tag='".$tag."' ORDER BY time DESC LIMIT ".$pageRevisions2Keep.",1";
                if (! $tablepgname = $this->LoadSingle($sql))
                {
                    echo "<br><strong>There are less page revisions than what you specified. I've got nothing to do.</strong>";
                    break;
                }

                // Pickup the timestamp of the record corresponding to the # of revisions we requested.
                // ie, if we speficied 2, we need to look at the timestamp of the 3rd record and delete
                // from there on backwords in time (including that record)
                $purgeRevisionsBeforeDate = $tablepgname['time'];
                $purgeRevisionsBeforeDate = addslashes($purgeRevisionsBeforeDate);

                echo "<br>Deleting revisions dated ".$purgeRevisionsBeforeDate." and older";

                $ok = true;
                break;
            }

            echo "That's funny, that is! Am I meant to guess your intentions then? Please provide <em>some</em> input!";
            break;

        } while (false);

        // this is the exit point for the above DO loop. When we get here, $ok will be true or false
        // and it will be used below to enable/disable to GoAhead button.
        ?>

        <br>
        <!-- nonsense input so form submission works with rewrite mode -->
        <input type="hidden" name="pageRevisions2Keep" value=<?php echo "\"".$pageRevisions2Keep."\""; ?> >
        <input type="hidden" name="purgeRevisionsBeforeDate" value=<?php echo "\"".$purgeRevisionsBeforeDate."\""; ?> >
        <input type="hidden" name="tag" value=<?php echo "\"".$_POST['tag']."\""; ?> >

        <input style="WIDTH: 77px; HEIGHT: 24px" type="submit" name="Phase3" value="Go Ahead" <?php echo $ok ? "" : "disabled"; ?> >
        <input style="WIDTH: 67px; HEIGHT: 24px" type="submit" name="Cancel" value="Cancel">

        <?php
        echo $this->FormClose();
    }
    elseif ($_POST['Phase3'])
    // ---------------------------------------------------------------------------------------
    // Phase 3? User has pressed the GoAhead button
    {
        echo $this->FormOpen("prune");
        do
        {
            if (! $tag = $_POST["tag"])
            {
                echo "Tag variable was not set!???";
                break;
            }

            $tag = addslashes($tag);
            if (! $purgeRevisionsBeforeDate = $_POST["purgeRevisionsBeforeDate"])
            {
                $dateError = "";
                $purgeRevisionsBeforeDate = IsValidDate($purgeRevisionsBeforeDate, $dateError);
                if ($dateError != "")
                {
                    echo $dateError;
                    break;
                }
            }

            echo $sql = "DELETE from ".TABLENAME." WHERE tag = '".$tag."' AND time <= '".$purgeRevisionsBeforeDate."'";
            $this->Query($sql);
            $msg = "Deleted revisions dated ".$purgeRevisionsBeforeDate." and older";

        } while (false);
        echo $this->FormClose();

        // redirect back to page
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
        $this->redirect($this->config['base_url'].'PageAdmin', $msg);
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
    }
    elseif ($_POST["Cancel"])
    // ---------------------------------------------------------------------------------------
    // Phase 3? User chickened out
    {
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
        // Is there a better way to go back to the page that called me (actually, 2 pages back!)
        $this->redirect($this->config['base_url'].'PageAdmin');
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
        //************************************************************************************
    }
}

function IsValidDate($theDate, &$err)
{
    // Do date validation
    $dateobj = new DateClass;

    // Validate date and convert to database-friendly format
    if (! $internaldate = $dateobj->getInternalDate($theDate))
    {
        $err = $dateobj->errors." (".$theDate.")";
        $result = null;
    }
    else
    {
        $result = $dateobj->getExternalDate($internaldate);
    }

    return $result;
}

?>
</div>



Save this in date.class.php (same dir as wikka.php)
Please note that I did not write this class. Credit goes to A J Marston

<?php
//*****************************************************************************
// Copyright 2003 by A J Marston <http://www.tonymarston.net>
// Distributed under the GNU General Public Licence
//*****************************************************************************

class DateClass
{
   // member variables
   var $monthalpha;     // array of 3-character month names
   var $internaldate;   // date as held in the database (yyyymmdd)
   var $externaldate;   // date as shown to the user (dd Mmm yyyy)
   var $errors;         // error messages

// ****************************************************************************
// class constructor
// ****************************************************************************
   function DateClass ()
   {

       $this->monthalpha = array(1=>'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');

   } // DateClass

// ****************************************************************************
// accessor functions
// ****************************************************************************
   function getInternalDate ($input)
   // convert date from external format (as input by user)
   // to internal format (as used in the database)
   {

      // look for d(d)?m(m)?y(yyy) format
      $pattern = '(^[0-9]{1,2})'    // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2})'     // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,4}$)';   // 1 to 4 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[3],$regs[5]);
         return $result;
      } // if

      // look for d(d)?MMM?y(yyy) format
      $pattern = '(^[0-9]{1,2})'    // 1 or 2 digits
                 .'([^0-9a-zA-Z])'  // not alpha or numeric
                 .'([a-zA-Z]{1,})'  // 1 or more alpha
                 .'([^0-9a-zA-Z])'  // not alpha or numeric
                 .'([0-9]{1,4}$)';  // 1 to 4 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[3],$regs[5]);
         return $result;
      } // if

      // look for d(d)MMMy(yyy) format
      $pattern = '(^[0-9]{1,2})'    // 1 or 2 digits
                .'([a-zA-Z]{1,})'   // 1 or more alpha
                .'([0-9]{1,4}$)';   // 1 to 4 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[2],$regs[3]);
         return $result;
      } // if

      // look for MMM?d(d)?y(yyy) format
      $pattern = '(^[a-zA-Z]{1,})'  // 1 or more alpha
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2})'     // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,4}$)';   // 1 to 4 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[3],$regs[1],$regs[5]);
         return $result;
      } // if

      // look for MMMddyyyy format
      $pattern = '(^[a-zA-Z]{1,})'  // 1 or more alpha
                .'([0-9]{2})'       // 2 digits
                .'([0-9]{4}$)';     // 4 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[2],$regs[1],$regs[3]);
         return $result;
      } // if

      // look for yyyy?m(m)?d(d) format
      $pattern = '(^[0-9]{4})'      // 4 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2})'     // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2}$)';   // 1 to 2 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[5],$regs[3],$regs[1]);
         return $result;
      } // if

      // look for ddmmyyyy format
      $pattern = '(^[0-9]{2})'      // 2 digits
                .'([0-9]{2})'       // 2 digits
                .'([0-9]{4}$)';     // 4 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[2],$regs[3]);
         return $result;
      } // if

      // look for yyyy?MMM?d(d) format
      $pattern = '(^[0-9]{4})'      // 4 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([a-zA-Z]{1,})'   // 1 or more alpha
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2}$)';   // 1 to 2 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[5],$regs[3],$regs[1]);
         return $result;
      } // if

      $this->errors = 'This is not a valid date';
      return FALSE;

   } // getInternalDate

// ****************************************************************************
   function getInternalTime ($input)
   // convert time from external format (as input by user)
   // to internal format (as used in the database)
   {
      // look for HH?MM?SS format
      $pattern = '(^[0-9]{2})'      // 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{2})'       // 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{2}$)';     // 2 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyTime($regs[1],$regs[3],$regs[5]);
         return $result;
      } // if

      // look for HHMMSS format
      $pattern = '(^[0-9]{2})'      // 2 digits
                .'([0-9]{2})'       // 2 digits
                .'([0-9]{2}$)';     // 2 digits

      if (ereg($pattern, $input, $regs)) {
         $result = $this->verifyTime($regs[1],$regs[2],$regs[3]);
         return $result;
      } // if

      $this->errors = 'This is not a valid time';
      return FALSE;

   } // getInternalTime

// ****************************************************************************
   function verifyDate($day, $month, $year)
   {

      // convert alpha month to digits
      if (eregi('([a-z]{3})', $month)) {
         $month = ucfirst(strtolower($month));
         if (!$month = array_search($month, $this->monthalpha)) {
            $this->errors = 'Month name is invalid';
            return FALSE;
         } // if
      } // if

      // ensure that year has 4 digits
      if (strlen($year) == 1) {
         $year = '200' .$year;
      } // if
      if (strlen($year) == 2) {
         $year = '20' .$year;
      } // if
      if (strlen($year) == 3) {
         $year = '2' .$year;
      } // if

      if (!checkdate($month, $day, $year)) {
         $this->errors = 'This is not a valid date';
         return FALSE;
      } else {
         if (strlen($day) < 2) {
            $day = '0' .$day;        // add leading zero
         } // if
         if (strlen($month) < 2) {
            $month = '0' .$month;    // add leading zero
         } // if
         $this->internaldate = $year .'-' .$month .'-' .$day;
         return $this->internaldate;
      } // if

      return;

   } // verifyDate

// ****************************************************************************
   function verifyTime($hours, $minutes, $seconds)
   {

      if ($hours > 24) {
         $this->errors = 'Invalid HOURS';
         return FALSE;
      } // if

      if ($minutes > 59) {
         $this->errors = 'Invalid MINUTES';
         return FALSE;
      } // if

      if ($minutes > 59) {
         $this->errors = 'Invalid MINUTES';
         return FALSE;
      } // if

      return "$hours:$minutes:$seconds";

   } // verifyTime

// ****************************************************************************
   function getExternalDate ($input)
   // convert date from internal format (as used in the database)
   // to external format (as shown to the user))
   {

      // input may be 'yyyy-mm-dd' or 'yyyymmdd', so
      // check the length and process accordingly

      if (strlen($input) == 8) {
         // test for 'yyyymmdd'
         $pattern = '(^[0-9]{4})'   // 4 digits (yyyy)
                   .'([0-9]{2})'    // 2 digits (mm)
                   .'([0-9]{2}$)';  // 2 digits (dd)
         if (ereg($pattern, $input, $regs)) {
            if (!checkdate($regs[2], $regs[3], $regs[1])) {
               $this->errors = 'This is not a valid date';
               return FALSE;
            } else {
               $monthnum = (int)$regs[2];
               $this->externaldate = "$regs[3] " .$this->monthalpha[$monthnum] ." $regs[1]";
               return $this->externaldate;
            } // if
         } // if
         $this->errors = "Invalid date format: expected 'yyyymmdd'";
         return FALSE;
      } // if

      if (strlen($input) == 10) {
         // test for 'yyyy-mm-dd'
         $pattern = '(^[0-9]{4})'   // 4 digits (yyyy)
                   .'([^0-9])'      // not a digit
                   .'([0-9]{2})'    // 2 digits (mm)
                   .'([^0-9])'      // not a digit
                   .'([0-9]{2}$)';  // 2 digits (dd)
         if (ereg($pattern, $input, $regs)) {
            if (!checkdate($regs[3], $regs[5], $regs[1])) {
               $this->errors = 'This is not a valid date';
               return FALSE;
            } else {
               $monthnum = (int)$regs[3];
               $this->externaldate = "$regs[5] " .$this->monthalpha[$monthnum] ." $regs[1]";
               return $this->externaldate;
            } // if
         } // if
         $this->errors = "Invalid date format: expected 'dd-mm-yyyy'";
         return FALSE;
      } // if

      $this->errors = 'This is not a valid date';
      return $input;

   } // getExternalDate

// ****************************************************************************
   function addDays ($internaldate, $days)
   // add a number of days (may be negative) to $internaldate (YYYY-MM-DD)
   // and return the result in the same format
   {

      // ensure date is in internal format
      $internaldate = $this->getInternalDate($internaldate);

      // convert to the number of days since basedate (4714 BC)
      $julian = GregoriantoJD(substr($internaldate,5,2)
                             ,substr($internaldate,8,2)
                             ,substr($internaldate,0,4));

      $days = (int)$days;
      $julian = $julian + $days;

      // convert from Julian to Gregorian (format m/d/y)
      $gregorian = JDtoGregorian($julian);

      // split date into its component parts
      list ($month, $day, $year) = split ('[/]', $gregorian);

      // convert back into standard format
      $result = $this->getInternaldate("$day/$month/$year");

      return $result;

   } // addDays

// ****************************************************************************
   function getErrors ()
   {
      return $this->errors;

   } // getErrMsg

// ****************************************************************************
} // end DateClass
// ****************************************************************************

?>

Add prune link to footer:
Open templates/footer.php

find the following:
echo $this->HasAccess('write') ? '<a href="'.$this->Href('edit').'" title="Click to edit this page">Edit</a> ::'."\n" : '';


add after:
echo $this->HasAccess('write') ? '<a href="'.$this->Href('prune').'" title="Click to prune old revisions of this page">Prune</a> ::'."\n" : '';

Addition by GrahamKelly


Categories

CategoryUserContributions
There are no comments on this page.
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki