]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/Request.php
testsuite starters
[SourceForge/phpwiki.git] / lib / Request.php
1 <?php // -*-php-*-
2 rcs_id('$Id: Request.php,v 1.68 2004-10-12 13:13:19 rurban Exp $');
3 /*
4  Copyright (C) 2002,2004 $ThePhpWikiProgrammingTeam
5  
6  This file is part of PhpWiki.
7
8  PhpWiki is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or
11  (at your option) any later version.
12
13  PhpWiki is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  GNU General Public License for more details.
17
18  You should have received a copy of the GNU General Public License
19  along with PhpWiki; if not, write to the Free Software
20  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  */
22
23 // backward compatibility for PHP < 4.2.0
24 if (!function_exists('ob_clean')) {
25     function ob_clean() {
26         ob_end_clean();
27         ob_start();
28     }
29 }
30
31         
32 class Request {
33         
34     function Request() {
35         $this->_fix_magic_quotes_gpc();
36         $this->_fix_multipart_form_data();
37         
38         switch($this->get('REQUEST_METHOD')) {
39         case 'GET':
40         case 'HEAD':
41             $this->args = &$GLOBALS['HTTP_GET_VARS'];
42             break;
43         case 'POST':
44             $this->args = &$GLOBALS['HTTP_POST_VARS'];
45             break;
46         default:
47             $this->args = array();
48             break;
49         }
50         
51         $this->session = new Request_SessionVars; 
52         $this->cookies = new Request_CookieVars;
53         
54         if (ACCESS_LOG) {
55             if (! is_writeable(ACCESS_LOG)) {
56                 trigger_error
57                     (sprintf(_("%s is not writable."), _("The PhpWiki access log file"))
58                     . "\n"
59                     . sprintf(_("Please ensure that %s is writable, or redefine %s in config/config.ini."),
60                             sprintf(_("the file '%s'"), ACCESS_LOG),
61                             'ACCESS_LOG')
62                     , E_USER_NOTICE);
63             }
64             else
65                 $this->_log_entry = & new Request_AccessLogEntry($this,
66                                                                 ACCESS_LOG);
67         }
68         
69         $GLOBALS['request'] = $this;
70     }
71
72     function get($key) {
73         if (!empty($GLOBALS['HTTP_SERVER_VARS']))
74             $vars = &$GLOBALS['HTTP_SERVER_VARS'];
75         else // cgi or other servers than Apache
76             $vars = &$GLOBALS['HTTP_ENV_VARS'];
77
78         if (isset($vars[$key]))
79             return $vars[$key];
80
81         switch ($key) {
82         case 'REMOTE_HOST':
83             $addr = $vars['REMOTE_ADDR'];
84             if (defined('ENABLE_REVERSE_DNS') && ENABLE_REVERSE_DNS)
85                 return $vars[$key] = gethostbyaddr($addr);
86             else
87                 return $addr;
88         default:
89             return false;
90         }
91     }
92
93     function getArg($key) {
94         if (isset($this->args[$key]))
95             return $this->args[$key];
96         return false;
97     }
98
99     function getArgs () {
100         return $this->args;
101     }
102     
103     function setArg($key, $val) {
104         if ($val === false)
105             unset($this->args[$key]);
106         else
107             $this->args[$key] = $val;
108     }
109     
110     // Well oh well. Do we really want to pass POST params back as GET?
111     function getURLtoSelf($args = false, $exclude = array()) {
112         $get_args = $this->args;
113         if ($args)
114             $get_args = array_merge($get_args, $args);
115
116         // Err... good point...
117         // sortby buttons
118         if ($this->isPost()) {
119             $exclude = array_merge($exclude, array('action','auth'));
120             //$get_args = $args; // or only the provided
121             /*
122             trigger_error("Request::getURLtoSelf() should probably not be from POST",
123                           E_USER_NOTICE);
124             */
125         }
126
127         foreach ($exclude as $ex) {
128             if (!empty($get_args[$ex])) unset($get_args[$ex]);
129         }
130
131         $pagename = $get_args['pagename'];
132         unset ($get_args['pagename']);
133         if (!empty($get_args['action']) and $get_args['action'] == 'browse')
134             unset($get_args['action']);
135
136         return WikiURL($pagename, $get_args);
137     }
138
139     function isPost () {
140         return $this->get("REQUEST_METHOD") == "POST";
141     }
142
143     function isGetOrHead () {
144         return in_array($this->get('REQUEST_METHOD'),
145                         array('GET', 'HEAD'));
146     }
147
148     function httpVersion() {
149         if (!preg_match('@HTTP\s*/\s*(\d+.\d+)@', $this->get('SERVER_PROTOCOL'), $m))
150             return false;
151         return (float) $m[1];
152     }
153     
154     function redirect($url, $noreturn=true) {
155         $bogus = defined('DISABLE_HTTP_REDIRECT') and DISABLE_HTTP_REDIRECT;
156         
157         if (!$bogus) {
158             header("Location: $url");
159             /*
160              * "302 Found" is not really meant to be sent in response
161              * to a POST.  Worse still, according to (both HTTP 1.0
162              * and 1.1) spec, the user, if it is sent, the user agent
163              * is supposed to use the same method to fetch the
164              * redirected URI as the original.
165              *
166              * That means if we redirect from a POST, the user-agent
167              * supposed to generate another POST.  Not what we want.
168              * (We do this after a page save after all.)
169              *
170              * Fortunately, most/all browsers don't do that.
171              *
172              * "303 See Other" is what we really want.  But it only
173              * exists in HTTP/1.1
174              *
175              * FIXME: this is still not spec compliant for HTTP
176              * version < 1.1.
177              */
178             $status = $this->httpVersion() >= 1.1 ? 303 : 302;
179
180             $this->setStatus($status);
181         }
182
183         if ($noreturn) {
184             include_once('lib/Template.php');
185             $this->discardOutput();
186             $tmpl = new Template('redirect', $this, array('REDIRECT_URL' => $url));
187             $tmpl->printXML();
188             $this->finish();
189         }
190         else if ($bogus) {
191             return JavaScript("
192               function redirect(url) {
193                 if (typeof location.replace == 'function')
194                   location.replace(url);
195                 else if (typeof location.assign == 'function')
196                   location.assign(url);
197                 else
198                   window.location = url;
199               }
200               redirect('" . addslashes($url) . "')");
201         }
202     }
203
204     /** Set validators for this response.
205      *
206      * This sets a (possibly incomplete) set of validators
207      * for this response.
208      *
209      * The validator set can be extended using appendValidators().
210      *
211      * When you're all done setting and appending validators, you
212      * must call checkValidators() to check them and set the
213      * appropriate headers in the HTTP response.
214      *
215      * Example Usage:
216      *  ...
217      *  $request->setValidators(array('pagename' => $pagename,
218      *                                '%mtime' => $rev->get('mtime')));
219      *  ...
220      *  // Wups... response content depends on $otherpage, too...
221      *  $request->appendValidators(array('otherpage' => $otherpagerev->getPageName(),
222      *                                   '%mtime' => $otherpagerev->get('mtime')));
223      *  ...
224      *  // After all validators have been set:
225      *  $request->checkValidators();
226      */
227     function setValidators($validator_set) {
228         if (is_array($validator_set))
229             $validator_set = new HTTP_ValidatorSet($validator_set);
230         $this->_validators = $validator_set;
231     }
232     
233     /** Append more validators for this response. 
234      *  i.e dependencies on other pages mtimes
235      *  now it may be called in init also to simplify client code.
236      */ 
237     function appendValidators($validator_set) {
238         if (!isset($this->_validators)) {
239             $this->setValidators($validator_set);
240             return;
241         }
242         $this->_validators->append($validator_set);
243     }
244     
245     /** Check validators and set headers in HTTP response
246      *
247      * This sets the appropriate "Last-Modified" and "ETag"
248      * headers in the HTTP response.
249      *
250      * Additionally, if the validators match any(all) conditional
251      * headers in the HTTP request, this method will not return, but
252      * instead will send "304 Not Modified" or "412 Precondition
253      * Failed" (as appropriate) back to the client.
254      */
255     function checkValidators() {
256         $validators = &$this->_validators;
257         
258         // Set validator headers
259         if (($etag = $validators->getETag()) !== false)
260             header("ETag: " . $etag->asString());
261         if (($mtime = $validators->getModificationTime()) !== false)
262             header("Last-Modified: " . Rfc1123DateTime($mtime));
263
264         // Set cache control headers
265         $this->cacheControl();
266
267         if (CACHE_CONTROL == 'NO_CACHE')
268             return;             // don't check conditionals...
269         
270         // Check conditional headers in request
271         $status = $validators->checkConditionalRequest($this);
272         if ($status) {
273             // Return short response due to failed conditionals
274             $this->setStatus($status);
275             print "\n\n";
276             $this->discardOutput();
277             $this->finish();
278             exit();
279         }
280     }
281
282     /** Set the cache control headers in the HTTP response.
283      */
284     function cacheControl($strategy=CACHE_CONTROL, $max_age=CACHE_CONTROL_MAX_AGE) {
285         if ($strategy == 'NO_CACHE') {
286             $cache_control = "no-cache";
287             $max_age = -20;
288         }
289         elseif ($strategy == 'ALLOW_STALE' && $max_age > 0) {
290             $cache_control = sprintf("max-age=%d", $max_age);
291         }
292         else {
293             $cache_control = "must-revalidate";
294             $max_age = -20;
295         }
296         header("Cache-Control: $cache_control");
297         header("Expires: " . Rfc1123DateTime(time() + $max_age));
298         header("Vary: Cookie"); // FIXME: add more here?
299     }
300     
301     function setStatus($status) {
302         if (preg_match('|^HTTP/.*?\s(\d+)|i', $status, $m)) {
303             header($status);
304             $status = $m[1];
305         }
306         else {
307             $status = (integer) $status;
308             $reason = array('200' => 'OK',
309                             '302' => 'Found',
310                             '303' => 'See Other',
311                             '304' => 'Not Modified',
312                             '400' => 'Bad Request',
313                             '401' => 'Unauthorized',
314                             '403' => 'Forbidden',
315                             '404' => 'Not Found',
316                             '412' => 'Precondition Failed');
317             // FIXME: is it always okay to send HTTP/1.1 here, even for older clients?
318             header(sprintf("HTTP/1.1 %d %s", $status, $reason[$status]));
319         }
320
321         if (isset($this->_log_entry))
322             $this->_log_entry->setStatus($status);
323     }
324
325     function buffer_output($compress = true) {
326         if (defined('COMPRESS_OUTPUT')) {
327             if (!COMPRESS_OUTPUT)
328                 $compress = false;
329         }
330         elseif (!check_php_version(4,2,3))
331             $compress = false;
332         elseif (isCGI()) // necessary?
333             $compress = false;
334             
335         if ($GLOBALS['request']->getArg('start_debug'))
336             $compress = false;
337         // Should we compress even when apache_note is not available?
338         // sf.net bug #933183 and http://bugs.php.net/17557
339         // This effectively eliminates CGI, but all other servers also. hmm.
340         if ($compress 
341             and (!function_exists('ob_gzhandler') 
342                  or !function_exists('apache_note'))) 
343             $compress = false;
344
345         // New: we check for the client Accept-Encoding: "gzip" presence also
346         // This should eliminate a lot or reported problems.
347         if ($compress
348             and (!$this->get("HTTP_ACCEPT_ENCODING")
349                  or !strstr($this->get("HTTP_ACCEPT_ENCODING"), "gzip")))
350             $compress = false;
351
352         // Most RSS clients are NOT(!) application/xml gzip compatible yet. 
353         // Even if they are sending the accept-encoding gzip header!
354         // wget is, Mozilla, and MSIE no.
355         // Of the RSS readers only MagpieRSS 0.5.2 is. http://www.rssgov.com/rssparsers.html
356         // See also http://phpwiki.sourceforge.net/phpwiki/KnownBugs
357         if ($compress 
358             and $this->getArg('format') 
359             and strstr($this->getArg('format'), 'rss'))
360             $compress = false;
361
362         if ($compress) {
363             ob_start('ob_gzhandler');
364             /*
365              * Attempt to prevent Apache from doing the dreaded double-gzip.
366              *
367              * It would be better if we could detect when apache was going
368              * to zip for us, and then let it ... but I have yet to figure
369              * out how to do that.
370              */
371             if (function_exists('apache_note'))
372                 @apache_note('no-gzip', 1);
373         }
374         else {
375             // Now we alway buffer output.
376             // This is so we can set HTTP headers (e.g. for redirect)
377             // at any point.
378             // FIXME: change the name of this method.
379             ob_start();
380         }
381         $this->_is_buffering_output = true;
382     }
383
384     function discardOutput() {
385         if (!empty($this->_is_buffering_output)) {
386             ob_clean();
387             $this->_is_buffering_output = false;
388         } else {
389             trigger_error("Not buffering output", E_USER_NOTICE);
390         }
391     }
392     
393     function finish() {
394         session_write_close();
395         if (!empty($this->_is_buffering_output)) {
396             //header(sprintf("Content-Length: %d", ob_get_length()));
397             ob_end_flush();
398             $this->_is_buffering_output = false;
399         }
400         exit;
401     }
402
403     function getSessionVar($key) {
404         return $this->session->get($key);
405     }
406     function setSessionVar($key, $val) {
407         return $this->session->set($key, $val);
408     }
409     function deleteSessionVar($key) {
410         return $this->session->delete($key);
411     }
412
413     function getCookieVar($key) {
414         return $this->cookies->get($key);
415     }
416     function setCookieVar($key, $val, $lifetime_in_days = false, $path = false) {
417         return $this->cookies->set($key, $val, $lifetime_in_days, $path);
418     }
419     function deleteCookieVar($key) {
420         return $this->cookies->delete($key);
421     }
422     
423     function getUploadedFile($key) {
424         return Request_UploadedFile::getUploadedFile($key);
425     }
426     
427
428     function _fix_magic_quotes_gpc() {
429         $needs_fix = array('HTTP_POST_VARS',
430                            'HTTP_GET_VARS',
431                            'HTTP_COOKIE_VARS',
432                            'HTTP_SERVER_VARS',
433                            'HTTP_POST_FILES');
434         
435         // Fix magic quotes.
436         if (get_magic_quotes_gpc()) {
437             foreach ($needs_fix as $vars)
438                 $this->_stripslashes($GLOBALS[$vars]);
439         }
440     }
441
442     function _stripslashes(&$var) {
443         if (is_array($var)) {
444             foreach ($var as $key => $val)
445                 $this->_stripslashes($var[$key]);
446         }
447         elseif (is_string($var))
448             $var = stripslashes($var);
449     }
450     
451     function _fix_multipart_form_data () {
452         if (preg_match('|^multipart/form-data|', $this->get('CONTENT_TYPE')))
453             $this->_strip_leading_nl($GLOBALS['HTTP_POST_VARS']);
454     }
455     
456     function _strip_leading_nl(&$var) {
457         if (is_array($var)) {
458             foreach ($var as $key => $val)
459                 $this->_strip_leading_nl($var[$key]);
460         }
461         elseif (is_string($var))
462             $var = preg_replace('|^\r?\n?|', '', $var);
463     }
464 }
465
466 class Request_SessionVars {
467     function Request_SessionVars() {
468         // Prevent cacheing problems with IE 5
469         session_cache_limiter('none');
470                                         
471         session_start();
472     }
473     
474     function get($key) {
475         $vars = &$GLOBALS['HTTP_SESSION_VARS'];
476         if (isset($vars[$key]))
477             return $vars[$key];
478         return false;
479     }
480     
481     function set($key, $val) {
482         $vars = &$GLOBALS['HTTP_SESSION_VARS'];
483         if ($key == 'wiki_user') {
484             if (DEBUG) {
485               if (!$val) {
486                 trigger_error("delete user session", E_USER_WARNING);
487               } elseif (!isset($val->_level)) {
488                   trigger_error("lost level in session", E_USER_WARNING);
489               }
490             }
491             if (is_object($val)) {
492                 $val->page   = $GLOBALS['request']->getArg('pagename');
493                 $val->action = $GLOBALS['request']->getArg('action');
494                 // sessiondata may not exceed a certain size!
495                 // otherwise it will get lost.
496                 unset($val->_HomePagehandle);
497                 unset($val->_auth_dbi);
498                 unset($val->_HomePagehandle);
499                 if (isset($val->_group)) {
500                     unset($val->_group->request);
501                     unset($val->_group->user);
502                 }
503             }
504         }
505         if (!function_usable('get_cfg_var') or get_cfg_var('register_globals')) {
506             // This is funky but necessary, at least in some PHP's
507             $GLOBALS[$key] = $val;
508         }
509         $vars[$key] = $val;
510         if (isset($_SESSION))
511             $_SESSION[$key] = $val;
512         session_register($key);
513     }
514     
515     function delete($key) {
516         $vars = &$GLOBALS['HTTP_SESSION_VARS'];
517         if (!function_usable('ini_get') or ini_get('register_globals'))
518             unset($GLOBALS[$key]);
519         if (DEBUG) trigger_error("delete session $key",E_USER_WARNING);
520         unset($vars[$key]);
521         session_unregister($key);
522     }
523 }
524
525 class Request_CookieVars {
526     
527     function get($key) {
528         $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
529         if (isset($vars[$key])) {
530             @$val = unserialize(base64_decode($vars[$key]));
531             if (!empty($val))
532                 return $val;
533             @$val = urldecode($vars[$key]);
534             if (!empty($val))
535                 return $val;
536         }
537         return false;
538     }
539
540     function get_old($key) {
541         $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
542         if (isset($vars[$key])) {
543             @$val = unserialize(base64_decode($vars[$key]));
544             if (!empty($val))
545                 return $val;
546             @$val = unserialize($vars[$key]);
547             if (!empty($val))
548                 return $val;
549             @$val = $vars[$key];
550             if (!empty($val))
551                 return $val;
552         }
553         return false;
554     }
555
556     function set($key, $val, $persist_days = false, $path = false) {
557         // if already defined, ignore
558         if (defined('MAIN_setUser') and $key = 'WIKI_ID') return;
559         $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
560         if (is_numeric($persist_days)) {
561             $expires = time() + (24 * 3600) * $persist_days;
562         }
563         else {
564             $expires = 0;
565         }
566         if (is_array($val) or is_object($val))
567             $packedval = base64_encode(serialize($val));
568         else
569             $packedval = urlencode($val);
570         $vars[$key] = $packedval;
571         if ($path)
572             @setcookie($key, $packedval, $expires, $path);
573         else
574             @setcookie($key, $packedval, $expires);
575     }
576     
577     function delete($key) {
578         static $deleted = array();
579         if (isset($deleted[$key])) return;
580         $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
581         @setcookie($key,'',0);
582         @setcookie($key,'',0,defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '/');
583         unset($vars[$key]);
584         unset($GLOBALS['HTTP_COOKIE_VARS'][$key]);
585         $deleted[$key] = 1;
586     }
587 }
588
589 /* Win32 Note:
590    [\winnt\php.ini]
591    You must set "upload_tmp_dir" = "/tmp/" or "C:/tmp/"
592    Best on the same drive as apache, with forward slashes 
593    and with ending slash!
594    Otherwise "\\" => "" and the uploaded file will not be found.
595 */
596 class Request_UploadedFile {
597     function getUploadedFile($postname) {
598         global $HTTP_POST_FILES;
599         
600         if (!isset($HTTP_POST_FILES[$postname]))
601             return false;
602         
603         $fileinfo = &$HTTP_POST_FILES[$postname];
604         if ($fileinfo['error']) {
605             trigger_error("Upload error: #" . $fileinfo['error'],
606                           E_USER_ERROR);
607             return false;
608         }
609
610         // With windows/php 4.2.1 is_uploaded_file() always returns false.
611         // Be sure that upload_tmp_dir ends with a slash!
612         if (!is_uploaded_file($fileinfo['tmp_name'])) {
613             if (isWindows()) {
614                 if (!$tmp_file = get_cfg_var('upload_tmp_dir')) {
615                     $tmp_file = dirname(tempnam('', ''));
616                 }
617                 $tmp_file .= '/' . basename($fileinfo['tmp_name']);
618                 /* but ending slash in php.ini upload_tmp_dir is required. */
619                 if (ereg_replace('/+', '/', $tmp_file) != $fileinfo['tmp_name']) {
620                     trigger_error(sprintf("Uploaded tmpfile illegal: %s != %s.",$tmp_file, $fileinfo['tmp_name']).
621                                   "\n".
622                                   "Probably illegal TEMP environment or upload_tmp_dir setting.",
623                                   E_USER_ERROR);
624                     return false;
625                 } else {
626                     /*
627                     trigger_error(sprintf("Workaround for PHP/Windows is_uploaded_file() problem for %s.",
628                                           $fileinfo['tmp_name'])."\n".
629                                   "Probably illegal TEMP environment or upload_tmp_dir setting.", 
630                                   E_USER_NOTICE);
631                     */
632                     ;
633                 }
634             } else {
635               trigger_error(sprintf("Uploaded tmpfile %s not found.",$fileinfo['tmp_name'])."\n".
636                            " Probably illegal TEMP environment or upload_tmp_dir setting.",
637                           E_USER_WARNING);
638             }
639         }
640         return new Request_UploadedFile($fileinfo);
641     }
642     
643     function Request_UploadedFile($fileinfo) {
644         $this->_info = $fileinfo;
645     }
646
647     function getSize() {
648         return $this->_info['size'];
649     }
650
651     function getName() {
652         return $this->_info['name'];
653     }
654
655     function getType() {
656         return $this->_info['type'];
657     }
658
659     function getTmpName() {
660         return $this->_info['tmp_name'];
661     }
662
663     function open() {
664         if ( ($fd = fopen($this->_info['tmp_name'], "rb")) ) {
665             if ($this->getSize() < filesize($this->_info['tmp_name'])) {
666                 // FIXME: Some PHP's (or is it some browsers?) put
667                 //    HTTP/MIME headers in the file body, some don't.
668                 //
669                 // At least, I think that's the case.  I know I used
670                 // to need this code, now I don't.
671                 //
672                 // This code is more-or-less untested currently.
673                 //
674                 // Dump HTTP headers.
675                 while ( ($header = fgets($fd, 4096)) ) {
676                     if (trim($header) == '') {
677                         break;
678                     }
679                     else if (!preg_match('/^content-(length|type):/i', $header)) {
680                         rewind($fd);
681                         break;
682                     }
683                 }
684             }
685         }
686         return $fd;
687     }
688
689     function getContents() {
690         $fd = $this->open();
691         $data = fread($fd, $this->getSize());
692         fclose($fd);
693         return $data;
694     }
695 }
696
697 /**
698  * Create NCSA "combined" log entry for current request.
699  */
700 class Request_AccessLogEntry
701 {
702     /**
703      * Constructor.
704      *
705      * The log entry will be automatically appended to the log file
706      * when the current request terminates.
707      *
708      * If you want to modify a Request_AccessLogEntry before it gets
709      * written (e.g. via the setStatus and setSize methods) you should
710      * use an '&' on the constructor, so that you're working with the
711      * original (rather than a copy) object.
712      *
713      * <pre>
714      *    $log_entry = & new Request_AccessLogEntry($req, "/tmp/wiki_access_log");
715      *    $log_entry->setStatus(401);
716      * </pre>
717      *
718      *
719      * @param $request object  Request object for current request.
720      * @param $logfile string  Log file name.
721      */
722     function Request_AccessLogEntry (&$request, $logfile) {
723         $this->logfile = $logfile;
724         
725         $this->host  = $request->get('REMOTE_HOST');
726         $this->ident = $request->get('REMOTE_IDENT');
727         if (!$this->ident)
728             $this->ident = '-';
729         $this->user = '-';        // FIXME: get logged-in user name
730         $this->time = time();
731         $this->request = join(' ', array($request->get('REQUEST_METHOD'),
732                                          $request->get('REQUEST_URI'),
733                                          $request->get('SERVER_PROTOCOL')));
734         $this->status = 200;
735         $this->size = 0;
736         $this->referer = (string) $request->get('HTTP_REFERER');
737         $this->user_agent = (string) $request->get('HTTP_USER_AGENT');
738
739         global $Request_AccessLogEntry_entries;
740         if (!isset($Request_AccessLogEntry_entries)) {
741             register_shutdown_function("Request_AccessLogEntry_shutdown_function");
742         }
743         $Request_AccessLogEntry_entries[] = &$this;
744     }
745
746     /**
747      * Set result status code.
748      *
749      * @param $status integer  HTTP status code.
750      */
751     function setStatus ($status) {
752         $this->status = $status;
753     }
754     
755     /**
756      * Set response size.
757      *
758      * @param $size integer
759      */
760     function setSize ($size) {
761         $this->size = $size;
762     }
763     
764     /**
765      * Get time zone offset.
766      *
767      * This is a static member function.
768      *
769      * @param $time integer Unix timestamp (defaults to current time).
770      * @return string Zone offset, e.g. "-0800" for PST.
771      */
772     function _zone_offset ($time = false) {
773         if (!$time)
774             $time = time();
775         $offset = date("Z", $time);
776         $negoffset = "";
777         if ($offset < 0) {
778             $negoffset = "-";
779             $offset = -$offset;
780         }
781         $offhours = floor($offset / 3600);
782         $offmins  = $offset / 60 - $offhours * 60;
783         return sprintf("%s%02d%02d", $negoffset, $offhours, $offmins);
784     }
785
786     /**
787      * Format time in NCSA format.
788      *
789      * This is a static member function.
790      *
791      * @param $time integer Unix timestamp (defaults to current time).
792      * @return string Formatted date & time.
793      */
794     function _ncsa_time($time = false) {
795         if (!$time)
796             $time = time();
797
798         return date("d/M/Y:H:i:s", $time) .
799             " " . $this->_zone_offset();
800     }
801
802     /**
803      * Write entry to log file.
804      */
805     function write() {
806         $entry = sprintf('%s %s %s [%s] "%s" %d %d "%s" "%s"',
807                          $this->host, $this->ident, $this->user,
808                          $this->_ncsa_time($this->time),
809                          $this->request, $this->status, $this->size,
810                          $this->referer, $this->user_agent);
811
812         //Error log doesn't provide locking.
813         //error_log("$entry\n", 3, $this->logfile);
814
815         // Alternate method
816         if (($fp = fopen($this->logfile, "a"))) {
817             flock($fp, LOCK_EX);
818             fputs($fp, "$entry\n");
819             fclose($fp);
820         }
821     }
822 }
823
824 /**
825  * Shutdown callback.
826  *
827  * @access private
828  * @see Request_AccessLogEntry
829  */
830 function Request_AccessLogEntry_shutdown_function ()
831 {
832     global $Request_AccessLogEntry_entries;
833     
834     foreach ($Request_AccessLogEntry_entries as $entry) {
835         $entry->write();
836     }
837     unset($Request_AccessLogEntry_entries);
838 }
839
840
841 class HTTP_ETag {
842     function HTTP_ETag($val, $is_weak=false) {
843         $this->_val = hash($val);
844         $this->_weak = $is_weak;
845     }
846
847     /** Comparison
848      *
849      * Strong comparison: If either (or both) tag is weak, they
850      *  are not equal.
851      */
852     function equals($that, $strong_match=false) {
853         if ($this->_val != $that->_val)
854             return false;
855         if ($strong_match and ($this->_weak or $that->_weak))
856             return false;
857         return true;
858     }
859
860
861     function asString() {
862         $quoted = '"' . addslashes($this->_val) . '"';
863         return $this->_weak ? "W/$quoted" : $quoted;
864     }
865
866     /** Parse tag from header.
867      *
868      * This is a static member function.
869      */
870     function parse($strval) {
871         if (!preg_match(':^(W/)?"(.+)"$:i', trim($strval), $m))
872             return false;       // parse failed
873         list(,$weak,$str) = $m;
874         return new HTTP_ETag(stripslashes($str), $weak);
875     }
876
877     function matches($taglist, $strong_match=false) {
878         $taglist = trim($taglist);
879
880         if ($taglist == '*') {
881             if ($strong_match)
882                 return ! $this->_weak;
883             else
884                 return true;
885         }
886
887         while (preg_match('@^(W/)?"((?:\\\\.|[^"])*)"\s*,?\s*@i',
888                           $taglist, $m)) {
889             list($match, $weak, $str) = $m;
890             $taglist = substr($taglist, strlen($match));
891             $tag = new HTTP_ETag(stripslashes($str), $weak);
892             if ($this->equals($tag, $strong_match)) {
893                 return true;
894             }
895         }
896         return false;
897     }
898 }
899
900 // Possible results from the HTTP_ValidatorSet::_check*() methods.
901 // (Higher numerical values take precedence.)
902 define ('_HTTP_VAL_PASS', 0);           // Test is irrelevant
903 define ('_HTTP_VAL_NOT_MODIFIED', 1);   // Test passed, content not changed
904 define ('_HTTP_VAL_MODIFIED', 2);       // Test failed, content changed
905 define ('_HTTP_VAL_FAILED', 3);         // Precondition failed.
906
907 class HTTP_ValidatorSet {
908     function HTTP_ValidatorSet($validators) {
909         $this->_mtime = $this->_weak = false;
910         $this->_tag = array();
911         
912         foreach ($validators as $key => $val) {
913             if ($key == '%mtime') {
914                 $this->_mtime = $val;
915             }
916             elseif ($key == '%weak') {
917                 if ($val)
918                     $this->_weak = true;
919             }
920             else {
921                 $this->_tag[$key] = $val;
922             }
923         }
924     }
925
926     function append($that) {
927         if (is_array($that))
928             $that = new HTTP_ValidatorSet($that);
929
930         // Pick the most recent mtime
931         if (isset($that->_mtime))
932             if (!isset($this->_mtime) || $that->_mtime > $this->_mtime)
933                 $this->_mtime = $that->_mtime;
934
935         // If either is weak, we're weak
936         if (!empty($that->_weak))
937             $this->_weak = true;
938         if (is_array($this->_tag))
939             $this->_tag = array_merge($this->_tag, $that->_tag);
940         else
941             $this->_tag = $that->_tag;
942     }
943
944     function getETag() {
945         if (! $this->_tag)
946             return false;
947         return new HTTP_ETag($this->_tag, $this->_weak);
948     }
949
950     function getModificationTime() {
951         return $this->_mtime;
952     }
953     
954     function checkConditionalRequest (&$request) {
955         $result = max($this->_checkIfUnmodifiedSince($request),
956                       $this->_checkIfModifiedSince($request),
957                       $this->_checkIfMatch($request),
958                       $this->_checkIfNoneMatch($request));
959
960         if ($result == _HTTP_VAL_PASS || $result == _HTTP_VAL_MODIFIED)
961             return false;       // "please proceed with normal processing"
962         elseif ($result == _HTTP_VAL_FAILED)
963             return 412;         // "412 Precondition Failed"
964         elseif ($result == _HTTP_VAL_NOT_MODIFIED)
965             return 304;         // "304 Not Modified"
966
967         trigger_error("Ack, shouldn't get here", E_USER_ERROR);
968         return false;
969     }
970
971     function _checkIfUnmodifiedSince(&$request) {
972         if ($this->_mtime !== false) {
973             $since = ParseRfc1123DateTime($request->get("HTTP_IF_UNMODIFIED_SINCE"));
974             if ($since !== false && $this->_mtime > $since)
975                 return _HTTP_VAL_FAILED;
976         }
977         return _HTTP_VAL_PASS;
978     }
979
980     function _checkIfModifiedSince(&$request) {
981         if ($this->_mtime !== false and $request->isGetOrHead()) {
982             $since = ParseRfc1123DateTime($request->get("HTTP_IF_MODIFIED_SINCE"));
983             if ($since !== false) {
984                 if ($this->_mtime <= $since)
985                     return _HTTP_VAL_NOT_MODIFIED;
986                 return _HTTP_VAL_MODIFIED;
987             }
988         }
989         return _HTTP_VAL_PASS;
990     }
991
992     function _checkIfMatch(&$request) {
993         if ($this->_tag && ($taglist = $request->get("HTTP_IF_MATCH"))) {
994             $tag = $this->getETag();
995             if (!$tag->matches($taglist, 'strong'))
996                 return _HTTP_VAL_FAILED;
997         }
998         return _HTTP_VAL_PASS;
999     }
1000
1001     function _checkIfNoneMatch(&$request) {
1002         if ($this->_tag && ($taglist = $request->get("HTTP_IF_NONE_MATCH"))) {
1003             $tag = $this->getETag();
1004             $strong_compare = ! $request->isGetOrHead();
1005             if ($taglist) {
1006                 if ($tag->matches($taglist, $strong_compare)) {
1007                     if ($request->isGetOrHead())
1008                         return _HTTP_VAL_NOT_MODIFIED;
1009                     else
1010                         return _HTTP_VAL_FAILED;
1011                 }
1012                 return _HTTP_VAL_MODIFIED;
1013             }
1014         }
1015         return _HTTP_VAL_PASS;
1016     }
1017 }
1018
1019
1020 // $Log: not supported by cvs2svn $
1021 // Revision 1.67  2004/09/25 18:56:54  rurban
1022 // make start_debug logic work
1023 //
1024 // Revision 1.66  2004/09/25 16:24:52  rurban
1025 // dont compress on debugging
1026 //
1027 // Revision 1.65  2004/09/17 14:13:49  rurban
1028 // We check for the client Accept-Encoding: "gzip" presence also
1029 // This should eliminate a lot or reported problems.
1030 //
1031 // Note that this doesn#t fix RSS ssues:
1032 // Most RSS clients are NOT(!) application/xml gzip compatible yet.
1033 // Even if they are sending the accept-encoding gzip header!
1034 // wget is, Mozilla, and MSIE no.
1035 // Of the RSS readers only MagpieRSS 0.5.2 is. http://www.rssgov.com/rssparsers.html
1036 //
1037 // Revision 1.64  2004/09/17 13:32:36  rurban
1038 // Disable server-side gzip encoding for RSS (RDF encoding), even if the client says it
1039 // supports it. Mozilla has this error, wget works fine. IE not checked.
1040 //
1041 // Revision 1.63  2004/07/01 09:29:40  rurban
1042 // fixed another DbSession crash: wrong WikiGroup vars
1043 //
1044 // Revision 1.62  2004/06/27 10:26:02  rurban
1045 // oci8 patch by Philippe Vanhaesendonck + some ADODB notes+fixes
1046 //
1047 // Revision 1.61  2004/06/25 14:29:17  rurban
1048 // WikiGroup refactoring:
1049 //   global group attached to user, code for not_current user.
1050 //   improved helpers for special groups (avoid double invocations)
1051 // new experimental config option ENABLE_XHTML_XML (fails with IE, and document.write())
1052 // fixed a XHTML validation error on userprefs.tmpl
1053 //
1054 // Revision 1.60  2004/06/19 11:51:13  rurban
1055 // CACHE_CONTROL: NONE => NO_CACHE
1056 //
1057 // Revision 1.59  2004/06/13 11:34:22  rurban
1058 // fixed bug #969532 (space in uploaded filenames)
1059 // improved upload error messages
1060 //
1061 // Revision 1.58  2004/06/04 20:32:53  rurban
1062 // Several locale related improvements suggested by Pierrick Meignen
1063 // LDAP fix by John Cole
1064 // reanable admin check without ENABLE_PAGEPERM in the admin plugins
1065 //
1066 // Revision 1.57  2004/06/03 18:54:25  rurban
1067 // fixed "lost level in session" warning, now that signout sets level = 0 (before -1)
1068 //
1069 // Revision 1.56  2004/05/17 17:43:29  rurban
1070 // CGI: no PATH_INFO fix
1071 //
1072 // Revision 1.55  2004/05/15 18:31:00  rurban
1073 // some action=pdf Request fixes: With MSIE it works now. Now the work with the page formatting begins.
1074 //
1075 // Revision 1.54  2004/05/04 22:34:25  rurban
1076 // more pdf support
1077 //
1078 // Revision 1.53  2004/05/03 21:57:47  rurban
1079 // locale updates: we previously lost some words because of wrong strings in
1080 //   PhotoAlbum, german rewording.
1081 // fixed $_SESSION registering (lost session vars, esp. prefs)
1082 // fixed ending slash in listAvailableLanguages/Themes
1083 //
1084 // Revision 1.52  2004/05/03 13:16:47  rurban
1085 // fixed UserPreferences update, esp for boolean and int
1086 //
1087 // Revision 1.51  2004/05/02 21:26:38  rurban
1088 // limit user session data (HomePageHandle and auth_dbi have to invalidated anyway)
1089 //   because they will not survive db sessions, if too large.
1090 // extended action=upgrade
1091 // some WikiTranslation button work
1092 // revert WIKIAUTH_UNOBTAINABLE (need it for main.php)
1093 // some temp. session debug statements
1094 //
1095 // Revision 1.50  2004/04/29 19:39:44  rurban
1096 // special support for formatted plugins (one-liners)
1097 //   like <small><plugin BlaBla ></small>
1098 // iter->asArray() helper for PopularNearby
1099 // db_session for older php's (no &func() allowed)
1100 //
1101 // Revision 1.49  2004/04/26 20:44:34  rurban
1102 // locking table specific for better databases
1103 //
1104 // Revision 1.48  2004/04/13 09:13:50  rurban
1105 // sf.net bug #933183 and http://bugs.php.net/17557
1106 // disable ob_gzhandler if apache_note cannot be used.
1107 //   (conservative until we find why)
1108 //
1109 // Revision 1.47  2004/04/02 15:06:55  rurban
1110 // fixed a nasty ADODB_mysql session update bug
1111 // improved UserPreferences layout (tabled hints)
1112 // fixed UserPreferences auth handling
1113 // improved auth stability
1114 // improved old cookie handling: fixed deletion of old cookies with paths
1115 //
1116 // Revision 1.46  2004/03/30 02:14:03  rurban
1117 // fixed yet another Prefs bug
1118 // added generic PearDb_iter
1119 // $request->appendValidators no so strict as before
1120 // added some box plugin methods
1121 // PageList commalist for condensed output
1122 //
1123 // Revision 1.45  2004/03/24 19:39:02  rurban
1124 // php5 workaround code (plus some interim debugging code in XmlElement)
1125 //   php5 doesn't work yet with the current XmlElement class constructors,
1126 //   WikiUserNew does work better than php4.
1127 // rewrote WikiUserNew user upgrading to ease php5 update
1128 // fixed pref handling in WikiUserNew
1129 // added Email Notification
1130 // added simple Email verification
1131 // removed emailVerify userpref subclass: just a email property
1132 // changed pref binary storage layout: numarray => hash of non default values
1133 // print optimize message only if really done.
1134 // forced new cookie policy: delete pref cookies, use only WIKI_ID as plain string.
1135 //   prefs should be stored in db or homepage, besides the current session.
1136 //
1137 // Revision 1.44  2004/03/14 16:26:22  rurban
1138 // copyright line
1139 //
1140 // Revision 1.43  2004/03/12 20:59:17  rurban
1141 // important cookie fix by Konstantin Zadorozhny
1142 // new editpage feature: JS_SEARCHREPLACE
1143 //
1144 // Revision 1.42  2004/03/10 15:38:48  rurban
1145 // store current user->page and ->action in session for WhoIsOnline
1146 // better WhoIsOnline icon
1147 // fixed WhoIsOnline warnings
1148 //
1149 // Revision 1.41  2004/02/27 01:25:14  rurban
1150 // Workarounds for upload handling
1151 //
1152 // Revision 1.40  2004/02/26 01:39:51  rurban
1153 // safer code
1154 //
1155 // Revision 1.39  2004/02/24 15:14:57  rurban
1156 // fixed action=upload problems on Win32, and remove Merge Edit buttons: file does not exist anymore
1157 //
1158 // Revision 1.38  2004/01/25 10:26:02  rurban
1159 // fixed bug [ 541193 ] HTTP_SERVER_VARS are Apache specific
1160 // http://sourceforge.net/tracker/index.php?func=detail&aid=541193&group_id=6121&atid=106121
1161 // CGI and other servers than apache populate _ENV and not _SERVER
1162 //
1163 // Revision 1.37  2003/12/26 06:41:16  carstenklapp
1164 // Bugfix: Try to defer OS errors about session.save_path and ACCESS_LOG,
1165 // so they don't prevent IE from partially (or not at all) rendering the
1166 // page. This should help a little for the IE user who encounters trouble
1167 // when setting up a new PhpWiki for the first time.
1168 //
1169
1170 // Local Variables:
1171 // mode: php
1172 // tab-width: 8
1173 // c-basic-offset: 4
1174 // c-hanging-comment-ender-p: nil
1175 // indent-tabs-mode: nil
1176 // End:   
1177 ?>