PageRenderTime 73ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/gitblog.php

http://github.com/rsms/gitblog
PHP | 3690 lines | 2795 code | 463 blank | 432 comment | 463 complexity | 35d614f6e11a05757beeebfdfbce7c9c MD5 | raw file
  1. <?
  2. error_reporting(E_ALL);
  3. $gb_time_started = microtime(true);
  4. date_default_timezone_set(@date_default_timezone_get());
  5. /**
  6. * Configuration.
  7. *
  8. * These values can be overridden in gb-config.php (or somewhere else for that matter).
  9. */
  10. class gb {
  11. /** URL prefix for tags */
  12. static public $tags_prefix = 'tags/';
  13. /** URL prefix for categories */
  14. static public $categories_prefix = 'category/';
  15. /** URL prefix for the feed */
  16. static public $feed_prefix = 'feed';
  17. /**
  18. * URL prefix (strftime pattern).
  19. * Need to specify at least year and month. Day, time and so on is optional.
  20. * Changing this parameter does not affect the cache.
  21. */
  22. static public $posts_prefix = '%Y/%m/';
  23. /** URL prefix for pages */
  24. static public $pages_prefix = '';
  25. /** Number of posts per page. */
  26. static public $posts_pagesize = 10;
  27. /** Enables fuzzy URI matching of posts */
  28. static public $posts_fuzzy_lookup = true;
  29. /** URL to gitblog index _relative_ to gb::$site_url */
  30. static public $index_prefix = 'index.php';
  31. /** 'PATH_INFO' or any other string which will then be matched in $_GET[string] */
  32. static public $request_query = 'PATH_INFO';
  33. /**
  34. * When this query string key is set and the client is authorized,
  35. * the same effect as setting $version_query_key to "work" is achieved.
  36. */
  37. static public $preview_query_key = 'preview';
  38. /**
  39. * When this query string key is set and the client is authorized, the
  40. * specified version of a viewed post is displayed rather than the live
  41. * version.
  42. */
  43. static public $version_query_key = 'version';
  44. /**
  45. * When this query string key is set and gb::$is_preview is true, the
  46. * object specified by pathspec is loaded. This overrides parsing the URI
  47. * and is needed in cases where there are multiple posts with the same
  48. * name but with different file extensions (content types).
  49. */
  50. static public $pathspec_query_key = 'pathspec';
  51. /**
  52. * Log messages of priority >=$log_filter will be sent to syslog.
  53. * Disable logging by setting this to -1.
  54. * See the "Logging" section in gitblog.php for more information.
  55. */
  56. static public $log_filter = LOG_NOTICE;
  57. # --------------------------------------------------------------------------
  58. # The following are by default set in the gb-config.php file.
  59. # See gb-config.php for detailed documentation.
  60. /** Site title */
  61. static public $site_title = null;
  62. /** Site description */
  63. static public $site_description =
  64. 'Change this fancy description by editing gb::$site_description in gb-config.php';
  65. /** Shared secret */
  66. static public $secret = '';
  67. # --------------------------------------------------------------------------
  68. # Constants
  69. static public $version = '0.1.6';
  70. /** Absolute path to the gitblog directory */
  71. static public $dir;
  72. /** Absolute path to the site root */
  73. static public $site_dir;
  74. /** Absolute URL to the site root, not including gb::$index_prefix */
  75. static public $site_url;
  76. /** Absolute URL path (i.e. starts with a slash) to the site root */
  77. static public $site_path;
  78. /** Absolute path to current theme. Available when running a theme. */
  79. static public $theme_dir;
  80. /** Absolute URL to current theme. Available when running a theme. */
  81. static public $theme_url;
  82. static public $content_cache_fnext = '.content';
  83. static public $comments_cache_fnext = '.comments';
  84. static public $index_cache_fnext = '.index';
  85. /**
  86. * The strftime pattern used to build posts cachename.
  87. *
  88. * The granularity of this date is the "bottleneck", or "limiter", for
  89. * $posts_prefix. If you specify "%Y", $posts_prefix can define patterns with
  90. * granularity ranging from year to second. But if you set this parameter to
  91. * "%Y/%m/%d-" the minimum granularity of $posts_prefix goes up to day, which
  92. * means that this: $posts_prefix = '%Y/%m/' will not work, as day is
  93. * missing. However this: $posts_prefix = '%y-%m-%e/' and
  94. * $posts_prefix = '%y/%m/%e/%H/%M/' works fine, as they both have a
  95. * granularity of one day or more.
  96. *
  97. * It's recommended not to alter this value. The only viable case where
  98. * altering this is if you are posting many many posts every day, thus adding
  99. * day ($posts_cn_pattern = '%Y/%m/%d-') would give a slight file system
  100. * performance improvement on most file systems.
  101. */
  102. static public $posts_cn_pattern = '%Y/%m-';
  103. # --------------------------------------------------------------------------
  104. # The following are used at runtime.
  105. static public $title;
  106. static public $is_404 = false;
  107. static public $is_page = false;
  108. static public $is_post = false;
  109. static public $is_posts = false;
  110. static public $is_search = false;
  111. static public $is_tags = false;
  112. static public $is_categories = false;
  113. static public $is_feed = false;
  114. /**
  115. * Preview mode -- work content is loaded rather than live versions.
  116. *
  117. * This is automatically set to true by the request handler (end of this
  118. * file) when all of the following are true:
  119. *
  120. * - gb::$preview_query_key is set in the query string (i.e. "?preview")
  121. * - Client is authorized (gb::$authorized is non-false)
  122. */
  123. static public $is_preview = false;
  124. /**
  125. * A universal list of error messages (simple strings) which occured during
  126. * the current request handling.
  127. *
  128. * Themes should take care of this and display these error messages where
  129. * appropriate.
  130. */
  131. static public $errors = array();
  132. /** True if some part of gitblog (inside the gitblog directory) is the initial invoker */
  133. static public $is_internal_call = false;
  134. /** Contains the site.json structure or null if not loaded */
  135. static public $site_state = null;
  136. # --------------------------------------------------------------------------
  137. # Logging
  138. static public $log_open = false;
  139. static public $log_cb = null;
  140. /**
  141. * Send a message to syslog.
  142. *
  143. * INT CONSTANT DESCRIPTION
  144. * ---- ----------- ----------------------------------
  145. * 0 LOG_EMERG system is unusable
  146. * 1 LOG_ALERT action must be taken immediately
  147. * 2 LOG_CRIT critical conditions
  148. * 3 LOG_ERR error conditions
  149. * 4 LOG_WARNING warning conditions
  150. * 5 LOG_NOTICE normal, but significant, condition
  151. * 6 LOG_INFO informational message
  152. * 7 LOG_DEBUG debug-level message
  153. */
  154. static function log(/* [$priority,] $fmt [mixed ..] */) {
  155. $vargs = func_get_args();
  156. $priority = count($vargs) === 1 || !is_int($vargs[0]) ? LOG_NOTICE : array_shift($vargs);
  157. return self::vlog($priority, $vargs);
  158. }
  159. static function vlog($priority, $vargs, $btoffset=1, $prefix=null) {
  160. if ($priority > self::$log_filter)
  161. return true;
  162. if ($prefix === null) {
  163. $bt = debug_backtrace();
  164. while (!isset($bt[$btoffset]) && $btoffset >= 0)
  165. $btoffset--;
  166. $bt = isset($bt[$btoffset]) ? $bt[$btoffset] : $bt[$btoffset-1];
  167. $prefix = '['.(isset($bt['file']) ? gb_relpath(gb::$site_dir, $bt['file']).':'.$bt['line'] : '?').'] ';
  168. }
  169. $msg = $prefix;
  170. if(count($vargs) > 1) {
  171. $fmt = array_shift($vargs);
  172. $msg .= vsprintf($fmt, $vargs);
  173. }
  174. elseif ($vargs) {
  175. $msg .= $vargs[0];
  176. }
  177. if (!self::$log_open && !self::openlog() && $priority < LOG_WARNING) {
  178. trigger_error($msg, E_USER_ERROR);
  179. return $msg;
  180. }
  181. if (self::$log_cb) {
  182. $fnc = self::$log_cb;
  183. $fnc($priority, $msg);
  184. }
  185. if (syslog($priority, $msg))
  186. return $msg;
  187. return error_log($msg, 4) ? $msg : false;
  188. }
  189. static function openlog($ident=null, $options=LOG_PID, $facility=LOG_USER) {
  190. if ($ident === null) {
  191. $u = parse_url(gb::$site_url);
  192. $ident = 'gitblog.'.isset($u['host']) ? $u['host'] .'.' : '';
  193. if (isset($u['path']))
  194. $ident .= str_replace('/', '.', trim($u['path'],'/'));
  195. }
  196. self::$log_open = openlog($ident, $options, $facility);
  197. return self::$log_open;
  198. }
  199. # --------------------------------------------------------------------------
  200. # Info about the Request
  201. static protected $current_url = null;
  202. static function url_to($part=null, $htmlsafe=true) {
  203. $s = gb::$site_url.self::$index_prefix;
  204. if ($part) {
  205. if ($part{0} === '/') {
  206. $s .= strlen($part) > 1 ? substr($part, 1) : '';
  207. }
  208. else {
  209. $v = $part.'_prefix';
  210. $s .= self::$$v;
  211. }
  212. }
  213. return $htmlsafe ? h($s) : $s;
  214. }
  215. static function url() {
  216. if (self::$current_url === null) {
  217. $u = new GBURL();
  218. $u->secure = isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on');
  219. $u->scheme = $u->secure ? 'https' : 'http';
  220. $u->host = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] :
  221. (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
  222. if(($p = strpos($u->host,':')) !== false) {
  223. $u->port = intval(substr($u->host, $p+1));
  224. $u->host = substr($u->host, 0, $p);
  225. }
  226. elseif(isset($_SERVER['SERVER_PORT'])) {
  227. $u->port = intval($_SERVER['SERVER_PORT']);
  228. }
  229. else {
  230. $u->port = $u->secure ? 443 : 80;
  231. }
  232. $u->query = $_GET;
  233. $u->path = $u->query ? substr(@$_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'],'?'))
  234. : rtrim(@$_SERVER['REQUEST_URI'],'?');
  235. self::$current_url = $u;
  236. }
  237. return self::$current_url;
  238. }
  239. static function referrer_url($fallback_on_http_referer=false) {
  240. $dest = isset($_REQUEST['gb-referrer']) ? $_REQUEST['gb-referrer']
  241. : (isset($_REQUEST['referrer']) ? $_REQUEST['referrer'] : false);
  242. if ($fallback_on_http_referer && $dest === false && isset($_SERVER['HTTP_REFERER']))
  243. $dest = $_SERVER['HTTP_REFERER'];
  244. if ($dest) {
  245. $dest = new GBURL($dest);
  246. unset($dest['gb-error']);
  247. return $dest;
  248. }
  249. return false;
  250. }
  251. # --------------------------------------------------------------------------
  252. # Admin authentication
  253. static public $authorized = null;
  254. static public $_authenticators = null;
  255. static function authenticator($context='gb-admin') {
  256. if (self::$_authenticators === null)
  257. self::$_authenticators = array();
  258. elseif (isset(self::$_authenticators[$context]))
  259. return self::$_authenticators[$context];
  260. $users = array();
  261. foreach (GBUser::find() as $email => $account) {
  262. # only include actual users
  263. if (strpos($email, '@') !== false)
  264. $users[$email] = $account->passhash;
  265. }
  266. $chap = new CHAP($users, $context);
  267. self::$_authenticators[$context] = $chap;
  268. return $chap;
  269. }
  270. static function deauthorize($redirect=true, $context='gb-admin') {
  271. $old_authorized = self::$authorized;
  272. self::$authorized = null;
  273. if (self::authenticator($context)->deauthorize()) {
  274. if ($old_authorized)
  275. self::log('client deauthorized: '.$old_authorized->email);
  276. gb::event('client-deauthorized', $old_authorized);
  277. }
  278. if ($redirect) {
  279. header('HTTP/1.1 303 See Other');
  280. header('Location: '.(isset($_REQUEST['referrer']) ? $_REQUEST['referrer'] : gb::$site_url));
  281. exit(0);
  282. }
  283. }
  284. static function authenticate($force=true, $context='gb-admin') {
  285. $auth = self::authenticator($context);
  286. self::$authorized = null;
  287. if (($authed = $auth->authenticate())) {
  288. self::$authorized = GBUser::find($authed);
  289. return self::$authorized;
  290. }
  291. elseif ($force) {
  292. $url = gb_admin::$url . 'helpers/authorize.php?referrer='.urlencode(self::url());
  293. header('HTTP/1.1 303 See Other');
  294. header('Location: '.$url);
  295. exit('<html><body>See Other <a href="'.$url.'"></a></body></html>');
  296. }
  297. return $authed;
  298. }
  299. # --------------------------------------------------------------------------
  300. # Plugins
  301. static public $plugins_loaded = array();
  302. static function plugin_check_enabled($context, $name) {
  303. $plugin_config = self::data('plugins');
  304. if (!isset($plugin_config[$context]))
  305. return false;
  306. $name = str_replace(array('-', '.'), '_', $name);
  307. foreach ($plugin_config[$context] as $path) {
  308. $plugin_name = str_replace(array('-', '.'), '_', substr(basename($path), 0, -4));
  309. if ($plugin_name == $name);
  310. return true;
  311. }
  312. return false;
  313. }
  314. static function load_plugins($context) {
  315. $plugin_config = self::data('plugins');
  316. if (!isset($plugin_config[$context]))
  317. return;
  318. $plugins = $plugin_config[$context];
  319. if (!is_array($plugins))
  320. return;
  321. # load plugins
  322. foreach ($plugins as $path) {
  323. if (!$path)
  324. continue;
  325. # expand gitblog plugins
  326. if ($path{0} !== '/')
  327. $path = gb::$dir . '/plugins/'.$path;
  328. # get loadstate
  329. $loadstate = null;
  330. if (isset(self::$plugins_loaded[$path]))
  331. $loadstate = self::$plugins_loaded[$path];
  332. # check loadstate
  333. if ($loadstate === null) {
  334. # load if not loaded
  335. require $path;
  336. }
  337. elseif (in_array($context, $loadstate, true)) {
  338. # already loaded and inited in this context
  339. continue;
  340. }
  341. # call name_plugin::init($context)
  342. $name = str_replace(array('-', '.'), '_', substr(basename($path), 0, -4)); # assume .xxx
  343. $did_init = call_user_func(array($name.'_plugin', 'init'), $context);
  344. if ($loadstate === null)
  345. self::$plugins_loaded[$path] = $did_init ? array($context) : array();
  346. elseif ($did_init)
  347. self::$plugins_loaded[$path][] = $context;
  348. }
  349. }
  350. /** A JSONDict */
  351. static public $settings = null;
  352. # initialized after the gb class
  353. # --------------------------------------------------------------------------
  354. # Events
  355. static public $events = array();
  356. /** Register $callable for receiving $event s */
  357. static function observe($event, $callable) {
  358. if(isset(self::$events[$event]))
  359. self::$events[$event][] = $callable;
  360. else
  361. self::$events[$event] = array($callable);
  362. }
  363. /** Dispatch an event, optionally with arguments. */
  364. static function event(/* $event [, $arg ..] */ ) {
  365. $args = func_get_args();
  366. $event = array_shift($args);
  367. if(isset(self::$events[$event])) {
  368. foreach(self::$events[$event] as $callable) {
  369. if (call_user_func_array($callable, $args) === true)
  370. break;
  371. }
  372. }
  373. }
  374. /** Unregister $callable from receiving $event s */
  375. static function stop_observing($callable, $event=null) {
  376. if($event !== null) {
  377. if(isset(self::$events[$event])) {
  378. $a =& self::$events[$event];
  379. if(($i = array_search($callable, $a)) !== false) {
  380. unset($a[$i]);
  381. if(!$a)
  382. unset(self::$events[$event]);
  383. return true;
  384. }
  385. }
  386. }
  387. else {
  388. foreach(self::$events as $n => $a) {
  389. if(($i = array_search($callable, $a)) !== false) {
  390. unset(self::$events[$n][$i]);
  391. if(!self::$events[$n])
  392. unset(self::$events[$n]);
  393. return true;
  394. }
  395. }
  396. }
  397. return false;
  398. }
  399. # --------------------------------------------------------------------------
  400. # Filters
  401. static public $filters = array();
  402. /**
  403. * Add a filter
  404. *
  405. * Lower number for $priority means earlier execution of $func.
  406. *
  407. * If $func returns boolean FALSE the filter chain is broken, not applying
  408. * any more filter after the one returning FALSE. Returning anything else
  409. * have no effect.
  410. */
  411. static function add_filter($tag, $func, $priority=100) {
  412. if (!isset(self::$filters[$tag]))
  413. self::$filters[$tag] = array($priority => array($func));
  414. elseif (!isset(self::$filters[$tag][$priority]))
  415. self::$filters[$tag][$priority] = array($func);
  416. else
  417. self::$filters[$tag][$priority][] = $func;
  418. }
  419. /** Apply filters for $tag on $value */
  420. static function filter($tag, $value/*, [arg ..] */) {
  421. $vargs = func_get_args();
  422. $tag = array_shift($vargs);
  423. if (!isset(self::$filters[$tag]))
  424. return $value;
  425. $a = self::$filters[$tag];
  426. if ($a === null)
  427. return $value;
  428. ksort($a, SORT_NUMERIC);
  429. foreach ($a as $funcs) {
  430. foreach ($funcs as $func) {
  431. $value = call_user_func_array($func, $vargs);
  432. $vargs[0] = $value;
  433. }
  434. }
  435. return $vargs[0];
  436. }
  437. # --------------------------------------------------------------------------
  438. # defer -- Delayed execution
  439. static public $deferred = null;
  440. static public $deferred_time_limit = 30;
  441. /**
  442. * Schedule $callable for delayed execution.
  443. *
  444. * $callable will be executed after the response has been sent to the client.
  445. * This is useful for expensive operations which do not need to send
  446. * anything to the client.
  447. *
  448. * At the first call to defer, deferring will be "activated". This means that
  449. * output buffering is enabled, keepalive disabled and user-abort is ignored.
  450. * You can check to see if deferring is enabled by doing a truth check on
  451. * gb::$deferred. The event "did-activate-deferring" is also posted.
  452. *
  453. * Use deferring wth caution.
  454. *
  455. * A good example of when delayed execution is a good idea, is how the
  456. * email-notification plugin defers the mail action (this is actually part of
  457. * GBMail but this plugin makes good use of it).
  458. *
  459. * Events:
  460. *
  461. * - "did-activate-deferring"
  462. * Posted when defer is activated.
  463. *
  464. */
  465. static function defer($callable /* [$arg, .. ] */) {
  466. if (self::$deferred === null) {
  467. if (headers_sent())
  468. return false;
  469. ob_start();
  470. header('Transfer-Encoding: identity');
  471. header('Connection: close');
  472. self::$deferred = array();
  473. register_shutdown_function(array('gb','run_deferred'));
  474. ignore_user_abort(true);
  475. gb::event('did-activate-deferring');
  476. }
  477. self::$deferred[] = array($callable, array_slice(func_get_args(), 1));
  478. return true;
  479. }
  480. static function run_deferred() {
  481. try {
  482. # allow for self::$deferred_time_limit more seconds of processing
  483. global $gb_time_started;
  484. $time_spent = time()-$gb_time_started;
  485. @set_time_limit(self::$deferred_time_limit + $time_spent);
  486. if (headers_sent()) {
  487. # issue warning if output already started
  488. gb::log(LOG_WARNING,
  489. 'defer: output already started -- using interleaved execution');
  490. }
  491. else {
  492. # tell client the request is done
  493. $size = ob_get_length();
  494. header('Content-Length: '.$size);
  495. ob_end_flush();
  496. }
  497. # flush any pending output
  498. flush();
  499. # call deferred code
  500. foreach (self::$deferred as $f) {
  501. try {
  502. call_user_func_array($f[0], $f[1]);
  503. }
  504. catch (Exception $e) {
  505. gb::log(LOG_ERR, 'deferred %s failed with %s: %s',
  506. gb_strlimit(json_encode($f),40), get_class($e), $e->__toString());
  507. }
  508. }
  509. }
  510. catch (Exception $e) {
  511. gb::log(LOG_ERR, 'run_deferred failed with %s: %s', get_class($e), $e->__toString());
  512. }
  513. }
  514. # --------------------------------------------------------------------------
  515. # data -- arbitrary key-value storage
  516. static public $data_store_class = 'JSONDict';
  517. static public $data_stores = array();
  518. static function data($name, $default=null) {
  519. if (isset(self::$data_stores[$name]))
  520. return self::$data_stores[$name];
  521. $cls = self::$data_store_class;
  522. $store = new $cls($name);
  523. self::$data_stores[$name] = $store;
  524. if ($default && !is_array($store->storage()->get()))
  525. $store->storage()->set($default);
  526. return $store;
  527. }
  528. # --------------------------------------------------------------------------
  529. # reading object indices
  530. static public $object_indices = array();
  531. static function index($name, $fallback=null) {
  532. if (isset(self::$object_indices[$name]))
  533. return self::$object_indices[$name];
  534. if ($fallback !== null) {
  535. $obj = @unserialize(file_get_contents(self::index_path($name)));
  536. if ($obj === false)
  537. return $fallback;
  538. }
  539. else
  540. $obj = unserialize(file_get_contents(self::index_path($name)));
  541. self::$object_indices[$name] = $obj;
  542. return $obj;
  543. }
  544. static function index_cachename($name) {
  545. return $name.'.index';
  546. }
  547. static function index_path($name) {
  548. return gb::$site_dir.'/.git/info/gitblog/'.self::index_cachename($name);
  549. }
  550. # --------------------------------------------------------------------------
  551. # GitBlog
  552. static public $rebuilders = array();
  553. /** Execute a command inside a shell */
  554. static function shell($cmd, $input=null, $cwd=null, $env=null) {
  555. #var_dump($cmd);
  556. # start process
  557. $ps = gb_popen($cmd, $cwd, $env === null ? $_ENV : $env);
  558. if (!$ps)
  559. return null;
  560. # stdin
  561. if ($input)
  562. fwrite($ps['pipes'][0], $input);
  563. fclose($ps['pipes'][0]);
  564. # stdout
  565. $output = stream_get_contents($ps['pipes'][1]);
  566. fclose($ps['pipes'][1]);
  567. # stderr
  568. $errors = stream_get_contents($ps['pipes'][2]);
  569. fclose($ps['pipes'][2]);
  570. # wait and return
  571. return array(proc_close($ps['handle']), $output, $errors);
  572. }
  573. /** Glob with PCRE skip filter which defaults to skipping directories. */
  574. static function glob($pattern, $skip='/\/$/') {
  575. foreach (glob($pattern, GLOB_MARK|GLOB_BRACE) as $path)
  576. if ( ($skip && !preg_match($skip, $path)) || !$skip )
  577. return $path;
  578. return null;
  579. }
  580. static function pathToTheme($file='') {
  581. return gb::$site_dir.'/theme/'.$file;
  582. }
  583. static function tags($indexname='tags-by-popularity') {
  584. return gb::index($indexname);
  585. }
  586. static function categories($indexname='category-to-objs') {
  587. return gb::index($indexname);
  588. }
  589. static function urlToTags($tags) {
  590. return gb::$site_url . gb::$index_prefix . gb::$tags_prefix
  591. . implode(',', array_map('urlencode', $tags));
  592. }
  593. static function urlToTag($tag) {
  594. return gb::$site_url . gb::$index_prefix . gb::$tags_prefix
  595. . urlencode($tag);
  596. }
  597. static function urlToCategories($categories) {
  598. return gb::$site_url . gb::$index_prefix . gb::$categories_prefix
  599. . implode(',', array_map('urlencode', $categories));
  600. }
  601. static function urlToCategory($category) {
  602. return gb::$site_url . gb::$index_prefix . gb::$categories_prefix
  603. . urlencode($category);
  604. }
  605. static function init($add_sample_content=true, $shared='true', $theme='default', $mkdirmode=0775) {
  606. # sanity check
  607. $themedir = gb::$dir.'/themes/'.$theme;
  608. if (!is_dir($themedir)) {
  609. throw new InvalidArgumentException(
  610. 'no theme named '.$theme.' ('.$themedir.'not found or not a directory)');
  611. }
  612. # git init
  613. git::init(null, null, $shared);
  614. # Create empty standard directories
  615. mkdir(gb::$site_dir.'/content/posts', $mkdirmode, true);
  616. chmod(gb::$site_dir.'/content', $mkdirmode);
  617. chmod(gb::$site_dir.'/content/posts', $mkdirmode);
  618. mkdir(gb::$site_dir.'/content/pages', $mkdirmode);
  619. chmod(gb::$site_dir.'/content/pages', $mkdirmode);
  620. mkdir(gb::$site_dir.'/data', $mkdirmode);
  621. chmod(gb::$site_dir.'/data', $mkdirmode);
  622. # Create hooks and set basic config
  623. gb_maint::repair_repo_setup();
  624. # Copy default data sets
  625. $data_skeleton_dir = gb::$dir.'/skeleton/data';
  626. foreach (scandir($data_skeleton_dir) as $name) {
  627. if ($name{0} !== '.') {
  628. $path = $data_skeleton_dir.'/'.$name;
  629. if (is_file($path)) {
  630. copy($path, gb::$site_dir.'/data/'.$name);
  631. chmod(gb::$site_dir.'/data/'.$name, 0664);
  632. }
  633. }
  634. }
  635. # Copy .gitignore
  636. copy(gb::$dir.'/skeleton/gitignore', gb::$site_dir.'/.gitignore');
  637. chmod(gb::$site_dir.'/.gitignore', 0664);
  638. git::add('.gitignore');
  639. # Copy theme
  640. $lnname = gb::$site_dir.'/index.php';
  641. $lntarget = gb_relpath($lnname, $themedir.'/index.php');
  642. symlink($lntarget, $lnname) or exit($lntarget);
  643. git::add('index.php');
  644. # Add gb-config.php (might been added already, might be missing and/or
  645. # might be ignored by custom .gitignore -- doesn't really matter)
  646. git::add('gb-config.php', false);
  647. # Add sample content
  648. if ($add_sample_content) {
  649. # Copy example "about" page
  650. copy(gb::$dir.'/skeleton/content/pages/about.html', gb::$site_dir.'/content/pages/about.html');
  651. chmod(gb::$site_dir.'/content/pages/about.html', 0664);
  652. git::add('content/pages/about.html');
  653. # Copy example "about/intro" snippet
  654. mkdir(gb::$site_dir.'/content/pages/about', $mkdirmode);
  655. chmod(gb::$site_dir.'/content/pages/about', $mkdirmode);
  656. copy(gb::$dir.'/skeleton/content/pages/about/intro.html', gb::$site_dir.'/content/pages/about/intro.html');
  657. chmod(gb::$site_dir.'/content/pages/about/intro.html', 0664);
  658. git::add('content/pages/about/intro.html');
  659. # Copy example "hello world" post
  660. $s = file_get_contents(gb::$dir.'/skeleton/content/posts/0000-00-00-hello-world.html');
  661. $s = preg_replace('/published:.+/', 'published: '.date('H:i:s O'), $s);
  662. $name = 'content/posts/'.gmdate('Y/m-d').'-hello-world.html';
  663. $path = gb::$site_dir.'/'.$name;
  664. @mkdir(dirname($path), 0775, true);
  665. chmod(dirname($path), 0775);
  666. $s = str_replace('0000/00-00-hello-world.html', basename(dirname($name)).'/'.basename($name), $s);
  667. file_put_contents($path, $s);
  668. chmod($path, 0664);
  669. git::add($name);
  670. }
  671. return true;
  672. }
  673. static function version_parse($s) {
  674. if (is_int($s))
  675. return $s;
  676. $v = array_map('intval', explode('.', $s));
  677. if (count($v) < 3)
  678. return 0;
  679. return ($v[0] << 16) + ($v[1] << 8) + $v[2];
  680. }
  681. static function version_format($v) {
  682. return sprintf('%d.%d.%d', $v >> 16, ($v & 0x00ff00) >> 8, $v & 0x0000ff);
  683. }
  684. /** Load the site state */
  685. static function load_site_state() {
  686. $path = self::$site_dir.'/data/site.json';
  687. $data = @file_get_contents($path);
  688. if ($data === false) {
  689. # version <= 0.1.3 ?
  690. if (is_readable(gb::$site_dir.'/site.json'))
  691. gb::$site_state = @json_decode(file_get_contents(gb::$site_dir.'/site.json'), true);
  692. return gb::$site_state !== null;
  693. }
  694. gb::$site_state = json_decode($data, true);
  695. if (gb::$site_state === null || is_string(gb::$site_state)) {
  696. self::log(LOG_WARNING, 'syntax error in site.json -- moved to site.json.broken and creating new');
  697. if (!rename($path, $path.'.broken'))
  698. self::log(LOG_WARNING, 'failed to move "%s" to "%s"', $path, $path.'.broken');
  699. gb::$site_state = null;
  700. return false;
  701. }
  702. return true;
  703. }
  704. /**
  705. * Verify integrity of the site, automatically taking any actions to restore
  706. * it if broken.
  707. *
  708. * Return values:
  709. * 0 Nothing done (everything is probably OK).
  710. * -1 Error (the error has been logged through trigger_error).
  711. * 1 gitblog cache was updated.
  712. * 2 gitdir is missing and need to be created (git init).
  713. * 3 upgrade performed
  714. */
  715. static function verify_integrity() {
  716. $r = 0;
  717. if (!is_dir(gb::$site_dir.'/.git/info/gitblog')) {
  718. if (!is_dir(gb::$site_dir.'/.git')) {
  719. # 2: no repo/not initialized
  720. return 2;
  721. }
  722. # 1: gitblog cache updated
  723. gb_maint::sync_site_state();
  724. GBRebuilder::rebuild(true);
  725. return 1;
  726. }
  727. # load site.json
  728. $r = self::load_site_state();
  729. # check site state
  730. if ( $r === false
  731. || !isset(gb::$site_state['url'])
  732. || !gb::$site_state['url']
  733. || (
  734. gb::$site_state['url'] !== gb::$site_url
  735. && strpos(gb::$site_url, '://localhost') === false
  736. && strpos(gb::$site_url, '://127.0.0.1') === false
  737. )
  738. )
  739. {
  740. return gb_maint::sync_site_state() === false ? -1 : 0;
  741. }
  742. elseif (gb::$site_state['version'] !== gb::$version) {
  743. return gb_maint::upgrade(gb::$site_state['version']) ? 0 : -1;
  744. }
  745. elseif (gb::$site_state['posts_pagesize'] !== gb::$posts_pagesize) {
  746. gb_maint::sync_site_state();
  747. GBRebuilder::rebuild(true);
  748. return 1;
  749. }
  750. return 0;
  751. }
  752. static function verify_config() {
  753. if (!gb::$secret || strlen(gb::$secret) < 62) {
  754. header('HTTP/1.1 503 Service Unavailable');
  755. header('Content-Type: text/plain; charset=utf-8');
  756. exit("\n\ngb::\$secret is not set or too short.\n\nPlease edit your gb-config.php file.\n");
  757. }
  758. }
  759. static function verify() {
  760. if (self::verify_integrity() === 2) {
  761. header("Location: ".gb::$site_url."gitblog/admin/setup.php");
  762. exit(0);
  763. }
  764. gb::verify_config();
  765. }
  766. }
  767. #------------------------------------------------------------------------------
  768. # Initialize constants
  769. gb::$dir = dirname(__FILE__);
  770. ini_set('include_path', ini_get('include_path') . ':' . gb::$dir . '/lib');
  771. if (gb::$request_query === 'PATH_INFO')
  772. gb::$index_prefix = rtrim(gb::$index_prefix, '/').'/';
  773. $u = dirname($_SERVER['SCRIPT_NAME']);
  774. $s = dirname($_SERVER['SCRIPT_FILENAME']);
  775. if (substr($_SERVER['SCRIPT_FILENAME'], -20) === '/gitblog/gitblog.php')
  776. exit('you can not run gitblog.php directly');
  777. gb::$is_internal_call = ((strpos($s, '/gitblog/') !== false || substr($s, -8) === '/gitblog')
  778. && (strpos(realpath($s), realpath(gb::$dir)) === 0));
  779. # gb::$site_dir
  780. if (isset($gb_site_dir)) {
  781. gb::$site_dir = $gb_site_dir;
  782. unset($gb_site_dir);
  783. }
  784. else {
  785. if (gb::$is_internal_call) {
  786. # confirmed: inside gitblog -- back up to before the gitblog dir and
  787. # assume that's the site dir.
  788. $max = 20;
  789. while($s !== '/' && $max--) {
  790. if (substr($s, -7) === 'gitblog') {
  791. $s = dirname($s);
  792. $u = dirname($u);
  793. break;
  794. }
  795. $s = dirname($s);
  796. $u = dirname($u);
  797. }
  798. }
  799. gb::$site_dir = realpath($s);
  800. }
  801. # gb::$site_path -- must end in a slash ("/").
  802. if (isset($gb_site_path)) {
  803. gb::$site_path = $gb_site_path;
  804. unset($gb_site_path);
  805. }
  806. else {
  807. gb::$site_path = ($u === '/' ? $u : $u.'/');
  808. }
  809. # gb::$site_url -- URL to the base of the site. Must end in a slash ("/").
  810. if (isset($gb_site_url)) {
  811. gb::$site_url = $gb_site_url;
  812. unset($gb_site_url);
  813. }
  814. else {
  815. gb::$site_url = (isset($_SERVER['HTTPS']) ? 'https://' : 'http://')
  816. .$_SERVER['SERVER_NAME']
  817. .($_SERVER['SERVER_PORT'] !== '80' && $_SERVER['SERVER_PORT'] !== '443' ? ':'.$_SERVER['SERVER_PORT'] : '')
  818. .gb::$site_path;
  819. }
  820. # only set the following when called externally
  821. if (!gb::$is_internal_call) {
  822. # gb::$theme_dir
  823. if (isset($gb_theme_dir)) {
  824. gb::$theme_dir = $gb_theme_dir;
  825. unset($gb_theme_dir);
  826. }
  827. else {
  828. $bt = debug_backtrace();
  829. gb::$theme_dir = dirname($bt[0]['file']);
  830. }
  831. # gb::$theme_url
  832. if (isset($gb_theme_url)) {
  833. gb::$theme_url = $gb_theme_url;
  834. unset($gb_theme_url);
  835. }
  836. else {
  837. $relpath = gb_relpath(gb::$site_dir, gb::$theme_dir);
  838. if ($relpath === '' || $relpath === '.') {
  839. gb::$theme_url = gb::$site_url;
  840. }
  841. elseif ($relpath{0} === '.' || $relpath{0} === '/') {
  842. $uplevels = $max_uplevels = 0;
  843. if ($relpath{0} === '/') {
  844. $uplevels = 1;
  845. }
  846. if ($relpath{0} === '.') {
  847. function _empty($x) { return empty($x); }
  848. $max_uplevels = count(explode('/',trim(parse_url(gb::$site_url, PHP_URL_PATH), '/')));
  849. $uplevels = count(array_filter(explode('../', $relpath), '_empty'));
  850. }
  851. if ($uplevels > $max_uplevels) {
  852. trigger_error('gb::$theme_url could not be deduced since the theme you are '.
  853. 'using ('.gb::$theme_dir.') is not reachable from '.gb::$site_url.
  854. '. You need to manually define $gb_theme_url before including gitblog.php',
  855. E_USER_ERROR);
  856. }
  857. }
  858. else {
  859. gb::$theme_url = gb::$site_url . $relpath . '/';
  860. }
  861. }
  862. }
  863. unset($s);
  864. unset($u);
  865. #------------------------------------------------------------------------------
  866. # Define error handler which throws PHPException s
  867. function gb_throw_php_error($errno, $errstr, $errfile=null, $errline=-1, $errcontext=null) {
  868. if(error_reporting() === 0)
  869. return;
  870. try { gb::vlog(LOG_WARNING, array($errstr), 2); } catch (Exception $e) {}
  871. if ($errstr)
  872. $errstr = html_entity_decode(strip_tags($errstr), ENT_QUOTES, 'UTF-8');
  873. throw new PHPException($errstr, $errno, $errfile, $errline);
  874. }
  875. set_error_handler('gb_throw_php_error', E_ALL);
  876. #------------------------------------------------------------------------------
  877. # Load configuration
  878. if (file_exists(gb::$site_dir.'/gb-config.php'))
  879. include gb::$site_dir.'/gb-config.php';
  880. # no config? -- read defaults
  881. if (gb::$site_title === null) {
  882. require gb::$dir.'/skeleton/gb-config.php';
  883. }
  884. #------------------------------------------------------------------------------
  885. # Setup autoload and exception handler
  886. # Lazy class loader
  887. function __autoload($classname) {
  888. require $classname . '.php';
  889. }
  890. function gb_exception_handler($e) {
  891. if (ini_get('html_errors')) {
  892. if (headers_sent())
  893. $msg = GBException::formatHTMLBlock($e);
  894. else
  895. $msg = GBException::formatHTMLDocument($e);
  896. }
  897. else
  898. $msg = GBException::format($e, true, false, null, 0);
  899. exit($msg);
  900. }
  901. set_exception_handler('gb_exception_handler');
  902. # PATH patches: macports git. todo: move to admin/setup.php
  903. $_ENV['PATH'] .= ':/opt/local/bin';
  904. #------------------------------------------------------------------------------
  905. # Utilities
  906. # These classes and functions are used in >=90% of all use cases -- that's why
  907. # they are defined inline here in gitblog.php and not put in lazy files inside
  908. # lib/.
  909. /** Dictionary backed by a JSONStore */
  910. class JSONDict implements ArrayAccess, Countable {
  911. public $file;
  912. public $skeleton_file;
  913. public $cache;
  914. public $storage;
  915. function __construct($name_or_path, $is_path=false, $skeleton_file=null) {
  916. $this->file = ($is_path === false) ? gb::$site_dir.'/data/'.$name_or_path.'.json' : $name_or_path;
  917. $this->cache = null;
  918. $this->storage = null;
  919. $this->skeleton_file = $skeleton_file;
  920. }
  921. /** Retrieve the underlying JSONStore storage */
  922. function storage() {
  923. if ($this->storage === null)
  924. $this->storage = new JSONStore($this->file, $this->skeleton_file);
  925. return $this->storage;
  926. }
  927. /**
  928. * Higher level GET operation able to read deep values, which keys are
  929. * separated by $sep.
  930. */
  931. function get($key, $default=null, $sep='/') {
  932. if (!$sep) {
  933. if (($value = $this->offsetGet($key)) === null)
  934. return $default;
  935. return $value;
  936. }
  937. $keys = explode($sep, trim($key,$sep));
  938. if (($count = count($keys)) < 2) {
  939. if (($value = $this->offsetGet($key)) === null)
  940. return $default;
  941. return $value;
  942. }
  943. $value = $this->offsetGet($keys[0]);
  944. for ($i=1; $i<$count; $i++) {
  945. $key = $keys[$i];
  946. if (!is_array($value) || !isset($value[$key]))
  947. return $default;
  948. $value = $value[$key];
  949. }
  950. return $value;
  951. }
  952. /**
  953. * Higher level PUT operation able to set deep values, which keys are
  954. * separated by $sep.
  955. */
  956. function put($key, $value, $sep='/') {
  957. $temp_tx = false;
  958. $keys = explode($sep, trim($key, $sep));
  959. if (($count = count($keys)) < 2)
  960. return $this->offsetSet($key, $value);
  961. $this->cache === null;
  962. $storage = $this->storage();
  963. if (!$storage->transactionActive()) {
  964. $storage->begin();
  965. $temp_tx = true;
  966. }
  967. try {
  968. $storage->get(); # make sure $storage->data is loaded
  969. # two-key optimisation
  970. if ($count === 2) {
  971. $key1 = $keys[0];
  972. $d =& $storage->data[$key1];
  973. if (!isset($d))
  974. $d = array($keys[1] => $value);
  975. elseif (!is_array($d))
  976. $d = array($storage->data[$key1], $keys[1] => $value);
  977. else
  978. $d[$keys[1]] = $value;
  979. }
  980. else {
  981. $patch = null;
  982. $n = array();
  983. $leaf_key = array_pop($keys);
  984. $eroot = null;
  985. $e = $storage->data;
  986. $ef = true;
  987. # build patch
  988. foreach ($keys as $key) {
  989. $n[$key] = array();
  990. if ($patch === null) {
  991. $patch =& $n;
  992. $eroot =& $e;
  993. }
  994. if ($ef !== false) {
  995. if (isset($e[$key]) && is_array($e[$key]))
  996. $e =& $e[$key];
  997. else
  998. $ef = false;
  999. }
  1000. $n =& $n[$key];
  1001. }
  1002. # apply
  1003. if ($ef !== false) {
  1004. # quick patch (simply replace or set value)
  1005. if (!is_array($e))
  1006. $e = array($leaf_key => $value);
  1007. else
  1008. $e[$leaf_key] = $value;
  1009. $storage->data = $eroot;
  1010. }
  1011. else {
  1012. # merge patch
  1013. $n[$leaf_key] = $value;
  1014. $storage->data = array_merge_recursive($storage->data, $patch);
  1015. }
  1016. }
  1017. # commit changes
  1018. $this->cache = $storage->data;
  1019. if ($temp_tx === true)
  1020. $storage->commit();
  1021. }
  1022. catch (Exception $e) {
  1023. if ($temp_tx === true)
  1024. $storage->rollback();
  1025. throw $e;
  1026. }
  1027. }
  1028. function offsetGet($k) {
  1029. if ($this->cache === null)
  1030. $this->cache = $this->storage()->get();
  1031. return isset($this->cache[$k]) ? $this->cache[$k] : null;
  1032. }
  1033. function offsetSet($k, $v) {
  1034. $this->storage()->set($k, $v);
  1035. $this->cache = null; # will be reloaded at next call to get
  1036. }
  1037. function offsetExists($k) {
  1038. if ($this->cache === null)
  1039. $this->cache = $this->storage()->get();
  1040. return isset($this->cache[$k]);
  1041. }
  1042. function offsetUnset($k) {
  1043. $this->storage()->set($k, null);
  1044. $this->cache = null; # will be reloaded at next call to get
  1045. }
  1046. function count() {
  1047. if ($this->cache === null)
  1048. $this->cache = $this->storage()->get();
  1049. return count($this->cache);
  1050. }
  1051. function toJSON() {
  1052. $json = trim(file_get_contents($this->file));
  1053. return (!$json || $json{0} !== '{') ? '{}' : $json;
  1054. }
  1055. function __toString() {
  1056. if ($this->cache === null)
  1057. $this->cache = $this->storage()->get();
  1058. return var_export($this->cache ,1);
  1059. }
  1060. }
  1061. class GBURL implements ArrayAccess, Countable {
  1062. public $scheme;
  1063. public $host;
  1064. public $secure;
  1065. public $port;
  1066. public $path = '/';
  1067. public $query;
  1068. public $fragment;
  1069. static function parse($str) {
  1070. return new self($str);
  1071. }
  1072. function __construct($url=null) {
  1073. if ($url !== null) {
  1074. $p = @parse_url($url);
  1075. if ($p === false)
  1076. throw new InvalidArgumentException('unable to parse URL '.var_export($url,1));
  1077. foreach ($p as $k => $v) {
  1078. if ($k === 'query')
  1079. parse_str($v, $this->query);
  1080. else
  1081. $this->$k = $v;
  1082. }
  1083. $this->secure = $this->scheme === 'https';
  1084. if ($this->port === null)
  1085. $this->port = $this->scheme === 'https' ? 443 : ($this->scheme === 'http' ? 80 : null);
  1086. }
  1087. }
  1088. function __toString() {
  1089. return $this->toString();
  1090. }
  1091. function toString($scheme=true, $host=true, $port=true, $path=true, $query=true, $fragment=true) {
  1092. $s = '';
  1093. if ($scheme !== false) {
  1094. if ($scheme === true) {
  1095. if ($this->scheme)
  1096. $s = $this->scheme . '://';
  1097. }
  1098. else
  1099. $s = $scheme . '://';
  1100. }
  1101. if ($host !== false) {
  1102. if ($host === true)
  1103. $s .= $this->host;
  1104. else
  1105. $s .= $host;
  1106. if ($port === true && $this->port !== null && (
  1107. ($this->secure === true && $this->port !== 443)
  1108. || ($this->secure === false && $this->port !== 80)
  1109. ))
  1110. $s .= ':' . $this->port;
  1111. elseif ($port !== true && $port !== false)
  1112. $s .= ':' . $port;
  1113. }
  1114. if ($path !== false) {
  1115. if ($path === true)
  1116. $s .= $this->path;
  1117. else
  1118. $s .= $path;
  1119. }
  1120. if ($query === true && $this->query) {
  1121. if (($query = is_string($this->query) ? $this->query : http_build_query($this->query)))
  1122. $s .= '?'.$query;
  1123. }
  1124. elseif ($query !== true && $query !== false && $query)
  1125. $s .= '?'.(is_string($query) ? $query : http_build_query($query));
  1126. if ($fragment === true && $this->fragment)
  1127. $s .= '#'.$this->fragment;
  1128. elseif ($fragment !== true && $fragment !== false && $fragment)
  1129. $s .= '#'.$fragment;
  1130. return $s;
  1131. }
  1132. function __sleep() {
  1133. $this->query = http_build_query($this->query);
  1134. return get_object_vars($this);
  1135. }
  1136. function __wakeup() {
  1137. $v = $this->query;
  1138. $this->query = array();
  1139. parse_str($v, $this->query);
  1140. }
  1141. # ArrayAccess
  1142. function offsetGet($k) { return $this->query[$k]; }
  1143. function offsetSet($k, $v) { $this->query[$k] = $v; }
  1144. function offsetExists($k) { return isset($this->query[$k]); }
  1145. function offsetUnset($k) { unset($this->query[$k]); }
  1146. # Countable
  1147. function count() { return count($this->query); }
  1148. }
  1149. /** Human-readable representation of $var */
  1150. function r($var) {
  1151. return var_export($var, true);
  1152. }
  1153. /** Ture $path is an absolute path */
  1154. function gb_isabspath($path) {
  1155. return ($path && $path{0} === '/');
  1156. }
  1157. /** Boiler plate popen */
  1158. function gb_popen($cmd, $cwd=null, $env=null) {
  1159. $fds = array(array("pipe", "r"), array("pipe", "w"), array("pipe", "w"));
  1160. $ps = proc_open($cmd, $fds, $pipes, $cwd, $env);
  1161. if (!is_resource($ps)) {
  1162. trigger_error('gb_popen('.var_export($cmd,1).') failed in '.__FILE__.':'.__LINE__);
  1163. return null;
  1164. }
  1165. return array('handle'=>$ps, 'pipes'=>$pipes);
  1166. }
  1167. /** Sort GBContent objects on published, descending */
  1168. function gb_sortfunc_cobj_date_published_r(GBContent $a, GBContent $b) {
  1169. return $b->published->time - $a->published->time;
  1170. }
  1171. function gb_sortfunc_cobj_date_modified_r(GBContent $a, GBContent $b) {
  1172. return $b->modified->time - $a->modified->time;
  1173. }
  1174. /** path w/o extension */
  1175. function gb_filenoext($path) {
  1176. $p = strrpos($path, '.', strrpos($path, '/'));
  1177. return $p > 0 ? substr($path, 0, $p) : $path;
  1178. }
  1179. /** split path into array("path w/o extension", "extension") */
  1180. function gb_fnsplit($path) {
  1181. $p = strrpos($path, '.', strrpos($path, '/'));
  1182. return array($p > 0 ? substr($path, 0, $p) : $path,
  1183. $p !== false ? substr($path, $p+1) : '');
  1184. }
  1185. /**
  1186. * Commit id of current gitblog head. Used for URLs which should be
  1187. * cached in relation to gitblog versions.
  1188. */
  1189. function gb_headid() {
  1190. if (gb::$site_state !== null && @isset(gb::$site_state['gitblog']) && @isset(gb::$site_state['gitblog']['head']))
  1191. return gb::$site_state['gitblog']['head'];
  1192. return null;
  1193. }
  1194. /** Like readline, but acts on a byte array. Keeps state with $p */
  1195. function gb_sreadline(&$p, $str, $sep="\n") {
  1196. if ($p === null)
  1197. $p = 0;
  1198. $i = strpos($str, $sep, $p);
  1199. if ($i === false)
  1200. return null;
  1201. #echo "p=$p i=$i i-p=".($i-$p)."\n";
  1202. $line = substr($str, $p, $i-$p);
  1203. $p = $i + 1;
  1204. return $line;
  1205. }
  1206. /** Evaluate an escaped UTF-8 sequence, like the ones generated by git */
  1207. function gb_utf8_unescape($s) {
  1208. eval('$s = "'.$s.'";');
  1209. return $s;
  1210. }
  1211. function gb_normalize_git_name($name) {
  1212. return ($name && $name{0} === '"') ? gb_utf8_unescape(substr($name, 1, -1)) : $name;
  1213. }
  1214. /** Normalize $time (any format strtotime can handle) to a ISO timestamp. */
  1215. function gb_strtoisotime($time) {
  1216. $d = new DateTime($time);
  1217. return $d->format('c');
  1218. }
  1219. function gb_mkutctime($st) {
  1220. return gmmktime($st['tm_hour'], $st['tm_min'], $st['tm_sec'],
  1221. $st['tm_mon']+1, ($st['tm_mday'] === 0) ? 1 : $st['tm_mday'], 1900+$st['tm_year']);
  1222. }
  1223. function gb_format_duration($seconds, $format='%H:%M:%S.') {
  1224. $i = intval($seconds);
  1225. return gmstrftime($format, $i).sprintf('%03d', round($seconds*1000.0)-($i*1000));
  1226. }
  1227. function gb_hash($data) {
  1228. return base_convert(hash_hmac('sha1', $data, gb::$secret), 16, 36);
  1229. }
  1230. function gb_flush() {
  1231. if (gb::$deferred === null)
  1232. flush();
  1233. }
  1234. /**
  1235. * Calculate relative path.
  1236. *
  1237. * Example cases:
  1238. *
  1239. * /var/gitblog/site/theme, /var/gitblog/gitblog/themes/default => "../gitblog/themes/default"
  1240. * /var/gitblog/gitblog/themes/default, /var/gitblog/site/theme => "../../site/theme"
  1241. * /var/gitblog/site/theme, /etc/gitblog/gitblog/themes/default => "/etc/gitblog/gitblog/themes/default"
  1242. * /var/gitblog, gitblog/themes/default => "gitblog/themes/default"
  1243. * /var/gitblog/site/theme, /var/gitblog/site/theme => ""
  1244. */
  1245. function gb_relpath($from, $to) {
  1246. if ($from === $to)
  1247. return '.';
  1248. $fromv = explode('/', trim($from,'/'));
  1249. $tov = explode('/', trim($to,'/'));
  1250. $len = min(count($fromv), count($tov));
  1251. $r = array();
  1252. $likes = $back = 0;
  1253. for (; $likes<$len; $likes++)
  1254. if ($fromv[$likes] != $tov[$likes])
  1255. break;
  1256. if ((!$likes) && $to{0} === '/')
  1257. return $to;
  1258. if ($likes) {
  1259. array_pop($fromv);
  1260. $back = count($fromv) - $likes;
  1261. for ($x=0; $x<$back; $x++)
  1262. $r[] = '..';
  1263. $r = array_merge($r, array_slice($tov, $likes));
  1264. }
  1265. else {
  1266. $r = $tov;
  1267. }
  1268. return implode('/', $r);
  1269. }
  1270. function gb_hms_from_time($ts) {
  1271. $p = date('his', $ts);
  1272. return (intval($p{0}.$p{1})*60*60) + (intval($p{2}.$p{3})*60) + intval($p{4}.$p{5});
  1273. }
  1274. if (function_exists('mb_substr')) {
  1275. function gb_strlimit($str, $limit=20, $ellipsis='…') {
  1276. if (mb_strlen($str, 'utf-8') > $limit)
  1277. return rtrim(mb_substr($str,0,$limit-mb_strlen($ellipsis, 'utf-8'), 'utf-8')).$ellipsis;
  1278. return $str;
  1279. }
  1280. }
  1281. else {
  1282. function gb_strlimit($str, $limit=20, $ellipsis='…') {
  1283. if (strlen($str) > $limit)
  1284. return rtrim(substr($str,0,$limit-strlen($ellipsis))).$ellipsis;
  1285. return $str;
  1286. }
  1287. }
  1288. function gb_strbool($s, $empty_is_true=false) {
  1289. $s = strtoupper($s);
  1290. return ( $s === 'TRUE' || $s === 'YES' || $s === '1' || $s === 'ON' ||
  1291. ($s === '' && $empty_is_true) );
  1292. }
  1293. function gb_strtodomid($s) {
  1294. return trim(preg_replace('/[^A-Za-z0-9_-]+/m', '-', $s), '-');
  1295. }
  1296. function gb_tokenize_html($html) {
  1297. return preg_split('/(<.*>|\[.*\])/Us', $html, -1,
  1298. PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  1299. }
  1300. #------------------------------------------------------------------------------
  1301. class GBDateTime {
  1302. public $time;
  1303. public $offset;
  1304. function __construct($time=null, $offset=null) {
  1305. if ($time === null || is_int($time)) {
  1306. $this->time = ($time === null) ? time() : $time;
  1307. $this->offset = ($offset === null) ? self::localTimezoneOffset() : $offset;
  1308. }
  1309. else {
  1310. $st = date_parse($time);
  1311. if (isset($st['zone']) && $st['zone'] !== 0)
  1312. $this->offset = -$st['zone']*60;
  1313. if (isset($st['is_dst']) && $st['is_dst'] === true)
  1314. $this->offset += 3600;
  1315. $this->time = gmmktime($st['hour'], $st['minute'], $st['second'],
  1316. $st['month'], $st['day'], $st['year']);
  1317. if ($this->offset !== null)
  1318. $this->time -= $this->offset;
  1319. else
  1320. $this->offset = 0;
  1321. }
  1322. }
  1323. function format($format='%FT%H:%M:%S%z') {
  1324. return strftime($format, $this->time);
  1325. }
  1326. function utcformat($format='%FT%H:%M:%SZ') {
  1327. return gmstrftime($format, $this->time);
  1328. }
  1329. function origformat($format='%FT%H:%M:%S', $tzformat='H:i') {
  1330. return gmstrftime($format, $this->time + $this->offset)
  1331. . ($tzformat ? self::formatTimezoneOffset($this->offset, $tzformat) : '');
  1332. }
  1333. function condensed($ranges=array(86400=>'%H:%M', 31536000=>'%b %e'), $compared_to=null) {
  1334. if ($compared_to === null)
  1335. $diff = time() - $this->time;
  1336. elseif (is_int($compared_to))
  1337. $diff = $compared_to - $this->time;
  1338. else
  1339. $diff = $compared_to->time - $this->time;
  1340. ksort($ranges);
  1341. $default_format = isset($ranges[0]) ? array_shift($ranges) : '%Y-%m-%d';
  1342. # 1, 4, 129
  1343. foreach ($ranges as $threshold => $format) {
  1344. #printf('test %d vs %d (%s, %s)', $diff, $threshold, $format, $this);
  1345. if ($diff < $threshold)
  1346. return $this->origformat($format, false);
  1347. }
  1348. return $this->origformat($default_format, false);
  1349. }
  1350. /** Relative age */
  1351. function age($threshold=null, $yformat=null, $absformat=null, $suffix=null,
  1352. $compared_to=null, $momentago=null, $prefix=null)
  1353. {
  1354. if ($threshold === null) $threshold = 2592000; # 30 days
  1355. if ($yformat === null) $yformat='%B %e';
  1356. if ($absformat === null) $absformat='%B %e, %Y';
  1357. if ($suffix === null) $suffix=' ago';
  1358. if ($prefix === null) $prefix='';
  1359. if ($momentago === null) $momentago='A second';
  1360. if ($compared_to === null)
  1361. $diff = time() - $this->time;
  1362. elseif (is_int($compared_to))
  1363. $diff = $compared_to - $this->time;
  1364. else
  1365. $diff = $compared_to->time - $this->time;
  1366. if ($diff < 0)
  1367. $diff = -$diff;
  1368. if ($diff >= $threshold)
  1369. return $this->origformat($diff < 31536000 ? $yformat : $absformat, false);
  1370. if ($diff < 5)
  1371. return $prefix.$momentago.$suffix;
  1372. elseif ($diff < 50)
  1373. return $prefix.$diff.' '.($diff === 1 ? 'second' : 'seconds').$suffix;
  1374. elseif ($diff < 3000) {
  1375. $diff = (int)round($diff / 60);
  1376. return $prefix.$diff.' '.($diff === 1 ? 'minute' : 'minutes').$suffix;
  1377. }
  1378. elseif ($diff < 83600) {
  1379. $diff = (int)round($diff / 3600);
  1380. return $prefix.$diff.' '.($diff === 1 ? 'hour' : 'hours').$suffix;
  1381. }
  1382. elseif ($diff < 604800) {
  1383. $diff = (int)round($diff / 86400);
  1384. return $prefix.$diff.' '.($diff === 1 ? 'day' : 'days').$suffix;
  1385. }
  1386. elseif ($diff < 2628000) {
  1387. $diff = (int)round($diff / 604800);
  1388. return $prefix.$diff.' '.($diff === 1 ? 'week' : 'weeks').$suffix;
  1389. }
  1390. elseif ($diff < 31536000) {
  1391. $diff = (int)round($diff / 2628000);
  1392. return $prefix.$diff.' '.($diff === 1 ? 'month' : 'months').$suffix;
  1393. }
  1394. $diff = (int)round($diff / 31536000);
  1395. return $prefix.$diff.' '.($diff === 1 ? 'year' : 'years').$suffix;
  1396. }
  1397. /**
  1398. * The offset for timezones west of UTC is always negative, and for those
  1399. * east of UTC is always positive.
  1400. */
  1401. static function localTimezoneOffset() {
  1402. $tod = gettimeofday();
  1403. return -($tod['minuteswest']*60);
  1404. }
  1405. static function formatTimezoneOffset($offset, $format='H:i') {
  1406. return ($offset < 0) ? '-'.gmdate($format, -$offset) : '+'.gmdate($format, $offset);
  1407. }
  1408. function __toString() {
  1409. return $this->origformat();
  1410. }
  1411. function __sleep() {
  1412. #$this->d = gmstrftime('%FT%TZ', $this->time);
  1413. return array('time', 'offset');
  1414. }
  1415. function __wakeup() {
  1416. #$this->time = gb_mkutctime(strptime($this->d, '%FT%TZ'));
  1417. #unset($this->d);
  1418. }
  1419. static function __set_state($state) {
  1420. if (is_array($state)) {
  1421. $o = new self;
  1422. foreach ($state as $k => $v)
  1423. $o->$k = $v;
  1424. return $o;
  1425. }
  1426. return new self($state);
  1427. }
  1428. function reintrepretTimezone($tzoffset) {
  1429. $gmts = $this->offset === 0 ? $this->time : strtotime($this->utcformat());
  1430. $ds = gmstrftime('%FT%H:%M:%S', $gmts+$tzoffset) . self::formatTimezoneOffset($tzoffset);
  1431. return new GBDateTime($ds);
  1432. }
  1433. function mergeString($s, $adjustTimezone=false) {
  1434. $t = date_parse($s);
  1435. $ds = '';
  1436. if ($t['hour'] !== false)
  1437. $ds = sprintf('%02d:%02d:%02d', $t['hour'],$t['minute'],$t['second']);
  1438. else
  1439. $ds = $this->utcformat('%T');
  1440. $tzoffset = 0;
  1441. if (isset($t['zone'])) {
  1442. $tzoffset = -($t['zone']*60);
  1443. $ds .= self::formatTimezoneOffset($tzoffset);
  1444. }
  1445. else {
  1446. $ds .= self::formatTimezoneOffset($this->offset);
  1447. }
  1448. if ($adjustTimezone)
  1449. $default = explode('-',gmstrftime('%F', strtotime($this->utcformat('%F'))+$tzoffset));
  1450. else
  1451. $default = explode('-',$this->utcformat('%F'));
  1452. $ds = (($t['year'] !== false) ? $t['year'] : $default[0]). '-'
  1453. . (($t['month'] !== false) ? $t['month'] : $default[1]). '-'
  1454. . (($t['day'] !== false) ? $t['day'] : $default[2])
  1455. . 'T' . $ds;
  1456. return new GBDateTime($ds);
  1457. }
  1458. }
  1459. # -----------------------------------------------------------------------------
  1460. # Content (posts, pages, etc)
  1461. class GBAuthor {
  1462. public $name;
  1463. public $email;
  1464. function __construct($name=null, $email=null) {
  1465. $this->name = $name;
  1466. $this->email = $email;
  1467. }
  1468. static function parse($gitauthor) {
  1469. $gitauthor = trim($gitauthor);
  1470. $p = strpos($gitauthor, '<');
  1471. $name = '';
  1472. $email = '';
  1473. if ($p === 0) {
  1474. $email = trim($gitauthor, '<>');
  1475. }
  1476. elseif ($p === false) {
  1477. if (strpos($gitauthor, '@') !== false)
  1478. $email = $gitauthor;
  1479. else
  1480. $name = $gitauthor;
  1481. }
  1482. else {
  1483. $name = rtrim(substr($gitauthor, 0, $p));
  1484. $email = trim(substr($gitauthor, $p+1), '<>');
  1485. }
  1486. return new self($name, $email);
  1487. }
  1488. static function __set_state($state) {
  1489. if (is_array($state)) {
  1490. $o = new self;
  1491. foreach ($state as $k => $v)
  1492. $o->$k = $v;
  1493. return $o;
  1494. }
  1495. elseif (is_object($state) && $state instanceof self) {
  1496. return $state;
  1497. }
  1498. else {
  1499. return self::parse($state);
  1500. }
  1501. }
  1502. static function gitFormat($author, $fallback=null) {
  1503. if (!$author)
  1504. throw new InvalidArgumentException('first argument is empty');
  1505. if (!is_object($author))
  1506. $author = self::parse($author);
  1507. $s = '';
  1508. if ($author->name)
  1509. $s = $author->name . ' ';
  1510. if ($author->email)
  1511. $s .= '<'.$author->email.'>';
  1512. if (!$s) {
  1513. if ($fallback === null)
  1514. throw new InvalidArgumentException('neither name nor email is set');
  1515. $s = $fallback;
  1516. }
  1517. return $s;
  1518. }
  1519. function shortName() {
  1520. return array_shift(explode(' ', $this->gitAuthor()));
  1521. }
  1522. function gitAuthor() {
  1523. return self::gitFormat($this);
  1524. }
  1525. function __toString() {
  1526. return $this->gitAuthor();
  1527. }
  1528. }
  1529. class GBContent {
  1530. public $name; # relative to root tree
  1531. public $id;
  1532. public $mimeType = null;
  1533. public $author = null;
  1534. public $modified = null; # GBDateTime
  1535. public $published = false; # GBDateTime
  1536. function __construct($name=null, $id=null) {
  1537. $this->name = $name;
  1538. $this->id = $id;
  1539. }
  1540. function cachename() {
  1541. return gb_filenoext($this->name).gb::$content_cache_fnext;
  1542. }
  1543. function writeCache() {
  1544. gb::event('will-write-object-cache', $this);
  1545. $path = gb::$site_dir.'/.git/info/gitblog/'.$this->cachename();
  1546. $dirname = dirname($path);
  1547. if (!is_dir($dirname)) {
  1548. $p = gb::$site_dir.'/.git/info';
  1549. $parts = array_merge(array('gitblog'),explode('/',trim(dirname($this->cachename()),'/')));
  1550. foreach ($parts as $part) {
  1551. $p .= '/'.$part;
  1552. @mkdir($p, 0775);
  1553. @chmod($p, 0775);
  1554. }
  1555. }
  1556. $bw = file_put_contents($path, serialize($this), LOCK_EX);
  1557. chmod($path, 0664);
  1558. gb::event('did-write-object-cache', $this);
  1559. return $bw;
  1560. }
  1561. function reload($data, $commits=null) {
  1562. gb::event('will-reload-object', $this, $commits);
  1563. $this->mimeType = GBMimeType::forFilename($this->name);
  1564. }
  1565. function findCommits() {
  1566. if (!$this->name)
  1567. throw new UnexpectedValueException('name property is empty');
  1568. $v = GitCommit::find(array('names' => array($this->name)));
  1569. if ($v)
  1570. return $v[0];
  1571. return array();
  1572. }
  1573. protected function applyInfoFromCommits($commits) {
  1574. if (!$commits)
  1575. return;
  1576. gb::event('will-apply-info-from-commits', $this, $commits);
  1577. # latest one is last modified
  1578. $this->modified = $commits[0]->authorDate;
  1579. # first one is when the content was created
  1580. $initial = $commits[count($commits)-1];
  1581. $this->published = $initial->authorDate;
  1582. if (!$this->author)
  1583. $this->author = new GBAuthor($initial->authorName, $initial->authorEmail);
  1584. gb::event('did-apply-info-from-commits', $this, $commits);
  1585. }
  1586. function __sleep() {
  1587. return array('name','id','mimeType','author','modified','published');
  1588. }
  1589. function toBlob() {
  1590. // subclasses should implement this
  1591. }
  1592. }
  1593. class GBExposedContent extends GBContent {
  1594. public $slug;
  1595. public $meta;
  1596. public $title;
  1597. public $body;
  1598. public $excerpt;
  1599. public $tags = array();
  1600. public $categories = array();
  1601. public $comments;
  1602. public $commentsOpen = true;
  1603. public $pingbackOpen = true;
  1604. public $draft = false;
  1605. # not serialized but only used during runtime
  1606. public $_rawBody = null;
  1607. function __construct($name=null, $id=null, $slug=null, $meta=array(), $body=null) {
  1608. parent::__construct($name, $id);
  1609. $this->slug = $slug;
  1610. $this->meta = $meta;
  1611. $this->body = $body;
  1612. }
  1613. /* Get path to cached content where the slug is static. */
  1614. static function pathToCached($subdir, $slug) {
  1615. return gb::$site_dir.'/.git/info/gitblog/content/'.$subdir.'/'.$slug;
  1616. }
  1617. /** Find path to work content where the slug is static. Returns null if not found. */
  1618. static function pathToWork($subdir, $slug_fnpattern) {
  1619. $base = gb::$site_dir.'/content/'.$subdir.'/'.$slug_fnpattern;
  1620. return gb::glob($base . '*', '/(\.comments|\/)$/');
  1621. }
  1622. static function pathspecFromAbsPath($path) {
  1623. return substr($path, strlen(gb::$site_dir)+1);
  1624. }
  1625. static function find($slug, $subdir='', $class='GBExposedContent', $version=null, $applyBodyFilters=true) {
  1626. $version = self::parseVersion($version);
  1627. if ($version === 'work') {
  1628. # find path to raw content
  1629. if (($path = self::pathToWork($subdir, $slug)) === null)
  1630. return false;
  1631. # find cached
  1632. $cached = self::find($slug, $subdir, $class, false);
  1633. # load work
  1634. return self::loadWork($path, $cached, $class, null, $slug, $applyBodyFilters);
  1635. }
  1636. else {
  1637. $path = self::pathToCached($subdir, $slug . gb::$content_cache_fnext);
  1638. $data = @file_get_contents($path);
  1639. return $data === false ? false : unserialize($data);
  1640. }
  1641. }
  1642. static function parseVersion($version) {
  1643. if ($version === null) {
  1644. return gb::$is_preview ? 'work' : null;
  1645. }
  1646. elseif ($version === true) {
  1647. return 'work';
  1648. }
  1649. else {
  1650. $s = strtolower($version);
  1651. if ($s === 'live' || $s === 'head' || $s === 'current')
  1652. return null;
  1653. return $version;
  1654. }
  1655. }
  1656. static function loadWork($path, $post=false, $class='GBExposedContent', $id=null, $slug=null, $applyBodyFilters=true) {
  1657. if ($post === false)
  1658. $post = new $class(self::pathspecFromAbsPath($path), $id, $slug);
  1659. $post->id = $id;
  1660. gb::event('will-load-work-object', $post);
  1661. # load rebuild plugins before calling reload
  1662. gb::load_plugins('rebuild');
  1663. # reload post with work data
  1664. $post->reload(file_get_contents($path), null, $applyBodyFilters);
  1665. # set modified date from file
  1666. $post->modified = new GBDateTime(filemtime($path));
  1667. # set author if needed
  1668. if (!$post->author) {
  1669. # GBUser have the same properties as the regular class-less author
  1670. # object, so it's safe to just pass it on here, as a clone.
  1671. if (gb::$authorized) {
  1672. $post->author = clone gb::$authorized;
  1673. unset($post->passhash);
  1674. }
  1675. elseif (($padmin = GBUser::findAdmin())) {
  1676. $post->author = clone $padmin;
  1677. unset($post->passhash);
  1678. }
  1679. else {
  1680. $post->author = new GBAuthor();
  1681. }
  1682. }
  1683. gb::event('did-load-work-object', $post);
  1684. return $post;
  1685. }
  1686. static function findHeaderTerminatorOffset($data) {
  1687. if (($offset = strpos($data, "\n\n")) !== false)
  1688. return $offset+2;
  1689. if (($offset = strpos($data, "\r\n\r\n")) !== false)
  1690. return $offset+4;
  1691. return false;
  1692. }
  1693. function parseData($data) {
  1694. $this->body = null;
  1695. $this->meta = array();
  1696. # use meta for title if absent
  1697. if ($this->title === null)
  1698. $this->title = $this->slug;
  1699. # find header terminator
  1700. $bodystart = self::findHeaderTerminatorOffset($data);
  1701. if ($bodystart === false) {
  1702. $bodystart = 0;
  1703. gb::log(LOG_WARNING,
  1704. 'malformed exposed content object %s: missing header and/or body (LFLF or CRLFCRLF not found)',
  1705. $this->name);
  1706. }
  1707. else {
  1708. $this->body = substr($data, $bodystart);
  1709. }
  1710. if ($bodystart !== false && $bodystart > 0)
  1711. self::parseHeader(rtrim(substr($data, 0, $bodystart)), $this->meta);
  1712. }
  1713. static function parseHeader($lines, &$out) {
  1714. $lines = explode("\n", $lines);
  1715. $k = null;
  1716. foreach ($lines as $line) {
  1717. if (!$line)
  1718. continue;
  1719. if ($line{0} === ' ' || $line{0} === "\t") {
  1720. # continuation
  1721. if ($k !== null)
  1722. $out[$k] .= ltrim($line);
  1723. continue;
  1724. }
  1725. $line = explode(':', $line, 2);
  1726. if (isset($line[1])) {
  1727. $k = $line[0];
  1728. $out[strtolower($k)] = ltrim($line[1]);
  1729. }
  1730. }
  1731. }
  1732. function parseHeaderFields() {
  1733. gb::event('will-parse-object-meta', $this);
  1734. # lift lists
  1735. static $special_lists = array('tag'=>'tags', 'category'=>'categories');
  1736. foreach ($special_lists as $singular => $plural) {
  1737. $s = false;
  1738. if (isset($this->meta[$plural])) {
  1739. $s = $this->meta[$plural];
  1740. unset($this->meta[$plural]);
  1741. }
  1742. elseif (isset($this->meta[$singular])) {
  1743. $s = $this->meta[$singular];
  1744. unset($this->meta[$singular]);
  1745. }
  1746. if ($s)
  1747. $this->$plural = gb_cfilter::apply('parse-tags', $s);
  1748. }
  1749. # lift specials, like title, from meta to this
  1750. static $special_singles = array('title');
  1751. foreach ($special_singles as $singular) {
  1752. if (isset($this->meta['title'])) {
  1753. $this->title = $this->meta['title'];
  1754. unset($this->meta[$singular]);
  1755. }
  1756. }
  1757. # lift content type
  1758. $charset = 'utf-8';
  1759. if (isset($this->meta['content-type'])) {
  1760. $this->mimeType = $this->meta['content-type'];
  1761. if (preg_match('/^([^ ;\s\t]+)(?:[ \s\t]*;[ \s\t]*charset=([a-zA-Z0-9_-]+)|).*$/', $this->mimeType, $m)) {
  1762. if (isset($m[2]) && $m[2])
  1763. $charset = strtolower($m[2]);
  1764. $this->mimeType = $m[1];
  1765. }
  1766. unset($this->meta['content-type']);
  1767. }
  1768. # lift charset or encoding
  1769. if (isset($this->meta['charset'])) {
  1770. $charset = strtolower(trim($this->meta['charset']));
  1771. # we do not unset this, because it need to propagate to buildHeaderFields
  1772. }
  1773. elseif (isset($this->meta['encoding'])) {
  1774. $charset = strtolower(trim($this->meta['encoding']));
  1775. # we do not unset this, because it need to propagate to buildHeaderFields
  1776. }
  1777. # convert body text encoding?
  1778. if ($charset && $charset !== 'utf-8' && $charset !== 'utf8' && $charset !== 'ascii') {
  1779. if (function_exists('mb_convert_encoding')) {
  1780. $this->title = mb_convert_encoding($this->title, 'utf-8', $charset);
  1781. $this->body = mb_convert_encoding($this->body, 'utf-8', $charset);
  1782. }
  1783. elseif (function_exists('iconv')) {
  1784. $this->title = iconv($charset, 'utf-8', $this->title);
  1785. $this->body = iconv($charset, 'utf-8', $this->body);
  1786. }
  1787. else {
  1788. gb::log(LOG_ERR,
  1789. 'failed to convert text encoding of %s -- neither mbstring nor iconv extension is available.',
  1790. $this->name);
  1791. }
  1792. }
  1793. # transfer author meta tag
  1794. if (isset($this->meta['author'])) {
  1795. $this->author = GBAuthor::parse($this->meta['author']);
  1796. unset($this->meta['author']);
  1797. }
  1798. # specific publish (date and) time?
  1799. $mp = false;
  1800. if (isset($this->meta['publish'])) {
  1801. $mp = $this->meta['publish'];
  1802. unset($this->meta['publish']);
  1803. }
  1804. elseif (isset($this->meta['published'])) {
  1805. $mp = $this->meta['published'];
  1806. unset($this->meta['published']);
  1807. }
  1808. if ($mp) {
  1809. $mp = strtoupper($mp);
  1810. if ($mp === 'FALSE' || $mp === 'NO' || $mp === '0')
  1811. $this->draft = true;
  1812. elseif ($mp && $mp !== false && $mp !== 'TRUE' && $mp !== 'YES' && $mp !== '1')
  1813. $this->published = $this->published->mergeString($mp);
  1814. }
  1815. # handle booleans
  1816. static $bools = array('draft' => 'draft', 'comments' => 'commentsOpen', 'pingback' => 'pingbackOpen');
  1817. foreach ($bools as $mk => $ok) {
  1818. if (isset($this->meta[$mk])) {
  1819. $s = trim($this->meta[$mk]);
  1820. unset($this->meta[$mk]);
  1821. $this->$ok = ($s === '' || gb_strbool($s));
  1822. }
  1823. }
  1824. gb::event('did-parse-object-meta', $this);
  1825. }
  1826. function fnext() {
  1827. $fnext = null;
  1828. if ($this->mimeType) {
  1829. if (strpos($this->mimeType, '/') !== false)
  1830. $fnext = GBMimeType::forType($this->mimeType);
  1831. else
  1832. $fnext = $this->mimeType;
  1833. }
  1834. if (!$fnext)
  1835. $fnext = array_pop(gb_fnsplit($this->name));
  1836. return $fnext;
  1837. }
  1838. function reload($data, $commits=null, $applyBodyFilters=true) {
  1839. parent::reload($data, $commits);
  1840. $this->parseData($data);
  1841. $this->applyInfoFromCommits($commits);
  1842. $path = gb::$site_dir.'/'.$this->name;
  1843. if (is_file($path))
  1844. $this->modified = new GBDateTime(filemtime($path));
  1845. $this->parseHeaderFields();
  1846. if ($this->modified === null)
  1847. $this->modified = $this->published;
  1848. # apply filters
  1849. if ($applyBodyFilters) {
  1850. $fnext = $this->fnext();
  1851. gb_cfilter::apply('post-reload-GBExposedContent', $this);
  1852. gb_cfilter::apply('post-reload-GBExposedContent.'.$fnext, $this);
  1853. $cls = get_class($this);
  1854. if ($cls !== 'GBExposedContent') {
  1855. gb_cfilter::apply('post-reload-'.$cls, $this);
  1856. gb_cfilter::apply('post-reload-'.$cls.'.'.$fnext, $this);
  1857. }
  1858. }
  1859. gb::event('did-reload-object', $this);
  1860. }
  1861. function isWorkVersion() {
  1862. return (!$this->id || $this->id === 'work');
  1863. }
  1864. function isTracked() {
  1865. # if it has a real ID, it's tracked
  1866. if (!$this->isWorkVersion())
  1867. return true;
  1868. # ask git
  1869. try {
  1870. if ($this->name && git::id_for_pathspec($this->name))
  1871. return true;
  1872. }
  1873. catch (GitError $e) {}
  1874. return false;
  1875. }
  1876. /** True if there are local, uncommitted modifications */
  1877. function isDirty() {
  1878. if (!$this->isTracked())
  1879. return true;
  1880. $st = git::status();
  1881. return (isset($st['staged'][$this->name]) || isset($st['unstaged'][$this->name]));
  1882. }
  1883. function exists() {
  1884. if (!$this->name)
  1885. return false;
  1886. return is_file(gb::$site_dir.'/'.$this->name);
  1887. }
  1888. function recommendedFilenameExtension($default='') {
  1889. if (($ext = GBMimeType::forType($this->mimeType)))
  1890. return '.'.$ext;
  1891. return $default;
  1892. }
  1893. function recommendedName() {
  1894. return 'content/objects/'.$this->slug.$this->recommendedFilenameExtension();
  1895. }
  1896. function setRawBody($body) {
  1897. $this->_rawBody = $body;
  1898. }
  1899. function rawBody($data=null) {
  1900. if ($this->_rawBody === null) {
  1901. if ($data === null) {
  1902. if ($this->isWorkVersion())
  1903. $data = file_get_contents(gb::$site_dir.'/'.$this->name);
  1904. else
  1905. $data = git::cat_file($this->id);
  1906. }
  1907. $p = self::findHeaderTerminatorOffset($data);
  1908. if ($p === false)
  1909. return '';
  1910. $this->_rawBody = substr($data, $p);
  1911. }
  1912. return $this->_rawBody;
  1913. }
  1914. function body() {
  1915. return gb::filter('post-body', $this->body);
  1916. }
  1917. function textBody() {
  1918. return trim(preg_replace('/<[^>]*>/m', ' ', $this->body()));
  1919. }
  1920. /**
  1921. * Return a, possibly cloned, version of this post which contains a minimal
  1922. * set of information. Primarily used for paged posts pages.
  1923. */
  1924. function condensedVersion() {
  1925. $c = clone $this;
  1926. # excerpt member turns into a boolean "is ->body an excerpt?"
  1927. if ($c->excerpt) {
  1928. $c->body = $c->excerpt;
  1929. $c->excerpt = true;
  1930. }
  1931. else {
  1932. $c->excerpt = false;
  1933. }
  1934. # comments member turns into an integer "number of comments"
  1935. $c->comments = $c->comments ? $c->comments->countApproved() : 0;
  1936. gb::event('did-create-condensed-object', $this, $c);
  1937. return $c;
  1938. }
  1939. function urlpath() {
  1940. return str_replace('%2F', '/', urlencode($this->slug));
  1941. }
  1942. function url($include_version=false, $include_pathspec=false) {
  1943. $url = gb::$site_url . gb::$index_prefix . $this->urlpath();
  1944. if ($include_version !== false) {
  1945. if ($include_version === true)
  1946. $include_version = $this->id ? $this->id : 'work';
  1947. $url .= (strpos($url, '?') === false ? '?' : '&')
  1948. . gb::$version_query_key.'='.urlencode($include_version);
  1949. }
  1950. if ($include_pathspec === true) {
  1951. $url .= (strpos($url, '?') === false ? '?' : '&')
  1952. . gb::$pathspec_query_key.'='.urlencode($this->name);
  1953. }
  1954. return $url;
  1955. }
  1956. function commentsStageName() {
  1957. # not the same as gb::$comments_cache_fnext
  1958. return gb_filenoext($this->name).'.comments';
  1959. }
  1960. function getCommentsDB() {
  1961. return new GBCommentDB(gb::$site_dir.'/'.$this->commentsStageName(), $this);
  1962. }
  1963. function commentsLink($prefix='', $suffix='', $template='<a href="%u" class="numcomments" title="%t">%n</a>') {
  1964. if (!$this->comments || !($count = is_int($this->comments) ? $this->comments : $this->comments->countApproved()))
  1965. return '';
  1966. return strtr($template, array(
  1967. '%u' => h($this->url()).'#comments',
  1968. '%n' => $count,
  1969. '%t' => $this->numberOfComments()
  1970. ));
  1971. }
  1972. function tagLinks($prefix='', $suffix='', $template='<a href="%u">%n</a>', $nglue=', ', $endglue=' and ') {
  1973. return $this->collLinks('tags', $prefix, $suffix, $template, $nglue, $endglue);
  1974. }
  1975. function categoryLinks($prefix='', $suffix='', $template='<a href="%u">%n</a>', $nglue=', ', $endglue=' and ') {
  1976. return $this->collLinks('categories', $prefix, $suffix, $template, $nglue, $endglue);
  1977. }
  1978. function collLinks($what, $prefix='', $suffix='', $template='<a href="%u">%n</a>', $nglue=', ', $endglue=' and ', $htmlescape=true) {
  1979. if (!$this->$what)
  1980. return '';
  1981. $links = array();
  1982. $vn = $what.'_prefix';
  1983. $u = gb::$site_url . gb::$index_prefix . gb::$$vn;
  1984. foreach ($this->$what as $tag)
  1985. $links[] = strtr($template, array('%u' => $u.urlencode($tag), '%n' => h($tag)));
  1986. return $nglue !== null ? $prefix.sentenceize($links, null, $nglue, $endglue).$suffix : $links;
  1987. }
  1988. function numberOfComments($topological=true, $sone='comment', $smany='comments', $zero='No', $one='One') {
  1989. return counted($this->comments ? (is_int($this->comments) ? $this->comments : $this->comments->countApproved($topological)) : 0,
  1990. $sone, $smany, $zero, $one);
  1991. }
  1992. function numberOfShadowedComments($sone='comment', $smany='comments', $zero='No', $one='One') {
  1993. return counted($this->comments ? $this->comments->countShadowed() : 0,
  1994. $sone, $smany, $zero, $one);
  1995. }
  1996. function numberOfUnapprovedComments($sone='comment', $smany='comments', $zero='No', $one='One') {
  1997. return counted($this->comments ? $this->comments->countUnapproved() : 0,
  1998. $sone, $smany, $zero, $one);
  1999. }
  2000. function __sleep() {
  2001. static $members = array(
  2002. 'slug','meta','title','body','excerpt',
  2003. 'tags','categories',
  2004. 'comments',
  2005. 'commentsOpen','pingbackOpen',
  2006. 'draft');
  2007. return array_merge(parent::__sleep(), $members);
  2008. }
  2009. static function findByCacheName($cachename) {
  2010. return @unserialize(file_get_contents(gb::$site_dir.'/.git/info/gitblog/'.$cachename));
  2011. }
  2012. static function findByTags($tags, $pageno=0) {
  2013. return self::findByMetaIndex($tags, 'tag-to-objs', $pageno);
  2014. }
  2015. static function findByCategories($cats, $pageno=0) {
  2016. return self::findByMetaIndex($cats, 'category-to-objs', $pageno);
  2017. }
  2018. static function findByMetaIndex($tags, $indexname, $pageno) {
  2019. $index = gb::index($indexname);
  2020. $objs = self::_findByMetaIndex($tags, $index);
  2021. if (!$objs)
  2022. return false;
  2023. $page = GBPagedObjects::split($objs, $pageno);
  2024. if ($page !== false) {
  2025. if ($pageno === null) {
  2026. # no pageno specified returns a list of all pages
  2027. foreach ($page as $p)
  2028. $p->posts = new GBLazyObjectsIterator($p->posts);
  2029. }
  2030. else {
  2031. # specific, single page
  2032. $page->posts = new GBLazyObjectsIterator($page->posts);
  2033. }
  2034. }
  2035. return $page;
  2036. }
  2037. static function _findByMetaIndex($tags, $index, $op='and') {
  2038. $objs = array();
  2039. # no tags, no objects
  2040. if (!$tags)
  2041. return $objs;
  2042. # single tag is a simple operation
  2043. if (count($tags) === 1)
  2044. return isset($index[$tags[0]]) ? $index[$tags[0]] : $objs;
  2045. # multiple AND
  2046. if ($op === 'and')
  2047. return self::_findByMetaIndexAND($tags, $index);
  2048. else
  2049. throw new InvalidArgumentException('currently the only supported operation is "and"');
  2050. }
  2051. static function _findByMetaIndexAND($tags, $index) {
  2052. $cachenames = array();
  2053. $intersection = array_intersect(array_keys($index), $tags);
  2054. $rindex = array();
  2055. $seen = array();
  2056. $iteration = 0;
  2057. foreach ($intersection as $tag) {
  2058. $found = 0;
  2059. foreach ($index[$tag] as $objcachename) {
  2060. if (!isset($rindex[$objcachename])) {
  2061. if ($iteration)
  2062. break;
  2063. $rindex[$objcachename] = array($tag);
  2064. }
  2065. else
  2066. $rindex[$objcachename][] = $tag;
  2067. $found++;
  2068. }
  2069. if ($found === 0)
  2070. break;
  2071. $iteration++;
  2072. }
  2073. # only keep cachenames which matched at least all $tags
  2074. $len = count($tags);
  2075. foreach ($rindex as $cachename => $matched_tags)
  2076. if (count($matched_tags) >= $len)
  2077. $cachenames[] = $cachename;
  2078. return $cachenames;
  2079. }
  2080. function domID() {
  2081. return 'post-'.$this->published->utcformat('%Y-%m-%d-').gb_strtodomid($this->slug);
  2082. }
  2083. function buildHeaderFields() {
  2084. $header = array_merge(array(
  2085. 'title' => $this->title
  2086. ), $this->meta);
  2087. # optional fields
  2088. if ($this->published)
  2089. $header['published'] = $this->published->__toString();
  2090. if ($this->draft !== null)
  2091. $header['draft'] = $this->draft ? 'yes' : 'no';
  2092. if ($this->commentsOpen !== null)
  2093. $header['comments'] = $this->commentsOpen ? 'yes' : 'no';
  2094. if ($this->pingbackOpen !== null)
  2095. $header['pingback'] = $this->pingbackOpen ? 'yes' : 'no';
  2096. if ($this->author)
  2097. $header['author'] = GBAuthor::gitFormat($this->author);
  2098. if ($this->tags)
  2099. $header['tags'] = implode(', ', $this->tags);
  2100. if ($this->categories)
  2101. $header['categories'] = implode(', ', $this->categories);
  2102. # only set content-type if mimeType is not the same as default for file ext
  2103. if ($this->mimeType !== GBMimeType::forFilename($this->name))
  2104. $header['content-type'] = $this->mimeType;
  2105. # charset and encoding should be preserved in $this->meta if
  2106. # they existed in the first place.
  2107. return $header;
  2108. }
  2109. function __toString() {
  2110. return $this->name;
  2111. }
  2112. function toBlob() {
  2113. # build header
  2114. $header = $this->buildHeaderFields();
  2115. # mux header and body
  2116. $data = '';
  2117. foreach ($header as $k => $v) {
  2118. $k = trim($k);
  2119. $v = trim($v);
  2120. if (!$k || !$v)
  2121. continue;
  2122. $data .= $k.': '.str_replace(array("\n","\r\n"), "\n\t", $v)."\n";
  2123. }
  2124. # no header? still need LFLF
  2125. if (!$data)
  2126. $data .= "\n";
  2127. # append body
  2128. $data .= "\n".$this->rawBody();
  2129. if ($data{strlen($data)-1} !== "\n")
  2130. $data .= "\n";
  2131. return $data;
  2132. }
  2133. }
  2134. class GBLazyObjectsIterator implements Iterator {
  2135. public $objects;
  2136. function __construct($cachenames, $condensed=true) {
  2137. $this->objects = array_flip($cachenames);
  2138. $this->condensed = $condensed;
  2139. }
  2140. public function rewind() {
  2141. reset($this->objects);
  2142. }
  2143. public function current() {
  2144. $v = current($this->objects);
  2145. if (!is_object($v)) {
  2146. $v = GBExposedContent::findByCacheName(key($this->objects));
  2147. if ($this->condensed)
  2148. $v = $v->condensedVersion();
  2149. $this->objects[key($this->objects)] = $v;
  2150. }
  2151. return $v;
  2152. }
  2153. public function key() {
  2154. return key($this->objects);
  2155. }
  2156. public function next() {
  2157. return next($this->objects);
  2158. }
  2159. public function valid() {
  2160. return current($this->objects) !== false;
  2161. }
  2162. }
  2163. class GBPagedObjects {
  2164. public $posts;
  2165. public $nextpage = -1;
  2166. public $prevpage = -1;
  2167. public $numpages = 0;
  2168. public $numtotal = 0;
  2169. function __construct($posts, $nextpage=-1, $prevpage=-1, $numpages=0, $numtotal=0) {
  2170. $this->posts = $posts;
  2171. $this->nextpage = $nextpage;
  2172. $this->prevpage = $prevpage;
  2173. $this->numpages = $numpages;
  2174. $this->numtotal = $numtotal;
  2175. }
  2176. static function split($posts, $onlypageno=null, $pagesize=null) {
  2177. $numtotal = count($posts);
  2178. $pages = array_chunk($posts, $pagesize === null ? gb::$posts_pagesize : $pagesize);
  2179. $numpages = count($pages);
  2180. if ($onlypageno !== null && $onlypageno > $numpages)
  2181. return false;
  2182. foreach ($pages as $pageno => $page) {
  2183. if ($onlypageno !== null && $onlypageno !== $pageno)
  2184. continue;
  2185. $page = new self($page, -1, $pageno-1, $numpages, $numtotal);
  2186. if ($pageno < $numpages-1)
  2187. $page->nextpage = $pageno+1;
  2188. if ($onlypageno !== null)
  2189. return $page;
  2190. $pages[$pageno] = $page;
  2191. }
  2192. return $pages;
  2193. }
  2194. }
  2195. class GBPage extends GBExposedContent {
  2196. public $order = null; # order in menu, etc.
  2197. public $hidden = null; # hidden from menu, but still accessible (i.e. not the same thing as $draft)
  2198. function parseHeaderFields() {
  2199. parent::parseHeaderFields();
  2200. # transfer order meta tag
  2201. static $order_aliases = array('order', 'sort', 'priority');
  2202. foreach ($order_aliases as $singular) {
  2203. if (isset($this->meta[$singular])) {
  2204. $this->order = $this->meta[$singular];
  2205. unset($this->meta[$singular]);
  2206. }
  2207. }
  2208. # transfer hidden meta tag
  2209. static $hidden_aliases = array('hidden', 'hide', 'invisible');
  2210. foreach ($hidden_aliases as $singular) {
  2211. if (isset($this->meta[$singular])) {
  2212. $s = $this->meta[$singular];
  2213. $this->hidden = ($s === '' || gb_strbool($s));
  2214. unset($this->meta[$singular]);
  2215. }
  2216. }
  2217. }
  2218. function isCurrent() {
  2219. if (!gb::$is_page)
  2220. return false;
  2221. $url = gb::url();
  2222. return (strcasecmp(rtrim(substr($url->path,
  2223. strlen(gb::$site_path.gb::$index_prefix.gb::$pages_prefix)),'/'), $this->slug) === 0);
  2224. }
  2225. function recommendedName() {
  2226. return 'content/pages/'.$this->slug.$this->recommendedFilenameExtension();
  2227. }
  2228. static function mkCachename($slug) {
  2229. return 'content/pages/'.$slug.gb::$content_cache_fnext;
  2230. }
  2231. static function pathToCached($slug) {
  2232. return parent::pathToCached('pages', $slug . gb::$content_cache_fnext);
  2233. }
  2234. static function findByName($name, $version=null, $applyBodyFilters=true) {
  2235. if (strpos($name, 'content/pages/') !== 0)
  2236. $name = 'content/pages/' . $name;
  2237. return self::find($name, $version, null, $applyBodyFilters);
  2238. }
  2239. static function find($uri_path_or_slug, $version=null, $applyBodyFilters=true) {
  2240. return parent::find($uri_path_or_slug, 'pages', 'GBPage', $version, $applyBodyFilters);
  2241. }
  2242. static function urlTo($slug) {
  2243. return gb::$site_url . gb::$index_prefix . gb::$pages_prefix . $slug;
  2244. }
  2245. function __sleep() {
  2246. return array_merge(parent::__sleep(), array('order', 'hidden'));
  2247. }
  2248. function buildHeaderFields() {
  2249. $header = parent::buildHeaderFields();
  2250. if ($this->order !== null)
  2251. $header['order'] = $this->order;
  2252. if ($this->hidden !== null)
  2253. $header['hidden'] = $this->hidden;
  2254. return $header;
  2255. }
  2256. }
  2257. class GBPost extends GBExposedContent {
  2258. function urlpath() {
  2259. return $this->published->utcformat(gb::$posts_prefix)
  2260. . str_replace('%2F', '/', urlencode($this->slug));
  2261. }
  2262. function cachename() {
  2263. return self::mkCachename($this->published, $this->slug);
  2264. }
  2265. function parseData($data) {
  2266. if ($this->name)
  2267. self::parsePathspec($this->name, $this->published, $this->slug, $fnext);
  2268. return parent::parseData($data);
  2269. }
  2270. function recommendedName() {
  2271. if ($this->published === null || !($this->published instanceof GBDateTime))
  2272. throw new UnexpectedValueException('$this->published is not a valid GBDateTime and needed to create name');
  2273. return 'content/posts/'
  2274. . $this->published->utcformat(gb::$posts_cn_pattern)
  2275. . $this->slug . $this->recommendedFilenameExtension();
  2276. }
  2277. static function mkCachename($published, $slug) {
  2278. # Note: the path prefix is a dependency for GBContentFinalizer::finalize
  2279. return 'content/posts/'.$published->utcformat(gb::$posts_cn_pattern).$slug.gb::$content_cache_fnext;
  2280. }
  2281. static function cachenameFromURI($slug, &$strptime, $return_struct=false) {
  2282. if ($strptime === null || $strptime === false)
  2283. $strptime = strptime($slug, gb::$posts_prefix);
  2284. $prefix = gmstrftime(gb::$posts_cn_pattern, gb_mkutctime($strptime));
  2285. $suffix = $strptime['unparsed'];
  2286. if ($return_struct === true)
  2287. return array($prefix, $suffix);
  2288. return $prefix.$suffix;
  2289. }
  2290. static function pageByPageno($pageno) {
  2291. $path = self::pathToPage($pageno);
  2292. $data = @file_get_contents($path);
  2293. return $data === false ? false : unserialize($data);
  2294. }
  2295. static function pathToPage($pageno) {
  2296. return gb::$site_dir . sprintf('/.git/info/gitblog/content-paged-posts/%011d', $pageno);
  2297. }
  2298. static function pathToCached($slug, $strptime=null) {
  2299. $path = self::cachenameFromURI($slug, $strptime);
  2300. return GBExposedContent::pathToCached('posts', $path . gb::$content_cache_fnext);
  2301. }
  2302. /** Find path to work content of a post. Returns null if not found. */
  2303. static function pathToWork($slug, $strptime=null) {
  2304. $cachename = self::cachenameFromURI($slug, $strptime);
  2305. $basedir = gb::$site_dir.'/content/posts/';
  2306. $glob_skip = '/(\.comments|\/)$/';
  2307. # first, try if the post resides under the cachename, but in the workspace:
  2308. $path = $basedir . $cachename;
  2309. if (is_file($path))
  2310. return $path;
  2311. # try any file with the cachename as prefix
  2312. if ( ($path = gb::glob($path . '*', $glob_skip)) )
  2313. return $path;
  2314. # next, try a wider glob search
  2315. # todo: optimise: find minimum time resolution by examining $posts_cn_pattern
  2316. # for now we will assume the default resolution/granularity of 1 month.
  2317. $path = $basedir
  2318. . gmstrftime('{%Y,%y,%G,%g}{?,/}%m', gb_mkutctime($strptime))
  2319. . '{*,*/*,*/*/*,*/*/*/*}' . $strptime['unparsed'] . '*';
  2320. if ( ($path = gb::glob($path, $glob_skip)) )
  2321. return $path;
  2322. # we're out of luck :(
  2323. return null;
  2324. }
  2325. static function findByDateAndSlug($published, $slug) {
  2326. $path = gb::$site_dir.'/.git/info/gitblog/'.self::mkCachename($published, $slug);
  2327. return @unserialize(file_get_contents($path));
  2328. }
  2329. static function findByName($name, $version=null, $applyBodyFilters=true) {
  2330. if (strpos($name, 'content/posts/') !== 0)
  2331. $name = 'content/posts/' . $name;
  2332. return self::find($name, $version, null, $applyBodyFilters);
  2333. }
  2334. static function find($uri_path_or_slug, $version=null, $strptime=null, $applyBodyFilters=true) {
  2335. $version = self::parseVersion($version);
  2336. $path = false;
  2337. if (strpos($uri_path_or_slug, 'content/posts/') !== false) {
  2338. $path = $uri_path_or_slug;
  2339. if ($path{0} !== '/')
  2340. $path = gb::$site_dir.'/'.$path;
  2341. }
  2342. if ($version === 'work') {
  2343. # find path to raw content
  2344. if ((!$path || !is_file($path)) && ($path = self::pathToWork($uri_path_or_slug, $strptime)) === null)
  2345. return false;
  2346. # parse pathspec, producing date and actual slug needed to look up cached
  2347. try {
  2348. self::parsePathspec(self::pathspecFromAbsPath($path), $date, $slug, $fnext);
  2349. }
  2350. catch(UnexpectedValueException $e) {
  2351. return null;
  2352. }
  2353. # try to find a cached version
  2354. $post = self::findByDateAndSlug($date, $slug);
  2355. # load work
  2356. return self::loadWork($path, $post, 'GBPost', $version, $slug, $applyBodyFilters);
  2357. }
  2358. elseif ($version === null) {
  2359. if ($path) {
  2360. self::parsePathspec(self::pathspecFromAbsPath($path), $date, $slug, $fnext);
  2361. return self::findByDateAndSlug($date, $slug);
  2362. }
  2363. $path = self::pathToCached($uri_path_or_slug, $strptime);
  2364. $data = @file_get_contents($path);
  2365. if ($data === false && gb::$posts_fuzzy_lookup === true) {
  2366. # exact match failed -- try fuzzy matching using glob and our knowledge of patterns
  2367. list($prefix, $suffix) = self::cachenameFromURI($uri_path_or_slug, $strptime, true);
  2368. $path = strtr($prefix, array('/'=>'{/,*,.}','-'=>'{/,*,.}','.'=>'{/,*,.}')).'*'.$suffix;
  2369. $path = GBExposedContent::pathToCached('posts', $path . gb::$content_cache_fnext);
  2370. # try any file with the cachename as prefix
  2371. if ( ($path = gb::glob($path)) ) {
  2372. $data = @file_get_contents($path);
  2373. /*
  2374. Send premanent redirect if we found a valid article.
  2375. Discussion: This is where things might go really wrong -- imagine we did find an
  2376. article on fuzzy matching but it's _another_ article. Fail. But that
  2377. case is almost negligible since we only expand the time prefix, not
  2378. the slug.
  2379. */
  2380. global $gb_handle_request;
  2381. if ($data !== false
  2382. && isset($gb_handle_request) && $gb_handle_request === true
  2383. && ($post = unserialize($data)) && headers_sent() === false )
  2384. {
  2385. header('HTTP/1.1 301 Moved Permanently');
  2386. header('Location: '.$post->url());
  2387. exit('Moved to <a href="'.h($post->url()).'">'.h($post->url()).'</a>');
  2388. }
  2389. }
  2390. }
  2391. return $data === false ? false : unserialize($data);
  2392. }
  2393. throw new Exception('arbitrary version retrieval not yet implemented');
  2394. }
  2395. /**
  2396. * Parse a pathspec for a post into date, slug and file extension.
  2397. *
  2398. * Example:
  2399. *
  2400. * content/posts/2008-08/29-reading-a-book.html
  2401. * date: GBDateTime(2008-08-29T00:00:00Z) (resolution restricted by gb::$posts_cn_pattern)
  2402. * slug: "reading-a-book"
  2403. * fnext: "html"
  2404. */
  2405. static function parsePathspec($pathspec, &$date, &$slug, &$fnext) {
  2406. # cut away prefix "content/posts/"
  2407. $name = substr($pathspec, 14);
  2408. # split filename from filename extension
  2409. $lastdot = strrpos($name, '.', strrpos($name, '/'));
  2410. if ($lastdot !== false) {
  2411. $fnext = substr($name, $lastdot+1);
  2412. $name = substr($name, 0, $lastdot);
  2413. }
  2414. else {
  2415. $fnext = null;
  2416. }
  2417. # parse date and slug
  2418. static $subchars = '._/';
  2419. static $repchars = '---';
  2420. static $ptimes = array(
  2421. '%Y-%m-%d' => 10, '%y-%m-%d' => 8, '%G-%m-%d' => 10, '%g-%m-%d' => 8,
  2422. '%Y-%m' => 7, '%y-%m' => 5, '%G-%m' => 7, '%g-%m' => 5,
  2423. '%Y' => 4, '%y' => 2, '%G' => 4, '%g' => 2
  2424. );
  2425. $nametest = strtr($name, $subchars, $repchars);
  2426. # first, test gb::$posts_cn_pattern
  2427. if (($st = strptime($nametest, strtr(gb::$posts_cn_pattern, $subchars, $repchars))) !== false) {
  2428. $slug = ltrim($st['unparsed'], $subchars . '-');
  2429. }
  2430. else {
  2431. # next, test common patterns with as many items as gb::$posts_cn_pattern
  2432. $ptimes1 = array();
  2433. $n = 0;
  2434. $slug = false;
  2435. if (preg_match_all('/%\w/', gb::$posts_cn_pattern, $m))
  2436. $n = count($m[0]);
  2437. if ($n) {
  2438. if ($n == 1) $ptimes1 = array('%Y' => 4, '%y' => 2, '%G' => 4, '%g' => 2);
  2439. else if ($n == 2) $ptimes1 = array('%Y-%m' => 7, '%y-%m' => 5, '%G-%m' => 7, '%g-%m' => 5);
  2440. else if ($n == 3) $ptimes1 = array('%Y-%m-%d' => 10, '%y-%m-%d' => 8, '%G-%m-%d' => 10, '%g-%m-%d' => 8);
  2441. foreach ($ptimes1 as $pattern => $pattern_len) {
  2442. if (($st = strptime($nametest, $pattern)) !== false) {
  2443. $slug = ltrim(substr($name, $pattern_len), $subchars . '-');
  2444. break;
  2445. }
  2446. }
  2447. }
  2448. if ($slug === false) {
  2449. # finally, try a series of common patterns
  2450. foreach ($ptimes as $pattern => $pattern_len) {
  2451. if (($st = strptime($nametest, $pattern)) !== false) {
  2452. $slug = ltrim(substr($name, $pattern_len), $subchars . '-');
  2453. break;
  2454. }
  2455. }
  2456. }
  2457. }
  2458. # failed to parse
  2459. if ($st === false)
  2460. throw new UnexpectedValueException('unable to parse date from '.var_export($pathspec,1));
  2461. $date = gmstrftime('%FT%T+00:00', gb_mkutctime($st));
  2462. #$slug = ltrim($st['unparsed'], '-');
  2463. $date = new GBDateTime($date.'T00:00:00Z');
  2464. }
  2465. }
  2466. class GBComments extends GBContent implements IteratorAggregate, Countable {
  2467. /** [GBComment, ..] */
  2468. public $comments = array();
  2469. public $cachenamePrefix;
  2470. function __construct($name=null, $id=null, $cachenamePrefix=null, $comments=null) {
  2471. parent::__construct($name, $id);
  2472. $this->cachenamePrefix = $cachenamePrefix;
  2473. if ($comments !== null)
  2474. $this->comments = $comments;
  2475. }
  2476. function reload($data, $commits) {
  2477. parent::reload($data, $commits);
  2478. # apply info from commits, like publish date and author
  2479. $this->applyInfoFromCommits($commits);
  2480. # load actual comments
  2481. $db = new GBCommentDB();
  2482. $db->loadString($data);
  2483. $this->comments = $db->get();
  2484. # apply filters
  2485. gb_cfilter::apply('post-reload-comments', $this);
  2486. }
  2487. # these two are not serialized, but lazy-initialized by count()
  2488. public $_countTotal;
  2489. public $_countSpam;
  2490. public $_countApproved;
  2491. public $_countApprovedTopo;
  2492. /** Recursively count how many comments are in the $comments member */
  2493. function count($c=null) {
  2494. if ($c === null)
  2495. $c = $this;
  2496. if ($c->_countTotal !== null)
  2497. return $c->_countTotal;
  2498. $c->_countTotal = $c->_countApproved = $c->_countApprovedTopo = $c->_countSpam = 0;
  2499. if (!$c->comments)
  2500. return 0;
  2501. foreach ($c->comments as $comment) {
  2502. $c->_countTotal++;
  2503. if ($comment->approved) {
  2504. $c->_countApproved++;
  2505. $c->_countApprovedTopo++;
  2506. }
  2507. if ($comment->spam === true)
  2508. $c->_countSpam++;
  2509. $this->count($comment);
  2510. $c->_countTotal += $comment->_countTotal;
  2511. if ($comment->approved)
  2512. $c->_countApprovedTopo += $comment->_countApprovedTopo;
  2513. $c->_countSpam += $comment->_countSpam;
  2514. $c->_countApproved += $comment->_countApproved;
  2515. }
  2516. return $c->_countTotal;
  2517. }
  2518. function countApproved($topological=true) {
  2519. $this->count();
  2520. return $topological ? $this->_countApprovedTopo : $this->_countApproved;
  2521. }
  2522. /**
  2523. * Number of comments which are approved -- by their parent comments are
  2524. * not -- thus they are topologically speaking shadowed, or hidden.
  2525. */
  2526. function countShadowed() {
  2527. $this->count();
  2528. return $this->_countTotal - ($this->_countApprovedTopo + ($this->_countTotal - $this->_countApproved));
  2529. }
  2530. function countUnapproved($excluding_spam=true) {
  2531. $this->count();
  2532. return $this->_countTotal - $this->_countApproved - ($excluding_spam ? $this->_countSpam : 0);
  2533. }
  2534. function countSpam() {
  2535. $this->count();
  2536. return $this->_countSpam;
  2537. }
  2538. function cachename() {
  2539. if (!$this->cachenamePrefix)
  2540. throw new UnexpectedValueException('cachenamePrefix is empty or null');
  2541. return gb_filenoext($this->cachenamePrefix).gb::$comments_cache_fnext;
  2542. }
  2543. static function find($cachenamePrefix) {
  2544. $path = gb::$site_dir.'/.git/info/gitblog/'.gb_filenoext($cachenamePrefix).gb::$comments_cache_fnext;
  2545. return @unserialize(file_get_contents($path));
  2546. }
  2547. # implementation of IteratorAggregate
  2548. public function getIterator($onlyApproved=true) {
  2549. return new GBCommentsIterator($this, $onlyApproved);
  2550. }
  2551. function __sleep() {
  2552. return array_merge(parent::__sleep(), array('comments', 'cachenamePrefix'));
  2553. }
  2554. function toBlob() {
  2555. $db = new GBCommentDB();
  2556. $db->set($this->comments);
  2557. return $db->encodeData();
  2558. }
  2559. }
  2560. /**
  2561. * Comments iterator. Accessible from GBComments::iterator
  2562. */
  2563. class GBCommentsIterator implements Iterator {
  2564. protected $initialComment;
  2565. protected $comments;
  2566. protected $stack;
  2567. protected $idstack;
  2568. public $onlyApproved;
  2569. public $maxStackDepth;
  2570. function __construct($comment, $onlyApproved=false, $maxStackDepth=50) {
  2571. $this->initialComment = $comment;
  2572. $this->onlyApproved = $onlyApproved;
  2573. $this->maxStackDepth = $maxStackDepth;
  2574. }
  2575. function rewind() {
  2576. $this->comments = $this->initialComment->comments;
  2577. reset($this->comments);
  2578. $this->stack = array();
  2579. $this->idstack = array();
  2580. }
  2581. function current() {
  2582. $comment = current($this->comments);
  2583. if ($comment && $comment->id === null) {
  2584. $comment->id = $this->idstack;
  2585. $comment->id[] = key($this->comments);
  2586. $comment->id = implode('.', $comment->id);
  2587. }
  2588. return $comment;
  2589. }
  2590. function key() {
  2591. return count($this->idstack);
  2592. }
  2593. function next() {
  2594. $comment = current($this->comments);
  2595. if ($comment === false)
  2596. return;
  2597. if ($comment->comments && (!$this->onlyApproved || ($this->onlyApproved && $comment->approved)) ) {
  2598. # push current comments on stack
  2599. if (count($this->stack) === $this->maxStackDepth)
  2600. throw new OverflowException('stack depth > '.$this->maxStackDepth);
  2601. array_push($this->stack, $this->comments);
  2602. array_push($this->idstack, key($this->comments));
  2603. $this->comments = $comment->comments;
  2604. }
  2605. else {
  2606. next($this->comments);
  2607. }
  2608. # fast-forward to next approved comment, if applicable
  2609. if ($this->onlyApproved) {
  2610. $comment = current($this->comments);
  2611. if ($comment && !$comment->approved) {
  2612. #var_dump('FWD from unapproved comment '.$this->current()->id);
  2613. $this->next();
  2614. }
  2615. elseif (!$comment) {
  2616. #var_dump('VALIDATE');
  2617. $this->valid();
  2618. $comment = current($this->comments);
  2619. if ($comment === false || !$comment->approved)
  2620. $this->next();
  2621. }
  2622. }
  2623. }
  2624. function valid() {
  2625. if (key($this->comments) === null) {
  2626. if ($this->stack) {
  2627. # end of branch -- pop the stack
  2628. while ($this->stack) {
  2629. array_pop($this->idstack);
  2630. $this->comments = array_pop($this->stack);
  2631. next($this->comments);
  2632. if (key($this->comments) !== null || !$this->stack)
  2633. break;
  2634. }
  2635. return key($this->comments) !== null;
  2636. }
  2637. else {
  2638. return false;
  2639. }
  2640. }
  2641. if ($this->onlyApproved) {
  2642. $comment = current($this->comments);
  2643. while ($comment && !$comment->approved) {
  2644. $this->next();
  2645. $comment = current($this->comments);
  2646. }
  2647. return $comment ? true : false;
  2648. }
  2649. return true;
  2650. }
  2651. }
  2652. /**
  2653. * Comments object.
  2654. *
  2655. * Accessible from GBComments
  2656. * Managed through GBCommentDB
  2657. */
  2658. class GBComment {
  2659. const TYPE_COMMENT = 'c';
  2660. const TYPE_PINGBACK = 'p';
  2661. public $date;
  2662. public $ipAddress;
  2663. public $email;
  2664. public $uri;
  2665. public $name;
  2666. public $body;
  2667. public $approved;
  2668. public $comments;
  2669. public $type;
  2670. # members below are only serialized when non-null
  2671. public $spam;
  2672. # members below are never serialized
  2673. /** GBExposedContent set when appropriate. Might be null. */
  2674. public $post;
  2675. /** String id (indexpath), available during iteration, etc. */
  2676. public $id;
  2677. /* these two are not serialized, but lazy-initialized by GBComments::count() */
  2678. public $_countTotal;
  2679. public $_countApproved;
  2680. public $_countApprovedTopo;
  2681. /**
  2682. * Allowed tags, primarily used by GBFilter but here so that themes and
  2683. * other stuff can read it.
  2684. */
  2685. static public $allowedTags = array(
  2686. # tagname => allowed attributes
  2687. 'a' => array('href', 'target', 'rel', 'name'),
  2688. 'strong' => array(),
  2689. 'b' => array(),
  2690. 'blockquote' => array(),
  2691. 'em' => array(),
  2692. 'i' => array(),
  2693. 'img' => array('src', 'width', 'height', 'alt', 'title'),
  2694. 'u' => array(),
  2695. 's' => array(),
  2696. 'del' => array()
  2697. );
  2698. function __construct($state=array()) {
  2699. $this->type = self::TYPE_COMMENT;
  2700. if ($state) {
  2701. foreach ($state as $k => $v) {
  2702. if ($k === 'comments' && $v !== null) {
  2703. foreach ($v as $k2 => $v2)
  2704. $v[$k2] = new self($v2);
  2705. }
  2706. elseif ($k === 'date' && $v !== null) {
  2707. if (is_string($v))
  2708. $v = new GBDateTime($v);
  2709. elseif (is_array($v))
  2710. $v = new GBDateTime($v['time'], $v['offset']);
  2711. }
  2712. $this->$k = $v;
  2713. }
  2714. }
  2715. }
  2716. function body() {
  2717. return gb::filter('comment-body', $this->body);
  2718. }
  2719. function textBody() {
  2720. return trim(preg_replace('/<[^>]*>/m', ' ', $this->body()));
  2721. }
  2722. function duplicate(GBComment $other) {
  2723. return (($this->email === $other->email) && ($this->body === $other->body));
  2724. }
  2725. function gitAuthor() {
  2726. return ($this->name ? $this->name.' ' : '').($this->email ? '<'.$this->email.'>' : '');
  2727. }
  2728. function append(GBComment $comment) {
  2729. if ($this->comments === null) {
  2730. $k = 1;
  2731. $this->comments = array(1 => $comment);
  2732. }
  2733. else {
  2734. $k = array_pop(array_keys($this->comments))+1;
  2735. $this->comments[$k] = $comment;
  2736. }
  2737. return $k;
  2738. }
  2739. function nameLink($attrs='') {
  2740. if ($this->uri)
  2741. return '<a href="'.h($this->uri).'" '.$attrs.'>'.h($this->name).'</a>';
  2742. elseif ($attrs)
  2743. return '<span '.$attrs.'>'.h($this->name).'</span>';
  2744. else
  2745. return h($this->name);
  2746. }
  2747. function avatarURL($size=48, $fallback_url='') {
  2748. $s = 'http://www.gravatar.com/avatar.php?gravatar_id='
  2749. .md5($this->email)
  2750. .'&size='.$size;
  2751. if ($fallback_url) {
  2752. if ($fallback_url{0} !== '/')
  2753. $fallback_url = gb::$theme_url . $fallback_url;
  2754. $s .= '&default='. urlencode($fallback_url);
  2755. }
  2756. return $s;
  2757. }
  2758. function _post($post=null) {
  2759. if ($post === null) {
  2760. if ($this->post !== null) {
  2761. $post = $this->post;
  2762. }
  2763. else {
  2764. unset($post);
  2765. global $post;
  2766. }
  2767. }
  2768. if (!$post)
  2769. throw new UnexpectedValueException('unable to deduce $post needed to build url');
  2770. return $post;
  2771. }
  2772. function commentsObject($post=null) {
  2773. return $this->_post($post)->comments;
  2774. }
  2775. function _bounceURL($relpath, $post=null, $include_referrer=true) {
  2776. $post = $this->_post($post);
  2777. if ($this->id === null)
  2778. throw new UnexpectedValueException('$this->id is null');
  2779. $object = strpos(gb::$content_cache_fnext,'.') !== false ? gb_filenoext($post->cachename()) : $post->cachename();
  2780. return gb::$site_url.$relpath.'object='
  2781. .urlencode($object)
  2782. .'&comment='.$this->id
  2783. .($include_referrer ? '&referrer='.urlencode(gb::url()) : '');
  2784. }
  2785. function approveURL($post=null, $include_referrer=true) {
  2786. return $this->_bounceURL('gitblog/admin/helpers/approve-comment.php?action=approve&',
  2787. $post, $include_referrer);
  2788. }
  2789. function unapproveURL($post=null, $include_referrer=true) {
  2790. return $this->_bounceURL('gitblog/admin/helpers/approve-comment.php?action=unapprove&',
  2791. $post, $include_referrer);
  2792. }
  2793. function hamURL($post=null, $include_referrer=true) {
  2794. return $this->_bounceURL('gitblog/admin/helpers/spam-comment.php?action=ham&',
  2795. $post, $include_referrer);
  2796. }
  2797. function spamURL($post=null, $include_referrer=true) {
  2798. return $this->_bounceURL('gitblog/admin/helpers/spam-comment.php?action=spam&',
  2799. $post, $include_referrer);
  2800. }
  2801. function removeURL($post=null, $include_referrer=true) {
  2802. return $this->_bounceURL('gitblog/admin/helpers/remove-comment.php?',
  2803. $post, $include_referrer);
  2804. }
  2805. function commentURL($post=null) {
  2806. $post = $this->_post($post);
  2807. if ($this->id === null)
  2808. throw new UnexpectedValueException('$this->id is null');
  2809. return $post->url().'#comment-'.$this->id;
  2810. }
  2811. function __sleep() {
  2812. $members = array('date','ipAddress','email','uri','name','body',
  2813. 'approved','comments','type','id');
  2814. if ($this->spam !== null)
  2815. $members[] = 'spam';
  2816. return $members;
  2817. }
  2818. }
  2819. # -----------------------------------------------------------------------------
  2820. # Nonce
  2821. function gb_nonce_time($ttl) {
  2822. return (int)ceil(time() / $ttl);
  2823. }
  2824. function gb_nonce_make($context='', $ttl=86400) {
  2825. return gb_hash(gb_nonce_time($ttl) . $context . $_SERVER['REMOTE_ADDR']);
  2826. }
  2827. function gb_nonce_verify($nonce, $context='', $ttl=86400) {
  2828. $nts = gb_nonce_time($ttl);
  2829. # generated (0-12] hours ago
  2830. if ( gb_hash($nts . $context . $_SERVER['REMOTE_ADDR']) === $nonce )
  2831. return 1;
  2832. # generated (12-24) hours ago
  2833. if ( gb_hash(($nts - 1) . $context . $_SERVER['REMOTE_ADDR']) === $nonce )
  2834. return 2;
  2835. # Invalid nonce
  2836. return false;
  2837. }
  2838. # -----------------------------------------------------------------------------
  2839. # Author cookie
  2840. class gb_author_cookie {
  2841. static public $cookie;
  2842. static function set($email=null, $name=null, $url=null, $cookiename='gb-author') {
  2843. if (self::$cookie === null)
  2844. self::$cookie = array();
  2845. if ($email !== null) self::$cookie['email'] = $email;
  2846. if ($name !== null) self::$cookie['name'] = $name;
  2847. if ($url !== null) self::$cookie['url'] = $url;
  2848. $cookie = rawurlencode(serialize(self::$cookie));
  2849. $cookieurl = new GBURL(gb::$site_url);
  2850. setrawcookie($cookiename, $cookie, time()+(3600*24*365), $cookieurl->path, $cookieurl->host, $cookieurl->secure);
  2851. }
  2852. static function get($part=null, $cookiename='gb-author') {
  2853. if (self::$cookie === null) {
  2854. if (isset($_COOKIE[$cookiename])) {
  2855. $s = get_magic_quotes_gpc() ? stripslashes($_COOKIE[$cookiename]) : $_COOKIE[$cookiename];
  2856. self::$cookie = @unserialize($s);
  2857. }
  2858. if (!self::$cookie)
  2859. self::$cookie = array();
  2860. }
  2861. if ($part === null)
  2862. return self::$cookie;
  2863. return isset(self::$cookie[$part]) ? self::$cookie[$part] : null;
  2864. }
  2865. }
  2866. # -----------------------------------------------------------------------------
  2867. # Template helpers
  2868. gb::$title = array(gb::$site_title);
  2869. function gb_head() {
  2870. echo '<meta name="generator" content="Gitblog '.gb::$version."\" />\n";
  2871. gb::event('on-html-head');
  2872. }
  2873. function gb_footer() {
  2874. gb::event('on-html-footer');
  2875. }
  2876. function gb_title($glue=' — ', $html=true) {
  2877. $s = implode($glue, array_reverse(gb::$title));
  2878. return $html ? h($s) : $s;
  2879. }
  2880. function gb_site_title($link=true, $linkattrs='') {
  2881. if (!$link)
  2882. return h(gb::$site_title);
  2883. return '<a href="'.gb::$site_url.'"'.$linkattrs.'>'.h(gb::$site_title).'</a>';
  2884. }
  2885. function h($s) {
  2886. return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
  2887. }
  2888. function gb_nonce_field($context='', $referrer=true, $id_prefix='', $name='gb-nonce') {
  2889. $nonce = gb_nonce_make($context);
  2890. $name = h($name);
  2891. $html = '<input type="hidden" id="' . $id_prefix.$name . '" name="' . $name
  2892. . '" value="' . $nonce . '" />';
  2893. if ($referrer)
  2894. $html .= '<input type="hidden" name="gb-referrer" value="'. h(gb::url()) . '" />';
  2895. return $html;
  2896. }
  2897. function gb_timezone_offset_field($id='client-timezone-offset') {
  2898. return '<input type="hidden" id="'.$id.'" name="client-timezone-offset" value="" />'
  2899. . '<script type="text/javascript">'."\n//<![CDATA[\n"
  2900. . 'document.getElementById("'.$id.'").value = -((new Date()).getTimezoneOffset()*60);'
  2901. ."\n//]]></script>";
  2902. }
  2903. function gb_comment_author_field($what, $default_value='', $id_prefix='comment-', $attrs='') {
  2904. $value = gb_author_cookie::get($what);
  2905. if (!$value) {
  2906. $value = $default_value;
  2907. $attrs .= ' class="default" ';
  2908. }
  2909. return '<input type="text" id="'.$id_prefix.'author-'.$what.'" name="author-'
  2910. .$what.'" value="'.h($value).'"'
  2911. .' onfocus="if(this.value==unescape(\''.rawurlencode($default_value).'\')){this.value=\'\';this.className=\'\';}"'
  2912. .' onblur="if(this.value==\'\'){this.value=unescape(\''.rawurlencode($default_value).'\');this.className=\'default\';}"'
  2913. .' '.$attrs.' />';
  2914. }
  2915. function gb_comment_fields($post=null, $id_prefix='comment-') {
  2916. if ($post === null) {
  2917. unset($post);
  2918. global $post;
  2919. }
  2920. $post_cachename = strpos(gb::$content_cache_fnext,'.') !== false ? gb_filenoext($post->cachename()) : $post->cachename();
  2921. $nonce_context = 'post-comment-'.$post_cachename;
  2922. return gb_nonce_field($nonce_context, true, $id_prefix)
  2923. . gb_timezone_offset_field($id_prefix.'client-timezone-offset')
  2924. . '<input type="hidden" id="'.$id_prefix.'reply-post" name="reply-post" value="'.h($post_cachename).'" />'
  2925. . '<input type="hidden" id="'.$id_prefix.'reply-to" name="reply-to" value="" />';
  2926. }
  2927. function gb_tag_link($tag, $template='<a href="%u">%n</a>') {
  2928. $u = gb::$site_url . gb::$index_prefix . gb::$tags_prefix;
  2929. return strtr($template, array('%u' => $u.urlencode($tag), '%n' => h($tag)));
  2930. }
  2931. function sorted($iterable, $reverse_or_sortfunc=null, $sort_flags=SORT_REGULAR) {
  2932. if ($reverse_or_sortfunc === null || $reverse_or_sortfunc === false)
  2933. asort($iterable, $sort_flags);
  2934. elseif ($reverse_or_sortfunc === true)
  2935. arsort($iterable, $sort_flags);
  2936. else
  2937. uasort($iterable, $reverse_or_sortfunc);
  2938. return $iterable;
  2939. }
  2940. function ksorted($iterable, $reverse_or_sortfunc=null, $sort_flags=SORT_REGULAR) {
  2941. if ($reverse_or_sortfunc === null || $reverse_or_sortfunc === false)
  2942. ksort($iterable, $sort_flags);
  2943. elseif ($reverse_or_sortfunc === true)
  2944. krsort($iterable, $sort_flags);
  2945. else
  2946. uksort($iterable, $reverse_or_sortfunc);
  2947. return $iterable;
  2948. }
  2949. /**
  2950. * Ordinalize turns a number into an ordinal string used to denote the
  2951. * position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
  2952. *
  2953. * Examples
  2954. * ordinalize(1) -> "1st"
  2955. * ordinalize(2) -> "2nd"
  2956. * ordinalize(1002) -> "1002nd"
  2957. * ordinalize(1003) -> "1003rd"
  2958. */
  2959. function ordinalize($number) {
  2960. $i = intval($number);
  2961. $h = $i % 100;
  2962. if ($h === 11 || $h === 12 || $h === 13)
  2963. return $i.'th';
  2964. else {
  2965. $x = $i % 10;
  2966. if ($x === 1)
  2967. return $i.'st';
  2968. elseif ($x === 2)
  2969. return $i.'nd';
  2970. elseif ($x === 3)
  2971. return $i.'rd';
  2972. else
  2973. return $i.'th';
  2974. }
  2975. }
  2976. /**
  2977. * Counted turns a number into $zero if $n is 0, $one if $n is 1 or
  2978. * otherwise $n
  2979. *
  2980. * Examples:
  2981. * counted(0) -> "No"
  2982. * counted(1, 'comment', 'comments', 'No', 'One') -> "One comment"
  2983. * counted(7, 'comment', 'comments') -> "7 comments"
  2984. */
  2985. function counted($n, $sone='', $smany='', $zero='No', $one='One') {
  2986. if ($sone)
  2987. $sone = ' '.ltrim($sone);
  2988. if ($smany)
  2989. $smany = ' '.ltrim($smany);
  2990. return $n === 0 ? $zero.$smany : ($n === 1 ? $one.$sone : strval($n).$smany);
  2991. }
  2992. function sentenceize($collection, $applyfunc=null, $nglue=', ', $endglue=' and ') {
  2993. if (!$collection)
  2994. return '';
  2995. if ($applyfunc)
  2996. $collection = array_map($applyfunc, $collection);
  2997. $n = count($collection);
  2998. if ($n === 1)
  2999. return $collection[0];
  3000. else {
  3001. $end = array_pop($collection);
  3002. return implode($nglue, $collection).$endglue.$end;
  3003. }
  3004. }
  3005. # -----------------------------------------------------------------------------
  3006. /**
  3007. * Request handler.
  3008. *
  3009. * Activated by setting $gb_handle_request = true before loading gitblog.php.
  3010. * This construction is intended for themes.
  3011. *
  3012. * Example of a theme:
  3013. *
  3014. * <?
  3015. * $gb_handle_request = true;
  3016. * require './gitblog/gitblog.php';
  3017. * # send response based on gb::$is_* properties ...
  3018. * ?>
  3019. *
  3020. * Global variables:
  3021. *
  3022. * - $gb_request_uri
  3023. * Always available when parsing the request and contains the requested
  3024. * path -- anything after gb::$index_prefix. For example: "2009/07/some-post"
  3025. * when the full url (aquireable through gb::url()) might be
  3026. * "http://host/blog/2009/07/some-post"
  3027. *
  3028. * - $gb_time_started
  3029. * Always available and houses the microtime(true) when gitblog started to
  3030. * execute. This is used by various internal mechanisms, so please do not
  3031. * alter the value or Bad Things (TM) might happen.
  3032. *
  3033. * - $post
  3034. * Available for requests of posts and pages in which case its value is an
  3035. * instance of GBExposedContent (or a subclass thereof). However; the value
  3036. * is false if gb::$is_404 is set.
  3037. *
  3038. * - $postspage
  3039. * Available for requests which involve pages of content: the home page,
  3040. * feed (gb::$feed_prefix), content filed under a category/ies
  3041. * (gb::$categories_prefix), content taged with certain tags
  3042. * (gb::$tags_prefix).
  3043. *
  3044. * - $tags
  3045. * Available for requests listing content taged with certain tags
  3046. * (gb::$tags_prefix).
  3047. *
  3048. * - $categories
  3049. * Available for requests listing content taged with certain tags
  3050. * (gb::$categories_prefix).
  3051. *
  3052. * Events:
  3053. *
  3054. * - "will-parse-request"
  3055. * Posted before gitblog parses the request. For example, altering the
  3056. * global variable $gb_request_uri will cause gitblog to handle a
  3057. * different request than initially intended.
  3058. *
  3059. * - "will-handle-request"
  3060. * Posted after the request has been parsed but before gitblog handles it.
  3061. *
  3062. * - "did-handle-request"
  3063. * Posted after the request have been handled but before any deferred code
  3064. * is executed.
  3065. *
  3066. * When observing these events, the Global variables and the gb::$is_*
  3067. * properties should provide good grounds for taking descisions and/or changing
  3068. * the outcome.
  3069. *
  3070. * Before the request is parsed, any activated "online" plugins will be given
  3071. * a chance to initialize.
  3072. */
  3073. if (isset($gb_handle_request) && $gb_handle_request === true) {
  3074. if (gb::$request_query === 'PATH_INFO')
  3075. $gb_request_uri = isset($_SERVER['PATH_INFO']) ? trim($_SERVER['PATH_INFO'], '/') : '';
  3076. else
  3077. $gb_request_uri = isset($_GET[gb::$request_query]) ? trim($_GET[gb::$request_query], '/') : '';
  3078. # temporary, non-exported variables
  3079. $version = null;
  3080. $strptime = null;
  3081. $preview_pathspec = null;
  3082. # verify integrity and config
  3083. gb::verify();
  3084. # authed?
  3085. if (isset($_COOKIE['gb-chap']) && $_COOKIE['gb-chap']) {
  3086. gb::authenticate(false);
  3087. # now, gb::$authorized (a GBUser) is set (authed ok) or a CHAP
  3088. # constant (not authed).
  3089. }
  3090. # transfer errors from ?gb-error to gb::$errors
  3091. if (isset($_GET['gb-error']) && $_GET['gb-error']) {
  3092. if (is_array($_GET['gb-error']))
  3093. gb::$errors = array_merge(gb::$errors, $_GET['gb-error']);
  3094. else
  3095. gb::$errors[] = $_GET['gb-error'];
  3096. }
  3097. # preview mode?
  3098. if (isset($_GET[gb::$preview_query_key]) && gb::$authorized) {
  3099. gb::$is_preview = true;
  3100. $version = 'work';
  3101. }
  3102. elseif (isset($_GET[gb::$version_query_key]) && gb::$authorized) {
  3103. gb::$is_preview = true;
  3104. $version = $_GET[gb::$version_query_key];
  3105. }
  3106. if (gb::$is_preview === true && isset($_GET[gb::$pathspec_query_key])) {
  3107. $preview_pathspec = $_GET[gb::$pathspec_query_key];
  3108. }
  3109. if (gb::$is_preview)
  3110. header('Cache-Control: no-cache');
  3111. # load plugins
  3112. gb::load_plugins('request');
  3113. gb::event('will-parse-request');
  3114. register_shutdown_function(array('gb','event'), 'did-handle-request');
  3115. if ($gb_request_uri) {
  3116. if (strpos($gb_request_uri, gb::$categories_prefix) === 0) {
  3117. # category(ies)
  3118. $categories = array_map('urldecode', explode(',',
  3119. substr($gb_request_uri, strlen(gb::$categories_prefix))));
  3120. $postspage = GBExposedContent::findByCategories($categories,
  3121. isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 0);
  3122. gb::$is_categories = true;
  3123. gb::$is_404 = $postspage === false;
  3124. }
  3125. elseif (strpos($gb_request_uri, gb::$tags_prefix) === 0) {
  3126. # tag(s)
  3127. $tags = array_map('urldecode', explode(',',
  3128. substr($gb_request_uri, strlen(gb::$tags_prefix))));
  3129. $postspage = GBExposedContent::findByTags($tags,
  3130. isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 0);
  3131. gb::$is_tags = true;
  3132. gb::$is_404 = $postspage === false;
  3133. }
  3134. elseif (strpos($gb_request_uri, gb::$feed_prefix) === 0) {
  3135. # feed
  3136. $postspage = GBPost::pageByPageno(0);
  3137. gb::$is_feed = true;
  3138. gb::event('will-handle-request');
  3139. # if we got this far it means no event observers took over this task, so
  3140. # we run the built-in feed (Atom) code:
  3141. require gb::$dir.'/helpers/feed.php';
  3142. exit;
  3143. }
  3144. elseif (gb::$posts_prefix === '' || ($strptime = strptime($gb_request_uri, gb::$posts_prefix)) !== false) {
  3145. # post
  3146. if ($preview_pathspec !== null)
  3147. $post = GBPost::findByName($preview_pathspec, $version);
  3148. else
  3149. $post = GBPost::find(urldecode($gb_request_uri), $version, $strptime);
  3150. if ($post === false)
  3151. gb::$is_404 = true;
  3152. else
  3153. gb::$title[] = $post->title;
  3154. gb::$is_post = true;
  3155. # empty prefix and 404 -- try page
  3156. if (gb::$is_404 === true && gb::$posts_prefix === '') {
  3157. if ($preview_pathspec !== null)
  3158. $post = GBPage::findByName($preview_pathspec, $version);
  3159. else
  3160. $post = GBPage::find(urldecode($gb_request_uri), $version);
  3161. if ($post !== false) {
  3162. gb::$title[] = $post->title;
  3163. gb::$is_404 = false;
  3164. }
  3165. gb::$is_post = false;
  3166. gb::$is_page = true;
  3167. }
  3168. }
  3169. else {
  3170. # page
  3171. if ($preview_pathspec !== null)
  3172. $post = GBPage::findByName($preview_pathspec, $version);
  3173. else
  3174. $post = GBPage::find(urldecode($gb_request_uri), $version);
  3175. if ($post === false)
  3176. gb::$is_404 = true;
  3177. else
  3178. gb::$title[] = $post->title;
  3179. gb::$is_page = true;
  3180. }
  3181. # post 404?
  3182. if (isset($post) && $post && gb::$is_preview === false && ($post->draft === true || $post->published->time > time()))
  3183. gb::$is_404 = true;
  3184. }
  3185. else {
  3186. # posts
  3187. $postspage = GBPost::pageByPageno(isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 0);
  3188. gb::$is_posts = true;
  3189. gb::$is_404 = $postspage === false;
  3190. }
  3191. # unset temporary variables (not polluting global namespace)
  3192. unset($preview_pathspec);
  3193. unset($strptime);
  3194. unset($version);
  3195. gb::event('will-handle-request');
  3196. # from here on, the caller will have to do the rest
  3197. }
  3198. ?>