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