PageRenderTime 80ms CodeModel.GetById 38ms 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

Large files files are truncated, but you can click here to view the full 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) {

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