root/trunk/libs/Wakka.class.php

Revision 1889, 153.0 KB (checked in by BrianKoontz, 4 months ago)

Implemented usergroup ACLs. Refs #1140.

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: Wakka.class.php 1346 2009-03-03 03:38:17Z BrianKoontz $
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-2010 {@link http://wikkawiki.org/CreditsPage Wikka Development Team}
25 */
26
27/**
28 * Time to live for client-side cookies in seconds (90 days)
29 */
30if(!defined('PERSISTENT_COOKIE_EXPIRY')) define('PERSISTENT_COOKIE_EXPIRY', 7776000);
31/**
32 * Maximum length for displayed hostnames
33 */
34if (!defined('MAX_HOSTNAME_LENGTH_DISPLAY')) define('MAX_HOSTNAME_LENGTH_DISPLAY', 50);
35/**
36 * Length to use for generated part of id attribute.
37 */
38if (!defined('ID_LENGTH')) define('ID_LENGTH',10);              // @@@ maybe make length configurable
39/**#@-*/
40
41/**
42 * Signature for a spamlog metadata line; MUST look different than Wikka markup!
43 */
44define('SPAMLOG_SIG','-@-');
45
46/**#@+
47 * String constant defining a regular expresion pattern.
48 */
49/**
50 * To be used in replacing img tags having an alt attribute with the value of the alt attribute, trimmed.
51 * - $result[0] : the entire img tag
52 * - $result[1] : If the alt attribute exists, this holds the single character used to delimit the alt string.
53 * - $result[2] : The content of the alt attribute, after it has been trimmed, if the attribute exists.
54 */
55if (!defined('PATTERN_REPLACE_IMG_WITH_ALTTEXT')) define('PATTERN_REPLACE_IMG_WITH_ALTTEXT', '/<img[^>]*(?<=\\s)alt=("|\')\s*(.*?)\s*\\1.*?>/');
56/**
57 * Defines characters that are not valid for an ID.
58 * Defined as the negation of a character class comprising the characters that
59 * <i>are</i> valid in an ID. All but valid characters will be stripped when deriving
60 * an ID froma provided string.
61 */
62if (!defined('PATTERN_INVALID_ID_CHARS')) define ('PATTERN_INVALID_ID_CHARS', '/[^A-Za-z0-9_:.-\s]/');
63/**#@-*/
64
65/**#@+
66 * String constant defining a regularly used bit of constant text.
67 */
68if (!defined('WIKKA_URL_EXTENSION')) define('WIKKA_URL_EXTENSION', 'wikka.php?wakka=');
69/**#@-*/
70
71/**
72 * The Wikka core class.
73 *
74 * This class contains all the core methods used to run Wikka.
75 * @name                Wakka
76 * @package             Wikka
77 * @subpackage  Libs
78 *
79 */
80class Wakka
81{
82        /**
83         * Hold the Wikka version.
84         *
85         * @var         string
86         */
87        var $VERSION;
88        /**
89         * Hold the wikka config.
90         *
91         * @access      private
92         * @var         array
93         */
94        var $config = array();
95
96        /**
97         * Hold the connection-link to the database.
98         *
99         * @access      private
100         * @var         resource
101         */
102        var $dblink;
103
104        /**
105         * Hold a log of queries and the times used for them; used for debugging.
106         *
107         * @var         array
108         */
109        var $queryLog = array();
110
111        /**
112         * Hold the interWiki List.
113         *
114         * @var         array
115         */
116        var $interWiki = array();
117
118        /**#@+*
119         * Variable to store data about HTTP headers.
120         */
121        /**
122         * Keep track of whether a (at least one) cookie has been sent to the browser.
123         *
124         * @var         boolean
125         */
126        var $cookies_sent = FALSE;
127
128        /**
129         * Time to live for client-side cookies in seconds (90 days)
130         *
131         * @var integer
132         */
133        var $cookie_expiry = PERSISTENT_COOKIE_EXPIRY;
134
135        /**
136         *
137         * @var unknown_type
138         */
139        var $wikka_cookie_path;
140
141        /**
142         * Customized head elements to be added in the <head> section.
143         *
144         * Array one may use to gather customized elements to be added inside <head>
145         * section, like additional stylesheet links, customized javascript, ...
146         * Handlers and/or actions adding items to this variable are responsible for
147         * sanitizing values passed to it.
148         * Use {@link Wakka::AddCustomHeader()} to populate this array.
149         *
150         * @access      public
151         * @var         array
152         */
153        var $additional_headers = array();
154        /**#@-*/
155
156        /**#@+*
157         * Variable to store data about pages.
158         */
159        /**
160         * Hold record for the current page.
161         *
162         * @access      private
163         * @var         array
164         */
165        var $page;
166
167        /**
168         * Hold the name of the current page.
169         *
170         * @access      private
171         * @var         string
172         */
173        var $tag;
174
175        /**
176         * Title of the page to insert in the <title> element.
177         *
178         * @access      public
179         * @var         string
180         */
181        var $page_title = '';
182
183        /**#@+*
184         * Variable to store data about users.
185         */
186
187        /**
188         * Tracks whether the <b>current</b> user is registered or not.
189         *
190         * @access      public
191         * @var         boolean
192         */
193        var $registered = FALSE;
194        /**
195         * Name of <b>current</b> user if anonymous (effectively either IP address or host name).
196         *
197         * @access      public
198         * @var         string
199         */
200        var $anon_username = '';
201        /**
202         * Cache for usernames that are known to be registered.
203         *
204         * @access      public
205         * @var         array()
206         */
207        var $registered_users = array();
208        /**
209         * Cache for usernames/IP addresses/hostnames that are known to be <b>not</b> registered.
210         *
211         * @access      public
212         * @var         array()
213         */
214        var $anon_users = array();
215        /**#@-*/
216
217        /**#@+*
218         * URL or URL component, derived just once in {@link Wakka::Run()} for later usage.
219         */
220        /**
221         * Complete Wikka URL ready to append a page name to.
222         * Derived from {@link WIKKA_BASE_URL} and (if rewrite mode is NOT on)
223         * {@link WIKKA_URL_EXTENSION} concatenated.
224         *
225         * @var string
226         */
227        var $wikka_url = '';
228        /**#@-*/
229
230        /**
231         * Constructor.
232         * Database connection is established when the main class Wakka is constructed.
233         *
234         * @uses        Config::$mysql_database
235         * @uses        Config::$mysql_host
236         * @uses        Config::$mysql_password
237         * @uses        Config::$mysql_user
238         */
239        function Wakka($config)
240        {
241                $this->config = $config;
242
243                $this->dblink = @mysql_connect($this->GetConfigValue('mysql_host'), $this->GetConfigValue('mysql_user'), $this->GetConfigValue('mysql_password'));
244                mysql_query("SET NAMES 'utf8'", $this->dblink);
245                if ($this->dblink)
246                {
247                        if (!@mysql_select_db($this->GetConfigValue('mysql_database'), $this->dblink))
248                        {
249                                @mysql_close($this->dblink);
250                                $this->dblink = FALSE;
251                        }
252                }
253                $this->VERSION = WAKKA_VERSION;
254                $this->PATCH_LEVEL = WIKKA_PATCH_LEVEL;
255        }
256
257        /**#@+
258         * @category    Database
259         * @todo        move into a database class.
260         */
261
262        /**
263         * Send a query to the database.
264         *
265         * If the query fails, the function will simply die(). If SQL-
266         * Debugging is enabled, the query and the time it took to execute
267         * are added to the Query-Log.
268         *
269         * @uses        Config::$sql_debugging
270         * @uses        Wakka::GetMicroTime()
271         *
272         * @param       string  $query  mandatory: the query to be executed.
273         * @param       resource $dblink optional: connection to the database
274         * @return      array   the result of the query.
275         *
276         */
277        function Query($query, $dblink='')
278        {
279                // init - detect if called from object or externally
280                if ('' == $dblink)
281                {
282                        $dblink = $this->dblink;
283                        $object = TRUE;
284                        $start = $this->GetMicroTime();
285                }
286                else
287                {
288                        $object = FALSE;
289                }
290                if (!$result = mysql_query($query, $dblink))
291                {
292                        ob_end_clean();
293                        die("Query failed: ".$query." (".mysql_error().")"); #i18n
294                }
295                if ($object && $this->GetConfigValue('sql_debugging'))
296                {
297                        $time = $this->GetMicroTime() - $start;
298                        $this->queryLog[] = array(
299                                "query"         => $query,
300                                "time"          => $time);
301                }
302                return $result;
303        }
304
305        /**
306         * Return the first row of a query executed on the database.
307         *
308         * @uses        Wakka::LoadAll()
309         *
310         * @param       string  $query  mandatory: the query to be executed
311         * @return      mixed   an array with the first result row of the query, or FALSE if nothing was returned.
312         * @todo        for 1.3: check if indeed false is returned (compare with trunk)
313         */
314        function LoadSingle($query)
315        {
316                if ($data = $this->LoadAll($query))
317                return $data[0];
318        }
319
320        /**
321         * Return all results of a query executed on the database.
322         *
323         * @uses        Wakka::Query()
324         *
325         * @param       string $query mandatory: the query to be executed
326         * @return      array the result of the query.
327         */
328        function LoadAll($query)
329        {
330                $data = array();
331                if ($r = $this->Query($query))
332                {
333                        while ($row = mysql_fetch_assoc($r))
334                        {
335                                $data[] = $row;
336                        }
337                        mysql_free_result($r);
338                }
339                return $data;
340        }
341
342        /**
343         * Generic 'count' query.
344         *
345         * Get a count of the number of records in a given table that would be matched
346         * by the given (optional) WHERE criteria. Only a single table can be queried.
347         *
348         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
349         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
350         * @since               Wikka 1.1.6.4
351         * @version             1.1
352         *
353         * @access      public
354         * @uses        Wakka::GetConfigValue()
355         * @uses        Wakka::Query()
356         * @uses        Config::$table_prefix
357         *
358         * @param       string  $table  required: (logical) table name to query;
359         *                                                      prefix will be automatically added
360         * @param       string  $where  optional: criteria to be specified for a WHERE clause;
361         *                                                      do not include WHERE
362         * @param   boolean $usePrefix optional: if true, append prefix defined in wikka.config.php file; if false, do not append prefix
363         * @return      integer number of matches returned by MySQL
364         */
365        function getCount($table, $where='', $usePrefix=TRUE)                                                   # JW 2005-07-16
366        {
367                // build query
368                $prefix = '';
369                if(TRUE===$usePrefix)
370                {
371                        $prefix = $this->GetConfigValue('table_prefix');
372                }
373                $where = ('' != $where) ? ' WHERE '.$where : '';
374                $query = "
375                        SELECT COUNT(*)
376                        FROM ".$prefix.$table.
377                        $where;
378
379                // get and return the count as an integer
380                $count = (int)mysql_result($this->Query($query),0);
381                return $count;
382        }
383
384        /**
385         * Check if the MySQL-Version is higher or equal to a given (minimum) one.
386         *
387         * @param $major
388         * @param $minor
389         * @param $subminor
390         * @return unknown_type
391         * @todo        for 1.3: compare with trunk-version!
392         */
393        function CheckMySQLVersion($major, $minor, $subminor)
394        {
395                $result = @mysql_query('SELECT VERSION() AS version');
396                if ($result !== FALSE && @mysql_num_rows($result) > 0)
397                {
398                        $row   = mysql_fetch_array($result);
399                        $match = explode('.', $row['version']);
400                }
401                else
402                {
403                        $result = @mysql_query('SHOW VARIABLES LIKE \'version\'');
404                        if ($result !== FALSE && @mysql_num_rows($result) > 0)
405                        {
406                                $row   = mysql_fetch_row($result);
407                                $match = explode('.', $row[1]);
408                        }
409                        else
410                        {
411                                return 0;
412                        }
413                }
414
415                        $mysql_major = $match[0];
416                        $mysql_minor = $match[1];
417                        $mysql_subminor = $match[2][0].$match[2][1];
418
419                        if ($mysql_major > $major)
420                        {
421                                return 1;
422                        }
423                else
424                {
425                        if (($mysql_major == $major) && ($mysql_minor >= $minor) && ($mysql_subminor >= $subminor))
426                        {
427                                return 1;
428                        }
429                        else
430                        {
431                                return 0;
432                        }
433                }
434        }
435
436        /**#@-*/
437
438        /**#@+
439         * @category    Misc methods
440         */
441
442        /**
443         * @todo        replace by getmicrotime() in Compatibility library!
444         * @return unknown_type
445         */
446        function GetMicroTime()
447        {
448                list($usec, $sec) = explode(" ",microtime());
449                return ((float)$usec + (float)$sec);
450        }
451
452        /**
453         * Calculates the difference between two microtimes.
454         *
455         * @uses        Wakka::getmicrotime()
456         * @param       $from mandatory: start time
457         * @param       $to     optional: end time (default: now)
458         * @return      unknown_type
459         */
460        function microTimeDiff($from, $to ='') {
461                if (strlen($to) == 0) $to = getmicrotime();
462                $totaltime = ($to - $from);
463                return $totaltime;
464        }
465
466        /**
467         *
468         * @param $filename
469         * @param $notfoundText
470         * @param $vars
471         * @param $path
472         * @return unknown_type
473         * @todo        for 1.3: compare with trunk-version!
474         */
475        function IncludeBuffered($filename, $notfoundText='', $vars='', $path='')
476        {
477                # TODO: change parameter order, so $path (no default,. it's required)
478                # comes after $filename and only $notfoundtext and $vars will actually
479                # be optional with a default of ''. MK/2007-03-31
480
481                // check if required parameter $path is supplied (see TODO)
482                if ('' != trim($path))
483                {
484                        // build full (relative) path to requested plugin (method/action/formatter)
485                        $fullfilepath = $this->BuildFullpathFromMultipath($filename, $path);
486                        // check if requested file (handler/action/formatter) actually exists
487                        if (FALSE===empty($fullfilepath))
488                        {
489                                if (is_array($vars))
490                                {
491                                        // make the parameters also available by name (apart from the array itself):
492                                        // some callers rely on these separate values, so we extract them, too
493                                        // taking care not to overwrite any already-existing variable
494                                        extract($vars, EXTR_SKIP);      # [SEC] EXTR_SKIP avoids collision with existing filenames
495                                }
496                                ob_start();
497                                include($fullfilepath);
498                                $output = ob_get_contents();
499                                ob_end_clean();
500                                return $output;
501                        }
502                }
503                if ('' != trim($notfoundText))
504                {
505                        return '<em class="error">'.$this->htmlspecialchars_ent(trim($notfoundText)).'</em>';   # [SEC] make error (including (part of) request) safe to display
506                }
507                else
508                {
509                        return FALSE;
510                }
511        }
512
513        /**
514         * Create a unique id for an HTML element.
515         *
516         * Although - given Wikka accepts can use embedded HTML - it cannot be
517         * guaranteed that an id generated by this method is unique it tries its
518         * best to make it unique:
519         * - ids are organized into groups, with the group name used as a prefix
520         * - if an id is specified it is compared with other ids in the same group;
521         *   if an identical id exists within the same group, a sequence suffix is
522         *   added, otherwise the specified id is accepted and recorded as a member
523         *   of the group
524         * - if no id is specified (or an invalid one) an id will be generated, and
525         *   given a sequence suffix if needed
526         *
527         * For headings, it is possible to derive an id from the heading content;
528         * to support this, any embedded whitespace is replaced with underscores
529         * to generate a recognizable id that will remain (mostly) constant even if
530         * new headings are inserted in a page. (This is not done for embedded
531         * HTML.)
532         *
533         * The method supports embedded HTML as well: as long as the formatter
534         * passes each id found in embedded HTML through this method it can take
535         * care that the id is valid and unique.
536         * This works as follows:
537         * - indicate an 'embedded' id with group 'embed'
538         * - NO prefix will be added for this reserved group
539         * - ids will be recorded and checked for uniqueness and validity
540         * - invalid ids are replaced
541         * - already-existing ids in the group are given a sequence suffix
542         * The result is that as long as the already-defined id is valid and
543         * unique, it will be remain unchanged (but recorded to ensure uniqueness
544         * overall).
545         *
546         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
547         * @copyright   Copyright © 2005, Marjolein Katsma
548         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
549         * @since               Wikka 1.1.6.4
550         * @version             1.0
551         *
552         * @access      public
553         * @uses        ID_LENGTH
554         *
555         * @param       string  $group  required: id group (e.g. form, head); will be
556         *                                                      used as prefix (except for the reserved group
557         *                                                      'embed' to be used for embedded HTML only)
558         * @param       string  $id             optional: id to use; if not specified or
559         *                                                      invalid, an id will be generated; if not
560         *                                                      unique, a sequence number will be appended
561         * @return      string  resulting id
562         */
563        function makeId($group,$id='')
564        {
565                // initializations
566                static $aSeq = array();                                                                         # group sequences
567                static $aIds = array();                                                                         # used ids
568
569                // preparation for group
570                if (!preg_match('/^[A-Z-a-z]/',$group))                                         # make sure group starts with a letter
571                {
572                        $group = 'g'.$group;
573                }
574                if (!isset($aSeq[$group]))
575                {
576                        $aSeq[$group] = 0;
577                }
578                if (!isset($aIds[$group]))
579                {
580                        $aIds[$group] = array();
581                }
582                if ('embed' != $group)
583                {
584                        $id = preg_replace('/\s+/','_',trim($id));                              # replace any whitespace sequence in $id with a single underscore
585                }
586
587                // validation (full for 'embed', characters only for other groups since we'll add a prefix)
588                if ('embed' == $group)
589                {
590                        $validId = preg_match('/^[A-Za-z][A-Za-z0-9_:.-]*$/',$id);      # ref: http://www.w3.org/TR/html4/types.html#type-id
591                }
592                else
593                {
594                        $validId = preg_match('/^[A-Za-z0-9_:.-]*$/',$id);
595                }
596
597                // build or generate id
598                if ('' == $id || !$validId || in_array($id,$aIds))                      # ignore specified id if it is invalid or exists already
599                {
600                        $id = substr(md5($group.$id),0,ID_LENGTH);                              # use group and id as basis for generated id
601                }
602                $idOut = ('embed' == $group) ? $id : $group.'_'.$id;            # add group prefix (unless embedded HTML)
603                if (in_array($id,$aIds[$group]))
604                {
605                        $idOut .= '_'.++$aSeq[$group];                                                  # add suffiX to make ID unique
606                }
607
608                // result
609                $aIds[$group][] = $id;                                                                          # keep track of both specified and generated ids (without suffix)
610                return $idOut;
611        }
612
613        /**#@-*/
614
615        /**#@+
616         * @category    Security methods
617         */
618
619        /**
620         * Strip potentially dangerous tags from embedded HTML.
621         *
622         * @uses        Config::$safehtml_path
623         * @uses        instantiate()
624         * @uses        SafeHTML::parse()
625         *
626         * @param       string $html mandatory: HTML to be secured
627         * @return      string sanitized HTML
628         */
629        function ReturnSafeHTML($html)
630        {
631                $safehtml_classpath = $this->GetConfigValue('safehtml_path').DIRECTORY_SEPARATOR.'classes'.DIRECTORY_SEPARATOR.'safehtml.php';
632                require_once $safehtml_classpath;
633
634                // Instantiate the handler
635                $safehtml = instantiate('safehtml');
636
637                $filtered_output = $safehtml->parse($html);
638
639                return $filtered_output;
640        }
641
642        /**
643         * Make sure a (user-provided) URL does use &amp; instead of & and is protected from attacks.
644         *
645         * Any already-present '&amp;' is first turned into '&'; then hsc_secure()
646         * is applied so all ampersands are "escaped" while characters that could be
647         * used to create a script attack (< > or ") are "neutralized" by escaping
648         * them.
649         *
650         * This method should be applied on any user-provided url in actions,
651         * handlers etc.
652         *
653         * Note: hsc_secure() is the secure replacement for PHP's htmlspecialchars().
654         * See #427.
655         *
656         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
657         * @copyright   Copyright © 2004, Marjolein Katsma
658         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
659         * @version             1.0
660         *
661         * @access              public
662         * @uses                Wakka::hsc_secure()
663         *
664         * @param               string  $url  required: URL to sanitize
665         * @return              string  sanitzied URL
666         */
667        function cleanUrl($url)
668        {
669                #return htmlspecialchars(preg_replace('/&amp;/','&',$url));
670                return $this->hsc_secure(preg_replace('/&amp;/','&',$url));
671        }
672
673        /**
674         * Wrapper around hsc_secure() which preserves entity references.
675         *
676         * The first two parameters for this function as the same as those for
677         * htmlspecialchars() in PHP: the text to be treated, and an optional
678         * parameter determining how to handle quotes; both these parameters are
679         * passed on to our hsc_secure() replacement for htmlspecialchars().
680         *
681         * Since hsc_secure() does not need a character set parameter, we don't
682         * have that here any more either.
683         *
684         * A third 'doctype' parameter is for local use only and determines how
685         * pre-existing entity references are treated after hsc_secure() has done
686         * its work: numeic entity references are always "unescaped' since they are
687         * valid for both HTML and XML doctypes; for XML the named entity references
688         * for the special characters are unescaped as well, while for for HTML any
689         * named entity reference is unescaped. This parameter is optional and
690         * defaults to HTML.
691         *
692         * The function first applies hsc_secure() to the input string and then
693         * "unescapes" character entity references and numeric character references
694         * (both decimal and hexadecimal).
695         * Entities are recognized also if the ending semicolon is omitted at the
696         * end or before a newline or tag but for consistency the semicolon is
697         * always added in the output where it was omitted.
698         *
699         * Usage note:
700         * Where code should be rendered <em>as code</em> hsc_secure() should be
701         * used directly so that entity references are also rendered as such instead
702         * of as their corresponding characters.
703         *
704         * Documentation note:
705         * It seems the $doctype parameter was added in 1.1.6.2; version should have
706         * been bumped up to 1.1, and the param documented. We'll assume the updated
707         * version was indeed 1.1, and put this one using hsc_secure() at 1.2 (at
708         * the same time updating the 'XML' doctype with apos as named entity).
709         *
710         * @since       Wikka 1.1.6.0
711         * @version     1.2
712         *
713         * @access      public
714         * @uses        Wakka::hsc_secure()
715         *
716         * @param       string  $text required: text to be converted
717         * @param       integer $quote_style optional: quoting style - can be ENT_COMPAT
718         *                      (default, escape only double quotes), ENT_QUOTES (escape both
719         *                      double and single quotes) or ENT_NOQUOTES (don't escape any
720         *                      quotes)
721         * @param       string $doctype 'HTML' (default) or 'XML'; for XML only the XML
722         *                      standard entities are unescaped so we'll have valid XML content
723         * @return      string  converted string with escaped special characted but
724         *                      entity references intact
725         *
726         * @todo        (maybe) recognize valid html entities and only leave those
727         *                      alone, thus transform &error; to &amp;error;
728         * @todo        (later - maybe) support full range of situations where (in SGML)
729         *                      a terminating ; may legally be omitted (end, newline and tag are
730         *                      merely the most common ones); such usage is quite rare though
731         *                      and may not be worth the effort
732         */
733        function htmlspecialchars_ent($text,$quote_style=ENT_COMPAT,$doctype='HTML')
734        {
735                // re-establish default if overwritten because of third parameter
736                // [ENT_COMPAT] => 2
737                // [ENT_QUOTES] => 3
738                // [ENT_NOQUOTES] => 0
739                if (!in_array($quote_style,array(ENT_COMPAT,ENT_QUOTES,ENT_NOQUOTES)))
740                {
741                        $quote_style = ENT_COMPAT;
742                }
743
744                // define patterns
745                $terminator = ';|(?=($|[\n<]|&lt;))';   // semicolon; or end-of-string, newline or tag
746                $numdec = '#[0-9]+';                                    // numeric character reference (decimal)
747                $numhex = '#x[0-9a-f]+';                                // numeric character reference (hexadecimal)
748                if ($doctype == 'XML')                                  // pure XML allows only named entities for special chars
749                {
750                        // only valid named entities in XML (case-sensitive)
751                        $named = 'lt|gt|quot|apos|amp';
752                        $ignore_case = '';
753                        $entitystring = $named.'|'.$numdec.'|'.$numhex;
754                }
755                else                                                                    // (X)HTML
756                {
757                        $alpha  = '[a-z]+';                                     // character entity reference TODO $named='eacute|egrave|ccirc|...'
758                        $ignore_case = 'i';                                     // names can consist of upper and lower case letters
759                        $entitystring = $alpha.'|'.$numdec.'|'.$numhex;
760                }
761                $escaped_entity = '&amp;('.$entitystring.')('.$terminator.')';
762
763                // execute our replacement hsc_secure() function, passing on optional parameters
764                $output = $this->hsc_secure($text,$quote_style);
765
766                // "repair" escaped entities
767                // modifiers: s = across lines, i = case-insensitive
768                $output = preg_replace('/'.$escaped_entity.'/s'.$ignore_case,"&$1;",$output);
769
770                // return output
771                return $output;
772        }
773
774        /**
775         * Secure replacement for PHP built-in function htmlspecialchars().
776         *
777         * See ticket #427 (http://wush.net/trac/wikka/ticket/427) for the rationale
778         * for this replacement function.
779         *
780         * The INTERFACE for this function is almost the same as that for
781         * htmlspecialchars(), with the same default for quote style; however, there
782         * is no 'charset' parameter. The reason for this is as follows:
783         *
784         * The PHP docs say:
785         *      "The third argument charset defines character set used in conversion."
786         *
787         * I suspect PHP's htmlspecialchars() is working at the byte-value level and
788         * thus _needs_ to know (or assume) a character set because the special
789         * characters to be replaced could exist at different code points in
790         * different character sets. (If indeed htmlspecialchars() works at
791         * byte-value level that goes some  way towards explaining why the
792         * vulnerability would exist in this function, too, and not only in
793         * htmlentities() which certainly is working at byte-value level.)
794         *
795         * This replacement function however works at character level and should
796         * therefore be "immune" to character set differences - so no charset
797         * parameter is needed or provided. If a third parameter is passed, it will
798         * be silently ignored.
799         *
800         * In the OUTPUT there is a minor difference in that we use '&#39;' instead
801         * of PHP's '&#039;' for a single quote: this provides compatibility with
802         *      get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES)
803         * (see comment by mikiwoz at yahoo dot co dot uk on
804         * http://php.net/htmlspecialchars); it also matches the entity definition
805         * for XML 1.0
806         * (http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters).
807         * Like PHP we use a numeric character reference instead of '&apos;' for the
808         * single quote. For the other special characters we use the named entity
809         * references, as PHP is doing.
810         *
811         * And finally:
812         * The name for this function was basically inspired by waawaamilk (GeSHi),
813         * kindly provided by BenBE (GeSHi), happily acknowledged by WikkaWiki Dev
814         * Team and finally used by JavaWoman. :)
815         *
816         * @author              {@link http://wikkawiki.org/JavaWoman Marjolein Katsma}
817         *
818         * @since               Wikka 1.1.6.3
819         * @version             1.0
820         * @license             http://www.gnu.org/copyleft/lgpl.html
821         *                              GNU Lesser General Public License
822         * @copyright   Copyright 2007, {@link http://wikkawiki.org/CreditsPage
823         *                              Wikka Development Team}
824         *
825         * @access      public
826         *
827         * @param       string  $string string to be converted
828         * @param       integer $quote_style
829         *                      - ENT_COMPAT:   escapes &, <, > and double quote (default)
830         *                      - ENT_NOQUOTES: escapes only &, < and >
831         *                      - ENT_QUOTES:   escapes &, <, >, double and single quotes
832         * @return      string  converted string
833         */
834        function hsc_secure($string, $quote_style=ENT_COMPAT)
835        {
836                // init
837                $aTransSpecchar = array('&' => '&amp;',
838                                                                '"' => '&quot;',
839                                                                '<' => '&lt;',
840                                                                '>' => '&gt;'
841                                                                );                      // ENT_COMPAT set
842                if (ENT_NOQUOTES == $quote_style)       // don't convert double quotes
843                {
844                        unset($aTransSpecchar['"']);
845                }
846                elseif (ENT_QUOTES == $quote_style)     // convert single quotes as well
847                {
848                        $aTransSpecchar["'"] = '&#39;'; // (apos) htmlspecialchars() uses '&#039;'
849                }
850
851                // return translated string
852                return strtr($string,$aTransSpecchar);
853        }
854
855        /**
856         * Get a value provided by user (by get, post or cookie) and sanitize it.
857         * The method is also helpful to disable warning when the value was absent.
858         *
859         * Note that form token checks are enforced for all POST
860         * operations to prevent CSRF attacks.
861         *
862         * @version     1.0
863         *
864         * @uses        Wakka::htmlspecialchars_ent()
865         *
866         * @access      public
867         * @since       Wikka 1.3
868         *
869         * @param       string  $varname required: field name on get or post or cookie name
870         * @param       string  $gpc one of 'get', 'post', or 'cookie'. Optional,
871         *                      defaults to 'get'.
872         * @param   string  $authenticate one of TRUE (use for sensitive
873                                 POSTs or FALSE (do not check form token). Optional, defaults to
874                                 TRUE (most secure option).
875         * @return      string  sanitized value of $_GET[$varname] (or $_POST,
876         *          $_COOKIE, depending on $gpc).  Redirects with error message upon
877         *          authentication error.
878         */
879        function GetSafeVar($varname, $gpc='get', $authenticate=TRUE)
880        {
881                $safe_var = NULL;
882                if ($gpc == 'post')
883                {
884                        // Is this a posted form?
885                        if(NULL != $_POST)
886                        {
887                                if(TRUE == $authenticate)
888                                {
889                                        if(!isset($_POST['CSRFToken']))
890                                        {
891                                                $this->SetRedirectMessage('Authentication failed: NoCSRFToken');
892                                                $this->Redirect();
893                                        }
894                                        $CSRFToken = $this->htmlspecialchars_ent($_POST['CSRFToken']);
895                                        if($CSRFToken != $_SESSION['CSRFToken'])
896                                        {
897                                                $this->SetRedirectMessage('Authentication failed: CSRFToken mismatch');
898                                                $this->Redirect();
899                                        }
900                                }
901                                $safe_var = isset($_POST[$varname]) ? $_POST[$varname] : NULL;
902                        }
903                        else
904                        {
905                                $safe_var = NULL;
906                        }
907                }
908                elseif ($gpc == 'get')
909                {
910                        $safe_var = isset($_GET[$varname]) ? $_GET[$varname] : NULL;
911                }
912                elseif ($gpc == 'cookie')
913                {
914                        $safe_var = isset($_COOKIE[$varname]) ? $_COOKIE[$varname] : NULL;
915                }
916                return ($this->htmlspecialchars_ent($safe_var));
917        }
918
919        /**
920         * CODE presentation
921         */
922
923        /**
924         * Highlight a code block with GeSHi.
925         *
926         * The path to GeSHi and the GeSHi language files must be defined in the configuration.
927         *
928         * This implementation fits in with general Wikka behavior; e.g., we use classes and an external
929         * stylesheet to render hilighting.
930         *
931         * Apart from this fixed general behavior, WikiAdmin can configure a few behaviors via the
932         * configuration file:
933         * geshi_header                 - wrap code in div (default) or pre
934         * geshi_line_numbers   - disable line numbering, or enable normal or fancy line numbering
935         * geshi_tab_width              - override tab width (default is 8 but 4 is more commonly used in code)
936         *
937         * Limitation: while line numbering is supported, extra GeSHi styling for line numbers is not.
938         * When line numbering is enabled, the end user can "turn it on" by specifying a starting line
939         * number together with the language code in a code block, e.g., (php;260); this number is then
940         * passed as the $start parameter for this method.
941         *
942         * @since       wikka 1.1.6.0
943         *
944         * @access      public
945         * @uses        Config::$geshi_path
946         * @uses        Config::$geshi_languages_path
947         * @uses        Config::$geshi_header
948         * @uses        Config::$geshi_line_numbers
949         * @uses        Config::$geshi_tab_width
950         * @uses        GeShi
951         *
952         * @param       string  $sourcecode     required: source code to be highlighted
953         * @param       string  $language       required: language spec to select highlighter
954         * @param       integer $start          optional: start line number; if supplied and >= 1 line numbering
955         *                      will be turned on if it is enabled in the configuration.
956         * @return      string  code block with syntax highlighting classes applied
957         * @todo                support for GeSHi line number styles
958         * @todo                enable error handling
959         */
960        function GeSHi_Highlight($sourcecode, $language, $start=0)
961        {
962                // create GeSHi object
963                include_once($this->GetConfigValue('geshi_path').DIRECTORY_SEPARATOR.'geshi.php');
964                $geshi = instantiate('GeSHi', $sourcecode, $language, $this->GetConfigValue('geshi_languages_path'));                           # create object by reference
965
966                $geshi->enable_classes();                                                               # use classes for hilighting (must be first after creating object)
967                $geshi->set_overall_class('code');                                              # enables using a single stylesheet for multiple code fragments
968
969                // configure user-defined behavior
970                $geshi->set_header_type(GESHI_HEADER_DIV);                              # set default
971                if (NULL !== $this->GetConfigValue('geshi_header'))                             # config override
972                {
973                        if ('pre' == $this->GetConfigValue('geshi_header'))
974                        {
975                                $geshi->set_header_type(GESHI_HEADER_PRE);
976                        }
977                }
978                $geshi->enable_line_numbers(GESHI_NO_LINE_NUMBERS);             # set default
979                if ($start > 0)                                                                                 # line number > 0 _enables_ numbering
980                {
981                        if (NULL !== $this->GetConfigValue('geshi_line_numbers'))               # effect only if enabled in configuration
982                        {
983                                if ('1' == $this->GetConfigValue('geshi_line_numbers'))
984                                {
985                                        $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
986                                }
987                                elseif ('2' == $this->GetConfigValue('geshi_line_numbers'))
988                                {
989                                        $geshi->enable_line_numbers(GESHI_FANCY_LINE_NUMBERS);
990                                }
991                                if ($start > 1)
992                                {
993                                        $geshi->start_line_numbers_at($start);
994                                }
995                        }
996                }
997                if (NULL !== $this->GetConfigValue('geshi_tab_width'))                  # GeSHi override (default is 8)
998                {
999                        $geshi->set_tab_width($this->GetConfigValue('geshi_tab_width'));
1000                }
1001
1002                // parse and return highlighted code
1003                // comments added to make GeSHi-highlighted block visible in code JW/20070220
1004                return '<!--start GeSHi-->'."\n".$geshi->parse_code()."\n".'<!--end GeSHi-->'."\n";
1005        }
1006
1007        /**
1008         * Normalizes line endings to "*nix style" ("\n") in a string; handles both Dos/Win and Mac.
1009         *
1010         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1011         * @copyright   Copyright © 2005, Marjolein Katsma
1012         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1013         * @version             0.5
1014         *
1015         * @access              public
1016         *
1017         * @param               string  $content        required: string to be normalized
1018         * @return              string                          content with normalized line endings
1019         */
1020        function normalizeLines($content)
1021        {
1022                return str_replace("\r","\n",str_replace("\r\n","\n",$content));
1023        }
1024
1025
1026
1027        /**#@-*/
1028
1029        /**#@+
1030         * @category    Variable-related methods
1031         * @todo        decide if we need (all) these methods!
1032         *                      JW: my vote is NOT if all a getter does is return a variable directly;
1033         *                      but useful if there's some processing or checking involved -
1034         *                      in which case an accompanying "setter" method should be used
1035         *                      for creating/updating the variable - if only for consistency.
1036         *
1037         *                      JW: GetConfigValue() is one such - so I created its sister
1038         *                      SetConfigValue as well.
1039         */
1040
1041        /**
1042         * Get the name tag of the current page.
1043         *
1044         * @uses        Wakka::$tag
1045         *
1046         * @return      string the name of the page
1047         */
1048        function GetPageTag()
1049        {
1050                return preg_replace('/_+/', ' ', $this->tag);
1051        }
1052
1053        /**
1054         * Get the time the current verion of the current page was saved.
1055         *
1056         * @uses        Wakka::$page
1057         *
1058         * @return      string
1059         */
1060        function GetPageTime()
1061        {
1062                return $this->page['time'];
1063        }
1064
1065        /**
1066         * Get the handler used on the page.
1067         *
1068         * @uses        Wakka::$handler
1069         * @return string name of the handler.
1070         */
1071        function GetHandler()
1072        {
1073                return $this->handler;
1074        }
1075
1076        /**
1077         * Get the value of a given item from the wikka config.
1078         *
1079         * @uses        Wakka::$config
1080         *
1081         * @param       $name   mandatory: name of a key in the config array
1082         * @return      mixed   the value of the configuration item, or NULL if not found
1083         */
1084        function GetConfigValue($name, $default=NULL)
1085        {
1086                $val = (isset($this->config[$name])) ? $this->config[$name] : $default;
1087                return $val;
1088        }
1089        /**
1090         * Set the value of a given item from the wikka config.
1091         *
1092         * @uses        Wakka::$config
1093         *
1094         * @param       $name mandatory: name of a key in the config array
1095         * @param       $value mandatory: the value to set the item at
1096         *       */
1097        function SetConfigValue($name,$value)
1098        {
1099                $this->config[$name] = $value;
1100        }
1101
1102        /**
1103         * Get the name of the Wiki.
1104         *
1105         * @uses        Config::$wakka_name
1106         * @return      string the name of the Wiki.
1107         */
1108        function GetWakkaName()
1109        {
1110                return $this->GetConfigValue('wakka_name');
1111        }
1112
1113        /**
1114         * Get the wikka version.
1115         *
1116         * @return      string the wikka version
1117         */
1118        function GetWakkaVersion()
1119        {
1120                return $this->VERSION;
1121        }
1122
1123        /**
1124         *
1125         * @return unknown_type
1126         */
1127        function GetWikkaPatchLevel()
1128        {
1129                return $this->PATCH_LEVEL;
1130        }
1131
1132        /**
1133         * Log probably spammy comment.
1134         *
1135         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1136         * @copyright   Copyright © 2005, Marjolein Katsma
1137         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1138         * @version             0.7
1139         *
1140         * @access              public
1141         * @todo                - prepare strings for internationalization
1142         *
1143         * @uses                logSpam()
1144         *
1145         * @param               string  $tag            required: string page name
1146         * @param               string  $body           required: string containing comment body
1147         * @param               string  $reason         required: why attempt failed (urls|filter|nokey|badkey...)
1148         * @param               integer $urlcount       optional: number of (new) URLs
1149         * @param               integer $user           optional: original user/origin (rather than current user)
1150         * @param               integer $time           optional: original time (rather than time of logging)
1151         * @return              mixed                           bytes written if successful, FALSE otherwise.
1152         */
1153        function logSpamComment($tag,$body,$reason,$urlcount=0,$user='',$time='')
1154        {
1155                $type           = 'comment ';
1156                return $this->logSpam($type,$tag,$body,$reason,$urlcount,$user,$time);
1157        }
1158
1159        /**
1160         * Log probably spammy document.
1161         *
1162         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1163         * @copyright   Copyright © 2005, Marjolein Katsma
1164         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1165         * @version             0.7
1166         *
1167         * @access              public
1168         * @todo                - prepare strings for internationalization
1169         *
1170         * @uses                logSpam()
1171         *
1172         * @param               string  $tag            required: string page name
1173         * @param               string  $body           required: string containing comment body
1174         * @param               string  $reason         required: why attempt failed (urls|filter|nokey|badkey)
1175         * @param               integer $urlcount       optional: number of (new) URLs
1176         * @param               integer $user           optional: original user/origin (rather than current user)
1177         * @param               integer $time           optional: original time (rather than time of logging)
1178         * @return              mixed                           bytes written if successful, FALSE otherwise.
1179         */
1180        function logSpamDocument($tag,$body,$reason,$urlcount=0,$user='',$time='')
1181        {
1182                $type           = 'document';
1183                return $this->logSpam($type,$tag,$body,$reason,$urlcount,$user,$time);
1184        }
1185
1186        /**
1187         * Log probably spammy feedback.
1188         *
1189         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1190         * @copyright   Copyright © 2005, Marjolein Katsma
1191         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1192         * @version             0.7
1193         *
1194         * @access              public
1195         * @todo                - prepare strings for internationalization
1196         *
1197         * @uses                logSpam()
1198         *
1199         * @param               string  $tag            required: string page name
1200         * @param               string  $body           required: string containing feedback text
1201         * @param               string  $reason         required: why attempt failed (urls|filter|nokey|badkey)
1202         * @param               integer $urlcount       optional: number of (new) URLs
1203         * @return              mixed                           bytes written if successful, FALSE otherwise.
1204         */
1205        function logSpamFeedback($tag,$body,$reason,$urlcount=0)
1206        {
1207                $type           = 'feedback';
1208                return $this->logSpam($type,$tag,$body,$reason,$urlcount);
1209        }
1210
1211        /**
1212         * Log probable spam (comment, document or feedback).
1213         *
1214         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1215         * @copyright   Copyright © 2005, Marjolein Katsma
1216         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1217         * @version             0.7
1218         *
1219         * @access              private
1220         *
1221         * @uses      DEFAULT_SPAMLOG_PATH
1222         * @uses      Config::$spamlog_path
1223         * @uses      Wakka::appendFile()
1224         * @uses      Wakka::GetUserName()
1225         * @uses      Wakka::htmlspecialchars_ent()
1226         *
1227         * @todo                - make recognition of mass delete i18n-proof
1228         *                              - use configured (later!) timezone
1229         *                              - use configured (later!) date/time format
1230         *
1231         * @param               string  $type           required: string containing type (document|comment)
1232         * @param               string  $tag            required: string page name
1233         * @param               string  $body           required: string containing content (document or comment)
1234         * @param               string  $reason         required: why attempt failed (urls|filter|nokey|badkey)
1235         * @param               integer $urlcount       required: number of (new) URLs
1236         * @param               integer $user           optional: user/origin - default current user/origin
1237         * @param               integer $time           optional: time - default current time
1238         * @return              mixed                           bytes written if successful, FALSE otherwise.
1239         */
1240        function logSpam($type,$tag,$body,$reason,$urlcount,$user='',$time='')
1241        {
1242                // set path
1243                $spamlogpath = $this->GetConfigValue('spamlog_path', DEFAULT_SPAMLOG_PATH);
1244                // gather data
1245                if ($user == '')
1246                {
1247                        $user = $this->GetUserName();                                   # defaults to REMOTE_HOST to domain for anonymous user
1248                }
1249                if ($time == '')
1250                {
1251                        $time = date('Y-m-d H:i:s');                                    # current date/time
1252                }
1253                if (preg_match('/^mass delete/',$reason))                       # @@@ i18n
1254                {
1255                        $originip = '0.0.0.0';                                                  # don't record deleter's IP address!
1256                }
1257                else
1258                {
1259                        $originip = $_SERVER['REMOTE_ADDR'];
1260                }
1261                $ua                     = (isset($_SERVER['HTTP_USER_AGENT'])) ? '['.$_SERVER['HTTP_USER_AGENT'].']' : '[?]';
1262                $ua = $this->htmlspecialchars_ent($ua);
1263                $body           = $this->htmlspecialchars_ent(trim($body));
1264                $sig            = SPAMLOG_SIG.' '.$type.' '.$time.' '.$tag.' - '.$originip.' - '.$user.' '.$ua.' - '.$reason.' - '.$urlcount."\n";
1265                $content        = $sig.$body."\n\n";
1266
1267                // add data to log     
1268                return $this->appendFile($spamlogpath,$content);        # nr. of bytes written if successful, FALSE otherwise
1269        }
1270
1271        /**
1272         * Get all meta data lines from the spamlog and return the data in an array.
1273         *
1274         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1275         * @copyright   Copyright © 2005, Marjolein Katsma
1276         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1277         * @version             0.3
1278         *
1279         * @uses      DEFAULT_SPAMLOG_PATH
1280         * @uses      Config::$spamlog_path
1281         *
1282         * @access              public
1283         *
1284         * @return              array           array with associative array for each metadata item in a line
1285         */
1286        function getSpamlogSummary()
1287        {
1288                // set path
1289                $spamlogpath = $this->GetConfigValue('spamlog_path', DEFAULT_SPAMLOG_PATH);
1290                $aSummary = array();
1291                $aLines = file($spamlogpath);                                           # get file as array so we can...
1292                foreach ($aLines as $line)                                                      # ... select the metadata
1293                {
1294                        if (preg_match('/^'.SPAMLOG_SIG.'/',$line))
1295                        {
1296                                // gather data
1297                                list($header,$originIp,$userAgent,$reason,$urls) = explode(' - ',$line);
1298                                list(,$type,$day,$time,$page) = preg_split('/\s+/',$header);
1299
1300                                $rc = preg_match('/^([^ ]+) \[([^\]]+)\]$/',$userAgent,$aMatches);
1301                                $user = (isset($aMatches[1])) ? $aMatches[1] : '?';
1302                                $ua   = (isset($aMatches[2])) ? $aMatches[2] : '?';
1303
1304                                // write data
1305                                $aSummary[] = array('type'      => $type,
1306                                                                        'date'  => $day.' '.$time,
1307                                                                        'day'   => $day,
1308                                                                        'time'  => $time,
1309                                                                        'page'  => $page,
1310                                                                        'origin'=> $originIp,
1311                                                                        'user'  => $user,
1312                                                                        'ua'    => $ua,
1313                                                                        'reason'=> $reason,
1314                                                                        'urls'  => $urls
1315                                                                        );
1316                        }
1317                }
1318                return $aSummary;
1319        }
1320
1321        // FILES (handling of text files)
1322        /**
1323         * Read a local file, normalizing line endings.
1324         *
1325         * Reads a local file (not bothering to read in packets as would be needed
1326         * for network or remote files). Returns the content as a string with
1327         * normalized line endings ("\n"), or FALSE if the process failed for some
1328         * reason. Thus it is the caller's responsibility to provide a correct path
1329         * to an existing file.
1330         *
1331         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1332         * @copyright   Copyright © 2005, Marjolein Katsma
1333         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1334         * @version             0.5
1335         * @todo
1336         *
1337         * @access              public
1338         * @uses                normalizeLines()
1339         *
1340         * @param               string  $file  required: relative or absolute file path
1341         * @return              mixed           normalized file content if found, FALSE if not
1342         */
1343        function readFile($file)
1344        {
1345                #if version_compare(PHP_VERSION >= '4.3.0')
1346                if (function_exists('file_get_contents'))                       # gives best performance
1347                {
1348                        $content = file_get_contents($file);
1349                }
1350                else                                                                                            # alternative
1351                {
1352                        $fh = @fopen($file,'r');                                                # suppress warning with @
1353                        if (!$fh)
1354                        {
1355                                $content = FALSE;
1356                        }
1357                        else
1358                        {
1359                                $content = fread($fh,filesize($file));
1360                                fclose($fh);
1361                        }
1362                }
1363                if (FALSE !== $content)
1364                {
1365                        $content = $this->normalizeLines($content);             # normalize line endings
1366                }
1367                return $content;
1368        }
1369
1370        /**
1371         * Writes new content to a (text) file.
1372         *
1373         * The content is normalized for line endings ("\n") before writing; this
1374         * implies this method CANNOT be used for binary files.
1375         * Returns the number of bytes written if successful, FALSE otherwise.
1376         *
1377         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1378         * @copyright   Copyright © 2005, Marjolein Katsma
1379         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1380         * @version             0.5
1381         * @todo
1382         *
1383         * @access              public
1384         * @uses                normalizeLines()
1385         *
1386         * @param               string  $file           required: relative or absolute file path
1387         * @param               string  $content        required: contents be written to the file
1388         * @return              mixed                           bytes written if successful, FALSE otherwise.
1389         */
1390        function writeFile($file,$content)
1391        {
1392                $rc = FALSE;
1393                $content = $this->normalizeLines($content);             # normalize line endings
1394                if (function_exists('file_put_contents'))               # most efficient
1395                {
1396                        $rc = file_put_contents($file,$content);
1397#if (FALSE === $rc) echo 'file_put_contents FALSE!<br/>';
1398                        if (strlen($content) > 0 && $rc == 0) $rc = FALSE;              # for compatibility with fwrite() @@@ needed?
1399                }
1400                else                                                                                    # alternative
1401                {
1402                        $fh = @fopen($file,'w');                                        # open file for writing; suppress warning with @
1403                        if (FALSE !== $fh)
1404                        {
1405                                $rc = @fwrite($fh,$content);
1406                                fclose($fh);
1407                        }
1408                }
1409                return $rc;                                                                             # number of bytes written or FALSE if writing failed
1410        }
1411
1412        /**
1413         * Appends new content to an existing file.
1414         *
1415         * The content is normalized for line endings ("\n") before writing; this
1416         * implies this method CANNOT be used for binary files.
1417         * Returns the number of bytes written if successful, FALSE otherwise.
1418         *
1419         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
1420         * @copyright   Copyright © 2005, Marjolein Katsma
1421         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
1422         * @version             0.5
1423         * @todo
1424         *
1425         * @access              public
1426         * @uses                normalizeLines()
1427         *
1428         * @param               string  $file           required: relative or absolute file path
1429         * @param               string  $content        required: contents be written to the file
1430         * @return              mixed                           bytes written if successful, FALSE otherwise.
1431         */
1432        function appendFile($file,$content)
1433        {
1434                $rc = FALSE;
1435                $content = $this->normalizeLines($content);             # normalize line endings
1436                if (function_exists('file_put_contents'))               # most efficient
1437                {
1438                        $rc = file_put_contents($file,$content,FILE_APPEND);
1439                        if (strlen($content) > 0 && $rc == 0) $rc = FALSE;              # for compatibility with fwrite() @@@ needed?
1440                }
1441                else                                                                                    # alternative
1442                {
1443                        $fh = @fopen($file,'a');                                # open file for appending/writing; suppress warning with @
1444                        if (FALSE !== $fh)
1445                        {
1446                                $rc = @fwrite($fh,$content);
1447                                fclose($fh);
1448                        }
1449                }
1450                return $rc;                                                                             # number of bytes written or FALSE if writing failed
1451        }
1452
1453
1454
1455        /**#@-*/
1456
1457        /**#@+
1458         * @category    Page
1459         */
1460
1461        /**
1462         * LoadPage loads the page whose name is $tag.
1463         *
1464         * If parameter $time is provided, LoadPage returns the page as it was at that exact time.
1465         * If parameter $time is not provided, it returns the page as its latest state.
1466         * LoadPage and LoadPageById remember the page tag or page id they've queried by caching them,
1467         * so, these methods try first to retrieve data from cache if available.
1468         *
1469         * @access      public
1470         * @uses        Config::$table_prefix
1471         * @uses        Wakka:LoadSingle()
1472         * @uses        Wakka:CachePage()
1473         * @uses        Wakka:CacheNonExistentPage()
1474         * @uses        Wakka:GetCachedPage()
1475         *
1476         * @param       string  $tag    mandatory: name of the page to load
1477         * @param       string  $time   optional: timestamp if a specific revision should be loaded
1478         * @param       boolean $cache  optional: if TRUE and the latest version was requested,
1479         *                                      an attempt to retrieve from cache will be made first.
1480         *                                      default: TRUE
1481         * @return      mixed   array with page structure, or FALSE if not retrieved
1482         * @todo        for 1.3: compare with trunk
1483         */
1484        function LoadPage($tag, $time='', $cache=TRUE)
1485        {
1486                // Always replace '_' with ws
1487                $tag = preg_replace('/_+/', ' ', $tag);
1488                // retrieve from cache
1489                if (!$time && $cache) {
1490                        $page = isset($this->pageCache[$tag]) ? $this->pageCache[$tag] : null;
1491                        if ($page=="cached_nonexistent_page") return null;
1492                }
1493                // load page
1494                if (!isset($page)) $page = $this->LoadSingle("select * from ".$this->GetConfigValue('table_prefix')."pages where tag = '".mysql_real_escape_string($tag)."' ".($time ? "and time = '".mysql_real_escape_string($time)."'" : "and latest = 'Y'")." limit 1");
1495                // cache result
1496                if ($page && !$time) {
1497                        $this->pageCache[$page["tag"]] = $page;
1498                } elseif (!$page) {
1499                        $this->pageCache[$tag] = "cached_nonexistent_page";
1500                }
1501                return $page;
1502        }
1503
1504        /**
1505         * GetCachedPage gets a page from cache whose name is $tag.
1506         *
1507         * @access      public
1508         * @uses        Wakka::$pageCache
1509         *
1510         * @param       mixed   $tag    the name of the page to retrieve from cache.
1511         * @return      mixed   an array as returned by LoadPage(), or FALSE if absent from cache.
1512         */
1513        function GetCachedPage($tag)
1514        {
1515                return (isset($this->pageCache[$tag])) ? $this->pageCache[$tag] : null;
1516        }
1517
1518        /**
1519         * CachePage caches a page to prevent reusing MySQL operations when reloading it.
1520         *
1521         * <p>Cached pages are stored in the array {@link Wakka::pageCache}.</p>
1522         * <p>If this is the latest version of the page, the page name is used as a key for the array. That page name
1523         * may be lowercased if the database doesn't work with case sensitive collation. Lowercasing it enhances the
1524         * power of caching by preventing reloading of a page (with mysql) under another case. But if the database
1525         * needs to work with case sensitive collation (like cp1250_czech_cs), you must set a config value named
1526         * `pagename_case_sensitive' to 1, and this lowercasing will be disabled.</p>
1527         * <p>CachePage also stores the page under a key made of a special marker slash+sharp (/#) concatenated with
1528         * the page id. As example, a page having id=208 will be stored at $this->pageCache['/#208'].This ensures
1529         * that a page previously loaded by its name or by id will be retrieved from cache if the page id match.</p>
1530         * <p>Normally, the type of the value of the array is an array containing the page data, as returned by
1531         * LoadPage. However, If this is the latest version of the page, a link will be made between the page id and
1532         * the page tag. In such case, the value of an entry of $this->pageCache[] will be just a string beginning
1533         * with a slash (/), and to retrieve the data, you have to use this string as a key for the array
1534         * $this->pageCache[] after suppressing the leading slash.</p>
1535         *
1536         * @access      public
1537         * @uses        Wakka::$pageCache
1538         *
1539         * @param       mixed   $page
1540         * @return      void
1541         */
1542        function CachePage($page)
1543        {
1544                $this->pageCache[$page['tag']] = $page;
1545        }
1546
1547        /**
1548         * Check whether the page is already assigned a title to set in the <title> tag.
1549         *
1550         * @access      public
1551         * @uses        Wakka::$page_title
1552         *
1553         * @return      boolean
1554         */
1555        function HasPageTitle()
1556        {
1557                return ('' != $this->page_title);
1558        }
1559
1560        /**
1561         * Store page data.
1562         *
1563         * @uses        Wakka::$page
1564         * @uses        Wakka::$tag
1565         * @param       string  $page
1566         * @return      void
1567         */
1568        function SetPage($page)
1569        {
1570                $this->page = $page;
1571                if ($this->page['tag'])
1572                {
1573                        $this->tag = $this->page['tag'];
1574                }
1575        }
1576
1577        /**
1578         * Store the title of a page (as derived by the formatter).
1579         *
1580         * Actually, the title of the page is chosen from the text inside headings
1581         * h1 through h4, that is encountered first.
1582         * (But that process isn't happening in this function! see wakka3callback().)
1583         *
1584         * @access      public
1585         * @uses        Wakka::$page_title
1586         *
1587         * @param       string  $page_title     the new title of the page.
1588         * @return      void
1589         * @todo        probably better to use the already-existing Wakka::$page array to store this?
1590         */
1591        function SetPageTitle($page_title)
1592        {
1593                $stripped_page_title = trim(strip_tags($page_title));
1594                if(null != $stripped_page_title)
1595                {
1596                        $this->page_title = $stripped_page_title;
1597                }
1598        }
1599
1600        /**
1601         * LoadPageById loads a page whose id is $id.
1602         *
1603         * @access      public
1604         * @uses        Wakka::GetCachedPageById()
1605         * @uses        Wakka::LoadSingle()
1606         * @uses        Wakka::GetConfigValue()
1607         * @uses        Config::$table_prefix
1608         *
1609         * @param       int             $id             mandatory: Id of the page to load.
1610         * @return      array with page structure identified by $id, or ? if no page could be retrieved
1611         * @todo        for 1.3: compare and add caching ability
1612         * @todo        for 1.3: check LoadSingle for return value
1613         */
1614        function LoadPageById($id)
1615        {
1616                return $this->LoadSingle("
1617                        SELECT *
1618                        FROM ".$this->GetConfigValue('table_prefix')."pages
1619                        WHERE id = '".mysql_real_escape_string($id)."'
1620                        LIMIT 1"
1621                        );
1622        }
1623
1624        /**
1625         * LoadRevisions: Load revisions of a page.
1626         *
1627         * @access      public
1628         * @uses        Wakka::GetConfigValue()
1629         * @uses        Wakka::LoadAll()
1630         *
1631         * @param       string  $page Name of the page to view revisions of
1632         * @return      array   This value contains * from page.
1633         */
1634        function LoadRevisions($page)
1635        {
1636                return $this->LoadAll("select * from ".$this->GetConfigValue('table_prefix')."pages where tag = '".mysql_real_escape_string($page)."' order by id desc");
1637        }
1638
1639        /**
1640         * LoadOldestRevision: Load the oldest known revision of a page.
1641         *
1642         * @access      public
1643         * @uses        Config::$pagename_case_sensitive
1644         * @uses        Config::$table_prefix
1645         * @uses        Wakka::GetConfigValue()
1646         * @uses        Wakka::LoadSingle()
1647         *
1648         * @param       string  $tag    The name of the page to load oldest revision of.
1649         * @return      array
1650         * @todo        review usage of cache - see NOTES above. Also note that revisions
1651         *                      are (intentionally or not) stored only if config flag
1652         *                      'pagename_case_sensitive' is FALSE
1653         */
1654        function LoadOldestRevision($tag)
1655        {
1656                if (!$this->GetConfigValue('pagename_case_sensitive'))
1657                {
1658                        $tag_lowercase = strtolower($tag);
1659                }
1660                // @@@ $tag_lowercase won't have a value if pagename_case_sensitive is TRUE!
1661                        $oldest_revision = $this->LoadSingle("
1662                                SELECT note, id, time, user
1663                                FROM ".$this->GetConfigValue('table_prefix')."pages
1664                                WHERE tag = '".mysql_real_escape_string($tag)."'
1665                                ORDER BY time
1666                                LIMIT 1"
1667                                );
1668                return $oldest_revision;
1669        }
1670
1671        /**
1672         * Load pages linking to a given page.
1673         *
1674         * @uses        Config::$table_prefix
1675         * @uses        Wakka::LoadAll()
1676         * @param       string  $tag    mandatory: name of page to find referring links to
1677         * @return      array   one record with a page name for each page found (empty array if none found).
1678         */
1679        function LoadPagesLinkingTo($tag)       // #410
1680        {
1681                return $this->LoadAll("
1682                        SELECT from_tag AS page_tag
1683                        FROM ".$this->GetConfigValue('table_prefix')."links
1684                        WHERE to_tag = '".mysql_real_escape_string($tag)."'
1685                        ORDER BY page_tag"
1686                        );
1687        }
1688
1689        /**
1690         * Load the last x edited pages on the wiki.
1691         *
1692         * @uses        Config::$table_prefix
1693         * @uses        Wakka::LoadAll()
1694         * @uses        Wakka::CachePage()
1695         *
1696         * @return      array   the last x pages that were changed (empty array if none found)
1697         * @todo        use constant for default limit value (no "magic numbers!")
1698         * @todo        do we need the whole page for each, or only specific fields?
1699         */
1700        function LoadRecentlyChanged()
1701        {
1702                $pages = $this->LoadAll("
1703                        SELECT *
1704                        FROM ".$this->GetConfigValue('table_prefix')."pages
1705                        WHERE latest = 'Y'
1706                        ORDER BY id DESC"
1707                        );
1708                if ($pages)
1709                {
1710                        foreach ($pages as $page)
1711                        {
1712                                $this->CachePage($page);
1713                        }
1714                }
1715                return $pages;
1716        }
1717
1718        /**
1719         * Load pages that need to be created.
1720         *
1721         * @access      public
1722         * @uses        Config::$table_prefix
1723         * @uses        Wakka::LoadAll()
1724         *
1725         * @return      array
1726         * @todo        it would be useful to set a LIMIT ($max) here as well
1727         */
1728        function LoadWantedPages()
1729        {
1730                $pre = $this->GetConfigValue('table_prefix');
1731                return $this->LoadAll("
1732                        SELECT DISTINCT
1733                                ".$pre."links.to_tag AS tag,
1734                                COUNT(".$pre."links.from_tag) AS count
1735                        FROM ".$pre."links
1736                        LEFT JOIN ".$pre."pages
1737                                ON ".$pre."links.to_tag = ".$pre."pages.tag
1738                        WHERE ".$pre."pages.tag is NULL
1739                        GROUP BY ".$pre."links.to_tag
1740                        ORDER BY count desc");
1741        }
1742
1743        /**
1744         * Ask if a pagename needs to be created.
1745         *
1746         * When an existing page links to a page that hasn't yet been created, this latter needs
1747         * to be created, or the reference needs to be deleted.
1748         *
1749         * @access      public
1750         * @uses        Wakka::LoadWantedPages()
1751         *
1752         * @param       string  $tag    Name of the page to ask if it needs to be created
1753         * @return      boolean TRUE if $tag needs to be created
1754         * @todo        exmine old comment: '#410 - but function not used in 1.1.6.3 -OR- trunk?'
1755         * @todo page_tag or tag?
1756         */
1757        function IsWantedPage($tag)
1758        {
1759                if ($pages = $this->LoadWantedPages())
1760                {
1761                        foreach ($pages as $page)
1762                        {
1763                                if ($page['page_tag'] == $tag)
1764                                {
1765                                        return TRUE;
1766                                }
1767                        }
1768                }
1769                return FALSE;
1770        }
1771
1772        /**
1773         * Load all orphaned pages.
1774         *
1775         * Orphaned pages are existing pages that no others page on the wiki links to.
1776         * Thus, the only chance this page could be reached may be from search or
1777         * special pages like PageIndex. A good quality wiki should not have any orphaned page.
1778         *
1779         * @uses        Config::$table_prefix
1780         * @uses        Wakka::LoadAll()
1781         * @access      public
1782         * @return      array   List of orphaned pages
1783         */
1784        function LoadOrphanedPages()
1785        {
1786                $pre = $this->GetConfigValue('table_prefix');
1787                $pages = $this->LoadAll("
1788                        SELECT DISTINCT tag
1789                        FROM ".$pre."pages
1790                        LEFT JOIN ".$pre."links
1791                                ON ".$pre."pages.tag = ".$pre."links.to_tag
1792                        WHERE ".$pre."links.to_tag IS NULL
1793                        ORDER BY tag"
1794                        );
1795                return $pages;
1796        }
1797
1798        /**
1799         * Load all active page names of the wiki and their respective owners.
1800         *
1801         * @uses        Config::$table_prefix
1802         * @uses        Wakka::GetConfigValue()
1803         * @uses        Wakka::LoadAll()
1804         * @access      public
1805         * @return      array   List of all page titles (and the page owner), ordered by page name
1806         */
1807        function LoadPageTitles()               // @@@ name no longer matches function
1808        {
1809                return $this->LoadAll("
1810                        SELECT DISTINCT tag, owner
1811                        FROM ".$this->GetConfigValue('table_prefix')."pages
1812                        WHERE latest = 'Y'
1813                        ORDER BY tag"
1814                        );
1815        }
1816
1817        /**
1818         * Get names of pages (tags) owned by the specified user.
1819         *
1820         * @uses        Config::$table_prefix
1821         * @uses        Wakka::GetConfigValue()
1822         * @uses        Wakka::LoadAll()
1823         *
1824         * @param       string  $owner
1825         * @return      array   one row for each page owned by $owner
1826         */
1827        function LoadPagesByOwner($owner)
1828        {
1829                return $this->LoadAll("
1830                        SELECT tag
1831                        FROM ".$this->GetConfigValue('table_prefix')."pages
1832                        WHERE `latest` = 'Y'
1833                                AND `owner` = '".mysql_real_escape_string($owner)."'
1834                        ORDER BY `tag`"
1835                        );
1836        }
1837
1838        /**
1839         * Load all pages in the wiki.
1840         *
1841         * Using this function should be avoided since it really loads everything from the pages table!
1842         *
1843         * @return unknown_type
1844         * @todo        for 1.3:see trunk and comment above on LoadPageTitles()
1845         */
1846        function LoadAllPages()
1847        {
1848                return $this->LoadAll("
1849                        SELECT * FROM ".$this->GetConfigValue('table_prefix')."pages
1850                        WHERE latest = 'Y'
1851                        ORDER BY tag"
1852                        );
1853        }
1854
1855        /**
1856         * Save a page.
1857         *
1858         * @uses        Config::$table_prefix
1859         * @uses        Config::$wikiping_server
1860         * @uses        Wakka::GetPingParams()
1861         * @uses        Wakka::existsUser()
1862         * @uses        Wakka::GetUserName()
1863         * @uses        Wakka::HasAccess()
1864         * @uses        Wakka::LoadPage()
1865         * @uses        Wakka::Query()
1866         * @uses        Wakka::WikiPing()
1867         *
1868         * @param       string  $tag mandatory:name of the page
1869         * @param       string  $body mandatory:content of the page
1870         * @param       string  $note mandatory:edit-note
1871         * @param       string  $owner
1872         * @todo for 1.3:in trunk the page-title is stored together with the page
1873         */
1874        function SavePage($tag, $body, $note, $owner=null)
1875        {
1876                // Always replace '_' with ws
1877                $tag = preg_replace('/_+/', ' ', $tag);
1878                // get name of current user
1879                $user = $this->GetUserName();
1880
1881                // TODO: check write privilege
1882                if ($this->HasAccess('write', $tag))
1883                {
1884                        // If $owner is specified, don't do an owner check
1885                        if(empty($owner))
1886                        {
1887                                // is page new?
1888                                if (!$oldPage = $this->LoadPage($tag))
1889                                {
1890                                        // current user is owner if user is logged in, otherwise, no owner.
1891                                        if ($this->existsUser())
1892                                        {
1893                                                $owner = $user;
1894                                        }
1895                                }
1896                                else
1897                                {
1898                                        // aha! page isn't new. keep owner!
1899                                        $owner = $oldPage['owner'];
1900                                }
1901                        }
1902                        // Parse page title
1903                        $page_title = $this->ParsePageTitle($body);
1904
1905                        // set all other revisions to old
1906                        $this->Query("
1907                                UPDATE ".$this->GetConfigValue('table_prefix')."pages
1908                                SET latest = 'N'
1909                                WHERE tag = '".mysql_real_escape_string($tag)."'"
1910                                );
1911
1912                        // add new revision
1913                        $this->Query("
1914                                INSERT INTO ".$this->GetConfigValue('table_prefix')."pages
1915                                SET     tag             = '".mysql_real_escape_string($tag)."',
1916                                  title = '".mysql_real_escape_string($page_title)."',
1917                                        time    = now(),
1918                                        owner   = '".mysql_real_escape_string($owner)."',
1919                                        user    = '".mysql_real_escape_string($user)."',
1920                                        note    = '".mysql_real_escape_string($note)."',
1921                                        latest  = 'Y',
1922                                        body    = '".mysql_real_escape_string($body)."'"
1923                                );
1924
1925                        // WikiPing
1926                        if ($pingdata = $this->GetPingParams($this->GetConfigValue('wikiping_server'), $tag, $user, $note))
1927                        {
1928                                $this->WikiPing($pingdata);
1929                        }
1930                }
1931        }
1932
1933        /**#@-*/
1934
1935        /**#@+
1936         * @category    Search methods
1937         */
1938
1939        /**
1940         * Full text search, case-sensitive
1941         *
1942         * @access      public
1943         *
1944         * @param       string  $phrase the text to be searched for
1945     * @param   string  $caseSensitive  optional: 0 for case-insensitive search (default), 1 for case-sensitive search
1946         * @param   string $utf8Compatible optional: 0 for legacy search (case sensitive, wildcards, but incompatible with some character codings), 1 for UTF-8 compatible searches (non-case-sensitive, no wildcards)
1947         * @return      string  Search results 
1948         */
1949        function FullTextSearch($phrase, $caseSensitive=0, $utf8Compatible=0)
1950        {
1951                if(empty($phrase))
1952                {
1953                        return NULL;
1954                }
1955                $sql = '';
1956                if(0 == $utf8Compatible)
1957                {
1958                        $id = '';
1959                        // Should work with any browser/entity conversion scheme
1960                        $search_phrase = mysql_real_escape_string($phrase);
1961                        if ( 1 == $caseSensitive ) $id = ', id';
1962                        $sql  = "select * from ".$this->GetConfigValue('table_prefix')."pages where latest = ".  "'Y'" ." and match(tag, body".$id.") against(". "'$search_phrase'" ." IN BOOLEAN MODE) order by time DESC";
1963                }
1964                else
1965                {
1966                        $search_phrase = mysql_real_escape_string($phrase);
1967                        $sql  = "select * from ".$this->GetConfigValue('table_prefix')."pages WHERE latest = ". "'Y'";
1968                        foreach( explode(' ', $search_phrase) as $term ) 
1969                                $sql .= " AND ((`tag` LIKE '%{$term}%') OR (body LIKE '%{$term}%'))";
1970                }
1971                $data = $this->LoadAll($sql);
1972                return $data;
1973        }
1974
1975
1976        /**
1977         *
1978         * @param $phrase
1979         * @return unknown_type
1980         */
1981        function FullCategoryTextSearch($phrase)
1982        {
1983                return $this->LoadAll("select * from ".$this->GetConfigValue('table_prefix')."pages where latest = 'Y' and match(body) against('".mysql_real_escape_string($phrase)."' IN BOOLEAN MODE)");
1984        }
1985
1986        /**#@-*/
1987
1988        /**#@+
1989         * @category    Content-related methods
1990         */
1991
1992        /**
1993         * [Short description needed here].
1994         *
1995         * @access      public
1996         *
1997         * @param       string  $textvalue      the text to be cleaned
1998         * @param       string  $pattern_prohibited_chars       optional: valid regular expression pattern.
1999         *                      Characters that match this expression will be stripped.
2000         *                      If this is set to an empty string, every character will be valid.
2001         * @param       boolean $decode_html_entities   should htmlentities be decoded?
2002         * @return      string  The text after some characters stripped
2003         * @todo        Better strategy:
2004         *                      pull out the nodeToTextOnly() bit (usable for TOC) and use this:
2005         *                      1) separately in wikka3callback in Formatter (so a TOC can be built!)
2006         *                      2) for turning heading into a <title> text (instead of this function!)
2007         *                      THEN: rename this to what it was intended to do: textToValidId()
2008         *                      (and use that in the Formatter, of course)
2009         * @todo        move regexes to library #34
2010         */
2011        function CleanTextNode($textvalue, $pattern_prohibited_chars = '/[^A-Za-z0-9_:.-\s]/', $decode_html_entities = TRUE)
2012        {
2013                // START -- nodeToTextOnly
2014                $textvalue = trim($textvalue);
2015                // First find and replace any image having an alt attribute with its (trimmed) alt text
2016                // Image tags missing an alt attribute are not replaced.
2017                $textvalue = preg_replace(PATTERN_REPLACE_IMG_WITH_ALTTEXT, '\\2', $textvalue);
2018                // @@@ JW/2005-05-27 now first replace linebreaks <br/> and other whitespace with single spaces!!
2019                // Remove all other tags, including img tags that missed an alt attribute
2020                $textvalue = strip_tags($textvalue);
2021                // @@@ this all-text result is usable for a TOC!!!
2022                // Use this if we have a condition set to generate a TOC
2023                // END -- nodeToTextOnly
2024
2025                if ($decode_html_entities)
2026                {
2027                        if (function_exists('html_entity_decode'))
2028                        {
2029                                // replace entities that can be interpreted
2030                                // use default charset ISO-8859-1 because other chars won't be valid for an ID anyway
2031                                $textvalue = html_entity_decode($textvalue, ENT_NOQUOTES);
2032                        }
2033                        // remove any remaining entities (so we don't end up with strange words and numbers in the ID text)
2034                        $textvalue = preg_replace('/&[#]?.+?;/','',$textvalue);
2035                }
2036                // finally remove non-ID characters (except whitespace which is handled by makeId())
2037                if ($pattern_prohibited_chars)  // @@@ make this into a global constant instead of a parameter!
2038                {
2039                        $textvalue = preg_replace($pattern_prohibited_chars, '', $textvalue);
2040                }
2041                return $textvalue;
2042        }
2043
2044        /**
2045         *
2046         * @uses        Wakka::IsAdmin()
2047         * @uses        Wakka::GetUser()
2048         * @uses        Wakka::IncludeBuffered()
2049         * @uses        Wakka::Format()
2050         *
2051         * @param $menu
2052         * @return string menu items as an unordered list
2053         */
2054        function MakeMenu($menu)
2055        {
2056                switch(TRUE)
2057                {
2058                        case $this->IsAdmin():
2059                        $menu_file = $menu.'.admin.inc';
2060                        break;
2061
2062                        case $this->GetUser():
2063                        $menu_file = $menu.'.user.inc';
2064                        break;
2065
2066                        default:
2067                        $menu_file = $menu.'.inc';
2068                        break;
2069                }
2070                if ($this->BuildFullpathFromMultipath($menu_file,$this->GetConfigValue('menu_config_path'))) #878
2071                {
2072                        $menu_src = $this->IncludeBuffered($menu_file, '', '', $this->GetConfigValue('menu_config_path')); #878
2073                        $menu_array = explode("\n", trim($menu_src)); #951
2074                        $menu_output = '<ul class="menu" id="'.$menu.'">'."\n";
2075                        foreach ($menu_array as $menu_item)
2076                        {
2077                                $menu_output .= '<li>'.$this->Format($menu_item).'</li>'."\n";
2078                        }
2079                        $menu_output .= '</ul>'."\n";
2080                }
2081                else
2082                {
2083                        $menu_output = '<ul id="'.$menu.'">'."\n";
2084                        $menu_output .= '<li>no menu defined</li>'."\n";
2085                        $menu_output .= '</ul>'."\n";
2086                }
2087                return $menu_output;
2088        }
2089
2090        /**
2091         * Return the title of the current page.
2092         *
2093         * The page title is cleaned and trimmed. See {@link
2094         *      Wakka::wakka3callback()} to find how the title is derived.
2095         * If SetPageTitle() was unable to choose a title for the page,
2096         *      the page name is used by default.
2097         * Attempts to retrieve page title from DB if $tag is specified
2098         *      and is not the current page that's loaded
2099         *
2100         * @uses        Wakka::GetPageTag()
2101         * @uses        Wakka::HasPageTitle()
2102         * @uses        Wakka::$page_title
2103         * @uses        Wakka::LoadSingle()
2104         *
2105         *
2106         * @param       string  @tag    optional: page to get title for (default current page)
2107         * @return      mixed   the title of the current page or the page name if none found, trimmed
2108         */
2109        function PageTitle($tag=null)
2110        {
2111                if ($tag === null)
2112                {
2113                        $tag = $this->GetPageTag();
2114                }
2115                if ($this->HasPageTitle() && $tag == $this->GetPageTag())
2116                {
2117                        return $this->page_title;
2118                }
2119                $query = "SELECT title FROM ".
2120                                $this->GetConfigValue('table_prefix').
2121                                "pages WHERE tag = '".
2122                                mysql_real_escape_string($tag).
2123                                "' AND LATEST = 'Y'";
2124                $res = $this->LoadSingle($query);
2125                $page_title = trim($res['title']) !== '' ? $res['title'] : $tag;
2126                return trim(strip_tags($page_title));
2127        }
2128
2129        /**
2130         * Parses the body of a page for a page title
2131         *
2132         * Searches for first instance of header markup in page body and
2133         * returns this string as the page title, or empty if none found
2134         *
2135         * @param body string page body
2136         * @return string the title of the current page, or empty string
2137         */
2138        function ParsePageTitle($body)
2139        {
2140                $page_title = '';
2141                if (preg_match("#(={3,6})([^=].*?)\\1#s", $body, $matches))
2142                {
2143                        list($h_fullmatch, $h_markup, $h_heading) = $matches;
2144                        $page_title = $h_heading;
2145                }
2146                // We need trim because $this->Format() appends a carriage return
2147                return trim(strip_tags($this->Format($page_title)));
2148        }
2149
2150        /**
2151         * Check by name if a page exists.
2152         *
2153         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman}
2154         * @copyright   Copyright © 2004, Marjolein Katsma
2155         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
2156         * @version             1.1
2157         *
2158         * NOTE: v. 1.0 -> 1.1
2159         *              - name changed from ExistsPage() to existsPage() !!!
2160         *              - added $prefix param so it can be used from installer
2161         *              - added $current param so it checks by default for a current page only
2162         *
2163         * @access      public
2164         * @uses        Query()
2165         *
2166         * @param       string  $page  page name to check
2167         * @param       string  $prefix optional: table prefix to use
2168         *                                      pass NULL if you need to override the $active parameter
2169         *                                      default: prefix as in configuration file
2170         * @param       mixed   $dblink optional: connection resource, or NULL to get
2171         *                                      object's connection
2172         * @param       string  $active optional: if TRUE, check for actgive page only
2173         *                                      default: TRUE
2174         * @return      boolean TRUE if page exists, FALSE otherwise
2175         */
2176        function existsPage($page, $prefix='', $dblink=NULL, $active=TRUE)
2177        {
2178                // init
2179                $count = 0;
2180                $table_prefix = (empty($prefix) && isset($this)) ? $this->GetConfigValue('table_prefix') : $prefix;
2181                if (is_null($dblink))
2182                {
2183                        $dblink = $this->dblink;
2184                }
2185                // build query
2186                $query = "SELECT COUNT(tag)
2187                                FROM ".$table_prefix."pages
2188                                WHERE tag='".mysql_real_escape_string($page)."'";
2189                if ($active)
2190                {
2191                        $query .= "             AND latest='Y'";
2192                }
2193                // do query
2194                if ($r = Wakka::Query($query, $dblink))
2195                {
2196                        $count = mysql_result($r,0);
2197                        mysql_free_result($r);
2198                }
2199                // report
2200                return ($count > 0) ? TRUE : FALSE;
2201        }
2202
2203        /**#@-*/
2204
2205        /**#@+
2206         * @category WikiPing
2207         * @author      DreckFehler
2208         */
2209
2210        /**
2211         * WikiPing an external server.
2212         *
2213         * @param $host
2214         * @param $data
2215         * @param $contenttype
2216         * @param $maxAttempts
2217         * @return unknown_type
2218         *
2219         * @todo        move to a dedicated class (plugin)
2220         */
2221        function HTTPpost($host, $data, $contenttype='application/x-www-form-urlencoded', $maxAttempts = 5)
2222        {
2223                $attempt = 0;
2224                $status = 300;
2225                $result = '';
2226                while ($status >= 300 && $status < 400 && $attempt++ <= $maxAttempts)
2227                {
2228                        $url = parse_url($host);
2229                        if (isset($url['path']) == FALSE)
2230                        {
2231                                $url['path'] = '/';
2232                        }
2233                        if (isset($url["port"]) == FALSE)
2234                        {
2235                                $url['port'] = 80;
2236                        }
2237                        if ($socket = fsockopen ($url['host'], $url['port'], $errno, $errstr, 15))
2238                        {
2239                                $strQuery = 'POST '.$url['path'].' HTTP/1.1'."\n";
2240                                $strQuery .= 'Host: '.$url['host']."\n";
2241                                $strQuery .= 'Content-Length: '.strlen($data)."\n";
2242                                $strQuery .= 'Content-Type: '.$contenttype."\n";
2243                                $strQuery .= 'Connection: close'."\n\n";
2244                                $strQuery .= $data;
2245
2246                                // send request & get response
2247                                fputs($socket, $strQuery);
2248                                $bHeader = TRUE;
2249                                while (!feof($socket))
2250                                {
2251                                        $strLine = trim(fgets($socket, 512));
2252                                        if (strlen($strLine) == 0)
2253                                        {
2254                                                $bHeader = FALSE; // first empty line ends header-info
2255                                        }
2256                                        if ($bHeader)
2257                                        {
2258                                                if (!$status)
2259                                                {
2260                                                        $status = $strLine;
2261                                                }
2262                                                if (preg_match('/^Location:\s(.*)/', $strLine, $matches))
2263                                                {
2264                                                        $location = $matches[1];
2265                                                }
2266                                        }
2267                                        else
2268                                        {
2269                                                $result .= trim($strLine)."\n";
2270                                        }
2271                                }
2272                                fclose ($socket);
2273                        }
2274                        else
2275                        {
2276                                $status = '999 timeout';
2277                        }
2278
2279                        if ($status)
2280                        {
2281                                if (preg_match('/(\d){3}/', $status, $matches))
2282                                {
2283                                        $status = $matches[1];
2284                                }
2285                        }
2286                        else
2287                        {
2288                                $status = 999;
2289                        }
2290                        $host = $location;              // @@@ not used anywhere! (unless params are passed by reference - which they are not)
2291                }
2292                if (preg_match('/^[\da-fA-F]+(.*)$/', $result, $matches))
2293                {
2294                        $result = $matches[1];
2295                }
2296                return $result;
2297        }
2298
2299        /**
2300         * Broadcast wiki changes to external servers.
2301         *
2302         * @uses        Wakka::htmlspecialchars_ent()
2303         * @uses        Wakka::HTTPpost()
2304         * @param       $ping
2305         * @param       $debug
2306         * @return      unknown_type
2307         *
2308         * @todo        move to a dedicated class (plugin)
2309         */
2310        function WikiPing($ping, $debug = FALSE)
2311        {
2312                if ($ping)
2313                {
2314                        $rpcRequest = '';
2315                        $rpcRequest .= "<methodCall>\n";
2316                        $rpcRequest .= "<methodName>wiki.ping</methodName>\n";
2317                        $rpcRequest .= "<params>\n";
2318                        $rpcRequest .= "<param>\n<value>\n<struct>\n";
2319                        $rpcRequest .= "<member>\n<name>tag</name>\n<value>".$ping['tag']."</value>\n</member>\n";
2320                        $rpcRequest .= "<member>\n<name>url</name>\n<value>".$ping['taglink']."</value>\n</member>\n";
2321                        $rpcRequest .= "<member>\n<name>wiki</name>\n<value>".$ping['wiki']."</value>\n</member>\n";
2322                        if ($ping['author'])
2323                        {
2324                                $rpcRequest .= "<member>\n<name>author</name>\n<value>".$ping['author']."</value>\n</member>\n";
2325                                if ($ping['authorpage'])
2326                                {
2327                                        $rpcRequest .= "<member>\n<name>authorpage</name>\n<value>".$ping['authorpage']."</value>\n</member>\n";
2328                                }
2329                        }
2330                        if ($ping['history'])
2331                        {
2332                                $rpcRequest .= "<member>\n<name>history</name>\n<value>".$ping['history']."</value>\n</member>\n";
2333                        }
2334                        if ($ping['changelog'])
2335                        {
2336                                $rpcRequest .= "<member>\n<name>changelog</name>\n<value>".$this->htmlspecialchars_ent($ping['changelog'],ENT_COMPAT,'XML')."</value>\n</member>\n";
2337                        }
2338                        $rpcRequest .= "</struct>\n</value>\n</param>\n";
2339                        $rpcRequest .= "</params>\n";
2340                        $rpcRequest .= "</methodCall>\n";
2341
2342                        foreach (explode(' ', $ping['server']) as $server)
2343                        {
2344                                $response = $this->HTTPpost($server, $rpcRequest, 'text/xml');
2345                                if ($debug)
2346                                {
2347                                        print $response;
2348                                }
2349                        }
2350                }
2351        }
2352
2353        /**
2354         * Gather the necessary parameters for WikiPing.
2355         *
2356         * @uses        Wakka::Href()
2357         * @uses        Wakka::GetWakkaName()
2358         * @uses        Wakka::LoadPage()
2359         *
2360         * @param       string  $server mandatory:
2361         * @param       string  $tag    mandatory:
2362         * @param       string  $user   mandatory:
2363         * @param       string  $changelog      optional:
2364         * @return      mixed   either an array with the WikiPing-params or FALSE
2365         *                                      if retrieving one of the required parameters failed
2366         * @todo        move to a dedicated class (plugin)
2367         */
2368        function GetPingParams($server, $tag, $user, $changelog = '')
2369        {
2370                // init
2371                $ping = array();
2372                if ($server)
2373                {
2374                        $ping['server'] = $server;
2375                        if ($tag) // set page-title
2376                        {
2377                                $ping["tag"] = $tag;
2378                        }
2379                        else
2380                        {
2381                                return FALSE;
2382                        }
2383                        if (!$ping['taglink'] = $this->Href('', $tag)) // set page-url
2384                        {
2385                                return FALSE;
2386                        }
2387                        if (!$ping['wiki'] = $this->GetWakkaName()) // set site-name
2388                        {
2389                                return FALSE;
2390                        }
2391                                $ping['history'] = $this->Href('revisions', $tag); // set url to history
2392       
2393                                if ($user)
2394                                {
2395                                        $ping['author'] = $user; // set username
2396                                        // @todo use existsPage instead
2397                                        if ($this->LoadPage($user))
2398                                        {
2399                                                $ping['authorpage'] = $this->Href('', $user);   // set link to user page
2400                                        }
2401                                }
2402                                if ($changelog)
2403                                {
2404                                        $ping['changelog'] = $changelog;
2405                                }
2406                        return $ping;
2407                }
2408                else return FALSE;
2409        }
2410
2411        /**#@-*/
2412
2413        /**#@+
2414         * @category    Cookie-related methods
2415         *
2416         * Note: Be sure to check the auto login functionality in
2417         * setup/install.php if any changes are made to the way session
2418         * cookies are set. Since these functions are not yet available
2419         * when install.php is called, they must be duplicated in that
2420         * file. Changes here without appropriate changes in install.php
2421         * may result in login/logout failures! See ticket #800 for more
2422         * info.
2423         */
2424
2425        /**
2426         *
2427         * @uses        Wakka::SetCookie()
2428         * @param $name
2429         * @param $value
2430         * @return unknown_type
2431         */
2432        function SetSessionCookie($name, $value)
2433        {
2434                SetCookie($name.$this->GetConfigValue('wiki_suffix'), $value, 0, $this->wikka_cookie_path);
2435                $_COOKIE[$name.$this->GetConfigValue('wiki_suffix')] = $value;
2436                $this->cookies_sent = TRUE;
2437        }
2438
2439        /**
2440         *
2441         * @uses        Wakka::SetCookie()
2442         * @param $name
2443         * @param $value
2444         * @return unknown_type
2445         */
2446        function SetPersistentCookie($name, $value)
2447        {
2448                SetCookie($name.$this->GetConfigValue('wiki_suffix'), $value, time() + $this->cookie_expiry, $this->wikka_cookie_path);
2449                $_COOKIE[$name.$this->GetConfigValue('wiki_suffix')] = $value;
2450                $this->cookies_sent = TRUE;
2451        }
2452
2453        /**
2454         *
2455         * @uses        Wakka::SetCookie()
2456         * @param $name
2457         * @return unknown_type
2458         */
2459        function DeleteCookie($name)
2460        {
2461                SetCookie($name.$this->GetConfigValue('wiki_suffix'), "", 1, $this->wikka_cookie_path);
2462                $_COOKIE[$name.$this->GetConfigValue('wiki_suffix')] = "";
2463                $this->cookies_sent = TRUE;
2464        }
2465
2466        /**
2467         *
2468         * @param $name
2469         * @return unknown_type
2470         */
2471        function GetCookie($name)
2472        {
2473                if (isset($_COOKIE[$name.$this->GetConfigValue('wiki_suffix')]))
2474                {
2475                        return $_COOKIE[$name.$this->GetConfigValue('wiki_suffix')];
2476                }
2477                else
2478                {
2479                        return FALSE;
2480                }
2481        }
2482
2483        /**#@-*/
2484
2485
2486        /**#@+
2487         * @category    HTTP/GET/POST/LINK related
2488         */
2489
2490        /**
2491         * Store a message in the session to be displayed after redirection.
2492         *
2493         * @param       string  $message        text to be stored
2494         */
2495        function SetRedirectMessage($message)
2496        {
2497                $_SESSION['redirectmessage'] = $message;
2498        }
2499
2500        /**
2501         * Get a message, if one was stored before redirection.
2502         * To set the message, either use {@link Wakka::SetRedirectMessage()} or the second parameter
2503         * of the {@link Wakka::Redirect()} method.
2504         * The message is passed transparently between {@link Wakka::SetRedirectMessage()} and
2505         * GetRedirectMessage(). It is the responsibility of any code setting and getting that
2506         * message to perform any validation against the message (quotes handling, XHTML validation, ...)
2507         *
2508         * @see Wakka::Redirect()
2509         * @see Wakka::SetRedirectMessage()
2510         * @return string either the text of the message or an empty string.
2511         */
2512        function GetRedirectMessage()
2513        {
2514                $message = '';
2515                if (isset($_SESSION['redirectmessage']))
2516                {
2517                        $message = $_SESSION['redirectmessage'];
2518                        $_SESSION['redirectmessage'] = '';
2519                }
2520                return $message;
2521        }
2522
2523        /**
2524         * Performs a redirection to another page.
2525         *
2526         * On IIS server, and if the page had sent any cookies, the redirection must not be performed
2527         * by using the 'Location:' header: We use meta http-equiv OR javascript OR link (Credits MarceloArmonas)
2528         *
2529         * @author      {@link http://wikkawiki.org/DotMG Mahefa Randimbisoa} (added IIS support)
2530         *
2531         * @access      public
2532         * @since       Wikka 1.1.6.2
2533         *
2534         * @param       string  $url: destination URL; if not specified redirect to the same page.
2535         * @param       string  $message: message that will show as alert in the destination URL
2536         */
2537        function Redirect($url='', $message='')
2538        {
2539                if ($message != '')
2540                {
2541                        $_SESSION['redirectmessage'] = $message;
2542                }
2543                $url = ($url == '' ) ? $this->wikka_url.$this->GetPageTag() : $url;
2544                if ((preg_match('/IIS/i', $_SERVER['SERVER_SOFTWARE'])) && ($this->cookies_sent))
2545                {
2546                        @ob_end_clean();
2547                        die('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2548<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head><title>Redirected to '.$this->Href($url).'</title>'.
2549'<meta http-equiv="refresh" content="0; url=\''.$url.'\'" /></head><body><div><script type="text/javascript">window.location.href="'.$url.'";</script>'.
2550'</div><noscript>If your browser does not redirect you, please follow <a href="'.$this->Href($url).'">this link</a></noscript></body></html>');
2551                }
2552                else
2553                {
2554                        header('Location: '.$url);
2555                }
2556                exit;
2557        }
2558
2559        /**
2560         * Return the pagename (with optional handler appended).
2561         *
2562         * @param $handler
2563         * @param $tag
2564         * @return unknown_type
2565         */
2566        function MiniHref($handler='', $tag='')
2567        {
2568                if (!$tag = trim($tag)) $tag = $this->GetPageTag();
2569                $tag = preg_replace('/\s+/', '_', $tag);
2570                return $tag.($handler ? "/".$handler : "");
2571        }
2572
2573        /**
2574         * Returns the full URL to a page/handler.
2575         *
2576         * @uses        Config::$rewrite_mode
2577         * @uses        Wakka::MiniHref()
2578         * @param       $method
2579         * @param       $tag
2580         * @param       $params
2581         * @return      unknown_type
2582         */
2583        function Href($method='', $tag='', $params='')
2584        {
2585                $href = $this->wikka_url.$this->MiniHref($method, $tag);
2586                if ($params)
2587                {
2588                        $href .= ($this->GetConfigValue('rewrite_mode') ? '?' : '&amp;').$params;
2589                }
2590                return $href;
2591        }
2592
2593        /**
2594         * Creates a link from Wikka markup.
2595         *
2596         * Beware of the $title parameter: quotes and backslashes should be previously
2597         * escaped before the title is passed to this method.
2598         *
2599         * @access      public
2600         *
2601         * @uses        Wakka::GetInterWikiUrl()
2602         * @uses        Wakka::Href()
2603         * @uses        Wakka::htmlspecialchars_ent()
2604         * @uses        Wakka::LoadPage()
2605         * @uses        Wakka::TrackLinkTo()
2606         * @uses        Wakka::existsPage()
2607         *
2608         * @param       mixed   $tag            mandatory:
2609         * @param       string  $handler        optional:
2610         * @param       string  $text           optional:
2611         * @param       boolean $track          optional:
2612         * @param       boolean $escapeText     optional:
2613         * @param       string  $title          optional:
2614         * @param       string  $class          optional:
2615         * @param   boolean $assumePageExists   optional:
2616         * @return      string  an HTML hyperlink (a href) element
2617         * @todo        move regexps to regexp-library          #34
2618         */
2619        function Link($tag, $handler='', $text='', $track=TRUE, $escapeText=TRUE, $title='', $class='', $assumePageExists=TRUE)
2620        {
2621                // init
2622                if (!$text)
2623                {
2624                        $text = $tag;
2625                }
2626                if ($escapeText)        // escape text?
2627                {
2628                        $text = $this->htmlspecialchars_ent($text);
2629                }
2630                $tag = $this->htmlspecialchars_ent($tag); #142 & #148
2631                $handler = $this->htmlspecialchars_ent($handler);
2632                $title_attr = $title ? ' title="'.$this->htmlspecialchars_ent($title).'"' : '';
2633                $url = '';
2634                $link = '';
2635
2636                // is this an interwiki link?
2637                // before the : should be a WikiName; anything after can be (nearly) anything that's allowed in a URL
2638                if (preg_match('/^([A-ZÄÖÜ][A-Za-zÄÖÜßäöü]+)[:](\S*)$/', $tag, $matches))       // @@@ FIXME #34 (inconsistent with Formatter)
2639                {
2640                        $url = $this->GetInterWikiUrl($matches[1], $matches[2]);
2641                        $class = 'interwiki';
2642                }
2643                // fully-qualified URL? this uses the same pattern as StaticHref() does;
2644                // it's a recognizing pattern, not a validation pattern
2645                // @@@ move to regex libary!
2646                elseif (preg_match('/^(http|https|ftp|news|irc|gopher):\/\/([^\\s\"<>]+)$/', $tag))
2647                {
2648                        $url = $tag; // this is a valid external URL
2649                        // add ext class only if URL is external
2650                        if (!preg_match('/'.$_SERVER['SERVER_NAME'].'/', $tag))
2651                        {
2652                                $class = 'ext';
2653                        }
2654                }
2655                // Is this an e-mail address?
2656                elseif (preg_match('/^.+\@.+$/', $tag))
2657                {
2658                        $url = 'mailto:'.$tag;
2659                        $class = 'mailto';
2660                }
2661                /*
2662                // check for protocol-less URLs
2663                elseif (!preg_match('/:/', $tag))
2664                {
2665                        $url = 'http://'.$tag;
2666                        $class = 'ext';
2667                }
2668                */
2669                else
2670                {
2671                        // it's a wiki link
2672                        if (isset($_SESSION['linktracking']) && $_SESSION['linktracking'] && $track)
2673                        {
2674                                $this->TrackLinkTo($tag);
2675                        }
2676                        if (!$assumePageExists && !$this->existsPage($tag))
2677                        {
2678                                $link = '<a class="missingpage" href="'.$this->Href('edit', $tag).'" title="'.T_("Create this page").'">'.$text.'</a>';
2679                        }
2680                        else
2681                        {
2682                                $link = '<a class="'.$class.'" href="'.$this->Href($handler, $tag).'"'.$title_attr.'>'.$text.'</a>';
2683                        }
2684                }
2685
2686                //return $url ? '<a class="'.$class.'" href="'.$url.'">'.$text.'</a>' : $text;
2687                if ('' != $url)
2688                {
2689                        $result = '<a class="'.$class.'" href="'.$url.'">'.$text.'</a>';
2690                }
2691                elseif ('' != $link)
2692                {
2693                        $result = $link;
2694                }
2695                else
2696                {
2697                        $result = $text;
2698                }
2699                return $result;
2700        }
2701
2702        /**
2703         * Takes an array of pages returned by LoadAll() and renders it as a table or unordered list.
2704         *
2705         * @author              {@link http://wikkawiki.org/DotMG DotMG}
2706         *
2707         * @access              public
2708         * @uses        Wakka::Link()
2709         * @uses        Wakka::PageTitle()
2710         *
2711         * @param       mixed   $pages                  required: Array of pages returned by LoadAll
2712         * @param       array $array_param An associative array representing the options to pass to the function ListPages
2713         *         The following keys are currently supported :
2714         *            nopagesText: Text to display when the list is empty, default: empty string
2715         *            class: a space separated list of classNames used for styling the enclosing div or table tag
2716         *            compact: If 0, use table; if 1: use unordered list. Default: 0
2717         *            columns: If compact = 0, number of columns of the table. Default: 3
2718         *            show_edit_link: If true, each page is followed by an edit link. Default: false.
2719         *            show_page_title: If true, show the title of the page after the page name. Default: true.
2720         *            sort: Should the data be sorted before being listed? Default: no (no sorting)
2721         *  Other possible values for sort:
2722         *   ignore_case ou ksort: sort page names, ignoring case
2723         *   reverse ou rsort: sort in reverse order, not ignoring case
2724         *   ignore_case_reverse ou krsort: sort in reverse order, not ignoring case
2725         * @return      string  formated array contents
2726         * @todo        Use as a wrapper for the new array functions - avoiding table layout and enhancing scannability of the result!!!
2727         */
2728        function ListPages($pages, $array_param=array())
2729        {
2730                $defaut_options = array(
2731                                'nopagesText' => '',
2732                                'class' => '',
2733                                'compact' => 0,
2734                                'columns' => 3,
2735                                'show_edit_link' => false,
2736                                'show_page_title' => true,
2737                                'sort' => 'no');
2738                $options = array_merge($defaut_options, $array_param);
2739
2740                $output_edit_link = '';
2741                $output_page_title = '';
2742                $output_body = '';
2743                $class = '';
2744
2745                if (!$pages)
2746                {
2747                        return ($options['nopagesText']);
2748                }
2749                if ($options['class'])
2750                {
2751                        $class = ' class="'.str_replace('"', '', $options['class']).'"';
2752                }
2753                if ($options['compact'])
2754                {
2755                        $output_start = "\n<div".$class.'><ul>';
2756                        $output_end = '</ul></div>';
2757                }
2758                else
2759                {
2760                        $output_start = '<table '.$class.'><tr>';
2761                        $output_end = '</tr></table>';
2762                }
2763                // sorting
2764                foreach ($pages as $page)
2765                {
2766                        $k = strtolower($page['page_tag']);
2767                        $list[strtolower($k)] = $page['page_tag'];
2768                }
2769                switch ($options['sort'])
2770                {
2771                        case 'ignore_case': case 'ksort':
2772                                ksort($list);
2773                                break;
2774                        case 'no': case false: case 0:
2775                                break;
2776                        case 'reverse': case 'rsort':
2777                                rsort($list);
2778                                break;
2779                        case 'ignore_case_reverse': case 'krsort':
2780                                krsort($list);
2781                                break;
2782                        default:
2783                                sort($list);
2784                }
2785                $count = 0;
2786                foreach ($list as $val)
2787                {
2788                        if ($options['show_edit_link'])
2789                        {
2790                                $output_edit_link = ' <small>['.$this->Link($val, 'edit', WIKKA_PAGE_EDIT_LINK_DESC, false, true, sprintf(WIKKA_PAGE_EDIT_LINK_TITLE, $val)).']</small>';
2791                        }
2792                        if ($options['show_page_title'])
2793                        {
2794                                $output_page_title = ' <span class="pagetitle">['.$this->PageTitle($val).']</span>';
2795                        }
2796                        if ($options['compact'])
2797                        {
2798                                $text = preg_replace('!^Category!i', '', $val);
2799                                $output_link = $this->Link($val, '', $text);
2800                                $output_item_sep_begin = "\n <li>";
2801                                $output_item_sep_end = "</li>";
2802                                $output_body .= $output_item_sep_begin.$output_link.$output_edit_link.$output_page_title.$output_item_sep_end;
2803                        }
2804                        else
2805                        {
2806                                if ($count == intval($options['columns']))
2807                                {
2808                                        $output_body .= '</tr><tr>';
2809                                        $count = 0;
2810                                }
2811                                $output_link = $this->Link($val);
2812                                $output_item_sep_begin = "\n <td>";
2813                                $output_item_sep_end = "</td>";
2814                                $output_body .= $output_item_sep_begin.$output_link.$output_edit_link.$output_page_title.$output_item_sep_end;
2815                        }
2816                        $count ++;
2817                }
2818                return $output_start.$output_body.$output_end;
2819        }
2820
2821
2822        /**
2823         * Create a href for a static file.
2824         *
2825         * It takes a parameter $filepath, the path of the static file, and returns
2826         * a string representing a fully-qualified URL.
2827         * This function should be used everywhere a static file should be attached
2828         * to a wikkapage via XHTML tag attributes that expect a URL, such as href,
2829         * src, or archive tags, or attributes in elements in XML/RSS.
2830         *
2831         * Its main purpose is to avoid "path confusion" when a relative URL would be
2832         * attached to a <b>rewritten</b> (base) URL; without rewriting there's no
2833         * problem, but when mod_rewrite is active, it's really necessary:
2834         * a base_href doesn't help (and is in fact unnecessary when using
2835         * fully-qualified paths as returned by this method).
2836         *
2837         * @access      public
2838         * @uses WIKKA_BASE_DOMAIN_URL
2839         * @uses WIKKA_BASE_URL
2840         *
2841         * @param       string  $filepath       path for a static file; this can be either:
2842         *                              - a relative path
2843         *                              - an absolute path (starting with a slash)
2844         *                              - a fully-qualified URL (in which case the input is simply returned)
2845         * @return      string  a standardized fully-qualified URL
2846         */
2847        function StaticHref($filepath)
2848        {
2849#echo "\n<!--StaticHref - in: ".$filepath."-->\n";
2850                /*
2851                $result = $this->Href('dummyhandler','dummypagename');
2852                $result = str_replace('wikka.php?wakka=', '', $result);
2853                $result = str_replace('dummypagename/dummyhandler', $filepath, $result);
2854                */
2855                // fully-qualified URL? this uses the same pattern as Link() does;
2856                // it's a recognizing pattern, not a validation pattern
2857                // @@@ move to regex libary!
2858                if (preg_match('/^(http|https|ftp|news|irc|gopher):\/\/([^\\s\"<>]+)$/', $filepath))
2859                {
2860                        $result = $filepath;
2861                }
2862                elseif ('/' == substr($filepath,0,1))   // absolute path
2863                {
2864                        $result = WIKKA_BASE_DOMAIN_URL.$filepath;
2865                }
2866                else                                                            // relative path
2867                {
2868                        $result = WIKKA_BASE_URL.$filepath;
2869                }
2870#echo "<!--StaticHref - out: ".$result."-->\n";
2871                return $result;
2872        }
2873
2874        // function PregPageLink($matches) { return $this->Link($matches[1]); }
2875
2876        /**
2877         * Check if a given string contains prohibited characters.
2878         * Currently, these prohibited characters are:
2879         *   [ ] { } % + | ? = < > ' " / 0x00-0x1f 0x7f ,
2880         *
2881         * @param       string $text mandatory:
2882         * @return      integer 1 if $text is a wikiname, 0 otherwise
2883         * @todo        move regexps to regexp-library          #34
2884         * @todo        return a boolean
2885         */
2886        function IsWikiName($text)
2887        {
2888                $result = preg_match("/[\[\]\{\}%\+\|\?=<>\'\"\/\\x00-\\x1f\\x7f,]/", html_entity_decode($text));
2889                return !$result;
2890        }
2891
2892        /**
2893         *
2894         * @param       string  $tag    mandatory: (wiki) pagename the link points to.
2895         */
2896        function TrackLinkTo($tag)
2897        {
2898                $_SESSION['linktable'][] = $tag;
2899        }
2900
2901        /**
2902         *
2903         * @return      array
2904         */
2905        function GetLinkTable()
2906        {
2907                return $_SESSION['linktable'];
2908        }
2909
2910        /**
2911         *
2912         * @return      void
2913         */
2914        function ClearLinkTable()
2915        {
2916                $_SESSION['linktable'] = array();
2917        }
2918
2919        /**
2920         *
2921         * @return      void
2922         */
2923        function StartLinkTracking()
2924        {
2925                $_SESSION['linktracking'] = 1;
2926        }
2927
2928        /**
2929         *
2930         * @return      void
2931         */
2932        function StopLinkTracking()
2933        {
2934                $_SESSION['linktracking'] = 0;
2935        }
2936
2937        /**
2938         *
2939         * @uses        Wakka::GetLinkTable()
2940         * @uses        Wakka::Query()
2941         * @uses        Wakka::GetPageTag()
2942         * @uses        Config::$table_prefix
2943         * @return      void
2944         */
2945        function WriteLinkTable()
2946        {
2947                // delete entries for current page from link table
2948                $this->Query("
2949                        DELETE
2950                        FROM ".$this->GetConfigValue('table_prefix')."links
2951                        WHERE from_tag = '".mysql_real_escape_string($this->GetPageTag())."'"
2952                        );
2953                // build and insert new entries for current page in link table
2954                if ($linktable = $this->GetLinkTable())
2955                {
2956                        $from_tag = mysql_real_escape_string($this->GetPageTag());
2957                        $written = array();
2958                        $sql = '';
2959                        foreach ($linktable as $to_tag)
2960                        {
2961                                $lower_to_tag = strtolower($to_tag);
2962                                if ((!isset($written[$lower_to_tag])) && ($lower_to_tag != strtolower($from_tag)))
2963                                {
2964                                        if ($sql)
2965                                        {
2966                                                $sql .= ', ';
2967                                        }
2968                                        $sql .= "('".$from_tag."', '".mysql_real_escape_string($to_tag)."')";
2969                                        $written[$lower_to_tag] = 1;
2970                                }
2971                        }
2972                        if($sql)
2973                        {
2974                                $this->Query("
2975                                        INSERT INTO ".$this->GetConfigValue('table_prefix')."links
2976                                        VALUES ".$sql
2977                                        );
2978                        }
2979                }
2980        }
2981
2982        /**#@-*/
2983
2984        /*#@+
2985         * @category    Template methods
2986         */
2987
2988        /**
2989         * Add a custom header to be inserted inside the <head> section.
2990         *
2991         * @access      public
2992         * @uses        Wakka::$additional_headers
2993         *
2994         * @param       string  $additional_headers     any valid XHTML code that is legal inside the <head> section.
2995         * @param       string  $indent optional: indent string, default is a tabulation. This will be inserted before $additional_headers
2996         * @param       string  $sep    optional: separator string, this will separate your additional headers. This will be inserted after
2997         *                                      $additional_headers, default value is a line feed.
2998         * @return      void
2999         * @todo        Let the "displayer" of these headers handle indent and separator - code layout doesn't belong here
3000         */
3001        function AddCustomHeader($additional_headers, $indent = "\t", $sep = "\n")
3002        {
3003                $this->additional_headers[] = $indent.$additional_headers.$sep;
3004        }
3005
3006        /**
3007         * Output the header for Wikka-pages.
3008         *
3009         * @uses        Wakka::GetThemePath()
3010         * @uses        Wakka::IncludeBuffered()
3011         * @return      mixed string with the header of a wikka-page, string with an error-message or FALSE.
3012         */
3013        function Header()
3014        {
3015                $filename = 'header.php';
3016                $path = $this->GetThemePath();
3017                $header = $this->IncludeBuffered($filename, T_("A header template could not be found. Please make sure that a file called <code>header.php</code> exists in the templates directory."), '', $path);
3018                return $header;
3019        }
3020
3021        /**
3022         * Output the footer for Wikka-pages.
3023         *
3024         * @uses        Wakka::GetThemePath()
3025         * @uses        Wakka::IncludeBuffered()
3026         * @uses        mixed string with the footer of a wikka-page, string with an error-message or FALSE.
3027         */
3028        function Footer()
3029        {
3030                $filename = 'footer.php';
3031                $path = $this->GetThemePath();
3032                $footer = $this->IncludeBuffered($filename, T_("A footer template could not be found. Please make sure that a file called <code>footer.php</code> exists in the templates directory."), '', $path);
3033                return $footer;
3034        }
3035
3036        /**
3037     * Returns a valid template path (defaults to 'default' if theme
3038         * does not exist)
3039         *
3040         * Tries to resolve valid pathname given a 'theme' param in
3041         * wikka.config.php.  Failing that, tries to revert to a
3042         * "fallback" default theme path (currently 'templates/default').
3043         * Failing that, returns NULL.
3044         *
3045         * @uses        Wakka::GetUser()
3046         * @uses        Wakka::GetConfigValue()
3047         * @uses        Wakka::BuildFullpathFromMultipath()
3048         * @uses        Config::$theme
3049         * @uses        Config::wikka_template_path
3050         * @param  string path_sep Use this to override the OS default
3051         * DIRECTORY_SEPARATOR (usually used in conjunction with CSS path
3052         * generation). Default is DIRECTORY_SEPARATOR.
3053         * @param  string theme_override Specify a specific theme. Default is NULL (use configuration theme).
3054         *
3055     * @return string A fully-qualified pathname or NULL if none found
3056         */
3057         function GetThemePath($path_sep = DIRECTORY_SEPARATOR, $theme_override = NULL)
3058         {
3059                //check if custom theme is set in user preferences
3060                if ($user = $this->GetUser())
3061                {
3062                        $theme =  (isset($user['theme']) && $user['theme']!='')? $user['theme'] : $this->GetConfigValue('theme');
3063                }
3064                else
3065                {
3066                        $theme = $this->GetConfigValue('theme');
3067                }
3068                if(NULL !== $theme_override)
3069                {
3070                        $theme = $theme_override;
3071                }
3072                $path = $this->BuildFullpathFromMultipath($theme, $this->GetConfigValue('wikka_template_path'), $path_sep);
3073                if(FALSE===file_exists($path))
3074                {
3075                        // Check on fallback theme dir...
3076                        if(FALSE===file_exists('templates'.$path_sep.'default'))
3077                        {
3078                                return NULL;
3079                        }
3080                        else
3081                        {
3082                                return 'templates'.$path_sep.'default';
3083                        }
3084                }
3085                return $path;
3086        }
3087
3088        /**
3089        * Build a drop-down menu with a list of available themes
3090        *
3091        * This function reads the content of the templates/ and plugins/templates paths and builds
3092        * a list of available themes. Themes in the plugin tree override default themes with the same
3093        * name.
3094        * @since
3095        * @param string $default_theme optional: marks a specific theme as selected by default
3096        */
3097        function SelectTheme($default_theme='default')
3098        {
3099                $plugin = array();
3100                $core = array();
3101                // plugin path
3102                $hdl = opendir('plugins/templates');
3103                while ($g = readdir($hdl))
3104                {
3105                        if ($g[0] == '.') continue;
3106                        else
3107                        {
3108                                $plugin[] = $g;
3109                        }
3110                }
3111                // default path
3112                $hdl = opendir('templates');
3113                while ($f = readdir($hdl))
3114                {
3115                        if ($f[0] == '.') continue;
3116                        // theme override
3117                        else if (!in_array($f, $plugin))
3118                        {
3119                                $core[] = $f;
3120                        }
3121                }
3122                $output = '<select id="select_theme" name="theme">';
3123                $output .= '<option disabled="disabled">'.sprintf(T_("Default themes (%s)"), count($core)).'</option>';
3124                foreach ($core as $c)
3125                {
3126                        $output .= "\n ".'<option value="'.$c.'"';
3127                        if ($c == $default_theme) $output .= ' selected="selected"';
3128                        $output .= '>'.$c.'</option>';
3129                }
3130                //display custom themes if any
3131                if (count($plugin)>0)
3132                {
3133                        $output .= '<option disabled="disabled">'.sprintf(T_("Custom themes (%s)"), count($plugin)).'</option>';
3134                        foreach ($plugin as $p)
3135                        {
3136                                $output .= "\n ".'<option value="'.$p.'"';
3137                                if ($p == $default_theme) $output .= ' selected="selected"';
3138                                $output .= '>'.$p.'</option>';
3139                        }
3140                }
3141                $output .= '</select>';
3142                echo $output;
3143        }
3144
3145        /**#@-*/
3146
3147        /**
3148         * @category    Form methods
3149         */
3150
3151        /**
3152         * Build an opening form tag with specified or generated attributes.
3153         *
3154         * This method builds an opening form tag, taking care that the result is valid XHTML
3155         * no matter where the parameters come from: invalid parameters are ignored and defaults used.
3156         * This enables this method to be used with user-provided parameter values.
3157         *
3158         * The form will always have the required action attribute and an id attribute to provide
3159         * a 'hook' for styling and scripting. This method tries its best to ensure the id attribute
3160         * is unique, among other things by adding a 'form_' prefix to make it different from ids for
3161         * other elements.
3162         * For a file upload form ($file=TRUE) the appropriate method and enctype attributes are generated.
3163         *
3164         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman} (Advanced version: complete rewrite; 2005)
3165         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
3166         *
3167         * @access      public
3168         * @uses        makeId()
3169         * @uses        ID_LENGTH
3170         * @uses        existsHandler()
3171         * @uses        existsPage()
3172         * @uses        Href()
3173         * @uses        MiniHref()      only for hidden field
3174         *
3175         * @param       string  $handler        optional: "handler" which consists of handler name and possibly a query string
3176         *                                                              to be used as part of action attribute
3177         * @param       string  $tag            optional: page name to be used for action attribute;
3178         *                                                              if not specified, the current page will be used
3179         * @param       string  $formMethod     optional: method attribute; must be POST (default) or GET;
3180         *                                                              anything but POST is ignored and considered as GET;
3181         *                                                              always converted to lowercase
3182         * @param       string  $id                     optional: id attribute
3183         * @param       string  $class          optional: class attribute
3184         * @param       boolean $file           optional: specifies whether there will be a file upload field;
3185         *                                                              default: FALSE; if TRUE sets method attribute to POST and generates
3186         *                                                              appropriate enctype attribute
3187         * @return      string opening form tag
3188         * @todo        extend to handle a complete (external) URL instead of (handler+)pagename
3189         * @todo        extend to allow extra attributes
3190         */
3191        function FormOpen($handler='', $tag='', $formMethod='post', $id='', $class='', $file=FALSE, $anchor='')
3192        {
3193                // init
3194                $attrMethod = '';                                                                       // no method for HTML default 'get'
3195                $attrClass = '';
3196                $attrEnctype = '';                                                                      // default no enctype -> HTML default application/x-www-form-urlencoded
3197                $hidden = array();
3198                // derivations
3199                $handler = trim($handler);
3200                $tag = trim($tag);
3201                $id = trim($id);
3202                $class = trim($class);
3203                $anchor = trim($anchor);
3204                // validations
3205                #$validHandler = $this->existsHandler($handler);
3206                #$validPage = $this->existsPage($tag);
3207                // validation needed only if parameters are actually specified
3208                #$handler = ($validHandler) ? $handler : '';
3209                if (!empty($handler) && !$this->existsHandler($handler))
3210                {
3211                        $handler = '';
3212                }
3213                #$tag = ($validPage) ? $tag : '';
3214                if (!empty($tag) && !$this->existspage($tag))
3215                {
3216                        $tag = '';      // Href() will pick up current page name if none specified
3217                }
3218
3219                // form action (action is a required attribute!)
3220                // !!! If rewrite mode is off, "tag" has to be passed as a hidden field
3221                // rather than part of the URL (where it gets ignored on submit!)
3222                if ($this->GetConfigValue('rewrite_mode'))
3223                {
3224                        // @@@ add passed extra GET params here by passing them as extra
3225                        // parameter to Href()
3226                        $attrAction = ' action="'.$this->Href($handler, $tag).$anchor.'"';
3227                }
3228                else
3229                {
3230                        $attrAction = ' action="'.$this->Href($handler, $tag).$anchor.'"';
3231                        // #670: This value will short-circuit the value of wakka=... in URL.
3232                        $hidden['wakka'] = $this->MiniHref($handler, ('' == $tag ? $this->GetPageTag(): $tag));
3233                        // @@@ add passed extra GET params here by adding them as extra
3234                        // entries to $hidden (probably not by adding them to Href()
3235                        // but that needs to be tested when we get to it!)
3236                }
3237                // form method (ignore anything but post) and enctype
3238                if (TRUE === $file)
3239                {
3240                        $attrMethod  = ' method="post"';                                // required for file upload
3241                        $attrEnctype = ' enctype="multipart/form-data"';// required for file upload
3242                }
3243                elseif (preg_match('/^post$/i',$formMethod))            // ignore case...
3244                {
3245                        $attrMethod = ' method="post"';                                 // ...but generate lowercase
3246                }
3247                // form id
3248                if ('' == $id)                                                                          // if no id given, generate one based on other parameters
3249                {
3250                        $id = substr(md5($handler.$tag.$formMethod.$class),0,ID_LENGTH);
3251                }
3252                $attrId = ' id="'.$this->makeId('form',$id).'"';        // make sure we have a unique id
3253                // form class
3254                if ('' != $class)
3255                {
3256                        $attrClass = ' class="'.$class.'"';
3257                }
3258
3259                // add validation key fields used against FormSpoofing
3260                if('post' == $formMethod)
3261                {
3262                        $hidden['CSRFToken'] = $_SESSION['CSRFToken'];
3263                }
3264
3265                // build HTML fragment
3266                $fragment = '<form'.$attrAction.$attrMethod.$attrEnctype.$attrId.$attrClass.'>'."\n";
3267                // construct and add hidden fields (necessary if we are NOT using rewrite mode)
3268                if (count($hidden) > 0)
3269                {
3270                        $fragment .= '<fieldset class="hidden">'."\n";
3271                        foreach ($hidden as $name => $value)
3272                        {
3273                                $fragment .= '  <input type="hidden" name="'.$name.'" value="'.$value.'" />'."\n";
3274                        }
3275                        $fragment .= '</fieldset>'."\n";
3276                }
3277
3278                // return resulting HTML fragment
3279                return $fragment;
3280        }
3281
3282        /**
3283         * Close form
3284         *
3285         * @return      string  the XHTML tag to close a form and a newline.
3286         */
3287        function FormClose()
3288        {
3289                $result = '</form>'."\n";
3290                return $result;
3291        }
3292
3293        /**#@-*/
3294
3295        /**#@+
3296         * @category    Interwiki
3297         */
3298
3299        /**
3300         * Read the list of interWikis from interwiki.conf.
3301         *
3302         * interwiki.conf in the main dir of wikka holds a list of urls to other
3303         * websites and a shortcut for them, making it possible to use shortcuts
3304         * to their pages instead of the full URL.
3305         *
3306         * The file must have only one entry per line consisting of:
3307         * shortcut full_URL
3308         *
3309         * @uses        Wakka::AddInterWiki()
3310         * @todo        allow multiple spaces and/or tabs as delimiter
3311         */
3312        function ReadInterWikiConfig()
3313        {
3314                if ($lines = file('interwiki.conf'))
3315                {
3316                        foreach ($lines as $line)
3317                        {
3318                                if ($line = trim($line))
3319                                {
3320                                        list($wikiName, $wikiUrl) = explode(' ', trim($line));  // @@@ allow any tabs/spaces, not just single space
3321                                        $this->AddInterWiki($wikiName, $wikiUrl);
3322                                }
3323                        }
3324                }
3325        }
3326
3327        /**
3328         * Add an interWiki to the interWiki list.
3329         *
3330         * @param       string  $name   mandatory: shortcut for the interWiki
3331         * @param       string  $url    mandatory: url for the interwiki
3332         */
3333        function AddInterWiki($name, $url)
3334        {
3335                $this->interWiki[strtolower($name)] = $url;
3336        }
3337
3338        /**
3339         * Return the full URL of an interwiki for a given shortcut, if in the list.
3340         *
3341         * @param  string $name mandatory: the shortcut for the interWiki
3342         * @param  string $tag  mandatory: name of a page in the other wiki
3343         * @return string the full URL for $tag or an empty string
3344         * @todo        for 1.3: in trunk the function returns an empty string if the IW is not in the list
3345         */
3346        function GetInterWikiUrl($name, $tag)
3347        {
3348                if (isset($this->interWiki[strtolower($name)]))
3349                {
3350                        return $this->interWiki[strtolower($name)].$tag;
3351                }
3352        }
3353
3354        /**#@-*/
3355
3356        /*#@+
3357         * @category    Referrers
3358         */
3359
3360        /**
3361         * Log REFERRERS.
3362         * Store external referrer into table wikka_referrers. The referrer's host is
3363         * checked against a blacklist (table wikka_blacklist) and it will be ignored
3364         * if it's present at this table.
3365         *
3366         * @uses        Wakka::cleanUrl()
3367         * @uses        Wakka::GetConfigValue()
3368         * @uses        Wakka::LoadSingle()
3369         * @uses        Wakka::Query()
3370         * @uses        Config::$table_prefix
3371         * @param       $tag
3372         * @param       $referrer
3373         * @return      void
3374         */
3375        function LogReferrer($tag = '', $referrer = '')
3376        {
3377                // fill values
3378                if (!$tag = trim($tag))
3379                {
3380                        #$tag = $this->GetPageTag();
3381                        $tag = $this->GetPageTag();
3382                }
3383                #if (!$referrer = trim($referrer)) $referrer = $_SERVER["HTTP_REFERER"]; NOTICE
3384                if (empty($referrer))
3385                {
3386                        $referrer = (isset($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : '';  #38
3387                }
3388                $referrer = trim($this->cleanUrl($referrer));                   # secured JW 2005-01-20
3389
3390                // check if it's coming from another site
3391                if (!empty($referrer) && !preg_match('/^'.preg_quote(WIKKA_BASE_URL, '/').'/', $referrer))
3392                {
3393                        $parsed_url = parse_url($referrer);
3394                        $spammer = $parsed_url['host'];
3395                        $blacklist = $this->LoadSingle("
3396                                SELECT *
3397                                FROM ".$this->GetConfigValue('table_prefix')."referrer_blacklist
3398                                WHERE spammer = '".mysql_real_escape_string($spammer)."'"
3399                                );
3400                        if (FALSE == $blacklist)
3401                        {
3402                                $this->Query("
3403                                        INSERT INTO ".$this->GetConfigValue('table_prefix')."referrers
3404                                        SET page_tag    = '".mysql_real_escape_string($tag)."',
3405                                                referrer        = '".mysql_real_escape_string($referrer)."',
3406                                                time            = now()"
3407                                        );
3408                        }
3409                }
3410        }
3411
3412        /**
3413         * @uses        Wakka::LoadAll()
3414         * @uses        Wakka::GetConfigValue()
3415         * @uses        Config::$table_prefix
3416         * @param $tag
3417         * @return unknown_type
3418         */
3419        function LoadReferrers($tag = '')
3420        {
3421                $where = ($tag = trim($tag)) ? "                        WHERE page_tag = '".mysql_real_escape_string($tag)."'" : '';
3422                $referrers = $this->LoadAll("
3423                        SELECT referrer, COUNT(referrer) AS num
3424                        FROM ".$this->GetConfigValue('table_prefix')."referrers".
3425                        $where."
3426                        GROUP BY referrer
3427                        ORDER BY num DESC"
3428                        );
3429                return $referrers;
3430        }
3431
3432        /**#@-*/
3433
3434        /*#@+
3435         * @category    PLUGINS: Actions/Handlers
3436         */
3437
3438        /**
3439         * Handle the call to an action.
3440         *
3441         * @uses        Wakka::GetConfigValue()
3442         * @uses        Wakka::IncludeBuffered()
3443         * @uses        Wakka::StartLinkTracking()
3444         * @uses        Wakka::StopLinkTracking()
3445         *
3446         * @param       string  $actionspec     mandatory: the complete content of the action "tag"
3447         * @param       int             $forcelinktracking      optional: set to TRUE (or something that evaluates to it...)
3448         *                                      to ensure that the included content is tracked for links; default: 0
3449         * @return      string  output produced by {@link Wakka::IncludeBuffered()} or an error message
3450         * @todo        move regexes to central regex library                   #34
3451         * @todo        use action config files (e.g., pass only specified parameters)  #446
3452         * @todo        don't use numbers when booleans are intended! TRUE and FALSE advertize their intention much clearer
3453         */
3454        function Action($actionspec, $forceLinkTracking = 0)    // @@@
3455        {
3456                // parse action spec and check if we have a syntactically valid action name     [SEC]
3457                // the regex allows an action name consisting of letters and numbers ONLY
3458                // and thus provides defense against directory traversal or XSS (via action *name*)
3459                if (!preg_match('/^\s*([a-zA-Z0-9]+)(\s.+?)?\s*$/', $actionspec, $matches))     # see also #34
3460                {
3461                        return '<em class="error">'.T_("Unknown action; the action name must not contain special characters.").'</em>'; # [SEC]
3462                }
3463                else
3464                {
3465                        // valid action name, so we pull out the parts, and make the action name lowercase
3466                        $action_name    = strtolower($matches[1]);
3467                        $paramlist              = (isset($matches[2])) ? trim($matches[2]) : '';
3468                }
3469
3470                // prepare an array for extract() (in $this->IncludeBuffered()) to work with
3471                $vars = array();
3472                // search for parameters if there was more than just a (syntactically valid) action name
3473                if ('' != $paramlist)
3474                {
3475                        // match all attributes (key and value)
3476                        preg_match_all('/([a-zA-Z0-9]+)=(\"|\')(.*)\\2/U', $paramlist, $matches);       # [SEC] parameter name should not be empty
3477
3478                        // prepare an array for extract() (in $this->IncludeBuffered()) to work with
3479                        #$vars = array();
3480                        if (is_array($matches))
3481                        {
3482                                for ($a = 0; $a < count($matches[0]); $a++)
3483                                {
3484                                        // The parameter value is sanitized using htmlspecialchars_ent();
3485                                        // if an action really needs "raw" HTML as input it can
3486                                        // still be "unescaped" by the action itself; otherwise,
3487                                        // any HTML will be displayed _as code_, but not interpreted.
3488                                        // For any other action htmlspecialchars_ent() guards against
3489                                        // XSS via user-supplied action parameters.
3490                                        // NOTE 1:      this may not provide *complete* protection against XSS!
3491                                        // NOTE 2:      It is still the responsibility of each action
3492                                        //                      to validate its own parameters!
3493                                        //                      That includes guarding against directory traversal.
3494                                        // Check to see if linktracking is desired (for
3495                                        // instance, when using {{image}} tags to link to
3496                                        // other wiki pages
3497                                        if(FALSE !== strpos($matches[1][$a], "forceLinkTracking")) 
3498                                        {
3499                                                if(TRUE == $this->htmlspecialchars_ent($matches[3][$a]))
3500                                                {
3501                                                        $forceLinkTracking = 1; 
3502                                                }
3503                                                else
3504                                                {
3505                                                        $forceLinkTracking = 0;
3506                                                }
3507                                        }
3508                                        else 
3509                                        {
3510                                                $vars[$matches[1][$a]] = $this->htmlspecialchars_ent($matches[3][$a]);  // parameter name = sanitized value [SEC]
3511                                        }
3512                                }
3513                        }
3514                        $vars['wikka_vars'] = $paramlist; // <<< add the complete parameter-string to the array
3515                }
3516                if (!$forceLinkTracking)
3517                {
3518                                /**
3519                                 * @var boolean holds previous state of LinkTracking before we StopLinkTracking(). It will then be used to test if we should StartLinkTracking() or not.
3520                                 */
3521                                $link_tracking_state = (isset($_SESSION['linktracking'])) ? $_SESSION['linktracking'] : 0;
3522                                $this->StopLinkTracking();
3523                }
3524                $result =
3525                $this->IncludeBuffered(strtolower($action_name).DIRECTORY_SEPARATOR.strtolower($action_name).'.php',
3526                sprintf(T_("Unknown action \"%s\""), '"'.$action_name.'"'), $vars, $this->GetConfigValue('action_path'));
3527                if ($link_tracking_state)
3528                {
3529                        $this->StartLinkTracking();
3530                }
3531                return $result;
3532        }
3533
3534        /**
3535         * Use a handler (on the current page).
3536         *
3537         * @uses        Wakka::GetConfigValue()
3538         * @uses        Wakka::IncludeBuffered()
3539         * @uses        Wakka::wrapHandlerError()
3540         * @uses        Config::$handler_path
3541         *
3542         * @param       string  $handler        mandatory: name of handler to execute
3543         * @return      string  output produced by {@link Wakka::IncludeBuffered()} or an error message
3544         * @todo        use templating class            JW: more likely to be used in handler itself!
3545         * @todo        use handler config files;                               #446
3546         * @todo        move regexes to central regex library                   #34
3547         * @todo        implement further validation instead of simply extracting the part after the last slash
3548         *                      -OR- handle this in wikka.php through more intelligent parsing
3549         */
3550        function Handler($handler)
3551        {
3552                if (strstr($handler, '/'))
3553                {
3554                        // Observations - MK 2007-03-30
3555                        // extract part after the last slash (if the whole request contained multiple slashes)
3556                        // @@@
3557                        // but should such requests be accepted in the first place?
3558                        // at least it is a SORT of defense against directory traversal (but not necessarily XSS)
3559                        // NOTE that name syntax check now takes care of XSS
3560                        $handler = substr($handler, strrpos($handler, '/')+1);
3561                }
3562                // check valid handler name syntax (similar to Action())
3563                // @todo move regexp to library
3564                if (!preg_match('/^([a-zA-Z0-9_.-]+)$/', $handler)) // allow letters, numbers, underscores, dashes and dots only (for now); see also #34
3565                {
3566                        return $this->wrapHandlerError(T_("Unknown handler; the handler name must not contain special characters."));   # [SEC]
3567                }
3568                else
3569                {
3570                        // valid handler name; now make sure it's lower case
3571                        $handler = strtolower($handler);
3572                }
3573                $handlerLocation = $handler.DIRECTORY_SEPARATOR.$handler.'.php';        #89
3574                $tempOutput = $this->IncludeBuffered($handlerLocation, '', '', $this->GetConfigValue('handler_path'));
3575                if (FALSE===$tempOutput)
3576                {
3577                        return $this->wrapHandlerError(sprintf(T_("Sorry, %s is an unknown handler."), '"'.$handlerLocation.'"'));
3578                }
3579                return $tempOutput;
3580        }
3581
3582        /**
3583         * Wrap a error message in a content div and an em tag, to avoid breaking the layout on handler errors.
3584         *
3585         * @author              {@link http://wikkawiki.org/TormodHaugen Tormod Haugen} (created 2010)
3586         *
3587         * @uses        Wakka::htmlspecialchars_ent
3588         *
3589         * @param       string $errorMessage    Localized error message to be wrapped to avoid breaking layout
3590         * @return      string The wrapped error message
3591         */
3592        function wrapHandlerError($errorMessage)
3593        {
3594                $errorMessage = $this->htmlspecialchars_ent(trim($errorMessage));
3595                $errorMessage = '<div id="content"><em class="error">'.$errorMessage.'</em></div>';
3596
3597                return $errorMessage;
3598        }
3599
3600        /**
3601         * Check if a handler (specified after page name) really exists.
3602         *
3603         * May be passed as handler plus query string; we'll need to look at handler only
3604         * so we strip off any querystring first.
3605         *
3606         * @author              {@link http://wikkawiki.org/JavaWoman JavaWoman} (created 2005; rewrite 2007)
3607         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
3608         *
3609         * @access      public
3610         * @uses        Wakka::GetConfigValue()
3611         *
3612         * @param       string  $handler        handler name, optionally with appended parameters
3613         * @return      boolean TRUE if handler is found, FALSE otherwise
3614         */
3615        function existsHandler($handler)
3616        {
3617                // first strip off any query string
3618                $parts = preg_split('/&/',$handler,1);                          # return only one part
3619                $handler = $parts[0];
3620#echo 'handler: '.$handler.'<br/>';
3621                // now check if a handler by that name exists
3622#echo 'checking path: '.$this->GetConfigValue('handler_path').DIRECTORY_SEPARATOR.'page'.DIRECTORY_SEPARATOR.$handler.'.php'.'<br/>';
3623                $exists = $this->BuildFullpathFromMultipath($handler.DIRECTORY_SEPARATOR.$handler.'.php', $this->GetConfigValue('handler_path'));
3624                // return conclusion
3625                if(TRUE===empty($exists))
3626                {
3627                        return FALSE;
3628                }
3629                return TRUE;
3630        }
3631
3632        /**
3633         * Render a string using a given formatter or the standard Wakka by default.
3634         *
3635         * @uses        Config::$wikka_formatter_path
3636         * @uses        Wakka::GetConfigValue()
3637         * @uses        Wakka::IncludeBuffered()
3638         *
3639         * @param       string  $text                   the source text to format
3640         * @param       string  $formatter              the name of the formatter. This name is linked to a file with the same name, located in the folder
3641         *                      specified by {@link Config::$wikka_formatter_path}, and with extension .php; which is called to process the text $text
3642         * @param       string  $format_option  a comma separated list of string options, in the form of 'option1;option2;option3'
3643         *                      this value is passed to compact() to re-create the variable on formatters/wakka.php
3644         * @return      string  output produced by {@link Wakka::IncludeBuffered()} or an error message
3645         * @todo        move regexes to central regex library                   #34
3646         */
3647        function Format($text, $formatter='wakka', $format_option='')
3648        {
3649                // check valid formatter name syntax (same as Handler())
3650                // the regex allows an action name consisting of letters, numbers, and
3651                // underscores, hyphens and dots ONLY and thus provides defense against
3652                // directory traversal or XSS (via handler *name*)
3653                if (!preg_match('/^([a-zA-Z0-9_.-]+)$/', $formatter)) # see also #34
3654                {
3655                        $out = '<em class="error">'.T_("Unknown formatter; the formatter name must not contain special characters.").'</em>';   # [SEC]
3656                }
3657                else
3658                {
3659                        // valid formatter name; now make sure it's lower case
3660                        $formatter = strtolower($formatter);
3661                        // prepare variables
3662                        $formatter_location                     = $formatter.'.php';
3663                        $formatter_location_disp        = '<code>'.$this->htmlspecialchars_ent($formatter_location).'</code>';  // [SEC] make error (including (part of) request) safe to display
3664                        $formatter_not_found            = sprintf(T_("Formatter \"%s\" not found"),$formatter_location_disp);
3665                        // produce output
3666                        //$out = $this->IncludeBuffered($formatter_location, $this->GetConfigValue('wikka_formatter_path'), $formatter_not_found, FALSE, compact('text', 'format_option')); // @@@
3667                        $out = $this->IncludeBuffered($formatter_location, $formatter_not_found, compact('text', 'format_option'), $this->GetConfigValue('wikka_formatter_path'));
3668                }
3669                return $out;
3670        }
3671
3672        /**#@-*/
3673
3674        /*#@+
3675         *@category     User
3676         */
3677
3678        /**
3679         * Authenticate a user from (persistent) cookies.
3680         *
3681         * @uses        Wakka::GetConfigValue()
3682         * @uses        Wakka::LoadAll()
3683         *
3684         * @return      boolean TRUE if user authenticated from cookie, FALSE if not
3685         */
3686        function authenticateUserFromCookies()
3687        {
3688                // init
3689                $result = NULL;
3690                $c_username     = $this->getWikkaCookie('user_name');
3691                $c_pass         = $this->getWikkaCookie('pass');
3692                // find user(s)
3693                $users = $this->LoadAll("
3694                        SELECT *
3695                        FROM ".$this->GetConfigValue('table_prefix')."users
3696                        WHERE name = '".mysql_real_escape_string($c_username)."'"
3697                        );
3698                // evaluate result
3699                if (is_array($users))
3700                {
3701                        $count = count($users);
3702                }
3703                switch (TRUE)
3704                {
3705                        case (FALSE === $users):
3706                                $result = FALSE;                // query failed!!       @@@ notify admin
3707                                break;
3708                        case ($count > 1):
3709                                $result = FALSE;                // multiple users by same name: DB error!!      @@@ notify admin
3710                                break;
3711                        case ($count == 0):
3712                                $result = FALSE;                // not a registered user
3713                                break;
3714                        default:                                        // $count == 1 - OK: one user found
3715                                break;
3716                }
3717                // OK so far, check password
3718                if (NULL === $result)
3719                {
3720                        $user_rec = $users[0];          // get first (single) row
3721                        if (isset($user_rec['challenge']) && isset($user_rec['password']))
3722                        {
3723                                $pwd = md5($user_rec['challenge'].$user_rec['password']);
3724                                if ($c_pass != $pwd)
3725                                {
3726                                        $result = FALSE;        // "No, not authenticated"
3727                                }
3728                                else
3729                                {
3730                                        // valid password supplied: $user data is authenticated:
3731                                        // cache username and login user
3732                                        $result = TRUE;
3733                                        $this->registered_users[] = $user_rec['name'];  // cache actual name as in DB
3734                                        $this->loginUser($user_rec);
3735                                }
3736                        }
3737                        else
3738                        {
3739                                $result = FALSE;                // incomplete record: DB error!!
3740                        }
3741                }
3742                return $result;                                 // will be either TRUE or FALSE
3743        }
3744
3745        /**
3746         * Load data for a given user (by name).
3747         *
3748         * Attempts to load the user data from the database, and if successful,
3749         * adds the user name to the registered user name cache.
3750         *
3751         * If the data was successfully retrieved, the user data is returned
3752         * in an array; if not, FALSE is returned.
3753         *
3754         * @uses        Wakka::registered_users
3755         * @uses        Wakka::LoadSingle()
3756         * @uses        Wakka::GetConfigValue()
3757         *
3758         * @param       string  $username       mandatory: user name to retrieve data for
3759         * @return      mixed   array with user data if successful, FALSE otherwise
3760         */
3761        function loadUserData($username)
3762        {
3763                // data retrieval by name: get from database
3764                $user = $this->LoadSingle("
3765                        SELECT *
3766                        FROM ".$this->GetConfigValue('table_prefix')."users
3767                        WHERE name = '".mysql_real_escape_string($username)."'
3768                        LIMIT 1"
3769                        );
3770                if (is_array($user))
3771                {
3772                        // store user name in cache
3773                        $this->registered_users[] = $user['name'];      // cache actual name as in DB
3774                }
3775                // return results
3776                return $user;
3777        }
3778
3779        /**
3780         * Load a given user.
3781         *
3782         * in trunk: <b>Replaced by {@link Wakka::authenticateUserFromCookies()},
3783         * {@link Wakka::existsUser()} or {@link Wakka::loadUserData()} depending on
3784         * purpose!</b>
3785         *
3786         * @param $name
3787         * @param $password
3788         * @return unknown_type
3789         * @todo        see above
3790         */
3791        function LoadUser($name, $password = 0)
3792        {
3793                return $this->LoadSingle("
3794                        SELECT *
3795                        FROM ".$this->GetConfigValue('table_prefix')."users
3796                        WHERE name = '".mysql_real_escape_string($name)."' ".($password === 0 ? "" : "and password = '".mysql_real_escape_string($password)."'")."
3797                        LIMIT 1"
3798                        );
3799        }
3800
3801        /**
3802         * Load all users registered at the wiki from the database.
3803         *
3804         * @uses        Wakka::GetConfigValue()
3805         * @uses        Wakka::LoadAll()
3806         *
3807         * @return      array   contains data for all users
3808         * $todo        add 'start' and 'max' parameters to support paging
3809         */
3810        function LoadUsers()
3811        {
3812                $users = $this->LoadAll("
3813                        SELECT *
3814                        FROM ".$this->GetConfigValue('table_prefix')."users
3815                        ORDER BY name"
3816                        );
3817                return $users;
3818        }
3819
3820        /**
3821         * Get the name or (IP/hostname) address of the current user.
3822         *
3823         * If the user is not logged-in, the host name is only looked up if enabled
3824         * in the config (since it can lead to long page generation times).
3825         * Set 'enable_user_host_lookup' in wikka.config.php to 1 to do the look-up.
3826         * Otherwise the ip-address is used.
3827         *
3828         * @uses        Wakka::GetUser()
3829         * @uses        Wakka::GetConfigValue()
3830         * @uses        Config::$enable_user_host_lookup
3831         *
3832         * @return      string  name of registered user, or IP address or host name for
3833         *                      anonymous user
3834         * @todo        return only IP address or host name if explicitly requested:
3835         *                      we may want IP address even if reverse DNS is allowed in config!
3836         */
3837        function GetUserName()
3838        {
3839                if ($user = $this->GetUser())
3840                {
3841                        return $name = $user['name'];
3842                }
3843
3844                $ip = $_SERVER['REMOTE_ADDR'];
3845
3846                if ($this->GetConfigValue('enable_user_host_lookup') == 1)      // #240
3847                {
3848                        $ip = gethostbyaddr($ip) ? gethostbyaddr($ip) : $ip;
3849                }
3850
3851                return $this->anon_username = $ip;
3852        }
3853
3854        /**
3855         * Get data for logged-in user (NULL if user is not logged in).
3856         *
3857         * @return      mixed   array with user data, or FALSE if user not logged in
3858         */
3859        function GetUser()
3860        {
3861                return (isset($_SESSION['user'])) ? $_SESSION['user'] : NULL;
3862        }
3863
3864        /**
3865         *
3866         * @uses        Wakka::SetPersistentCookie()
3867         * @param       $user
3868         * @return      void
3869         */
3870        function SetUser($user)
3871        {
3872                $_SESSION['user'] = $user;
3873                $this->SetPersistentCookie('user_name', $user['name']);
3874                $this->SetPersistentCookie('pass', $user['password']);
3875                $this->registered = true;
3876        }
3877
3878        /**
3879         *
3880         * @uses        Wakka::GetConfigValue()
3881         * @uses        Wakka::DeleteCookie()
3882         * @uses        Wakka::GetUserName()
3883         * @uses        Wakka::Query()
3884         * @return unknown_type
3885         */
3886        function LogoutUser()
3887        {
3888                unset($_SESSION['show_comments']);
3889                $this->DeleteCookie('user_name');
3890                $this->DeleteCookie('pass');
3891                // Delete this session from sessions table
3892                $this->Query("DELETE FROM ".$this->GetConfigValue('table_prefix')."sessions WHERE userid='".$this->GetUserName()."' AND sessionid='".session_id()."'");
3893                $_SESSION['user'] = '';
3894                // This seems a good as place as any to purge all session records
3895                // older than PERSISTENT_COOKIE_EXPIRY, as this is not a
3896                // time-critical function for the user.  The assumption here
3897                // is that server-side sessions have long ago been cleaned up by PHP.
3898                $this->Query("
3899                        DELETE FROM ".$this->GetConfigValue('table_prefix')."sessions
3900                        WHERE DATE_SUB(NOW(), INTERVAL ".PERSISTENT_COOKIE_EXPIRY." SECOND) > session_start"
3901                        );
3902                $this->registered = false;
3903        }
3904
3905        /**
3906         * Returns user comment default style.
3907         *
3908         * If the user is not logged-in, comments are hidden by default.
3909         *
3910         * Must test for false condition with
3911         * "FALSE===UserWantsComments()" since this function may also
3912         * legally return a zero value.
3913         *
3914         * @uses        Wakka::GetUser()
3915         * @uses        Config::$default_comment_display
3916         * @param       tag             Page title
3917         * @return      mixed   threadtype if the user wants comments, FALSE otherwise
3918         */
3919        function UserWantsComments($tag)
3920        {
3921                if (!$user = $this->GetUser())
3922                {
3923                        $showcomments = FALSE;
3924                }
3925                elseif (!isset($user['show_comments'][$tag]))
3926                {
3927                        if (isset($user['default_comment_display']))
3928                        {
3929                                $showcomments = $user['default_comment_display'];       // user's default comment display
3930                        }
3931                        elseif (isset($config['default_comment_display']))
3932                        {
3933                                $showcomments = $config['default_comment_display'];     // configured default comment display
3934                        }
3935                        else
3936                        {
3937                                $showcomments = COMMENT_ORDER_DATE_ASC;                         // system default comment display
3938                        }
3939                }
3940                else
3941                {
3942                        $showcomments = $user['show_comments'][$tag];                   // user's preference for the given page
3943                }
3944                return $showcomments;
3945        }
3946
3947         /**
3948         * Formatter for user names.
3949         *
3950         * Renders usernames as links only when needed, avoiding the creation of
3951         * missing page links for users without a userpage. Makes other options
3952         * configurable (like truncating long hostnames or disabling link formatting).
3953         *
3954         * @author      {@link http://wikkawiki.org/DarTar Dario Taraborelli}
3955         *
3956         * @uses        Wakka::existsUser()
3957         * @uses        Wakka::existsPage()
3958         * @uses        Wakka::Link()
3959         *
3960         * @param       string  $username       mandatory: name of user or hostname retrieved from the DB;
3961         * @param       boolean $link   optional: enables/disables linking to userpage;
3962         * @param       string  $maxhostlength  optional: max length for hostname, hostnames longer
3963         *                                      then this will be truncated with an ellipsis;
3964         * @param       string  $ellipsis       optional: character (or string) to be used at the end of truncated hosts;
3965         * @return      string  $formatted_user: formatted username.
3966         * @todo        use constant for ellipsis
3967         * @todo        better title attribute text: a user page is not a 'profile'
3968         * @todo        internationalization (marked with #i18n)
3969         */
3970        function FormatUser($username, $link=TRUE, $maxhostlength=MAX_HOSTNAME_LENGTH_DISPLAY, $ellipsis='&#8230;')
3971        {
3972                global $debug;
3973                if (strlen($username) > 0)
3974                {
3975                        // check if user is registered
3976                        #if ($this->LoadUser($username))        // only checks if user is registered
3977                        if ($this->existsUser($username))
3978                        {
3979                                // check if userpage exists and if linking is enabled
3980                                #$formatted_user = ($this->existsPage($username) && ($link == 1)) ? $this->Link($username,'','','','','Open user profile for '.$username,'user') : '<span class="user">'.$username.'</span>'; // @@@ #i18n
3981                                $formatted_user = ($this->existsPage($username) && ((bool) $link)) ? $this->Link($username,'','','','','Open user profile for '.$username,'user') : '<span class="user">'.$username.'</span>'; // @@@ #i18n
3982                        }
3983                        else
3984                        {
3985                                // user is not registered (or no longer(!) e.g., user may have
3986                                // edited a page but since "unregistered": then we have a user
3987                                // name here, not a host name)
3988                                // truncate long (host) names
3989                                $formatted_user = (strlen($username) > $maxhostlength) ? '<span class="user_anonymous" title="'.$username.'">'.substr($username, 0, $maxhostlength).$ellipsis.'</span>' : '<span class="user_anonymous">'.$username.'</span>';
3990                        }
3991                }
3992                else
3993                {
3994                        // no user (page has empty user field)
3995                        $formatted_user = 'anonymous'; // @@@ #i18n T_("(.T_("unregistered user").'") or T_("anonymous")
3996                }
3997                return $formatted_user;
3998        }
3999
4000        /**
4001         * Check whether a given (or implied) user is (currently) registered.
4002         *
4003         * If no username is supplied, it simply returns the current "registered"
4004         * state from the object variable. It also maintains a "cache" of registered
4005         * usernames which is checked before resorting to a database query.
4006         *
4007         * @uses        Wakka::registered
4008         * @uses        Wakka::registered_users
4009         * @uses        Wakka::GetConfigValue()
4010         * @uses        Wakka::LoadSingle()
4011         *
4012         * @param       string  $username       optional: when omitted, "registered" state
4013         *                                      for current user is returned; when given, we check whether
4014         *                                      the username occurs in the cache or database.
4015         * @return      boolean TRUE is user is registered, FALSE otherwise
4016         */
4017        function existsUser($username=NULL)
4018        {
4019                global $debug;
4020                // init
4021                $result = FALSE;
4022                // looking for current user
4023                if (!is_string($username))
4024                {
4025                        $result = $this->registered;
4026                }
4027                // named user cached?
4028                elseif (in_array($username, $this->registered_users))
4029                {
4030                        $result = TRUE;
4031                }
4032                elseif (in_array($username,$this->anon_users))
4033                {
4034                        $result = FALSE;
4035                }
4036                // look up named user in database & cache name
4037                else
4038                {
4039                        $user = $this->LoadSingle("
4040                                SELECT `name`
4041                                FROM ".$this->GetConfigValue('table_prefix')."users
4042                                WHERE `name` = '".mysql_real_escape_string($username)."'
4043                                LIMIT 1"
4044                                );
4045                        if (is_array($user))
4046                        {
4047                                $result = TRUE;
4048                                $this->registered_users[] = $user['name'];      // cache actual name as in DB
4049                        }
4050                        else
4051                        {
4052                                // also cache UNregistered usernames
4053                                $this->anon_users[] = $username;                // @@@ declare & document
4054                        }
4055                }
4056                return $result;
4057        }
4058
4059        /**#@-*/
4060
4061        /*#@+
4062         * @category Comments
4063         */
4064
4065        /**
4066         * Load the comments for a (given) page.
4067         *
4068         * @uses        Wakka::GetConfigValue()
4069         * @uses        Wakka::LoadAll()
4070         * @uses        Wakka::TraverseComments()
4071         *
4072         * @param       string  $tag    mandatory: name of the page
4073         * @param       integer $order  optional: order of comments. Default: COMMENT_ORDER_DATE_ASC
4074         * @return      array   All the comments for this page ordered by $order
4075         * @todo        make single exit point to enable profiling
4076         */
4077        function LoadComments($tag, $order=NULL)
4078        {
4079                // default
4080                if ($order == NULL)
4081                {
4082                        if (isset($_SESSION['show_comments'][$tag]))
4083                        {
4084                                $order = $_SESSION['show_comments'][$tag];
4085                        }
4086                        else
4087                        {
4088                                $order = COMMENT_ORDER_DATE_ASC;
4089                        }
4090                }
4091                // handle requested order
4092                if ($order == COMMENT_ORDER_DATE_ASC)   // Return ASC by date
4093                {
4094                        // always returns an array, but it may be empty
4095                        return $this->LoadAll("
4096                                SELECT *
4097                                FROM ".$this->GetConfigValue('table_prefix')."comments
4098                                WHERE page_tag = '".mysql_real_escape_string($tag)."'
4099                                        AND (status IS NULL or status != 'deleted')
4100                                ORDER BY time"
4101                                );
4102                }
4103                elseif ($order == COMMENT_ORDER_DATE_DESC)
4104                {
4105                        // always returns an array, but it may be empty
4106                        return $this->LoadAll("
4107                                SELECT *
4108                                FROM ".$this->GetConfigValue('table_prefix')."comments
4109                                WHERE page_tag = '".mysql_real_escape_string($tag)."'
4110                                        AND (status IS NULL or status != 'deleted')
4111                                ORDER BY time DESC"
4112                                );
4113                }
4114                elseif ($order == COMMENT_ORDER_THREADED)
4115                {
4116                        $record = array();
4117                        $this->TraverseComments($tag, $record);
4118                        return $record;
4119                }
4120        }
4121
4122        /**
4123         * Select and load a single comment.
4124         *
4125         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
4126         * @copyright   Copyright © 2005, Marjolein Katsma
4127         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
4128         *
4129         * @access      public
4130         * @uses        LoadAll()
4131         *
4132         * @param       integer $comment_id     required: id of comment to be deleted
4133         * @return      array                           associative array with comment data.
4134         */
4135        function loadCommentId($comment_id)
4136        {
4137                return $this->LoadSingle("SELECT * FROM ".$this->GetConfigValue('table_prefix')."comments WHERE id = '".$comment_id."'");
4138        }
4139
4140        /**
4141         * Traverse comments in threaded order
4142         *
4143         * @uses        Wakka::GetConfigValue()
4144         * @uses        Wakka::LoadAll()
4145         * @uses        Wakka::CountAllComments()
4146         * @uses        Wakka::LoadSingle()
4147         * @uses        Wakka::TraverseComments()
4148         *
4149         * @param       string  $tag    mandatory: name of the page
4150         * @param       array   &$graph mandatory: empty array
4151         * @return      array   Ordered graph of comments and indent levels (values) for this page
4152         */
4153        function TraverseComments($tag, &$graph)
4154        {
4155                static $level = -1;
4156                static $visited = array();
4157                static $transformed_map = array();
4158                if (!$transformed_map)
4159                {
4160                        array_push($visited, 'NULL');
4161                        $count = $this->CountAllComments($tag); // redundant: just count($initial_map) after the query
4162                        // @@@ miss option for sort order here???
4163                        $initial_map = $this->LoadAll("
4164                                SELECT id, parent
4165                                FROM ".$this->GetConfigValue('table_prefix')."comments
4166                                WHERE page_tag = '".$tag."'
4167                                ORDER BY id ASC"
4168                                );
4169                        // Create an array of arrays, with the (unique) key of
4170                        // 'parent' pointing to an array of date-ordered
4171                        // children.
4172                        for ($i=0; $i<$count; ++$i)     // prefer to use $i++ here (even if equivalent)
4173                        {
4174                                $id = $initial_map[$i]['id'];
4175                                $parent = $initial_map[$i]['parent'];
4176                                if (!$parent)
4177                                {
4178                                        $parent = 'NULL';
4179                                }
4180                                if (!array_key_exists($parent, $transformed_map))
4181                                {
4182                                        $transformed_map[$parent] = array();
4183                                }
4184                                array_push($transformed_map[$parent], $id);
4185                        }
4186                }
4187                if (array_key_exists(end($visited), $transformed_map) && is_array($transformed_map[end($visited)]))
4188                {
4189                        $id = array_shift($transformed_map[end($visited)]);
4190                }
4191                if (isset($id))
4192                {
4193                        // Limit recursions to COMMENT_MAX_TRAVERSAL_DEPTH
4194                        if ($level >= COMMENT_MAX_TRAVERSAL_DEPTH)
4195                        {
4196                                --$level;
4197                                array_pop($visited);
4198                                $this->TraverseComments($tag, $graph);
4199                        }
4200                        else
4201                        {
4202                                // Traverse children
4203                                ++$level;
4204                                array_push($visited, $id);
4205                                // @@@  should check first whether LoadSingle() actually returns an
4206                                //              array, or FALSE in case the query fails (not found).
4207                                //              most of the other statements should probably not be
4208                                //              executed either if no result was returned from the database!
4209                                // @@@  can't the records be retrieved from $transformed_map instead?
4210                                $graph[] = $this->LoadSingle("
4211                                        SELECT *
4212                                        FROM ".$this->GetConfigValue('table_prefix')."comments
4213                                        WHERE id = ".$id
4214                                        );
4215                                end($graph);
4216                                $graph[key($graph)]['level'] = $level;
4217                                $this->TraverseComments($tag, $graph);
4218                        }
4219                }
4220                elseif ($level < 0)
4221                {
4222                        // End traversal
4223                        return;
4224                }
4225                else
4226                {
4227                        // Step back to the parent to find next child
4228                        --$level;
4229                        array_pop($visited);
4230                        $this->TraverseComments($tag, $graph);
4231                }
4232        }
4233
4234        /**
4235         * Count the undeleted comments for a (given) page.
4236         *
4237         * @uses        Wakka::getCount()
4238         *
4239         * @param       string $tag mandatory: name of the page
4240         * @return      integer Count of comments
4241         */
4242        function CountComments($tag)
4243        {
4244                $count = $this->getCount('comments', "page_tag = '".mysql_real_escape_string($tag)."' AND (status IS NULL OR status != 'deleted')");
4245                return $count;
4246        }
4247
4248        /**
4249         * Count all comments (deleted and undeleted) for a (given) page.
4250         *
4251         * @uses        Wakka::getCount()
4252         *
4253         * @param       string $tag mandatory: name of the page
4254         * @return      integer Count of comments
4255         */
4256        function CountAllComments($tag)
4257        {
4258                $count = $this->getCount('comments', "page_tag = '".mysql_real_escape_string($tag)."'");
4259                return $count;
4260        }
4261        /**
4262         * Load the last comments on the wiki, or, if specified, the last comments on a specific page.
4263         *
4264         * @uses        Wakka::GetConfigValue()
4265         * @uses        Wakka::LoadAll()
4266         * @uses        Wakka::GetUser()
4267         * @uses        Wakka::IsAdmin()
4268         * @uses        Config::$table_prefix
4269         *
4270         * @param       integer $limit  optional: number of last comments. default: 50
4271         * @param       string  $user   optional: name of user to retrieve comments for
4272         * @return      array   the last x comments
4273         * @todo        use constant for default limit value (no "magic numbers!")
4274         */
4275        function LoadRecentComments($limit=50, $user='')                // @@@
4276        {
4277                $where = 'WHERE';
4278                if(!empty($user) &&
4279                   ($this->GetUser() || $this->IsAdmin()))
4280                {
4281                        $where = " WHERE user = '".mysql_real_escape_string($user)."' AND ";
4282                }
4283                return $this->LoadAll("
4284                        SELECT *
4285                        FROM ".$this->GetConfigValue('table_prefix')."comments
4286                        ".$where." (status IS NULL or status != 'deleted')
4287                        ORDER BY time DESC
4288                        LIMIT ".intval($limit));
4289        }
4290
4291        /**
4292         * Load recently commented pages on the wiki.
4293         *
4294         * @uses        Wakka::GetConfigValue()
4295         * @uses        Wakka::LoadAll()
4296         * @uses        Wakka::GetUser()
4297         * @uses        Wakka::IsAdmin()
4298         * @uses        Config::$table_prefix
4299         *
4300         * @param       integer $limit  optional: number of last comments on different pages. default: 50
4301         * @param   string $user optional: list only comments by this user
4302         * @return      array   the last comments on x different pages
4303         * @todo        use constant for default limit value (no "magic numbers!")
4304         */
4305        function LoadRecentlyCommented($limit = 50, $user = '') // @@@
4306        {
4307                $where = ' AND 1 ';
4308                if(!empty($user) &&
4309                   ($this->GetUser() || $this->IsAdmin()))
4310                {
4311                        $where = " AND comments.user = '".mysql_real_escape_string($user)."' ";
4312                }
4313
4314                $sql = "
4315                        SELECT comments.id, comments.page_tag, comments.time, comments.comment, comments.user
4316                        FROM ".$this->GetConfigValue('table_prefix')."comments AS comments
4317                        LEFT JOIN ".$this->GetConfigValue('table_prefix')."comments AS c2
4318                                ON comments.page_tag = c2.page_tag
4319                                        AND comments.id < c2.id
4320                        WHERE c2.page_tag IS NULL
4321                                AND (comments.status IS NULL or comments.status != 'deleted')
4322                                        ".$where."
4323                        ORDER BY time DESC
4324                        LIMIT ".intval($limit);
4325                return $this->LoadAll($sql);
4326        }
4327
4328        /**
4329         * Save a given comment posted on a given page.
4330         *
4331         * @uses        Wakka::GetUserName()
4332         * @uses        Wakka::GetConfigValue()
4333         * @uses        Wakka::Query()
4334         *
4335         * @param       string  $page_tag       mandatory: name of the page
4336         * @param       string  $comment        mandatory: text of the comment
4337         * @param       mixed   $parent_id      optional:       integer id of parent comment
4338         */
4339        function SaveComment($page_tag, $comment, $parent_id)
4340        {
4341                // get current user
4342                $user = $this->GetUserName();
4343
4344                // add new comment
4345                $parent_id = mysql_real_escape_string($parent_id);
4346                if (!$parent_id)
4347                {
4348                        $parent_id = 'NULL';
4349                }
4350                $this->Query("
4351                        INSERT INTO ".$this->GetConfigValue('table_prefix')."comments
4352                        SET page_tag = '".mysql_real_escape_string($page_tag)."',
4353                                time = now(),
4354                                comment = '".mysql_real_escape_string($comment)."',
4355                                parent = ".$parent_id.",
4356                                user = '".mysql_real_escape_string($user)."'"
4357                        );
4358        }
4359
4360        /**
4361         * Delete a comment.
4362         *
4363         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
4364         * @copyright   Copyright © 2005, Marjolein Katsma
4365         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
4366         *
4367         * @access      public
4368         * @uses        Query()
4369         *
4370         * @param       integer $comment_id     required: id of comment to be deleted
4371         * @return      boolean                         TRUE if successful, FALSE otherwise.
4372         */
4373        function deleteComment($comment_id)
4374        {
4375                $rc = $this->Query("DELETE FROM ".$this->GetConfigValue('table_prefix')."comments ".
4376                                                        "WHERE id = '".$comment_id."'");
4377                return $rc;
4378        }
4379
4380        /**#@-*/
4381
4382        /*#@+
4383         * @category    ACCESS CONTROL
4384         */
4385
4386        /**
4387         * Check if current user is the owner of the current or a specified page.
4388         *
4389         * @access              public
4390         * @uses                Wakka::existsUser()
4391         * @uses                Wakka::IsAdmin()
4392         * @uses                Wakka::GetUserName()
4393         * @uses                Wakka::GetPageOwner()
4394         * @uses                Wakka::GetPageTag()
4395         *
4396         * @param       string  $tag    optional: page to be checked. Default: current page.
4397         * @return      boolean TRUE if the user is the owner, FALSE otherwise.
4398         */
4399        function UserIsOwner($tag = '')
4400        {
4401       
4402                // if not logged in, user can't be owner!
4403                if (!$this->GetUser())
4404                {
4405                        return FALSE;
4406                }
4407                // if user is admin, return true. Admin can do anything!
4408                if ($this->IsAdmin())
4409                {
4410                        return TRUE;
4411                }
4412
4413                // set default tag & check if user is owner
4414                if (!$tag = trim($tag)) $tag = $this->GetPageTag();
4415                if ($this->GetPageOwner($tag) == $this->GetUserName()) return TRUE;
4416        }
4417
4418        /**
4419         * Check if currently logged in user is listed in configuration list as admin.
4420         *
4421         * @access      public
4422         * @uses        Wakka::GetConfigValue()
4423         * @uses        Wakka::GetUserName()
4424         *
4425         * @param       string  $user
4426         * @return      boolean TRUE if the user is an admin, FALSE otherwise
4427         */
4428        function IsAdmin($user='')
4429        {
4430                $adminstring = $this->GetConfigValue('admin_users');
4431                $adminarray = explode(',' , $adminstring);
4432                if(TRUE===empty($user))
4433                {
4434                        $user = $this->GetUserName();
4435                }
4436                else if(is_array($user))
4437                {
4438                        $user = $user['name'];
4439                }
4440                foreach ($adminarray as $admin) {
4441                        if (trim($admin) == $user) return TRUE;
4442                }
4443        }
4444
4445        /**
4446         * Return the owner for a given or the current page, with a given revision time or the current version.
4447         *
4448         * @uses        Wakka::GetPageTag()
4449         * @uses        Wakka::LoadPage()
4450         *
4451         * @param       string  $tag    optional: name of the page. default: current page
4452         * @param       string  $time   optional: time (datetime format) of the page-revision. default: current version
4453         * @return      string  username of the owner of the page (empty if there is no owner)
4454         * @todo        make a more efficient query: we only need the owner column, not the whole page!
4455         */
4456        function GetPageOwner($tag = '', $time = '')
4457        {
4458                if (!$tag = trim($tag)) $tag = $this->GetPageTag();
4459                if ($page = $this->LoadPage($tag, $time))
4460                return $page['owner'];
4461        }
4462
4463        /**
4464         * Set page ownership of specified page to specified owner.
4465         *
4466         * @uses        Wakka::LoadUser()
4467         * @uses        Wakka::GetConfigValue()
4468         * @uses        Wakka::Query()
4469         *
4470         * @param       string  $tag    mandatory: name of the page
4471         * @param       string  $user   mandatory: name of the user
4472         * @todo        see if "(Public)" and "(Nobody)" have to be replaced by constants to allow i18n
4473         *                      JW: could keep these constants in the database but 'translate' them in the UI
4474         */
4475        function SetPageOwner($tag, $user)
4476        {
4477                // check if user exists
4478                if ('' != $user && ($this->LoadUser($user) || $user == '(Public)' || $user == '(Nobody)'))
4479                {
4480                        if ($user == '(Nobody)')
4481                        {
4482                                $user = '';
4483                        }
4484                        // update latest revision with new owner
4485                        $this->Query("
4486                                UPDATE ".$this->GetConfigValue('table_prefix')."pages
4487                                SET owner = '".mysql_real_escape_string($user)."'
4488                                WHERE tag = '".mysql_real_escape_string($tag)."'
4489                                        AND latest = 'Y'
4490                                LIMIT 1"
4491                                );
4492                }
4493        }
4494
4495        /**
4496         * Load the Access Control list for a given page and a given privilege.
4497         *
4498         * @uses        Wakka::GetConfigValue()
4499         * @uses        Wakka::LoadSingle()
4500         *
4501         * @param       string  $tag    mandatory:
4502         * @param       string  $privilege      mandatory:
4503         * @param       integer $useDefaults    optional:
4504         * @return      mixed   the page name and the acl or FALSE if not found
4505         * @todo        don't use numbers when booleans are intended! TRUE and FALSE advertize their intention much clearer
4506         * @todo        this should return a result in consistent form (no page_tag for
4507         *                      default, or included for DB result), with the ACL itself "normalized"
4508         *                      with only newline delimiters #226/comment8
4509         * @todo        make this return JUST an acl (normalized), not an array!
4510         */
4511        function LoadACL($tag, $privilege, $useDefaults = 1)    // @@@
4512        {
4513                if ((!$acl = $this->LoadSingle("
4514                        SELECT ".mysql_real_escape_string($privilege)."_acl
4515                        FROM ".$this->GetConfigValue('table_prefix')."acls
4516                        WHERE `page_tag` = '".mysql_real_escape_string($tag)."'
4517                        LIMIT 1"
4518                        )) && $useDefaults)
4519                {
4520                        $acl = array(
4521                                'page_tag' => $tag,                     // @@@ when is this needed? NEVER
4522                                $privilege.'_acl' => $this->GetConfigValue('default_'.$privilege.'_acl')
4523                                );
4524                }
4525                // @@@ normalize ACL before returning
4526                return $acl;
4527        }
4528
4529        /**
4530         * Load all Access Control lists for a given page.
4531         *
4532         * @uses        Wakka::GetConfigValue()
4533         * @uses        Wakka::GetConfigValue()
4534         * @uses        Wakka::LoadSingle()
4535         *
4536         * @param       string  $tag    mandatory: page to load ACLs for
4537         * @param       integer $useDefaults    optional:
4538         * @return      mixed   the page name and all acls or FALSE if not found
4539         * @todo        don't use numbers when booleans are intended! TRUE and FALSE advertize their intention much clearer
4540         * @todo        this should return a result with the ACLs "normalized" with only newline delimiters #226 comment 8
4541         * @todo        review usage: is page_tag really needed?
4542         * @todo        make function for retrieving (normalized!) defaults (using current list of ACLs)
4543         */
4544        function LoadAllACLs($tag, $useDefaults = 1)    // @@@
4545        {
4546                if ((!$acl = $this->LoadSingle("
4547                        SELECT *
4548                        FROM ".$this->GetConfigValue('table_prefix')."acls
4549                        WHERE `page_tag` = '".mysql_real_escape_string($tag)."'
4550                        LIMIT 1
4551                ")) && $useDefaults)
4552                {
4553                        $acl = array(
4554                                'page_tag' => $tag,
4555                                'read_acl' => $this->GetConfigValue('default_read_acl'),
4556                                'write_acl' => $this->GetConfigValue('default_write_acl'),
4557                                'comment_read_acl' => $this->GetConfigValue('default_comment_read_acl'),
4558                                'comment_post_acl' => $this->GetConfigValue('default_comment_post_acl')
4559                        );
4560                        // @@@ normalize ACLs
4561                }
4562                return $acl;
4563        }
4564
4565        /**
4566         * Save an Access Control List for a given privilege on a given page to the database.
4567         * If the ACL record doesn't already exist, it is first created with the
4568         * config defaults and updated with the passed privilege values.
4569         *
4570         * @uses        Wakka::GetConfigValue()
4571         * @uses        Wakka::LoadAllACLs()
4572         * @uses        Wakka::Query()
4573         *
4574         * @param       string  $tag    mandatory: name of the page
4575         * @param       string  $privilege      mandatory: name of the privilege
4576         * @param       string  $list   mandatory: a string containing the AC-Syntax
4577         * @todo        don't use numbers when booleans are intended! TRUE and FALSE advertize their intention much clearer
4578         * @todo        make function for retrieving (normalized!) defaults (using current list of ACLs)
4579         * @todo        rationalize combination with CloneACLs - too much duplication here
4580         */
4581        function SaveACL($tag, $privilege, $list)
4582        {
4583                // the $default will be put in the SET statement of the INSERT SQL for default values. It isn't used in UPDATE.
4584                $default = " read_acl = '', write_acl = '', comment_read_acl = '', comment_post_acl = '', ";
4585                // we strip the privilege_acl from default, to avoid redundancy
4586                $default = str_replace(" ".$privilege."_acl = '',", ' ', $default);
4587                if ($this->LoadACL($tag, $privilege, 0))
4588                {
4589                        $this->Query("
4590                                UPDATE ".$this->GetConfigValue('table_prefix')."acls
4591                                SET ".mysql_real_escape_string($privilege)."_acl = '".mysql_real_escape_string(trim(str_replace("\r", "", $list)))."'
4592                                WHERE page_tag = '".mysql_real_escape_string($tag)."'
4593                                LIMIT 1"
4594                                );
4595                }
4596                else
4597                {
4598                        $this->Query("
4599                                INSERT INTO ".$this->GetConfigValue('table_prefix')."acls
4600                                SET".$default." `page_tag` = '".mysql_real_escape_string($tag)."', ".mysql_real_escape_string($privilege)."_acl = '".mysql_real_escape_string(trim(str_replace("\r", "", $list)))."'"
4601                                );
4602                }
4603        }
4604
4605        /**
4606         * Split ACL list on pipes or commas, then trim any
4607         * whitespace. Return a pipe-delimited list. Used mainly
4608         * to remove carriage returns.
4609         *
4610         * @param       string  $list   mandatory: List of ACLs to trim
4611         * @return unknown_type
4612         */
4613        function TrimACLs($list)
4614        {
4615                $trimmed_list = '';
4616                foreach (explode("\n", $list) as $line)
4617                {
4618                        $line = trim($line);
4619                        $trimmed_list .= $line."\n";
4620                }
4621                return $trimmed_list;
4622        }
4623
4624        /**
4625         * Check to see if a user is a member of an ACL usergroup (i.e.,
4626         * the username appears within a set of "+" symbols).
4627         *
4628         * @param string $who   mandatory: Username
4629         * @param string $group mandatory: Name of page with list of users
4630         * @return boolean true if $who is member of $group
4631         */
4632    function isGroupMember($who, $group)
4633    {
4634        $thegroup=$this->LoadPage($group);
4635        if (isset($thegroup)) {
4636            $search = "+".$who."+"; // In the GroupListPages, the participants logins have to be embbeded inside '+' signs
4637            return (boolean)(substr_count($thegroup["body"], $search));
4638        }
4639        else return false;
4640    }
4641
4642
4643        /**
4644         * Determine if the (current) user has specified access for the specified page.
4645         *
4646         * Returns true if $username (defaults to current user) has $privilege
4647         * access on $page (defaults to current page).
4648         *
4649         * @uses        Wakka::ACLs
4650         * @uses        Wakka::existsUser()
4651         * @uses        Wakka::UserIsOwner()
4652         * @uses        Wakka::LoadACL()
4653         *
4654         * @param       string  $privilege      mandatory: privilege which shall be checked
4655         * @param       string  $tag    optional: name of the page default: current page
4656         * @param       string  $username       optional: name of the user default: current user
4657         * @return      boolean TRUE if user has access, FALSE if not.
4658         * @todo        move regexps to regexp-library          #34
4659         * @todo        the $username parameter is not currently used consistently; but it could be leveraged for allowing/denying access by IP address in ALCs #543
4660         */
4661        function HasAccess($privilege, $tag='', $username='')
4662        {
4663                // set defaults
4664                if (!$tag) $tag = $this->GetPageTag();
4665                if (!$username) $username = $this->GetUserName();
4666                                       
4667        // Get a user object for the named user
4668        $user = ($username == $this->GetUserName()) ? $this->GetUser() : $this->LoadUser($username);
4669                                       
4670                // If user is owner or admin, return true.
4671                // Owner and admin can do anything!
4672        if ($user != FALSE) {
4673           if ($this->IsAdmin($username) || $this->GetPageOwner($tag) == $username) return TRUE;
4674        }
4675
4676                // see whether user is registered
4677        $registered = $user != FALSE;
4678
4679                // load acl
4680                if ($tag == $this->GetPageTag())
4681                {
4682                        $acl = $this->ACLs[$privilege."_acl"];
4683                }
4684                else
4685                {
4686                        $tag_ACLs = $this->LoadAllACLs($tag);
4687                        $acl = $tag_ACLs[$privilege."_acl"];
4688                }
4689
4690                // fine fine... now go through acl
4691                foreach (explode("\n", $acl) as $line)
4692                {
4693                        // check for inversion character "!"
4694                        if (preg_match("/^[!](.*)$/", $line, $matches))
4695                        {
4696                                $negate = 1;
4697                                $line = $matches[1];
4698                        }
4699                        else
4700                        {
4701                                $negate = 0;
4702                        }
4703
4704                        // if there's still anything left... lines with just a "!" don't count!
4705                        if ($line)
4706                        {
4707                                switch ($line[0])
4708                                {
4709                                // comments
4710                                case "#":
4711                                        break;
4712                                // everyone
4713                                case "*":
4714                                        return !$negate;
4715                                // only registered users
4716                                case "+":
4717                                        // return ($this->registered) ? !$negate : false;
4718                                        return ($registered) ? !$negate : $negate;
4719                                // aha! a user entry.
4720                                default:
4721                                        if (strtolower($line) == strtolower($username))
4722                                        {
4723                                                return !$negate;
4724                                        }
4725                    // this may be a UserGroup so we check if $user
4726                                        // is part of the group
4727                    else if (($this->isGroupMember($username, $line)))
4728                    {
4729                        return !$negate;
4730                    }
4731                                }
4732                        }
4733                }
4734
4735                // tough luck.
4736                return FALSE;
4737        }
4738
4739        // ANTI-SPAM
4740        /**
4741         * Read contents of badwords file.
4742         *
4743         * Reads the content of the badwords file. Return contents as string if found, FALSE if not.
4744         *
4745         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
4746         * @copyright   Copyright © 2005, Marjolein Katsma
4747         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
4748         * @version             0.5
4749         *
4750         * @uses      DEFAULT_BADWORDS_PATH
4751         * @uses      Config::$badwords_path
4752         * @uses      Wakka::normalizeLines()
4753         *
4754         * @access              public
4755         *
4756         * @return              mixed           normalized file content (sorted) if found, FALSE if not
4757         */
4758        function readBadWords()
4759        {
4760                $badwordspath = $this->GetConfigValue('badwords_path', DEFAULT_BADWORDS_PATH);
4761                if (file_exists($badwordspath))
4762                {
4763                        $aBadWords = file($badwordspath);                               # get file as array so we can...
4764                        $aBadWords = array_unique($aBadWords);                  # ...remove duplicates...
4765                        function _rot13($val) {
4766                                return str_rot13($val);
4767                        };
4768                        $aBadWords = array_map("_rot13", $aBadWords);
4769
4770                        natcasesort($aBadWords);                                                # ...and sort
4771                        $badwords = $this->normalizeLines(implode('',$aBadWords));      # turn back into string
4772                }
4773                else
4774                {
4775                        $badwords = FALSE;
4776                }
4777                return $badwords;
4778        }
4779
4780        /**
4781         * Writes or rewrites the badwords file from a string with one word per line.
4782         *
4783         * Input must be a string with (preferably) one word per line; empty lines are filtered.
4784         * If the file exists, it is overwritten with the new content.
4785         * Returns TRUE if successful, FALSE otherwise.
4786         *
4787         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
4788         * @copyright   Copyright © 2005, Marjolein Katsma
4789         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
4790         * @version             0.6
4791         *
4792         * @uses      DEFAULT_BADWORDS_PATH
4793         * @uses      Config::$badwords_path
4794         * @uses      Wakka::writeFile()
4795         *
4796         * @access              public
4797         * @uses                writeFile()
4798         *
4799         * @param               string  $lines  lines with one bad word on each
4800         * @return              mixed                   bytes written if successful, FALSE otherwise.
4801         */
4802        function writeBadWords($lines)
4803        {
4804                $badwordspath = $this->GetConfigValue('badwords_path', DEFAULT_BADWORDS_PATH);
4805                $rc = FALSE;
4806                if (file_exists($badwordspath))
4807                {
4808                        // build content
4809                        $lines = $this->normalizeLines($lines);                 # normalize line endings (needed for explode!)
4810                        $lines = preg_replace('/[ \t]+/',"\n",$lines);  # split any multiple-word lines
4811                        $aBadWords = explode("\n",$lines);                              # turn into array so we can...
4812                        $aBadWords = array_unique($aBadWords);                  # ...remove duplicates
4813                        natcasesort($aBadWords);                                                # ...and sort
4814                        $badwords = '';
4815                        foreach ($aBadWords as $word)
4816                        {
4817                                if ('' !== $word) $badwords .= str_rot13($word)."\n";   # get rid of empty lines
4818                        }
4819                        $content = trim($badwords);
4820                        // write to file
4821                        $rc = $this->writeFile($badwordspath,$content);
4822                }
4823                return $rc;                                                                                     # number of bytes written or FALSE if writing failed
4824        }
4825
4826        /**
4827         * Retrieves badwords in a format ready for a RegEx, with '|' between each word.
4828         *
4829         * Turns the content of the badwords file into a RegEx (minus delimiters).
4830         * Return contents as a string if there is any; FALSE if file not found or empty.
4831         *
4832         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
4833         * @copyright   Copyright © 2005, Marjolein Katsma
4834         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
4835         * @version             0.5
4836         *
4837         * @access              public
4838         * @uses                readBadWords()
4839         *
4840         * @return              mixed           RegEx with words if found and not empty, FALSE otherwise
4841         */
4842        function getBadWords()
4843        {
4844                $badwords = $this->readBadWords();
4845                if (FALSE === $badwords || '' == $badwords)
4846                {
4847                        return FALSE;
4848                }
4849                else
4850                {
4851                        return '('.str_replace("\n",'|',$badwords).')';
4852                }
4853        }
4854
4855        /**
4856         * Check content to see if it contains any bad words.
4857         *
4858         * Uses a RegEx built by getBadWords() to check the given content.
4859         * Returns TRUE if teh content contains any of the bad words, FALSE otherwise.
4860         *
4861         * @author              {@link http://wikka.jsnx.com/JavaWoman JavaWoman}
4862         * @copyright   Copyright © 2005, Marjolein Katsma
4863         * @license             http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
4864         * @version             0.5
4865         *
4866         * @access              public
4867         * @uses                getBadWords()
4868         * @todo
4869         *
4870         * @param               string $content string to check for occurrence of bad words
4871         * @return              boolean                 TRUE if content contains badwords, FALSE otherwise
4872         */
4873        function hasBadWords($content)
4874        {
4875                $re = $this->getBadWords();
4876                if (FALSE === $re)
4877                {
4878                        return FALSE;                                   # no match since no words are defined
4879                }
4880                else
4881                {
4882                        return preg_match('/'.$re.'/i',$content);               # case-insensitive comparison
4883                }
4884        }
4885
4886        /**#@-*/
4887
4888        /**
4889         * Build a (possibly valid) filepath from a delimited list of paths
4890         *
4891         * This function takes a list of paths delimited by ":"
4892         * (Unix-style), ";" (Window-style), or "," (Wikka-style)  and
4893         * attempts to construct a fully-qualified pathname to a specific
4894         * file.  By default, this function checks to see if the file
4895         * pointed to by the fully-qualified pathname exists.  First valid
4896         * match wins.  Disabling this feature will return the first valid
4897         * constructed path (i.e, a path containing a valid directory, but
4898         * not necessarily pointing to an existant file).
4899         *
4900         * @param string $filename mandatory: filename to be used in
4901         *                      construction of fully-qualified filepath
4902         * @param string $pathlist mandatory: list of
4903         *                      paths (delimited by ":", ";", or ",")
4904         * @param  string path_sep Use this to override the OS default
4905     *              DIRECTORY_SEPARATOR (usually used in conjunction with CSS path
4906     *              generation). Default is DIRECTORY_SEPARATOR.
4907         * @param  boolean $checkIfFileExists optional: if TRUE, returns
4908         *                      only a pathname that points to a file that exists
4909         *                      (default)
4910         * @return string A fully-qualified pathname or NULL if none found
4911         */
4912        function BuildFullpathFromMultipath($filename, $pathlist, $path_sep = DIRECTORY_SEPARATOR, $checkIfFileExists=TRUE)
4913        {
4914                $paths = preg_split('/;|:|,/', $pathlist);
4915                if(empty($paths[0])) return NULL;
4916                if(FALSE === $checkIfFileExists)
4917                {
4918                        // Just return first directory that exists
4919                        foreach($paths as $path)
4920                        {
4921                                $path = trim($path);
4922                                if(file_exists($path))
4923                                {
4924                                        return $path.$path_sep.$filename;
4925                                }
4926                        }
4927                        return NULL;
4928                }
4929                foreach($paths as $path)
4930                {
4931                        $path = trim($path);
4932                        $fqfn = $path.$path_sep.$filename;
4933                        if(file_exists($fqfn)) return $fqfn;
4934                }
4935                return NULL;
4936        }
4937
4938        /**
4939         * MAINTENANCE
4940         */
4941
4942        /**
4943         * Purge referrers and old page revisions.
4944         *
4945         * @uses        Wakka::GetConfigValue()
4946         * @uses        Wakka::Query()
4947         * @uses        Config::$referrers_purge_time
4948         * @uses        Config::$pages_purge_time
4949         * @uses        Config::$table_prefix
4950         *
4951         */
4952        function Maintenance()
4953        {
4954                // purge referrers
4955                if ($days = $this->GetConfigValue("referrers_purge_time"))
4956                {
4957                        $this->Query("
4958                                DELETE FROM ".$this->GetConfigValue('table_prefix')."referrers
4959                                WHERE time < date_sub(now(), interval '".mysql_real_escape_string($days)."' day)"
4960                                );
4961                }
4962
4963                // purge old page revisions
4964                if ($days = $this->GetConfigValue("pages_purge_time"))
4965                {
4966                        $this->Query("
4967                                DELETE FROM ".$this->GetConfigValue('table_prefix')."pages
4968                                WHERE time < date_sub(now(), interval '".mysql_real_escape_string($days)."' day)
4969                                        AND latest = 'N'"
4970                                );
4971                        $this->Query("delete from ".$this->GetConfigValue('table_prefix')."pages where time < date_sub(now(), interval '".mysql_real_escape_string($days)."' day) and latest = 'N'");
4972                }
4973        }
4974
4975        /**
4976         * THE BIG EVIL NASTY ONE!
4977         *
4978         * @uses        Wakka::Footer()
4979         * @uses        Wakka::GetUser()
4980         * @uses        Wakka::GetCookie()
4981         * @uses        Wakka::GetMicroTime()
4982         * @uses        Wakka::Handler()
4983         * @uses        Wakka::Header()
4984         * @uses        Wakka::Href()
4985         * @uses        Wakka::LoadAllACLs()
4986         * @uses        Wakka::LoadPage()
4987         * @uses        Wakka::LoadUser()
4988         * @uses        Wakka::LogReferrer()
4989         * @uses        Wakka::ReadInterWikiConfig()
4990         * @uses        Wakka::Redirect()
4991         * @uses        Wakka::SetCookie()
4992         * @uses        Wakka::SetUser()
4993         * @uses        Wakka::SetPage()
4994         * @uses        Config::$root_page
4995         * @param $tag
4996         * @param $method
4997         * @return unknown_type
4998         */
4999        function Run($tag, $method = '')
5000        {
5001                $newtag = '';
5002                // Set default cookie path
5003                $base_url_path = preg_replace('/wikka\.php/', '', $_SERVER['SCRIPT_NAME']);
5004                $this->wikka_cookie_path = ('/' == $base_url_path) ? '/' : substr($base_url_path,0,-1);
5005
5006                // do our stuff!
5007                $this->wikka_url = ((bool) $this->GetConfigValue('rewrite_mode')) ? WIKKA_BASE_URL : WIKKA_BASE_URL.WIKKA_URL_EXTENSION;
5008                $this->config['base_url'] = $this->wikka_url; #backward compatibility
5009
5010                if (!$this->handler = trim($method)) $this->handler = 'show';
5011                if (!$this->tag = trim($tag)) $this->Redirect($this->Href('', $this->GetConfigValue('root_page')));
5012                if ($this->GetUser())
5013                {
5014                        $this->registered = true;
5015                }
5016                else
5017                {
5018                        if ($user = $this->LoadUser($this->GetCookie('user_name'), $this->GetCookie('pass'))) $this->SetUser($user);
5019                        if ((isset($_COOKIE['wikka_user_name'])) && ($user = $this->LoadUser($_COOKIE['wikka_user_name'], $_COOKIE['wikka_pass'])))
5020                        {
5021                                //Old cookies : delete them
5022                                SetCookie('wikka_user_name', '', 1, $this->wikka_cookie_path);
5023                                $_COOKIE['wikka_user_name'] = '';
5024                                SetCookie('wikka_pass', '', 1, $this->wikka_cookie_path);
5025                                $_COOKIE['wikka_pass'] = '';
5026                                $this->SetUser($user);
5027                        }
5028                }
5029                $this->SetPage($this->LoadPage($tag, $this->GetSafeVar('time', 'get'))); #312
5030
5031                $this->LogReferrer();
5032                $this->ACLs = $this->LoadAllACLs($this->GetPageTag());
5033                $this->ReadInterWikiConfig();
5034                if(!($this->GetMicroTime()%3)) $this->Maintenance();
5035
5036                if (preg_match('/\.(xml|mm)$/', $this->GetHandler()))
5037                {
5038                        header('Content-type: text/xml');
5039                        print($this->handler($this->GetHandler()));
5040                }
5041                // raw page handler
5042                elseif ($this->GetHandler() == "raw")
5043                {
5044                        header('Content-type: text/plain');
5045                        print($this->handler($this->GetHandler()));
5046                }
5047                // grabcode page handler
5048                elseif ($this->GetHandler() == 'grabcode')
5049                {
5050                        print($this->handler($this->GetHandler()));
5051                }
5052                elseif (preg_match('/\.(gif|jpg|png)$/', $this->GetHandler()))          # should not be necessary
5053                {
5054                        header('Location: images/' . $this->GetHandler());
5055                }
5056                elseif (preg_match('/\.css$/', $this->GetHandler()))                                    # should not be necessary
5057                {
5058                        header('Location: css/' . $this->GetHandler());
5059                }
5060                elseif(0 !== strcmp($newtag = preg_replace('/\s+/', '_', $tag), $tag))
5061                {
5062                        header("Location: ".$this->Href('', $newtag));
5063                }
5064                elseif($this->GetHandler() == 'html')
5065                {
5066                        header('Content-type: text/html');
5067                        print($this->handler($this->GetHandler()));
5068                }
5069                else
5070                {
5071                        //output page
5072                        $content_body = $this->handler($this->GetHandler());
5073                        echo $this->Header();
5074                        echo $content_body;
5075                        echo $this->Footer();
5076                }
5077        }
5078}
5079?>
Note: See TracBrowser for help on using the browser.