PageRenderTime 97ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/fastpage/lib/fastpage.php

https://bitbucket.org/ideamans/fastpage
PHP | 3412 lines | 1755 code | 443 blank | 1214 comment | 435 complexity | 3843cee3f4221c92b69b0ff3ddc27fc0 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * FastPage
  4. *
  5. * Licensed under the MIT License
  6. *
  7. * @copyright Copyright 2011, ideaman's Inc. (http://www.ideamans.com)
  8. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  9. */
  10. /**
  11. * Version.
  12. */
  13. define( 'FASTPAGE_VERSION', '1.0.0 RC4' );
  14. /**
  15. * Error constant for profiling.
  16. */
  17. define( 'E_FP_PROFILE', 0x00 );
  18. /**
  19. * A class gathering utility static functions.
  20. *
  21. * @package FastPage
  22. * @author Kunihiko Miyanaga@ideaman's Inc.
  23. */
  24. class FastPage_Util {
  25. /**
  26. * Time units.
  27. */
  28. const minute = 60;
  29. const hour = 3600;
  30. const day = 86400;
  31. const week = 604800;
  32. const month = 2592000;
  33. const year = 31536000;
  34. /**
  35. * Aliases of server variables.
  36. */
  37. public static $server_aliases = array(
  38. 'HTTP_HOST' => array( 'HTTP_HOST', 'SERVER_NAME' ),
  39. 'SCRIPT_NAME' => array( 'SCRIPT_NAME', 'PHP_SELF' ),
  40. );
  41. /**
  42. * Returns millisecond time.
  43. *
  44. * @return float Current epoc time in millisecond.
  45. */
  46. public static function millisec() {
  47. return function_exists('microtime')? microtime(true) * 1000.0: time() / 1000.0;
  48. }
  49. /**
  50. * Returns memory_usege safety.
  51. *
  52. * @return integer Memory usage.
  53. */
  54. public static function memory_usage() {
  55. return function_exists('memory_get_usage')? memory_get_usage(): 0;
  56. }
  57. /**
  58. * Splits path like url or directory, and resolves periods.
  59. *
  60. * @param string $path A target path as string.
  61. * @param string $separator Separator of target such as / or DIRECTORY_SEPARATOR. The default is '/'.
  62. * @return array Partials of the path as an array.
  63. */
  64. public static function split_path( $path, $separator = '/' ) {
  65. // Split path with separator.
  66. if ( strlen($separator) == 1 ) {
  67. $paths = explode( $separator, $path );
  68. } else {
  69. $regex = '![' . preg_quote($separator) . ']!';
  70. $paths = preg_split( $regex, $path );
  71. }
  72. $result = array( array_shift($paths) );
  73. // Join path partials.
  74. $count = count($paths);
  75. for ( $i = 0; $i < $count; $i++ ) {
  76. $partial = $paths[$i];
  77. if ( $partial == '..' && $i != 0 ) {
  78. // Back to the parent.
  79. array_pop( $result );
  80. } else if ( !$partial && $i > 0 && $i < $count - 1 ) {
  81. // Skip a blank not the first or the last.
  82. } else if ( $partial == '.' ) {
  83. // Skip as same directory.
  84. } else {
  85. // Push to the last.
  86. $result []= $partial;
  87. }
  88. }
  89. return $result;
  90. }
  91. /**
  92. * Catenate some path partials and normalize it.
  93. *
  94. * @param variable $PARTIALS Partials of path as variable numbers arguments.
  95. * @return string Catenated path.
  96. */
  97. public static function cat_path( ) {
  98. if ( func_num_args() < 1 ) return '';
  99. // Expand args.
  100. $args = func_get_args();
  101. $paths = array();
  102. foreach ( $args as $arg ) {
  103. if ( is_array($arg) ) {
  104. $paths = array_merge( $paths, $arg );
  105. } else if ( is_string($arg) ) {
  106. $paths []= $arg;
  107. }
  108. }
  109. $path = join( DIRECTORY_SEPARATOR, $paths );
  110. // Path maybe contains mixed separators in windows.
  111. $separator = DIRECTORY_SEPARATOR;
  112. if ( $separator != '/' ) $separator .= '/';
  113. $partials = self::split_path( $path, $separator );
  114. return join( DIRECTORY_SEPARATOR, $partials );
  115. }
  116. /**
  117. * Ensure include_path.
  118. *
  119. * @param string $path Path to set include path.
  120. */
  121. public static function ensure_include_path( $path ) {
  122. // Check if include_path has the path.
  123. $include_path = ini_get('include_path');
  124. $include_paths = explode( PATH_SEPARATOR, $include_path );
  125. // Append if the path does not exists.
  126. if ( false === array_search( $path, $include_paths ) ) {
  127. $include_path .= PATH_SEPARATOR . $path;
  128. ini_set( 'include_path', $include_path );
  129. }
  130. }
  131. /**
  132. * Enclose string with quotes.
  133. *
  134. * @param string $value A string to quote.
  135. * @param string $quotation Optional. Character of quotation. Default is double quotation.
  136. */
  137. public static function quote( $value, $quotation = '"' ) {
  138. if ( !isset($value) ) $value = '';
  139. return $quotation . $value . $quotation;
  140. }
  141. /**
  142. * Gets the Etag(entity tag) of a file formated like Apache.
  143. *
  144. * @param string $path A path of file to get Etag.
  145. * @param boolean $quote If true, double-quote the result.
  146. * @return string The Etag of the file.
  147. */
  148. public static function file_etag( $path, $quote = true ) {
  149. if ( file_exists($path) && false !== ( $stats = stat($path) ) ) {
  150. $etag = sprintf('%x-%x-%x', $stats['ino'], $stats['size'], $stats['mtime'] * 1000000 );
  151. if ( $quote ) $etag = self::quote( $etag );
  152. return $etag;
  153. }
  154. return null;
  155. }
  156. /**
  157. * Strips single and double quotes at the head and at the last.
  158. *
  159. * @param string $value A string to remove quotation.
  160. * @return string The string removed quotation.
  161. */
  162. public static function strip_quotes( $value ) {
  163. // FIXME: Shoud return blank string ''?
  164. if ( !$value ) return $value;
  165. // Remove the first quote.
  166. $first = substr( $value, 0, 1 );
  167. if ( $first == '"' || $first == "'" ) {
  168. $value = substr( $value, 1 );
  169. } else {
  170. $first == '';
  171. }
  172. // Remove the last quote.
  173. $last = substr( $value, -1, 1 );
  174. if ( $last == $first || ( !$first && ( $last == '"' || $last == "'" ) ) ) {
  175. $value = substr( $value, 0, -1 );
  176. }
  177. return $value;
  178. }
  179. /**
  180. * Strip URL parameters.
  181. *
  182. * @param string $url A URL to remove parameters.
  183. * @return string The URL removed parameters.
  184. */
  185. public static function strip_url_params( $url ) {
  186. if ( !$url ) return $url;
  187. $url = preg_replace( '![\?#].*!', '', $url );
  188. return $url;
  189. }
  190. /**
  191. * Get server variable by a key or some keys.
  192. *
  193. * @param mixed $name A key string or an array of keys.
  194. * @param mixed $default The default value if server variable not found.
  195. * @return mixed The server value. Return null if not found.
  196. */
  197. public static function server( $name, $default = null ) {
  198. // Search alias.
  199. if ( is_string($name) && isset(self::$server_aliases[$name]) ) {
  200. $name = self::$server_aliases[$name];
  201. }
  202. if ( is_array($name) ) {
  203. foreach ( $name as $var ) {
  204. if ( isset($_SERVER[$var]) && $_SERVER[$var] ) {
  205. return $_SERVER[$var];
  206. }
  207. }
  208. } else if ( isset($_SERVER[$name]) && $_SERVER[$name] ) {
  209. return $_SERVER[$name];
  210. }
  211. return $default;
  212. }
  213. /**
  214. * Match IP address with patterns.
  215. *
  216. * @param mixed $patterns IP address patterns as a string or an array.
  217. * @param string $ip IP address to match.
  218. * @param boolean $include_self Add IP address of myself.
  219. * @return boolean Matched or not.
  220. */
  221. public static function match_ips( $patterns, $ip, $include_self = true ) {
  222. // Normalize patterns to an array.
  223. if ( !is_array($patterns) ) {
  224. $patterns = preg_split( '!\\s*,\\s*!', $patterns );
  225. }
  226. // Add myself to patterns.
  227. if ( $include_self ) {
  228. $patterns []= '127.0.0.1';
  229. if ( $server_addr = self::server('SERVER_ADDR') ) {
  230. $patterns []= $server_addr;
  231. }
  232. }
  233. // Check each pattern.
  234. foreach ( $patterns as $p ) {
  235. // Prefix matching.
  236. if ( !$p ) continue;
  237. if ( $p == substr( $ip, 0, strlen($p) ) ) {
  238. return true;
  239. }
  240. }
  241. return false;
  242. }
  243. /**
  244. * Unserialize as object of a class.
  245. *
  246. * @param string $serialized Serialized string.
  247. * @param string $class Class required.
  248. * @return mixed The unserialized value.
  249. */
  250. public static function unserialize_as( &$value, $class = null ) {
  251. if ( !$value ) return null;
  252. $result = @unserialize($value);
  253. if ( $class ) {
  254. if ( !is_a( $result, $class ) ) {
  255. return null;
  256. }
  257. }
  258. return $result;
  259. }
  260. }
  261. /**
  262. * Exception class.
  263. *
  264. * Currently almost empty and not to be used.
  265. *
  266. * @package FastPage
  267. * @author Kunihiko Miyanaga
  268. */
  269. class FastPage_Exception extends Exception { }
  270. /**
  271. * Base object class supports basic accessors.
  272. *
  273. * Properties stored in $_vars member.
  274. *
  275. * @package FastPage
  276. * @author Kunihiko Miyanaga@ideaman's Inc.
  277. */
  278. class FastPage_Object {
  279. /**
  280. * Members store.
  281. */
  282. protected $_vars = array();
  283. /**
  284. * Constructor.
  285. *
  286. * @param array $defaults Default values of object.
  287. */
  288. public function __construct( $defaults = null ) {
  289. if ( isset($defaults) && is_array($defaults) ) {
  290. $this->_vars = $defaults;
  291. }
  292. }
  293. /**
  294. * Property getter.
  295. *
  296. * Implimentatin of getter. Get named property from $_vars member.
  297. *
  298. * @param string $name Name of the property.
  299. * @return mixed The property value. If not defined, return null.
  300. */
  301. public function __get( $name ) {
  302. if ( isset($this->_vars[$name]) ) {
  303. return $this->_vars[$name];
  304. }
  305. return null;
  306. }
  307. /**
  308. * Property setter.
  309. *
  310. * Implimentation of setter. Set named property to $_vars member. Not to be used directory.
  311. *
  312. * @param string $name Name of the property.
  313. * @param mixed $value Value of the property.
  314. * @return mixed The value echoed back.
  315. */
  316. public function __set( $name, $value ) {
  317. $this->_vars[$name] = $value;
  318. return $value;
  319. }
  320. /**
  321. * Dictionary accessor.
  322. *
  323. * Implimentation of dictionary type accessor.
  324. * Get and set dictionary type named property.
  325. * Usually to be wrapped as other function.
  326. *
  327. * @param string $namespace Name of the dictionary.
  328. * @param string $name Key of the dictionary value.
  329. * @param mixed $value The dictionary value to set. If you set null, this function behaves as a getter. Default is null.
  330. * @return mixed The dictionary value. No match found, return null.
  331. */
  332. public function __dictionary( $namespace, $name, $value = null ) {
  333. if ( isset($value) ) {
  334. // As a setter.
  335. $this->_vars[$namespace][$name] = $value;
  336. return $value;
  337. }
  338. // As a getter.
  339. if ( isset($this->_vars[$namespace][$name]) ) {
  340. return $this->_vars[$namespace][$name];
  341. }
  342. return null;
  343. }
  344. /**
  345. * Name keys of all properties.
  346. *
  347. * @return Array Names of all properties as an array.
  348. */
  349. public function keys() {
  350. return array_keys( $this->_vars );
  351. }
  352. /**
  353. * Get all properties as an array.
  354. *
  355. * @return Array Properties of the object.
  356. */
  357. public function to_array() {
  358. return $_vars;
  359. }
  360. /**
  361. * Sets values from an array or another object.
  362. *
  363. * @param mixed $values An array of properties or an another obejct.
  364. * @param boolean $override To be overridden each properties already exists. Default is true.
  365. */
  366. public function merge_values( $values, $override = true ) {
  367. if ( is_array($values) ) {
  368. foreach ( $values as $name => $value ) {
  369. if ( $override || !isset($this->_vars[$name]) ) {
  370. $this->_vars[$name] = $value;
  371. }
  372. }
  373. } else if ( is_object($values) && is_a( $values, 'FastPage_Object' ) ) {
  374. $this->merge_values( $values->to_array(), $override );
  375. }
  376. }
  377. }
  378. /**
  379. * Log record.
  380. *
  381. * Currently, a simple class of basic object.
  382. *
  383. * @package FastPage
  384. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  385. * @var integer $timestamp Timestamp in milli second.
  386. * @var integer $memory_usage Memory usage.
  387. * @var integer $level Lovel of log in PHP constant like E_ERROR.
  388. * @var integer $message The log message human readable.
  389. */
  390. class FastPage_LogRecord extends FastPage_Object { }
  391. /**
  392. * Profile record.
  393. *
  394. * Current, a simple class of basic object.
  395. *
  396. * @package FastPage
  397. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  398. * @var string $name Process name.
  399. * @var integer $start Timestamp started in milli second.
  400. * @var integer $end Timestamp ended in milli second.
  401. * @var integer $duration Duration in milli second.
  402. */
  403. class FastPage_Profile extends FastPage_Object { }
  404. /**
  405. * Debugger.
  406. *
  407. * @package FastPage
  408. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  409. * @var boolean $enabled Enabled debugging.
  410. * @var array $logs Log records.
  411. * @var integer $error_message_type Message type to call error_log on error record added.
  412. * @var string $error_destination Destination to call error_log on error record added.
  413. * @var float $start Starting timestamp.
  414. * @var float $last Last logging timestamp.
  415. * @var FastPage_LogRecord $last Last log record.
  416. * @var float $debugging_time Total debugging time.
  417. * @var integer $debbuggin_memory_usage Total debugging memory usage.
  418. * @var integer $debugging_in_nest Nesting depth of debugging time.
  419. */
  420. class FastPage_Debug extends FastPage_Object {
  421. public $logs = array();
  422. public $start = 0.0;
  423. public $last = 0.0;
  424. public $debugging_time = 0.0;
  425. public $debugging_memory_usage = 0;
  426. public $debugging_in_nest = 0;
  427. /**
  428. * Singleton instance.
  429. */
  430. public static $instance;
  431. /**
  432. * Numeric log members.
  433. *
  434. * These members will be computed delta and sum.
  435. */
  436. public static $numeric_log_members = array( 'timestamp', 'memory_usage', 'memory_peak_usage' );
  437. /**
  438. * Get singleton instance.
  439. *
  440. * @return FastPage_Debug The instance.
  441. */
  442. public static function instance() {
  443. return self::$instance;
  444. }
  445. /**
  446. * Start debug.
  447. *
  448. * @param integer $message_type Optional. Passed to error_log function.
  449. * @param integer $destination Optional. Passed to error_log function.
  450. * @return boolean Current debug mode.
  451. */
  452. public static function start( $message_type = 0, $destination = '' ) {
  453. // Starting time. If set the time, set global $FASTPAGE_START;
  454. $time = FastPage_Util::millisec();
  455. global $FASTPAGE_START;
  456. if ( $FASTPAGE_START ) $time = $FASTPAGE_START;
  457. $debug = new self;
  458. $in = $debug->debug_in();
  459. $debug->start = $debug->last = $time;
  460. $debug->error_message_type = $message_type;
  461. $debug->error_destination = $destination;
  462. // Enabled debug.
  463. $debug->enabled = true;
  464. // Singleton.
  465. self::$instance = $debug;
  466. $debug->debug_out($in);
  467. return $debug;
  468. }
  469. /**
  470. * Start to debugging time.
  471. *
  472. * @return array Current time in millisecond and memory usage.
  473. */
  474. public function debug_in() {
  475. if ( !$this->enabled ) return null;
  476. $this->debugging_in_nest++;
  477. return array( FastPage_Util::millisec(), FastPage_Util::memory_usage() );
  478. }
  479. /**
  480. * Stop to debugging time.
  481. *
  482. * @param array $in Time and memory usage to debug in.
  483. */
  484. public function debug_out( $in ) {
  485. if ( !is_array($in) ) return;
  486. $this->debugging_in_nest--;
  487. if ( $this->debugging_in_nest == 0 ) {
  488. $out = FastPage_Util::millisec();
  489. $this->debugging_time += ( $out - $in[0] );
  490. $out = FastPage_Util::memory_usage();
  491. $this->debugging_memory_usage += ( $out - $in[1] );
  492. }
  493. }
  494. /**
  495. * Add new log record.
  496. *
  497. * @param FastPage $fastpage FastPage instance.
  498. * @param integer $level Log level in PHP constants.
  499. * @param string $message Log message human readable.
  500. * @param mixed $hint Hint value for the log record.
  501. */
  502. public function add_log( $fastpage, $level, $message, $hint = null ) {
  503. // Not in debugging, do nothing.
  504. if ( !$this->enabled ) return;
  505. $in = $this->debug_in();
  506. // Timestamp and usage.
  507. $msec = FastPage_Util::millisec();
  508. $usage = FastPage_Util::memory_usage();
  509. $peak_usage = function_exists('memory_get_peak_usage')? memory_get_peak_usage(true): $usage;
  510. // Log record.
  511. $record = new FastPage_LogRecord;
  512. $record->timestamp = $msec - $this->start;
  513. $record->memory_usage = $usage;
  514. $record->memory_peak_usage = $peak_usage;
  515. $record->level = $level;
  516. $record->message = $message;
  517. $record->hint = $hint;
  518. // Run callbacks if FastPage instance passed.
  519. if ( isset($fastpage) ) {
  520. $fastpage->run_callbacks( 'debug_log', FastPage_Callback::all, $record );
  521. }
  522. $this->last = $record;
  523. $this->logs []= $record;
  524. // Pass through to error_log() if E_ERROR or E_USER_ERROR.
  525. if ( $level | E_ERROR | E_USER_ERROR ) {
  526. error_log( $message, $this->error_message_type, $this->error_destination );
  527. }
  528. $this->debug_out($in);
  529. return $record;
  530. }
  531. /**
  532. * Add new log record (short hand to singleton instance add_log).
  533. */
  534. public static function log( $fastpage, $level, $message, $hint = null ) {
  535. if ( !self::$instance ) return;
  536. self::$instance->add_log( $fastpage, $level, $message, $hint );
  537. }
  538. /**
  539. * Merge another debug for forking.
  540. *
  541. * @param FastPage_Debug $debug Debug object to merge.
  542. */
  543. public function merge( $debug ) {
  544. $in = $this->debug_in();
  545. // Make profiles unique.
  546. $profiles = array();
  547. foreach ( $this->logs as $log ) {
  548. if ( $log->level == E_FP_PROFILE ) {
  549. $profiles[$log->message] = true;
  550. }
  551. }
  552. foreach ( $debug->logs as $log ) {
  553. if ( $log->level == E_FP_PROFILE ) {
  554. if ( !isset($profiles[$log->message]) ) {
  555. $this->logs []= $log;
  556. }
  557. } else {
  558. $this->logs []= $log;
  559. }
  560. }
  561. // Sum debugging time.
  562. $this->debugging_time += $debug->debugging_time;
  563. $this->debug_out($in);
  564. }
  565. /**
  566. * Get log records.
  567. *
  568. * @param $mask Mask to filter log level like E_ALL.
  569. * @return array Log records. If not started logging, return empty array.
  570. */
  571. public function log_records( $mask = E_ALL ) {
  572. // If logging not started, do nothing.
  573. if ( !$this->enabled ) return array();
  574. $in = $this->debug_in();
  575. $records = array();
  576. $lasts = array();
  577. $sums = array();
  578. foreach ( $this->logs as $record ) {
  579. if ( $record->level & $mask ) {
  580. // Get delta and sum of timestamp and memory usage.
  581. foreach ( self::$numeric_log_members as $prop ) {
  582. $delta = 'delta_' . $prop;
  583. $sum = 'sum_' . $prop;
  584. $record->$delta = isset($lasts[$prop])? $record->$prop - $lasts[$prop]: 0;
  585. $lasts[$prop] = $record->$prop;
  586. $sums[$prop] = isset($sums[$prop])? $sums[$prop] + $record->$delta: $record->$delta;
  587. $record->$sum = $sums[$prop];
  588. }
  589. $records []= $record;
  590. }
  591. }
  592. $this->debug_out($in);
  593. return $records;
  594. }
  595. /**
  596. * Get profiling records.
  597. *
  598. * @return array Profiling results.
  599. */
  600. public function log_profiles() {
  601. // If logging not started, do nothing.
  602. if ( !$this->enabled ) return array();
  603. $in = $this->debug_in();
  604. // Gather profiles.
  605. $profiles = array();
  606. $last = $this->last;
  607. foreach ( $this->logs as $log ) {
  608. if ( $log->level != E_FP_PROFILE ) continue;
  609. if ( isset($profiles[$log->message]) ) {
  610. // Update profile result.
  611. $profile = $profiles[$log->message];
  612. foreach ( self::$numeric_log_members as $prop ) {
  613. $to = $prop . '_to';
  614. $from = $prop . '_from';
  615. $delta = 'delta_' . $prop;
  616. $profile->$to = $log->$prop;
  617. $profile->$delta = $profile->$to - $profile->$from;
  618. }
  619. } else {
  620. // New profile result.
  621. $profile = new FastPage_Profile;
  622. $profile->name = $log->message;
  623. foreach ( self::$numeric_log_members as $prop ) {
  624. $to = $prop . '_to';
  625. $from = $prop . '_from';
  626. $delta = 'delta_' . $prop;
  627. $profile->$from = $log->$prop;
  628. $profile->$to = $last->$prop;
  629. $profile->$delta = $profile->$to - $profile->$from;
  630. }
  631. $profiles[$log->message] = $profile;
  632. }
  633. }
  634. $this->debug_out($in);
  635. return $profiles;
  636. }
  637. }
  638. /**
  639. * Text filters.
  640. *
  641. * @package FastPage
  642. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  643. */
  644. class FastPage_TextFilters extends FastPage_Object {
  645. /**
  646. * Filter definitions.
  647. */
  648. protected $filters = array();
  649. /**
  650. * Add a new condition to filters.
  651. *
  652. * @param string $condition Condition string usable wildcard(*) and regular expressions starts with ~.
  653. * @param boolean $ignore_case Set true, if ignore charactors case.
  654. */
  655. public function add( $condition, $ignore_case = false, $options = null ) {
  656. $args = array( $condition, $ignore_case, $options );
  657. $this->filters []= $args;
  658. }
  659. /**
  660. * Test if match a text for each filters.
  661. *
  662. * @param string $text An string to test.
  663. * @return boolean True, if one of filters is matched at least.
  664. */
  665. public function match( $text ) {
  666. // Has no filters, return false.
  667. if ( count($this->filters) < 1 ) return false;
  668. // Evaluate conditions.
  669. foreach ( $this->filters as $array ) {
  670. // Filter properties.
  671. $filter = $array[0];
  672. if ( !isset($filter) ) continue;
  673. $ignore_case = $array[1];
  674. $options = $array[2];
  675. // Convert to regular expression.
  676. $regex = null;
  677. if ( substr( $filter, 0, 1 ) == '~' ) {
  678. // By regular expression.
  679. $regex = substr( $filter, 1 );
  680. } else if ( strchr( $filter, '*' ) !== false ) {
  681. // Include wildcard.
  682. $regex = str_replace( '*', '(.*?)', $filter );
  683. } else if ( $options ) {
  684. // Has options.
  685. $regex = '^' . preg_quote($filter) . '$';
  686. }
  687. if ( $regex ) {
  688. // Regex compare.
  689. if ( !isset($options) ) $options = '';
  690. if ( $ignore_case ) $options .= 'i';
  691. // Escape.
  692. $regex = str_replace( '!', '\!', $regex );
  693. // Regular expression matched?
  694. $regex = '!' . $regex . '!' . $options;
  695. if ( preg_match( $regex, $text ) ) return true;
  696. } else if ( $ignore_case ) {
  697. if ( strtolower($text) == strtolower($filter) ) return true;
  698. } else {
  699. // String compare.
  700. if ( $text == $filter ) return true;
  701. }
  702. }
  703. // Return false, if no match.
  704. return false;
  705. }
  706. }
  707. /**
  708. * Base of a helper class.
  709. *
  710. * Helper classes has a reference of FastPage instance to access FastPage configurations.
  711. *
  712. * @package FastPage
  713. * @author Kunihiko Miyanaga@ideamans's Inc.
  714. */
  715. class FastPage_Helper extends FastPage_Object {
  716. /**
  717. * Constructor.
  718. *
  719. * @param FastPage $fastpage FastPage instance.
  720. */
  721. public function __construct( $fastpage ) {
  722. parent::__construct();
  723. // Store FastPage instance.
  724. $this->fastpage = $fastpage;
  725. }
  726. }
  727. /**
  728. * Plugin base class.
  729. *
  730. * Currently just an alias of FastPage_Helper.
  731. *
  732. * @var string $version Version of the plugin.
  733. */
  734. class FastPage_Plugin extends FastPage_Helper {
  735. /**
  736. * Constructor.
  737. */
  738. public function __construct( $fastpage ) {
  739. parent::__construct($fastpage);
  740. $this->version = '[UNKNOWN]';
  741. }
  742. }
  743. /**
  744. * Tree structured configuration.
  745. *
  746. * @package FastPage
  747. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  748. */
  749. class FastPage_Config extends FastPage_Helper {
  750. /**
  751. * Child configs.
  752. */
  753. protected $sub_configs = array();
  754. /**
  755. * Parent config.
  756. */
  757. public $parent_config = null;
  758. /**
  759. * Get a sub config.
  760. *
  761. * @param string $name Name of sub config.
  762. * @return FastPage_Config Sub config object. If not exist, return new object.
  763. */
  764. public function sub( $name ) {
  765. if ( !isset($this->sub_configs[$name]) ) {
  766. $class = get_class($this);
  767. $this->sub_configs[$name] = new $class( $this->fastpage );
  768. $this->sub_configs[$name]->parent_config = $this;
  769. }
  770. return $this->sub_configs[$name];
  771. }
  772. /**
  773. * Undefined function behaves as dictionary.
  774. */
  775. public function __call( $name, $args ) {
  776. $count = count($args);
  777. $key = $count > 0? $args[0]: null;
  778. $value = $count > 1? $args[1]: null;
  779. if ( $key ) {
  780. return $this->__dictionary( $name, $key, $value );
  781. }
  782. }
  783. /**
  784. * Set default value.
  785. *
  786. * Update self value if not set yet.
  787. *
  788. * @param string $name Name of config value.
  789. * @param mixed $value Default value of the config.
  790. */
  791. public function _default( $name, $value ) {
  792. $current = $this->$name;
  793. if ( !isset($current) ) {
  794. $this->$name = $value;
  795. }
  796. }
  797. }
  798. /**
  799. * Simple HTTP client helper.
  800. *
  801. * @package FastPage
  802. * @author Kunihiko Miyanaga
  803. * @var string $library HTTP client library: builtin, curl or http_request.
  804. */
  805. class FastPage_SimpleHTTP extends FastPage_Helper {
  806. /**
  807. * Get url content.
  808. *
  809. * Return null if failed.
  810. *
  811. * @param string $url URL to get.
  812. * @return string Response body.
  813. */
  814. public function get( $url ) {
  815. // Detect library.
  816. $config_lib = $this->fastpage->config('http')->library;
  817. $config_lib = $config_lib? strtolower($config_lib): 'builtin';
  818. $enabled = array();
  819. if ( ini_get('allow_url_fopen') ) {
  820. $enabled['builtin'] = 'builtin';
  821. }
  822. if ( function_exists('curl_init') ) {
  823. $enabled['curl'] = 'curl';
  824. }
  825. $enabled['http_request'] = 'http_request';
  826. $this->library = $library = $enabled[$config_lib]? $config_lib: array_shift($enabled);
  827. // Timeout.
  828. $timeout = $this->fastpage->config('http')->timeout;
  829. if ( !$timeout ) $timeout = 60;
  830. $result = null;
  831. try {
  832. if ( $library == 'builtin' ) {
  833. // Use builtin function.
  834. $context = stream_context_create( array(
  835. 'http' => array(
  836. 'timeout' => $timeout,
  837. 'header' => array( 'Accept-Encoding:' )
  838. )
  839. ) );
  840. $result = file_get_contents( $url, false, $context );
  841. } else if ( $library == 'curl' ) {
  842. // Use cURL.
  843. $ch = curl_init( $url );
  844. $opts = array(
  845. CURLOPT_HEADER => false,
  846. CURLOPT_RETURNTRANSFER => true,
  847. CURLOPT_BINARYTRANSFER => true,
  848. CURLOPT_FOLLOWLOCATION => true,
  849. CURLOPT_MAXREDIRS => 10,
  850. CURLOPT_SSL_VERIFYPEER => true,
  851. CURLOPT_CONNECTTIMEOUT => $timeout,
  852. CURLOPT_TIMEOUT => $timeout,
  853. CURLOPT_HTTPHEADER => array( 'Accept-Encoding:' )
  854. );
  855. // CURLOPT_MUTE maybe deprecated.
  856. if ( defined('CURLOPT_MUTE') ) $opts[CURLOPT_MUTE] = true;
  857. foreach ( $opts as $key => $value ) {
  858. curl_setopt( $ch, $key, $value );
  859. }
  860. $result = curl_exec($ch);
  861. curl_close($ch);
  862. } else {
  863. // Try to use Pear HTTP_Request.
  864. require_once('HTTP/Request.php');
  865. $opts = array(
  866. 'timeout' => $timeout,
  867. 'allowRedirects' => true,
  868. 'maxRedirects' => 10
  869. );
  870. $http = new HTTP_Request( $url, $opts );
  871. $http->removeHeader('accept-encoding');
  872. $res = $http->sendRequest();
  873. if ( !PEAR::isError($res) ) {
  874. $result = $http->getResponseBody();
  875. }
  876. }
  877. } catch ( Exception $ex ) {
  878. FastPage_Debug::log( $this->fastpage, E_USER_ERROR, "Simple HTTP get $url because: " . $ex->getMessage() );
  879. }
  880. return $result;
  881. }
  882. }
  883. /**
  884. * User agent helper.
  885. *
  886. * @package FastPage
  887. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  888. * @var string $full_string Full user agent string.
  889. * @var boolean $data_uri_enabled If data URI schema is enabled on the user agent.
  890. */
  891. class FastPage_UserAgent extends FastPage_Helper {
  892. /**
  893. * Constructor.
  894. */
  895. public function __construct( $fastpage, $ua = null ) {
  896. parent::__construct($fastpage);
  897. if ( !isset($ua) ) {
  898. // Detect user agent.
  899. $ua = FastPage_Util::server( 'HTTP_USER_AGENT', '' );
  900. }
  901. // Default.
  902. $this->full_string = $ua;
  903. $this->data_uri_enabled = false;
  904. // Data URI scheme ability.
  905. // Supported on WebKit, Gecko, Opera and IE8+.
  906. if ( preg_match( '!WebKit|Gecko|Opera!i', $ua ) ) {
  907. $this->data_uri_enabled = true;
  908. } else if ( preg_match( '!MSIE ([0-9\.]+)!', $ua, $matches ) ) {
  909. $version = (float)$matches[1];
  910. if ( $version >= 8.0 ) {
  911. $this->data_uri_enabled = true;
  912. }
  913. }
  914. // Run callbacks.
  915. $fastpage->run_callbacks( 'parse_user_agent', FastPage_Callback::all, $this );
  916. // Logging.
  917. if ( !$this->data_uri_enabled ) {
  918. FastPage_Debug::log( $fastpage, E_USER_NOTICE, 'The use agent does not support Data URI scheme: ' . $ua );
  919. }
  920. }
  921. }
  922. /**
  923. * A helper class to parse and modify an HTML element simply.
  924. *
  925. * @package FastPage
  926. * @author Kunihiko Miyanaga@ideaman's Inc.
  927. */
  928. class FastPage_HTMLElement extends FastPage_Helper {
  929. public $_attribute = array();
  930. public $_attributes = null;
  931. // Common regular expressions.
  932. const attribute_regex_prefix = '!(?<=\\s';
  933. const attribute_regex_suffix = '=)(["\'])(.*?)(?=\\1|>)!i';
  934. const remove_attribute_regex_prefix = '!(?<=\\s)';
  935. const remove_attribute_regex_suffix = '=(["\'])(.*?)(\\1\\s*)!i';
  936. const attributes_regex = '!(?<=\\s)([^=]+)=(["\'])(.*?)(?=\\2|>)!i';
  937. /**
  938. * Constructor.
  939. *
  940. * @param FastPage $fastpage An instance of FastPage class.
  941. * @param string $fragment A source of HTML element fragment.
  942. */
  943. public function __construct( $fastpage, $fragment ) {
  944. parent::__construct($fastpage);
  945. // Initialize.
  946. $this->original = $this->html = $fragment;
  947. // Check format.
  948. if ( substr( $this->html, 0, 1 ) != '<' || substr( $this->html, -1, 1 ) != '>' ) {
  949. FastPage_Debug::log( $fastpage, E_USER_WARNING, 'Not a wellformed HTML element: ' . $this->html );
  950. throw new FastPage_Exception( 'Not a wellformed HTML element: ' . $this->html );
  951. }
  952. // Parse tag name.
  953. if ( preg_match( '!^</?([^\s]+)!', $this->html, $matches ) ) {
  954. $this->tag_name = $matches[1];
  955. }
  956. }
  957. /**
  958. * Gets values of an attribute.
  959. *
  960. * @param string $name Name of an attribute.
  961. * @param string $value Value of the attribute. If set null, this function behaves as a getter.
  962. * @return string Value of the attribute. If not found, returns null.
  963. */
  964. public function attribute($name, $value = null) {
  965. // Regulara expression to find an atribute.
  966. $regex = self::attribute_regex_prefix . preg_quote($name) . self::attribute_regex_suffix;
  967. if ( isset($value) ) {
  968. // Replace attribute value.
  969. $this->html = preg_replace( $regex, "\\1$value", $this->html, -1, $count );
  970. // Cache the result if replaced.
  971. if ( $count > 0 ) {
  972. $this->_attribute[$name] = $value;
  973. $this->_attributes = null;
  974. }
  975. } else {
  976. // Get attribute value.
  977. if ( !isset($this->_attribute[$name]) ) {
  978. // Not cached, find the value from HTML.
  979. if ( preg_match( $regex, $this->html, $matches ) ) {
  980. $this->_attribute[$name] = $matches[2];
  981. } else {
  982. // Not found, cache false as boolean.
  983. $this->_attribute[$name] = false;
  984. }
  985. }
  986. $value = $this->_attribute[$name];
  987. return $value !== false? $value: null;
  988. }
  989. }
  990. /**
  991. * Remove an attribute.
  992. *
  993. * @param string $name Name of an attribute to remove.
  994. */
  995. public function remove_attribute($name) {
  996. // Regulara expression to find an atribute.
  997. $regex = self::remove_attribute_regex_prefix . preg_quote($name) . self::remove_attribute_regex_suffix;
  998. $this->html = preg_replace( $regex, '', $this->html, -1, $count );
  999. // Cache the result if replaced.
  1000. if ( $count > 0 ) {
  1001. unset($this->_attribute[$name]);
  1002. $this->_attributes = null;
  1003. }
  1004. }
  1005. /**
  1006. * Gets all attribute as an array.
  1007. *
  1008. * @return Attributes name and values as an array.
  1009. */
  1010. public function attributes() {
  1011. if ( !isset($this->_attributes) ) {
  1012. // Not cached, find with regular expression.
  1013. $this->_attributes = array();
  1014. if ( preg_match_all( self::attributes_regex, $this->html, $matches, PREG_SET_ORDER ) ) {
  1015. foreach ( $matches as $match ) {
  1016. $this->_attributes[$match[1]] = $match[3];
  1017. }
  1018. }
  1019. }
  1020. return $this->_attributes;
  1021. }
  1022. }
  1023. /**
  1024. * Cache helper.
  1025. *
  1026. * @package FastPage
  1027. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1028. * @var integer $default_expires Default second to expire cache.
  1029. */
  1030. class FastPage_Cache extends FastPage_Helper {
  1031. protected $_cache = array();
  1032. /**
  1033. * Constructor.
  1034. */
  1035. public function __construct( $fastpage ) {
  1036. parent::__construct($fastpage);
  1037. $config = $this->fastpage->config('cache');
  1038. // Max key length.
  1039. $this->max_key_length = $config->max_key_length;
  1040. if ( !isset($this->max_key_length) ) $this->max_key_length = 128;
  1041. // Cache key normalization algorithm.
  1042. $this->normalize_key_algorithm = $config->normalize_key_algorithm;
  1043. if ( !isset($this->normalize_key_algorithm) ) $this->normalize_key_algorithm = 'md5';
  1044. // Default expire.
  1045. $this->default_expire = $config->default_expire;
  1046. if ( !isset($this->default_expire) ) $this->default_expire = 60 * 60; // 1 hour
  1047. }
  1048. /**
  1049. * Normalize cache key.
  1050. *
  1051. * Convert long key with md5 or sha1.
  1052. *
  1053. * @param string $raw_key Raw cache key.
  1054. * @return string Normalized key.
  1055. */
  1056. public function normalize_key( $raw_key ) {
  1057. if ( strlen($raw_key) > $this->max_key_length ) {
  1058. $algorithm = $this->normalize_key_algorithm;
  1059. if ( $algorithm != 'sha1' ) $algorithm = 'md5';
  1060. $key = $algorithm($raw_key);
  1061. } else {
  1062. $key = $raw_key;
  1063. }
  1064. return $key;
  1065. }
  1066. /**
  1067. * Check if exist key.
  1068. *
  1069. * @param string $namespace Namespace.
  1070. * @param string $raw_key Cache key.
  1071. * @return boolean True if exists key.
  1072. */
  1073. public function exist( $namespace, $raw_key ) {
  1074. if ( !array_key_exists( $namespace, $this->_cache ) ) {
  1075. return false;
  1076. }
  1077. // Check array.
  1078. $key = $this->normalize_key($raw_key);
  1079. return array_key_exists( $key, $this->_cache[$namespace] );
  1080. }
  1081. /**
  1082. * Get cached value.
  1083. *
  1084. * * This is reference implementation not effective as cache.
  1085. *
  1086. * @param string $namespace Namespace of cache.
  1087. * @param string $key Key of cache item.
  1088. * @return mixed Cached value. If not cached, return null.
  1089. */
  1090. public function get( $namespace, $raw_key ) {
  1091. // Convert a key.
  1092. $key = $this->normalize_key($raw_key);
  1093. // Search cached value.
  1094. if ( isset($this->_cache[$namespace]) ) {
  1095. if ( isset($this->_cache[$namespace][$key]) ) {
  1096. $entry = $this->_cache[$namespace][$key];
  1097. if ( $entry['expire'] > time() ) {
  1098. return $entry['value'];
  1099. } else {
  1100. unset( $this->_cache[$namespace][$key] );
  1101. }
  1102. }
  1103. }
  1104. return null;
  1105. }
  1106. /**
  1107. * Set cache value.
  1108. *
  1109. * * This is reference implementation not effective as cache.
  1110. *
  1111. * @param string $namespace Namespace of cache.
  1112. * @param string $raw_key Key of cache entry.
  1113. * @param mixed $value Value of cache entry.
  1114. * @param integer $expire Second to expire the cache. If null, use default_expire.
  1115. */
  1116. public function set( $namespace, $raw_key, $value, $expire = null ) {
  1117. // Expire.
  1118. if ( !isset($expire) ) {
  1119. $expire = $this->default_expire;
  1120. }
  1121. // Convert a key.
  1122. $key = $this->normalize_key($raw_key);
  1123. $this->_cache[$namespace][$key] = array( 'expire' => time() + $expire, 'value' => $value );
  1124. }
  1125. /**
  1126. * Delete value.
  1127. *
  1128. * @param string $namespace Namespace.
  1129. * @param string $raw_key Cache key.
  1130. */
  1131. public function delete( $namespace, $raw_key ) {
  1132. if ( !isset($this->_cache[$namespace]) ) return;
  1133. $key = $this->normalize_key($raw_key);
  1134. unset( $this->_cache[$namespace][$key] );
  1135. }
  1136. /**
  1137. * Flush all cache entries.
  1138. *
  1139. * * This is reference implementation not effective as cache.
  1140. *
  1141. * @param string $namespace Namespace of cache. If null, clear all cache.
  1142. */
  1143. public function flush( $namespace = null ) {
  1144. if ( isset($namespace) ) {
  1145. $this->_cache[$namespace] = array();
  1146. } else {
  1147. $this->_cache = array();
  1148. }
  1149. }
  1150. /**
  1151. * Purge expired cache.
  1152. *
  1153. * * This is reference implementation not effective as cache.
  1154. *
  1155. * @param string $namespace Namespace of cache. If null, purge all cache.
  1156. */
  1157. public function purge( $namespace = null ) {
  1158. if ( isset($namespace) ) {
  1159. if ( is_array($this->_cache[$namespace]) ) {
  1160. $now = time();
  1161. foreach ( $this->_cache[$namespace] as $key => $entry ) {
  1162. if ( $entry['expire'] <= $now ) {
  1163. unset( $this->_cache[$namespace][$key] );
  1164. }
  1165. }
  1166. }
  1167. } else {
  1168. foreach ( $this->_cache as $ns => $caches ) {
  1169. $this->purge( $ns );
  1170. }
  1171. }
  1172. }
  1173. }
  1174. /**
  1175. * File cache helper.
  1176. *
  1177. * @package FastPage
  1178. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1179. * @var string $extension An extension of cache file.
  1180. */
  1181. class FastPage_FileCache extends FastPage_Cache {
  1182. protected $paths_to_ready = array();
  1183. /**
  1184. * Constructor.
  1185. *
  1186. * Check if ready to use directory.
  1187. */
  1188. public function __construct( $fastpage ) {
  1189. parent::__construct($fastpage);
  1190. $config = $this->fastpage->config('cache');
  1191. // Cache dir.
  1192. $this->cache_dir = $config->cache_dir;
  1193. if ( !isset($this->cache_dir) ) $this->cache_dir = './cache';
  1194. // Cache key index charactors, default is 2.
  1195. $this->key_index_chars = $config->key_index_chars;
  1196. if ( !isset($this->key_index_chars) || !$this->key_index_chars )
  1197. $this->key_index_chars = 2;
  1198. // Extension.
  1199. $this->extension = $config->extension;
  1200. if ( !isset($this->extension) || strlen($this->extension) < 1 )
  1201. $this->extension = 'cache';
  1202. // Permissions.
  1203. $this->file_perms = $config->file_perms;
  1204. if ( !isset($this->file_perms) ) $this->file_perms = 0644;
  1205. $this->dir_perms = $config->dir_perms;
  1206. if ( !isset($this->dir_perms) ) $this->dir_perms = 0755;
  1207. // Ready to use paths.
  1208. $this->paths_to_ready = array();
  1209. // Ready?
  1210. $this->ready();
  1211. }
  1212. /**
  1213. * Check if ready to use.
  1214. *
  1215. * Check existence, directory and writable.
  1216. */
  1217. protected function ready( $namespace = null, $key_index = null ) {
  1218. $path = $this->cache_dir;
  1219. if ( isset($namespace) ) {
  1220. if ( isset($key_index) ) {
  1221. $path = FastPage_Util::cat_path( $path, urlencode($namespace), $key_index );
  1222. } else {
  1223. $path = FastPage_Util::cat_path( $path, urlencode($namespace) );
  1224. }
  1225. }
  1226. // Lookup cache.
  1227. if ( isset($this->paths_to_ready[$path]) ) return;
  1228. if ( file_exists($path) && is_dir($path) && is_writable($path) ) {
  1229. // Ready to use.
  1230. $this->paths_to_ready[$path] = true;
  1231. return;
  1232. }
  1233. if ( file_exists($path) ) {
  1234. // Is directory?
  1235. if ( !is_dir($path) ) {
  1236. throw new FastPage_Exception( "Path: $path is already exists as not a directory." );
  1237. }
  1238. // Is writable?
  1239. if ( !is_writable($path) ) {
  1240. throw new FastPage_Exception( "Path: $path is not writable." );
  1241. }
  1242. } else {
  1243. // Try to make directory.
  1244. if ( false === mkdir( $path, $this->dir_perms, true ) ) {
  1245. throw new FastPage_Exception( "Can not create $path as directory." );
  1246. }
  1247. }
  1248. // Mark as ready.
  1249. $this->paths_to_ready[$path] = true;
  1250. }
  1251. /**
  1252. * Get index of key.
  1253. */
  1254. protected function key_index( $key ) {
  1255. $index = substr( $key, 0, $this->key_index_chars );
  1256. $index = str_pad( $index, $this->key_index_chars, '_' );
  1257. return $index;
  1258. }
  1259. /**
  1260. * Get file path for key.
  1261. */
  1262. protected function cache_path( $namespace, $raw_key ) {
  1263. // Normalize the key.
  1264. $key = $this->normalize_key($raw_key);
  1265. // Build cache path.
  1266. // Replace periods for security reason.
  1267. $file = urlencode($key);
  1268. $file = str_replace( '.', '%2e', $file );
  1269. $index = $this->key_index( $file );
  1270. if ( $this->extension ) $file .= '.' . $this->extension;
  1271. // Ready to use?
  1272. $this->ready( $namespace );
  1273. $this->ready( $namespace, $index );
  1274. return FastPage_Util::cat_path( $this->cache_dir, urlencode($namespace), $index, $file );
  1275. }
  1276. /**
  1277. * Override exist method.
  1278. */
  1279. public function exist( $namespace, $raw_key ) {
  1280. $path = $this->cache_path( $namespace, $raw_key );
  1281. return file_exists($path);
  1282. }
  1283. /**
  1284. * Override get method.
  1285. */
  1286. public function get( $namespace, $raw_key ) {
  1287. $path = $this->cache_path( $namespace, $raw_key );
  1288. if ( file_exists($path) && is_file($path) && is_readable($path) ) {
  1289. if ( filemtime($path) > time() ) return @file_get_contents($path);
  1290. @unlink($path);
  1291. }
  1292. return null;
  1293. }
  1294. /**
  1295. * Override set method.
  1296. */
  1297. public function set( $namespace, $raw_key, $value, $expire = null ) {
  1298. // Expires.
  1299. if ( !isset($expire) ) {
  1300. $expire = $this->default_expire;
  1301. }
  1302. // Put a file.
  1303. $path = $this->cache_path( $namespace, $raw_key );
  1304. @file_put_contents( $path, $value );
  1305. if ( file_exists($path) ) {
  1306. @chmod( $path, $this->file_perms );
  1307. @touch( $path, time() + $expire );
  1308. }
  1309. }
  1310. /**
  1311. * Override delete method.
  1312. */
  1313. public function delete( $namespace, $raw_key ) {
  1314. $path = $this->cache_path( $namespace, $raw_key );
  1315. if ( file_exists($path) && is_file($path) ) {
  1316. @unlink($path);
  1317. $dir = dirname($path);
  1318. $dh = opendir( $dir );
  1319. $entries = 0;
  1320. while ( false !== ( $file = readdir($dh) ) ) {
  1321. if ( $file != '.' && $file != '..' ) {
  1322. $entries++;
  1323. break;
  1324. }
  1325. }
  1326. closedir($dh);
  1327. if ( $entries == 0 ) @rmdir($dir);
  1328. }
  1329. }
  1330. /**
  1331. * Clear files and directories recursively.
  1332. */
  1333. protected function clear_recursive( $dir, $expire = null ) {
  1334. if ( !file_exists($dir) || !is_dir($dir) ) return;
  1335. $dh = opendir($dir);
  1336. $entries = 0;
  1337. while ( false !== ( $file = readdir($dh) ) ) {
  1338. // Skip . or ..
  1339. if ( $file == '.' || $file == '..' )
  1340. continue;
  1341. // Increment at once.
  1342. $entries++;
  1343. $path = FastPage_Util::cat_path( $dir, $file );
  1344. if ( is_file( $path ) ) {
  1345. // Skip if not expired.
  1346. if ( isset($expire) && filemtime($path) > $expire )
  1347. continue;
  1348. // Delete a file.
  1349. if ( @unlink($path) ) $entries--;
  1350. } else if ( is_dir( $path ) ) {
  1351. // Clear resursively.
  1352. $sub_entries = $this->clear_recursive( $path, $expire );
  1353. if ( $sub_entries == 0 ) {
  1354. // Remove directory if empty.
  1355. if ( @rmdir($path) ) $entries--;
  1356. }
  1357. }
  1358. }
  1359. closedir($dh);
  1360. return $entries;
  1361. }
  1362. /**
  1363. * Flush and purge proxy.
  1364. */
  1365. public function clear( $namespace = null, $expire = null ) {
  1366. $dir = $this->cache_dir;
  1367. if ( isset($namespace) ) $dir = FastPage_Util::cat_path( $dir, urlencode($namespace) );
  1368. $this->clear_recursive( $dir, $expire );
  1369. }
  1370. /**
  1371. * Override flush method.
  1372. */
  1373. public function flush( $namespace = null ) {
  1374. $this->clear( $namespace, null );
  1375. }
  1376. /**
  1377. * Override clear method.
  1378. */
  1379. public function purge( $namespace = null ) {
  1380. $this->clear( $namespace, time() );
  1381. }
  1382. }
  1383. /**
  1384. * Callback object.
  1385. *
  1386. * @package FastPage
  1387. * @author Kunihiko Miyanaga@ideamans.com
  1388. * @var FastPage_Core $fastpage FastPage_Core instance.
  1389. * @var string $event Callback event.
  1390. * @var integer $priority Callback priority.
  1391. * @var mixed $function Callable function as string, or array of object and method..
  1392. * @var mixed $hint Callback hint.
  1393. * @var integer $index Callback index in priority chain.
  1394. */
  1395. class FastPage_Callback extends FastPage_Object {
  1396. /**
  1397. * Callback option: Run only the first callback returns a value not null.
  1398. */
  1399. const first_one = 0x01;
  1400. /**
  1401. * Callback option: Run all callbacks and return nothing.
  1402. */
  1403. const all = 0x02;
  1404. /**
  1405. * Callback option: Run all callbacks and return those results that is not null as an array.
  1406. */
  1407. const results_array = 0x04;
  1408. /**
  1409. * Callback option: Run all callbacks and operate logically with 'OR'.
  1410. */
  1411. const results_and = 0x08;
  1412. /**
  1413. * Callback option: Run all callbacks and operate logically with 'AND'.
  1414. */
  1415. const results_or = 0x10;
  1416. /**
  1417. * Constructor.
  1418. *
  1419. * @param FastPage_Core $fastpage FastPage_Core object contains the callback.
  1420. * @param string $event Callback event.
  1421. * @param integer $priority Callback priority.
  1422. * @param mixed $function Callable function.
  1423. * @param mixed $hint Callback hint.
  1424. */
  1425. public function __construct( $fastpage, $event, $priority, $function, $hint ) {
  1426. $this->fastpage = $fastpage;
  1427. $this->event = $event;
  1428. $this->priority = $priority;
  1429. $this->function = $function;
  1430. $this->hint = $hint;
  1431. }
  1432. }
  1433. /**
  1434. * Onetime task object.
  1435. *
  1436. * @package FastPage
  1437. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1438. * @var FastPage_Core $fastpage FastPage_Core instance.
  1439. * @var string $name Task name.
  1440. * @var integer $index Index in task queue.
  1441. * @var mixed $function Callable function as string, or array of object and method.
  1442. * @var mixed $args Arguments to call function.
  1443. * @var mixed $result Task result.
  1444. */
  1445. class FastPage_OnetimeTask extends FastPage_Object {
  1446. /**
  1447. * Constructor.
  1448. *
  1449. * @param FastPage_Core $fastpage FastPage_Core object queueing the task.
  1450. * @param string $name Name of the task.
  1451. * @param mixed $function Task function.
  1452. * @param mixed $args Arguments to call function.
  1453. */
  1454. public function __construct( $fastpage, $name, $function, $args ) {
  1455. $this->fastpage = $fastpage;
  1456. $this->name = $name;
  1457. $this->function = $function;
  1458. $this->args = $args;
  1459. }
  1460. }
  1461. /**
  1462. * Core features of FastPage like callback and helpers system.
  1463. *
  1464. * @package FastPage
  1465. * @author Kunihiko Miyanaga@ideaman's Inc.
  1466. */
  1467. class FastPage_Core extends FastPage_Object {
  1468. /**
  1469. * Callbacks chain.
  1470. */
  1471. protected $_callbacks = array();
  1472. /**
  1473. * Onetime tasks queue.
  1474. */
  1475. protected $_onetime_tasks = array();
  1476. /**
  1477. * Helper singleton instances.
  1478. */
  1479. protected $_helper_instance = array();
  1480. /**
  1481. * Plugins.
  1482. */
  1483. protected $_plugins = array();
  1484. /**
  1485. * Constructor
  1486. */
  1487. public function __construct() {
  1488. parent::__construct();
  1489. // Built-in helpers.
  1490. $helpers = array(
  1491. 'config' => 'Config',
  1492. 'cache' => 'FileCache',
  1493. 'html_element' => 'HTMLElement',
  1494. 'user_agent' => 'UserAgent',
  1495. 'simple_http' => 'SimpleHTTP'
  1496. );
  1497. foreach ( $helpers as $helper => $class ) {
  1498. $this->helper_class( $helper, 'FastPage_' . $class );
  1499. }
  1500. // MIME types.
  1501. $mime_types = array(
  1502. 'jpeg' => 'image/jpeg',
  1503. 'jpg' => 'image/jpeg',
  1504. 'gif' => 'image/gif',
  1505. 'png' => 'image/png',
  1506. 'css' => 'text/css',
  1507. 'js' => 'text/javascript',
  1508. 'html' => 'text/html',
  1509. 'htm' => 'text/html'
  1510. );
  1511. foreach ( $mime_types as $name => $value ) {
  1512. $this->config()->mime_type( $name, $value );
  1513. }
  1514. }
  1515. /*
  1516. * Helper class accessor.
  1517. *
  1518. */
  1519. public function helper_class( $name, $value = null ) {
  1520. // Remove current singleton instance.
  1521. unset( $this->_helper_instance[$name] );
  1522. return $this->__dictionary( 'helper_class', $name, $value );
  1523. }
  1524. /**
  1525. * Create new helper object.
  1526. *
  1527. * @param Array $name Name of helper.
  1528. * @param boolean $singleton Optional. If true, the instance will be stored and reused as singleton.
  1529. * @param mixed $param1 Optional. The first parameter for constructor.
  1530. * @param mixed $param2 Optional. The second parameter for constructor.
  1531. * @return mixed A new object of helper class.
  1532. */
  1533. public function helper_instance( $helper, $singleton = false, $param1 = null, $param2 = null ) {
  1534. // If singleton and already exists, return the instance.
  1535. if ( $singleton && array_key_exists( $helper, $this->_helper_instance ) ) {
  1536. return $this->_helper_instance[$helper];
  1537. }
  1538. // Helper class.
  1539. $class = $this->helper_class($helper);
  1540. // Create instance with params.
  1541. // FIXME: Which is a better way?
  1542. $obj = null;
  1543. if ( class_exists($class) ) {
  1544. if ( isset($param1) && isset($param2) ) {
  1545. $obj = new $class( $this, $param1, $param2 );
  1546. } else if ( isset($param1) ) {
  1547. $obj = new $class( $this, $param1 );
  1548. } else {
  1549. $obj = new $class( $this );
  1550. }
  1551. }
  1552. // Store instance if singleton.
  1553. if ( $singleton ) {
  1554. $this->_helper_instance[$helper] = $obj;
  1555. }
  1556. // Run callbacks.
  1557. $this->run_callbacks( 'helper_created.' . $helper, FastPage_Callback::all, $obj );
  1558. return $obj;
  1559. }
  1560. /**
  1561. * Document root by host.
  1562. */
  1563. public function document_root( $host = '', $value = null ) {
  1564. // Is as default?
  1565. if ( $host == '' || $host == $this->config()->default_host ) {
  1566. if ( isset($value) )
  1567. return $this->config()->default_document_root = $value;
  1568. else
  1569. return $this->config()->default_document_root;
  1570. }
  1571. return $this->config()->document_root( $host, $value );
  1572. }
  1573. /**
  1574. * MIME type
  1575. */
  1576. public function mime_type( $ext, $value = null ) {
  1577. return $this->config()->mime_type( $ext, $value );
  1578. }
  1579. /**
  1580. * Add callback to chain.
  1581. *
  1582. * Callback has an event name and priority.
  1583. * Callback will be called by order priority value is smaller in each event.
  1584. *
  1585. * @param string $event Name of an event.
  1586. * @param integer $priority Priority of the callback.
  1587. * @param mixed $function Function locator: Name of the function as a string or array of an object and method name.
  1588. * @param mixed $hint Hint value that will be passed to calling.
  1589. * @return FastPage_Callback Callback object added to chain.
  1590. */
  1591. public function add_callback( $event, $priority, $function, $hint = null ) {
  1592. // Ensure chain.
  1593. if ( !isset($this->_callbacks[$event][$priority]) ) {
  1594. $this->_callbacks[$event][$priority] = array();
  1595. }
  1596. // Create callback object and push to chain.
  1597. $callback = new FastPage_Callback( $this, $event, $priority, $function, $hint );
  1598. $callback->index = array_push( $this->_callbacks[$event][$priority], $callback );
  1599. return $callback;
  1600. }
  1601. /**
  1602. * Remove callback.
  1603. *
  1604. * @param FastPage_Callback $callback Callback object.
  1605. */
  1606. public function remove_callback( $callback ) {
  1607. if ( isset( $this->_callbacks[$callback->event][$callback->priority][$callback->index] ) ) {
  1608. unset( $this->_callbacks[$callback->event][$callback->priority][$callback->index] );
  1609. }
  1610. }
  1611. /**
  1612. * Run callbacks.
  1613. *
  1614. * @param string $event Name of an event.
  1615. * @param integer $flags Options to run callbacks chain.
  1616. * @param mixed $args Argument to run callback.
  1617. */
  1618. public function run_callbacks( $event, $flags, $args = null ) {
  1619. if ( isset($this->_callbacks[$event]) ) {
  1620. // Sort priorities.
  1621. $priorities = array_keys($this->_callbacks[$event]);
  1622. sort( $priorities, SORT_NUMERIC );
  1623. // Call functions.
  1624. $result = null;
  1625. foreach ( $priorities as $priority ) {
  1626. $callbacks = $this->_callbacks[$event][$priority];
  1627. foreach ( $callbacks as $callback ) {
  1628. $function = $callback->function;
  1629. $hint = $callback->hint;
  1630. // Call function.
  1631. if ( !is_callable($function) )
  1632. continue;
  1633. $r = call_user_func( $function, $callback, $args, $hint );
  1634. // Treat result.
  1635. if ( isset($r) ) {
  1636. if ( $flags & FastPage_Callback::first_one ) {
  1637. return $r;
  1638. } else if ( $flags & FastPage_Callback::all ) {
  1639. if ( !isset($result) ) $result = 0;
  1640. $result++;
  1641. } else if ( $flags & FastPage_Callback::results_array ) {
  1642. if ( !isset($result) ) $result = array();
  1643. $result []= $r;
  1644. } else if ( $flags & FastPage_Callback::results_and ) {
  1645. $result = isset($result)? $result && $r: $r;
  1646. } else if ( $flags & FastPage_Callback::results_or ) {
  1647. $result = isset($result)? $result || $r: $r;
  1648. }
  1649. }
  1650. }
  1651. }
  1652. return $result;
  1653. }
  1654. return null;
  1655. }
  1656. /**
  1657. * Add a new task to queue.
  1658. */
  1659. public function add_onetime_task( $name, $function, $args = null ) {
  1660. // Cannot add lambda.
  1661. if ( is_string($function) && preg_match( '!^\\x00lambda_!', $function ) ) {
  1662. throw new FastPage_Exception( 'Onetime task can not be lambda by create_function.' );
  1663. }
  1664. // Add task to queue.
  1665. $task = new FastPage_OnetimeTask( $this, $name, $function, $args );
  1666. $task->index = array_push( $this->_onetime_tasks, $task );
  1667. return $task;
  1668. }
  1669. /**
  1670. * Remove onetime task from queue.
  1671. */
  1672. public function remove_onetime_task( $task ) {
  1673. if ( isset($task->index) && isset($this->_tasks[$task->index]) ) {
  1674. unset( $this->_onetime_tasks[$task->index] );
  1675. }
  1676. }
  1677. /**
  1678. * Check if has tasks in queue.
  1679. */
  1680. public function has_onetime_tasks() {
  1681. return count($this->_onetime_tasks)? true: false;
  1682. }
  1683. /**
  1684. * Run onetime tasks.
  1685. */
  1686. public function run_onetime_tasks() {
  1687. foreach ( $this->_onetime_tasks as $task ) {
  1688. $pofile = 'Task: ' . $task->name;
  1689. FastPage_Debug::log( $this, E_FP_PROFILE, $pofile, $task );
  1690. $task->result = call_user_func( $task->function, $task, $task->args );
  1691. FastPage_Debug::log( $this, E_FP_PROFILE, $pofile, $task );
  1692. }
  1693. }
  1694. /**
  1695. * Save onetime tasks to cache.
  1696. *
  1697. * @return string Cache key of saved onetime tasks.
  1698. */
  1699. public function save_onetime_tasks() {
  1700. $cache = $this->cache();
  1701. $serialized = serialize($this->_onetime_tasks);
  1702. $key = md5($serialized);
  1703. $cache->set( 'onetime_tasks', $key, $serialized );
  1704. return $key;
  1705. }
  1706. /**
  1707. * Load onetime tasks from cache.
  1708. *
  1709. * @param string $key Cache key of saved onetime tasks.
  1710. */
  1711. public function load_onetime_tasks( $key ) {
  1712. $cache = $this->cache();
  1713. $serialized = $cache->get( 'onetime_tasks', $key );
  1714. $cache->delete( 'onetime_tasks', $key );
  1715. if ( !$serialized ) return;
  1716. $tasks = @unserialize($serialized);
  1717. if ( !is_array($tasks) ) return;
  1718. foreach ( $tasks as $task ) {
  1719. if ( is_a( $task, 'FastPage_OnetimeTask' ) ) {
  1720. $task->index = array_push( $this->_onetime_tasks, $task );
  1721. }
  1722. }
  1723. }
  1724. /**
  1725. * Run regular tasks.
  1726. */
  1727. public function run_regular_tasks() {
  1728. $this->run_callbacks( 'regular_tasks', FastPage_Callback::all );
  1729. }
  1730. /**
  1731. * Returns the config helper object.
  1732. *
  1733. * @param strings $ARGS Path to sub config.
  1734. * @return FastPage_Config Configuration object.
  1735. */
  1736. public function config() {
  1737. $config = $this->helper_instance( 'config', true );
  1738. if ( func_num_args() > 0 ) {
  1739. // Traverse sub configs.
  1740. $path = func_get_args();
  1741. foreach ( $path as $sub ) {
  1742. if ( !is_string($sub) ) break;
  1743. $config = $config->sub($sub);
  1744. }
  1745. }
  1746. return $config;
  1747. }
  1748. /**
  1749. * Returns the cache helper object.
  1750. *
  1751. * @return FastPage_Cache Caching object.
  1752. */
  1753. public function cache() {
  1754. return $this->helper_instance( 'cache', true );
  1755. }
  1756. /**
  1757. * Returns a new simple HTTP client helper object.
  1758. *
  1759. * @return FastPage_SimpleHTTP Simple HTTP object.
  1760. */
  1761. public function simple_http() {
  1762. return $this->helper_instance( 'simple_http', false );
  1763. }
  1764. /**
  1765. * Returns a new FastPage_HTMLElement object to parse a fragment.
  1766. *
  1767. * @param string $fragment HTML source fragment to parse.
  1768. * @return FastPage_HTMLElement object to parse and modify the element.
  1769. */
  1770. public function parse_html_element( $fragment ) {
  1771. try {
  1772. return $this->helper_instance( 'html_element', false, $fragment );
  1773. } catch ( FastPage_Exception $e ) {
  1774. return null;
  1775. }
  1776. }
  1777. /**
  1778. * Get plugin instance.
  1779. *
  1780. * @param string $name Name of plugin.
  1781. * @return mixed Plugin instance.
  1782. */
  1783. public function plugin( $name ) {
  1784. if ( isset($this->_plugins[$name]) ) {
  1785. return $this->_plugins[$name];
  1786. }
  1787. return false;
  1788. }
  1789. /**
  1790. * Load plugin.
  1791. *
  1792. * @param string $name Name of plugin.
  1793. * @param mixed $args Argument passed to plugin function.
  1794. * @return mixed Return value of plugin function. If not defined plugin function, return null.
  1795. */
  1796. public function load_plugin( $name, $args = null ) {
  1797. // Already loaded?
  1798. if ( $plugin = $this->plugin($name) ) return $plugin;
  1799. FastPage_Debug::log( $this, E_FP_PROFILE, 'Load plugin: ' . $name );
  1800. $plugin_dir = $this->config()->plugin_dir;
  1801. if ( !isset($plugin_dir) ) {
  1802. // Initialize plugin directory path.
  1803. $base_dir = dirname(__FILE__);
  1804. $plugin_dir = FastPage_Util::cat_path( $base_dir, '../plugins');
  1805. }
  1806. // Load plugin file.
  1807. $plugin_path = FastPage_Util::cat_path( $plugin_dir, "$name.php" );
  1808. if ( file_exists($plugin_path) ) {
  1809. require_once($plugin_path);
  1810. $class_prefix = 'FastPage_Plugin_';
  1811. // Create an instance of plugin class.
  1812. $class_names = array( $class_prefix . $name, $name );
  1813. foreach ( $class_names as $class ) {
  1814. if ( class_exists($class) ) {
  1815. $plugin = $this->_plugins[$name] = new $class( $this, $args );
  1816. // Callback.
  1817. $this->run_callbacks( 'plugin_loaded', FastPage_Callback::all, $plugin );
  1818. FastPage_Debug::log( $this, E_USER_NOTICE, "Plugin loaded: $name Ver." . $plugin->version );
  1819. break;
  1820. }
  1821. // Plugin class not found.
  1822. if ( !$plugin ) {
  1823. FastPage_Debug::log( $this, E_USER_ERROR, 'Plugin class ' . $class . ' does not exist.' );
  1824. }
  1825. }
  1826. } else {
  1827. // Plugin file not found.
  1828. FastPage_Debug::log( $this, E_USER_ERROR, 'Plugin file ' . $plugin_path . ' does not exist.' );
  1829. }
  1830. FastPage_Debug::log( $this, E_FP_PROFILE, 'Load plugin: ' . $name );
  1831. return $plugin;
  1832. }
  1833. /**
  1834. * Load config file.
  1835. */
  1836. public function load_config_file( $config_path = null ) {
  1837. if ( $config_path === false ) return;
  1838. // Load config file.
  1839. FastPage_Debug::log( $this, E_FP_PROFILE, 'Load config file' );
  1840. if ( !isset($config_path) ) {
  1841. // Auto detection.
  1842. $config_path = './config.php';
  1843. if ( !file_exists($config_path) ) {
  1844. $script_path = FastPage_Util::server('SCRIPT_FILENAME');
  1845. if ( !isset($script_path) ) {
  1846. $script_path = __FILE__;
  1847. }
  1848. $script_dir = dirname($script_path);
  1849. $config_path = FastPage_Util::cat_path( $script_dir, 'config.php' );
  1850. }
  1851. }
  1852. // Load config.
  1853. if ( $config_path !== false ) {
  1854. if ( file_exists($config_path) ) {
  1855. $fastpage = $app = $this;
  1856. include($config_path);
  1857. } else {
  1858. FastPage_Debug::log( $this, E_USER_ERROR, 'Can not load config file: ' . $config_path );
  1859. }
  1860. }
  1861. FastPage_Debug::log( $this, E_FP_PROFILE, 'Load config file' );
  1862. }
  1863. }
  1864. /**
  1865. * FastPage request.
  1866. *
  1867. * @package FastPage
  1868. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1869. * @var string $method Request method.
  1870. * @var string $url Reqeust URL.
  1871. * @var string $path Target Path.
  1872. * @var string $host Host name.
  1873. */
  1874. class FastPage_Request extends FastPage_Helper { }
  1875. /**
  1876. * FastPage response.
  1877. *
  1878. * @package FastPage
  1879. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1880. */
  1881. class FastPage_Response extends FastPage_Helper {
  1882. public $headers = array();
  1883. /**
  1884. * Build HTTP headers with properties.
  1885. *
  1886. * @return array HTTP headers.
  1887. */
  1888. public function build_headers() {
  1889. $headers = $this->headers;
  1890. // Content length.
  1891. if ( $this->body ) {
  1892. $headers['Content-Length'] = strlen($this->body);
  1893. }
  1894. // Etag.
  1895. if ( $this->etag ) {
  1896. $headers['ETag'] = FastPage_Util::quote($this->etag);
  1897. }
  1898. // Content Type.
  1899. if ( $this->content_type ) {
  1900. $content_type = $this->content_type;
  1901. if ( $this->charset && false === strstr( strtolower($content_type), 'charset=' ) ) {
  1902. $content_type .= ';charset=' . $this->charset;
  1903. }
  1904. $headers['Content-Type'] = $content_type;
  1905. }
  1906. return $headers;
  1907. }
  1908. };
  1909. /**
  1910. * FastPage processing context.
  1911. *
  1912. * @package FastPage
  1913. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1914. *
  1915. * @var FastPage_Request $request
  1916. */
  1917. class FastPage_Context extends FastPage_Helper {
  1918. public function __construct($fastpage) {
  1919. parent::__construct($fastpage);
  1920. // Default values.
  1921. $this->requires_etag = true;
  1922. // Request and response.
  1923. $this->request = $fastpage->helper_instance( 'request', false );
  1924. $this->response = $fastpage->helper_instance( 'response', false );
  1925. $this->user_agent = $fastpage->helper_instance( 'user_agent', false );
  1926. }
  1927. /**
  1928. * Check if match Etag of request and result.
  1929. *
  1930. * @return boolean If matched, return true.
  1931. */
  1932. public function match_etag() {
  1933. // No need etag.
  1934. if ( !$this->requires_etag ) {
  1935. return false;
  1936. }
  1937. // Generate etag.
  1938. if ( !$this->response->etag ) {
  1939. $this->response->etag = $this->fastpage->generate_etag( $this );
  1940. }
  1941. // Compare etag.
  1942. if ( $this->request->etag && $this->response->etag && $this->request->etag == $this->response->etag ) {
  1943. $this->not_modified = true;
  1944. return true;
  1945. }
  1946. return false;
  1947. }
  1948. }
  1949. /**
  1950. * External file referrence like image or JavaScript.
  1951. *
  1952. * @package FastPage
  1953. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  1954. * @var string $url Original URL.
  1955. * @var string $path Real path.
  1956. * @var string $domain Domain of the external.
  1957. * @var string $relative_path Relative path from the document root.
  1958. * @var string $mime_type MIME type.
  1959. * @var string $replace_type Type of replacement like data_uri or inline.
  1960. * @var string $replace Replacement.
  1961. * @var mixed $index Index in the set.
  1962. * @var integer $redundancy Redudancy in the set.
  1963. */
  1964. class FastPage_External extends FastPage_Object {
  1965. /**
  1966. * Etag of the file.
  1967. *
  1968. * @param boolean $quote If true, double-quote the result.
  1969. * @return string Etag of the file.
  1970. */
  1971. public function etag( $quote = true ) {
  1972. if ( isset($this->etag) ) {
  1973. // Already gotten.
  1974. return $this->etag;
  1975. }
  1976. if ( $this->path ) {
  1977. // Get etag.
  1978. $this->etag = FastPage_Util::file_etag( $this->path, $quote );
  1979. if ( !$this->etag ) $this->etag = false;
  1980. return $this->etag;
  1981. }
  1982. return null;
  1983. }
  1984. /**
  1985. * Convert to Data URI schema.
  1986. */
  1987. public function data_uri() {
  1988. if ( $this->mime_type && file_exists($this->path) ) {
  1989. if ( false !== ( $contents = file_get_contents($this->path) ) ) {
  1990. $data_uri = 'data:' . $this->mime_type . ';base64,';
  1991. $data_uri .= base64_encode($contents);
  1992. return $data_uri;
  1993. }
  1994. }
  1995. return null;
  1996. }
  1997. /**
  1998. * Check if the file is image.
  1999. */
  2000. public function is_image() {
  2001. if ( strtolower( substr( $this->mime_type, 0, 6 ) ) == 'image/' ) {
  2002. return true;
  2003. }
  2004. return false;
  2005. }
  2006. /**
  2007. * Check if the file is JavaScript.
  2008. */
  2009. public function is_js() {
  2010. if ( strtolower( $this->mime_type ) == 'text/javascript' ) {
  2011. return true;
  2012. }
  2013. return false;
  2014. }
  2015. }
  2016. /**
  2017. * Collection of an external referrence.
  2018. *
  2019. * @package FastPage
  2020. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  2021. * @var string $base_dir Base directory path.
  2022. * @var array $externals Array collection of FastPage_External object.
  2023. */
  2024. class FastPage_Externals extends FastPage_Helper {
  2025. public $externals = array();
  2026. /**
  2027. * Constructor.
  2028. *
  2029. * @param FastPage $fastpage FastPage object.
  2030. * @param array $urls URLs to make collection.
  2031. * @param string $base_dir Base directory path for the set.
  2032. */
  2033. public function __construct( $fastpage, $context, $urls ) {
  2034. FastPage_Helper::__construct($fastpage);
  2035. $this->base_dir = $context->response->base_dir;
  2036. // Parse URLs.
  2037. $config = $fastpage->config();
  2038. $redundancies = array();
  2039. foreach ( $urls as $index => $url ) {
  2040. // Skip excluded url.
  2041. if ( $config->exclude_url->match($url) ) continue;
  2042. // Get path, but skip if it matches excluded path.
  2043. $domain = $context->request->host;
  2044. $relative_path = '';
  2045. $path = $fastpage->url_to_path( $url, $this->base_dir, true, $domain, $relative_path );
  2046. $mime_type = '';
  2047. if ( $path && is_file($path) ) {
  2048. if ( $config->exclude_path->match($path) ) continue;
  2049. // Mime type.
  2050. $pi = pathinfo($path);
  2051. $ext = $pi['extension'];
  2052. $mime_type = $config->mime_type($ext);
  2053. // Count up redundancies.
  2054. if ( !isset($redundancies[$path]) ) {
  2055. $redundancies[$path] = 0;
  2056. }
  2057. $redundancies[$path]++;
  2058. }
  2059. // New reference.
  2060. $external = new FastPage_External;
  2061. $external->url = $url;
  2062. $external->path = $path;
  2063. $external->domain = $domain;
  2064. $external->relative_path = $relative_path;
  2065. $external->mime_type = $mime_type;
  2066. $external->index = $index;
  2067. // Add to array.
  2068. $this->externals[$index] = $external;
  2069. }
  2070. // Apply redundancies.
  2071. foreach ( $this->externals as $external ) {
  2072. if ( $external->path && isset($redundancies[$external->path]) ) {
  2073. $external->redundancy = $redundancies[$external->path];
  2074. }
  2075. }
  2076. }
  2077. /**
  2078. * Force to free external object.
  2079. *
  2080. * @param integer $index Index of external.
  2081. */
  2082. public function free($index) {
  2083. unset($this->externals[$index]);
  2084. }
  2085. }
  2086. /**
  2087. * Main class of FastPage library.
  2088. *
  2089. * @package FastPage
  2090. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  2091. */
  2092. class FastPage extends FastPage_Core {
  2093. /**
  2094. * Constructor.
  2095. */
  2096. public function __construct() {
  2097. parent::__construct();
  2098. // Default configs.
  2099. $config = $this->config();
  2100. // Make JavaScripts inline.
  2101. $config->sub('inline_js')->max_file_size = 1024 * 10;
  2102. // Max file size and files to convert Data URI schema.
  2103. $config->sub('data_uri')->max_file_size = 1024 * 10;
  2104. $config->sub('data_uri')->max_redundancy = 3;
  2105. // Exclude URLs.
  2106. $config->exclude_url = new FastPage_TextFilters;
  2107. $config->exclude_path = new FastPage_TextFilters;
  2108. // Built-in helpers.
  2109. $builtin_helpers = array(
  2110. 'context' => 'Context',
  2111. 'request' => 'Request',
  2112. 'response' => 'Response',
  2113. 'externals' => 'Externals',
  2114. );
  2115. foreach ( $builtin_helpers as $helper => $class ) {
  2116. $this->helper_class( $helper, 'FastPage_' . $class );
  2117. }
  2118. // Built-in body handlers.
  2119. $builtin_handlers = array(
  2120. 'css' => 'css',
  2121. 'html' => 'html',
  2122. 'htm' => 'html'
  2123. );
  2124. // Handlers.
  2125. foreach ( $builtin_handlers as $ext => $type ) {
  2126. $this->add_callback( 'convert_body.' . $ext, 10, array( $this, 'cb_convert_' . $type ) );
  2127. }
  2128. // Other built-in callbacks.
  2129. $builtin_callbacks = array(
  2130. 'original_response',
  2131. 'generate_etag',
  2132. 'convert_externals'
  2133. );
  2134. foreach ( $builtin_callbacks as $cb ) {
  2135. $this->add_callback( $cb, 5, array( $this, 'cb_' . $cb ) );
  2136. }
  2137. // Run callback.
  2138. $this->run_callbacks( 'init_fastpage', FastPage_Callback::all, $this );
  2139. }
  2140. /**
  2141. * Resolve URL to real path.
  2142. *
  2143. * @param string $url A URL to resolve real path.
  2144. * @param string $base_dir A base directory path to resolve relative URL.
  2145. * @param boolean $safety Deny to refer above of document root.
  2146. * @param string $domain Optinal. Domain part of URL.
  2147. * @param string $relative_path Relative path from document root.
  2148. */
  2149. public function url_to_path( $url, $base_dir = null, $safety = true, &$domain = null, &$relative_path = null ) {
  2150. // Strip params.
  2151. $url = FastPage_Util::strip_url_params($url);
  2152. $document_root = $this->document_root();
  2153. $path = null;
  2154. if ( substr( $url, 0, 1 ) == '/' && substr( $url, 1, 1 ) != '/' ) {
  2155. // In the case of absolute path.
  2156. $path = FastPage_Util::cat_path( $document_root, $url );
  2157. } else if ( preg_match( '!^(https?:)?//([^/]+)(/.+)!i', $url, $matches ) ) {
  2158. // In the case of absolute URL.
  2159. $path = array_pop($matches);
  2160. $domain = array_pop($matches);
  2161. $document_root = $this->document_root($domain);
  2162. if ( !$document_root ) return null;
  2163. $path = FastPage_Util::cat_path( $document_root, $path );
  2164. } else if ( isset($base_dir) ) {
  2165. // In the case of relative path.
  2166. $path = FastPage_Util::cat_path( $base_dir, $url );
  2167. }
  2168. $len = strlen($document_root);
  2169. if ( substr( $path, 0, $len ) == $document_root ) {
  2170. $relative_path = substr( $path, $len );
  2171. if ( substr( $relative_path, 0, 1 ) != DIRECTORY_SEPARATOR )
  2172. $relative_path = DIRECTORY_SEPARATOR . $relative_path;
  2173. } else if ( $safety ) {
  2174. // Never allow to refer above of document_root by security reason.
  2175. return null;
  2176. }
  2177. return $path;
  2178. }
  2179. /**
  2180. * Returns a new FastPage_Context instance.
  2181. *
  2182. * @return FastPage_Context A new instance.
  2183. */
  2184. public function new_context() {
  2185. $context = $this->helper_instance( 'context' );
  2186. // Run callbacks.
  2187. $this->run_callbacks( 'new_context', FastPage_Callback::all, $context );
  2188. $this->last_context = $context;
  2189. return $context;
  2190. }
  2191. /**
  2192. * Returns the last context.
  2193. *
  2194. * @return FastPage_Context The instance.
  2195. */
  2196. public function last_context() {
  2197. return $this->last_context;
  2198. }
  2199. /**
  2200. * Generate etag.
  2201. *
  2202. * A proxy to run generate_etag callbacks.
  2203. *
  2204. * @param mixed $context FastPage context object.
  2205. */
  2206. public function generate_etag( $context ) {
  2207. FastPage_Debug::log( $this, E_FP_PROFILE, "Generating etag" );
  2208. $result = $this->run_callbacks( 'generate_etag', FastPage_Callback::first_one, $context );
  2209. FastPage_Debug::log( $this, E_FP_PROFILE, "Generating etag", $result );
  2210. return $result;
  2211. }
  2212. /**
  2213. * Default callback to generate Etag.
  2214. */
  2215. public function cb_generate_etag( $callback, $context, $hint ) {
  2216. $fastpage = $callback->fastpage;
  2217. // Standard etag generator with md5 or sha1.
  2218. $algorithm = $fastpage->config('etag')->algorithm;
  2219. if ( !isset($algorithm) || $algorithm != 'sha1' )
  2220. $algorithm = 'md5';
  2221. // Etag partials.
  2222. $partials = array();
  2223. // Body source.
  2224. if( $context->response->body ) {
  2225. $partials []= $algorithm($context->response->body);
  2226. }
  2227. // Files reffered.
  2228. // Join to "path1:etag1;..pathN:etagN".
  2229. if ( $context->response->externals ) {
  2230. $file_etags = array();
  2231. foreach ( $context->response->externals->externals as $external ) {
  2232. if ( !$external->path ) continue;
  2233. if ( null !== ( $file_etag = FastPage_Util::file_etag($external->path) ) ) {
  2234. $file_etags []= $external->path . ':' . $file_etag;
  2235. }
  2236. }
  2237. if( count($file_etags) ) {
  2238. $joined = join( ';', $file_etags );
  2239. $partials []= $algorithm($joined);
  2240. }
  2241. }
  2242. $etag = join( '-', $partials );
  2243. return $etag;
  2244. }
  2245. /**
  2246. * Get original response.
  2247. *
  2248. * A proxy to run original_response callbacks.
  2249. *
  2250. * @param FastPage_Context $context FastPage request context.
  2251. */
  2252. public function original_response( $context ) {
  2253. FastPage_Debug::log( $this, E_FP_PROFILE, 'Original response' );
  2254. // Default response information.
  2255. $path = $context->request->path;
  2256. if ( $path ) {
  2257. $pi = pathinfo($path);
  2258. $ext = strtolower($pi['extension']);
  2259. $base_dir = $pi['dirname'];
  2260. $content_type = $this->mime_type($ext);
  2261. $context->response->merge_values( array(
  2262. 'path' => $path,
  2263. 'ext' => $ext,
  2264. 'base_dir' => $base_dir,
  2265. 'content_type' => $content_type
  2266. ) );
  2267. }
  2268. if ( $this->run_callbacks( 'original_response', FastPage_Callback::first_one, $context ) ) {
  2269. // Detect response charset.
  2270. $charset = $this->config()->content_charset;
  2271. if ( !$charset || strtolower($charset) == 'auto' ) {
  2272. if ( function_exists('mb_detect_encoding') ) {
  2273. $charset = mb_detect_encoding( $context->response->body );
  2274. } else {
  2275. $charset = 'UTF-8';
  2276. }
  2277. }
  2278. $context->response->charset = $charset;
  2279. // Normalize to UTF-8.
  2280. if ( strtoupper($charset) != 'UTF-8' && function_exists('mb_convert_encoding') ) {
  2281. $context->response->body = mb_convert_encoding( $context->response->body, 'UTF-8', $charset );
  2282. }
  2283. FastPage_Debug::log( $this, E_FP_PROFILE, 'Original response' );
  2284. return true;
  2285. }
  2286. FastPage_Debug::log( $this, E_FP_PROFILE, 'Original response' );
  2287. return null;
  2288. }
  2289. /**
  2290. * Default 'original_response' callback handler.
  2291. *
  2292. * @param FastPage_Context $context Request context.
  2293. */
  2294. public function cb_original_response( $callback, $context ) {
  2295. if ( !$path = $context->response->path ) return null;
  2296. // Expand the body source.
  2297. $content_type = $context->response->content_type;
  2298. if ( $content_type == 'text/html' || $content_type == 'text/css' ) {
  2299. $body = file_get_contents($path);
  2300. // Set response properties.
  2301. $context->response->merge_values( array(
  2302. 'body' => &$body,
  2303. ) );
  2304. return true;
  2305. }
  2306. return null;
  2307. }
  2308. /**
  2309. * Convert external file reference.
  2310. *
  2311. * A proxy to run generate_etag callbacks.
  2312. *
  2313. * @param mixed $context FastPage context object.
  2314. */
  2315. public function convert_externals( $context ) {
  2316. FastPage_Debug::log( $this, E_FP_PROFILE, 'Convert externals' );
  2317. $result = $this->run_callbacks( 'convert_externals', FastPage_Callback::all, $context );
  2318. FastPage_Debug::log( $this, E_FP_PROFILE, 'Convert externals' );
  2319. return $result;
  2320. }
  2321. /**
  2322. * Convert externals.
  2323. *
  2324. * @param FastPage_Externals $externals External file references.
  2325. */
  2326. public function cb_convert_externals( $callback, $context, $hint ) {
  2327. $fastpage = $callback->fastpage;
  2328. $externals = $context->response->externals;
  2329. if ( !$externals ) return;
  2330. // Config.
  2331. $config = $this->config();
  2332. $data_uri_config = $fastpage->config('data_uri');
  2333. $inline_js_config = $fastpage->config('inline_js');
  2334. $cache = $fastpage->cache();
  2335. foreach ( $externals->externals as $index => $external ) {
  2336. if ( $external->is_image() ) {
  2337. // Convert image to Data_URI.
  2338. if ( !$context->user_agent->data_uri_enabled ) continue;
  2339. // Skip duplicated image.
  2340. if ( $data_uri_config->max_redundancy && $external->redundancy && $external->redundancy > $data_uri_config->max_redundancy ) {
  2341. continue;
  2342. }
  2343. // Check file size.
  2344. $size = filesize($external->path);
  2345. if ( !$size ) continue;
  2346. if ( $data_uri_config->max_file_size && $size > $data_uri_config->max_file_size ) {
  2347. continue;
  2348. }
  2349. // Replace to data uri.
  2350. if ( $data_uri = $external->data_uri() ) {
  2351. $external->replace_type = 'url';
  2352. $external->replace = $data_uri;
  2353. }
  2354. } else if ( $external->is_js() ) {
  2355. // Convert JavaScript.
  2356. // Check file size.
  2357. $size = filesize($external->path);
  2358. if ( !$size ) continue;
  2359. if ( $size && $size > $inline_js_config->max_file_size ) {
  2360. continue;
  2361. }
  2362. // Make JavaScript inline.
  2363. if ( false !== ( $contents = file_get_contents($external->path) ) ) {
  2364. $external->replace_type = 'inline';
  2365. $external->replace = $contents;
  2366. }
  2367. }
  2368. }
  2369. }
  2370. /**
  2371. * A proxy to run convert_* callbacks.
  2372. */
  2373. public function convert_body( $context ) {
  2374. FastPage_Debug::log( $this, E_FP_PROFILE, 'Convert body' );
  2375. // Extension.
  2376. if ( !($ext = $context->response->ext ) ) {
  2377. return null;
  2378. }
  2379. $event = 'convert_body.' . $ext;
  2380. if ( $this->run_callbacks( $event, FastPage_Callback::first_one, $context ) ) {
  2381. // Convert encoding.
  2382. if ( function_exists('mb_convert_encoding') ) {
  2383. FastPage_Debug::log( $this, E_FP_PROFILE, 'Convert encoding' );
  2384. $charset = isset($context->charset)? $context->charset: $context->response->charset;
  2385. if ( strtoupper($charset) != 'UTF-8' ) {
  2386. $context->response->body = mb_convert_encoding( $context->response->body, $charset, 'UTF-8' );
  2387. $context->response->charset = $charset;
  2388. }
  2389. FastPage_Debug::log( $this, E_FP_PROFILE, 'Convert encoding' );
  2390. }
  2391. $context->response->content_length = strlen($context->response->body);
  2392. }
  2393. FastPage_Debug::log( $this, E_FP_PROFILE, 'Convert body' );
  2394. }
  2395. /**
  2396. * Default callback to convert CSS body.
  2397. */
  2398. public function cb_convert_css( $callback, $context, $hint ) {
  2399. $fastpage = $callback->fastpage;
  2400. FastPage_Debug::log( $this, E_FP_PROFILE, 'Parse CSS source' );
  2401. // Tokenize.
  2402. FastPage_Debug::log( $this, E_FP_PROFILE, 'Tokenize' );
  2403. $tokens = preg_split( '/(url\([^\)]+\))/i', $context->response->body, null, PREG_SPLIT_DELIM_CAPTURE );
  2404. $tokens_count = count($tokens);
  2405. FastPage_Debug::log( $this, E_FP_PROFILE, 'Tokenize' );
  2406. // Enum urls.
  2407. $urls = array();
  2408. for( $i = 1; $i < $tokens_count; $i += 2 ) {
  2409. $url = substr( $tokens[$i], 4, -1 ); // Strip url(...)
  2410. $url = FastPage_Util::strip_quotes($url); // Strip quotes.
  2411. $urls[$i] = $url;
  2412. }
  2413. FastPage_Debug::log( $this, E_FP_PROFILE, 'Parse CSS source' );
  2414. // Make external references collection.
  2415. $context->response->externals = $fastpage->helper_instance( 'externals', false, $context, $urls );
  2416. // Etag matching.
  2417. if ( $context->match_etag() ) return true;
  2418. // Replace tokens.
  2419. $fastpage->convert_externals($context);
  2420. FastPage_Debug::log( $this, E_FP_PROFILE, 'Rebuild CSS source' );
  2421. $externals = $context->response->externals;
  2422. foreach ( $externals->externals as $index => $external ) {
  2423. if ( $external->replace_type == 'url' && $external->replace ) {
  2424. $tokens[$index] = 'url("' . $external->replace . '")';
  2425. }
  2426. // Free objects.
  2427. unset($external);
  2428. $externals->free($index);
  2429. }
  2430. // Build body.
  2431. $context->response->body = join( '', $tokens );
  2432. unset($tokens);
  2433. FastPage_Debug::log( $this, E_FP_PROFILE, 'Rebuild CSS source' );
  2434. return true;
  2435. }
  2436. /**
  2437. * Default callback to convert HTML body.
  2438. */
  2439. public function cb_convert_html( $callback, $context, $hint ) {
  2440. $fastpage = $callback->fastpage;
  2441. FastPage_Debug::log( $this, E_FP_PROFILE, 'Parse HTML source' );
  2442. // Tokenize.
  2443. FastPage_Debug::log( $this, E_FP_PROFILE, 'Tokenize' );
  2444. $tokens = preg_split('!(</?(?:img|script)\s?[^>]*>)!i', $context->response->body, null, PREG_SPLIT_DELIM_CAPTURE);
  2445. $tokens_count = count($tokens);
  2446. FastPage_Debug::log( $this, E_FP_PROFILE, 'Tokenize' );
  2447. // Enum URLs.
  2448. $elements = array();
  2449. $urls = array();
  2450. for ( $i = 0; $i < $tokens_count; $i++ ) {
  2451. if ( substr( $tokens[$i], 0, 4 ) == '<img' || substr( $tokens[$i], 0, 7 ) == '<script' ) {
  2452. $element = $fastpage->parse_html_element($tokens[$i]);
  2453. if ( !isset($element) ) continue;
  2454. // Src attribute
  2455. $src = $element->attribute('src');
  2456. if ( !isset($src) ) continue;
  2457. $elements[$i] = $element;
  2458. $urls[$i] = $src;
  2459. }
  2460. }
  2461. FastPage_Debug::log( $this, E_FP_PROFILE, 'Parse HTML source' );
  2462. // Make external references collections.
  2463. $context->response->externals = $fastpage->helper_instance( 'externals', false, $context, $urls );
  2464. // Etag matching.
  2465. if ( $context->match_etag() ) return true;
  2466. // Replace tokens.
  2467. $fastpage->convert_externals($context);
  2468. FastPage_Debug::log( $this, E_FP_PROFILE, 'Rebuild HTML source' );
  2469. $externals = $context->response->externals;
  2470. foreach ( $externals->externals as $index => $external ) {
  2471. // Need to replace?
  2472. if ( !$external->replace || !$external->replace_type ) continue;
  2473. // HTML element.
  2474. $element = $elements[$index];
  2475. if ( !$element ) continue;
  2476. if ( $element->tag_name == 'img' ) {
  2477. // IMG element.
  2478. if ( $external->replace_type == 'url' ) {
  2479. // Replace src attribute to converted.
  2480. $element->attribute( 'src', $external->replace );
  2481. $tokens[$index] = $element->html;
  2482. }
  2483. } else if ( $element->tag_name == 'script' ) {
  2484. // SCRIPT element.
  2485. if ( $external->replace_type == 'inline' ) {
  2486. // Make script inline.
  2487. $element->remove_attribute('src');
  2488. $tokens[$index] = $element->html . "\n" .$external->replace . "\n";
  2489. } else if ( $external->replace_type == 'url') {
  2490. // Replace src attribute to converted.
  2491. $element->attribute( 'src', $external->replace );
  2492. $tokens[$index] = $element->html;
  2493. }
  2494. }
  2495. // Free objects.
  2496. unset($external);
  2497. $externals->free($index);
  2498. unset($element);
  2499. unset($elements[$index]);
  2500. }
  2501. // Build body.
  2502. $context->response->body = join( '', $tokens );
  2503. unset($tokens);
  2504. FastPage_Debug::log( $this, E_FP_PROFILE, 'Rebuild HTML source' );
  2505. return true;
  2506. }
  2507. }
  2508. /**
  2509. * FastPage http exception.
  2510. */
  2511. class FastPage_HTTPException extends FastPage_Exception {
  2512. public $fastpage;
  2513. public $code;
  2514. public $level;
  2515. public $message;
  2516. /**
  2517. * HTTP response code prefix.
  2518. */
  2519. public static $response_code_prefix = 'HTTP/1.1';
  2520. /**
  2521. * HTTP response code table.
  2522. */
  2523. public static $response_codes = array(
  2524. 200 => 'OK',
  2525. 304 => 'Not Modified',
  2526. 403 => 'Forbidden',
  2527. 404 => 'Not Found',
  2528. 501 => 'Not Implemented',
  2529. 505 => 'Internal Server Error',
  2530. );
  2531. /**
  2532. * Constructor.
  2533. *
  2534. * Log message and keep http response code.
  2535. *
  2536. * @param object $fastpage FastPage instance.
  2537. * @param mixed $code HTTP response code as integer or CODE MESSAGE as string.
  2538. * @param integer $level Log level.
  2539. * @param string $message Log message.
  2540. */
  2541. public function __construct( $fastpage, $code, $level = 0, $message = '') {
  2542. $this->fastpage = $fastpage;
  2543. $this->code = $code;
  2544. $this->level = $level;
  2545. $this->message = $message;
  2546. // Add log.
  2547. if ( $message ) {
  2548. FastPage_Debug::log( $fastpage, $level, $message );
  2549. }
  2550. }
  2551. /**
  2552. * Send HTTP response code.
  2553. */
  2554. public function response_code() {
  2555. // Output response code header.
  2556. $prefix = self::$response_code_prefix;
  2557. $code = $this->code;
  2558. $error = isset(self::$response_codes[$code])? self::$response_codes[$code]: '';
  2559. if ( isset($error) ) {
  2560. return "$prefix $code $error";
  2561. } else {
  2562. return "$prefix $code";
  2563. }
  2564. }
  2565. }
  2566. /**
  2567. * FastPage application wrapper.
  2568. *
  2569. * @package FastPage
  2570. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  2571. */
  2572. class FastPage_App extends FastPage {
  2573. protected static $instance;
  2574. public function __construct() {
  2575. parent::__construct();
  2576. // Default config.
  2577. $config = $this->config();
  2578. $config->default_host = FastPage_Util::server('HTTP_HOST');
  2579. $config->default_document_root = FastPage_Util::server('DOCUMENT_ROOT');
  2580. $config->directory_index = array('index.html');
  2581. $config->sub('tasks')->regular_probability = 1000;
  2582. // Built-in callbacks.
  2583. $this->add_callback( 'regular_tasks', 5, array( $this, 'cb_regular_tasks' ) );
  2584. }
  2585. /**
  2586. * Callback handler of default regular_tasks.
  2587. */
  2588. public function cb_regular_tasks( $callback ) {
  2589. $fastpage = $callback->fastpage;
  2590. // Purge cache.
  2591. FastPage_Debug::log( $this, E_FP_PROFILE, 'Purge cache' );
  2592. $cache = $fastpage->cache();
  2593. $cache->purge();
  2594. FastPage_Debug::log( $this, E_FP_PROFILE, 'Purge cache' );
  2595. return true;
  2596. }
  2597. /**
  2598. * Get singleton instance.
  2599. *
  2600. * @return FastPage_App Singleton instance.
  2601. */
  2602. public function instance() {
  2603. return self::$instance;
  2604. }
  2605. /**
  2606. * Prepare request.
  2607. *
  2608. * Must be overridden.
  2609. *
  2610. * @param FastPage_Context $context Context object.
  2611. */
  2612. public function prepare_request( $context ) { }
  2613. /*
  2614. * Output headers.
  2615. */
  2616. public function output_headers( $response_code, $context ) {
  2617. FastPage_Debug::log( $this, E_FP_PROFILE, 'Output Headers' );
  2618. // Run callback.
  2619. $this->run_callbacks( 'output_headers', FastPage_Callback::all, $context );
  2620. // Response code.
  2621. if ( $response_code ) {
  2622. header( $response_code );
  2623. }
  2624. // Output headers.
  2625. $headers = $context->response->build_headers();
  2626. $headers['X-FastPage'] = FASTPAGE_VERSION;
  2627. foreach ( $headers as $name => $value ) {
  2628. $value = preg_replace( '/[\n\r]+/', ' ', $value );
  2629. header("$name: $value");
  2630. }
  2631. FastPage_Debug::log( $this, E_FP_PROFILE, 'Output Headers' );
  2632. }
  2633. /**
  2634. * Output body.
  2635. */
  2636. public function output_body( $context ) {
  2637. FastPage_Debug::log( $this, E_FP_PROFILE, 'Output Body' );
  2638. // Run callback.
  2639. $this->run_callbacks( 'output_body', FastPage_Callback::all, $context );
  2640. if ( $context->response->body ) {
  2641. ob_start();
  2642. echo $context->response->body;
  2643. }
  2644. FastPage_Debug::log( $this, E_FP_PROFILE, 'Output Body' );
  2645. }
  2646. /*
  2647. * Run process.
  2648. */
  2649. public function run() {
  2650. FastPage_Debug::log( $this, E_FP_PROFILE, 'Run app' );
  2651. // Create a new context and start process.
  2652. $context = $this->new_context();
  2653. // Server cache frequency.
  2654. $frequency = $this->config('server_cache')->frequency;
  2655. $method_is_get = $context->request->method == 'GET'? true: false;
  2656. try {
  2657. // Prepare request.
  2658. $this->prepare_request($context);
  2659. if ( !$context->request->url ) {
  2660. throw new FastPage_HTTPException( $this, 404, E_USER_ERROR, 'Cannot get target url.' );
  2661. }
  2662. // Etag.
  2663. if ( $context->requires_etag && $etag = FastPage_Util::server('HTTP_IF_NONE_MATCH') ) {
  2664. $context->request->etag = FastPage_Util::strip_quotes($etag);
  2665. }
  2666. // Check server cache.
  2667. $hit_server_cache = false;
  2668. $cache = $this->cache();
  2669. if ( $frequency && $method_is_get ) {
  2670. // Make cache keys array.
  2671. $context->request->server_cache_keys = array(
  2672. 'URL' => $context->request->url,
  2673. '_GET' => &$_GET,
  2674. '_COOKIE' => &$_COOKIE,
  2675. '_SESSION' => &$_SESSION
  2676. );
  2677. $this->run_callbacks( 'server_cache_keys', FastPage_Callback::all, $context );
  2678. $key = $context->request->server_cache_key = md5( serialize($context->request->server_cache_keys) );
  2679. // Search cache key.
  2680. if ( $cached = $cache->get( 'server_cache', $key ) ) {
  2681. // Cache enabled and cached context exists.
  2682. $response = @unserialize($cached);
  2683. if ( $response ) {
  2684. FastPage_Debug::log( $this, E_USER_NOTICE, 'URL cache hit.' );
  2685. $context->response = $response;
  2686. $hit_server_cache = true;
  2687. }
  2688. }
  2689. }
  2690. if ( !$hit_server_cache ) {
  2691. // Start normal flow.
  2692. // Get original response.
  2693. $result = $this->original_response($context);
  2694. if ( !$result ) {
  2695. // Can't get original response.
  2696. throw new FastPage_HTTPException( $this, 404, E_USER_ERROR, 'Cannot get original response.' );
  2697. }
  2698. // Pass through?
  2699. if ( $context->pass_through ) {
  2700. throw new FastPage_HTTPException( $this, 200, E_USER_NOTICE, 'Pass through the request.' );
  2701. }
  2702. // Convert the source.
  2703. $result = $this->convert_body($context);
  2704. }
  2705. // Not modified?
  2706. if ( $context->match_etag() ) {
  2707. // Not modified, clear body not to send.
  2708. $context->response->body = '';
  2709. throw new FastPage_HTTPException( $this, 304, E_USER_NOTICE, 'Not modified.' );
  2710. }
  2711. // Output body.
  2712. // FIXME: This may not be smart...
  2713. throw new FastPage_HTTPException( $this, 200 );
  2714. } catch ( FastPage_HTTPException $ex ) {
  2715. // Output HTTP response.
  2716. $response_code = $ex->response_code();
  2717. // Cache the result.
  2718. if ( $frequency && $method_is_get && !$hit_server_cache && $response_code == 200 ) {
  2719. $now = time();
  2720. $expire = (int)( ceil( $now / $frequency ) * $frequency ) - $now;
  2721. $caching = serialize($context->response);
  2722. $cache->set( 'server_cache', $context->request->server_cache_key, $caching, $expire );
  2723. FastPage_Debug::log( $this, E_USER_NOTICE, 'Server cache stored. It will expire after ' . $expire . ' seconds.' );
  2724. }
  2725. $this->output_headers( $response_code, $context );
  2726. $this->output_body( $context );
  2727. }
  2728. // $this->add_onetime_task( 'dummy_task', array( $this, 'dummy_task' ) );
  2729. // Run background tasks.
  2730. $this->run_background_tasks();
  2731. FastPage_Debug::log( $this, E_FP_PROFILE, 'Run app' );
  2732. }
  2733. /**
  2734. * Run background tasks.
  2735. */
  2736. public function run_background_tasks() {
  2737. // Onetime tasks.
  2738. if ( $this->has_onetime_tasks() ) {
  2739. // Fork tasks.
  2740. FastPage_Debug::log( $this, E_FP_PROFILE, 'Fork onetime tasks' );
  2741. $key = $this->save_onetime_tasks();
  2742. if ( !$this->fork_tasks( array( 'onetime_tasks' => $key ) ) ) {
  2743. // Failed to fork, run at here.
  2744. FastPage_Debug::log( $this, E_USER_WARNING, 'Forking onetime tasks failed.' );
  2745. $this->run_onetime_tasks();
  2746. }
  2747. FastPage_Debug::log( $this, E_FP_PROFILE, 'Fork onetime tasks' );
  2748. }
  2749. // Regular tasks.
  2750. if ( $prob = $this->config('tasks')->regular_probability ) {
  2751. // If random value match 1, fork regular tasks.
  2752. srand( (int)microtime(true) );
  2753. if ( 1 == rand( 1, $prob ) ) {
  2754. FastPage_Debug::log( $this, E_FP_PROFILE, 'Fork regular tasks' );
  2755. if ( !$this->fork_tasks() ) {
  2756. // Failed to fork, run at here.
  2757. FastPage_Debug::log( $this, E_USER_WARNING, 'Forking regular tasks failed.' );
  2758. $this->run_regular_tasks();
  2759. }
  2760. FastPage_Debug::log( $this, E_FP_PROFILE, 'Fork regular tasks' );
  2761. }
  2762. }
  2763. }
  2764. /**
  2765. * Forking URL.
  2766. *
  2767. * @param string $script Script name.
  2768. * @param array $param URL parameters as array.
  2769. * @param string $param_prefix Prefix to pass http_build_query.
  2770. */
  2771. public function fork_url( $script, $param = array(), $param_prefix = '' ) {
  2772. // Get base from config.
  2773. $url_base = $this->config()->fork_url_base;
  2774. // Detect URL if not defined.
  2775. if ( !$url_base ) {
  2776. $name = FastPage_Util::server('SCRIPT_NAME');
  2777. $host = FastPage_Util::server('HTTP_HOST');
  2778. $script_uri = "http://$host$name";
  2779. $url_base = preg_replace( '!/[^/]*$!', '/', $script_uri );
  2780. }
  2781. // Concat script.
  2782. $url = FastPage_Util::cat_path( $url_base, $script );
  2783. // Add param.
  2784. if ( count($param) ) {
  2785. $url .= '?' . http_build_query( $param, $param_prefix, '&' );
  2786. }
  2787. return $url;
  2788. }
  2789. /**
  2790. * Forking tasks via HTTP getting task.php.
  2791. *
  2792. * @param array $param Parameters to pass task.php.
  2793. * @return boolean False if failed forking.
  2794. */
  2795. public function fork_tasks( $param = array() ) {
  2796. $result = true;
  2797. // Build url.
  2798. if ( $debug = FastPage_Debug::instance() ) {
  2799. $param['start'] = $debug->start;
  2800. }
  2801. $url = $this->fork_url( 'task.php', $param );
  2802. // Fork tasks via HTTP.
  2803. if ( $url ) {
  2804. $http = $this->simple_http();
  2805. $response = $http->get($url);
  2806. if ( !isset($response) ) {
  2807. // Failed to get.
  2808. $result = false;
  2809. } else if ( $response != 'OK' ) {
  2810. $result = @unserialize($response);
  2811. // Append logs.
  2812. if ( is_array($result) ) {
  2813. // Merge debug logs.
  2814. if ( $debug = FastPage_Debug::instance() ) {
  2815. if ( isset($result['debug']) && is_a( $result['debug'], 'FastPage_Debug' ) ) {
  2816. $debug->merge( $result['debug'] );
  2817. }
  2818. }
  2819. } else {
  2820. // Some error occured, print raw response.
  2821. echo $response;
  2822. }
  2823. }
  2824. } else {
  2825. FastPage_Debug::log( $this, E_USER_ERROR, 'Failure to get task forking URL.' );
  2826. $result = false;
  2827. }
  2828. return $result;
  2829. }
  2830. /**
  2831. * Bootstrap of FastPage_App.
  2832. *
  2833. * @param string $class The child class name of FastPage_App.
  2834. * @param mixed $config_path Optionsl. Config file path to load. Auto detection by default. Set false if no need to load config file.
  2835. * @return FastPage_Rewrite
  2836. */
  2837. static public function boot( $class, $config_path = null ) {
  2838. FastPage_Debug::log( null, E_FP_PROFILE, 'Bootstrap' );
  2839. // Create an instance.
  2840. if ( !class_exists($class) ) {
  2841. FastPage_Debug::log( null, E_USER_ERROR, 'Class: ' . $class . ' does not found.' );
  2842. header( 'HTTP/1.1 501 Not Implemented: ' . $class );
  2843. return;
  2844. }
  2845. FastPage_Debug::log( null, E_USER_NOTICE, 'Boot up: ' . $class );
  2846. $fastpage = self::$instance = new $class();
  2847. // Load config file.
  2848. $fastpage->load_config_file( $config_path );
  2849. // Check IP addres if be debuggable.
  2850. if ( $ips = $fastpage->config('debug')->allow_ips ) {
  2851. $remote = FastPage_Util::server('REMOTE_ADDR');
  2852. if ( !FastPage_Util::match_ips( $ips, $remote, true ) ) {
  2853. // Cancel debugging.
  2854. FastPage_Debug::$instance = null;
  2855. }
  2856. }
  2857. // Run the instance.
  2858. // * Good to loop this in service type process.
  2859. $fastpage->run();
  2860. FastPage_Debug::log( $fastpage, E_FP_PROFILE, 'Bootstrap' );
  2861. $fastpage->run_callbacks( 'app_finished', FastPage_Callback::all );
  2862. return;
  2863. }
  2864. }
  2865. /**
  2866. * Rewite application for mod_rewite.
  2867. *
  2868. * @package FastPage
  2869. * @author Kunihiko Miyanaga <miyanaga@ideamans.com>
  2870. * @var mixed request_url_var Server variable name to get URL.
  2871. */
  2872. class FastPage_App_Rewrite extends FastPage_App {
  2873. /**
  2874. * Override prepare_request method.
  2875. */
  2876. public function prepare_request( $context ) {
  2877. // Host and method.
  2878. $context->request->method = FastPage_Util::server('REQUEST_METHOD');
  2879. $context->request->host = FastPage_Util::server('HTTP_HOST');
  2880. // Target URL.
  2881. $url = FastPage_Util::strip_url_params( FastPage_Util::server('REQUEST_URI') );
  2882. $path = $this->url_to_path( $url );
  2883. if ( is_dir($path) ) {
  2884. // Search directory index.
  2885. foreach ( $this->config()->directory_index as $index ) {
  2886. $index_path = FastPage_Util::cat_path( $path, $index );
  2887. if ( is_file($index_path) ) {
  2888. $url = FastPage_Util::cat_path( $url, $index );
  2889. $path = $index_path;
  2890. break;
  2891. }
  2892. }
  2893. }
  2894. if ( !is_file($path) ) {
  2895. // Not found.
  2896. return;
  2897. }
  2898. $context->request->url = $url;
  2899. $context->request->path = $path;
  2900. }
  2901. }
  2902. /**
  2903. * Task application.
  2904. *
  2905. * @package FastPage
  2906. * @author Kunihiko Miyanaga
  2907. */
  2908. class FastPage_App_Task extends FastPage_App {
  2909. /**
  2910. * Error handler to redirect to FastPage_Debug.
  2911. */
  2912. public function error_handler( $errno, $errstr, $errfile, $errline ) {
  2913. $message = $errstr;
  2914. if ( $errfile && $errline ) {
  2915. $message .= "($errfile:$errline)";
  2916. }
  2917. FastPage_Debug::log( $this, $errno, $message );
  2918. return true;
  2919. }
  2920. /**
  2921. * Run background tasks.
  2922. */
  2923. public function run() {
  2924. // Set error handler to redirect to FastPage_Debug.
  2925. set_error_handler( array( $this, 'error_handler' ) );
  2926. $denied = false;
  2927. // Check IP.
  2928. if ( $ips = $this->config('tasks')->allow_ips ) {
  2929. $remote = FastPage_Util::server('REMOTE_ADDR');
  2930. if ( !FastPage_Util::match_ips( $ips, $remote, true ) ) {
  2931. $denied = true;
  2932. }
  2933. }
  2934. // Exit if denied.
  2935. if ( $denied ) {
  2936. header('HTTP/1.1 403 Forbidden');
  2937. return;
  2938. }
  2939. // If not in debug mode, try to close client connection immediately.
  2940. if ( !FastPage_Debug::instance() ) {
  2941. $response = 'OK';
  2942. ignore_user_abort(true);
  2943. header('Content-Type: application/x-fastpage');
  2944. header('Connection: close');
  2945. header('Content-Length: ' . strlen($response));
  2946. echo $response;
  2947. while( @ob_end_flush() );
  2948. flush();
  2949. }
  2950. // Run tasks.
  2951. ob_start();
  2952. if ( isset($_GET['onetime_tasks']) ) {
  2953. // Restore and run tasks.
  2954. $key = $_GET['onetime_tasks'];
  2955. $this->load_onetime_tasks($key);
  2956. $this->run_onetime_tasks();
  2957. } else {
  2958. // Run regular tasks.
  2959. $this->run_regular_tasks();
  2960. }
  2961. ob_end_clean();
  2962. // If debug mode, output serialized debug object.
  2963. if ( $debug = FastPage_Debug::instance() ) {
  2964. $response = serialize( array( 'debug' => $debug ) );
  2965. echo $response;
  2966. }
  2967. }
  2968. }