[ Index ]
 

Code source de Serendipity 1.2

Accédez au Source d'autres logiciels libres

title

Body

[fermer]

/bundled-libs/XML/RPC/ -> Server.php (source)

   1  <?php
   2  
   3  /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
   4  
   5  /**
   6   * Server commands for our PHP implementation of the XML-RPC protocol
   7   *
   8   * This is a PEAR-ified version of Useful inc's XML-RPC for PHP.
   9   * It has support for HTTP transport, proxies and authentication.
  10   *
  11   * PHP versions 4 and 5
  12   *
  13   * LICENSE: License is granted to use or modify this software
  14   * ("XML-RPC for PHP") for commercial or non-commercial use provided the
  15   * copyright of the author is preserved in any distributed or derivative work.
  16   *
  17   * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESSED OR
  18   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  19   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  20   * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
  21   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  22   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  23   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  24   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  26   * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27   *
  28   * @category   Web Services
  29   * @package    XML_RPC
  30   * @author     Edd Dumbill <edd@usefulinc.com>
  31   * @author     Stig Bakken <stig@php.net>
  32   * @author     Martin Jansen <mj@php.net>
  33   * @author     Daniel Convissor <danielc@php.net>
  34   * @copyright  1999-2001 Edd Dumbill, 2001-2005 The PHP Group
  35   * @version    CVS: $Id: Server.php,v 1.29 2005/08/14 20:25:35 danielc Exp $
  36   * @link       http://pear.php.net/package/XML_RPC
  37   */
  38  
  39  
  40  /**
  41   * Pull in the XML_RPC class
  42   */
  43  require_once dirname(__FILE__) . '/../RPC.php';
  44  
  45  /**
  46   * signature for system.listMethods: return = array,
  47   * parameters = a string or nothing
  48   * @global array $GLOBALS['XML_RPC_Server_listMethods_sig']
  49   */
  50  $GLOBALS['XML_RPC_Server_listMethods_sig'] = array(
  51      array($GLOBALS['XML_RPC_Array'],
  52            $GLOBALS['XML_RPC_String']
  53      ),
  54      array($GLOBALS['XML_RPC_Array'])
  55  );
  56  
  57  /**
  58   * docstring for system.listMethods
  59   * @global string $GLOBALS['XML_RPC_Server_listMethods_doc']
  60   */
  61  $GLOBALS['XML_RPC_Server_listMethods_doc'] = 'This method lists all the'
  62          . ' methods that the XML-RPC server knows how to dispatch';
  63  
  64  /**
  65   * signature for system.methodSignature: return = array,
  66   * parameters = string
  67   * @global array $GLOBALS['XML_RPC_Server_methodSignature_sig']
  68   */
  69  $GLOBALS['XML_RPC_Server_methodSignature_sig'] = array(
  70      array($GLOBALS['XML_RPC_Array'],
  71            $GLOBALS['XML_RPC_String']
  72      )
  73  );
  74  
  75  /**
  76   * docstring for system.methodSignature
  77   * @global string $GLOBALS['XML_RPC_Server_methodSignature_doc']
  78   */
  79  $GLOBALS['XML_RPC_Server_methodSignature_doc'] = 'Returns an array of known'
  80          . ' signatures (an array of arrays) for the method name passed. If'
  81          . ' no signatures are known, returns a none-array (test for type !='
  82          . ' array to detect missing signature)';
  83  
  84  /**
  85   * signature for system.methodHelp: return = string,
  86   * parameters = string
  87   * @global array $GLOBALS['XML_RPC_Server_methodHelp_sig']
  88   */
  89  $GLOBALS['XML_RPC_Server_methodHelp_sig'] = array(
  90      array($GLOBALS['XML_RPC_String'],
  91            $GLOBALS['XML_RPC_String']
  92      )
  93  );
  94  
  95  /**
  96   * docstring for methodHelp
  97   * @global string $GLOBALS['XML_RPC_Server_methodHelp_doc']
  98   */
  99  $GLOBALS['XML_RPC_Server_methodHelp_doc'] = 'Returns help text if defined'
 100          . ' for the method passed, otherwise returns an empty string';
 101  
 102  /**
 103   * dispatch map for the automatically declared XML-RPC methods.
 104   * @global array $GLOBALS['XML_RPC_Server_dmap']
 105   */
 106  $GLOBALS['XML_RPC_Server_dmap'] = array(
 107      'system.listMethods' => array(
 108          'function'  => 'XML_RPC_Server_listMethods',
 109          'signature' => $GLOBALS['XML_RPC_Server_listMethods_sig'],
 110          'docstring' => $GLOBALS['XML_RPC_Server_listMethods_doc']
 111      ),
 112      'system.methodHelp' => array(
 113          'function'  => 'XML_RPC_Server_methodHelp',
 114          'signature' => $GLOBALS['XML_RPC_Server_methodHelp_sig'],
 115          'docstring' => $GLOBALS['XML_RPC_Server_methodHelp_doc']
 116      ),
 117      'system.methodSignature' => array(
 118          'function'  => 'XML_RPC_Server_methodSignature',
 119          'signature' => $GLOBALS['XML_RPC_Server_methodSignature_sig'],
 120          'docstring' => $GLOBALS['XML_RPC_Server_methodSignature_doc']
 121      )
 122  );
 123  
 124  /**
 125   * @global string $GLOBALS['XML_RPC_Server_debuginfo']
 126   */
 127  $GLOBALS['XML_RPC_Server_debuginfo'] = '';
 128  
 129  
 130  /**
 131   * Lists all the methods that the XML-RPC server knows how to dispatch
 132   *
 133   * @return object  a new XML_RPC_Response object
 134   */
 135  function XML_RPC_Server_listMethods($server, $m)
 136  {
 137      global $XML_RPC_err, $XML_RPC_str, $XML_RPC_Server_dmap;
 138  
 139      $v = new XML_RPC_Value();
 140      $outAr = array();
 141      foreach ($server->dmap as $key => $val) {
 142          $outAr[] = new XML_RPC_Value($key, 'string');
 143      }
 144      foreach ($XML_RPC_Server_dmap as $key => $val) {
 145          $outAr[] = new XML_RPC_Value($key, 'string');
 146      }
 147      $v->addArray($outAr);
 148      return new XML_RPC_Response($v);
 149  }
 150  
 151  /**
 152   * Returns an array of known signatures (an array of arrays)
 153   * for the given method
 154   *
 155   * If no signatures are known, returns a none-array
 156   * (test for type != array to detect missing signature)
 157   *
 158   * @return object  a new XML_RPC_Response object
 159   */
 160  function XML_RPC_Server_methodSignature($server, $m)
 161  {
 162      global $XML_RPC_err, $XML_RPC_str, $XML_RPC_Server_dmap;
 163  
 164      $methName = $m->getParam(0);
 165      $methName = $methName->scalarval();
 166      if (strpos($methName, 'system.') === 0) {
 167          $dmap = $XML_RPC_Server_dmap;
 168          $sysCall = 1;
 169      } else {
 170          $dmap = $server->dmap;
 171          $sysCall = 0;
 172      }
 173      //  print "<!-- ${methName} -->\n";
 174      if (isset($dmap[$methName])) {
 175          if ($dmap[$methName]['signature']) {
 176              $sigs = array();
 177              $thesigs = $dmap[$methName]['signature'];
 178              for ($i = 0; $i < sizeof($thesigs); $i++) {
 179                  $cursig = array();
 180                  $inSig = $thesigs[$i];
 181                  for ($j = 0; $j < sizeof($inSig); $j++) {
 182                      $cursig[] = new XML_RPC_Value($inSig[$j], 'string');
 183                  }
 184                  $sigs[] = new XML_RPC_Value($cursig, 'array');
 185              }
 186              $r = new XML_RPC_Response(new XML_RPC_Value($sigs, 'array'));
 187          } else {
 188              $r = new XML_RPC_Response(new XML_RPC_Value('undef', 'string'));
 189          }
 190      } else {
 191          $r = new XML_RPC_Response(0, $XML_RPC_err['introspect_unknown'],
 192                                    $XML_RPC_str['introspect_unknown']);
 193      }
 194      return $r;
 195  }
 196  
 197  /**
 198   * Returns help text if defined for the method passed, otherwise returns
 199   * an empty string
 200   *
 201   * @return object  a new XML_RPC_Response object
 202   */
 203  function XML_RPC_Server_methodHelp($server, $m)
 204  {
 205      global $XML_RPC_err, $XML_RPC_str, $XML_RPC_Server_dmap;
 206  
 207      $methName = $m->getParam(0);
 208      $methName = $methName->scalarval();
 209      if (strpos($methName, 'system.') === 0) {
 210          $dmap = $XML_RPC_Server_dmap;
 211          $sysCall = 1;
 212      } else {
 213          $dmap = $server->dmap;
 214          $sysCall = 0;
 215      }
 216  
 217      if (isset($dmap[$methName])) {
 218          if ($dmap[$methName]['docstring']) {
 219              $r = new XML_RPC_Response(new XML_RPC_Value($dmap[$methName]['docstring']),
 220                                                          'string');
 221          } else {
 222              $r = new XML_RPC_Response(new XML_RPC_Value('', 'string'));
 223          }
 224      } else {
 225          $r = new XML_RPC_Response(0, $XML_RPC_err['introspect_unknown'],
 226                                       $XML_RPC_str['introspect_unknown']);
 227      }
 228      return $r;
 229  }
 230  
 231  /**
 232   * @return void
 233   */
 234  function XML_RPC_Server_debugmsg($m)
 235  {
 236      global $XML_RPC_Server_debuginfo;
 237      $XML_RPC_Server_debuginfo = $XML_RPC_Server_debuginfo . $m . "\n";
 238  }
 239  
 240  
 241  /**
 242   * A server for receiving and replying to XML RPC requests
 243   *
 244   * <code>
 245   * $server = new XML_RPC_Server(
 246   *     array(
 247   *         'isan8' =>
 248   *             array(
 249   *                 'function' => 'is_8',
 250   *                 'signature' =>
 251   *                      array(
 252   *                          array('boolean', 'int'),
 253   *                          array('boolean', 'int', 'boolean'),
 254   *                          array('boolean', 'string'),
 255   *                          array('boolean', 'string', 'boolean'),
 256   *                      ),
 257   *                 'docstring' => 'Is the value an 8?'
 258   *             ),
 259   *     ),
 260   *     1,
 261   *     0
 262   * ); 
 263   * </code>
 264   *
 265   * @category   Web Services
 266   * @package    XML_RPC
 267   * @author     Edd Dumbill <edd@usefulinc.com>
 268   * @author     Stig Bakken <stig@php.net>
 269   * @author     Martin Jansen <mj@php.net>
 270   * @author     Daniel Convissor <danielc@php.net>
 271   * @copyright  1999-2001 Edd Dumbill, 2001-2005 The PHP Group
 272   * @version    Release: 1.4.0
 273   * @link       http://pear.php.net/package/XML_RPC
 274   */
 275  class XML_RPC_Server
 276  {
 277      /**
 278       * The dispatch map, listing the methods this server provides.
 279       * @var array
 280       */
 281      var $dmap = array();
 282  
 283      /**
 284       * The present response's encoding
 285       * @var string
 286       * @see XML_RPC_Message::getEncoding()
 287       */
 288      var $encoding = '';
 289  
 290      /**
 291       * Debug mode (0 = off, 1 = on)
 292       * @var integer
 293       */
 294      var $debug = 0;
 295  
 296      /**
 297       * The response's HTTP headers
 298       * @var string
 299       */
 300      var $server_headers = '';
 301  
 302      /**
 303       * The response's XML payload
 304       * @var string
 305       */
 306      var $server_payload = '';
 307  
 308  
 309      /**
 310       * Constructor for the XML_RPC_Server class
 311       *
 312       * @param array $dispMap   the dispatch map. An associative array
 313       *                          explaining each function. The keys of the main
 314       *                          array are the procedure names used by the
 315       *                          clients. The value is another associative array
 316       *                          that contains up to three elements:
 317       *                            + The 'function' element's value is the name
 318       *                              of the function or method that gets called.
 319       *                              To define a class' method: 'class::method'.
 320       *                            + The 'signature' element (optional) is an
 321       *                              array describing the return values and
 322       *                              parameters
 323       *                            + The 'docstring' element (optional) is a
 324       *                              string describing what the method does
 325       * @param int $serviceNow  should the HTTP response be sent now?
 326       *                          (1 = yes, 0 = no)
 327       * @param int $debug       should debug output be displayed?
 328       *                          (1 = yes, 0 = no)
 329       *
 330       * @return void
 331       */
 332      function XML_RPC_Server($dispMap, $serviceNow = 1, $debug = 0)
 333      {
 334          global $HTTP_RAW_POST_DATA;
 335  
 336          if ($debug) {
 337              $this->debug = 1;
 338          } else {
 339              $this->debug = 0;
 340          }
 341  
 342          $this->dmap = $dispMap;
 343  
 344          if ($serviceNow) {
 345              $this->service();
 346          } else {
 347              $this->createServerPayload();
 348              $this->createServerHeaders();
 349          }
 350      }
 351  
 352      /**
 353       * @return string  the debug information if debug debug mode is on
 354       */
 355      function serializeDebug()
 356      {
 357          global $XML_RPC_Server_debuginfo, $HTTP_RAW_POST_DATA;
 358  
 359          if ($this->debug) {
 360              XML_RPC_Server_debugmsg('vvv POST DATA RECEIVED BY SERVER vvv' . "\n"
 361                                      . $HTTP_RAW_POST_DATA
 362                                      . "\n" . '^^^ END POST DATA ^^^');
 363          }
 364  
 365          if ($XML_RPC_Server_debuginfo != '') {
 366              return "<!-- PEAR XML_RPC SERVER DEBUG INFO:\n\n"
 367                     . preg_replace('/-(?=-)/', '- ', $XML_RPC_Server_debuginfo)
 368                     . "-->\n";
 369          } else {
 370              return '';
 371          }
 372      }
 373  
 374      /**
 375       * Sends the response
 376       *
 377       * The encoding and content-type are determined by
 378       * XML_RPC_Message::getEncoding()
 379       *
 380       * @return void
 381       *
 382       * @uses XML_RPC_Server::createServerPayload(),
 383       *       XML_RPC_Server::createServerHeaders()
 384       */
 385      function service()
 386      {
 387          if (!$this->server_payload) {
 388              $this->createServerPayload();
 389          }
 390          if (!$this->server_headers) {
 391              $this->createServerHeaders();
 392          }
 393          if (!is_array($this->server_headers)) {
 394              $this->server_headers = explode("\n", $this->server_headers);
 395          }
 396          foreach($this->server_headers AS $header) {
 397              header($header);
 398          }
 399          print $this->server_payload;
 400      }
 401  
 402      /**
 403       * Generates the payload and puts it in the $server_payload property
 404       *
 405       * @return void
 406       *
 407       * @uses XML_RPC_Server::parseRequest(), XML_RPC_Server::$encoding,
 408       *       XML_RPC_Response::serialize(), XML_RPC_Server::serializeDebug()
 409       */
 410      function createServerPayload()
 411      {
 412          $r = $this->parseRequest();
 413          $this->server_payload = '<?xml version="1.0" encoding="'
 414                                . $this->encoding . '"?>' . "\n"
 415                                . $this->serializeDebug()
 416                                . $r->serialize();
 417      }
 418  
 419      /**
 420       * Determines the HTTP headers and puts them in the $server_headers
 421       * property
 422       *
 423       * @return boolean  TRUE if okay, FALSE if $server_payload isn't set.
 424       *
 425       * @uses XML_RPC_Server::createServerPayload(),
 426       *       XML_RPC_Server::$server_headers
 427       */
 428      function createServerHeaders()
 429      {
 430          if (!$this->server_payload) {
 431              return false;
 432          }
 433          $this->server_headers = 'Content-Length: '
 434                                . strlen($this->server_payload) . "\r\n"
 435                                . 'Content-Type: text/xml;'
 436                                . ' charset=' . $this->encoding;
 437          return true;
 438      }
 439  
 440      /**
 441       * @return array
 442       */
 443      function verifySignature($in, $sig)
 444      {
 445          for ($i = 0; $i < sizeof($sig); $i++) {
 446              // check each possible signature in turn
 447              $cursig = $sig[$i];
 448              if (sizeof($cursig) == $in->getNumParams() + 1) {
 449                  $itsOK = 1;
 450                  for ($n = 0; $n < $in->getNumParams(); $n++) {
 451                      $p = $in->getParam($n);
 452                      // print "<!-- $p -->\n";
 453                      if ($p->kindOf() == 'scalar') {
 454                          $pt = $p->scalartyp();
 455                      } else {
 456                          $pt = $p->kindOf();
 457                      }
 458                      // $n+1 as first type of sig is return type
 459                      if ($pt != $cursig[$n+1]) {
 460                          $itsOK = 0;
 461                          $pno = $n+1;
 462                          $wanted = $cursig[$n+1];
 463                          $got = $pt;
 464                          break;
 465                      }
 466                  }
 467                  if ($itsOK) {
 468                      return array(1);
 469                  }
 470              }
 471          }
 472          if (isset($wanted)) {
 473              return array(0, "Wanted $wanted}, got $got} at param $pno}");
 474          } else {
 475              $allowed = array();
 476              foreach ($sig as $val) {
 477                  end($val);
 478                  $allowed[] = key($val);
 479              }
 480              $allowed = array_unique($allowed);
 481              $last = count($allowed) - 1;
 482              if ($last > 0) {
 483                  $allowed[$last] = 'or ' . $allowed[$last];
 484              }
 485              return array(0,
 486                           'Signature permits ' . implode(', ', $allowed)
 487                                  . ' parameters but the request had '
 488                                  . $in->getNumParams());
 489          }
 490      }
 491  
 492      /**
 493       * @return object  a new XML_RPC_Response object
 494       *
 495       * @uses XML_RPC_Message::getEncoding(), XML_RPC_Server::$encoding
 496       */
 497      function parseRequest($data = '')
 498      {
 499          global $XML_RPC_xh, $HTTP_RAW_POST_DATA,
 500                  $XML_RPC_err, $XML_RPC_str, $XML_RPC_errxml,
 501                  $XML_RPC_defencoding, $XML_RPC_Server_dmap;
 502  
 503          if ($data == '') {
 504              $data = $HTTP_RAW_POST_DATA;
 505          }
 506  
 507          $this->encoding = XML_RPC_Message::getEncoding($data);
 508          $parser_resource = xml_parser_create($this->encoding);
 509          $parser = (int) $parser_resource;
 510  
 511          $XML_RPC_xh[$parser] = array();
 512          $XML_RPC_xh[$parser]['cm']     = 0;
 513          $XML_RPC_xh[$parser]['isf']    = 0;
 514          $XML_RPC_xh[$parser]['params'] = array();
 515          $XML_RPC_xh[$parser]['method'] = '';
 516          $XML_RPC_xh[$parser]['stack'] = array();    
 517          $XML_RPC_xh[$parser]['valuestack'] = array();    
 518  
 519          $plist = '';
 520  
 521          // decompose incoming XML into request structure
 522  
 523          xml_parser_set_option($parser_resource, XML_OPTION_CASE_FOLDING, true);
 524          xml_set_element_handler($parser_resource, 'XML_RPC_se', 'XML_RPC_ee');
 525          xml_set_character_data_handler($parser_resource, 'XML_RPC_cd');
 526          if (!xml_parse($parser_resource, $data, 1)) {
 527              // return XML error as a faultCode
 528              $r = new XML_RPC_Response(0,
 529                                        $XML_RPC_errxml+xml_get_error_code($parser_resource),
 530                                        sprintf('XML error: %s at line %d',
 531                                                xml_error_string(xml_get_error_code($parser_resource)),
 532                                                xml_get_current_line_number($parser_resource)));
 533              xml_parser_free($parser_resource);
 534          } elseif ($XML_RPC_xh[$parser]['isf']>1) {
 535              $r = new XML_RPC_Response(0,
 536                                        $XML_RPC_err['invalid_request'],
 537                                        $XML_RPC_str['invalid_request']
 538                                        . ': '
 539                                        . $XML_RPC_xh[$parser]['isf_reason']);
 540              xml_parser_free($parser_resource);
 541          } else {
 542              xml_parser_free($parser_resource);
 543              $m = new XML_RPC_Message($XML_RPC_xh[$parser]['method']);
 544              // now add parameters in
 545              for ($i = 0; $i < sizeof($XML_RPC_xh[$parser]['params']); $i++) {
 546                  // print '<!-- ' . $XML_RPC_xh[$parser]['params'][$i]. "-->\n";
 547                  $plist .= "$i - " . var_export($XML_RPC_xh[$parser]['params'][$i], true) . " \n";
 548                  $m->addParam($XML_RPC_xh[$parser]['params'][$i]);
 549              }
 550              XML_RPC_Server_debugmsg($plist);
 551  
 552              // now to deal with the method
 553              $methName = $XML_RPC_xh[$parser]['method'];
 554              if (strpos($methName, 'system.') === 0) {
 555                  $dmap = $XML_RPC_Server_dmap;
 556                  $sysCall = 1;
 557              } else {
 558                  $dmap = $this->dmap;
 559                  $sysCall = 0;
 560              }
 561  
 562              if (isset($dmap[$methName]['function'])
 563                  && is_string($dmap[$methName]['function'])
 564                  && strpos($dmap[$methName]['function'], '::') !== false)
 565              {
 566                  $dmap[$methName]['function'] =
 567                          explode('::', $dmap[$methName]['function']);
 568              }
 569  
 570              if (isset($dmap[$methName]['function'])
 571                  && is_callable($dmap[$methName]['function']))
 572              {
 573                  // dispatch if exists
 574                  if (isset($dmap[$methName]['signature'])) {
 575                      $sr = $this->verifySignature($m,
 576                                                   $dmap[$methName]['signature'] );
 577                  }
 578                  if (!isset($dmap[$methName]['signature']) || $sr[0]) {
 579                      // if no signature or correct signature
 580                      if ($sysCall) {
 581                          $r = call_user_func($dmap[$methName]['function'], $this, $m);
 582                      } else {
 583                          $r = call_user_func($dmap[$methName]['function'], $m);
 584                      }
 585                      if (!is_a($r, 'XML_RPC_Response')) {
 586                          $r = new XML_RPC_Response(0, $XML_RPC_err['not_response_object'],
 587                                                    $XML_RPC_str['not_response_object']);
 588                      }
 589                  } else {
 590                      $r = new XML_RPC_Response(0, $XML_RPC_err['incorrect_params'],
 591                                                $XML_RPC_str['incorrect_params']
 592                                                . ': ' . $sr[1]);
 593                  }
 594              } else {
 595                  // else prepare error response
 596                  $r = new XML_RPC_Response(0, $XML_RPC_err['unknown_method'],
 597                                            $XML_RPC_str['unknown_method']);
 598              }
 599          }
 600          return $r;
 601      }
 602  
 603      /**
 604       * Echos back the input packet as a string value
 605       *
 606       * @return void
 607       *
 608       * Useful for debugging.
 609       */
 610      function echoInput()
 611      {
 612          global $HTTP_RAW_POST_DATA;
 613  
 614          $r = new XML_RPC_Response(0);
 615          $r->xv = new XML_RPC_Value("'Aha said I: '" . $HTTP_RAW_POST_DATA, 'string');
 616          print $r->serialize();
 617      }
 618  }
 619  
 620  /*
 621   * Local variables:
 622   * tab-width: 4
 623   * c-basic-offset: 4
 624   * c-hanging-comment-ender-p: nil
 625   * End:
 626   */
 627  
 628  ?>


Généré le : Sat Nov 24 09:00:37 2007 par Balluche grâce à PHPXref 0.7
  Clicky Web Analytics