root/branches/1.1.6.7/libs/Wakka.class.php

Revision 1373, 73.0 KB (checked in by BrianKoontz, 15 months ago)

Inadvertent editing error corrected. This fixes the issue of URLs not
rendering as clickable links. Refs #877.

Line 
1<?php
2/**
3 * This file is part of Wikka, a PHP wiki engine.
4 *
5 * It includes the Wakka class, which provides the core functions
6 * to run Wikka.
7 *
8 * @package Wikka
9 * @subpackage Libs
10 * @version $Id$
11 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
12 * @filesource
13 *
14 * @author      {@link http://www.mornography.de/ Hendrik Mans}
15 * @author      {@link http://wikkawiki.org/JsnX Jason Tourtelotte}
16 * @author      {@link http://wikkawiki.org/JavaWoman Marjolein Katsma}
17 * @author      {@link http://wikkawiki.org/NilsLindenberg Nils Lindenberg}
18 * @author      {@link http://wikkawiki.org/DotMG Mahefa Randimbisoa}
19 * @author      {@link http://wikkawiki.org/DarTar Dario Taraborelli}
20 * @author      {@link http://wikkawiki.org/BrianKoontz Brian Koontz}
21 *
22 * @copyright Copyright 2002-2003, Hendrik Mans <hendrik@mans.de>
23 * @copyright Copyright 2004-2005, Jason Tourtelotte <wikka-admin@jsnx.com>
24 * @copyright Copyright 2006-2009 {@link http://wikkawiki.org/CreditsPage Wikka Development Team}
25 */
26
27// Time to live for client-side cookies in seconds (90 days)
28if(!defined('PERSISTENT_COOKIE_EXPIRY')) define('PERSISTENT_COOKIE_EXPIRY', 7776000);
29
30// i18n TODO:move to language file
31if(!defined('CREATE_THIS_PAGE_LINK_TITLE')) define('CREATE_THIS_PAGE_LINK_TITLE', 'Create this page');
32
33/**
34 * The Wikka core.
35 *
36 * This class contains all the core methods used to run Wikka.
37 * @name Wakka
38 * @package Wikka
39 * @subpackage Libs
40 *
41 */
42class Wakka
43{
44        var $config = array();
45        var $dblink;
46        var $page;
47        var $tag;
48        var $queryLog = array();
49        var $interWiki = array();
50        var $VERSION;
51        var $cookies_sent = false;
52        var $cookie_expiry = PERSISTENT_COOKIE_EXPIRY; 
53        var $wikka_cookie_path;
54        var $additional_headers = array();
55
56        /**
57         * Constructor
58         */
59        function Wakka($config)
60        {
61                $this->config = $config;
62                $this->dblink = @mysql_connect($this->config["mysql_host"], $this->config["mysql_user"], $this->config["mysql_password"]);
63                if ($this->dblink)
64                {
65                        if (!@mysql_select_db($this->config["mysql_database"], $this->dblink))
66                        {
67                                @mysql_close($this->dblink);
68                                $this->dblink = false;
69                        }
70                }
71                $this->VERSION = WAKKA_VERSION;
72        }
73
74        /**
75         * Database methods
76         */
77        function Query($query, $dblink='')
78        {
79                // init - detect if called from objct or externally
80                if ('' == $dblink)
81                {
82                        $dblink = $this->dblink;
83                        $object = TRUE;
84                        $start = $this->GetMicroTime();
85                }
86                else
87                {
88                        $object = FALSE;
89                }
90                if (!$result = mysql_query($query, $dblink))
91                {
92                        ob_end_clean();
93                        die("Query failed: ".$query." (".mysql_error().")");
94                }
95                if ($object && $this->config['sql_debugging'])
96                {
97                        $time = $this->GetMicroTime() - $start;
98                        $this->queryLog[] = array(
99                                "query"         => $query,
100                                "time"          => $time);
101                }
102                return $result;
103        }
104        function LoadSingle($query) { if ($data = $this->LoadAll($query)) return $data[0]; }
105        function LoadAll($query)
106        {
107                $data = array();
108                if ($r = $this->Query($query))
109                {
110                        while ($row = mysql_fetch_assoc($r)) $data[] = $row;
111                        mysql_free_result($r);
112                }
113                return $data;
114        }
115        /**
116         * Generic 'count' query.
117         *
118         * Get a count of the number of records in a given table that would be matched
119         * by the given (optional) WHERE criteria. Only a single table can be queried.
120         *
121         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
122         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
123         * @since               Wikka 1.1.6.4
124         * @version             1.1
125         *
126         * @access      public
127         * @uses        Wakka::GetConfigValue()
128         * @uses        Wakka::Query()
129         *
130         * @param       string  $table  required: (logical) table name to query;
131         *                                                      prefix will be automatically added
132         * @param       string  $where  optional: criteria to be specified for a WHERE clause;
133         *                                                      do not include WHERE
134         * @return      integer number of matches returned by MySQL
135         * @todo        move into a database class.
136         */
137        function getCount($table, $where='')                                                    # JW 2005-07-16
138        {
139                // build query
140                $where = ('' != $where) ? ' WHERE '.$where : '';
141                $query = "
142                        SELECT COUNT(*)
143                        FROM ".$this->GetConfigValue('table_prefix').$table.
144                        $where;
145
146                // get and return the count as an integer
147                $count = (int)mysql_result($this->Query($query),0);
148                return $count;
149        }
150        function CheckMySQLVersion($major, $minor, $subminor)
151        {
152                $result = @mysql_query('SELECT VERSION() AS version');
153                if ($result != FALSE && @mysql_num_rows($result) > 0)
154                {
155                        $row   = mysql_fetch_array($result);
156                        $match = explode('.', $row['version']);
157                }
158                else
159                {
160                        $result = @mysql_query('SHOW VARIABLES LIKE \'version\'');
161                        if ($result != FALSE && @mysql_num_rows($result) > 0) {
162                                $row   = mysql_fetch_row($result);
163                                $match = explode('.', $row[1]);
164                        } else {
165                                return 0;
166                        }
167                }
168
169                $mysql_major = $match[0];
170                $mysql_minor = $match[1];
171                $mysql_subminor = $match[2][0].$match[2][1];
172
173                if ($mysql_major > $major) {
174                        return 1;
175                } else {
176                        if (($mysql_major == $major) && ($mysql_minor >= $minor) && ($mysql_subminor >= $subminor)) {
177                                return 1;
178                        } else {
179                                return 0;
180                        }
181                }
182        }
183
184        /**
185         * Misc methods
186         */
187        function GetMicroTime() { list($usec, $sec) = explode(" ",microtime()); return ((float)$usec + (float)$sec); }
188        function IncludeBuffered($filename, $notfoundText='', $vars='', $path='')
189        {
190                # TODO: change parameter order, so $path (no default,. it's required)
191                # comes after $filename and only $notfoundtext and $vars will actually
192                # be optional with a default of ''. MK/2007-03-31
193
194                // check if required parameter $path is supplied (see TODO)
195                if ('' != trim($path))
196                {
197                        // build full (relative) path to requested plugin (method/action/formatter)
198                        $fullfilepath = $this->BuildFullpathFromMultipath($filename, $path);
199                        // check if requested file (method/action/formatter) actually exists
200                        if (FALSE===empty($fullfilepath))
201                        {
202                                if (is_array($vars))
203                                {
204                                        // make the parameters also available by name (apart from the array itself):
205                                        // some callers rely on these separate values, so we extract them, too
206                                        // taking care not to overwrite any already-existing variable
207                                        extract($vars, EXTR_SKIP);      # [SEC] EXTR_SKIP avoids collision with existing filenames
208                                }
209                                ob_start();
210                                include($fullfilepath);
211                                $output = ob_get_contents();
212                                ob_end_clean();
213                                return $output;
214                        }
215                }
216                if ('' != trim($notfoundText))
217                {
218                        return '<em class="error">'.$this->htmlspecialchars_ent(trim($notfoundText)).'</em>';   # [SEC] make error (including (part of) request) safe to display
219                }
220                else
221                {
222                        return false;
223                }
224        }
225
226        /**
227         * Create a unique id for an HTML element.
228         *
229         * Although - given Wikka accepts can use embedded HTML - it cannot be
230         * guaranteed that an id generated by this method is unique it tries its
231         * best to make it unique:
232         * - ids are organized into groups, with the group name used as a prefix
233         * - if an id is specified it is compared with other ids in the same group;
234         *   if an identical id exists within the same group, a sequence suffix is
235         *   added, otherwise the specified id is accepted and recorded as a member
236         *   of the group
237         * - if no id is specified (or an invalid one) an id will be generated, and
238         *   given a sequence suffix if needed
239         *
240         * For headings, it is possible to derive an id from the heading content;
241         * to support this, any embedded whitespace is replaced with underscores
242         * to generate a recognizable id that will remain (mostly) constant even if
243         * new headings are inserted in a page. (This is not done for embedded
244         * HTML.)
245         *
246         * The method supports embedded HTML as well: as long as the formatter
247         * passes each id found in embedded HTML through this method it can take
248         * care that the id is valid and unique.
249         * This works as follows:
250         * - indicate an 'embedded' id with group 'embed'
251         * - NO prefix will be added for this reserved group
252         * - ids will be recorded and checked for uniqueness and validity
253         * - invalid ids are replaced
254         * - already-existing ids in the group are given a sequence suffix
255         * The result is that as long as the already-defined id is valid and
256         * unique, it will be remain unchanged (but recorded to ensure uniqueness
257         * overall).
258         *
259         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
260         * @copyright   Copyright © 2005, Marjolein Katsma
261         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
262         * @since               Wikka 1.1.6.4
263         * @version             1.0
264         *
265         * @access      public
266         * @uses        ID_LENGTH
267         *
268         * @param       string  $group  required: id group (e.g. form, head); will be
269         *                                                      used as prefix (except for the reserved group
270         *                                                      'embed' to be used for embedded HTML only)
271         * @param       string  $id             optional: id to use; if not specified or
272         *                                                      invalid, an id will be generated; if not
273         *                                                      unique, a sequence number will be appended
274         * @return      string  resulting id
275         */
276        function makeId($group,$id='')
277        {
278                // initializations
279                static $aSeq = array();                                                                         # group sequences
280                static $aIds = array();                                                                         # used ids
281
282                // preparation for group
283                if (!preg_match('/^[A-Z-a-z]/',$group))                                         # make sure group starts with a letter
284                {
285                        $group = 'g'.$group;
286                }
287                if (!isset($aSeq[$group]))
288                {
289                        $aSeq[$group] = 0;
290                }
291                if (!isset($aIds[$group]))
292                {
293                        $aIds[$group] = array();
294                }
295                if ('embed' != $group)
296                {
297                        $id = preg_replace('/\s+/','_',trim($id));                              # replace any whitespace sequence in $id with a single underscore
298                }
299
300                // validation (full for 'embed', characters only for other groups since we'll add a prefix)
301                if ('embed' == $group)
302                {
303                        $validId = preg_match('/^[A-Za-z][A-Za-z0-9_:.-]*$/',$id);      # ref: http://www.w3.org/TR/html4/types.html#type-id
304                }
305                else
306                {
307                        $validId = preg_match('/^[A-Za-z0-9_:.-]*$/',$id);
308                }
309
310                // build or generate id
311                if ('' == $id || !$validId || in_array($id,$aIds))                      # ignore specified id if it is invalid or exists already
312                {
313                        $id = substr(md5($group.$id),0,ID_LENGTH);                              # use group and id as basis for generated id
314                }
315                $idOut = ('embed' == $group) ? $id : $group.'_'.$id;            # add group prefix (unless embedded HTML)
316                if (in_array($id,$aIds[$group]))
317                {
318                        $idOut .= '_'.++$aSeq[$group];                                                  # add suffiX to make ID unique
319                }
320
321                // result
322                $aIds[$group][] = $id;                                                                          # keep track of both specified and generated ids (without suffix)
323                return $idOut;
324        }
325
326        /**
327         * Strip potentially dangerous tags from embedded HTML.
328         *
329         * @param       string $html mandatory: HTML to be secured
330         * @return      string sanitized HTML
331         */
332        function ReturnSafeHTML($html)
333        {
334                require_once('3rdparty/core/safehtml/classes/safehtml.php');
335
336                // Instantiate the handler
337                $safehtml =& new safehtml();
338
339                $filtered_output = $safehtml->parse($html);
340
341                return $filtered_output;
342        }
343
344        /**
345         * Make sure a (user-provided) URL does use &amp; instead of & and is protected from attacks.
346         *
347#        * Any already-present '&amp;' is first turned into '&'; then htmlspecialchars() is applied so
348         * Any already-present '&amp;' is first turned into '&'; then hsc_secure()
349         * is applied so all ampersands are "escaped" while characters that could be
350         * used to create a script attack (< > or ") are "neutralized" by escaping
351         * them.
352         *
353         * This method should be applied on any user-provided url in actions,
354         * handlers etc.
355         *
356         * Note: hsc_secure() is the secure replacement for PHP's htmlspecialchars().
357         * See #427.
358         *
359         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
360         * @copyright   Copyright © 2004, Marjolein Katsma
361         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
362         * @version             1.0
363         *
364         * @access              public
365         * @uses                Wakka::hsc_secure()
366         * @param               string  $url  required: URL to sanitize
367         * @return              string  sanitzied URL
368         */
369        function cleanUrl($url)
370        {
371                #return htmlspecialchars(preg_replace('/&amp;/','&',$url));
372                return $this->hsc_secure(preg_replace('/&amp;/','&',$url));
373        }
374
375        /**
376         * Wrapper around hsc_secure() which preserves entity references.
377         *
378         * The first two parameters for this function as the same as those for
379         * htmlspecialchars() in PHP: the text to be treated, and an optional
380         * parameter determining how to handle quotes; both these parameters are
381         * passed on to our hsc_secure() replacement for htmlspecialchars().
382         *
383         * Since hsc_secure() does not need a character set parameter, we don't
384         * have that here any more either.
385         *
386         * A third 'doctype' parameter is for local use only and determines how
387         * pre-existing entity references are treated after hsc_secure() has done
388         * its work: numeic entity references are always "unescaped' since they are
389         * valid for both HTML and XML doctypes; for XML the named entity references
390         * for the special characters are unescaped as well, while for for HTML any
391         * named entity reference is unescaped. This parameter is optional and
392         * defaults to HTML.
393         *
394         * The function first applies hsc_secure() to the input string and then
395         * "unescapes" character entity references and numeric character references
396         * (both decimal and hexadecimal).
397         * Entities are recognized also if the ending semicolon is omitted at the
398         * end or before a newline or tag but for consistency the semicolon is
399         * always added in the output where it was omitted.
400         *
401         * Usage note:
402         * Where code should be rendered <em>as code</em> hsc_secure() should be
403         * used directly so that entity references are also rendered as such instead
404         * of as their corresponding characters.
405         *
406         * Documentation note:
407         * It seems the $doctype parameter was added in 1.1.6.2; version should have
408         * been bumped up to 1.1, and the param documented. We'll assume the updated
409         * version was indeed 1.1, and put this one using hsc_secure() at 1.2 (at
410         * the same time updating the 'XML' doctype with apos as named entity).
411         *
412         * @access      public
413         * @since       Wikka 1.1.6.0
414         * @version     1.2
415         *
416         * @uses        Wakka::hsc_secure()
417         * @param       string  $text required: text to be converted
418         * @param       integer $quote_style optional: quoting style - can be ENT_COMPAT
419         *                      (default, escape only double quotes), ENT_QUOTES (escape both
420         *                      double and single quotes) or ENT_NOQUOTES (don't escape any
421         *                      quotes)
422         * @param       string $doctype 'HTML' (default) or 'XML'; for XML only the XML
423         *                      standard entities are unescaped so we'll have valid XML content
424         * @return      string  converted string with escaped special characted but
425         *                      entity references intact
426         *
427         * @todo        (maybe) recognize valid html entities and only leave those
428         *                      alone, thus transform &error; to &amp;error;
429         * @todo        later - maybe) support full range of situations where (in SGML)
430         *                      a terminating ; may legally be omitted (end, newline and tag are
431         *                      merely the most common ones); such usage is quite rare though
432         *                      and may not be worth the effort
433         */
434        function htmlspecialchars_ent($text,$quote_style=ENT_COMPAT,$doctype='HTML')
435        {
436                // re-establish default if overwritten because of third parameter
437                // [ENT_COMPAT] => 2
438                // [ENT_QUOTES] => 3
439                // [ENT_NOQUOTES] => 0
440                if (!in_array($quote_style,array(ENT_COMPAT,ENT_QUOTES,ENT_NOQUOTES))) {
441                        $quote_style = ENT_COMPAT;
442                }
443
444                // define patterns
445                $terminator = ';|(?=($|[\n<]|&lt;))';   // semicolon; or end-of-string, newline or tag
446                $numdec = '#[0-9]+';                                    // numeric character reference (decimal)
447                $numhex = '#x[0-9a-f]+';                                // numeric character reference (hexadecimal)
448                if ($doctype == 'XML')                                  // pure XML allows only named entities for special chars
449                {
450                        // only valid named entities in XML (case-sensitive)
451                        $named = 'lt|gt|quot|apos|amp';
452                        $ignore_case = '';
453                        $entitystring = $named.'|'.$numdec.'|'.$numhex;
454                }
455                else                                                                    // (X)HTML
456                {
457                        $alpha  = '[a-z]+';                                     // character entity reference TODO $named='eacute|egrave|ccirc|...'
458                        $ignore_case = 'i';                                     // names can consist of upper and lower case letters
459                        $entitystring = $alpha.'|'.$numdec.'|'.$numhex;
460                }
461                $escaped_entity = '&amp;('.$entitystring.')('.$terminator.')';
462
463                // execute our replacement hsc_secure() function, passing on optional parameters
464                $output = $this->hsc_secure($text,$quote_style);
465
466                // "repair" escaped entities
467                // modifiers: s = across lines, i = case-insensitive
468                $output = preg_replace('/'.$escaped_entity.'/s'.$ignore_case,"&$1;",$output);
469
470                // return output
471                return $output;
472        }
473
474        /**
475         * Secure replacement for PHP built-in function htmlspecialchars().
476         *
477         * See ticket #427 (http://wush.net/trac/wikka/ticket/427) for the rationale
478         * for this replacement function.
479         *
480         * The INTERFACE for this function is almost the same as that for
481         * htmlspecialchars(), with the same default for quote style; however, there
482         * is no 'charset' parameter. The reason for this is as follows:
483         *
484         * The PHP docs say:
485         *      "The third argument charset defines character set used in conversion."
486         *
487         * I suspect PHP's htmlspecialchars() is working at the byte-value level and
488         * thus _needs_ to know (or assume) a character set because the special
489         * characters to be replaced could exist at different code points in
490         * different character sets. (If indeed htmlspecialchars() works at
491         * byte-value level that goes some  way towards explaining why the
492         * vulnerability would exist in this function, too, and not only in
493         * htmlentities() which certainly is working at byte-value level.)
494         *
495         * This replacement function however works at character level and should
496         * therefore be "immune" to character set differences - so no charset
497         * parameter is needed or provided. If a third parameter is passed, it will
498         * be silently ignored.
499         *
500         * In the OUTPUT there is a minor difference in that we use '&#39;' instead
501         * of PHP's '&#039;' for a single quote: this provides compatibility with
502         *      get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES)
503         * (see comment by mikiwoz at yahoo dot co dot uk on
504         * http://php.net/htmlspecialchars); it also matches the entity definition
505         * for XML 1.0
506         * (http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters).
507         * Like PHP we use a numeric character reference instead of '&apos;' for the
508         * single quote. For the other special characters we use the named entity
509         * references, as PHP is doing.
510         *
511         * And finally:
512         * The name for this function was basically inspired by waawaamilk (GeSHi),
513         * kindly provided by BenBE (GeSHi), happily acknowledged by WikkaWiki Dev
514         * Team and finally used by JavaWoman. :)
515         *
516         * @author              {@link http://wikkawiki.org/JavaWoman Marjolein Katsma}
517         *
518         * @since               Wikka 1.1.7
519         * @version             1.0
520         * @license             http://www.gnu.org/copyleft/lgpl.html
521         *                              GNU Lesser General Public License
522         * @copyright   Copyright 2007, {@link http://wikkawiki.org/CreditsPage
523         *                              Wikka Development Team}
524         *
525         * @access      public
526         * @param       string  $string string to be converted
527         * @param       integer $quote_style
528         *                      - ENT_COMPAT:   escapes &, <, > and double quote (default)
529         *                      - ENT_NOQUOTES: escapes only &, < and >
530         *                      - ENT_QUOTES:   escapes &, <, >, double and single quotes
531         * @return      string  converted string
532         */
533         function hsc_secure($string, $quote_style=ENT_COMPAT)
534         {
535                // init
536                $aTransSpecchar = array('&' => '&amp;',
537                                                                '"' => '&quot;',
538                                                                '<' => '&lt;',
539                                                                '>' => '&gt;'
540                                                                );                      // ENT_COMPAT set
541                if (ENT_NOQUOTES == $quote_style)       // don't convert double quotes
542                {
543                        unset($aTransSpecchar['"']);
544                }
545                elseif (ENT_QUOTES == $quote_style)     // convert single quotes as well
546                {
547                        $aTransSpecchar["'"] = '&#39;'; // (apos) htmlspecialchars() uses '&#039;'
548                }
549
550                // return translated string
551                return strtr($string,$aTransSpecchar);
552         }
553
554        /**
555         * Get a value provided by user (by get, post or cookie) and sanitize it.
556         * The method is also helpful to disable warning when the value was absent.
557         *
558         * @access      public
559         * @since       Wikka 1.1.7.0
560         * @version     1.0
561         *
562         * @param       string  $varname required: field name on get or post or cookie name
563         * @param       string  $gpc one of get, post, request and cookie. Optional, defaults to request.
564         * @return      string  sanitized value of $_GET[$varname] (or $_POST, $_COOKIE, depending on $gpc)
565         */
566        function GetSafeVar($varname, $gpc='get')
567        {
568                $safe_var = null;
569                if ($gpc == 'post')
570                {
571                        $safe_var = isset($_POST[$varname]) ? $_POST[$varname] : null;
572                }
573                elseif ($gpc == 'get')
574                {
575                        $safe_var = isset($_GET[$varname]) ? $_GET[$varname] : null;
576                }
577                elseif ($gpc == 'cookie')
578                {
579                        $safe_var = isset($_COOKIE[$varname]) ? $_COOKIE[$varname] : null;
580                }
581                return ($this->htmlspecialchars_ent($safe_var));
582        }
583
584        /**
585         * Highlight a code block with GeSHi.
586         *
587         * The path to GeSHi and the GeSHi language files must be defined in the configuration.
588         *
589         * This implementation fits in with general Wikka behavior; e.g., we use classes and an external
590         * stylesheet to render hilighting.
591         *
592         * Apart from this fixed general behavior, WikiAdmin can configure a few behaviors via the
593         * configuration file:
594         * geshi_header                 - wrap code in div (default) or pre
595         * geshi_line_numbers   - disable line numbering, or enable normal or fancy line numbering
596         * geshi_tab_width              - override tab width (default is 8 but 4 is more commonly used in code)
597         *
598         * Limitation: while line numbering is supported, extra GeSHi styling for line numbers is not.
599         * When line numbering is enabled, the end user can "turn it on" by specifying a starting line
600         * number together with the language code in a code block, e.g., (php;260); this number is then
601         * passed as the $start parameter for this method.
602         *
603         * @access      public
604         * @since       wikka 1.1.6.0
605         * @uses        Wakka::config
606         * @uses        GeShi
607         * @todo        support for GeSHi line number styles
608         * @todo        enable error handling
609         *
610         * @param       string  $sourcecode     required: source code to be highlighted
611         * @param       string  $language       required: language spec to select highlighter
612         * @param       integer $start          optional: start line number; if supplied and >= 1 line numbering
613         *                      will be turned on if it is enabled in the configuration.
614         * @return      string  code block with syntax highlighting classes applied
615         */
616        function GeSHi_Highlight($sourcecode, $language, $start=0)
617        {
618                // create GeSHi object
619                include_once($this->config['geshi_path'].'/geshi.php');
620                $geshi =& new GeSHi($sourcecode, $language, $this->config['geshi_languages_path']);                             # create object by reference
621
622                $geshi->enable_classes();                                                               # use classes for hilighting (must be first after creating object)
623                $geshi->set_overall_class('code');                                              # enables using a single stylesheet for multiple code fragments
624
625                // configure user-defined behavior
626                $geshi->set_header_type(GESHI_HEADER_DIV);                              # set default
627                if (isset($this->config['geshi_header']))                               # config override
628                {
629                        if ('pre' == $this->config['geshi_header'])
630                        {
631                                $geshi->set_header_type(GESHI_HEADER_PRE);
632                        }
633                }
634                $geshi->enable_line_numbers(GESHI_NO_LINE_NUMBERS);             # set default
635                if ($start > 0)                                                                                 # line number > 0 _enables_ numbering
636                {
637                        if (isset($this->config['geshi_line_numbers']))         # effect only if enabled in configuration
638                        {
639                                if ('1' == $this->config['geshi_line_numbers'])
640                                {
641                                        $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
642                                }
643                                elseif ('2' == $this->config['geshi_line_numbers'])
644                                {
645                                        $geshi->enable_line_numbers(GESHI_FANCY_LINE_NUMBERS);
646                                }
647                                if ($start > 1)
648                                {
649                                        $geshi->start_line_numbers_at($start);
650                                }
651                        }
652                }
653                if (isset($this->config['geshi_tab_width']))                    # GeSHi override (default is 8)
654                {
655                        $geshi->set_tab_width($this->config['geshi_tab_width']);
656                }
657
658                // parse and return highlighted code
659                // comments added to make GeSHi-highlighted block visible in code JW/20070220
660                return '<!--start GeSHi-->'."\n".$geshi->parse_code()."\n".'<!--end GeSHi-->'."\n";
661        }
662
663        /**
664         * Variable-related methods
665         */
666        function GetPageTag() { return $this->tag; }
667        function GetPageTime() { return $this->page["time"]; }
668        function GetMethod() { return $this->method; }
669        function GetConfigValue($name) { return (isset($this->config[$name])) ? $this->config[$name] : null; }
670        function GetWakkaName() { return $this->GetConfigValue("wakka_name"); }
671        function GetWakkaVersion() { return $this->VERSION; }
672
673        /**
674         * Page-related methods
675         */
676        function LoadPage($tag, $time = "", $cache = 1) {
677                // retrieve from cache
678                if (!$time && $cache) {
679                        $page = isset($this->pageCache[$tag]) ? $this->pageCache[$tag] : null;
680                        if ($page=="cached_nonexistent_page") return null;
681                }
682                // load page
683                if (!isset($page)) $page = $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($tag)."' ".($time ? "and time = '".mysql_real_escape_string($time)."'" : "and latest = 'Y'")." limit 1");
684                // cache result
685                if ($page && !$time) {
686                        $this->pageCache[$page["tag"]] = $page;
687                } elseif (!$page) {
688                        $this->pageCache[$tag] = "cached_nonexistent_page";
689                }
690                return $page;
691        }
692        function IsLatestPage() {
693                return $this->latest;
694        }
695        function GetCachedPage($tag) { return (isset($this->pageCache[$tag])) ? $this->pageCache[$tag] : null; }
696        function CachePage($page) { $this->pageCache[$page["tag"]] = $page; }
697        function SetPage($page) { $this->page = $page; if ($this->page["tag"]) $this->tag = $this->page["tag"]; }
698        function LoadPageById($id) { return $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where id = '".mysql_real_escape_string($id)."' limit 1"); }
699        function LoadRevisions($page) { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($page)."' order by id desc"); }
700        function LoadPagesLinkingTo($tag) { return $this->LoadAll("select from_tag as tag from ".$this->config["table_prefix"]."links where to_tag = '".mysql_real_escape_string($tag)."' order by tag"); }
701        function LoadRecentlyChanged()
702        {
703                if ($pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by id desc"))
704                {
705                        foreach ($pages as $page)
706                        {
707                                $this->CachePage($page);
708                        }
709                        return $pages;
710                }
711        }
712        function LoadWantedPages() { return $this->LoadAll("select distinct ".$this->config["table_prefix"]."links.to_tag as tag,count(".$this->config["table_prefix"]."links.from_tag) as count from ".$this->config["table_prefix"]."links left join ".$this->config["table_prefix"]."pages on ".$this->config["table_prefix"]."links.to_tag = ".$this->config["table_prefix"]."pages.tag where ".$this->config["table_prefix"]."pages.tag is NULL group by ".$this->config["table_prefix"]."links.to_tag order by count desc"); }
713        function IsWantedPage($tag)
714        {
715                if ($pages = $this->LoadWantedPages())
716                {
717                        foreach ($pages as $page)
718                        {
719                                if ($page["tag"] == $tag) return true;
720                        }
721                }
722                return false;
723        }
724        function LoadOrphanedPages() { return $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages left join ".$this->config["table_prefix"]."links on ".$this->config["table_prefix"]."pages.tag = ".$this->config["table_prefix"]."links.to_tag where ".$this->config["table_prefix"]."links.to_tag is NULL order by tag"); }
725        function LoadPageTitles() { return $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages order by tag"); }
726        function LoadAllPages() { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by tag"); }
727        function FullTextSearch($phrase, $caseSensitive = 0)
728        {
729                $id = '';
730                // Should work with any browser/entity conversion scheme
731                $search_phrase = mysql_real_escape_string($phrase);
732                if ( 1 == $caseSensitive ) $id = ', id';
733                $sql  = 'select * from '.$this->config['table_prefix'].'pages';
734                $sql .= ' where latest = '.  "'Y'"  .' and match(tag, body'.$id.')';
735                $sql .= ' against('.  "'$search_phrase'"  .' IN BOOLEAN MODE)';
736                $sql .= ' order by time DESC';
737               
738                $data = $this->LoadAll($sql);
739
740                return $data;
741        }
742        function FullCategoryTextSearch($phrase) { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' and match(body) against('".mysql_real_escape_string($phrase)."' IN BOOLEAN MODE)"); }
743        function SavePage($tag, $body, $note, $owner=null)
744        {
745                // get current user
746                $user = $this->GetUserName();
747
748                // TODO: check write privilege
749                if ($this->HasAccess("write", $tag))
750                {
751                        // If $owner is specified, don't do an owner check
752                        if(empty($owner))
753                        {
754                                // is page new?
755                                if (!$oldPage = $this->LoadPage($tag))
756                                {
757                                        // current user is owner if user is logged in, otherwise, no owner.
758                                        if ($this->GetUser()) $owner = $user;
759                                }
760                                else
761                                {
762                                        // aha! page isn't new. keep owner!
763                                        $owner = $oldPage["owner"];
764                                }
765                        }
766
767                        // set all other revisions to old
768                        $this->Query("update ".$this->config["table_prefix"]."pages set latest = 'N' where tag = '".mysql_real_escape_string($tag)."'");
769
770                        // add new revision
771                        $this->Query("insert into ".$this->config["table_prefix"]."pages set ".
772                                "tag = '".mysql_real_escape_string($tag)."', ".
773                                "time = now(), ".
774                                "owner = '".mysql_real_escape_string($owner)."', ".
775                                "user = '".mysql_real_escape_string($user)."', ".
776                                "note = '".mysql_real_escape_string($note)."', ".
777                                "latest = 'Y', ".
778                                "body = '".mysql_real_escape_string($body)."'");
779
780                        if ($pingdata = $this->GetPingParams($this->config["wikiping_server"], $tag, $user, $note))
781                                $this->WikiPing($pingdata);
782                }
783        }
784        function PageTitle() {
785                $title = "";
786                $pagecontent = $this->page["body"];
787                if (ereg( "(=){3,5}([^=\n]+)(=){3,5}", $pagecontent, $title)) {
788                        $formatting_tags = array("**", "//", "__", "##", "''", "++", "#%", "@@", "\"\"");
789                        $title = str_replace($formatting_tags, "", $title[2]);
790                }
791                if ($title) return strip_tags($this->Format($title));                           # fix for forced links in heading
792                else return $this->GetPageTag();
793        }
794
795        // WIKI PING  -- Coded by DreckFehler
796        function HTTPpost($host, $data, $contenttype="application/x-www-form-urlencoded", $maxAttempts = 5) {
797                $attempt =0; $status = 300; $result = "";
798                while ($status >= 300 && $status < 400 && $attempt++ <= $maxAttempts) {
799                        $url = parse_url($host);
800                        if (isset($url["path"]) == false) $url["path"] = "/";
801                        if (isset($url["port"]) == false) $url["port"] = 80;
802
803                        if ($socket = fsockopen ($url["host"], $url["port"], $errno, $errstr, 15)) {
804                                $strQuery = "POST ".$url["path"]." HTTP/1.1\n";
805                                $strQuery .= "Host: ".$url["host"]."\n";
806                                $strQuery .= "Content-Length: ".strlen($data)."\n";
807                                $strQuery .= "Content-Type: ".$contenttype."\n";
808                                $strQuery .= "Connection: close\n\n";
809                                $strQuery .= $data;
810
811                                // send request & get response
812                                fputs($socket, $strQuery);
813                                $bHeader = true;
814                                while (!feof($socket)) {
815                                        $strLine = trim(fgets($socket, 512));
816                                        if (strlen($strLine) == 0) $bHeader = false; // first empty line ends header-info
817                                        if ($bHeader) {
818                                                if (!$status) $status = $strLine;
819                                                if (preg_match("/^Location:\s(.*)/", $strLine, $matches)) $location = $matches[1];
820                                        } else $result .= trim($strLine)."\n";
821                                }
822                                fclose ($socket);
823                        } else $status = "999 timeout";
824
825                        if ($status) {
826                                if(preg_match("/(\d){3}/", $status, $matches)) $status = $matches[1];
827                        } else $status = 999;
828                        $host = $location;
829                }
830                if (preg_match("/^[\da-fA-F]+(.*)$/", $result, $matches)) $result = $matches[1];
831                return $result;
832        }
833        function WikiPing($ping, $debug = false) {
834                if ($ping) {
835                        $rpcRequest .= "<methodCall>\n";
836                        $rpcRequest .= "<methodName>wiki.ping</methodName>\n";
837                        $rpcRequest .= "<params>\n";
838                        $rpcRequest .= "<param>\n<value>\n<struct>\n";
839                        $rpcRequest .= "<member>\n<name>tag</name>\n<value>".$ping["tag"]."</value>\n</member>\n";
840                        $rpcRequest .= "<member>\n<name>url</name>\n<value>".$ping["taglink"]."</value>\n</member>\n";
841                        $rpcRequest .= "<member>\n<name>wiki</name>\n<value>".$ping["wiki"]."</value>\n</member>\n";
842                        if ($ping["author"]) {
843                                $rpcRequest .= "<member>\n<name>author</name>\n<value>".$ping["author"]."</value>\n</member>\n";
844                                if ($ping["authorpage"]) $rpcRequest .= "<member>\n<name>authorpage</name>\n<value>".$ping["authorpage"]."</value>\n</member>\n";
845                        }
846                        if ($ping["history"]) $rpcRequest .= "<member>\n<name>history</name>\n<value>".$ping["history"]."</value>\n</member>\n";
847                        if ($ping["changelog"]) $rpcRequest .= "<member>\n<name>changelog</name>\n<value>".$this->htmlspecialchars_ent($ping['changelog'],ENT_COMPAT,'XML')."</value>\n</member>\n";
848                        $rpcRequest .= "</struct>\n</value>\n</param>\n";
849                        $rpcRequest .= "</params>\n";
850                        $rpcRequest .= "</methodCall>\n";
851
852                        foreach (explode(" ", $ping["server"]) as $server) {
853                                $response = $this->HTTPpost($server, $rpcRequest, "text/xml");
854                                if ($debug) print $response;
855                        }
856                }
857        }
858        function GetPingParams($server, $tag, $user, $changelog = "") {
859                $ping = array();
860                if ($server) {
861                        $ping["server"] = $server;
862                        if ($tag) $ping["tag"] = $tag; else return false; // set page-title
863                        if (!$ping["taglink"] = $this->Href("", $tag)) return false; // set page-url
864                                if (!$ping["wiki"] = $this->config["wakka_name"]) return false; // set site-name
865                        $ping["history"] = $this->Href("revisions", $tag); // set url to history
866
867                        if ($user) {
868                                $ping["author"] = $user; // set username
869                                if ($this->LoadPage($user)) $ping["authorpage"] = $this->Href("", $user); // set link to user page
870                        }
871                        if ($changelog) $ping["changelog"] = $changelog;
872                        return $ping;
873                } else return false;
874        }
875
876        // COOKIES
877        // Note: Be sure to check the auto login functionality in
878        // setup/install.php if any changes are made to the way session
879        // cookies are set. Since these functions are not yet available
880        // when install.php is called, they must be duplicated in that
881        // file. Changes here without appropriate changes in install.php
882        // may result in login/logout failures! See ticket #800 for more
883        // info.
884        function SetSessionCookie($name, $value) {
885                SetCookie($name.$this->config['wiki_suffix'], $value, 0, $this->wikka_cookie_path); $_COOKIE[$name.$this->config['wiki_suffix']] = $value; $this->cookies_sent = true; }
886        function SetPersistentCookie($name, $value) {
887                SetCookie($name.$this->config['wiki_suffix'], $value, time() + $this->cookie_expiry, $this->wikka_cookie_path); $_COOKIE[$name.$this->config['wiki_suffix']] = $value; $this->cookies_sent = true; }
888        function DeleteCookie($name) {
889                SetCookie($name.$this->config['wiki_suffix'], "", 1, $this->wikka_cookie_path); $_COOKIE[$name.$this->config['wiki_suffix']] = ""; $this->cookies_sent = true; }
890        function GetCookie($name)
891        {
892                if (isset($_COOKIE[$name.$this->config['wiki_suffix']]))
893                {
894                        return $_COOKIE[$name.$this->config['wiki_suffix']];
895                }
896                else
897                {
898                        return FALSE;
899                }
900        }
901       
902        // SESSION
903       
904        /**
905         * Create and store a secret session key.
906         *
907         * Creates a random value and a random field name to be used to pass on the value.
908         * The key,value pair is stored in the session as a serialized array.
909         *
910         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
911         * @copyright   Copyright © 2005, Marjolein Katsma
912         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
913         * @version             0.5
914         *
915         * @access              public
916         *
917         * @param               string  $keyname        required: name under which created secret key should be stored in the session
918         * @return              array                           fieldname and key value.
919         */
920        function createSessionKey($keyname)
921        {
922                // create key and field name for it
923                $key = md5(getmicrotime());
924                $field = 'f'.substr(md5($key.getmicrotime()),0,10);
925                // store session key
926                $_SESSION[$keyname] = serialize(array($field,$key));
927                # BEGIN DEBUG - do not activate on a production server!
928                # echo '<div class="debug">'."\n";
929                # echo 'Session key:<br/>';
930                # echo 'name: '.$keyname.' - field: '.$field.' - key: '.$key.'<br/>';
931                # echo '</div>'."\n";
932                # END DEBUG
933                // return name, value pair
934                return array($field,$key);
935        }
936        /**
937         * Retrieve the secret session key.
938         *
939         * Retrieves a named secret key and returns the result as an array with name,value pair.
940         * Returns FALSE if the key is not found.
941         *
942         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
943         * @copyright   Copyright © 2005, Marjolein Katsma
944         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
945         * @version             0.5
946         *
947         * @access              public
948         *
949         * @param               string  $keyname        required: name of secret key to retrieve from the session
950         * @return              mixed                           array with name,value pair on success, FALSE if entry not found.
951         */
952        function getSessionKey($keyname)
953        {
954                if (!isset($_SESSION[$keyname]))
955                {
956                        return FALSE;
957                }
958                else
959                {
960                        $aKey = unserialize($_SESSION[$keyname]);               # retrieve secret key data
961                        unset($_SESSION[$keyname]);                                             # clear secret key
962                        return $aKey;
963                }
964        }
965        /**
966         * Check hidden session key: it must be passed and it must have the correct name & value.
967         *
968         * Looks for a given name,value pair passed either in POST (default) or in GET request.
969         * Returns TRUE if the correct field and value is found, a reason for failure otherwise.
970         * Make sure to check for identity TRUE (TRUE === returnval), do not evaluate return value
971         * as boolean!!
972         *
973         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
974         * @copyright   Copyright © 2005, Marjolein Katsma
975         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
976         * @version             0.5
977         *
978         * @access              public
979         * @todo                - prepare strings for internationalization
980         *
981         * @param               array   $aKey   required: fieldname, key value pair.
982         * @param               string  $method optional: form method; default post;
983         * @return              mixed                   TRUE if correct name,value found; reason for failure otherwise.
984         */
985        function hasValidSessionKey($aKey, $method='post')
986        {
987                // get pair to look for
988                list($ses_field,$ses_key) = $aKey;
989                // check method and prepare what to look for
990                if (isset($method))
991                {
992                        $aServervars = ($method == 'get') ? $_GET : $_POST;
993                }
994                else
995                {
996                        $aServervars = $_POST;                                  # default
997                }
998       
999                // check passed values
1000                if (!isset($aServervars[$ses_field]))
1001                {
1002                        return 'form no key';                                   # key not present
1003                }
1004                elseif ($aServervars[$ses_field] != $ses_key)
1005                {
1006                        return 'form bad key';                                  # incorrect value passed
1007                }
1008                else
1009                {
1010                        return TRUE;                                                    # all is well
1011                }
1012        }
1013
1014        // HTTP/GET/POST/LINK RELATED
1015
1016        function SetRedirectMessage($message) { $_SESSION["redirectmessage"] = $message; }
1017        function GetRedirectMessage() { $message = $_SESSION["redirectmessage"]; $_SESSION["redirectmessage"] = ""; return $message; }
1018        /**
1019         * Performs a redirection to another page.
1020         *
1021         * On IIS server, and if the page had sent any cookies, the redirection must not be performed
1022         * by using the 'Location:' header: We use meta http-equiv OR javascript OR link (Credits MarceloArmonas)
1023         * @author {@link http://wikkawiki.org/DotMG Mahefa Randimbisoa} (added IIS support)
1024         * @access      public
1025         * @since       Wikka 1.1.6.2
1026         *
1027         * @param       string  $url: destination URL; if not specified redirect to the same page.
1028         * @param       string  $message: message that will show as alert in the destination URL
1029         */
1030        function Redirect($url='', $message='')
1031        {
1032                if ($message != '') $_SESSION["redirectmessage"] = $message;
1033                $url = ($url == '' ) ? $this->config['base_url'].$this->tag : $url;
1034                if ((eregi('IIS', $_SERVER["SERVER_SOFTWARE"])) && ($this->cookies_sent))
1035                {
1036                        @ob_end_clean();
1037                        die('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1038<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head><title>Redirected to '.$this->Href($url).'</title>'.
1039'<meta http-equiv="refresh" content="0; url=\''.$url.'\'" /></head><body><div><script type="text/javascript">window.location.href="'.$url.'";</script>'.
1040'</div><noscript>If your browser does not redirect you, please follow <a href="'.$this->Href($url).'">this link</a></noscript></body></html>');
1041                }
1042                else
1043                {
1044                        header("Location: ".$url);
1045                }
1046                exit;
1047        }
1048        // returns just PageName[/method].
1049        function MiniHref($method = "", $tag = "") { if (!$tag = trim($tag)) $tag = $this->tag; return $tag.($method ? "/".$method : ""); }
1050        // returns the full url to a page/method.
1051        function Href($method = "", $tag = "", $params = "")
1052        {
1053                $href = $this->config["base_url"].$this->MiniHref($method, $tag);
1054                if ($params)
1055                {
1056                        $href .= ($this->config["rewrite_mode"] ? "?" : "&amp;").$params;
1057                }
1058                return $href;
1059        }
1060        /**
1061         * Creates a link from Wikka markup.
1062         *
1063         * Beware of the $title parameter: quotes and backslashes should be previously
1064         * escaped before the title is passed to this method.
1065         *
1066         * @access      public
1067         *
1068         * @uses        Wakka::GetInterWikiUrl()
1069         * @uses        Wakka::Href()
1070         * @uses        Wakka::htmlspecialchars_ent()
1071         * @uses        Wakka::LoadPage()
1072         * @uses        Wakka::TrackLinkTo()
1073         * @uses        Wakka::existsPage()
1074         *
1075         * @param       mixed   $tag            mandatory:
1076         * @param       string  $handler        optional:
1077         * @param       string  $text           optional:
1078         * @param       boolean $track          optional:
1079         * @param       boolean $escapeText     optional:
1080         * @param       string  $title          optional:
1081         * @param       string  $class          optional:
1082         * @return      string  an HTML hyperlink (a href) element
1083         * @todo        move regexps to regexp-library          #34
1084         */
1085        function Link($tag, $handler='', $text='', $track=TRUE, $escapeText=TRUE, $title='', $class='')
1086        {
1087                // init
1088                if (!$text)
1089                {
1090                        $text = $tag;
1091                }
1092                if ($escapeText)        // escape text?
1093                {
1094                        $text = $this->htmlspecialchars_ent($text);
1095                }
1096                $tag = $this->htmlspecialchars_ent($tag); #142 & #148
1097                $handler = $this->htmlspecialchars_ent($handler);
1098                $title_attr = $title ? ' title="'.$this->htmlspecialchars_ent($title).'"' : '';
1099                $url = '';
1100                $wikilink = '';
1101
1102                // is this an interwiki link?
1103                // before the : should be a WikiName; anything after can be (nearly) anything that's allowed in a URL
1104                if (preg_match('/^([A-ZÄÖÜ][A-Za-zÄÖÜßäöü]+)[:](\S*)$/', $tag, $matches))       // @@@ FIXME #34 (inconsistent with Formatter)
1105                {
1106                        $url = $this->GetInterWikiUrl($matches[1], $matches[2]);
1107                        $class = 'interwiki';
1108                }
1109                // fully-qualified URL? this uses the same pattern as StaticHref() does;
1110                // it's a recognizing pattern, not a validation pattern
1111                // @@@ move to regex libary!
1112                elseif (preg_match('/^(http|https|ftp|news|irc|gopher):\/\/([^\\s\"<>]+)$/', $tag))
1113                {
1114                        $url = $tag; // this is a valid external URL
1115                        // add ext class only if URL is external
1116                        if (!preg_match('/'.$_SERVER['SERVER_NAME'].'/', $tag))
1117                        {
1118                                $class = 'ext';
1119                        }
1120                }
1121                // is this a full link? i.e., does it contain something *else* than valid WikiName characters?
1122                // FIXME just use (!IsWikiName($tag)) here (then fix the RE there!)
1123                // @@@ First move to regex library
1124                elseif (preg_match('/[^[:alnum:]ÄÖÜßäöü]/', $tag))              // FIXED #34 - removed commas
1125                {
1126                        // check for email addresses
1127                        if (preg_match('/^.+\@.+$/', $tag))
1128                        {
1129                                $url = 'mailto:'.$tag;
1130                                $class = 'mailto';
1131                        }
1132                        // check for protocol-less URLs
1133                        elseif (!preg_match('/:/', $tag))
1134                        {
1135                                $url = 'http://'.$tag;
1136                                $class = 'ext';
1137                        }
1138                }
1139                else
1140                {
1141                        // it's a wiki link
1142                        if (isset($_SESSION['linktracking']) && $_SESSION['linktracking'] && $track)
1143                        {
1144                                $this->TrackLinkTo($tag);
1145                        }
1146                        //$linkedPage = $this->LoadPage($tag);
1147                        // return ($linkedPage ? '<a class="'.$class.'" href="'.$this->Href($handler, $linkedPage['tag']).'"'.$title_attr.'>'.$text.'</a>' : '<a class="missingpage" href="'.$this->Href("edit", $tag).'" title="'.CREATE_THIS_PAGE_LINK_TITLE.'">'.$text.'</a>'); #i18n
1148                        // MODIFIED to use existsPage() (more efficient!)
1149                        if (!$this->existsPage($tag))
1150                        {
1151                                $link = '<a class="missingpage" href="'.$this->Href('edit', $tag).'" title="'.CREATE_THIS_PAGE_LINK_TITLE.'">'.$text.'</a>';
1152                        }
1153                        else
1154                        {
1155                                $link = '<a class="'.$class.'" href="'.$this->Href($handler, $tag).'"'.$title_attr.'>'.$text.'</a>';
1156                        }
1157                }
1158
1159                //return $url ? '<a class="'.$class.'" href="'.$url.'">'.$text.'</a>' : $text;
1160                if ('' != $url)
1161                {
1162                        $result = '<a class="'.$class.'" href="'.$url.'">'.$text.'</a>';
1163                }
1164                elseif ('' != $link)
1165                {
1166                        $result = $link;
1167                }
1168                else
1169                {
1170                        $result = $text;
1171                }
1172                return $result;
1173        }
1174        // function PregPageLink($matches) { return $this->Link($matches[1]); }
1175        function IsWikiName($text) { return preg_match("/^[A-Z,ÄÖÜ][a-z,ßäöü]+[A-Z,0-9,ÄÖÜ][A-Z,a-z,0-9,ÄÖÜ,ßäöü]*$/", $text); }
1176        function TrackLinkTo($tag) { $_SESSION["linktable"][] = $tag; }
1177        function GetLinkTable() { return $_SESSION["linktable"]; }
1178        function ClearLinkTable() { $_SESSION["linktable"] = array(); }
1179        function StartLinkTracking() { $_SESSION["linktracking"] = 1; }
1180        function StopLinkTracking() { $_SESSION["linktracking"] = 0; }
1181        function WriteLinkTable()
1182        {
1183                // delete old link table
1184                $this->Query("delete from ".$this->config["table_prefix"]."links where from_tag = '".mysql_real_escape_string($this->GetPageTag())."'");
1185                // build new link table
1186                if ($linktable = $this->GetLinkTable())
1187                {
1188                        $from_tag = mysql_real_escape_string($this->GetPageTag());
1189                        $written = array();
1190                        foreach ($linktable as $to_tag)
1191                        {
1192                                $lower_to_tag = strtolower($to_tag);
1193                                if (!$written[$lower_to_tag])
1194                                {
1195                                        $this->Query("insert into ".$this->config["table_prefix"]."links set from_tag = '".$from_tag."', to_tag = '".mysql_real_escape_string($to_tag)."'");
1196                                        $written[$lower_to_tag] = 1;
1197                                }
1198                        }
1199                }
1200        }
1201        function Header() {
1202                $header = $this->IncludeBuffered('header.php', ERROR_HEADER_MISSING, '',  $this->GetConfigValue('wikka_template_path'));
1203                return $header;
1204        }
1205        function Footer() {
1206                $footer = $this->IncludeBuffered('footer.php', ERROR_FOOTER_MISSING, '', $this->GetConfigValue('wikka_template_path'));
1207                return $footer;
1208        }
1209
1210        // FORMS
1211        /**
1212         * Open form.
1213         *
1214         * @uses        Wakka::GetConfigValue()
1215         *
1216         * @todo        replace with advanced FormOpen (so IDs are generated, among other things!)
1217         * @todo        check if the hidden field is still needed - Href() already provides
1218         *                      the wakka= part of the URL... everything seems to work fine with
1219         *                      or without rewrite mode, and without this hidden field!
1220         */
1221        /* replaced by http://wikkawiki.org/AdvancedFormOpen
1222        function FormOpen($method = "", $tag = "", $formMethod = "post")
1223        {
1224                $result = "<form action=\"".$this->Href($method, $tag)."\" method=\"".$formMethod."\">\n";
1225                if (!$this->config["rewrite_mode"]) $result .= "<input type=\"hidden\" name=\"wakka\" value=\"".$this->MiniHref($method, $tag)."\" />\n";
1226                return $result;
1227        }
1228        */
1229        /**
1230         * Build an opening form tag with specified or generated attributes.
1231         *
1232         * This method builds an opening form tag, taking care that the result is valid XHTML
1233         * no matter where the parameters come from: invalid parameters are ignored and defaults used.
1234         * This enables this method to be used with user-provided parameter values.
1235         *
1236         * The form will always have the required action attribute and an id attribute to provide
1237         * a 'hook' for styling and scripting. This method tries its best to ensure the id attribute
1238         * is unique, among other things by adding a 'form_' prefix to make it different from ids for
1239         * other elements.
1240         * For a file upload form ($file=TRUE) the appropriate method and enctype attributes are generated.
1241         *
1242         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman} (Advanced version: complete rewrite; 2005)
1243         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1244         *
1245         * @access      public
1246         * @uses        ID_LENGTH
1247         * @uses        Wakka::makeId()
1248         * @uses        Wakka::existsHandler()
1249         * @uses        Wakka::existsPage()
1250         * @uses        Wakka::Href()
1251         * @uses        Wakka::MiniHref()       only for hidden field
1252         *
1253         * @param       string  $handler        optional: "handler" which consists of handler name and possibly a query string
1254         *                                                              to be used as part of action attribute
1255         * @param       string  $tag            optional: page name to be used for action attribute;
1256         *                                                              if not specified, the current page will be used
1257         * @param       string  $formMethod     optional: method attribute; must be POST (default) or GET;
1258         *                                                              anything but POST is ignored and considered as GET;
1259         *                                                              always converted to lowercase
1260         * @param       string  $id                     optional: id attribute
1261         * @param       string  $class          optional: class attribute
1262         * @param       boolean $file           optional: specifies whether there will be a file upload field;
1263         *                                                              default: FALSE; if TRUE sets method attribute to POST and generates
1264         *                                                              appropriate enctype attribute
1265         * @return      string opening form tag
1266         * @todo        extend to handle a complete (external) URL instead of (handler+)pagename
1267         * @todo        extend to allow extra attributes
1268         */
1269        function FormOpen($handler='', $tag='', $formMethod='post', $id='', $class='', $file=FALSE)
1270        {
1271                // init
1272                $attrMethod = '';                                                                       // no method for HTML default 'get'
1273                $attrClass = '';
1274                $attrEnctype = '';                                                                      // default no enctype -> HTML default application/x-www-form-urlencoded
1275                $hidden = array();
1276                // derivations
1277                $handler = trim($handler);
1278                $tag = trim($tag);
1279                $id = trim($id);
1280                $class = trim($class);
1281                // validations (needed only if parameters are actually specified)
1282                if (!empty($handler) && !$this->existsHandler($handler))
1283                {
1284                        $handler = '';
1285                }
1286                if (!empty($tag) && !$this->existsPage($tag))   // name change, interface change (check for active page only)
1287                {
1288                        $tag = '';      // Href() will pick up current page name if none specified
1289                }
1290
1291                // form action (action is a required attribute!)
1292                // !!! If rewrite mode is off, "tag" has to be passed as a hidden field
1293                // rather than part of the URL (where it gets ignored on submit!)
1294                if ($this->GetConfigValue('rewrite_mode'))
1295                {
1296                        // @@@ add passed extra GET params here by passing them as extra
1297                        // parameter to Href()
1298                        $attrAction = ' action="'.$this->Href($handler, $tag).'"';
1299                }
1300                else
1301                {
1302                        $attrAction = ' action="'.$this->Href($handler, $tag).'"';
1303                        // #670: This value will short-circuit the value of wakka=... in URL.
1304                        $hidden['wakka'] = $this->MiniHref($handler, ('' == $tag ? $this->GetPageTag(): $tag));
1305                        // @@@ add passed extra GET params here by adding them as extra
1306                        // entries to $hidden (probably not by adding them to Href()
1307                        // but that needs to be tested when we get to it!)
1308                }
1309                // form method (ignore anything but post) and enctype
1310                if (TRUE === $file)
1311                {
1312                        $attrMethod  = ' method="post"';                                // required for file upload
1313                        $attrEnctype = ' enctype="multipart/form-data"';// required for file upload
1314                }
1315                elseif (preg_match('/^post$/i',$formMethod))            // ignore case...
1316                {
1317                        $attrMethod = ' method="post"';                                 // ...but generate lowercase
1318                }
1319                // form id
1320                if ('' == $id)                                                                          // if no id given, generate one based on other parameters
1321                {
1322                        $id = substr(md5($handler.$tag.$formMethod.$class),0,ID_LENGTH);
1323                }
1324                $attrId = ' id="'.$this->makeId('form',$id).'"';        // make sure we have a unique id
1325                // form class
1326                if ('' != $class)
1327                {
1328                        $attrClass = ' class="'.$class.'"';
1329                }
1330               
1331                // add validation key fields
1332                if('post' == $formMethod)
1333                {
1334                        $tmp = $this->createSessionKey($id);
1335                        $hidden[$tmp[0]] = $tmp[1];
1336                        unset($tmp);
1337                        $hidden['form_id'] = $id;       
1338                }
1339               
1340                // build HTML fragment
1341                $fragment = '<form'.$attrAction.$attrMethod.$attrEnctype.$attrId.$attrClass.'>'."\n";
1342                // construct and add hidden fields (necessary if we are NOT using rewrite mode)
1343                if (count($hidden) > 0)
1344                {
1345                        $fragment .= '<fieldset class="hidden">'."\n";
1346                        foreach ($hidden as $name => $value)
1347                        {
1348                                $fragment .= '  <input type="hidden" name="'.$name.'" value="'.$value.'" />'."\n";
1349                        }
1350                        $fragment .= '</fieldset>'."\n";
1351                }
1352
1353                // return resulting HTML fragment
1354                return $fragment;
1355        }
1356        /**
1357         * Close form
1358         *
1359         * @return      string  the XHTML tag to close a form and a newline.
1360         */
1361        function FormClose()
1362        {
1363                $result = '</form>'."\n";
1364                return $result;
1365        }
1366
1367        // INTERWIKI STUFF
1368        function ReadInterWikiConfig()
1369        {
1370                if ($lines = file("interwiki.conf"))
1371                {
1372                        foreach ($lines as $line)
1373                        {
1374                                if ($line = trim($line))
1375                                {
1376                                        list($wikiName, $wikiUrl) = explode(" ", trim($line));
1377                                        $this->AddInterWiki($wikiName, $wikiUrl);
1378                                }
1379                        }
1380                }
1381        }
1382        function AddInterWiki($name, $url)
1383        {
1384                $this->interWiki[strtolower($name)] = $url;
1385        }
1386        function GetInterWikiUrl($name, $tag) {
1387                if (isset($this->interWiki[strtolower($name)]))
1388                {
1389                        return $this->interWiki[strtolower($name)].$tag;
1390                }
1391        }
1392
1393        // REFERRERS
1394        function LogReferrer($tag='', $referrer='')
1395        {
1396                // fill values
1397                if (!$tag = trim($tag))
1398                {
1399                        #$tag = $this->GetPageTag();
1400                        $tag = $this->tag;
1401                }
1402                #if (!$referrer = trim($referrer)) $referrer = $_SERVER["HTTP_REFERER"]; NOTICE
1403                if (empty($referrer))
1404                {
1405                        $referrer = (isset($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : '';
1406                }
1407                $referrer = trim($this->cleanUrl($referrer));                   # secured JW 2005-01-20
1408
1409                // check if it's coming from another site
1410                #if ($referrer && !preg_match('/^'.preg_quote($this->GetConfigValue('base_url'), '/').'/', $referrer))
1411                if (!empty($referrer) && !preg_match('/^'.preg_quote($this->GetConfigValue('base_url'), '/').'/', $referrer))
1412                {
1413                        $parsed_url = parse_url($referrer);
1414                        $spammer = $parsed_url['host'];
1415                        $blacklist = $this->LoadSingle("
1416                                SELECT *
1417                                FROM ".$this->GetConfigValue('table_prefix')."referrer_blacklist
1418                                WHERE spammer = '".mysql_real_escape_string($spammer)."'"
1419                                );
1420                        if (FALSE == $blacklist)
1421                        {
1422                                $this->Query("
1423                                        INSERT INTO ".$this->GetConfigValue('table_prefix')."referrers
1424                                        SET page_tag    = '".mysql_real_escape_string($tag)."',
1425                                                referrer        = '".mysql_real_escape_string($referrer)."',
1426                                                time            = now()"
1427                                        );
1428                        }
1429                }
1430        }
1431        function LoadReferrers($tag = "")
1432        {
1433                $where = ($tag = trim($tag)) ? "                        WHERE page_tag = '".mysql_real_escape_string($tag)."'" : '';
1434                $referrers = $this->LoadAll("
1435                        SELECT referrer, COUNT(referrer) AS num
1436                        FROM ".$this->GetConfigValue('table_prefix')."referrers".
1437                        $where."
1438                        GROUP BY referrer
1439                        ORDER BY num DESC"
1440                        );
1441                return $referrers;
1442        }
1443
1444        // SANITY CHECKS
1445
1446        /**
1447         * Check by name if a page exists.
1448         *
1449         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
1450         * @copyright   Copyright © 2004, Marjolein Katsma
1451         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1452         * @version             1.1
1453         *
1454         * NOTE: v. 1.0 -> 1.1
1455         *              - name changed from ExistsPage() to existsPage() !!!
1456         *              - added $prefix param so it can be used from installer
1457         *              - added $current param so it checks by default for a current page only
1458         *
1459         * @access      public
1460         * @uses        Query()
1461         *
1462         * @param       string  $page  page name to check
1463         * @param       string  $prefix optional: table prefix to use
1464         *                                      pass NULL if you need to override the $active parameter
1465         *                                      default: prefix as in configuration file
1466         * @param       mixed   $dblink optional: connection resource, or NULL to get
1467         *                                      object's connection
1468         * @param       string  $active optional: if TRUE, check for actgive page only
1469         *                                      default: TRUE
1470         * @return      boolean TRUE if page exists, FALSE otherwise
1471         */
1472        function existsPage($page, $prefix='', $dblink=NULL, $active=TRUE)
1473        {
1474                // init
1475                $count = 0;
1476                $table_prefix = (empty($prefix) && isset($this)) ? $this->config['table_prefix'] : $prefix;
1477                if (is_null($dblink))
1478                {
1479                        $dblink = $this->dblink;
1480                }
1481                // build query
1482                $query = "SELECT COUNT(tag)
1483                                FROM ".$table_prefix."pages
1484                                WHERE tag='".mysql_real_escape_string($page)."'";
1485                if ($active)
1486                {
1487                        $query .= "             AND latest='Y'";
1488                }
1489                // do query
1490                if ($r = Wakka::Query($query, $dblink))
1491                {
1492                        $count = mysql_result($r,0);
1493                        mysql_free_result($r);
1494                }
1495                // report
1496                return ($count > 0) ? TRUE : FALSE;
1497        }
1498        /**
1499         * Check if a handler (specified after page name) really exists.
1500         *
1501         * May be passed as handler plus query string; we'll need to look at handler only
1502         * so we strip off any querystring first.
1503         *
1504         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman} (created 2005; rewrite 2007)
1505         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1506         *
1507         * @access      public
1508         * @uses        Wakka::GetConfigValue()
1509         *
1510         * @param       string  $handler        handler name, optionally with appended parameters
1511         * @return      boolean TRUE if handler is found, FALSE otherwise
1512         */
1513        function existsHandler($handler)
1514        {
1515                // first strip off any query string
1516                $parts = preg_split('/&/',$handler,1);                          # return only one part
1517                $handler = $parts[0];
1518#echo 'handler: '.$handler.'<br/>';
1519                // now check if a handler by that name exists
1520#echo 'checking path: '.$this->GetConfigValue('handler_path').DIRECTORY_SEPARATOR.'page'.DIRECTORY_SEPARATOR.$handler.'.php'.'<br/>';
1521                $exists = $this->BuildFullpathFromMultipath('page'.DIRECTORY_SEPARATOR.$handler.'.php', $this->GetConfigValue('handler_path')); 
1522                // return conclusion
1523                if(TRUE===empty($exists)) 
1524                { 
1525                        return FALSE; 
1526                } 
1527                return TRUE; 
1528        }
1529
1530        // PLUGINS
1531
1532        function Action($actionspec, $forceLinkTracking = 0)
1533        {
1534                // parse action spec and check if we have a syntactically valid action name     [SEC]
1535                // allows action name consisting of letters and numbers ONLY
1536                // and thus provides defense against directory traversal or XSS
1537                if (!preg_match('/^\s*([a-zA-Z0-9]+)(\s.+?)?\s*$/', $actionspec, $matches))     # see also #34
1538                {
1539                        return '<em class="error">Unknown action; the action name must not contain special characters.</em>';   # [SEC]
1540                }
1541                else
1542                {
1543                        // valid action name, so we pull out the parts
1544                        $action_name    = strtolower($matches[1]);
1545                        $paramlist              = (isset($matches[2])) ? trim($matches[2]) : '';
1546                }
1547
1548                // prepare an array for extract() (in $this->IncludeBuffered()) to work with
1549                $vars = array();
1550                // search for parameters if there was more than just a (syntactically valid) action name
1551                if ('' != $paramlist)
1552                {
1553                        // match all attributes (key and value)
1554                        preg_match_all('/([a-zA-Z0-9]+)=(\"|\')(.*)\\2/U', $paramlist, $matches);       # [SEC] parameter name should not be empty
1555
1556                        // prepare an array for extract() (in $this->IncludeBuffered()) to work with
1557                        #$vars = array();
1558                        if (is_array($matches))
1559                        {
1560                                for ($a = 0; $a < count($matches[0]); $a++)
1561                                {
1562                                        // parameter value is sanitized using htmlspecialchars_ent(); if an
1563                                        // action really needs "raw" HTML as input it can still be "unescaped"by the action
1564                                        // itself; for any other action this guards against XSS or directory traversal
1565                                        // via user-supplied action parameters. Any HTML will be displayed _as code_,
1566                                        // but not interpreted.
1567                                        $vars[$matches[1][$a]] = $this->htmlspecialchars_ent($matches[3][$a]);  // parameter name = sanitized value [SEC]
1568                                }
1569                        }
1570                        $vars['wikka_vars'] = $paramlist; // <<< add the complete parameter-string to the array
1571                }
1572                if (!$forceLinkTracking) $this->StopLinkTracking();
1573
1574                $result = $this->IncludeBuffered($action_name.'.php', 'Unknown action "'.$action_name.'"', $vars, $this->config['action_path']);
1575                $this->StartLinkTracking();
1576                return $result;
1577        }
1578        function Method($method)
1579        {
1580                if (strstr($method, '/'))
1581                {
1582                        # Observations - MK 2007-03-30
1583                        # extract part after the last slash (if the whole request contained multiple slashes)
1584                        # TODO:
1585                        # but should such requests be accepted in the first place?
1586                        # at least it is a SORT of defense against directory traversal (but not necessarily XSS)
1587                        # NOTE that name syntax check now takes care of XSS
1588                        $method = substr($method, strrpos($method, '/')+1);
1589                }
1590                // check valid method name syntax (similar to Action())
1591                if (!preg_match('/^([a-zA-Z0-9_.-]+)$/', $method)) // allow letters, numbers, underscores, dashes and dots only (for now); see also #34
1592                {
1593                        return '<em class="error">Unknown method; the method name must not contain special characters.</em>';   # [SEC]
1594                }
1595                else
1596                {
1597                        // valid method name; now make sure it's lower case
1598                        $method = strtolower($method);
1599                }
1600                if (!$handler = $this->page['handler']) $handler = 'page';      # there are no other handlers (yet)
1601                $methodLocation = $handler.DIRECTORY_SEPARATOR.$method.'.php';  #89
1602                return $this->IncludeBuffered($methodLocation, 'Unknown method "'.$methodLocation.'"', '', $this->config['handler_path']);
1603        }
1604        function Format($text, $formatter='wakka')
1605        {
1606                // check valid formatter name syntax (similar to Action())
1607                if (!preg_match('/^([a-zA-Z0-9_.-]+)$/', $formatter)) // allow letters, numbers, underscores, dashes and dots only (for now); see also #34
1608                {
1609                        return '<em class="error">Unknown formatter; the formatter name must not contain special characters.</em>';     # [SEC]
1610                }
1611                else
1612                {
1613                        // valid method name; now make sure it's lower case
1614                        $formatter      = strtolower($formatter);
1615                }
1616                return $this->IncludeBuffered($formatter.'.php', 'Formatter "'.$formatter.'" not found', compact("text"), $this->config['wikka_formatter_path']);
1617        }
1618
1619        /**
1620         * Build a (possibly valid) filepath from a delimited list of paths 
1621         *
1622         * This function takes a list of paths delimited by ":"
1623         * (Unix-style), ";" (Window-style), or "," (Wikka-style)  and
1624         * attempts to construct a fully-qualified pathname to a specific
1625         * file.  By default, this function checks to see if the file
1626         * pointed to by the fully-qualified pathname exists.  First valid
1627         * match wins.  Disabling this feature will return the first valid
1628         * constructed path (i.e, a path containing a valid directory, but
1629         * not necessarily pointing to an existant file).
1630         * 
1631         * @param string $filename mandatory: filename to be used in
1632         *              construction of fully-qualified filepath 
1633         * @param string $pathlist mandatory: list of
1634         *              paths (delimited by ":", ";", or ",")
1635         * @param  boolean $checkIfFileExists optional: if TRUE, returns
1636         *              only a pathname that points to a file that exists
1637         *              (default)
1638         * @return string A fully-qualified pathname or NULL if none found
1639         */ 
1640        function BuildFullpathFromMultipath($filename, $pathlist, $checkIfFileExists=TRUE) 
1641        { 
1642                $paths = preg_split('/;|:|,/', $pathlist); 
1643                if(empty($paths[0])) return NULL; 
1644                if(FALSE === $checkIfFileExists) 
1645                { 
1646                        // Just return first directory that exists
1647                        foreach($paths as $path) 
1648                        { 
1649                                $path = trim($path); 
1650                                if(file_exists($path)) 
1651                                { 
1652                                                return $path.DIRECTORY_SEPARATOR.$filename; 
1653                                } 
1654                        } 
1655                        return NULL; 
1656                } 
1657                foreach($paths as $path) 
1658                { 
1659                        $path = trim($path); 
1660                        $fqfn = $path.DIRECTORY_SEPARATOR.$filename; 
1661                        if(file_exists($fqfn)) return $fqfn; 
1662                } 
1663                return NULL; 
1664        } 
1665
1666        // USERS
1667        function LoadUser($name, $password = 0) { return $this->LoadSingle("select * from ".$this->config['table_prefix']."users where name = '".mysql_real_escape_string($name)."' ".($password === 0 ? "" : "and password = '".mysql_real_escape_string($password)."'")." limit 1"); }
1668        function LoadUsers() { return $this->LoadAll("select * from ".$this->config['table_prefix']."users order by name"); }
1669        function GetUserName() { if ($user = $this->GetUser()) $name = $user["name"]; else if (!$name = gethostbyaddr($_SERVER["REMOTE_ADDR"])) $name = $_SERVER["REMOTE_ADDR"]; return $name; }
1670        function GetUser() { return (isset($_SESSION["user"])) ? $_SESSION["user"] : NULL; }
1671        function SetUser($user) { $_SESSION["user"] = $user; $this->SetPersistentCookie("user_name", $user["name"]); $this->SetPersistentCookie("pass", $user["password"]); }
1672        function LogoutUser() 
1673        { 
1674                $this->DeleteCookie("user_name"); 
1675                $this->DeleteCookie("pass"); 
1676                // Delete this session from sessions table
1677                $this->Query("DELETE FROM ".$this->config['table_prefix']."sessions WHERE userid='".$this->GetUserName()."' AND sessionid='".session_id()."'");
1678                $_SESSION["user"] = ""; 
1679                // This seems a good as place as any to purge all session records
1680                // older than PERSISTENT_COOKIE_EXPIRY, as this is not a
1681                // time-critical function for the user.  The assumption here
1682                // is that  server-side sessions have long ago been cleaned up by PHP.
1683                $this->Query("DELETE FROM ".$this->config['table_prefix']."sessions WHERE DATE_SUB(NOW(), INTERVAL ".PERSISTENT_COOKIE_EXPIRY." SECOND) > session_start");
1684        }
1685        function UserWantsComments() { if (!$user = $this->GetUser()) return false; return ($user["show_comments"] == "Y"); }
1686
1687
1688        // COMMENTS
1689        /**
1690         * Load the comments for a (given) page.
1691         *
1692         * @uses        Wakka::LoadAll()
1693         * @param       string $tag mandatory: name of the page
1694         * @return      array all the comments for this page
1695         */
1696        function LoadComments($tag) { return $this->LoadAll("SELECT * FROM ".$this->config["table_prefix"]."comments WHERE page_tag = '".mysql_real_escape_string($tag)."' ORDER BY time"); }
1697        /**
1698         * Load the last 50 comments on the wiki.
1699         *
1700         * @uses        Wakka::LoadAll()
1701         * @param       integer $limit optional: number of last comments. default: 50
1702         * @param   string $user optional: list only comments by this user
1703         * @return      array the last x comments
1704         */
1705        function LoadRecentComments($limit = 50, $user = '') 
1706        { 
1707                $where = '';
1708                if(!empty($user) && 
1709                   ($this->GetUser() || $this->IsAdmin()))
1710                {
1711                        $where = " where user = '".mysql_real_escape_string($user)."' ";
1712                }
1713                return $this->LoadAll("SELECT * FROM ".$this->config["table_prefix"]."comments $where ORDER BY time DESC LIMIT ".intval($limit)); 
1714        }
1715        /**
1716         * Load the last 50 comments on different pages on the wiki.
1717         *
1718         * @uses        Wakka::LoadAll()
1719         * @param       integer $limit optional: number of last comments on different pages. default: 50
1720         * @param   string $user optional: list only comments by this user
1721         * @return      array the last x comments on different pages
1722         */
1723        function LoadRecentlyCommented($limit = 50, $user = '')
1724        {
1725                $where = ' and 1 ';
1726                if(!empty($user) && 
1727                   ($this->GetUser() || $this->IsAdmin()))
1728                {
1729                        $where = " and comments.user = '".mysql_real_escape_string($user)."' ";
1730                }
1731
1732                $sql = "SELECT comments.id, comments.page_tag, comments.time, comments.comment, comments.user"
1733                        . " FROM ".$this->config["table_prefix"]."comments AS comments"
1734                        . " LEFT JOIN ".$this->config["table_prefix"]."comments AS c2 ON comments.page_tag = c2.page_tag AND comments.id < c2.id"
1735                        . " WHERE c2.page_tag IS NULL "
1736                        . $where
1737                        . " ORDER BY time DESC "
1738                        . " LIMIT ".intval($limit);
1739                return $this->LoadAll($sql);
1740        }
1741        /**
1742         * Save a (given) comment for a (given) page.
1743         *
1744         * @uses        Wakka::GetUserName()
1745         * @uses        Wakka::Query()
1746         * @param       string $page_tag mandatory: name of the page
1747         * @param       string $comment mandatory: text of the comment
1748         */
1749        function SaveComment($page_tag, $comment)
1750        {
1751                // get current user
1752                $user = $this->GetUserName();
1753
1754                // add new comment
1755                $this->Query("INSERT INTO ".$this->config["table_prefix"]."comments SET ".
1756                        "page_tag = '".mysql_real_escape_string($page_tag)."', ".
1757                        "time = now(), ".
1758                        "comment = '".mysql_real_escape_string($comment)."', ".
1759                        "user = '".mysql_real_escape_string($user)."'");
1760        }
1761
1762        // ACCESS CONTROL
1763        /**
1764         * Check if current user is the owner of the current or a specified page.
1765         *
1766         * @access              public
1767         * @uses                Wakka::GetPageOwner()
1768         * @uses                Wakka::GetPageTag()
1769         * @uses                Wakka::GetUser()
1770         * @uses                Wakka::GetUserName()
1771         * @uses                Wakka::IsAdmin()
1772         *
1773         * @param               string  $tag optional: page to be checked. Default: current page.
1774         * @return              boolean TRUE if the user is the owner, FALSE otherwise.
1775         */
1776        function UserIsOwner($tag = "")
1777        {
1778                // if not logged in, user can't be owner!
1779                if (!$this->GetUser()) return false;
1780
1781                // if user is admin, return true. Admin can do anything!
1782                if ($this->IsAdmin()) return true;
1783
1784                // set default tag & check if user is owner
1785                if (!$tag = trim($tag)) $tag = $this->GetPageTag();
1786                if ($this->GetPageOwner($tag) == $this->GetUserName()) return true;
1787        }
1788        //returns true if user is listed in configuration list as admin
1789        function IsAdmin($user='') {
1790                $adminstring = $this->config["admin_users"];
1791                $adminarray = explode(',' , $adminstring);
1792
1793                if(TRUE===empty($user))
1794                {
1795                        $user = $this->GetUserName();
1796                }
1797                else if(is_array($user))
1798                {
1799                        $user = $user['name'];
1800                }
1801                foreach ($adminarray as $admin) {
1802                        if (trim($admin) == $user) return true;
1803                }
1804        }
1805        function GetPageOwner($tag = "", $time = "") { if (!$tag = trim($tag)) $tag = $this->GetPageTag(); if ($page = $this->LoadPage($tag, $time)) return $page["owner"]; }
1806        function SetPageOwner($tag, $user)
1807        {
1808                // check if user exists
1809                if( $user <> '' && ($this->LoadUser($user) || $user == "(Public)" || $user == "(Nobody)"))
1810                {
1811                        if ($user == "(Nobody)") $user = "";
1812                        // update latest revision with new owner
1813                        $this->Query("update ".$this->config["table_prefix"]."pages set owner = '".mysql_real_escape_string($user)."' where tag = '".mysql_real_escape_string($tag)."' and latest = 'Y' limit 1");
1814                }
1815        }
1816        function LoadACL($tag, $privilege, $useDefaults = 1)
1817        {
1818                if ((!$acl = $this->LoadSingle("SELECT ".mysql_real_escape_string($privilege)."_acl FROM ".$this->config["table_prefix"]."acls WHERE page_tag = '".mysql_real_escape_string($tag)."' LIMIT 1")) && $useDefaults)
1819                {
1820                        $acl = array("page_tag" => $tag, $privilege."_acl" => $this->GetConfigValue("default_".$privilege."_acl"));
1821                }
1822                return $acl;
1823        }
1824        function LoadAllACLs($tag, $useDefaults = 1)
1825        {
1826                if ((!$acl = $this->LoadSingle("SELECT * FROM ".$this->config["table_prefix"]."acls WHERE page_tag = '".mysql_real_escape_string($tag)."' LIMIT 1")) && $useDefaults)
1827                {
1828                        $acl = array("page_tag" => $tag, "read_acl" => $this->GetConfigValue("default_read_acl"), "write_acl" => $this->GetConfigValue("default_write_acl"), "comment_acl" => $this->GetConfigValue("default_comment_acl"));
1829                }
1830                return $acl;
1831        }
1832        function SaveACL($tag, $privilege, $list) {
1833                // the $default will be put in the SET statement of the INSERT SQL for default values. It isn't used in UPDATE.
1834                $default = "read_acl = '', write_acl = '', comment_acl = '', ";
1835                // we strip the privilege_acl from default, to avoid redundancy
1836                $default = str_replace($privilege."_acl = '',", '', $default);
1837                if ($this->LoadACL($tag, $privilege, 0)) $this->Query("UPDATE ".$this->config["table_prefix"]."acls SET ".mysql_real_escape_string($privilege)."_acl = '".mysql_real_escape_string(trim(str_replace("\r", "", $list)))."' WHERE page_tag = '".mysql_real_escape_string($tag)."' LIMIT 1");
1838                else $this->Query("INSERT INTO ".$this->config["table_prefix"]."acls SET $default page_tag = '".mysql_real_escape_string($tag)."', ".mysql_real_escape_string($privilege)."_acl = '".mysql_real_escape_string(trim(str_replace("\r", "", $list)))."'");
1839        }
1840        function TrimACLs($list) {
1841                foreach (explode("\n", $list) as $line)
1842                {
1843                        $line = trim($line);
1844                        $trimmed_list .= $line."\n";
1845                }
1846                return $trimmed_list;
1847        }
1848        // returns true if $user (defaults to current user) has access to $privilege on $page_tag (defaults to current page)
1849        function HasAccess($privilege, $tag = "", $user = "")
1850        {
1851                // set defaults
1852                if (!$tag) $tag = $this->GetPageTag();
1853                if (!$user) $user = $this->GetUserName();
1854
1855                // if current user is owner, return true. owner can do anything!
1856                if ($this->UserIsOwner($tag)) return true;
1857
1858                // see whether user is registered and logged in
1859                $registered = false;
1860                if ($this->GetUser()) $registered = true;
1861
1862                // load acl
1863                if ($tag == $this->GetPageTag())
1864                {
1865                        $acl = $this->ACLs[$privilege."_acl"];
1866                }
1867                else
1868                {
1869                        $tag_ACLs = $this->LoadAllACLs($tag);
1870                        $acl = $tag_ACLs[$privilege."_acl"];
1871                }
1872
1873                // fine fine... now go through acl
1874                foreach (explode("\n", $acl) as $line)
1875                {
1876                        // check for inversion character "!"
1877                        if (preg_match("/^[!](.*)$/", $line, $matches))
1878                        {
1879                                $negate = 1;
1880                                $line = $matches[1];
1881                        }
1882                        else
1883                        {
1884                                $negate = 0;
1885                        }
1886
1887                        // if there's still anything left... lines with just a "!" don't count!
1888                        if ($line)
1889                        {
1890                                switch ($line[0])
1891                                {
1892                                // comments
1893                                case "#":
1894                                        break;
1895                                // everyone
1896                                case "*":
1897                                        return !$negate;
1898                                // only registered users
1899                                case "+":
1900                                        // return ($registered) ? !$negate : false;
1901                                        return ($registered) ? !$negate : $negate;
1902                                // aha! a user entry.
1903                                default:
1904                                        if ($line == $user)
1905                                        {
1906                                                return !$negate;
1907                                        }
1908                                }
1909                        }
1910                }
1911
1912                // tough luck.
1913                return false;
1914        }
1915
1916        /**
1917         * Add a custom header to be inserted inside the <meta> tag. 
1918         * 
1919         * @uses Wakka::$additional_headers
1920         * @param string $additional_headers any valid XHTML code that is legal inside the <meta> tag.
1921         * @param string $indent optional indent string, default is a tabulation. This will be inserted before $additional_headers
1922         * @param string $sep optional separator string, this will separate you additional headers. This will be inserted after
1923         *      $additional_headers, default value is a line feed.
1924         * @access public
1925         * @return void
1926         */ 
1927        function AddCustomHeader($additional_headers, $indent = "\t", $sep = "\n") 
1928        { 
1929                $this->additional_headers[] = $indent.$additional_headers.$sep; 
1930        }
1931
1932        // MAINTENANCE
1933        function Maintenance()
1934        {
1935                // purge referrers
1936                if ($days = $this->GetConfigValue("referrers_purge_time")) {
1937                        $this->Query("DELETE FROM ".$this->config["table_prefix"]."referrers WHERE time < date_sub(now(), interval '".mysql_real_escape_string($days)."' day)");
1938                }
1939
1940                // purge old page revisions
1941                if ($days = $this->GetConfigValue("pages_purge_time")) {
1942                        $this->Query("delete from ".$this->config["table_prefix"]."pages where time < date_sub(now(), interval '".mysql_real_escape_string($days)."' day) and latest = 'N'");
1943                }
1944        }
1945
1946        // THE BIG EVIL NASTY ONE!
1947        function Run($tag, $method = "")
1948        {
1949                // Set default cookie path
1950                $base_url_path = preg_replace('/wikka\.php/', '', $_SERVER['SCRIPT_NAME']);
1951                $this->wikka_cookie_path = ('/' == $base_url_path) ? '/' : substr($base_url_path,0,-1);
1952
1953                // do our stuff!
1954                if (!$this->method = trim($method)) $this->method = "show";
1955                if (!$this->tag = trim($tag)) $this->Redirect($this->Href("", $this->config["root_page"]));
1956                if (!$this->GetUser() && ($user = $this->LoadUser($this->GetCookie('user_name'), $this->GetCookie('pass')))) $this->SetUser($user);
1957                if ((!$this->GetUser() && isset($_COOKIE["wikka_user_name"])) && ($user = $this->LoadUser($_COOKIE["wikka_user_name"], $_COOKIE["wikka_pass"])))
1958                {
1959                        //Old cookies : delete them
1960                        SetCookie('wikka_user_name', "", 1, $this->wikka_cookie_path);
1961                        $_COOKIE['wikka_user_name'] = "";
1962                        SetCookie('wikka_pass', '', 1, $this->wikka_cookie_path);
1963                        $_COOKIE['wikka_pass'] = "";
1964                        $this->SetUser($user);
1965                }
1966                $this->SetPage($this->LoadPage($tag, (isset($_GET['time']) ? $_GET['time'] :''))); #312
1967
1968                $this->LogReferrer();
1969                $this->ACLs = $this->LoadAllACLs($this->tag);
1970                $this->ReadInterWikiConfig();
1971                if(!($this->GetMicroTime()%3)) $this->Maintenance();
1972
1973                if (preg_match('/\.(xml|mm)$/', $this->method))
1974                {
1975                        header("Content-type: text/xml");
1976                        print($this->Method($this->method));
1977                }
1978                // raw page handler
1979                elseif ($this->method == "raw")
1980                {
1981                        header("Content-type: text/plain");
1982                        print($this->Method($this->method));
1983                }
1984                // grabcode page handler
1985                elseif ($this->method == "grabcode")
1986                {
1987                        print($this->Method($this->method));
1988                }
1989                elseif (preg_match('/\.(gif|jpg|png)$/', $this->method))                # should not be necessary
1990                {
1991                        header('Location: images/' . $this->method);
1992                }
1993                elseif (preg_match('/\.css$/', $this->method))                                  # should not be necessary
1994                {
1995                        header('Location: css/' . $this->method);
1996                }
1997                else
1998                {
1999                        $content_body = $this->Method($this->method);
2000                        print($this->Header().$content_body.$this->Footer());
2001                }
2002        }
2003}
2004?>
Note: See TracBrowser for help on using the browser.