2 rcs_id('$Id: Request.php,v 1.38 2004-01-25 10:26:02 rurban Exp $');
5 // backward compatibility for PHP < 4.2.0
6 if (!function_exists('ob_clean')) {
17 $this->_fix_magic_quotes_gpc();
18 $this->_fix_multipart_form_data();
20 switch($this->get('REQUEST_METHOD')) {
23 $this->args = &$GLOBALS['HTTP_GET_VARS'];
26 $this->args = &$GLOBALS['HTTP_POST_VARS'];
29 $this->args = array();
33 $this->session = new Request_SessionVars;
34 $this->cookies = new Request_CookieVars;
37 if (! is_writeable(ACCESS_LOG)) {
39 (sprintf(_("%s is not writable."), _("The PhpWiki access log file"))
41 . sprintf(_("Please ensure that %s is writable, or redefine %s in index.php."),
42 sprintf(_("the file '%s'"), ACCESS_LOG),
47 $this->_log_entry = & new Request_AccessLogEntry($this,
51 $GLOBALS['request'] = $this;
55 if (!empty($GLOBALS['HTTP_SERVER_VARS']))
56 $vars = &$GLOBALS['HTTP_SERVER_VARS'];
57 else // cgi or other servers than Apache
58 $vars = &$GLOBALS['_ENV'];
60 if (isset($vars[$key]))
65 $addr = $vars['REMOTE_ADDR'];
66 if (defined('ENABLE_REVERSE_DNS') && ENABLE_REVERSE_DNS)
67 return $vars[$key] = gethostbyaddr($addr);
75 function getArg($key) {
76 if (isset($this->args[$key]))
77 return $this->args[$key];
85 function setArg($key, $val) {
87 unset($this->args[$key]);
89 $this->args[$key] = $val;
92 // Well oh well. Do we really want to pass POST params back as GET?
93 function getURLtoSelf($args = false, $exclude = array()) {
95 // Err... good point...
97 trigger_error("Request::getURLtoSelf() should probably not be from POST",
100 $get_args = $this->args;
102 $get_args = array_merge($get_args, $args);
104 foreach ($exclude as $ex) {
105 if (!empty($get_args[$ex])) unset($get_args[$ex]);
108 $pagename = $get_args['pagename'];
109 unset ($get_args['pagename']);
110 if ($get_args['action'] == 'browse')
111 unset($get_args['action']);
113 return WikiURL($pagename, $get_args);
117 return $this->get("REQUEST_METHOD") == "POST";
120 function isGetOrHead () {
121 return in_array($this->get('REQUEST_METHOD'),
122 array('GET', 'HEAD'));
125 function httpVersion() {
126 if (!preg_match('@HTTP\s*/\s*(\d+.\d+)@', $this->get('SERVER_PROTOCOL'), $m))
128 return (float) $m[1];
131 function redirect($url, $noreturn=true) {
132 $bogus = defined('DISABLE_HTTP_REDIRECT') and DISABLE_HTTP_REDIRECT;
135 header("Location: $url");
137 * "302 Found" is not really meant to be sent in response
138 * to a POST. Worse still, according to (both HTTP 1.0
139 * and 1.1) spec, the user, if it is sent, the user agent
140 * is supposed to use the same method to fetch the
141 * redirected URI as the original.
143 * That means if we redirect from a POST, the user-agent
144 * supposed to generate another POST. Not what we want.
145 * (We do this after a page save after all.)
147 * Fortunately, most/all browsers don't do that.
149 * "303 See Other" is what we really want. But it only
152 * FIXME: this is still not spec compliant for HTTP
155 $status = $this->httpVersion() >= 1.1 ? 303 : 302;
157 $this->setStatus($status);
161 include_once('lib/Template.php');
162 $this->discardOutput();
163 $tmpl = new Template('redirect', $this, array('REDIRECT_URL' => $url));
169 function redirect(url) {
170 if (typeof location.replace == 'function')
171 location.replace(url);
172 else if (typeof location.assign == 'function')
173 location.assign(url);
175 window.location = url;
177 redirect('" . addslashes($url) . "')");
181 /** Set validators for this response.
183 * This sets a (possibly incomplete) set of validators
186 * The validator set can be extended using appendValidators().
188 * When you're all done setting and appending validators, you
189 * must call checkValidators() to check them and set the
190 * appropriate headers in the HTTP response.
194 * $request->setValidators(array('pagename' => $pagename,
195 * '%mtime' => $rev->get('mtime')));
197 * // Wups... response content depends on $otherpage, too...
198 * $request->appendValidators(array('otherpage' => $otherpagerev->getPageName(),
199 * '%mtime' => $otherpagerev->get('mtime')));
201 * // After all validators have been set:
202 * $request->checkValidators();
204 function setValidators($validator_set) {
205 if (is_array($validator_set))
206 $validator_set = new HTTP_ValidatorSet($validator_set);
207 $this->_validators = $validator_set;
210 /** Append validators for this response.
212 * This appends additional validators to this response.
213 * You must call setValidators() before calling this method.
215 function appendValidators($validator_set) {
216 $this->_validators->append($validator_set);
219 /** Check validators and set headers in HTTP response
221 * This sets the appropriate "Last-Modified" and "ETag"
222 * headers in the HTTP response.
224 * Additionally, if the validators match any(all) conditional
225 * headers in the HTTP request, this method will not return, but
226 * instead will send "304 Not Modified" or "412 Precondition
227 * Failed" (as appropriate) back to the client.
229 function checkValidators() {
230 $validators = &$this->_validators;
232 // Set validator headers
233 if (($etag = $validators->getETag()) !== false)
234 header("ETag: " . $etag->asString());
235 if (($mtime = $validators->getModificationTime()) !== false)
236 header("Last-Modified: " . Rfc1123DateTime($mtime));
238 // Set cache control headers
239 $this->cacheControl();
241 if (CACHE_CONTROL == 'NONE')
242 return; // don't check conditionals...
244 // Check conditional headers in request
245 $status = $validators->checkConditionalRequest($this);
247 // Return short response due to failed conditionals
248 $this->setStatus($status);
250 $this->discardOutput();
256 /** Set the cache control headers in the HTTP response.
258 function cacheControl($strategy=CACHE_CONTROL, $max_age=CACHE_CONTROL_MAX_AGE) {
259 if ($strategy == 'NONE') {
260 $cache_control = "no-cache";
263 elseif ($strategy == 'ALLOW_STALE' && $max_age > 0) {
264 $cache_control = sprintf("max-age=%d", $max_age);
267 $cache_control = "must-revalidate";
270 header("Cache-Control: $cache_control");
271 header("Expires: " . Rfc1123DateTime(time() + $max_age));
272 header("Vary: Cookie"); // FIXME: add more here?
275 function setStatus($status) {
276 if (preg_match('|^HTTP/.*?\s(\d+)|i', $status, $m)) {
281 $status = (integer) $status;
282 $reason = array('200' => 'OK',
284 '303' => 'See Other',
285 '304' => 'Not Modified',
286 '400' => 'Bad Request',
287 '401' => 'Unauthorized',
288 '403' => 'Forbidden',
289 '404' => 'Not Found',
290 '412' => 'Precondition Failed');
291 // FIXME: is it always okay to send HTTP/1.1 here, even for older clients?
292 header(sprintf("HTTP/1.1 %d %s", $status, $reason[$status]));
295 if (isset($this->_log_entry))
296 $this->_log_entry->setStatus($status);
299 function buffer_output($compress = true) {
300 if (defined('COMPRESS_OUTPUT')) {
301 if (!COMPRESS_OUTPUT)
304 elseif (!function_exists('version_compare')
305 || version_compare(phpversion(), '4.2.3', "<")) {
309 if (!function_exists('ob_gzhandler'))
313 ob_start('ob_gzhandler');
315 * Attempt to prevent Apache from doing the dreaded double-gzip.
317 * It would be better if we could detect when apache was going
318 * to zip for us, and then let it ... but I have yet to figure
319 * out how to do that.
321 @apache_note('no-gzip', 1);
324 // Now we alway buffer output.
325 // This is so we can set HTTP headers (e.g. for redirect)
327 // FIXME: change the name of this method.
330 $this->_is_buffering_output = true;
333 function discardOutput() {
334 if (!empty($this->_is_buffering_output))
337 trigger_error("Not buffering output", E_USER_NOTICE);
341 if (!empty($this->_is_buffering_output)) {
342 //header(sprintf("Content-Length: %d", ob_get_length()));
348 function getSessionVar($key) {
349 return $this->session->get($key);
351 function setSessionVar($key, $val) {
352 return $this->session->set($key, $val);
354 function deleteSessionVar($key) {
355 return $this->session->delete($key);
358 function getCookieVar($key) {
359 return $this->cookies->get($key);
361 function setCookieVar($key, $val, $lifetime_in_days = false) {
362 return $this->cookies->set($key, $val, $lifetime_in_days);
364 function deleteCookieVar($key) {
365 return $this->cookies->delete($key);
368 function getUploadedFile($key) {
369 return Request_UploadedFile::getUploadedFile($key);
373 function _fix_magic_quotes_gpc() {
374 $needs_fix = array('HTTP_POST_VARS',
381 if (get_magic_quotes_gpc()) {
382 foreach ($needs_fix as $vars)
383 $this->_stripslashes($GLOBALS[$vars]);
387 function _stripslashes(&$var) {
388 if (is_array($var)) {
389 foreach ($var as $key => $val)
390 $this->_stripslashes($var[$key]);
392 elseif (is_string($var))
393 $var = stripslashes($var);
396 function _fix_multipart_form_data () {
397 if (preg_match('|^multipart/form-data|', $this->get('CONTENT_TYPE')))
398 $this->_strip_leading_nl($GLOBALS['HTTP_POST_VARS']);
401 function _strip_leading_nl(&$var) {
402 if (is_array($var)) {
403 foreach ($var as $key => $val)
404 $this->_strip_leading_nl($var[$key]);
406 elseif (is_string($var))
407 $var = preg_replace('|^\r?\n?|', '', $var);
411 class Request_SessionVars {
412 function Request_SessionVars() {
413 // Prevent cacheing problems with IE 5
414 session_cache_limiter('none');
420 $vars = &$GLOBALS['HTTP_SESSION_VARS'];
421 if (isset($vars[$key]))
426 function set($key, $val) {
427 $vars = &$GLOBALS['HTTP_SESSION_VARS'];
428 if (!function_usable('ini_get') or ini_get('register_globals')) {
429 // This is funky but necessary, at least in some PHP's
430 $GLOBALS[$key] = $val;
433 session_register($key);
436 function delete($key) {
437 $vars = &$GLOBALS['HTTP_SESSION_VARS'];
438 if (!function_usable('ini_get') or ini_get('register_globals'))
439 unset($GLOBALS[$key]);
441 session_unregister($key);
445 class Request_CookieVars {
448 $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
449 if (isset($vars[$key])) {
450 @$val = unserialize($vars[$key]);
457 function set($key, $val, $persist_days = false) {
458 $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
460 if (is_numeric($persist_days)) {
461 $expires = time() + (24 * 3600) * $persist_days;
467 $packedval = serialize($val);
468 $vars[$key] = $packedval;
469 setcookie($key, $packedval, $expires, '/');
472 function delete($key) {
473 $vars = &$GLOBALS['HTTP_COOKIE_VARS'];
479 class Request_UploadedFile {
480 function getUploadedFile($postname) {
481 global $HTTP_POST_FILES;
483 if (!isset($HTTP_POST_FILES[$postname]))
486 $fileinfo = &$HTTP_POST_FILES[$postname];
487 if (!is_uploaded_file($fileinfo['tmp_name']))
488 return false; // possible malicious attack.
490 return new Request_UploadedFile($fileinfo);
493 function Request_UploadedFile($fileinfo) {
494 $this->_info = $fileinfo;
498 return $this->_info['size'];
502 return $this->_info['name'];
506 return $this->_info['type'];
509 function getTmpName() {
510 return $this->_info['tmp_name'];
514 if ( ($fd = fopen($this->_info['tmp_name'], "rb")) ) {
515 if ($this->getSize() < filesize($this->_info['tmp_name'])) {
516 // FIXME: Some PHP's (or is it some browsers?) put
517 // HTTP/MIME headers in the file body, some don't.
519 // At least, I think that's the case. I know I used
520 // to need this code, now I don't.
522 // This code is more-or-less untested currently.
524 // Dump HTTP headers.
525 while ( ($header = fgets($fd, 4096)) ) {
526 if (trim($header) == '') {
529 else if (!preg_match('/^content-(length|type):/i', $header)) {
539 function getContents() {
541 $data = fread($fd, $this->getSize());
548 * Create NCSA "combined" log entry for current request.
550 class Request_AccessLogEntry
555 * The log entry will be automatically appended to the log file
556 * when the current request terminates.
558 * If you want to modify a Request_AccessLogEntry before it gets
559 * written (e.g. via the setStatus and setSize methods) you should
560 * use an '&' on the constructor, so that you're working with the
561 * original (rather than a copy) object.
564 * $log_entry = & new Request_AccessLogEntry($req, "/tmp/wiki_access_log");
565 * $log_entry->setStatus(401);
569 * @param $request object Request object for current request.
570 * @param $logfile string Log file name.
572 function Request_AccessLogEntry (&$request, $logfile) {
573 $this->logfile = $logfile;
575 $this->host = $request->get('REMOTE_HOST');
576 $this->ident = $request->get('REMOTE_IDENT');
579 $this->user = '-'; // FIXME: get logged-in user name
580 $this->time = time();
581 $this->request = join(' ', array($request->get('REQUEST_METHOD'),
582 $request->get('REQUEST_URI'),
583 $request->get('SERVER_PROTOCOL')));
586 $this->referer = (string) $request->get('HTTP_REFERER');
587 $this->user_agent = (string) $request->get('HTTP_USER_AGENT');
589 global $Request_AccessLogEntry_entries;
590 if (!isset($Request_AccessLogEntry_entries)) {
591 register_shutdown_function("Request_AccessLogEntry_shutdown_function");
593 $Request_AccessLogEntry_entries[] = &$this;
597 * Set result status code.
599 * @param $status integer HTTP status code.
601 function setStatus ($status) {
602 $this->status = $status;
608 * @param $size integer
610 function setSize ($size) {
615 * Get time zone offset.
617 * This is a static member function.
619 * @param $time integer Unix timestamp (defaults to current time).
620 * @return string Zone offset, e.g. "-0800" for PST.
622 function _zone_offset ($time = false) {
625 $offset = date("Z", $time);
631 $offhours = floor($offset / 3600);
632 $offmins = $offset / 60 - $offhours * 60;
633 return sprintf("%s%02d%02d", $negoffset, $offhours, $offmins);
637 * Format time in NCSA format.
639 * This is a static member function.
641 * @param $time integer Unix timestamp (defaults to current time).
642 * @return string Formatted date & time.
644 function _ncsa_time($time = false) {
648 return date("d/M/Y:H:i:s", $time) .
649 " " . $this->_zone_offset();
653 * Write entry to log file.
656 $entry = sprintf('%s %s %s [%s] "%s" %d %d "%s" "%s"',
657 $this->host, $this->ident, $this->user,
658 $this->_ncsa_time($this->time),
659 $this->request, $this->status, $this->size,
660 $this->referer, $this->user_agent);
662 //Error log doesn't provide locking.
663 //error_log("$entry\n", 3, $this->logfile);
666 if (($fp = fopen($this->logfile, "a"))) {
668 fputs($fp, "$entry\n");
678 * @see Request_AccessLogEntry
680 function Request_AccessLogEntry_shutdown_function ()
682 global $Request_AccessLogEntry_entries;
684 foreach ($Request_AccessLogEntry_entries as $entry) {
687 unset($Request_AccessLogEntry_entries);
692 function HTTP_ETag($val, $is_weak=false) {
693 $this->_val = hash($val);
694 $this->_weak = $is_weak;
699 * Strong comparison: If either (or both) tag is weak, they
702 function equals($that, $strong_match=false) {
703 if ($this->_val != $that->_val)
705 if ($strong_match and ($this->_weak or $that->_weak))
711 function asString() {
712 $quoted = '"' . addslashes($this->_val) . '"';
713 return $this->_weak ? "W/$quoted" : $quoted;
716 /** Parse tag from header.
718 * This is a static member function.
720 function parse($strval) {
721 if (!preg_match(':^(W/)?"(.+)"$:i', trim($strval), $m))
722 return false; // parse failed
723 list(,$weak,$str) = $m;
724 return new HTTP_ETag(stripslashes($str), $weak);
727 function matches($taglist, $strong_match=false) {
728 $taglist = trim($taglist);
730 if ($taglist == '*') {
732 return ! $this->_weak;
737 while (preg_match('@^(W/)?"((?:\\\\.|[^"])*)"\s*,?\s*@i',
739 list($match, $weak, $str) = $m;
740 $taglist = substr($taglist, strlen($match));
741 $tag = new HTTP_ETag(stripslashes($str), $weak);
742 if ($this->equals($tag, $strong_match)) {
750 // Possible results from the HTTP_ValidatorSet::_check*() methods.
751 // (Higher numerical values take precedence.)
752 define ('_HTTP_VAL_PASS', 0); // Test is irrelevant
753 define ('_HTTP_VAL_NOT_MODIFIED', 1); // Test passed, content not changed
754 define ('_HTTP_VAL_MODIFIED', 2); // Test failed, content changed
755 define ('_HTTP_VAL_FAILED', 3); // Precondition failed.
757 class HTTP_ValidatorSet {
758 function HTTP_ValidatorSet($validators) {
759 $this->_mtime = $this->_weak = false;
760 $this->_tag = array();
762 foreach ($validators as $key => $val) {
763 if ($key == '%mtime') {
764 $this->_mtime = $val;
766 elseif ($key == '%weak') {
771 $this->_tag[$key] = $val;
776 function append($that) {
778 $that = new HTTP_ValidatorSet($that);
780 // Pick the most recent mtime
781 if (isset($that->_mtime))
782 if (!isset($this->_mtime) || $that->_mtime > $this->_mtime)
783 $this->_mtime = $that->_mtime;
785 // If either is weak, we're weak
786 if (!empty($that->_weak))
789 $this->_tag = array_merge($this->_tag, $that->_tag);
795 return new HTTP_ETag($this->_tag, $this->_weak);
798 function getModificationTime() {
799 return $this->_mtime;
802 function checkConditionalRequest (&$request) {
803 $result = max($this->_checkIfUnmodifiedSince($request),
804 $this->_checkIfModifiedSince($request),
805 $this->_checkIfMatch($request),
806 $this->_checkIfNoneMatch($request));
808 if ($result == _HTTP_VAL_PASS || $result == _HTTP_VAL_MODIFIED)
809 return false; // "please proceed with normal processing"
810 elseif ($result == _HTTP_VAL_FAILED)
811 return 412; // "412 Precondition Failed"
812 elseif ($result == _HTTP_VAL_NOT_MODIFIED)
813 return 304; // "304 Not Modified"
815 trigger_error("Ack, shouldn't get here", E_USER_ERROR);
819 function _checkIfUnmodifiedSince(&$request) {
820 if ($this->_mtime !== false) {
821 $since = ParseRfc1123DateTime($request->get("HTTP_IF_UNMODIFIED_SINCE"));
822 if ($since !== false && $this->_mtime > $since)
823 return _HTTP_VAL_FAILED;
825 return _HTTP_VAL_PASS;
828 function _checkIfModifiedSince(&$request) {
829 if ($this->_mtime !== false and $request->isGetOrHead()) {
830 $since = ParseRfc1123DateTime($request->get("HTTP_IF_MODIFIED_SINCE"));
831 if ($since !== false) {
832 if ($this->_mtime <= $since)
833 return _HTTP_VAL_NOT_MODIFIED;
834 return _HTTP_VAL_MODIFIED;
837 return _HTTP_VAL_PASS;
840 function _checkIfMatch(&$request) {
841 if ($this->_tag && ($taglist = $request->get("HTTP_IF_MATCH"))) {
842 $tag = $this->getETag();
843 if (!$tag->matches($taglist, 'strong'))
844 return _HTTP_VAL_FAILED;
846 return _HTTP_VAL_PASS;
849 function _checkIfNoneMatch(&$request) {
850 if ($this->_tag && ($taglist = $request->get("HTTP_IF_NONE_MATCH"))) {
851 $tag = $this->getETag();
852 $strong_compare = ! $request->isGetOrHead();
854 if ($tag->matches($taglist, $strong_compare)) {
855 if ($request->isGetOrHead())
856 return _HTTP_VAL_NOT_MODIFIED;
858 return _HTTP_VAL_FAILED;
860 return _HTTP_VAL_MODIFIED;
863 return _HTTP_VAL_PASS;
868 // $Log: not supported by cvs2svn $
869 // Revision 1.37 2003/12/26 06:41:16 carstenklapp
870 // Bugfix: Try to defer OS errors about session.save_path and ACCESS_LOG,
871 // so they don't prevent IE from partially (or not at all) rendering the
872 // page. This should help a little for the IE user who encounters trouble
873 // when setting up a new PhpWiki for the first time.
880 // c-hanging-comment-ender-p: nil
881 // indent-tabs-mode: nil