source: trunk/www.guidonia.net/wp/wp-content/plugins/tubepress/classes/net/php/pear/HTTP/Request2.class.php@ 44

Last change on this file since 44 was 44, checked in by luciano, 14 years ago
File size: 24.4 KB
Line 
1<?php
2/**
3 * Class representing a HTTP request
4 *
5 * PHP version 5
6 *
7 * LICENSE:
8 *
9 * Copyright (c) 2008, Alexey Borzov <avb@php.net>
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 *
16 * * Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 * * Redistributions in binary form must reproduce the above copyright
19 * notice, this list of conditions and the following disclaimer in the
20 * documentation and/or other materials provided with the distribution.
21 * * The names of the authors may not be used to endorse or promote products
22 * derived from this software without specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
25 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
26 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
27 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
30 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
31 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
32 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
33 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
34 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 *
36 * @category HTTP
37 * @package HTTP_Request2
38 * @author Alexey Borzov <avb@php.net>
39 * @license http://opensource.org/licenses/bsd-license.php New BSD License
40 * @version CVS: $Id: Request2.php,v 1.5 2008/11/17 12:30:12 avb Exp $
41 * @link http://pear.php.net/package/HTTP_Request2
42 */
43
44
45/**
46 * Class representing a HTTP request
47 *
48 * @category HTTP
49 * @package HTTP_Request2
50 * @author Alexey Borzov <avb@php.net>
51 * @version Release: 0.1.0
52 * @link http://tools.ietf.org/html/rfc2616#section-5
53 */
54class net_php_pear_HTTP_Request2
55{
56 /**#@+
57 * Constants for HTTP request methods
58 *
59 * @link http://tools.ietf.org/html/rfc2616#section-5.1.1
60 */
61 const METHOD_OPTIONS = 'OPTIONS';
62 const METHOD_GET = 'GET';
63 const METHOD_HEAD = 'HEAD';
64 const METHOD_POST = 'POST';
65 const METHOD_PUT = 'PUT';
66 const METHOD_DELETE = 'DELETE';
67 const METHOD_TRACE = 'TRACE';
68 const METHOD_CONNECT = 'CONNECT';
69 /**#@-*/
70
71 /**#@+
72 * Constants for HTTP authentication schemes
73 *
74 * @link http://tools.ietf.org/html/rfc2617
75 */
76 const AUTH_BASIC = 'basic';
77 const AUTH_DIGEST = 'digest';
78 /**#@-*/
79
80 /**
81 * Fileinfo magic database resource
82 * @var resource
83 * @see detectMimeType()
84 */
85 private static $_fileinfoDb;
86
87 /**
88 * Observers attached to the request (instances of SplObserver)
89 * @var array
90 */
91 protected $observers = array();
92
93 /**
94 * Request URL
95 * @var net_php_pear_Net_URL2
96 */
97 protected $url;
98
99 /**
100 * Request method
101 * @var string
102 */
103 protected $method = self::METHOD_GET;
104
105 /**
106 * Authentication data
107 * @var array
108 * @see getAuth()
109 */
110 protected $auth;
111
112 /**
113 * Request headers
114 * @var array
115 */
116 protected $headers = array();
117
118 /**
119 * Configuration parameters
120 * @var array
121 * @see setConfig()
122 */
123 protected $config = array(
124 'adapter' => 'net_php_pear_HTTP_Request2_Adapter_Socket',
125 'connect_timeout' => 10,
126 'use_brackets' => true,
127 'protocol_version' => '1.1',
128 'buffer_size' => 16384,
129 'proxy_host' => '',
130 'proxy_port' => '',
131 'proxy_user' => '',
132 'proxy_password' => '',
133 'proxy_auth_scheme' => self::AUTH_BASIC
134 );
135
136 /**
137 * Last event in request / response handling, intended for observers
138 * @var array
139 * @see getLastEvent()
140 */
141 protected $lastEvent = array(
142 'name' => 'start',
143 'data' => null
144 );
145
146 /**
147 * Request body
148 * @var string|resource
149 * @see setBody()
150 */
151 protected $body = '';
152
153 /**
154 * Array of POST parameters
155 * @var array
156 */
157 protected $postParams = array();
158
159 /**
160 * Array of file uploads (for multipart/form-data POST requests)
161 * @var array
162 */
163 protected $uploads = array();
164
165 /**
166 * Adapter used to perform actual HTTP request
167 * @var HTTP_Request2_Adapter
168 */
169 protected $adapter;
170
171
172 /**
173 * Constructor. Can set request URL, method and configuration array.
174 *
175 * Also sets a default value for User-Agent header.
176 *
177 * @param string|Net_Url2 Request URL
178 * @param string Request method
179 * @param array Configuration for this Request instance
180 */
181 public function __construct($url = null, $method = self::METHOD_GET, $config = array())
182 {
183 if (!empty($url)) {
184 $this->setUrl($url);
185 }
186 if (!empty($method)) {
187 $this->setMethod($method);
188 }
189 $this->setConfig($config);
190 $this->setHeader('user-agent', 'HTTP_Request2/0.1.0 ' .
191 '(http://pear.php.net/package/http_request2) ' .
192 'PHP/' . phpversion());
193 }
194
195 /**
196 * Sets the URL for this request
197 *
198 * If the URL has userinfo part (username & password) these will be removed
199 * and converted to auth data. If the URL does not have a path component,
200 * that will be set to '/'.
201 *
202 * @param string|net_php_pear_Net_URL2 Request URL
203 * @return HTTP_Request2
204 * @throws HTTP_Request2_Exception
205 */
206 public function setUrl($url)
207 {
208 if (is_string($url)) {
209 $url = new net_php_pear_Net_URL2($url);
210 }
211 if (!$url instanceof net_php_pear_Net_URL2) {
212 throw new net_php_pear_HTTP_Request2_Exception('Parameter is not a valid HTTP URL');
213 }
214 // URL contains username / password?
215 if ($url->getUserinfo()) {
216 $username = $url->getUser();
217 $password = $url->getPassword();
218 $this->setAuth(rawurldecode($username), $password? rawurldecode($password): '');
219 $url->setUserinfo('');
220 }
221 if ('' == $url->getPath()) {
222 $url->setPath('/');
223 }
224 $this->url = $url;
225
226 return $this;
227 }
228
229 /**
230 * Returns the request URL
231 *
232 * @return net_php_pear_Net_URL2
233 */
234 public function getUrl()
235 {
236 return $this->url;
237 }
238
239 /**
240 * Sets the request method
241 *
242 * @param string
243 * @return HTTP_Request2
244 * @throws HTTP_Request2_Exception if the method name is invalid
245 */
246 public function setMethod($method)
247 {
248 // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1
249 if (preg_match('![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!', $method)) {
250 throw new net_php_pear_HTTP_Request2_Exception("Invalid request method '{$method}'");
251 }
252 $this->method = $method;
253
254 return $this;
255 }
256
257 /**
258 * Returns the request method
259 *
260 * @return string
261 */
262 public function getMethod()
263 {
264 return $this->method;
265 }
266
267 /**
268 * Sets the configuration parameters
269 *
270 * $config array can have the following keys:
271 * <ul>
272 * <li> 'adapter' - adapter to use (string)</li>
273 * <li> 'connect_timeout' - Connection timeout in seconds (float)</li>
274 * <li> 'use_brackets' - Whether to append [] to array variable names (bool)</li>
275 * <li> 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)</li>
276 * <li> 'buffer_size' - Buffer size to use for reading and writing (int)</li>
277 * <li> 'proxy_host' - Proxy server host (string)</li>
278 * <li> 'proxy_port' - Proxy server port (integer)</li>
279 * <li> 'proxy_user' - Proxy auth username (string)</li>
280 * <li> 'proxy_password' - Proxy auth password (string)</li>
281 * <li> 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)</li>
282 * </ul>
283 *
284 * @param array array of the form ('param name' => 'param value')
285 * @return HTTP_Request2
286 * @throws HTTP_Request2_Exception If the parameter is unknown
287 */
288 public function setConfig($config = array())
289 {
290 foreach ($config as $k => $v) {
291 if (!array_key_exists($k, $this->config)) {
292 throw new net_php_pear_HTTP_Request2_Exception("Unknown configuration parameter '{$k}'");
293 }
294 $this->config[$k] = $v;
295 }
296
297 return $this;
298 }
299
300 /**
301 * Returns the value of the configuration parameter
302 *
303 * @return mixed
304 * @throws HTTP_Request2_Exception If the parameter is unknown
305 */
306 public function getConfigValue($name)
307 {
308 if (!array_key_exists($name, $this->config)) {
309 throw new net_php_pear_HTTP_Request2_Exception("Unknown configuration parameter '{$name}'");
310 }
311 return $this->config[$name];
312 }
313
314 /**
315 * Sets the autentification data
316 *
317 * @param string user name
318 * @param string password
319 * @param string authentication scheme
320 * @return HTTP_Request2
321 */
322 public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC)
323 {
324 if (empty($user)) {
325 $this->auth = null;
326 } else {
327 $this->auth = array(
328 'user' => (string)$user,
329 'password' => (string)$password,
330 'scheme' => $scheme
331 );
332 }
333
334 return $this;
335 }
336
337 /**
338 * Returns the authentication data
339 *
340 * The array has the keys 'user', 'password' and 'scheme', where 'scheme'
341 * is one of the HTTP_Request2::AUTH_* constants.
342 *
343 * @return array
344 */
345 public function getAuth()
346 {
347 return $this->auth;
348 }
349
350 /**
351 * Sets request header(s)
352 *
353 * The first parameter may be either a full header string 'header: value' or
354 * header name. In the former case $value parameter is ignored, in the latter
355 * the header's value will either be set to $value or the header will be
356 * removed if $value is null. The first parameter can also be an array of
357 * headers, in that case method will be called recursively.
358 *
359 * Note that headers are treated case insensitively as per RFC 2616.
360 *
361 * <code>
362 * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar'
363 * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz'
364 * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux'
365 * $req->setHeader('FOO'); // removes 'Foo' header from request
366 * </code>
367 *
368 * @param string|array header name, header string ('Header: value')
369 * or an array of headers
370 * @param string|null header value, header will be removed if null
371 * @return HTTP_Request2
372 * @throws net_php_pear_HTTP_Request2_Exception
373 */
374 public function setHeader($name, $value = null)
375 {
376 if (is_array($name)) {
377 foreach ($name as $k => $v) {
378 if (is_string($k)) {
379 $this->setHeader($k, $v);
380 } else {
381 $this->setHeader($v);
382 }
383 }
384 } else {
385 if (!$value && strpos($name, ':')) {
386 list($name, $value) = array_map('trim', explode(':', $name, 2));
387 }
388 // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2
389 if (preg_match('![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!', $name)) {
390 throw new net_php_pear_HTTP_Request2_Exception("Invalid header name '{$name}'");
391 }
392 // Header names are case insensitive anyway
393 $name = strtolower($name);
394 if (!$value) {
395 unset($this->headers[$name]);
396 } else {
397 $this->headers[$name] = $value;
398 }
399 }
400
401 return $this;
402 }
403
404 /**
405 * Returns the request headers
406 *
407 * The array is of the form ('header name' => 'header value'), header names
408 * are lowercased
409 *
410 * @return array
411 */
412 public function getHeaders()
413 {
414 return $this->headers;
415 }
416
417 /**
418 * Appends a cookie to "Cookie:" header
419 *
420 * @param string cookie name
421 * @param string cookie value
422 * @return HTTP_Request2
423 * @throws net_php_pear_HTTP_Request2_Exception
424 */
425 public function addCookie($name, $value)
426 {
427 $cookie = $name . '=' . $value;
428 // Disallowed characters: http://cgi.netscape.com/newsref/std/cookie_spec.html
429 if (preg_match('/[\s,;]/', $cookie)) {
430 throw new net_php_pear_HTTP_Request2_Exception("Invalid cookie: '{$cookie}'");
431 }
432 $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';
433 $this->setHeader('cookie', $cookies . $cookie);
434
435 return $this;
436 }
437
438 /**
439 * Sets the request body
440 *
441 * @param string Either a string with the body or filename containing body
442 * @param bool Whether first parameter is a filename
443 * @return HTTP_Request2
444 * @throws net_php_pear_HTTP_Request2_Exception
445 */
446 public function setBody($body, $isFilename = false)
447 {
448 if (!$isFilename) {
449 $this->body = (string)$body;
450 } else {
451 if (!($fp = @fopen($body, 'rb'))) {
452 throw new net_php_pear_HTTP_Request2_Exception("Cannot open file {$body}");
453 }
454 $this->body = $fp;
455 if (empty($this->headers['content-type'])) {
456 $this->setHeader('content-type', self::detectMimeType($body));
457 }
458 }
459
460 return $this;
461 }
462
463 /**
464 * Returns the request body
465 *
466 * @return string|resource|net_php_pear_HTTP_Request2_MultipartBody
467 */
468 public function getBody()
469 {
470 if (self::METHOD_POST == $this->method &&
471 (!empty($this->postParams) || !empty($this->uploads))
472 ) {
473 if ('application/x-www-form-urlencoded' == $this->headers['content-type']) {
474 $body = http_build_query($this->postParams, '', '&');
475 if (!$this->getConfigValue('use_brackets')) {
476 $body = preg_replace('/%5B\d+%5D=/', '=', $body);
477 }
478 return $body;
479
480 } elseif ('multipart/form-data' == $this->headers['content-type']) {
481 require_once 'HTTP/Request2/MultipartBody.php';
482 return new net_php_pear_HTTP_Request2_MultipartBody(
483 $this->postParams, $this->uploads, $this->getConfigValue('use_brackets')
484 );
485 }
486 }
487 return $this->body;
488 }
489
490 /**
491 * Adds a file to form-based file upload
492 *
493 * Used to emulate file upload via a HTML form. The method also sets
494 * Content-Type of HTTP request to 'multipart/form-data'.
495 *
496 * If you just want to send the contents of a file as the body of HTTP
497 * request you should use setBody() method.
498 *
499 * @param string name of file-upload field
500 * @param mixed full name of local file
501 * @param string filename to send in the request
502 * @param string content-type of file being uploaded
503 * @return HTTP_Request2
504 * @throws net_php_pear_HTTP_Request2_Exception
505 */
506 public function addUpload($fieldName, $filename, $sendFilename = null,
507 $contentType = null)
508 {
509 if (!is_array($filename)) {
510 if (!($fp = @fopen($filename, 'rb'))) {
511 throw new net_php_pear_HTTP_Request2_Exception("Cannot open file {$filename}");
512 }
513 $this->uploads[$fieldName] = array(
514 'fp' => $fp,
515 'filename' => empty($sendFilename)? basename($filename): $sendFilename,
516 'size' => filesize($filename),
517 'type' => empty($contentType)? self::detectMimeType($filename): $contentType
518 );
519 } else {
520 $fps = $names = $sizes = $types = array();
521 foreach ($filename as $f) {
522 if (!is_array($f)) {
523 $f = array($f);
524 }
525 if (!($fp = @fopen($f[0], 'rb'))) {
526 throw new net_php_pear_HTTP_Request2_Exception("Cannot open file {$f[0]}");
527 }
528 $fps[] = $fp;
529 $names[] = empty($f[1])? basename($f[0]): $f[1];
530 $sizes[] = filesize($f[0]);
531 $types[] = empty($f[2])? self::detectMimeType($f[0]): $f[2];
532 }
533 $this->uploads[$fieldName] = array(
534 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types
535 );
536 }
537 if (empty($this->headers['content-type']) ||
538 'application/x-www-form-urlencoded' == $this->headers['content-type']
539 ) {
540 $this->setHeader('content-type', 'multipart/form-data');
541 }
542
543 return $this;
544 }
545
546 /**
547 * Adds POST parameter(s) to the request.
548 *
549 * @param string|array parameter name or array ('name' => 'value')
550 * @param mixed parameter value (can be an array)
551 * @return HTTP_Request2
552 */
553 public function addPostParameter($name, $value = null)
554 {
555 if (!is_array($name)) {
556 $this->postParams[$name] = $value;
557 } else {
558 foreach ($name as $k => $v) {
559 $this->addPostParameter($k, $v);
560 }
561 }
562 if (empty($this->headers['content-type'])) {
563 $this->setHeader('content-type', 'application/x-www-form-urlencoded');
564 }
565
566 return $this;
567 }
568
569 /**
570 * Notifies all observers
571 */
572 public function notify()
573 {
574 foreach ($this->observers as $observer) {
575 $observer->update($this);
576 }
577 }
578
579 /**
580 * Sets the last event
581 *
582 * Adapters should use this method to set the current state of the request
583 * and notify the observers.
584 *
585 * @param string event name
586 * @param mixed event data
587 */
588 public function setLastEvent($name, $data = null)
589 {
590 $this->lastEvent = array(
591 'name' => $name,
592 'data' => $data
593 );
594 $this->notify();
595 }
596
597 /**
598 * Returns the last event
599 *
600 * Observers should use this method to access the last change in request.
601 * The following event names are possible:
602 * <ul>
603 * <li>'connect' - after connection to remote server,
604 * data is the destination (string)</li>
605 * <li>'disconnect' - after disconnection from server</li>
606 * <li>'sentHeaders' - after sending the request headers,
607 * data is the headers sent (string)</li>
608 * <li>'sentBodyPart' - after sending a part of the request body,
609 * data is the length of that part (int)</li>
610 * <li>'receivedHeaders' - after receiving the response headers,
611 * data is HTTP_Request2_Response object</li>
612 * <li>'receivedBodyPart' - after receiving a part of the response
613 * body, data is that part (string)</li>
614 * <li>'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still
615 * encoded by Content-Encoding</li>
616 * <li>'receivedBody' - after receiving the complete response
617 * body, data is HTTP_Request2_Response object</li>
618 * </ul>
619 * Different adapters may not send all the event types. Mock adapter does
620 * not send any events to the observers.
621 *
622 * @return array The array has two keys: 'name' and 'data'
623 */
624 public function getLastEvent()
625 {
626 return $this->lastEvent;
627 }
628
629 /**
630 * Sets the adapter used to actually perform the request
631 *
632 * You can pass either an instance of a class implementing HTTP_Request2_Adapter
633 * or a class name. The method will only try to include a file if the class
634 * name starts with HTTP_Request2_Adapter_, it will also try to prepend this
635 * prefix to the class name if it doesn't contain any underscores, so that
636 * <code>
637 * $request->setAdapter('curl');
638 * </code>
639 * will work.
640 *
641 * @param string|HTTP_Request2_Adapter
642 * @return HTTP_Request2
643 * @throws net_php_pear_HTTP_Request2_Exception
644 */
645 public function setAdapter($adapter)
646 {
647 if (is_string($adapter)) {
648 if (!class_exists($adapter, false)) {
649 if (false === strpos($adapter, '_')) {
650 $adapter = 'net_php_pear_HTTP_Request2_Adapter_' . ucfirst($adapter);
651 }
652 if (preg_match('/^net_php_pear_HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter)) {
653
654 }
655 if (!class_exists($adapter, false)) {
656 throw new net_php_pear_HTTP_Request2_Exception("Class {$adapter} not found");
657 }
658 }
659 $adapter = new $adapter;
660 }
661 if (!$adapter instanceof net_php_pear_HTTP_Request2_Adapter) {
662 throw new net_php_pear_HTTP_Request2_Exception('Parameter is not a HTTP request adapter');
663 }
664 $this->adapter = $adapter;
665
666 return $this;
667 }
668
669 /**
670 * Sends the request and returns the response
671 *
672 * @throws net_php_pear_HTTP_Request2_Exception
673 * @return net_php_pear_HTTP_Request2_Response
674 */
675 public function send()
676 {
677 // Sanity check for URL
678 if (!$this->url instanceof net_php_pear_Net_URL2) {
679 throw new net_php_pear_HTTP_Request2_Exception('No URL given');
680 } elseif (!$this->url->isAbsolute()) {
681 throw new net_php_pear_HTTP_Request2_Exception('Absolute URL required');
682 } elseif (!in_array(strtolower($this->url->getScheme()), array('https', 'http'))) {
683 throw new net_php_pear_HTTP_Request2_Exception('Not a HTTP URL');
684 }
685 if (empty($this->adapter)) {
686 $this->setAdapter($this->getConfigValue('adapter'));
687 }
688 // magic_quotes_runtime may break file uploads and chunked response
689 // processing; see bug #4543
690 if ($magicQuotes = ini_get('magic_quotes_runtime')) {
691 ini_set('magic_quotes_runtime', false);
692 }
693 // force using single byte encoding if mbstring extension overloads
694 // strlen() and substr(); see bug #1781, bug #10605
695 if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
696 $oldEncoding = mb_internal_encoding();
697 mb_internal_encoding('iso-8859-1');
698 }
699
700 try {
701 $response = $this->adapter->sendRequest($this);
702 } catch (Exception $e) {
703 }
704 // cleanup in either case (poor man's "finally" clause)
705 if ($magicQuotes) {
706 ini_set('magic_quotes_runtime', true);
707 }
708 if (!empty($oldEncoding)) {
709 mb_internal_encoding($oldEncoding);
710 }
711 // rethrow the exception
712 if (!empty($e)) {
713 throw $e;
714 }
715 return $response;
716 }
717
718 /**
719 * Tries to detect MIME type of a file
720 *
721 * The method will try to use fileinfo extension if it is available,
722 * deprecated mime_content_type() function in the other case. If neither
723 * works, default 'application/octet-stream' MIME type is returned
724 *
725 * @param string filename
726 * @return string file MIME type
727 */
728 protected static function detectMimeType($filename)
729 {
730 // finfo extension from PECL available
731 if (function_exists('finfo_open')) {
732 if (!isset(self::$_fileinfoDb)) {
733 self::$_fileinfoDb = @finfo_open(FILEINFO_MIME);
734 }
735 if (self::$_fileinfoDb) {
736 $info = finfo_file(self::$_fileinfoDb, $filename);
737 }
738 }
739 // (deprecated) mime_content_type function available
740 if (empty($info) && function_exists('mime_content_type')) {
741 return mime_content_type($filename);
742 }
743 return empty($info)? 'application/octet-stream': $info;
744 }
745}
746?>
Note: See TracBrowser for help on using the repository browser.