PageRenderTime 61ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/sparkplug/sparkplug.php

https://code.google.com/p/sparkplug-framework/
PHP | 2320 lines | 1142 code | 300 blank | 878 comment | 103 complexity | a887e62d6aa40a4c8bdcd1d56e6260d1 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
  1. <?php
  2. /*
  3. Copyright 2009-2012 Sam Weiss
  4. All Rights Reserved.
  5. This file is part of Spark/Plug.
  6. Spark/Plug is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation, either version 3 of the License, or
  9. (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. if (!defined('spark/plug'))
  18. {
  19. header('HTTP/1.1 403 Forbidden');
  20. exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
  21. }
  22. define('spark_plug_version', '1.1.0');
  23. /**
  24. * The SparkException class is our custom exception class.
  25. *
  26. * @package Spark/Plug
  27. */
  28. // -----------------------------------------------------------------------------
  29. class SparkException extends Exception
  30. {
  31. private $_vars;
  32. /**
  33. * Construct a new exception.
  34. *
  35. * @param string $message
  36. * @param int $code
  37. * @return SparkException object
  38. */
  39. public function __construct($message, $code = 0, $vars = NULL)
  40. {
  41. parent::__construct($message, $code);
  42. $this->_vars = empty($vars) ? array() : $vars;
  43. }
  44. /**
  45. * Return vars associated with this exception.
  46. *
  47. * @return array
  48. */
  49. public function vars()
  50. {
  51. return $this->_vars;
  52. }
  53. /**
  54. * Return named var associated with this exception.
  55. *
  56. * @param string $name
  57. * @return mixed
  58. */
  59. public function getVar($name, $default = NULL)
  60. {
  61. return isset($this->_vars[$name]) ? $this->_vars[$name] : $default;
  62. }
  63. }
  64. /**
  65. * The SparkHTTPException class is used to automatically convert HTTP status codes to exceptions.
  66. *
  67. * @package Spark/Plug
  68. */
  69. // -----------------------------------------------------------------------------
  70. class SparkHTTPException extends SparkException
  71. {
  72. private $_httpStatusCode;
  73. private $_httpStatusText;
  74. /**
  75. * Convert HTTP status code to equivalent exception.
  76. *
  77. * @param string $message
  78. * @param int $code
  79. * @return SparkException object
  80. */
  81. public function __construct($httpStatusCode, $httpStatusText, $message = '', $vars = '')
  82. {
  83. parent::__construct($message, 0, $vars);
  84. $this->_httpStatusCode = $httpStatusCode;
  85. $this->_httpStatusText = $httpStatusText;
  86. }
  87. /**
  88. * Return HTTP status code associated with this exception.
  89. *
  90. * @return int
  91. */
  92. public function getHTTPStatusCode()
  93. {
  94. return $this->_httpStatusCode;
  95. }
  96. /**
  97. * Return HTTP status text associated with this exception.
  98. *
  99. * @return string
  100. */
  101. public function getHTTPStatusText()
  102. {
  103. return $this->_httpStatusText;
  104. }
  105. /**
  106. * Return HTTP status messages associated with this exception.
  107. * Suitable for use in HTTP header.
  108. *
  109. * @return string
  110. */
  111. public function getHTTPStatus()
  112. {
  113. return $this->_httpStatusCode . ' ' . $this->_httpStatusText;
  114. }
  115. }
  116. class SparkHTTPException_BadRequest extends SparkHTTPException
  117. {
  118. public function __construct($message = NULL, $vars = NULL)
  119. {
  120. parent::__construct(400, 'Bad Request', $message, $vars);
  121. }
  122. }
  123. class SparkHTTPException_Unauthorized extends SparkHTTPException
  124. {
  125. public function __construct($message = NULL, $vars = NULL)
  126. {
  127. parent::__construct(401, 'Unauthorized', $message, $vars);
  128. }
  129. }
  130. class SparkHTTPException_Forbidden extends SparkHTTPException
  131. {
  132. public function __construct($message = NULL, $vars = NULL)
  133. {
  134. isset($message) || $message = 'Permission denied.';
  135. parent::__construct(403, 'Forbidden', $message, $vars);
  136. }
  137. }
  138. class SparkHTTPException_NotFound extends SparkHTTPException
  139. {
  140. public function __construct($message = NULL, $vars = NULL)
  141. {
  142. isset($message) || $message = 'The page you requested was not found.';
  143. parent::__construct(404, 'Not Found', $message, $vars);
  144. }
  145. }
  146. class SparkHTTPException_MethodNotAllowed extends SparkHTTPException
  147. {
  148. public function __construct($message = NULL, $vars = NULL)
  149. {
  150. parent::__construct(405, 'Method Not Allowed', $message, $vars);
  151. }
  152. }
  153. class SparkHTTPException_Conflict extends SparkHTTPException
  154. {
  155. public function __construct($message = NULL, $vars = NULL)
  156. {
  157. parent::__construct(409, 'Conflict', $message, $vars);
  158. }
  159. }
  160. class SparkHTTPException_Gone extends SparkHTTPException
  161. {
  162. public function __construct($message = NULL, $vars = NULL)
  163. {
  164. parent::__construct(410, 'Gone', $message, $vars);
  165. }
  166. }
  167. class SparkHTTPException_LengthRequired extends SparkHTTPException
  168. {
  169. public function __construct($message = NULL, $vars = NULL)
  170. {
  171. parent::__construct(411, 'Length Required', $message, $vars);
  172. }
  173. }
  174. class SparkHTTPException_RequestEntityTooLarge extends SparkHTTPException
  175. {
  176. public function __construct($message = NULL, $vars = NULL)
  177. {
  178. parent::__construct(413, 'Request Entity Too Large', $message, $vars);
  179. }
  180. }
  181. class SparkHTTPException_RequestURITooLong extends SparkHTTPException
  182. {
  183. public function __construct($message = NULL, $vars = NULL)
  184. {
  185. parent::__construct(414, 'Request-URI Too Long', $message, $vars);
  186. }
  187. }
  188. class SparkHTTPException_UnsupportedMediaType extends SparkHTTPException
  189. {
  190. public function __construct($message = NULL, $vars = NULL)
  191. {
  192. parent::__construct(415, 'Unsupported Media Type', $message, $vars);
  193. }
  194. }
  195. class SparkHTTPException_InternalServerError extends SparkHTTPException
  196. {
  197. public function __construct($message = NULL, $vars = NULL)
  198. {
  199. isset($message) || $message = 'Your request could not be processed.';
  200. parent::__construct(500, 'Internal Server Error', $message, $vars);
  201. }
  202. }
  203. class SparkHTTPException_NotImplemented extends SparkHTTPException
  204. {
  205. public function __construct($message = NULL, $vars = NULL)
  206. {
  207. parent::__construct(501, 'Not Implemented', $message, $vars);
  208. }
  209. }
  210. class SparkHTTPException_ServiceUnavailable extends SparkHTTPException
  211. {
  212. public function __construct($message = NULL, $vars = NULL)
  213. {
  214. parent::__construct(503, 'Service Unavailable', $message, $vars);
  215. }
  216. }
  217. /**
  218. * The SparkPHPException class is used to automatically convert PHP errors to exceptions.
  219. * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
  220. *
  221. * @package Spark/Plug
  222. */
  223. // -----------------------------------------------------------------------------
  224. final class SparkPHPException
  225. {
  226. /**
  227. * Convert PHP error to equivalent exception.
  228. *
  229. * @param int $code
  230. * @param string $message
  231. * @param string $file
  232. * @param int $line
  233. * @throw ErrorException
  234. */
  235. public static function errorHandler($code, $message, $file, $line)
  236. {
  237. throw new ErrorException($message, 0, $code, $file, $line);
  238. }
  239. }
  240. /**
  241. * The SparkUtil class contains some utility functions commonly needed by web apps.
  242. * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
  243. *
  244. * @package Spark/Plug
  245. */
  246. // -----------------------------------------------------------------------------
  247. final class SparkUtil
  248. {
  249. const kRequestMethod_HEAD = 1;
  250. const kRequestMethod_GET = 2;
  251. const kRequestMethod_POST = 3;
  252. const kRequestMethod_PUT = 4;
  253. const kRequestMethod_DELETE = 5;
  254. const kRequestMethod_OPTIONS = 6;
  255. private static $_http_methods = array
  256. (
  257. 'head' => self::kRequestMethod_HEAD,
  258. 'get' => self::kRequestMethod_GET,
  259. 'post' => self::kRequestMethod_POST,
  260. 'put' => self::kRequestMethod_PUT,
  261. 'delete' => self::kRequestMethod_DELETE,
  262. 'options' => self::kRequestMethod_OPTIONS,
  263. );
  264. /**
  265. * Return document root.
  266. *
  267. * @return string
  268. */
  269. final public static function doc_root()
  270. {
  271. return $_SERVER['DOCUMENT_ROOT'];
  272. }
  273. /**
  274. * Return whether this page was accessed securely.
  275. *
  276. * @return boolean
  277. */
  278. final public static function is_https()
  279. {
  280. return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on');
  281. }
  282. /**
  283. * Return scheme string.
  284. *
  285. * @return string
  286. */
  287. final public static function scheme()
  288. {
  289. return self::is_https() ? 'https://' : 'http://';
  290. }
  291. /**
  292. * Return host string.
  293. *
  294. * @return string
  295. */
  296. final public static function host()
  297. {
  298. return $_SERVER['HTTP_HOST'];
  299. }
  300. /**
  301. * Return server name string.
  302. *
  303. * @return string
  304. */
  305. final public static function server_name()
  306. {
  307. return $_SERVER['SERVER_NAME'];
  308. }
  309. /**
  310. * Return server ip address string.
  311. *
  312. * @return string
  313. */
  314. final public static function server_addr()
  315. {
  316. return $_SERVER['SERVER_ADDR'];
  317. }
  318. /**
  319. * Return script name.
  320. *
  321. * @return string
  322. */
  323. final public static function script_name()
  324. {
  325. return $_SERVER['SCRIPT_NAME'];
  326. }
  327. /**
  328. * Return original request URI.
  329. *
  330. * @return string
  331. */
  332. final public static function request_uri()
  333. {
  334. return $_SERVER['REQUEST_URI'];
  335. }
  336. /**
  337. * Return original request URI minus any query parameters.
  338. *
  339. * @return string
  340. */
  341. final public static function request_uri_base()
  342. {
  343. return self::remove_query_from_url(self::request_uri());
  344. }
  345. /**
  346. * Return original request URL.
  347. *
  348. * @return string
  349. */
  350. final public static function self_url()
  351. {
  352. if (($host = self::host()) === '')
  353. {
  354. $host = self::server_name();
  355. }
  356. return self::scheme() . $host . self::request_uri();
  357. }
  358. /**
  359. * Return original request method.
  360. *
  361. * @return string
  362. */
  363. final public static function request_method()
  364. {
  365. return !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
  366. }
  367. /**
  368. * Return original query string.
  369. *
  370. * @return string
  371. */
  372. final public static function query_string()
  373. {
  374. return !empty($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
  375. }
  376. /**
  377. * Return user's browser agent string.
  378. *
  379. * @return string
  380. */
  381. final public static function user_agent()
  382. {
  383. return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
  384. }
  385. /**
  386. * Return user's IP address.
  387. *
  388. * @return string
  389. */
  390. final public static function remote_ip()
  391. {
  392. return $_SERVER['REMOTE_ADDR'];
  393. }
  394. /**
  395. * Return referring url.
  396. *
  397. * @return string
  398. */
  399. final public static function referrer_url()
  400. {
  401. return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
  402. }
  403. /**
  404. * Return referring url.
  405. *
  406. * @return string
  407. */
  408. final public static function get_http_method($method = NULL)
  409. {
  410. if (!$method)
  411. {
  412. $method = self::request_method();
  413. }
  414. if ($method = @self::$_http_methods[strtolower($method)])
  415. {
  416. return $method;
  417. }
  418. return NULL;
  419. }
  420. /**
  421. * Return boolean indicating whether the current request was made via ajax.
  422. *
  423. * @return boolean
  424. */
  425. final public static function is_ajax_request()
  426. {
  427. return (strtolower(@$_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
  428. }
  429. /**
  430. * Return array of request headers.
  431. *
  432. * @return array
  433. */
  434. final public static function request_headers()
  435. {
  436. if (!function_exists('apache_request_headers'))
  437. {
  438. function apache_request_headers()
  439. {
  440. $headers = array();
  441. foreach($_SERVER as $key => $value)
  442. {
  443. if (substr_compare($key, 'HTTP_', 0, 5) === 0)
  444. {
  445. $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key,5)))));
  446. $headers[$key] = $value;
  447. }
  448. }
  449. return $headers;
  450. }
  451. }
  452. return apache_request_headers();
  453. }
  454. /**
  455. * Check if two IP addresses match or are similar.
  456. *
  457. * @param string $ip1
  458. * @param string $ip2
  459. * @return boolean
  460. */
  461. final public static function match_ip($ip1, $ip2, $matchOctets = 4)
  462. {
  463. // full IP match?
  464. if ($ip1 == $ip2)
  465. {
  466. return true;
  467. }
  468. // entire IP address did not match (probably a pesky proxy server)
  469. // break the address into octets
  470. $octets1 = explode('.' , $ip1, 4);
  471. $octets2 = explode('.' , $ip2, 4);
  472. for ($match = 0; $match < $matchOctets; ++$match)
  473. {
  474. if ($octets1[$match] != $octets2[$match])
  475. {
  476. return false;
  477. }
  478. }
  479. return true;
  480. }
  481. /**
  482. * Check for integer value, and force to type integer if true.
  483. *
  484. * @param any &$val
  485. * @return boolean
  486. */
  487. final public static function valid_int(&$val)
  488. {
  489. if (is_int($val))
  490. {
  491. return true;
  492. }
  493. if (preg_match('/^[-+]?\d+$/', $val))
  494. {
  495. $val = (int)$val;
  496. return true;
  497. }
  498. return false;
  499. }
  500. /**
  501. * Check if URL is valid.
  502. *
  503. * httpurl = https?://{hostport}(/{hpath}(\?{search})?)?
  504. * hostport = host(:{port})?
  505. * host = {hostname}|{hostnumber}
  506. * hostname = ({domainlabel}\.)*{toplabel}
  507. * domainlabel = {alphadigit}(({alphadigit}|\-)*{alphadigit})?
  508. * toplabel = {alpha}(({alphadigit}|\-)*{alphadigit})?
  509. * hostnumber = {digits}\.{digits}\.{digits}\.{digits}
  510. * port = {digits}
  511. * hpath = {hsegment}(/{hsegment})*
  512. * hsegment = ({uchar}|[;:@&=])*
  513. * search = ({uchar}|[;:@&=])*
  514. * uchar = {unreserved}|{escape}
  515. * unreserved = {alphadigit}|{safe}|{extra}
  516. * escape = %{hex}{hex}
  517. * safe = [\$\-\_\.\+]
  518. * extra = [\!\*\'\(\)\,]
  519. * hex = [0-9a-f]
  520. * alpha = [a-z]
  521. * digit = [0-9]
  522. * alphadigit = [a-z0-9]
  523. *
  524. * hsegment = (?:[a-z0-9\$\-\_\.\+\!\*\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*
  525. *
  526. * return preg_match('#^https?://(?:(?:(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)*[a-z](?:[a-z0-9\-]*[a-z0-9])?)|(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}))(?::[0-9]{1,5})?(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)*(?:\?(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)?)?$#i', $item);
  527. *
  528. *
  529. * @param string $url
  530. * @return boolean
  531. */
  532. final public static function valid_url($url, $requireScheme = true)
  533. {
  534. return preg_match('#^(?:https?://)' . ($requireScheme ? '' : '?') . '(?:(?:(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)*[a-z](?:[a-z0-9\-]*[a-z0-9])?)|(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}))(?::[0-9]{1,5})?(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)*(?:\?(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)?$#i', $url) ? true : false;
  535. }
  536. /**
  537. * Check if URL path is valid.
  538. *
  539. * @param string $url
  540. * @return boolean
  541. */
  542. final public static function valid_url_path($url)
  543. {
  544. return preg_match('#^(?:/(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)*(?:\?(?:[a-z0-9\$\-\_\.\+\!\*\\\'\(\)\,\;\:\@\&\=\~]|(?:%[0-9a-f]{2}))*)?$#i', $url) ? true : false;
  545. }
  546. /**
  547. * Check if email address is valid according to HTML5 spec (http://dev.w3.org/html5/spec/states-of-the-type-attribute.html#valid-e-mail-address).
  548. *
  549. * @param string $email
  550. * @return boolean
  551. */
  552. final public static function valid_email($email)
  553. {
  554. return preg_match('/^([[:alnum:]!#$%&\'*+-\/=?^_`{|}.])+@[[:alnum:]-]+(\.[[:alnum:]-]+)*$/', $email) ? true : false;
  555. }
  556. /**
  557. * Extract scheme and host from url.
  558. *
  559. * @param string $url
  560. * @return string
  561. */
  562. final public static function extract_scheme_host_from_url($url)
  563. {
  564. return preg_replace('#^(https?://[^/]+)(.*)$#', '$1', $url);
  565. }
  566. /**
  567. * Extract host from url.
  568. *
  569. * @param string $url
  570. * @return string
  571. */
  572. final public static function extract_host_from_url($url)
  573. {
  574. return preg_replace('#^https?://([^/]+)(.*)$#', '$1', $url);
  575. }
  576. /**
  577. * Remove query string from url.
  578. *
  579. * @param string $url
  580. * @return string
  581. */
  582. final public static function remove_query_from_url($url)
  583. {
  584. return preg_replace('/\?.*$/', '', $url);
  585. }
  586. /**
  587. * Create a nice big random string.
  588. *
  589. * @return string
  590. */
  591. final public static function make_nonce($length = 32)
  592. {
  593. $nonce = '';
  594. do
  595. {
  596. $nonce .= sha1(uniqid(mt_rand(), true));
  597. } while (strlen($nonce) < $length);
  598. return substr($nonce, 0, $length);
  599. }
  600. /**
  601. * Create a version 4 random UUID.
  602. *
  603. * @return string
  604. */
  605. final public static function make_uuid($canonical = true)
  606. {
  607. $format = $canonical ? '%04x%04x-%04x-%04x-%04x-%04x%04x%04x' : '%04x%04x%04x%04x%04x%04x%04x%04x';
  608. return sprintf
  609. (
  610. $format,
  611. mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
  612. mt_rand(0, 0x0fff) | 0x4000,
  613. mt_rand(0, 0x3fff) | 0x8000,
  614. mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
  615. );
  616. }
  617. /**
  618. * Encode a UUID into smaller ASCII string.
  619. *
  620. * @param string $uuid
  621. * @return string
  622. */
  623. final public static function encode_uuid($uuid)
  624. {
  625. return str_replace(array('/','+'), array('_','-'), substr(base64_encode(pack('H*', str_replace('-', '', $uuid))), 0, 22));
  626. }
  627. /**
  628. * Decode an encoded UUID.
  629. *
  630. * @param string $uuid
  631. * @return string
  632. */
  633. final public static function decode_uuid($uuid)
  634. {
  635. $decoded = bin2hex(base64_decode(str_replace(array('_','-'), array('/','+'), $uuid) . '=='));
  636. return substr($decoded, 0, 8) . '-' . substr($decoded, 8, 4) . '-' . substr($decoded, 12, 4) . '-' . substr($decoded, 16, 4) . '-' . substr($decoded, 20);
  637. }
  638. /**
  639. * Clean (erase) all output buffers and turn off output buffering.
  640. */
  641. final public static function ob_end_clean_all()
  642. {
  643. while(@ob_end_clean())
  644. ;
  645. }
  646. /**
  647. * Determine whether a value contains a valid serialized string, and optionally
  648. * return the unserialized result.
  649. *
  650. * @param any $value value to check
  651. * @param and &$decoded unserialized result (only valid if method returns true)
  652. * @return bool
  653. */
  654. final public static function is_serialized($value, &$decoded = NULL)
  655. {
  656. if (!is_string($value))
  657. {
  658. return false;
  659. }
  660. if ($value === 'b:0;')
  661. {
  662. $decoded = false;
  663. return true;
  664. }
  665. return (($decoded = @unserialize($value)) !== false);
  666. }
  667. /**
  668. * Determine whether a value contains a valid JSON-encoded string, and optionally
  669. * return the decoded result.
  670. *
  671. * @param any $value value to check
  672. * @param and &$decoded decoded result (only valid if method returns true)
  673. * @return bool
  674. */
  675. final public static function is_json($value, &$decoded = NULL)
  676. {
  677. if (!is_string($value))
  678. {
  679. return false;
  680. }
  681. return (($decoded = @json_decode($value)) !== NULL) || (json_last_error() === JSON_ERROR_NONE);
  682. }
  683. }
  684. /**
  685. * The SparkInflector class is used to convert strings to/from under_score and CamelCase.
  686. * This class may be used by core classes, application classes and plugs.
  687. * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
  688. *
  689. * @package Spark/Plug
  690. */
  691. // -----------------------------------------------------------------------------
  692. final class SparkInflector
  693. {
  694. /**
  695. * Convert an indentifier to readable text ("example_identifier" -> "Example Identifier").
  696. *
  697. * @param string $s
  698. * @return string
  699. */
  700. final public static function humanize($s)
  701. {
  702. return ucfirst(str_replace('_', ' ', $s));
  703. }
  704. /**
  705. * Convert readable text to identifier ("Example Identifier" -> "example_identifier").
  706. *
  707. * @param string $s
  708. * @return string
  709. */
  710. final public static function dehumanize($s)
  711. {
  712. return strtolower(str_replace(' ', '_', $s));
  713. }
  714. /**
  715. * Convert an indentifier to CamelCase ("example_identifier" -> "ExampleIdentifier").
  716. *
  717. * @param string $s
  718. * @return string
  719. */
  720. final public static function camelize($s)
  721. {
  722. return str_replace(' ', '', ucwords(str_replace('_', ' ', $s)));
  723. }
  724. /**
  725. * Convert an indentifier from CamelCase to underscores ("ExampleIdentifier" -> "example_identifier").
  726. *
  727. * @param string $s
  728. * @return string
  729. */
  730. final public static function decamelize($s)
  731. {
  732. return trim(strtolower(preg_replace(array('/([A-Z])([a-z])/', '/([a-z])([A-Z])/'), array('_$1$2', '$1_$2'), $s)), '_');
  733. }
  734. }
  735. /**
  736. * The SparkConfig class is used to store and retrieve configuration data.
  737. * This class may be used by core classes, application classes and plugs.
  738. * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
  739. *
  740. * @package Spark/Plug
  741. */
  742. // -----------------------------------------------------------------------------
  743. final class SparkConfig
  744. {
  745. private $_config; // key-value dictionary of configuration settings
  746. /**
  747. * Construct a new Config object.
  748. *
  749. * @param array $config: key-value dictionary of configuration values
  750. */
  751. public function __construct(&$config)
  752. {
  753. $this->_config =& $config;
  754. }
  755. /**
  756. * Retrieve a previously stored configuration item.
  757. * If the item is not found, return the provided default value (if any).
  758. *
  759. * @param string $key Name of configuration parameter to retrieve
  760. * @param string $default Default value to return if configuration parameter not found
  761. * @return any Value of configuration parameter if found, or default if not found
  762. */
  763. final public function get($key, $default = NULL)
  764. {
  765. return self::paramGet($this->_config, $key, $default);
  766. }
  767. /**
  768. * Store a new configuration item.
  769. *
  770. * @param string $key Name of configuration parameter to store
  771. * @param string $val Value to store
  772. */
  773. final public function set($key, $val)
  774. {
  775. $this->_config[$key] = $val;
  776. }
  777. /**
  778. * Retrieve a value from a parameter array.
  779. * If the item is not found, return the provided default value (if any).
  780. *
  781. * @param string $key Name of parameter to retrieve
  782. * @param string $default Default value to return if parameter not found
  783. * @return any Value of parameter if found, or default if not found
  784. */
  785. final public static function paramGet($arr, $key, $default = NULL)
  786. {
  787. return isset($arr[$key]) ? $arr[$key] : $default;
  788. }
  789. /**
  790. * Retrieve a (normalized) PHP configuration setting.
  791. *
  792. * @param string $key Name of configuration parameter to retrieve
  793. * @param string $default Default value to return if configuration parameter not found
  794. * @return string Value of configuration parameter if found, or default if not found
  795. */
  796. final public static function php_ini_get($key, $default = NULL)
  797. {
  798. switch (strtolower($val = ini_get($key)))
  799. {
  800. case '':
  801. return $default;
  802. case '0':
  803. case 'off':
  804. case 'false':
  805. case 'no':
  806. case 'none':
  807. return false;
  808. case '1':
  809. case 'on':
  810. case 'true':
  811. case 'yes':
  812. return true;
  813. default:
  814. return $val;
  815. }
  816. }
  817. }
  818. /**
  819. * The SparkObserver class is used to register observable events and their observers.
  820. * Once events and observers have been registered, events may be fired and all observers
  821. * registered for those events will be notified.
  822. * This class may be used by core classes, application classes and plugs.
  823. * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
  824. *
  825. * @package Spark/Plug
  826. */
  827. // -----------------------------------------------------------------------------
  828. final class SparkObserver
  829. {
  830. private $_events; // list of observable events
  831. /**
  832. * Construct a new SparkObserver object.
  833. *
  834. */
  835. public function __construct()
  836. {
  837. $this->_events = array();
  838. }
  839. /**
  840. * Register an observer for one or more events.
  841. *
  842. * An observer can be one of:
  843. *
  844. * a function name
  845. * a lambda function
  846. * an array: (class name, static method name)
  847. * an array: (object, method name)
  848. *
  849. * One or more events can be registered for each observer.
  850. * Events can be specified as either an array of event names or as a
  851. * comma-delimited string containing one or more event names.
  852. *
  853. * @param string $observer Called when registered events fires
  854. * @param string $events Event (or events) to observe
  855. */
  856. final public function observe($observer, $events)
  857. {
  858. if (!is_array($events))
  859. {
  860. $events = explode(',', str_replace(' ', '', $events));
  861. }
  862. // add the observer to the event list for each event it is observing
  863. foreach($events as $event)
  864. {
  865. $this->_events[$event][] = $observer;
  866. }
  867. }
  868. /**
  869. * Notify observers that an event has fired.
  870. * Observers will be passed the event name plus any additional args passed to
  871. * this method.
  872. *
  873. * @param mixed $events Event (or events) to fire
  874. * @param mixed $params1...$paramN variable argument list to pass to observer
  875. */
  876. final public function notify($events)
  877. {
  878. if (!is_array($events))
  879. {
  880. $events = explode(',', str_replace(' ', '', $events));
  881. }
  882. // A "composite event" is of the form "general:more_specific:most_specific",
  883. // a series of component events separated by colons in which each subsequent
  884. // component is more specific than the preceding component. For composite
  885. // events, we send multiple notifications, so listeners may listen on on as
  886. // specific a component as desired. For example, given composite event:
  887. // "site_changed:content:page_saved:edited"
  888. // we would send the event notification to listeners of the folowing four events:
  889. // "site_changed"
  890. // "site_changed:content"
  891. // "site_changed:content:page_saved"
  892. // "site_changed:content:page_saved:edited"
  893. //
  894. // Note that the specific event (in this case, "site_changed:content:page_saved:edited")
  895. // is always passed as the first argument to the listener's callback function.
  896. foreach ($events as $event)
  897. {
  898. $first = true;
  899. foreach (explode(':', $event) as $component)
  900. {
  901. if (!empty($component))
  902. {
  903. if ($first)
  904. {
  905. $notification = $component;
  906. $first = false;
  907. }
  908. else
  909. {
  910. $notification .= ':' . $component;
  911. }
  912. if (isset($this->_events[$notification]))
  913. {
  914. foreach ($this->_events[$notification] as $observer)
  915. {
  916. $args = func_get_args();
  917. $args[0] = $event;
  918. call_user_func_array($observer, $args);
  919. }
  920. }
  921. }
  922. }
  923. }
  924. }
  925. }
  926. /**
  927. * The Spark class is Spark/Plug's object factory. It loads plugs and constructs
  928. * object hierarchies on the fly when instantiating a new object.
  929. * This class may be used by core classes, application classes and plugs.
  930. * This is one of the few *final* core classes in Spark/Plug (not extensible by plugs).
  931. *
  932. * @package Spark/Plug
  933. */
  934. // -----------------------------------------------------------------------------
  935. final class Spark
  936. {
  937. private $_spDir; // Spark/Plug directory
  938. private $_observer; // global observer
  939. private $_plugList; // list if installed plugs indexed by class name
  940. private $_plugMap; // maps class names to list of plugs that implement/extend that class name
  941. private $_plugCacheDir; // directory containing cached plugs
  942. private $_plugDirStack; // directories containing plugs
  943. // --------------------------------------------------------------------------
  944. /**
  945. * Construct a new Spark object, bootstrap the application, and "activate" plugs.
  946. * Only plugs that extend (derive directly from) the SparkPlug class need be specified
  947. * at this time. Additional plugs may be activated at any time by invoking the findPlugs()
  948. * method of this class.
  949. *
  950. * @param array $classes Array of names of plugs to "activate" (make available to the running application)
  951. * @param string|array $searchPaths Optional path(s) to search for plugs
  952. * @param string $cacheDir Optional path to plug cache directory
  953. */
  954. final public function __construct($classes = array(), $searchPaths = NULL, $cacheDir = NULL)
  955. {
  956. $this->_observer = new SparkObserver();
  957. $this->_plugList = array();
  958. $this->_plugMap = array();
  959. $this->_plugCacheDir = NULL;
  960. $this->_spDir = dirname(__FILE__);
  961. if (!empty($cacheDir))
  962. {
  963. $this->setPlugCacheDir($cacheDir);
  964. $this->_observer->observe(array($this, 'flushPlugCache'), 'Spark:cache:request_flush');
  965. }
  966. if (empty($searchPaths))
  967. {
  968. $searchPaths = array(NULL);
  969. }
  970. foreach ($searchPaths as $dir)
  971. {
  972. if (empty($dir))
  973. {
  974. $this->_plugDirStack[] = "{$this->_spDir}/plugs";
  975. }
  976. else
  977. {
  978. if ($dir[0] !== '/')
  979. {
  980. $dir = "{$this->_spDir}/{$dir}";
  981. }
  982. $this->_plugDirStack[] = $dir;
  983. }
  984. }
  985. $this->findPlugs($classes);
  986. // load the SparkPlug class and all its plugs
  987. $this->loadClass('SparkPlug');
  988. }
  989. // --------------------------------------------------------------------------
  990. /**
  991. * Return the global observer object
  992. *
  993. * @return object Observer object
  994. */
  995. final public function observer()
  996. {
  997. return $this->_observer;
  998. }
  999. // --------------------------------------------------------------------------
  1000. /**
  1001. * Initialize the Spark object and load plugs that extend core classes.
  1002. * This method is invoked at the bottom of this file.
  1003. *
  1004. */
  1005. final public function init()
  1006. {
  1007. // load all the core classes and their respective plugs
  1008. $this->loadClass('SparkModel');
  1009. $this->loadClass('SparkView');
  1010. $this->loadClass('SparkController');
  1011. $this->loadClass('SparkApplication');
  1012. }
  1013. // --------------------------------------------------------------------------
  1014. /**
  1015. * Returns the path to the plug cache directory.
  1016. *
  1017. * @return string Path to plug cache directory
  1018. */
  1019. // --------------------------------------------------------------------------
  1020. public function getPlugCacheDir()
  1021. {
  1022. return $this->_plugCacheDir;
  1023. }
  1024. // --------------------------------------------------------------------------
  1025. /**
  1026. * Call this method to change the plug cache directory for subsequent plug cache operations.
  1027. * Used to dynamically change where code is cached based on caller's criteria (eg. host name).
  1028. * Should be called as early as possible. Note that some classes may have already been cached
  1029. * by the time this method is called.
  1030. *
  1031. * @param string $cacheDir Path to new plug cache directory
  1032. *
  1033. * @return string Previous cache directory path
  1034. */
  1035. // --------------------------------------------------------------------------
  1036. public function setPlugCacheDir($cacheDir)
  1037. {
  1038. $oldDir = $this->_plugCacheDir;
  1039. $this->_plugCacheDir = $cacheDir;
  1040. if ($this->_plugCacheDir[0] !== '/')
  1041. {
  1042. $this->_plugCacheDir = "{$this->_spDir}/{$this->_plugCacheDir}";
  1043. }
  1044. return $oldDir;
  1045. }
  1046. // --------------------------------------------------------------------------
  1047. /**
  1048. * Call this method to flush the plug code cache.
  1049. * Not usually called directly, as it can be invoked via the "Spark:cache:request_flush"
  1050. * notification.
  1051. */
  1052. // --------------------------------------------------------------------------
  1053. public function flushPlugCache()
  1054. {
  1055. if (!empty($this->_plugCacheDir))
  1056. {
  1057. $iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->_plugCacheDir), RecursiveIteratorIterator::CHILD_FIRST);
  1058. foreach ($iter as $file)
  1059. {
  1060. $filePath = $file->getPathname();
  1061. if ($file->isDir())
  1062. {
  1063. $fileName = $file->getFilename();
  1064. if ($fileName !== '.' && $fileName !== '..')
  1065. {
  1066. rmdir($filePath);
  1067. }
  1068. }
  1069. else
  1070. {
  1071. unlink($filePath);
  1072. }
  1073. }
  1074. $this->_observer->notify('Spark:cache:flush');
  1075. }
  1076. }
  1077. // --------------------------------------------------------------------------
  1078. /**
  1079. * Call this method to make additional plugs available to be loaded.
  1080. * Subsequent invocations of manufacture() will consider these additional plugs when
  1081. * determining which plugs to load. If $searchPaths parameter is provided, it will
  1082. * be used instead of the main plug directory for the scope of this invocation only.
  1083. * If $callback is provided, it will be called for each plug and passed the plug as a
  1084. * parameter.
  1085. *
  1086. * @param array $classes Array of plug names to make available
  1087. * @param string|array $searchPaths Optional path(s) to search for plugs
  1088. * @param string $callback Callback (optional)
  1089. */
  1090. final public function findPlugs($classes, $searchPaths = NULL, $callback = NULL)
  1091. {
  1092. if (!empty($classes))
  1093. {
  1094. if (empty($searchPaths))
  1095. {
  1096. $searchPaths = $this->_plugDirStack;
  1097. }
  1098. elseif (!is_array($searchPaths))
  1099. {
  1100. $searchPaths = array($searchPaths);
  1101. }
  1102. foreach ($classes as $class)
  1103. {
  1104. // allow nested plug directories
  1105. if ($subPath = (strpos($class, '/') === false) ? '' : '/'.dirName($class))
  1106. {
  1107. $class = basename($class);
  1108. }
  1109. foreach ($searchPaths as $plugDir)
  1110. {
  1111. $dirName = $plugDir . $subPath . '/' . SparkInflector::decamelize($class);
  1112. if (file_exists($dirName))
  1113. {
  1114. break;
  1115. }
  1116. }
  1117. // load plug's manifest
  1118. unset($plug);
  1119. require($dirName . '/manifest.php');
  1120. // add plug to plug map
  1121. if (isset($plug))
  1122. {
  1123. $plugs = $plug['plugs'];
  1124. unset($plug['plugs']);
  1125. foreach ($plugs as $nextPlug)
  1126. {
  1127. if ($callback)
  1128. {
  1129. $nextPlug = array_merge($plug, $nextPlug); // make sure callback has all the plug info
  1130. // Note: We do not use call_user_func() here because it does not allow pass-by-reference
  1131. if (is_string($callback))
  1132. {
  1133. if (!$callback($nextPlug))
  1134. {
  1135. continue;
  1136. }
  1137. }
  1138. elseif (is_array($callback))
  1139. {
  1140. if (!$callback[0]->$callback[1]($nextPlug))
  1141. {
  1142. continue;
  1143. }
  1144. }
  1145. }
  1146. $name = $nextPlug['name'];
  1147. // if an explicit path is provided, use it, otherwise deduce a path from the plug's name
  1148. if ($path = @$nextPlug['path'])
  1149. {
  1150. // explicit path can be absolute, relative to plugs directory, or relative to plug's own directory
  1151. if ($path[0] != '/')
  1152. {
  1153. if (!strncasecmp($path, 'plugs/', 6))
  1154. {
  1155. $path = $plugDir . '/' . substr($path, 6);
  1156. }
  1157. else
  1158. {
  1159. $path = $dirName . '/' . $path;
  1160. }
  1161. }
  1162. }
  1163. else
  1164. {
  1165. $path = $dirName . '/' . SparkInflector::decamelize($name) . '.php';
  1166. }
  1167. // plug can be one of two types:
  1168. //
  1169. // 1. an extension plug that augments an existing class,
  1170. // or
  1171. // 2. a base plug that simply provides some new service
  1172. if ($extends = @$nextPlug['extends'])
  1173. {
  1174. // extension plugs can declare a preferred load order to help avoid conflicts
  1175. $order = isset($nextPlug['order']) ? max(1, min(100, $nextPlug['order'])) : 50;
  1176. }
  1177. else
  1178. {
  1179. $extends = $name;
  1180. $order = 0;
  1181. }
  1182. $plugInfo = array('name'=>$name, 'extends'=>$extends, 'order'=>$order, 'file'=>$path);
  1183. if (!empty($nextPlug['requires']))
  1184. {
  1185. if (is_string($requires = $nextPlug['requires']))
  1186. {
  1187. $requires = array_map('trim', explode(',', $requires));
  1188. }
  1189. $plugInfo['requires'] = $requires;
  1190. }
  1191. if (($order === 0) && isset($nextPlug['base_class']))
  1192. {
  1193. $plugInfo['base_class'] = $nextPlug['base_class'];
  1194. }
  1195. $this->_plugList[$name] =& $plugInfo;
  1196. $this->_plugMap[$extends][$order][] =& $plugInfo;
  1197. unset($plugInfo);
  1198. }
  1199. unset($plug);
  1200. }
  1201. }
  1202. // sort plugs according to load order
  1203. foreach(array_keys($this->_plugMap) as $key)
  1204. {
  1205. ksort($this->_plugMap[$key]);
  1206. }
  1207. }
  1208. }
  1209. // --------------------------------------------------------------------------
  1210. /**
  1211. * Call this method to make an additional plug available to be loaded.
  1212. * Subsequent invocations of manufacture() will consider this plug when
  1213. * determining which plugs to load. Unlike findPlugs(), this method can
  1214. * accept a callback to load the plug's code, rather than a file.
  1215. *
  1216. * @param array $plug:
  1217. * (
  1218. * string 'name' => name of plug to add,
  1219. * string 'requires' => name(s) of class(es) (if any) required by this plug,
  1220. * string 'extends' => name of class (if any) being extended by this plug,
  1221. * int 'order' => load order for this plug
  1222. * string 'file' => location of plug's code file, OR
  1223. * mixed 'callback' => callback to create the plug,
  1224. */
  1225. final public function addPlug($plug)
  1226. {
  1227. if (!empty($plug['requires']))
  1228. {
  1229. if (is_string($requires = $plug['requires']))
  1230. {
  1231. $requires = array_map('trim', explode(',', $requires));
  1232. }
  1233. $plug['requires'] = $requires;
  1234. }
  1235. $plug['order'] = !empty($plug['extends']) ? max(1, min(100, !empty($plug['order']) ? $plug['order'] : 100)) : 0;
  1236. if (empty($plug['extends']))
  1237. {
  1238. $plug['extends'] = $plug['name'];
  1239. }
  1240. $this->_plugList[$plug['name']] =& $plug;
  1241. $this->_plugMap[$plug['extends']][$plug['order']][] =& $plug;
  1242. ksort($this->_plugMap[$plug['extends']]);
  1243. }
  1244. // --------------------------------------------------------------------------
  1245. /**
  1246. * Call this method to retrieve information about an installed plug.
  1247. *
  1248. * @param string $name: => name of plug
  1249. * @return array of plug info
  1250. */
  1251. final public function getPlug($name)
  1252. {
  1253. return @$this->_plugList[$name];
  1254. }
  1255. // --------------------------------------------------------------------------
  1256. /**
  1257. * Call this method to retrieve information about all installed plugs that extend a specified plug.
  1258. *
  1259. * @param string $name: => name of extended plug
  1260. * @return array of plug infos
  1261. */
  1262. final public function getExtensions($name)
  1263. {
  1264. if (empty($this->_plugMap[$name]))
  1265. {
  1266. return NULL;
  1267. }
  1268. $extensions = array();
  1269. foreach ($this->_plugMap[$name] as $plugList)
  1270. {
  1271. $extensions = array_merge($extensions, $plugList);
  1272. }
  1273. return $extensions;
  1274. }
  1275. // --------------------------------------------------------------------------
  1276. /**
  1277. * Dynamically build a class hierarchy and instantiate a new object.
  1278. * This method is used to create any object that you want to be extensible.
  1279. *
  1280. * For example, any object derived from SparkPlug may call:
  1281. *
  1282. * $myObject = $this->factory->manufacture('MyClass');
  1283. *
  1284. * and $myObject will be created and will include the functionality provided by
  1285. * all plugs that extend MyClass.
  1286. *
  1287. * However, if instead you call:
  1288. *
  1289. * $myObject = new MyClass;
  1290. *
  1291. * you will still create a MyClass object, but it will not include any plug functionality.
  1292. *
  1293. * @param string $class Name of class to instantiate
  1294. * @return object
  1295. */
  1296. final public function manufacture($class)
  1297. {
  1298. $this->_observer->notify('Spark:manufacture:' . $class, $class);
  1299. // load the class and all its extension plugs
  1300. $class = $this->loadClass($class);
  1301. // get parameters for constructor (remove the class name from start of args)
  1302. $params = func_get_args();
  1303. array_shift($params);
  1304. // return call_user_func_array(array($class, '__construct'), $params);
  1305. // can't call a constructor via call_user_func_array() because it is not a static method
  1306. switch ($c = count($params))
  1307. {
  1308. case 0:
  1309. return new $class();
  1310. case 1:
  1311. return new $class($params[0]);
  1312. case 2:
  1313. return new $class($params[0], $params[1]);
  1314. case 3:
  1315. return new $class($params[0], $params[1], $params[2]);
  1316. case 4:
  1317. return new $class($params[0], $params[1], $params[2], $params[3]);
  1318. case 5:
  1319. return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
  1320. case 6:
  1321. return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5]);
  1322. case 7:
  1323. return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6]);
  1324. case 8:
  1325. return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7]);
  1326. case 9:
  1327. return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7], $params[8]);
  1328. case 10:
  1329. return new $class($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7], $params[8], $params[9]);
  1330. default:
  1331. throw new SparkException('too many arguments (' . $c . ') passed to constructor (max 10)');
  1332. }
  1333. }
  1334. // --------------------------------------------------------------------------
  1335. /**
  1336. * Call this method to load a class and all its extension plugs.
  1337. * Optionally cache the class to the plug cache directory.
  1338. *
  1339. * @param string $class Name of class to load
  1340. * @param boolean $abstract True if this is an abstract base class
  1341. * @return string Name of instantiated class (alternate implementation may change class name)
  1342. */
  1343. final public function loadClass($class, $abstract = false)
  1344. {
  1345. // no need to load a class more than once
  1346. if (class_exists($class, false))
  1347. {
  1348. return $class;
  1349. }
  1350. // If caching is enabled, check the cache directory for this class.
  1351. if (!empty($this->_plugCacheDir))
  1352. {
  1353. $cacheFile = $this->_plugCacheDir . '/' . $class . '.php';
  1354. if ((@include $cacheFile) !== false)
  1355. {
  1356. return $class;
  1357. }
  1358. }
  1359. // Extensible classes are always declared with an underscore prefix on the name,
  1360. // which allows us to use the class as a base class for plugs, while leaving the
  1361. // ultimate class name (without the underscore) free for dynamic creation once
  1362. // all the plugs have been loaded.
  1363. $baseClass = '_' . $class;
  1364. // if the base class does not exist, it is a plug itself and needs to be loaded
  1365. if (isset($this->_plugMap[$class])) // any plugs want to extend this class?
  1366. {
  1367. foreach ($this->_plugMap[$class] as $plugs)
  1368. {
  1369. foreach ($plugs as $plug)
  1370. {
  1371. $plugName = $plug['name']; // plug's name
  1372. $plugOrder = $plug['order']; // plug's load order
  1373. $plugCode = false; // plug's code
  1374. // take care of dependencies
  1375. foreach ((array)@$plug['requires'] as $dependency)
  1376. {
  1377. $this->loadClass($dependency);
  1378. }
  1379. // To allow easier integration with external class libraries, the manifest may
  1380. // specify alternate class names for base classes.
  1381. if (($plugOrder == 0) && isset($plug['base_class']))
  1382. {
  1383. $baseClass = $plug['base_class'];
  1384. }
  1385. // load the plug's code
  1386. if ($plugFile = @$plug['file'])
  1387. {
  1388. $plugCode = file_get_contents($plugFile);
  1389. }
  1390. elseif ($callback = @$plug['callback'])
  1391. {
  1392. // Note: We do not use call_user_func() here because it does not allow pass-by-reference
  1393. if (is_string($callback))
  1394. {
  1395. $plugCode = $callback($plug);
  1396. }
  1397. elseif (is_array($callback))
  1398. {
  1399. $plugCode = $callback[0]->$callback[1]($plug);
  1400. }
  1401. }
  1402. if ($plugCode !== false)
  1403. {
  1404. $plugCode = preg_replace("/\s*<\?(php)?/", '', $plugCode, 1);
  1405. // Here's the tricky bit... We dynamically rewrite the plug's class definition to build our
  1406. // class hierarchy. (Self-modifying code - oh my!)
  1407. // So, each plug is loaded in turn and serves as the base class for the next plug to be loaded.
  1408. // Performance of this mechanism actually turns out to be quite respectable. The major downside
  1409. // is that opcode cachers will not be able to cache the bytecode of plugs loaded in this manner,
  1410. // (which is generally true of eval'd code). However, this drawback may be mitigated by enabling
  1411. // plug caching, which writes the entire class inheritance chain to a single class file.
  1412. if ($plugOrder != 0) // not a base class?
  1413. {
  1414. $firstClass = isset($plug['first_class']) ? $plug['first_class'] : $plugName;
  1415. $plugCode = preg_replace("/class\s+{$firstClass}\s+extends\s+(?:{$class}|{$baseClass})/", "class {$firstClass} extends {$baseClass}", $plugCode, 1, $count);
  1416. if ($count !== 1) // something fishy here, best abort
  1417. {
  1418. throw new SparkException('plug err: Could not load plug "' . ($plugFile ? $plugFile : $plugName) . '"');
  1419. }
  1420. }
  1421. // create cache file if caching enabled
  1422. if (!isset($cacheFileHandle) && !empty($this->_plugCacheDir))
  1423. {
  1424. if (($cacheFileHandle = @fopen($cacheFile, 'w')) !== false)
  1425. {
  1426. if (fwrite($cacheFileHandle, "<?php\n") === false)
  1427. {
  1428. throw new SparkException('plug err: Could not write plug cache file "' . $cacheFile . '"');
  1429. }
  1430. }
  1431. }
  1432. if (!empty($cacheFileHandle))
  1433. {
  1434. if (fwrite($cacheFileHandle, $plugCode) === false)
  1435. {
  1436. throw new SparkException('plug err: Could not write plug cache file "' . $cacheFile . '"');
  1437. }
  1438. }
  1439. elseif ($this->loadPlug($plugCode) === false)
  1440. {
  1441. throw new SparkException('plug err: Could not load plug "' . ($plugFile ? $plugFile : $plugName) . '"');
  1442. }
  1443. // plug successfully loaded and will serve as the base class for the next to load (if any)
  1444. if ($plugOrder != 0) // not a base class?
  1445. {
  1446. $baseClass = $plugName;
  1447. }
  1448. }
  1449. }
  1450. }
  1451. }
  1452. // Finally, dynamically generate the requested class as a simple wrapper class.
  1453. // This makes the class name a legitimate object in the PHP namespace so it can be referred to by
  1454. // other functions, etc.
  1455. $plugCode = "\nif (!class_exists('{$class}', false)) { " . ($abstract ? 'abstract ' : '') . "class {$class} extends {$baseClass} {} }\n";
  1456. if (!empty($cacheFileHandle))
  1457. {
  1458. if (fwrite($cacheFileHandle, $plugCode) === false)
  1459. {
  1460. throw new SparkException('plug err: Could not write plug cache file "' . $cacheFile . '"');
  1461. }
  1462. fclose($cacheFileHandle);
  1463. include $cacheFile;
  1464. }
  1465. else
  1466. {
  1467. if (!class_exists($baseClass, false))
  1468. {
  1469. throw new SparkException('loader err: Could not load class "' . $baseClass . '"');
  1470. }
  1471. $this->loadPlug($plugCode);
  1472. }
  1473. return $class;
  1474. }
  1475. // --------------------------------------------------------------------------
  1476. /**
  1477. * Private method to isolate the loading of the plug's code.
  1478. *
  1479. * @param string $plugCode Plug code to load into PHP interpreter
  1480. * @return mixed Result of loading the plug's code
  1481. */
  1482. final private function loadPlug($plugCode)
  1483. {
  1484. return eval($plugCode);
  1485. }
  1486. }
  1487. /**
  1488. * The _SparkPlug (SparkPlug) class is Spark/Plug's universal base object class. Any
  1489. * class that wants access to the application object and/or the Spark factory object
  1490. * should derive from this class or one of its descendants. This class is not meant to
  1491. * be instantiated directly. When specifying this class as a base class in your class
  1492. * definition, be sure to extend SparkPlug (not _SparkPlug). Otherwise, your derived class
  1493. * will not inherit the capabilities of any plugs that extend this class.
  1494. * This class may be used by core classes, application classes and plugs.
  1495. *
  1496. * @package Spark/Plug
  1497. */
  1498. // -----------------------------------------------------------------------------
  1499. abstract class _SparkPlug
  1500. {
  1501. public $app; // convenience reference to global application object
  1502. public $config; // convenience reference to application configuration store
  1503. public $factory; // convenience reference back global object factory
  1504. public $observer; // convenience reference to global observer
  1505. // --------------------------------------------------------------------------
  1506. /**
  1507. * Construct a new SparkPlug object. Make key application objects available to all
  1508. * derived classes.
  1509. */
  1510. public function __construct()
  1511. {
  1512. $this->app = SparkApplication::instance();
  1513. $this->config = $this->app->config();
  1514. $this->factory = $this->app->factory();
  1515. $this->observer = $this->factory->observer();
  1516. }
  1517. /**
  1518. * Create a new model object. Calls through to the application's newModel() method.
  1519. *
  1520. * @param string $name Name of model class to instatiate
  1521. * @return object Model object
  1522. */
  1523. public function newModel($name, $params = NULL)
  1524. {
  1525. return $this->app->newModel($name, $params);
  1526. }
  1527. /**
  1528. * Create a full url to the specified (static) path. Calls through to the application's urlToStatic() method.
  1529. *
  1530. * @param string $path Path to which to create full URL
  1531. * @param bool $withHost Whether to include host in URL
  1532. * @param bool $withScheme Whether to include scheme in URL
  1533. * @param bool $secure Whether to generate a secure URL (true), a non-secure URL (false), or default (NULL)
  1534. * @return string URL
  1535. */
  1536. public function urlToStatic($path, $withHost = false, $withScheme = true, $secure = NULL)
  1537. {
  1538. return $this->app->urlToStatic($path, $withHost, $withScheme, $secure);
  1539. }
  1540. /**
  1541. * Create a full url to the specified (dynamic) path. Calls through to the application's urlTo() method.
  1542. *
  1543. * @param string $path Path to which to create full URL
  1544. * @param bool $withHost Whether to include host in URL
  1545. * @param bool $withScheme Whether to include scheme in URL
  1546. * @param bool $secure Whether to generate a secure URL (true), a non-secure URL (false), or default (NULL)
  1547. * @return string URL
  1548. */
  1549. public function urlTo($path, $withHost = false, $withScheme = true, $secure = NULL)
  1550. {
  1551. return $this->app->urlTo($path, $withHost, $withScheme, $secure);
  1552. }
  1553. /**
  1554. * Create a full url to the specified (static) secure path. Calls through to the application's urlToStaticSecure() method.
  1555. *
  1556. * @param string $path Path to which to create secure URL
  1557. * @return string URL
  1558. */
  1559. public function urlToStaticSecure($path)
  1560. {
  1561. return $this->app->urlToStaticSecure($path);
  1562. }
  1563. /**
  1564. * Create a full url to the specified (dynamic) secure path. Calls through to the application's urlToSecure() method.
  1565. *
  1566. * @param string $path Path to which to create secure URL
  1567. * @return string URL
  1568. */
  1569. public function urlToSecure($path)
  1570. {
  1571. return $this->app->urlToSecure($path);
  1572. }
  1573. /**
  1574. * Create a redirect to the specified (static) path. Calls through to the application's urlToStatic() method.
  1575. *
  1576. * @param string $path Path to which to create full URL to redirect to
  1577. * @param bool $secure Whether to redirect to a secure page (true), a non-secure page (false), or default (NULL)
  1578. */
  1579. public function redirectStatic($path, $secure = NULL)
  1580. {
  1581. $this->redirectAbsolute($this->urlToStatic($path, true, true, $secure));
  1582. }
  1583. /**
  1584. * Create a redirect to the specified path. Calls through to the application's urlTo() method.
  1585. *
  1586. * @param string $path Path to which to create full URL to redirect to
  1587. * @param bool $secure Whether to redirect to a secure page (true), a non-secure page (false), or default (NULL)
  1588. */
  1589. public function redirect($path, $secure = NULL)
  1590. {
  1591. $this->redirectAbsolute($this->urlTo($path, true, true, $secure));
  1592. }
  1593. /**
  1594. * Create a redirect to the specified absolute url.
  1595. *
  1596. * @param string $url Absolute URL to redirect to
  1597. */
  1598. public function redirectAbsolute($url)
  1599. {
  1600. $this->observer->notify('SparkPlug:redirect:before', $url);
  1601. header('Location: ' . $url);
  1602. exit;
  1603. }
  1604. }
  1605. // -----------------------------------------------------------------------------
  1606. /**
  1607. * Now we have defined the core classes that allow us to create the global factory object.
  1608. * We must instantiate the global factory object now because doing so will dynamically create
  1609. * the SparkPlug class definition, which the following class declarations depend on.
  1610. * The file that includes this file my declare a global array of plugs to load into the application.
  1611. * We pass that array to the constructor.
  1612. */
  1613. $spark = new Spark(isset($plugs) ? $plugs : NULL, isset($plug_search_paths) ? $plug_search_paths : NULL, isset($plug_cache_dir) ? $plug_cache_dir : NULL);
  1614. unset($plugs);
  1615. unset($plug_search_paths);
  1616. unset($plug_cache_dir);
  1617. // -----------------------------------------------------------------------------
  1618. /**
  1619. * The _SparkModel (SparkModel) class is Spark/Plug's model base class.
  1620. * While Spark/Plug is a model-view-controller framework, use of models is optional.
  1621. * If your application uses models, they should be derived from this class or one of its
  1622. * descendants. When specifying this class as a base class in your class definition, be sure
  1623. * to extend SparkModel (not _SparkModel). Otherwise, your derived class will not inherit the
  1624. * capabilities of any plugs that extend this class.
  1625. *
  1626. * To create a new model, invoke the newModel() method on any object derived from SparkPlug:
  1627. *
  1628. * $model = $this->newModel('MyModel');
  1629. *
  1630. * @package Spark/Plug
  1631. */
  1632. // -----------------------------------------------------------------------------
  1633. abstract class _SparkModel extends SparkPlug
  1634. {
  1635. /**
  1636. * Construct a new SparkModel object.
  1637. * Not much to see here. Move along...
  1638. */
  1639. public function __construct()
  1640. {
  1641. parent::__construct();
  1642. }
  1643. //---------------------------------------------------------------------------
  1644. /**
  1645. * Convenience function to return the current date/time UTC in a database-friendly format.
  1646. *
  1647. * @return string Current date/time UTC
  1648. */
  1649. public static function now($iso_8601 = false)
  1650. {
  1651. return $iso_8601 ? (gmdate('Y-m-d') . 'T' . gmdate('H:i:s') . 'Z') : gmdate('Y-m-d H:i:s');
  1652. }
  1653. //---------------------------------------------------------------------------
  1654. /**
  1655. * Convenience function to return an "empty" date in a database-friendly format.
  1656. *
  1657. * @return string Empty date
  1658. */
  1659. public static function never()
  1660. {
  1661. return '0000-00-00';
  1662. }
  1663. //---------------------------------------------------------------------------
  1664. /**
  1665. * Convenience method to load a helper file. Calls through to app's helper method.
  1666. *
  1667. * @param string $helperList Comma-delimited list of names of helpers to load
  1668. */
  1669. protected function helper($helperList)
  1670. {
  1671. return $this->app->helper($helperList);
  1672. }
  1673. }
  1674. /**
  1675. * The _SparkView (SparkView) class is Spark/Plug's view base object class.
  1676. * This class is not meant to be instantiated directly. When specifying this
  1677. * class as a base class in your class definition, be sure to extend SparkView (not _SparkView).
  1678. * Otherwise, your derived class will not inherit the capabilities of any plugs that extend this class.
  1679. * This class will not generally be used by Spark/Plug applications directly, as Spark/Plug automatically
  1680. * creates a single view class on initialization, which is reused for all view rendering.
  1681. *
  1682. * @package Spark/Plug
  1683. */
  1684. // -----------------------------------------------------------------------------
  1685. abstract class _SparkView extends SparkPlug
  1686. {
  1687. private $_viewDirStack; // list of directories where view files can be found
  1688. private $_viewStack; // keeps track of nested view contexts
  1689. private $_viewDefault; // name of default view (rendered if none is explicitly specified)
  1690. private $_viewNameExtension; // optional view filename extension
  1691. private $_helperDirStack; // list of directories where helper files can be found
  1692. private $_helperNameExtension; // optional helper filename extension
  1693. private $_loadedHelpers; // track helpers so we only load them once
  1694. /**
  1695. * Construct a new SparkView object.
  1696. * Not much to see here. Move along...
  1697. */
  1698. public function __construct($viewDir, $viewNameExtension = '', $helperDir = NULL, $helperNameExtension = '')
  1699. {
  1700. parent::__construct();
  1701. $this->_viewDirStack = (array)$viewDir;
  1702. $this->_viewStack = array();
  1703. $this->_viewDefault = '';
  1704. $this->_viewNameExtension = $viewNameExtension;
  1705. $this->_helperDirStack = (array)$helperDir;
  1706. $this->_helperNameExtension = $helperNameExtension;
  1707. $this->_loadedHelpers = NULL;
  1708. }
  1709. // --------------------------------------------------------------------------
  1710. /**
  1711. * Set name of default view (rendered if none specified).
  1712. *
  1713. * @param string $viewDefault Name of default view
  1714. */
  1715. final public function setDefault($viewDefault)
  1716. {
  1717. $this->_viewDefault = $viewDefault;
  1718. }
  1719. // --------------------------------------------------------------------------
  1720. /**
  1721. * Push a view directory onto the stack.
  1722. *
  1723. * @param string $dir Path to view directory
  1724. */
  1725. final public function pushViewDir($dir)
  1726. {
  1727. $this->_viewDirStack[] = $dir;
  1728. }
  1729. // --------------------------------------------------------------------------
  1730. /**
  1731. * Pop the view directory stack.
  1732. *
  1733. */
  1734. final public function popViewDir()
  1735. {
  1736. if (count($this->_viewDirStack) > 1)
  1737. {
  1738. return array_pop($this->_viewDirStack);
  1739. }
  1740. return $this->_viewDirStack[0];
  1741. }
  1742. // --------------------------------------------------------------------------
  1743. /**
  1744. * Push a helper directory onto the stack.
  1745. *
  1746. * @param string $dir Path to helper directory
  1747. */
  1748. final public function pushHelperDir($dir)
  1749. {
  1750. $this->_helperDirStack[] = $dir;
  1751. }
  1752. // --------------------------------------------------------------------------
  1753. /**
  1754. * Pop the helper directory stack.
  1755. *
  1756. */
  1757. final public function popHelperDir()
  1758. {
  1759. if (count($this->_helperDirStack) > 1)
  1760. {
  1761. return array_pop($this->_helperDirStack);
  1762. }
  1763. return $this->_helperDirStack[0];
  1764. }
  1765. // --------------------------------------------------------------------------
  1766. /**
  1767. * Render a view file.
  1768. * Accepts 0, 1, 2 or 3 parameters. Parameters may appear in any order and are as follows:
  1769. *
  1770. * @param string $viewList Comma-delimited list of names of views to render (first found will be rendered)
  1771. * @param array $vars Array of variables to make available to view (extract into view as locals)
  1772. * @param bool $returnBuffer true if rendered result should be returned instead of buffered for display
  1773. * @return string Result of render (if $returnBuffer is true)
  1774. */
  1775. final public function render()
  1776. {
  1777. $viewList = NULL;
  1778. $vars = array();
  1779. $returnBuffer = false;
  1780. // decode the argument list - all are optional and they can appear in any order
  1781. foreach (func_get_args() as $arg)
  1782. {
  1783. if (is_string($arg))
  1784. {
  1785. $viewList = array_map('trim', explode(',', $arg));
  1786. }
  1787. elseif (is_array($arg))
  1788. {
  1789. $vars = $arg;
  1790. }
  1791. elseif (is_bool($arg))
  1792. {
  1793. $returnBuffer = $arg;
  1794. }
  1795. }
  1796. $nested = !empty($this->_viewStack);
  1797. // use the default view if no view specified
  1798. if (!$nested && empty($viewList))
  1799. {
  1800. $viewList = array($this->_viewDefault);
  1801. }
  1802. // start buffering if necessary
  1803. if ($returnBuffer || !$nested)
  1804. {
  1805. ob_start();
  1806. }
  1807. // nested views inherit the variables of the containing view(s)
  1808. if ($nested)
  1809. {
  1810. $vars = array_merge(end($this->_viewStack), $vars);
  1811. }
  1812. // push our context on a stack, for use by views nested within this one
  1813. $this->_viewStack[] = $vars;
  1814. // load the view
  1815. $foundIt = false;
  1816. $viewNames = array_map(array('SparkInflector', 'decamelize'), $viewList);
  1817. try
  1818. {
  1819. for ($iViewDir = count($this->_viewDirStack) - 1; $iViewDir >= 0; --$iViewDir)
  1820. {
  1821. foreach ($viewList as $iView => $view)
  1822. {
  1823. $viewFileName = $viewNames[$iView] . $this->_viewNameExtension . '.php';
  1824. if (file_exists($viewPath = "{$this->_viewDirStack[$iViewDir]}/{$viewFileName}"))
  1825. {
  1826. // send pre_render notification, allowing recipient to alter the contents of the vars array
  1827. $this->observer->notify('SparkView:render:before:' . $view, $view, (object)array('vars'=>&$vars));
  1828. $this->loadView($viewPath, $vars);
  1829. $foundIt = true;
  1830. // send post_render notification
  1831. $this->observer->notify('SparkView:render:after:' . $view, $view);
  1832. break 2;
  1833. }
  1834. }
  1835. }
  1836. if (!$foundIt)
  1837. {
  1838. throw new SparkException('render err: Could not load view "' . $viewFileName . '"');
  1839. }
  1840. }
  1841. catch (Exception $e)
  1842. {
  1843. array_pop($this->_viewStack);
  1844. throw $e;
  1845. }
  1846. array_pop($this->_viewStack);
  1847. // return the rendered result if this is the outer-most view (or if caller requested it)
  1848. if ($returnBuffer || !$nested)
  1849. {
  1850. return ob_get_clean();
  1851. }
  1852. }
  1853. // --------------------------------------------------------------------------
  1854. /**
  1855. * Load a view helper.
  1856. *
  1857. * @param string $helperList Array or comma-delimited string listing of names of helpers to load
  1858. */
  1859. final public function helper($helperList)
  1860. {
  1861. $foundIt = true;
  1862. if (is_string($helperList))
  1863. {
  1864. $helperList = array_map('trim', explode(',', $helperList));
  1865. }
  1866. $helperList = array_map(array('SparkInflector', 'decamelize'), $helperList);
  1867. foreach ($helperList as $helper)
  1868. {
  1869. if (!empty($this->_loadedHelpers[$helper]))
  1870. {
  1871. continue;
  1872. }
  1873. for ($iHelperDir = count($this->_helperDirStack) - 1; $iHelperDir >= 0; --$iHelperDir)
  1874. {
  1875. $helperFileName = $helper . $this->_helperNameExtension . '.php';
  1876. if (file_exists($helperPath = "{$this->_helperDirStack[$iHelperDir]}/{$helperFileName}"))
  1877. {
  1878. require($helperPath);
  1879. $this->_loadedHelpers[$helper] = true;
  1880. continue 2;
  1881. }
  1882. }
  1883. $foundIt = false;
  1884. break;
  1885. }
  1886. if (!$foundIt)
  1887. {
  1888. throw new SparkException('loader err: Could not load helper "' . $helperFileName . '"');
  1889. }
  1890. }
  1891. // --------------------------------------------------------------------------
  1892. /**
  1893. * Escape item for inclusion in HTML output.
  1894. *
  1895. * @param string $item Text to escape
  1896. * @param boolean $encodeAll Whether to encode all characters to entity equivalents
  1897. * @param string $charset Character set of text
  1898. * @return string Escaped item
  1899. */
  1900. final public static function escape_html($item, $encodeAll = false, $charset = 'UTF-8')
  1901. {
  1902. return $encodeAll
  1903. ? htmlentities($item, ENT_QUOTES, $charset)
  1904. : htmlspecialchars($item, ENT_QUOTES, $charset);
  1905. }
  1906. // deprecated alias for escape_html
  1907. final public static function escape($item, $encodeAll = false, $charset = 'UTF-8')
  1908. {
  1909. return self::escape_html($item, $encodeAll, $charset);
  1910. }
  1911. // --------------------------------------------------------------------------
  1912. /**
  1913. * Reverse HTML escaping.
  1914. *
  1915. * @param string $item Text to unescape
  1916. * @param boolean $decodeAll Whether to decode all entities or only special entities
  1917. * @param string $charset Character set of text
  1918. * @return string Unescaped item
  1919. */
  1920. final public static function unescape_html($item, $decodeAll = false, $charset = 'UTF-8')
  1921. {
  1922. return $decodeAll
  1923. ? html_entity_decode($item, ENT_QUOTES, $charset)
  1924. : htmlspecialchars_decode($item, ENT_QUOTES);
  1925. }
  1926. // --------------------------------------------------------------------------
  1927. /**
  1928. * Escape item for inclusion in XML output.
  1929. *
  1930. * @param string $item Text to escape
  1931. * @param string $charset Character set of text
  1932. * @return string Escaped item
  1933. */
  1934. final public static function escape_xml($item, $charset = 'UTF-8')
  1935. {
  1936. return htmlspecialchars($item, ENT_NOQUOTES, $charset);
  1937. }
  1938. // --------------------------------------------------------------------------
  1939. /**
  1940. * Reverse XML escaping.
  1941. *
  1942. * @param string $item Text to unescape
  1943. * @param string $charset Character set of text
  1944. * @return string Unescaped item
  1945. */
  1946. final public static function unescape_xml($item, $charset = 'UTF-8')
  1947. {
  1948. return htmlspecialchars_decode($item, ENT_QUOTES);
  1949. }
  1950. // --------------------------------------------------------------------------
  1951. /**
  1952. * Escape item for inclusion in query string or form-encoded output.
  1953. *
  1954. * @param string $item Text to escape
  1955. * @param string $charset Character set of text (ignored)
  1956. * @return string Escaped item
  1957. */
  1958. final public static function escape_uri($item, $charset = 'UTF-8')
  1959. {
  1960. return urlencode($item);
  1961. }
  1962. // --------------------------------------------------------------------------
  1963. /**
  1964. * Reverse URL escaping.
  1965. *
  1966. * @param string $item Text to escape
  1967. * @param string $charset Character set of text (ignored)
  1968. * @return string Unescaped item
  1969. */
  1970. final public static function unescape_uri($item, $charset = 'UTF-8')
  1971. {
  1972. return urldecode($item);
  1973. }
  1974. // --------------------------------------------------------------------------
  1975. /**
  1976. * Load a view file.
  1977. *
  1978. * Note the funky use of func_get_args() to retrieve the filename. This is due to
  1979. * the fact that the extract operation can potentially overwrite the $file param
  1980. * with a local var named 'file'.
  1981. *
  1982. * @param string $file Path to view file
  1983. * @param array $vars Array of variables to make available to view (extract into view as locals)
  1984. */
  1985. final private static function array_first($array) { return $array[0]; }
  1986. final private function loadView($file, $vars)
  1987. {
  1988. extract($vars, EXTR_OVERWRITE);
  1989. unset($vars);
  1990. require(self::array_first(func_get_args()));
  1991. }
  1992. }
  1993. /**
  1994. * The _SparkController (SparkController) class is Spark/Plug's controller base class.
  1995. * Your application's controllers should be derived from this class or one of its
  1996. * descendants. When specifying this class as a base class in your class definition, be sure
  1997. * to extend SparkController (not _SparkController). Otherwise, your derived class will not
  1998. * inherit the capabilities of any plugs that extend this class.
  1999. *
  2000. * Your application will not generally create controller objecct directly, as Spark/Plug
  2001. * automatically instantiates a controller object based on the current URL.
  2002. *
  2003. * @package Spark/Plug
  2004. */
  2005. // -----------------------------------------------------------------------------
  2006. abstract class _SparkController extends SparkPlug
  2007. {
  2008. /**
  2009. * Construct a new SparkController object.
  2010. * Not much to see here. Move along...
  2011. *
  2012. * @param object $app Reference to the application that instantiated us.
  2013. */
  2014. public function __construct($app)
  2015. {
  2016. parent::_