PageRenderTime 71ms CodeModel.GetById 15ms app.highlight 43ms RepoModel.GetById 1ms app.codeStats 0ms

/coreylib.php

http://github.com/collegeman/coreylib
PHP | 2252 lines | 1524 code | 274 blank | 454 comment | 327 complexity | 032cc54fda6214bf6b5fcd78b0c35f31 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/*
   3Plugin Name: coreylib
   4Plugin URI: http://github.com/collegeman/coreylib
   5Description: A small PHP library for downloading, caching, and extracting data formatted as XML or JSON
   6Version: 2.0
   7Author: Aaron Collegeman
   8Author URI: http://github.com/collegeman
   9License: GPL2
  10*/
  11
  12/**
  13 * coreylib
  14 * Parse and cache XML and JSON.
  15 * @author Aaron Collegeman aaron@collegeman.net
  16 * @version 2.0
  17 *
  18 * Copyright (C)2008-2010 Fat Panda LLC.
  19 *
  20 * This program is free software; you can redistribute it and/or modify
  21 * it under the terms of the GNU General Public License as published by
  22 * the Free Software Foundation; either version 2 of the License, or
  23 * (at your option) any later version.
  24 *
  25 * This program is distributed in the hope that it will be useful,
  26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  28 * GNU General Public License for more details.
  29 *
  30 * You should have received a copy of the GNU General Public License along
  31 * with this program; if not, write to the Free Software Foundation, Inc.,
  32 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
  33 */
  34  
  35// src/core.php
  36
  37 
  38/**
  39 * Generic Exception wrapper
  40 */
  41class clException extends Exception {}
  42 
  43/**
  44 * Configuration defaults.
  45 */
  46// enable debugging output
  47@define('COREYLIB_DEBUG', false);
  48// maximum number of times to retry downloading content before failure
  49@define('COREYLIB_MAX_DOWNLOAD_ATTEMPTS', 3);
  50// the number of seconds to wait before timing out on CURL requests
  51@define('COREYLIB_DEFAULT_TIMEOUT', 30);
  52// the default HTTP method for requesting data from the URL
  53@define('COREYLIB_DEFAULT_METHOD', 'get');
  54// set this to true to disable all caching activity
  55@define('COREYLIB_NOCACHE', false);
  56// default cache strategy is clFileCache
  57@define('COREYLIB_DEFAULT_CACHE_STRATEGY', 'clFileCache');
  58// the name of the folder to create for clFileCache files - this folder is created inside the path clFileCache is told to use
  59@define('COREYLIB_FILECACHE_DIR', '.coreylib');
  60// auto-detect WordPress environment?
  61@define('COREYLIB_DETECT_WORDPRESS', true);
  62
  63/**
  64 * Coreylib core.
  65 */
  66class clApi {
  67  
  68  // request method
  69  const METHOD_GET = 'get';
  70  const METHOD_POST = 'post';
  71  private $method;
  72  
  73  // the URL provided in the constructor
  74  private $url;
  75  // default HTTP headers
  76  private $headers = array(
  77    
  78  );
  79  // default curlopts
  80  private $curlopts = array(
  81    CURLOPT_USERAGENT => 'coreylib/2.0',
  82    CURLOPT_SSL_VERIFYPEER => false,
  83    CURLOPT_SSL_VERIFYHOST => false
  84  );
  85  // the parameters being passed in the request
  86  private $params = array();
  87  // basic authentication 
  88  private $user;
  89  private $pass;
  90  // the cURL handle used to get the content
  91  private $ch;
  92  // reference to caching strategy
  93  private $cache;
  94  // the download
  95  private $download;
  96  // the cache key
  97  private $cache_key;
  98  
  99  /**
 100   * @param String $url The URL to connect to, with or without query string
 101   * @param clCache $cache An instance of an implementation of clCache, or null (the default)
 102   *   to trigger the use of the global caching impl, or false, to indicate that no caching
 103   *   should be performed.
 104   */
 105  function __construct($url, $cache = null) {
 106    // parse the URL and extract things like user, pass, and query string
 107    if (( $parts = @parse_url($url) ) && strtolower($parts['scheme']) != 'file') {
 108      $this->user = @$parts['user'];
 109      $this->pass = @$parts['pass'];
 110      @parse_str($parts['query'], $this->params);
 111      // rebuild $url
 112      $url = sprintf('%s://%s%s', 
 113        $parts['scheme'], 
 114        $parts['host'] . ( @$parts['port'] ? ':'.$parts['port'] : '' ),
 115        $parts['path'] . ( @$parts['fragment'] ? ':'.$parts['fragment'] : '')
 116      );
 117    }
 118    // stash the processed $url
 119    $this->url = $url;
 120    // setup the default request method
 121    $this->method = ($method = strtolower(COREYLIB_DEFAULT_METHOD)) ? $method : self::METHOD_GET;
 122    
 123    $this->curlopt(CURLOPT_CONNECTTIMEOUT, COREYLIB_DEFAULT_TIMEOUT);
 124    
 125    $this->cache = is_null($cache) ? coreylib_get_cache() : $cache;
 126  }
 127
 128  function getUrl() {
 129    return $this->url;
 130  }
 131  
 132  /**
 133   * Download and parse the data from the specified endpoint using an HTTP GET.
 134   * @param mixed $cache_for An expression of time (e.g., 10 minutes), or 0 to cache forever, or FALSE to flush the cache, or -1 to skip over all caching (the default)
 135   * @param string One of clApi::METHOD_GET or clApi::METHOD_POST, or null
 136   * @param string (optional) Force the node type, ignoring content type signals and auto-detection
 137   * @return clNode if parsing succeeds; otherwise FALSE.
 138   * @see http://php.net/manual/en/function.strtotime.php
 139   */
 140  function &parse($cache_for = -1, $override_method = null, $node_type = null) {
 141    $node = false;
 142    
 143    if (is_null($this->download)) {
 144      $this->download = $this->download(false, $cache_for, $override_method);
 145    }
 146      
 147    // if the download succeeded
 148    if ($this->download->is2__()) {
 149      if ($node_type) {
 150        $node = clNode::getNodeFor($this->download->getContent(), $node_type);
 151      } else if ($this->download->isXml()) {
 152        $node = clNode::getNodeFor($this->download->getContent(), 'xml');
 153      } else if ($this->download->isJson()) {
 154        $node = clNode::getNodeFor($this->download->getContent(), 'json');
 155      } else {
 156        throw new clException("Unable to determine content type. You can force a particular type by passing a third argument to clApi->parse(\$cache_for = -1, \$override_method = null, \$node_type = null).");
 157      }
 158    } 
 159      
 160    return $node;
 161  }
 162  
 163  /**
 164   * Download and parse the data from the specified endpoint using an HTTP POST.
 165   * @param mixed $cache_for An expression of time (e.g., 10 minutes), or 0 to cache forever, or FALSE to flush the cache, or -1 to skip over all caching (the default)
 166   * @return bool TRUE if parsing succeeds; otherwise FALSE.
 167   * @see http://php.net/manual/en/function.strtotime.php
 168   * @deprecated Use clApi->parse($cache_for, clApi::METHOD_POST) instead.
 169   */
 170  function post($cache_for = -1) {
 171    return $this->parse($cache_for, self::METHOD_POST);
 172  }
 173  
 174  /**
 175   * Retrieve the content of the parsed document.
 176   */
 177  function getContent() {
 178    return $this->download ? $this->download->getContent() : '';
 179  }
 180  
 181  /**
 182   * Print the content of the parsed document.
 183   */
 184  function __toString() {
 185    return $this->getContent();
 186  }
 187  
 188  /**
 189   * Set or get a coreylib configuration setting.
 190   * @param mixed $option Can be either a string or an array of key/value configuration settings
 191   * @param mixed $value The value to assign
 192   * @return mixed If $value is null, then return the value stored by $option; otherwise, null.
 193   */
 194  static function setting($option, $value = null) {
 195    if (!is_null($value) || is_array($option)) {
 196      if (is_array($option)) {
 197        self::$options = array_merge(self::$options, $option);
 198      } else {
 199        self::$options[$option] = $value;
 200      }
 201    } else {
 202      return @self::$options[$option];
 203    }
 204  }
 205  
 206  /**
 207   * Set or get an HTTP header configuration
 208   * @param mixed $name Can be either a string or an array of key/value pairs
 209   * @param mixed $value The value to assign
 210   * @return mixed If $value is null, then return the value stored by $name; otherwise, null.
 211   */
 212  function header($name, $value = null) {
 213    if (!is_null($value) || is_array($name)) {
 214      if (is_array($name)) {
 215        $this->headers = array_merge($this->headers, $name);
 216      } else {
 217        $this->headers[$name] = $value;
 218      }
 219    } else {
 220      return @$this->headers[$name];
 221    }
 222  }
 223  
 224  /**
 225   * Set or get a request parameter
 226   * @param mixed $name Can be either a string or an array of key/value pairs
 227   * @param mixed $value The value to assign
 228   * @return mixed If $value is null, then return the value stored by $name; otherwise, null.
 229   */
 230  function param($name, $value = null) {
 231    if (!is_null($value) || is_array($name)) {
 232      if (is_array($name)) {
 233        $this->params = array_merge($this->params, $name);
 234      } else {
 235        $this->params[$name] = $value;
 236      }
 237    } else {
 238      return @$this->params[$name];
 239    }
 240  }
 241  
 242  /**
 243   * Set or get a CURLOPT configuration
 244   * @param mixed $opt One of the CURL option constants, or an array of option/value pairs
 245   * @param mixed $value The value to assign
 246   * @return mixed If $value is null, then return the value stored by $opt; otherwise, null.
 247   */
 248  function curlopt($opt, $value = null) {
 249    if (!is_null($value) || is_array($opt)) {
 250      if (is_array($opt)) {
 251        $this->curlopts = array_merge($this->curlopts, $opt);
 252      } else {
 253        $this->curlopts[$opt] = $value;
 254      }
 255    } else {
 256      return @$this->curlopts[$opt];
 257    }
 258  }
 259  
 260  /**
 261   * Download the content according to the settings on this object, or load from the cache.
 262   * @param bool $queue If true, setup a CURL connection and return the handle; otherwise, execute the handle and return the content
 263   * @param mixed $cache_for One of:
 264   *    An expression of how long to cache the data (e.g., "10 minutes")
 265   *    0, indicating cache duration should be indefinite
 266   *    FALSE to regenerate the cache
 267   *    or -1 to skip over all caching (the default)
 268   * @param string $override_method one of clApi::METHOD_GET or clApi::METHOD_POST; optional, defaults to null. 
 269   * @return clDownload
 270   * @see http://php.net/manual/en/function.strtotime.php
 271   */
 272  function &download($queue = false, $cache_for = -1, $override_method = null) {
 273    $method = is_null($override_method) ? $this->method : $override_method;
 274  
 275    $qs = http_build_query($this->params);
 276    $url = ($method == self::METHOD_GET ? $this->url.($qs ? '?'.$qs : '') : $this->url);
 277    
 278    // use the URL to generate a cache key unique to request and any authentication data present
 279    $this->cache_key = $cache_key = md5($method.$this->user.$this->pass.$url.$qs);
 280    if (($download = $this->cacheGet($cache_key, $cache_for)) !== false) {
 281      return $download;
 282    }
 283    
 284    // TODO: implement file:// protocol here
 285    
 286    $this->ch = curl_init($url);
 287    
 288    // authenticate?
 289    if ($this->user) {
 290      curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
 291      curl_setopt($this->ch, CURLOPT_USERPWD, "$this->user:$this->pass");
 292    }
 293    
 294    // set headers
 295    $headers = array();
 296    foreach($this->headers as $name => $value) {
 297      $headers[] = "{$name}: {$value}";
 298    }
 299    curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
 300    
 301    // apply pre-set curl opts, allowing some (above) to be overwritten
 302    foreach($this->curlopts as $opt => $val) {
 303      curl_setopt($this->ch, $opt, $val);
 304    }
 305    
 306    curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
 307    
 308    if ($this->method != self::METHOD_POST) {
 309      curl_setopt($this->ch, CURLOPT_HTTPGET, true);
 310    } else {
 311      curl_setopt($this->ch, CURLOPT_POST, true);
 312      curl_setopt($this->ch, CURLOPT_POSTFIELDS, $this->params);
 313    }
 314    
 315    if ($queue) {
 316      $download = new clDownload($this->ch, false);
 317      
 318    } else {
 319      $content = curl_exec($this->ch);
 320      $download = new clDownload($this->ch, $content);
 321      
 322      // cache?
 323      if ($download->is2__()) {
 324        $this->cacheSet($cache_key, $download, $cache_for);
 325      }
 326    }
 327    
 328    return $download;
 329  }
 330  
 331  function setDownload(&$download) {
 332    if (!($download instanceof clDownload)) {
 333      throw new Exception('$download must be of type clDownload');
 334    }
 335    
 336    $this->download = $download;
 337  }
 338  
 339  function cacheWith($clCache) {
 340    $this->cache = $clCache;
 341  }
 342  
 343  function cacheGet($cache_key, $cache_for = -1) {
 344    if (!$this->cache || COREYLIB_NOCACHE || $cache_for === -1 || $cache_for === false) {
 345      return false;
 346    }
 347    return $this->cache->get($cache_key);
 348  }
 349  
 350  function cacheSet($cache_key, $download, $cache_for = -1) {
 351    if (!$this->cache || COREYLIB_NOCACHE || $cache_for === -1) {
 352      return false;
 353    } else {
 354      return $this->cache->set($cache_key, $download, $cache_for);
 355    }
 356  }
 357
 358  /**
 359   * Delete cache entry for this API.
 360   * Note that the cache key is generated from several components of the request,
 361   * including: the request method, the URL, the query string (parameters), and
 362   * any username or password used. Changing any one of these before executing
 363   * this function will modify the cache key used to store/retrieve the cached
 364   * response. So, make sure to fully configure your clApi instance before running 
 365   * this method.
 366   * @param string $override_method For feature parity with clApi->parse, allows
 367   * for overriding the HTTP method used in cache key generation. 
 368   * @return A reference to this clApi instance (to support method chaining)
 369   */
 370  function &flush($override_method = null) {
 371    $method = is_null($override_method) ? $this->method : $override_method;
 372    $qs = http_build_query($this->params);
 373    $url = ($method == self::METHOD_GET ? $this->url.($qs ? '?'.$qs : '') : $this->url);
 374    // use the URL to generate a cache key unique to request and any authentication data present
 375    $cache_key = md5($method.$this->user.$this->pass.$url.$qs);
 376    $this->cacheDel($cache_key);
 377
 378    return $this;
 379  }
 380  
 381  function cacheDel($cache_key = null) {
 382    if (!$this->cache || COREYLIB_NOCACHE) {
 383      return false;
 384    } else {
 385      return $this->cache->del($cache_key);
 386    }
 387  }
 388  
 389  function getCacheKey() {
 390    return $this->cache_key;
 391  }
 392  
 393  function &getDownload() {
 394    return $this->download;
 395  }
 396
 397  static $sort_by = null;
 398
 399  /**
 400   * Given a collection of clNode objects, use $selector to query a set of nodes
 401   * from each, then (optionally) sort those nodes by one or more sorting filters.
 402   * Sorting filters should be specified <type>:<selector>, where <type> is one of
 403   * str, num, date, bool, or fx and <selector> is a valid node selector expression.
 404   * The value at <selector> in each node will be converted to <type>, and the 
 405   * collection will then be sorted by those converted values. In the special case
 406   * of fx, <selector> should instead be a callable function. The function (a custom)
 407   * sorting rule, should be implemented as prescribed by the usort documentation,
 408   * and should handle node value selection internally.
 409   * @param mixed $apis array(clNode), an array of stdClass objects (the return value of clApi::exec), a single clNode instance, or a URL to query
 410   * @param string $selector
 411   * @param string $sort_by
 412   * @return array(clNode) A (sometimes) sorted collection of clNode objects
 413   * @see http://www.php.net/manual/en/function.usort.php
 414   */
 415  static function &grep($nodes, $selector, $sort_by = null /* dynamic args */) {
 416    $args = func_get_args();
 417    $nodes = @array_shift($args);
 418
 419    if (!$nodes) {
 420      return false;
 421
 422    } else if (!is_array($nodes)) {
 423      if ($nodes instanceof clNode) {
 424        $nodes = array($nodes);
 425      } else {
 426        $api = new clApi((string) $nodes);
 427        if ($node = $api->parse()) {
 428          clApi::log("The URL [$nodes] did not parse, so clApi::grep fails.", E_USER_ERROR);
 429          return false;
 430        }
 431        $nodes = array($node);
 432      }
 433    }
 434
 435    $selector = @array_shift($args);
 436
 437    if (!$selector) {
 438      clApi::log('clApi::grep requires $selector argument (arg #2)', E_USER_WARNING);
 439      return false;
 440    }
 441
 442    $sort_by = array();
 443
 444    foreach($args as $s) {
 445      if (preg_match('/(.*?)\:(.*)/', $s, $matches)) {
 446        @list($type, $order) = preg_split('/,\s*/', $matches[1]);
 447        if (!$order) {
 448          $order = 'asc';
 449        }
 450        $sort_by[] = (object) array(
 451          'type' => $type,
 452          'order' => strtolower($order),
 453          'selector' => $matches[2] 
 454        );
 455      } else {
 456        clApi::log("clApi::grep $sort_by arguments must be formatted <type>:<selector>: [{$s}] is invalid.", E_USER_WARNING);
 457      }
 458    }
 459
 460    // build the node collection
 461    $grepd = array();
 462    foreach($nodes as $node) {
 463      // automatically detect clApi::exec results...
 464      if ($node instanceof stdClass) {
 465        if ($node->parsed) {
 466          $grepd = array_merge( $grepd, $node->parsed->get($selector)->toArray() );
 467        } else {
 468          clApi::log(sprintf("clApi::grep can't sort failed parse on [%s]", $node->api->getUrl()), E_USER_WARNING);
 469        }
 470      } else {
 471        $grepd = array_merge( $grepd, $node->get($selector)->toArray() );
 472      }
 473    }
 474
 475    // sort the collection
 476    foreach($sort_by as $s) {
 477      self::$sort_by = $s;
 478      usort($grepd, array('clApi', 'grep_sort'));
 479      if ($order == 'desc') {
 480        $grepd = array_reverse($grepd);
 481      }
 482    }
 483
 484    return $grepd;
 485  }
 486
 487  static function grep_sort($node1, $node2) {
 488    $sort_by = self::$sort_by;
 489    $v1 = $node1->get($sort_by->selector);
 490    $v2 = $node2->get($sort_by->selector);
 491
 492    if ($sort_by->type == 'string') {
 493      $v1 = (string) $v1;
 494      $v2 = (string) $v2;
 495      return strcasecmp($v1, $v2);
 496
 497    } else if ($sort_by->type == 'bool') {
 498      $v1 = (bool) (string) $v1;
 499      $v2 = (bool) (string) $v2;
 500      return ($v1 === $v2) ? 0 : ( $v1 === true ? -1 : 1 );
 501
 502    } else if ($sort_by->type == 'num') {
 503      $v1 = (float) (string) $v1;
 504      $v2 = (float) (string) $v2;
 505      return ($v1 === $v2) ? 0 : ( $v1 < $v2 ? -1 : 1 );
 506
 507    } else if ($sort_by->type == 'date') {
 508      $v1 = strtotime((string) $v1);
 509      $v2 = strtotime((string) $v2);
 510      return ($v1 === $v2) ? 0 : ( $v1 < $v2 ? -1 : 1 );
 511
 512    }
 513  }
 514  
 515  /**
 516   * Use curl_multi to execute a collection of clApi objects.
 517   */
 518  static function exec($apis, $cache_for = -1, $override_method = null, $node_type = null) {
 519    $mh = curl_multi_init();
 520    
 521    $handles = array();
 522    
 523    foreach($apis as $a => $api) {
 524      if (is_string($api)) {
 525        $api = new clApi($api);
 526        $apis[$a] = $api;
 527      } else if (!($api instanceof clApi)) {
 528        throw new Exception("clApi::exec expects an Array of clApi objects.");
 529      }
 530      
 531      $download = $api->download(true, $cache_for, $override_method);
 532      $ch = $download->getCurl();
 533      
 534      if ($download->getContent() === false) {
 535        curl_multi_add_handle($mh, $ch);
 536      } else {
 537        $api->setDownload($download);
 538      }
 539      
 540      $handles[(int) $ch] = array($api, $download, $ch);
 541    }
 542    
 543    do {
 544      $status = curl_multi_exec($mh, $active);
 545    } while($status == CURLM_CALL_MULTI_PERFORM || $active);
 546    
 547    foreach($handles as $ch => $ref) {
 548      list($api, $download, $ch) = $ref;
 549      
 550      // update the download object with content and CH info 
 551      $download->update(curl_multi_getcontent($ch), curl_getinfo($ch));
 552      
 553      // if the download was a success
 554      if ($download->is2__()) {
 555        // cache the download
 556        $api->cacheSet($api->getCacheKey(), $download, $cache_for);
 557      }
 558      
 559      $api->setDownload($download);
 560    }
 561    
 562    $results = array();
 563    
 564    foreach($apis as $api) {
 565      $results[] = (object) array(
 566        'api' => $api,
 567        'parsed' => $api->parse($cache_for = -1, $override_method = null, $node_type = null)
 568      );
 569    }
 570    
 571    curl_multi_close($mh);
 572    
 573    return $results;
 574  }
 575  
 576  /**
 577   * Print $msg to the error log.
 578   * @param mixed $msg Can be a string, or an Exception, or any other object
 579   * @param int $level One of the E_USER_* error level constants.
 580   * @return string The value of $msg, post-processing
 581   * @see http://www.php.net/manual/en/errorfunc.constants.php
 582   */
 583  static function log($msg, $level = E_USER_NOTICE) {
 584    if ($msg instanceof Exception) {
 585      $msg = $msg->getMessage();
 586    } else if (!is_string($msg)) {
 587      $msg = print_r($msg, true);
 588    }
 589
 590    if ($level == E_USER_NOTICE && !COREYLIB_DEBUG) {
 591      // SHHH...
 592      return $msg;
 593    }
 594    
 595    trigger_error($msg, $level);
 596    
 597    return $msg;
 598  }
 599  
 600}
 601
 602if (!function_exists('coreylib')):
 603  function coreylib($url, $cache_for = -1, $params = array(), $method = clApi::METHOD_GET) {
 604    $api = new clApi($url);
 605    $api->param($params);
 606    if ($node = $api->parse($cache_for, $method)) {
 607      return $node;
 608    } else {
 609      return false;
 610    }
 611  }
 612endif;
 613
 614class clDownload {
 615  
 616  private $content = '';
 617  private $ch;
 618  private $info;
 619  
 620  function __construct(&$ch = null, $content = false) {
 621    $this->ch = $ch;
 622    $this->info = curl_getinfo($this->ch);
 623    $this->content = $content;
 624  }
 625  
 626  function __sleep() {
 627    return array('info', 'content');
 628  }
 629 
 630  function getContent() {
 631    return $this->content;
 632  }
 633  
 634  function update($content, $info) {
 635    $this->content = $content;
 636    $this->info = $info;
 637  }
 638  
 639  function hasContent() {
 640    return (bool) strlen(trim($this->content));
 641  }
 642  
 643  function &getCurl() {
 644    return $this->ch;
 645  }
 646  
 647  function getInfo() {
 648    return $this->info;
 649  }
 650  
 651  private static $xmlContentTypes = array(
 652    'text/xml',
 653    'application/rss\+xml',
 654    'xml'
 655  );
 656  
 657  function isXml() {
 658    if (preg_match(sprintf('#(%s)#i', implode('|', self::$xmlContentTypes)), $this->info['content_type'])) {
 659      return true;
 660    } else if (stripos('<?xml', trim($this->content)) === 0) {
 661      return true;
 662    } else {
 663      return false;
 664    }
 665  }
 666  
 667  private static $jsonContentTypes = array(
 668    'text/javascript',
 669    'application/x-javascript',
 670    'application/json',
 671    'text/x-javascript',
 672    'text/x-json',
 673    '.*json.*'
 674  );
 675  
 676  function isJson() {
 677    if (preg_match(sprintf('#(%s)#i', implode('|', self::$jsonContentTypes)), $this->info['content_type'])) {
 678      return true;
 679    } else if (substr(trim($this->content), 0) === '{' && substr(trim($this->content), -1) === '}') {
 680      return true;
 681    } else if (substr(trim($this->content), 0) === '[' && substr(trim($this->content), -1) === ']') {
 682      return true;
 683    } else {
 684      return false;
 685    }
 686  }
 687  
 688  function __call($name, $args) {
 689    if (preg_match('/^is(\d+)(_)?(_)?$/', $name, $matches)) {
 690      $status = $this->info['http_code'];
 691      
 692      if (!$status) {
 693        return false;
 694      }
 695      
 696      $http_status_code = $matches[1];
 697      $any_ten = @$matches[2];
 698      $any_one = @$matches[3];
 699      
 700      if ($any_ten || $any_one) {
 701        for($ten = 0; $ten <= ($any_ten ? 0 : 90); $ten+=10) {
 702          for($one = 0; $one <= (($any_ten || $any_one) ? 0 : 9); $one++) {
 703            $code = $http_status_code . ($ten == 0 ? '0' : '') . ($ten + $one);
 704            if ($code == $status) {
 705              return true;
 706            }
 707          }
 708        }
 709      } else if ($status == $http_status_code) {
 710        return true;
 711      } else {
 712        return false;
 713      }
 714    } else {
 715      throw new clException("Call to unknown function: $name");
 716    }
 717  }
 718  
 719}
 720// src/cache.php
 721
 722
 723/**
 724 * Core caching pattern.
 725 */
 726abstract class clCache {
 727 
 728  /**
 729   * Get the value stored in this cache, uniquely identified by $cache_key.
 730   * @param string $cache_key The cache key
 731   * @param bool $return_raw Instead of returning the cached value, return a packet
 732   *   of type stdClass, with two properties: expires (the timestamp
 733   *   indicating when this cached data should no longer be valid), and value
 734   *   (the unserialized value that was cached there)
 735   */
 736  abstract function get($cache_key, $return_raw = false);
 737  
 738  /**
 739   * Update the cache at $cache_key with $value, setting the expiration
 740   * of $value to a moment in the future, indicated by $timeout.
 741   * @param string $cache_key Uniquely identifies this cache entry
 742   * @param mixed $value Some arbitrary value; can be any serializable type
 743   * @param mixed $timeout An expression of time or a positive integer indicating the number of seconds;
 744   *   a $timeout of 0 indicates "cache indefinitely."
 745   * @return a stdClass instance with two properties: expires (the timestamp
 746   * indicating when this cached data should no longer be valid), and value
 747   * (the unserialized value that was cached there)
 748   * @see http://php.net/manual/en/function.strtotime.php
 749   */
 750  abstract function set($cache_key, $value, $timeout = 0);
 751  
 752  /**
 753   * Remove from the cache the value uniquely identified by $cache_key
 754   * @param string $cache_key
 755   * @return true when the cache key existed; otherwise, false
 756   */
 757  abstract function del($cache_key);
 758  
 759  /**
 760   * Remove all cache entries.
 761   */
 762  abstract function flush();
 763  
 764  /** 
 765   * Store or retrieve the global cache object.
 766   */
 767  static $cache;
 768  static function cache($cache = null) {
 769    if (!is_null($cache)) {
 770      if (!($cache instanceof clCache)) {
 771        throw new Exception('Object %s does not inherit from clCache', get_class($object));
 772      }
 773      self::$cache = new clStash($cache);
 774    }
 775    
 776    if (!self::$cache) {
 777      try {
 778        // default is FileCache
 779        $class = COREYLIB_DEFAULT_CACHE_STRATEGY;
 780        $cache = new $class();
 781        self::$cache = new clStash($cache);
 782      } catch (Exception $e) {
 783        clApi::log($e, E_USER_WARNING);
 784        return false;
 785      }
 786    }
 787    
 788    return self::$cache;
 789  }
 790
 791  private static $buffers = array();
 792
 793  /**
 794   * Attempt to find some cached content. If it's found, echo
 795   * the content, and return true. If it's not found, invoke ob_start(),
 796   * and return false. In the latter case, the calling script should
 797   * next proceed to generate the content to be cached, then, the
 798   * script should call clCache::save(), thus caching the content and
 799   * printing it at the same time.
 800   * @param string $cache_key
 801   * @param mixed $cache_for An expression of how long the content 
 802   * should be cached
 803   * @param clCache $cache Optionally, a clCache implementation other
 804   * than the global default
 805   * @return mixed - see codedoc above
 806   */
 807  static function cached($cache_key, $cache_for = -1, $cache = null) {
 808    $cache = self::cache($cache);
 809
 810    if ($cached = $cache->get($cache_key, true)) {
 811      if ($cached->expires != 0 && $cached->expires <= self::time()) {
 812        self::$buffers[] = (object) array(
 813          'cache' => $cache,
 814          'cache_key' => $cache_key,
 815          'cache_for' => $cache_for
 816        );
 817        ob_start();
 818        return false;
 819      } else {
 820        echo $cached->value;
 821        return true;
 822      }
 823    } else {
 824      self::$buffers[] = (object) array(
 825        'cache' => $cache,
 826        'cache_key' => $cache_key,
 827        'cache_for' => $cache_for
 828      );
 829      ob_start();
 830      return false;
 831    }
 832  }
 833
 834  /**
 835   * Save the current cache buffer.
 836   * @see clCache::cached
 837   */
 838  static function save($cache_for = null) {
 839    if ($buffer = array_pop(self::$buffers)) {
 840      $buffer->cache->set($buffer->cache_key, ob_get_flush(), $cache_for ? $cache_for : $buffer->cache_for);
 841    } else {
 842      clApi::log("clCache::save called, but no buffer was open", E_USER_WARNING);
 843    }   
 844  }
 845
 846  /**
 847   * Cancel the current cache buffer.
 848   * @see clCache::cached
 849   */
 850  static function cancel() {
 851    if (!array_pop(self::$buffers)) {
 852      clApi::log("clCache::cancel called, but no buffer was open");
 853    }
 854  }
 855
 856  /**
 857   * Read data from the global clCache instance.
 858   */
 859  static function read($cache_key) {
 860    $cache = self::cache();
 861    return $cache->get($cache_key);
 862  }
 863
 864  /**
 865   * Delete content cached in the global default clCache instance.
 866   */
 867  static function delete($cache_key) {
 868    $cache = self::cache();
 869    $cache->del($cache_key);
 870  }
 871  
 872  /**
 873   * Write content to the global clCache instance.
 874   */
 875  static function write($cache_key, $value, $timeout = -1) {
 876    $cache = self::cache();
 877    return $cache->set($cache_key, $value, $timeout);
 878  }
 879
 880  /**
 881   * Convert timeout expression to timestamp marking the moment in the future
 882   * at which point the timeout (or expiration) would occur.
 883   * @param mixed $timeout An expression of time or a positive integer indicating the number of seconds
 884   * @see http://php.net/manual/en/function.strtotime.php
 885   * @return a *nix timestamp in the future, or the current time if $timeout is 0, always in GMT.
 886   */
 887  static function time($timeout = 0) {
 888    if ($timeout === -1) {
 889      return false;
 890    }
 891    
 892    if (!is_numeric($timeout)) {
 893      $original = trim($timeout);
 894  
 895      // normalize the expression: should be future
 896      $firstChar = substr($timeout, 0, 1);
 897      if ($firstChar == "-") {
 898        $timeout = substr($timeout, 1);
 899      } else if ($firstChar != "-") {
 900        if (stripos($timeout, 'last') === false) {
 901          $timeout = str_replace('last', 'next', $timeout);
 902        }
 903      }
 904      
 905      if (($timeout = strtotime(gmdate('c', strtotime($timeout)))) === false) {
 906        clApi::log("'$original' is an invalid expression of time.", E_USER_WARNING);
 907        return false;
 908      }
 909            
 910      return $timeout;
 911    } else {
 912      return strtotime(gmdate('c'))+$timeout;
 913    }
 914  }
 915
 916  /**
 917   * Produce a standard cache packet.
 918   * @param $value to be wrapped
 919   * @return stdClass
 920   */
 921  static function raw(&$value, $expires) {
 922    return (object) array(
 923      'created' => self::time(),
 924      'expires' => $expires,
 925      'value' => $value
 926    );
 927  }
 928  
 929}
 930
 931/**
 932 * A proxy for another caching system -- stashes the cached
 933 * data in memory, for fastest possible access. 
 934 */
 935class clStash extends clCache {
 936  
 937  private $proxied;
 938  private $mem = array();
 939  
 940  function __construct($cache) {
 941    if (is_null($cache)) {
 942      throw new clException("Cache object to proxy cannot be null.");
 943    } else if (!($cache instanceOf clCache)) {
 944      throw new clException("Cache object must inherit from clCache");
 945    }
 946    $this->proxied = $cache;
 947  }
 948  
 949  function get($cache_key, $return_raw = false) {
 950    if ($stashed = @$this->mem[$cache_key]) {
 951      // is the stash too old?
 952      if ($stashed->expires != 0 && $stashed->expires <= self::time()) {
 953        // yes, stash is too old. try to resource, just in case
 954        if ($raw = $this->proxied->get($cache_key, true)) {
 955          // there was something fresher in the proxied cache, to stash it
 956          $this->mem[$cache_key] = $raw;
 957          // then return the requested data
 958          return $return_raw ? $raw : $raw->value;
 959        // nope... we got nothing
 960        } else {
 961          return false;
 962        }
 963      // no, the stash was not too old
 964      } else {
 965        clApi::log("Cached data loaded from memory [{$cache_key}]");
 966        return $return_raw ? $stashed : $stashed->value;
 967      }
 968    // there was nothing in the stash:
 969    } else {
 970      // try to retrieve from the proxied cache:
 971      if ($raw = $this->proxied->get($cache_key, true)) {
 972        // there was a value in the proxied cache:
 973        $this->mem[$cache_key] = $raw;
 974        return $return_raw ? $raw : $raw->value;
 975      // nothing in the proxied cache:
 976      } else {
 977        return false;
 978      }
 979    }
 980  }
 981  
 982  function set($cache_key, $value, $timeout = 0) {
 983    return $this->mem[$cache_key] = $this->proxied->set($cache_key, $value, $timeout);
 984  }
 985  
 986  function del($cache_key) {
 987    unset($this->mem[$cache_key]);
 988    return $this->proxied->del($cache_key);
 989  }
 990  
 991  function flush() {
 992    $this->mem[] = array();
 993    $this->proxied->flush();
 994  }
 995  
 996}
 997
 998/**
 999 * Caches data to the file system.
1000 */
1001class clFileCache extends clCache {
1002  
1003  function get($cache_key, $return_raw = false) {
1004    // seek out the cached data
1005    if (@file_exists($path = $this->path($cache_key))) {
1006      // if the data exists, try to load it into memory
1007      if ($content = @file_get_contents($path)) {
1008        // if it can be read, try to unserialize it
1009        if ($raw = @unserialize($content)) {
1010          // if it's not expired
1011          if ($raw->expires == 0 || self::time() < $raw->expires) {
1012            // return the requested data type
1013            return $return_raw ? $raw : $raw->value;
1014          // otherwise, purge the file, note the expiration, and move on
1015          } else {
1016            @unlink($path);
1017            clApi::log("Cache was expired [{$cache_key}:{$path}]");
1018            return false;
1019          }
1020        // couldn't be unserialized
1021        } else {
1022          clApi::log("Failed to unserialize cache file: {$path}", E_USER_WARNING);
1023        }
1024      // data couldn't be read, or the cache file was empty
1025      } else {
1026        clApi::log("Failed to read cache file: {$path}", E_USER_WARNING);
1027      }
1028    // cache file did not exist
1029    } else {
1030      clApi::log("Cache does not exist [{$cache_key}:{$path}]");
1031      return false;
1032    }
1033  }
1034  
1035  function set($cache_key, $value, $timeout = 0) {
1036    // make sure $timeout is valid
1037    if (($expires = self::time($timeout)) === false) {
1038      return false;
1039    }
1040    
1041    if ($serialized = @serialize($raw = self::raw($value, $expires))) {
1042      if (!@file_put_contents($path = $this->path($cache_key), $serialized)) {
1043        clApi::log("Failed to write cache file: {$path}", E_USER_WARNING);
1044      } else {
1045        return $raw;
1046      }
1047    } else {
1048      clApi::log("Failed to serialize cache data [{$cache_key}]", E_USER_WARNING);
1049      return false;
1050    }
1051  }
1052  
1053  function del($cache_key) {
1054    if (@file_exists($path = $this->path($cache_key))) {
1055      return @unlink($path);
1056    } else {
1057      return false;
1058    }
1059  }
1060  
1061  function flush() {
1062    if ($dir = opendir($this->basepath)) {
1063      while($file = readdir($dir)) {
1064        if (preg_match('#\.coreylib$#', $file)) {
1065          @unlink($this->basepath . DIRECTORY_SEPARATOR . $file);
1066        }
1067      }
1068      closedir($this->basepath);
1069    }
1070  }
1071  
1072  private $basepath;
1073  
1074  /**
1075   * @throws clException When the path that is to be the basepath for the cache
1076   * files cannot be created and/or is not writable.
1077   */
1078  function __construct($root = null) {
1079    if (is_null($root)) {
1080      $root = realpath(dirname(__FILE__));
1081    }
1082    // prepend the coreylib folder
1083    $root .= DIRECTORY_SEPARATOR . COREYLIB_FILECACHE_DIR;
1084    // if it doesn't exist
1085    if (!@file_exists($root)) {
1086      // create it
1087      if (!@mkdir($root)) {
1088        throw new clException("Unable to create File Cache basepath: {$root}");
1089      }
1090    } 
1091    
1092    // otherwise, if it's not writable
1093    if (!is_writable($root)) {
1094      throw new clException("File Cache basepath exists, but is not writable: {$root}");
1095    }
1096    
1097    $this->basepath = $root;
1098  }
1099  
1100  private static $last_path;
1101  
1102  /**
1103   * Generate the file path.
1104   */
1105  private function path($cache_key = null) {
1106    return self::$last_path = $this->basepath . DIRECTORY_SEPARATOR . md5($cache_key) . '.coreylib';
1107  }
1108  
1109  static function getLastPath() {
1110    return self::$last_path;
1111  }
1112  
1113}
1114
1115/**
1116 * Caches data to the WordPress database.
1117 */
1118class clWordPressCache extends clCache {
1119  
1120  private $wpdb;
1121  
1122  function __construct() {
1123    global $wpdb;
1124    
1125    $wpdb->coreylib = $wpdb->prefix.'coreylib';
1126    
1127    $wpdb->query("
1128      CREATE TABLE IF NOT EXISTS $wpdb->coreylib (
1129        `cache_key` VARCHAR(32) NOT NULL,
1130        `value` TEXT,
1131        `expires` DATETIME,
1132        `created` DATETIME,
1133        PRIMARY KEY(`cache_key`)
1134      );
1135    ");
1136    
1137    if (!$wpdb->get_results("SHOW TABLES LIKE '$wpdb->coreylib'")) {
1138      clApi::log("Failed to create coreylib table for WordPress: {$wpdb->coreylib}");
1139    } else {
1140      $this->wpdb =& $wpdb;
1141    }
1142  }
1143  
1144  function get($cache_key, $return_raw = false) {
1145    if (!$this->wpdb) {
1146      return false;
1147    }
1148    
1149    // prepare the SQL
1150    $sql = $this->wpdb->prepare("SELECT * FROM {$this->wpdb->coreylib} WHERE `cache_key` = %s LIMIT 1", $cache_key);
1151    // seek out the cached data
1152    if ($raw = $this->wpdb->get_row($sql)) {
1153      // convert MySQL date strings to timestamps
1154      $raw->expires = is_null($raw->expires) ? 0 : strtotime($raw->expires);
1155      $raw->created = strtotime($raw->created);
1156      $raw->value = maybe_unserialize($raw->value);
1157      // if it's not expired
1158      if (is_null($raw->expires) || self::time() < $raw->expires) {
1159        // return the requested data type
1160        return $return_raw ? $raw : $raw->value;
1161      // otherwise, purge the file, note the expiration, and move on
1162      } else {
1163        $this->del($cache_key);
1164        clApi::log("Cache was expired {$this->wpdb->coreylib}[{$cache_key}]");
1165        return false;
1166      }
1167    
1168    // cache did not exist
1169    } else {
1170      clApi::log("Cache record does not exist {$this->wpdb->coreylib}[{$cache_key}]");
1171      return false;
1172    }
1173  }
1174  
1175  function set($cache_key, $value, $timeout = 0) {
1176    if (!$this->wpdb) {
1177      return false;
1178    }
1179    
1180    // make sure $timeout is valid
1181    if (($expires = self::time($timeout)) === false) {
1182      return false;
1183    }
1184    
1185    // if the value can be serialized
1186    if ($serialized = maybe_serialize($value)) {
1187      // prepare the SQL
1188      $sql = $this->wpdb->prepare("
1189        REPLACE INTO {$this->wpdb->coreylib} 
1190        (`cache_key`, `created`, `expires`, `value`) 
1191        VALUES 
1192        (%s, %s, %s, %s)
1193      ", 
1194        $cache_key,
1195        $created = date('Y/m/d H:i:s', self::time()),
1196        $expires = date('Y/m/d H:i:s', $expires),
1197        $serialized
1198      );
1199      
1200      // insert it!
1201      $this->wpdb->query($sql);
1202      if ($this->wpdb->query($sql)) {
1203        clApi::log("Stored content in {$this->wpdb->coreylib}[{$cache_key}]");
1204      } else {
1205        clApi::log("Failed to store content in {$this->wpdb->coreylib}[{$cache_key}]", E_USER_WARNING);
1206      }
1207      
1208      return (object) array(
1209        'expires' => $expires,
1210        'created' => $created,
1211        'value' => value
1212      );
1213    } else {
1214      clApi::log("Failed to serialize cache data [{$cache_key}]", E_USER_WARNING);
1215      return false;
1216    }
1217  }
1218  
1219  function del($cache_key) {
1220    if (!$this->enabled) {
1221      return false;
1222    }
1223    // prepare the SQL
1224    $sql = $this->wpdb->prepare("DELETE FROM {$this->wpdb->coreylib} WHERE `cache_key` = %s LIMIT 1", $cache_key);
1225    return $this->wpdb->query($sql);
1226  }
1227  
1228  function flush() {
1229    if (!$this->wpdb) {
1230      return false;
1231    }
1232    
1233    $this->wpdb->query("TRUNCATE {$this->wpdb->coreylib}");
1234  }
1235  
1236}
1237
1238function coreylib_set_cache($cache) {
1239  clCache::cache($cache);
1240}
1241
1242function coreylib_get_cache() {
1243  return clCache::cache();
1244}
1245
1246function coreylib_flush() {
1247  if ($cache = clCache::cache()) {
1248    $cache->flush();
1249  }
1250}
1251
1252function cl_cached($cache_key, $cache_for = -1, $cache = null) {
1253  return clCache::cached($cache_key, $cache_for, $cache);
1254}
1255
1256function cl_save($cache_for = null) {
1257  return clCache::save($cache_for);
1258}
1259
1260function cl_cancel() {
1261  return clCache::cancel();
1262}
1263
1264function cl_delete($cache_key) {
1265  return clCache::delete($cache_key);
1266}
1267
1268function cl_read($cache_key) {
1269  return clCache::read($cache_key);
1270}
1271
1272function cl_write($cache_key) {
1273  return clCache::write($cache_key);
1274}
1275// src/node.php
1276
1277
1278/**
1279 * Parser for jQuery-inspired selector syntax.
1280 */
1281class clSelector implements ArrayAccess, Iterator {
1282  
1283  static $regex;
1284  
1285  static $attrib_exp;
1286  
1287  private $selectors = array();
1288  
1289  private $i = 0;
1290  
1291  static $tokenize = array('#', ';', '&', ',', '.', '+', '*', '~', "'", ':', '"', '!', '^', '$', '[', ']', '(', ')', '=', '>', '|', '/', '@', ' ');
1292  
1293  private $tokens;
1294  
1295  function __construct($query, $direct = null) {
1296    if (!self::$regex) {
1297      self::$regex = self::generateRegEx();
1298    }
1299
1300    $tokenized = $this->tokenize($query);
1301    
1302    $buffer = '';
1303    $eos = false;
1304    $eoq = false;
1305    $direct_descendant_flag = false;
1306    $add_flag = false;
1307    
1308    // loop over the tokenized query
1309    for ($c = 0; $c<strlen($tokenized); $c++) {
1310      // is this the end of the query?
1311      $eoq = ($c == strlen($tokenized)-1);
1312      
1313      // get the current character
1314      $char = $tokenized{$c};
1315      
1316      // note descendants-only rule
1317      if ($char == '>') {
1318        $direct_descendant_flag = true;
1319      }
1320      
1321      // note add-selector-result rule
1322      else if ($char == ',') {
1323        $add_flag = true;
1324        $eos = true;
1325      }
1326      
1327      // is the character a separator?
1328      else if ($char == '/' || $char == "\t" || $char == "\n" || $char == "\r" || $char == ' ') {
1329        // end of selector reached
1330        $eos = strlen($buffer) && true;
1331      }
1332      
1333      else {
1334        $buffer .= $char;
1335      }
1336      
1337      if (strlen($buffer) && ($eoq || $eos)) {
1338        
1339        $sel = trim($buffer);
1340        
1341        // reset the buffer
1342        $buffer = '';
1343        $eos = false;
1344        
1345        // process and clear buffer
1346        if (!preg_match(self::$regex, $sel, $matches)) {
1347          throw new clException("Failed to parse [$sel], part of query [$query].");
1348        }
1349
1350        $sel = (object) array(
1351          'element' => $this->untokenize(@$matches['element']),
1352          'is_expression' => ($this->untokenize(@$matches['attrib_exp']) != false),
1353          // in coreylib v1, passing "@attributeName" retrieved a scalar value;
1354          'is_attrib_getter' => preg_match('/^@.*$/', $query),
1355          // defaults for these:
1356          'attrib' => null,
1357          'value' => null,
1358          'suffixes' => null,
1359          'test' => null,
1360          'direct_descendant_flag' => $direct_descendant_flag || ((!is_null($direct)) ? $direct : false),
1361          'add_flag' => $add_flag
1362        );
1363        
1364        $direct_descendant_flag = false;
1365        $add_flag = false;
1366
1367        // default element selection is "all," as in all children of current node
1368        if (!$sel->element && !$sel->is_attrib_getter) {
1369          $sel->element = '*';
1370        }
1371
1372        if ($exp = @$matches['attrib_exp']) {
1373          // multiple expressions?
1374          if (strpos($exp, '][') !== false) {
1375            $attribs = array();
1376            $values = array();
1377            $tests = array();
1378
1379            $exps = explode('][', substr($exp, 1, strlen($exp)-2));
1380            foreach($exps as $exp) {
1381              if (preg_match('#'.self::$attrib_exp.'#', "[{$exp}]", $matches)) {
1382                $attribs[] = $matches['attrib_exp_name'];
1383                $tests[] = $matches['test'];
1384                $values[] = $matches['value'];
1385              }
1386            }
1387
1388            $sel->attrib = $attribs;
1389            $sel->value = $values;
1390            $sel->test = $tests;
1391          // just one expression
1392          } else {
1393            $sel->attrib = array($this->untokenize(@$matches['attrib_exp_name']));
1394            $sel->value = array($this->untokenize(@$matches['value']));
1395            $sel->test = array(@$matches['test']);
1396          }
1397        // no expression
1398        } else {
1399          $sel->attrib = $this->untokenize(@$matches['attrib']);
1400        }
1401
1402        if ($suffixes = @$matches['suffix']) {
1403          $all = array_filter(explode(':', $suffixes));
1404          $suffixes = array();
1405
1406          foreach($all as $suffix) {
1407            $open = strpos($suffix, '(');
1408            $close = strrpos($suffix, ')');
1409            if ($open !== false && $close !== false) {
1410              $label = substr($suffix, 0, $open);
1411              $val = $this->untokenize(substr($suffix, $open+1, $close-$open-1));
1412            } else {
1413              $label = $suffix;
1414              $val = true;
1415            }
1416            $suffixes[$label] = $val;
1417          }
1418
1419          $sel->suffixes = $suffixes;
1420        }
1421
1422        // alias for eq(), and backwards compat with coreylib v1
1423        if (!isset($sel->suffixes['eq']) && ($index = @$matches['index'])) {
1424          $sel->suffixes['eq'] = $index;
1425        }
1426
1427        $this->selectors[] = $sel;
1428      }
1429    }
1430  }
1431  
1432  private function tokenize($string) {
1433    $tokenized = false;
1434    foreach(self::$tokenize as $t) {
1435      while(($at = strpos($string, "\\$t")) !== false) {
1436        $tokenized = true;
1437        $token = "TKS".count($this->tokens)."TKE";
1438        $this->tokens[] = $t;
1439        $string = substr($string, 0, $at).$token.substr($string, $at+2);
1440      }
1441    }
1442    return $tokenized ? 'TK'.$string : $string;
1443  }
1444  
1445  private function untokenize($string) {
1446    if (!$string || strpos($string, 'TK') !== 0) {
1447      return $string;
1448    } else {
1449      foreach($this->tokens as $i => $t) {
1450        $token = "TKS{$i}TKE";
1451        $string = preg_replace("/{$token}/", $t, $string);
1452      }
1453      return substr($string, 2);
1454    }
1455  }
1456  
1457  function __get($name) {
1458    $sel = @$this->selectors[$this->i];
1459    return $sel->{$name};
1460  }
1461  
1462  function has_suffix($name) {
1463    $sel = $this->selectors[$this->i];
1464    return @$sel->suffixes[$name];
1465  }
1466  
1467  function index() {
1468    return $this->i;
1469  }
1470  
1471  function size() {
1472    return count($this->selectors);
1473  }
1474  
1475  function current() {
1476    return $this->selectors[$this->i];
1477  }
1478  
1479  function key() {
1480    return $this->i;
1481  }
1482  
1483  function next() {
1484    $this->i++;
1485  }
1486  
1487  function rewind() {
1488    $this->i = 0;
1489  }
1490  
1491  function valid() {
1492    return isset($this->selectors[$this->i]);
1493  }
1494  
1495  function offsetExists($offset) {
1496    return isset($this->selectors[$offset]);
1497  }
1498  
1499  function offsetGet($offset) {
1500    return $this->selectors[$offset];
1501  }
1502  
1503  function offsetSet($offset, $value) {
1504    throw new clException("clSelector objects are read-only.");
1505  }
1506  
1507  function offsetUnset($offset) {
1508    throw new clException("clSelector objects are read-only.");
1509  }
1510  
1511  function getSelectors() {
1512    return $this->selectors;
1513  }
1514  
1515  static function generateRegEx() {
1516    // characters comprising valid names
1517    // should not contain any of the characters in self::$tokenize
1518    $name = '[A-Za-z0-9\_\-]+';
1519    
1520    // element express with optional index
1521    $element = "((?P<element>(\\*|{$name}))(\\[(?P<index>[0-9]+)\\])?)";
1522    
1523    // attribute expression 
1524    $attrib = "@(?P<attrib>{$name})";
1525    
1526    // tests of equality
1527    $tests = implode('|', array(
1528      // Selects elements that have the specified attribute with a value either equal to a given string or starting with that string followed by a hyphen (-).
1529      "\\|=",
1530      // Selects elements that have the specified attribute with a value containing the a given substring.
1531      "\\*=",
1532      // Selects elements that have the specified attribute with a value containing a given word, delimited by whitespace.
1533      "~=",
1534      // Selects elements that have the specified attribute with a value ending exactly with a given string. The comparison is case sensitive.
1535      "\\$=",
1536      // Selects elements that have the specified attribute with a value exactly equal to a certain value.
1537      "=",
1538      // Select elements that either don't have the specified attribute, or do have the specified attribute but not with a certain value.
1539      "\\!=",
1540      // Selects elements that have the specified attribute with a value beginning exactly with a given string.
1541      "\\^="
1542    ));
1543    
1544    // suffix selectors
1545    $suffixes = implode('|', array(
1546      // retun nth element
1547      ":eq\\([0-9]+\\)",
1548      // return the first element
1549      ":first",
1550      // return the last element
1551      ":last",
1552      // greater than index
1553      ":gt\\([0-9]+\\)",
1554      // less than index
1555      ":lt\\([0-9]+\\)",
1556      // even only
1557      ":even",
1558      // odd only
1559      ":odd",
1560      // empty - no children, no text
1561      ":empty",
1562      // parent - has children: text nodes count
1563      ":parent",
1564      // has - contains child element
1565      ":has\\([^\\)]+\\)",
1566      // text - text node in the element is
1567      ":contains\\([^\\)]+\\)"
1568    ));
1569    
1570    $suffix_exp = "(?P<suffix>({$suffixes})+)";
1571    
1572    // attribute expression
1573    self::$attrib_exp = $attrib_exp = "\\[@?((?P<attrib_exp_name>{$name})((?P<test>{$tests})\"(?P<value>.*)\")?)\\]";
1574    
1575    // the final expression
1576    return "#^{$element}?(({$attrib})|(?P<attrib_exp>{$attrib_exp}))*{$suffix_exp}*$#";
1577  }
1578  
1579}
1580
1581class clNodeArray implements ArrayAccess, Iterator {
1582  
1583  private $arr = array();
1584  private $i;
1585  private $root;
1586  
1587  function __construct($arr = null, $root = null) {
1588    $this->root = $root;
1589    
1590    if (!is_null($arr)) {
1591      if ($arr instanceof clNodeArray) {
1592        $this->arr = $arr->toArray();
1593      } else {
1594        $this->arr = $arr;
1595      }
1596    }
1597  }
1598  
1599  function toArray() {
1600    return $this->arr;
1601  }
1602  
1603  function __get($name) {
1604    if ($node = @$this->arr[0]) {
1605      return $node->{$name};
1606    } else {
1607      return null;
1608    }
1609  }
1610  
1611  function __call($name, $args) {
1612    if (($node = @$this->arr[0]) && is_object($node)) {
1613      return call_user_func_array(array($node, $name), $args);
1614    } else if (!is_null($node)) {
1615      throw new Exception("Value in clNodeArray at index 0 is not an object.");
1616    }
1617  }
1618  
1619  function size() {
1620    return count($this->arr);
1621  }
1622  
1623  /**
1624   * Run a selector query on the direct descendants of these nodes.
1625   * @return new clNodeArray containing direct descendants
1626   */
1627  function children($selector = '*') {
1628    $sel = $selector;
1629    if (!is_object($sel)) {
1630      $sel = new clSelector($sel, true);
1631    }
1632    
1633    $children = array();
1634    foreach($this->arr as $node) {
1635      $children = array_merge($children, $node->get($sel)->toArray());
1636      $sel->rewind();
1637    }
1638    
1639    return new clNodeArray($children, $this->root);
1640  }
1641  
1642  /**
1643   * Requery the root, and append the results to the stored array.
1644   * @return Reference to this clNodeArray (supports method chaining)
1645   */
1646  function add($selector = '*') {
1647    $this->arr = array_merge($this->arr, $this->root->get($selector)->toArray());    
1648    return $this;
1649  }
1650  
1651  function current() {
1652    return $this->arr[$this->i];
1653  }
1654  
1655  function key() {
1656    return $this->i;
1657  }
1658  
1659  function next() {
1660    $this->i++;
1661  }
1662  
1663  function rewind() {
1664    $this->i = 0;
1665  }
1666  
1667  function valid() {
1668    return isset($this->arr[$this->i]);
1669  }
1670  
1671  function offsetExists($offset) {
1672    if (is_string($offset)) {
1673      if ($node = @$this->arr[0]) {
1674        return isset($node[$offset]);
1675      } else {
1676        return false;
1677      }
1678    } else {
1679      return isset($this->arr[$offset]);
1680    }
1681  }
1682  
1683  function offsetGet($offset) {
1684    if (is_string($offset)) {
1685      if ($node = @$this->arr[0]) {
1686        return @$node[$offset];
1687      } else {
1688        return null;
1689      }
1690    } else {
1691      return @$this->arr[$…

Large files files are truncated, but you can click here to view the full file