├── .gitignore ├── admin ├── res │ ├── scheduled.png │ ├── scheduled.psd │ ├── arrow-deco-up.png │ ├── default-avatar.png │ ├── default-avatar.psd │ ├── throbber-small-black.png │ ├── jquery.json-1.3.min.js │ ├── sha1-min.js │ └── gb-admin.js ├── helpers │ ├── deauthorize.php │ ├── authorize.php │ ├── remove-comment.php │ ├── view-commit.php │ ├── save-post.php │ └── data.php ├── _footer.php ├── _base.php ├── index.php ├── maintenance │ ├── rebuild.php │ └── git-status.php ├── _header.php ├── manage │ ├── comments.php │ └── posts.php ├── settings │ └── basics.php └── setup.php ├── index.php ├── lib ├── GitUninitializedRepoError.php ├── GBObjectStore.php ├── PHPException.php ├── GitError.php ├── GBUser.php ├── GBBenchmark.php ├── GBHTTPDigestAuth.php ├── GBRebuilder.php ├── gb_input.php ├── GitPatch.php ├── JSONStore.php ├── FileDB.php ├── CHAP.php ├── gb_upgrade.php ├── gb_admin.php ├── GitCommit.php ├── json.php └── GBCommentDB.php ├── themes └── default │ ├── icons16.png │ ├── icons16.psd │ ├── icons48.png │ ├── icons48.psd │ ├── header-bg.png │ ├── inconsolata.otf │ ├── reply-over.png │ ├── default-avatar.png │ ├── default-avatar.psd │ ├── liberation-serif-bold.ttf │ ├── liberation-serif-italic.ttf │ ├── liberation-serif-regular.ttf │ ├── liberation-serif-bold-italic.ttf │ ├── sidebar.php │ ├── comment.js │ ├── posts.php │ ├── index.php │ └── post.php ├── skeleton ├── content │ ├── pages │ │ ├── about.html │ │ └── about │ │ │ └── intro.html │ └── posts │ │ └── 0000-00-00-hello-world.html ├── lighttpd.conf ├── gitignore ├── data │ └── plugins.json ├── apache2.htaccess ├── hooks │ ├── post-update │ └── post-commit └── gb-config.php ├── helpers ├── README.md ├── feed.php └── post-comment.php ├── hooks ├── post-patch.php ├── post-update.php └── post-update.sh ├── docs ├── filters.md ├── themes.md ├── events.md ├── core-devel │ ├── thoughts.txt │ ├── will-begin-request.php │ ├── comments.txt │ └── example.comments ├── site.json.md ├── plugins.md └── content.md ├── LICENSE ├── plugins ├── google-analytics.php ├── feedburner.php ├── php-content.php ├── email-notification.php ├── code-blocks.php └── akismet.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /site 2 | /gb-config.php 3 | .DS_Store 4 | ._* 5 | * copy.* 6 | -------------------------------------------------------------------------------- /admin/res/scheduled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/admin/res/scheduled.png -------------------------------------------------------------------------------- /admin/res/scheduled.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/admin/res/scheduled.psd -------------------------------------------------------------------------------- /admin/res/arrow-deco-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/admin/res/arrow-deco-up.png -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/GitUninitializedRepoError.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /themes/default/icons16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/icons16.png -------------------------------------------------------------------------------- /themes/default/icons16.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/icons16.psd -------------------------------------------------------------------------------- /themes/default/icons48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/icons48.png -------------------------------------------------------------------------------- /themes/default/icons48.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/icons48.psd -------------------------------------------------------------------------------- /admin/helpers/deauthorize.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/res/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/admin/res/default-avatar.png -------------------------------------------------------------------------------- /admin/res/default-avatar.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/admin/res/default-avatar.psd -------------------------------------------------------------------------------- /themes/default/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/header-bg.png -------------------------------------------------------------------------------- /themes/default/inconsolata.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/inconsolata.otf -------------------------------------------------------------------------------- /themes/default/reply-over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/reply-over.png -------------------------------------------------------------------------------- /admin/res/throbber-small-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/admin/res/throbber-small-black.png -------------------------------------------------------------------------------- /themes/default/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/default-avatar.png -------------------------------------------------------------------------------- /themes/default/default-avatar.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/default-avatar.psd -------------------------------------------------------------------------------- /themes/default/liberation-serif-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/liberation-serif-bold.ttf -------------------------------------------------------------------------------- /themes/default/liberation-serif-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/liberation-serif-italic.ttf -------------------------------------------------------------------------------- /themes/default/liberation-serif-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/liberation-serif-regular.ttf -------------------------------------------------------------------------------- /themes/default/liberation-serif-bold-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/gitblog/HEAD/themes/default/liberation-serif-bold-italic.ttf -------------------------------------------------------------------------------- /skeleton/content/pages/about.html: -------------------------------------------------------------------------------- 1 | title: About 2 | 3 | Hi and welcome to my awesome blog. 4 | 5 | Change this content by editing and committing content/pages/about.html 6 | -------------------------------------------------------------------------------- /skeleton/content/pages/about/intro.html: -------------------------------------------------------------------------------- 1 | title: About 2 | 3 | Hi and welcome to my awesome blog. 4 | 5 | Change me by editing and committing content/pages/about/intro.html 6 | -------------------------------------------------------------------------------- /skeleton/lighttpd.conf: -------------------------------------------------------------------------------- 1 | $HTTP["host"] == "your.hostname" { 2 | url.rewrite-once = ( 3 | "^(/(?!gitblog|attachments|index\.php|favicon\.ico).+)$" => "/index.php/$1" 4 | ) 5 | } -------------------------------------------------------------------------------- /skeleton/gitignore: -------------------------------------------------------------------------------- 1 | # general 2 | *~ 3 | *.old 4 | *.swp 5 | 6 | # mac specific 7 | .DS_Store 8 | * copy.* 9 | ._* 10 | 11 | # gitblog specific 12 | /data/site.json 13 | -------------------------------------------------------------------------------- /skeleton/data/plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "rebuild": [ 3 | "code-blocks.php", 4 | "markdown.php" 5 | ], 6 | "admin": [ 7 | "akismet.php", 8 | "email-notification.php" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /helpers/README.md: -------------------------------------------------------------------------------- 1 | # Helpers 2 | 3 | These helpers are available to visitors as well as possibly administrators. 4 | 5 | Helpers in admin/helpers require the client to be authorized as an administrator. 6 | -------------------------------------------------------------------------------- /skeleton/apache2.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | RewriteRule ^(.+) /index.php/$1 [L] 7 | 8 | -------------------------------------------------------------------------------- /admin/_footer.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | Gitblog/ 4 | (Processing time: , 5 | Git queries: ) 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /skeleton/content/posts/0000-00-00-hello-world.html: -------------------------------------------------------------------------------- 1 | title: Hello world 2 | tags: gitblog, example, silly 3 | category: samples 4 | published: 12:45 UTC 5 | 6 | Hi, hola, hej, hello world! 7 | 8 | You can edit or remove this post by changing and committing content/posts/0000/00-00-hello-world.html 9 | -------------------------------------------------------------------------------- /skeleton/hooks/post-update: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git --work-tree=.. checkout -f 3 | SECRET=$(grep 'gb::$secret' ../gb-config.php | cut -d' ' -f 3 | sed "s/[;']//g") 4 | curl \ 5 | -H 'X-gb-shared-secret: '$SECRET \ 6 | --connect-timeout 5 \ 7 | --max-time 30 \ 8 | --silent --show-error \ 9 | -k \ 10 | $(cat info/gitblog-site-url|cut -d' ' -f1)'gitblog/hooks/post-update.php' 11 | -------------------------------------------------------------------------------- /admin/_base.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skeleton/hooks/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd $(dirname "$0") 3 | SECRET=$(grep 'gb::$secret' ../../gb-config.php | cut -d' ' -f 3 | sed "s/[;']//g") 4 | git --git-dir=.. --work-tree=../.. log -p --full-index -1 \ 5 | | curl \ 6 | -H 'X-gb-shared-secret: '$SECRET \ 7 | -H 'Expect:' \ 8 | --connect-timeout 5 \ 9 | --max-time 30 \ 10 | --silent --show-error \ 11 | --data-binary @- \ 12 | -k \ 13 | $(cat ../info/gitblog-site-url)'gitblog/hooks/post-patch.php' 14 | -------------------------------------------------------------------------------- /hooks/post-patch.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hooks/post-update.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hooks/post-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SECRET= 3 | if [ -f ../secret ]; then 4 | SECRET=$(cat ../secret) 5 | else 6 | SECRET=$(perl -lne 's/^(gb::\$secret[\t ]*=[\t ]*'"'"'([^'"'"']+)'"'"'[\t ]*;[\t ]*$|.*$)/$2/g;if($_){print $_;}' ../gb-config.php) 7 | fi 8 | curl \ 9 | -H 'X-gb-shared-secret: '$SECRET \ 10 | --connect-timeout 5 \ 11 | --max-time 30 \ 12 | --silent --show-error \ 13 | -k \ 14 | $(cat info/gitblog-site-url|cut -d' ' -f1)'gitblog/hooks/post-update.php' 15 | -------------------------------------------------------------------------------- /docs/filters.md: -------------------------------------------------------------------------------- 1 | # Filters 2 | 3 | *This document is early work in progress* 4 | 5 | Gitblog applies filters -- a sorted list of callables -- to different objects. For instance post body, comments, etc. 6 | 7 | Plugins which alter the content of a gitblog hooks itself into one or more filter chains. 8 | 9 | See the [akismet](../plugins/akismet.php) and [markdown](../plugins/markdown.php) plugins for examples and look at [lib/GBFilter.php](../lib/GBFilter.php) for details and to see what filters exists and can be hooked into. 10 | -------------------------------------------------------------------------------- /admin/index.php: -------------------------------------------------------------------------------- 1 | 7 |
8 |

Dashboard

9 |

10 | This is work in progress 11 |

12 |

Common tasks

13 | 19 |
20 | -------------------------------------------------------------------------------- /lib/GBObjectStore.php: -------------------------------------------------------------------------------- 1 | classname = $classname; 6 | } 7 | 8 | function parseData() { 9 | parent::parseData(); 10 | $f = array($this->classname, '__set_state'); 11 | foreach ($this->data as $k => $v) 12 | $this->data[$k] = call_user_func($f, $v); 13 | } 14 | 15 | /*function encodeData() { 16 | parent::encodeData(); 17 | }*/ 18 | } 19 | ?> -------------------------------------------------------------------------------- /lib/PHPException.php: -------------------------------------------------------------------------------- 1 | getLine(); 11 | $file = $msg->getFile(); 12 | $errno = $msg->getCode(); 13 | $msg = $msg->getMessage(); 14 | if (isset($msg->errorInfo)) 15 | $this->errorInfo = $msg->errorInfo; 16 | } 17 | } 18 | parent::__construct($msg, $errno); 19 | if ($file != null) $this->file = $file; 20 | if ($line != -1) $this->line = $line; 21 | } 22 | } 23 | ?> -------------------------------------------------------------------------------- /lib/GitError.php: -------------------------------------------------------------------------------- 1 | command = $command; 8 | } 9 | 10 | public function formatMessage($html=null) { 11 | if ($html === null) 12 | $html = ini_get('html_errors') ? true : false; 13 | $message = trim(parent::formatMessage($html)); 14 | if ($this->command) { 15 | $message .= ($message ? '.' : '') 16 | . ($html ? '' : "\n\ncommand: ") 17 | . h($this->command) 18 | . ($html ? '' : ''); 19 | } 20 | return $message; 21 | } 22 | } 23 | ?> -------------------------------------------------------------------------------- /docs/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | *This document is early work in progress* 4 | 5 | A theme is simply a PHP script including [gitblog.php](../gitblog.php). 6 | 7 | Example of an extremely simple theme: 8 | 9 | 13 | 14 | 15 |

16 | 19 |

title ?>

20 | body() ?> 21 | 25 | 26 | 27 | 28 | The theme index.php is then placed (hardlinked or symlinked) into your document root. 29 | 30 | Have a closer look at the default theme in [themes/default](../themes/default) as it uses most of the 31 | functionality of Gitblog and contains comments. 32 | -------------------------------------------------------------------------------- /themes/default/sidebar.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | *This document is early work in progress* 4 | 5 | Gitblog post *events* when things are about to happen or did happen. These events can be observed and you (themes, plugins, etc) can run custom code when an observed event is posted. 6 | 7 | ## Standard events 8 | 9 | Currently the easist way to learn about standard (built-in) events is to grep for `gb::event(` in the gitblog directory: 10 | 11 | $ grep -r 'gb::event(' gitblog/ 12 | 13 | ## Example of use 14 | 15 | In a theme: 16 | 17 | function disable_keepalive() { 18 | header('Connection: close'); 19 | } 20 | gb::observe('will-handle-request', 'disable_keepalive'); 21 | 22 | A simple plugin: 23 | 24 | class example_plugin { 25 | static function init($context) { 26 | gb::observe('did-reload-object', __CLASS__.'::did_reload_object'); 27 | } 28 | 29 | static function did_reload_object(GBContent $post) { 30 | # do something with or because of $post 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /admin/maintenance/rebuild.php: -------------------------------------------------------------------------------- 1 | '.h($msg).'

'; 6 | gb_flush(); 7 | } 8 | gb::$log_cb = 'gb_log_html'; 9 | gb::authenticate(); 10 | gb::$title[] = 'Rebuild'; 11 | include '../_header.php'; 12 | ?> 13 | 19 |
20 |

Rebuilding

21 | 22 |

23 | 24 |

25 |
26 | 27 | -------------------------------------------------------------------------------- /docs/core-devel/thoughts.txt: -------------------------------------------------------------------------------- 1 | indexes 2 | tag > objid -- able to list objects tagged with one or more tags. 3 | category > objid -- able to list objects tagged with a category. 4 | pubdate > objid -- list objects published a certain day, month or year. 5 | 6 | structs 7 | paged posts (solid, N posts per page) 8 | page index (hollow, hierarchical, pages) 9 | 10 | slugs 11 | all external dates in UTC, all internal in local time. 12 | fixed length prefix mapping of db path (stage > cache): 13 | 2007/05/19/oh-hai.html > 2007/05/19/oh-hai 14 | 2007-05-19.oh-hai.html > 2007/05/19/oh-hai 15 | 2007/05-19-oh-hai.html > 2007/05/19/oh-hai 16 | stage: 17 | CHAR CHAR
CHAR ["." ]{0,1} 18 | content/posts/2007/05.19/oh-hai.html 19 | cache: 20 | "/" "/"
"/" 21 | content/posts/2007/05/19/oh-hai 22 | URL: 23 | Variable. For example: 24 | "-" "-"
"/" 25 | /2007-05-19/oh-hai -> maps to cache -> content/posts/2007/05/19/oh-hai 26 | 27 | -------------------------------------------------------------------------------- /skeleton/gb-config.php: -------------------------------------------------------------------------------- 1 | /secret and the config file. 15 | 16 | # If you have applied server rewrite rules, routing requests to index.php, you 17 | # probably want to set index_url to "" (the empty string). May differ 18 | # depending on your rewrite rules. 19 | #gb::$index_prefix = ''; 20 | # Note that if you use lighttpd, read this: "Configuring PHP" 21 | # http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs:ModFastCGI#FastCGI-and-Programming-Languages 22 | ?> -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Rasmus Andersson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/site.json.md: -------------------------------------------------------------------------------- 1 | # site.json 2 | 3 | The `site.json` file contains the current state of your gitblog and may be updated at any time by gitblog (in contrary to `gb-config.php` which gitblog will only read). 4 | 5 | For instance, `site.json` contains the following information: 6 | 7 | - `url`: Absolute site URL -- This value is used to determine if the site URL has 8 | changed, in relation to `.git/info/gitblog/site-url`, which is used by git hook trigger. 9 | 10 | - `version`: Gitblog version used -- Used to determine if an upgrade action need to be taken (when comparing this string to `gb::$version`). 11 | 12 | - `posts_pagesize`: Page size for posts -- If this differs from `gb::$posts_pagesize` a rebuild need to be issued, causing the new pagesize to be used live. 13 | 14 | - `plugins`: Active plugins -- Dictionary of *context => list-of-plugins* which are loaded, depending on which *context* is executed. Read more in [docs/plugins.md](../docs/plugins.md). 15 | 16 | This file will be automatically created by gitblog when needed. 17 | 18 | ## Example 19 | 20 | { 21 | "url": "http://blog.hunch.se/", 22 | "version": "0.1.3", 23 | "posts_pagesize": 10, 24 | "plugins": { 25 | "request": ["google-translate.php"], 26 | "rebuild": [ 27 | "code-blocks.php", 28 | "/Users/rasmus/markdown.php" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/core-devel/will-begin-request.php: -------------------------------------------------------------------------------- 1 | static public $events = array(); 2 | static public $lazy_triggers = array( 3 | 'will-begin-response' => array('ob_start', array(array('gb','_will_begin_response_ev_obf'), 1)) 4 | ); 5 | 6 | /** Register $callable for receiving $event s */ 7 | static function observe($event, $callable) { 8 | if(isset(self::$events[$event])) 9 | self::$events[$event][] = $callable; 10 | else { 11 | self::$events[$event] = array($callable); 12 | 13 | if (isset(self::$lazy_triggers[$event])) { 14 | $v = self::$lazy_triggers[$event]; 15 | call_user_func_array($v[0], isset($v[1]) ? $v[1] : array()); 16 | } 17 | } 18 | } 19 | 20 | static function $response_begun = false; 21 | 22 | static function _will_begin_response_ev_obf($chunk) { 23 | if (self::$response_begun === false) { 24 | self::$response_begun = true; 25 | $content_type = null; 26 | # try find content type in headers 27 | foreach (headers_list() as $h) { 28 | if (strpos(strtolower($h), 'content-type:') === 0) { 29 | $content_type = substr($h, 13); 30 | if (($p = strpos($content_type, ';'))) 31 | $content_type = substr($content_type, 0, $p); 32 | $content_type = trim($content_type); 33 | break; 34 | } 35 | } 36 | # post event 37 | self::event('will-begin-response', $content_type, $chunk); 38 | } 39 | 40 | return $chunk; 41 | } -------------------------------------------------------------------------------- /plugins/google-analytics.php: -------------------------------------------------------------------------------- 1 | '')); 17 | if (!self::$conf['property_id']) { 18 | gb::log(LOG_WARNING, 'missing property_id in google-analytics configuration'); 19 | } 20 | else { 21 | gb::observe('on-html-footer', array(__CLASS__, 'echo_tracking_code')); 22 | return true; 23 | } 24 | return false; 25 | } 26 | 27 | static function echo_tracking_code() { 28 | static $prefix = ''; 39 | } 40 | } 41 | ?> -------------------------------------------------------------------------------- /lib/GBUser.php: -------------------------------------------------------------------------------- 1 | name = $name; 8 | $this->email = $email; 9 | $this->passhash = $passhash; 10 | $this->admin = $admin; 11 | } 12 | 13 | static function __set_state($state) { 14 | $o = new self; 15 | foreach ($state as $k => $v) 16 | $o->$k = $v; 17 | return $o; 18 | } 19 | 20 | function save() { 21 | return self::storage()->set(strtolower($this->email), $this); 22 | } 23 | 24 | function delete() { 25 | return self::storage()->set(strtolower($this->email), null); 26 | } 27 | 28 | static public $_storage = null; 29 | 30 | static function storage() { 31 | if (self::$_storage === null) { 32 | self::$_storage = new GBObjectStore(gb::$site_dir.'/data/users.json', __CLASS__); 33 | self::$_storage->autocommitToRepo = true; 34 | } 35 | return self::$_storage; 36 | } 37 | 38 | static function find($email=null) { 39 | if ($email !== null) 40 | $email = strtolower($email); 41 | return self::storage()->get($email); 42 | } 43 | 44 | static function passhash($email, $passphrase, $context='gb-admin') { 45 | return CHAP::shadow($email, $passphrase, $context); 46 | } 47 | 48 | static function findAdmin() { 49 | foreach (self::storage()->get() as $email => $user) { 50 | if ($user->admin === true) 51 | return $user; 52 | } 53 | return null; 54 | } 55 | } 56 | ?> -------------------------------------------------------------------------------- /docs/core-devel/comments.txt: -------------------------------------------------------------------------------- 1 | Stored at {repo}/content/{pathname of content object}.comments and contains 2 | chunks of JSON data. 3 | 4 | An associative array is stored in this file and contains a list of associative 5 | comments (arrays themselves) keyed by auto-increment integer. When a new comment 6 | is added it receives number ((number of previous) + 1) -- this way a comment 7 | in the middle can be removed without newer comments being reassigned to other 8 | numbers/ids. 9 | 10 | Example: 11 | Post: {repo}/content/posts/2008/11/24-smisk.html 12 | Comments: {repo}/content/posts/2008/11/24-smisk.comments 13 | 14 | Comments are read like this: 15 | $comments = json_decode(file_get_contents(path)); 16 | 17 | And added like this: (pseudo code) 18 | fp = open(path) 19 | flock(fp, excl) 20 | $comments = json_decode(fread(fp)) 21 | if (!$comments) 22 | $comments = array(1 => $comment) 23 | else 24 | $comments[array_pop(array_keys($comments))+1] = $comment 25 | fseek(fp, 0) 26 | fwrite(fp, json_encode($comments)) 27 | funlock(fp) 28 | fclose(fp) 29 | git::add(path) 30 | git::commit('new comment on {post->}', 31 | comment->name.' <'.comment->email.'>') 32 | 33 | This is how .comment-files look: 34 | 35 | { 36 | "1" : { 37 | "date": "2008-11-24T21:27:06+0200", 38 | "ipAddress": "89.233.196.218", 39 | "email": "maxberggren@gmail.com", 40 | "uri": "http://maxberggren.com", 41 | "name": "Max Berggren", 42 | "content": "Whatever \"content\" in\nhere", 43 | "approved": true, 44 | "comments": [ 45 | /* comment, .. */ 46 | ] 47 | }, 48 | "2" : { 49 | ... 50 | } 51 | -------------------------------------------------------------------------------- /admin/_header.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | <?= gb_title() ?> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 27 |
> 28 |
29 | X 32 |
33 |
    34 |
  • 35 | 36 |
  • 37 | 38 |
  • 39 | 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /lib/GBBenchmark.php: -------------------------------------------------------------------------------- 1 | buff = getrusage(); 21 | if($end) { 22 | $this->rtime = microtime(true) - $this->rtime; 23 | $this->utime = floatval($this->buff["ru_utime.tv_sec"].$this->buff["ru_utime.tv_usec"] - $this->utime)/1000000.0; 24 | $this->stime = floatval($this->buff["ru_stime.tv_sec"].$this->buff["ru_stime.tv_usec"] - $this->stime)/1000000.0; 25 | } 26 | else { 27 | $this->rtime = microtime(true); 28 | $this->utime = $this->buff["ru_utime.tv_sec"].$this->buff["ru_utime.tv_usec"]; 29 | $this->stime = $this->buff["ru_stime.tv_sec"].$this->buff["ru_stime.tv_usec"]; 30 | } 31 | } 32 | 33 | function __construct($iterations) { 34 | $this->iterations = $iterations; 35 | $this->_rus(); 36 | } 37 | 38 | function rewind() { 39 | $this->current = 0; 40 | } 41 | 42 | function current() { 43 | return $this->current; 44 | } 45 | 46 | function key() { 47 | return $this->current; 48 | } 49 | 50 | function next() { 51 | $this->current++; 52 | } 53 | 54 | function valid() { 55 | if ($this->current >= $this->iterations) { 56 | $this->_rus(true); 57 | $this->t = microtime(1)-$this->t; 58 | printf("---------\n" 59 | ."real: %.6f s (%.6f ms mean per iteration)\n" 60 | ."user: %.6f s (%.6f ms mean per iteration)\n" 61 | ."sys: %.6f s (%.6f ms mean per iteration)\n", 62 | $this->rtime, ($this->rtime/$this->iterations)*1000.0, 63 | $this->utime, ($this->utime/$this->iterations)*1000.0, 64 | $this->stime, ($this->stime/$this->iterations)*1000.0 65 | ); 66 | return false; 67 | } 68 | return true; 69 | } 70 | } 71 | 72 | #foreach (GBBenchmark::iterations() as $iteration) 73 | # $m = strtotime('1993-01-14 19:36:11 +0400'); 74 | ?> -------------------------------------------------------------------------------- /admin/res/jquery.json-1.3.min.js: -------------------------------------------------------------------------------- 1 | 2 | (function($){function toIntegersAtLease(n) 3 | {return n<10?'0'+n:n;} 4 | Date.prototype.toJSON=function(date) 5 | {return this.getUTCFullYear()+'-'+ 6 | toIntegersAtLease(this.getUTCMonth())+'-'+ 7 | toIntegersAtLease(this.getUTCDate());};var escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};$.quoteString=function(string) 8 | {if(escapeable.test(string)) 9 | {return'"'+string.replace(escapeable,function(a) 10 | {var c=meta[a];if(typeof c==='string'){return c;} 11 | c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';} 12 | return'"'+string+'"';};$.toJSON=function(o,compact) 13 | {var type=typeof(o);if(type=="undefined") 14 | return"undefined";else if(type=="number"||type=="boolean") 15 | return o+"";else if(o===null) 16 | return"null";if(type=="string") 17 | {return $.quoteString(o);} 18 | if(type=="object"&&typeof o.toJSON=="function") 19 | return o.toJSON(compact);if(type!="function"&&typeof(o.length)=="number") 20 | {var ret=[];for(var i=0;iposts ? $postspage->posts[0]->modified->time : time(); 3 | 4 | header('Content-Type: application/atom+xml; charset=utf-8'); 5 | header('Last-Modified: '.date('r', $updated_time)); 6 | echo ''.PHP_EOL; 7 | ?> 8 | 13 | 14 | <?= h(gb::$site_title) ?> 15 | 16 | 17 | Gitblog 18 | posts as $post): ?> 19 | 20 | <?= h($post->title) ?> 21 | 22 | author->name) ?> 23 | 24 | 25 | 26 | url()) ?> 27 | published ?> 28 | modified ?> 29 | tagLinks('', '', '', 30 | "\n\t\t", "\n\t\t").($post->tags ? "\n" : '') ?> 31 | categoryLinks('', '', '', 32 | "\n\t\t", "\n\t\t").($post->categories ? "\n" : '') ?> 33 | comments ?> 34 | id ?> 35 | excerpt): ?> 36 | excerpt ?>]]> 37 | 38 | body() ?>excerpt): ?> 39 |

Read more...

40 | ]]>
41 | 42 | comments ?> 43 |
44 | 45 |
46 | -------------------------------------------------------------------------------- /admin/manage/comments.php: -------------------------------------------------------------------------------- 1 | 10 |
11 |

Pending comments

12 | 13 | 14 | name); ?> 15 | 16 | 19 | 54 | 55 | 56 |
17 | Avatar 18 | 20 | 38 | 39 | nameLink('class="name"') ?> 40 | 41 | on 42 | 43 | 44 | title ? $post->title : '('.substr($post->name,strlen('content/posts/')).')') ?> 45 | 46 | 47 | 48 | spam === true ? 'Spam':'Ham')?> 49 | 50 |

51 | textBody(), 300)) ?> 52 |

53 |
57 |
58 | 59 | -------------------------------------------------------------------------------- /admin/maintenance/git-status.php: -------------------------------------------------------------------------------- 1 | 8 |
9 |

Status

10 |

11 | On branch 12 | 13 | ← ahead of 14 | by commits. 15 | 16 |

17 | 18 |

Changes to be committed

19 |
    20 | $t): $status = $t['status']; ?> 21 |
  • 22 | 23 | 24 | 25 | → 26 | 27 | 28 | 29 |
  • 30 | 31 |
32 |

33 | Use git reset HEAD <file>... to unstage. 34 |

35 | 36 | 37 |

Changed but not updated

38 |
    39 | $t): $status = $t['status']; ?> 40 |
  • 41 | 42 | 43 | 44 | → 45 | 46 | 47 | 48 |
  • 49 | 50 |
51 |

52 | Use git add/rm <file>... to update what will be committed.
53 | Use git checkout -- <file>... to discard changes in working directory. 54 |

55 | 56 | 57 |

Untracked files

58 |
    59 | $status): ?> 60 |
  • 61 | 62 |
63 |

64 | Use git add <file>... to include in what will be committed. 65 |

66 | 67 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /admin/helpers/authorize.php: -------------------------------------------------------------------------------- 1 | email, $authed->name, gb::$site_url); 9 | gb::event('client-authorized', $authed); 10 | $url = ((isset($_REQUEST['referrer']) && $_REQUEST['referrer']) ? $_REQUEST['referrer'] : gb_admin::$url); 11 | header('HTTP/1.1 303 See Other'); 12 | header('Location: '.$url); 13 | exit('See Other '); 14 | } 15 | 16 | if (isset($_POST['chap-username'])) { 17 | if ($authed === CHAP::BAD_USER) { 18 | gb::$errors[] = 'No such user'; 19 | } 20 | elseif ($authed === CHAP::BAD_RESPONSE) { 21 | gb::$errors[] = 'Bad password'; 22 | } 23 | else { 24 | gb::$errors[] = 'Unknown error'; 25 | } 26 | } 27 | 28 | $auth = gb::authenticator(); 29 | include '../_header.php'; 30 | ?> 31 | 32 | 58 |
59 |

Authorize

60 |
62 |
63 | 64 |
65 |

66 | Username:
68 | Password: 69 |

70 |

71 | 72 |

73 |
74 |
75 | -------------------------------------------------------------------------------- /docs/core-devel/example.comments: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "date": null, 4 | "ipAddress": null, 5 | "email": "rasmus@flajm.se", 6 | "uri": null, 7 | "name": "John Doe", 8 | "message": "Hello hi tjena", 9 | "approved": true, 10 | "comments": { 11 | "1": { 12 | "date": null, 13 | "ipAddress": null, 14 | "email": "rasmus@notion.se", 15 | "uri": null, 16 | "name": "Mos Master 0", 17 | "message": "Hello hi tjena", 18 | "approved": true, 19 | "comments": { 20 | "1": { 21 | "date": null, 22 | "ipAddress": null, 23 | "email": "rasmus@notion.se", 24 | "uri": null, 25 | "name": "Yxi Kaksi 0", 26 | "message": "Hello hi tjena", 27 | "approved": null, 28 | "comments": null 29 | }, 30 | "2": { 31 | "date": null, 32 | "ipAddress": null, 33 | "email": "yxan1@hotmail.com", 34 | "uri": null, 35 | "name": "Yxi Kaksi 1", 36 | "message": "Hello hi tjena", 37 | "approved": true, 38 | "comments": null 39 | }, 40 | "3": { 41 | "date": null, 42 | "ipAddress": null, 43 | "email": "yxan2@hotmail.com", 44 | "uri": null, 45 | "name": "Yxi Kaksi 2", 46 | "message": "Hello hi tjena", 47 | "approved": true, 48 | "comments": null 49 | } 50 | } 51 | }, 52 | "2": { 53 | "date": null, 54 | "ipAddress": null, 55 | "email": "moset1@gmail.com", 56 | "uri": null, 57 | "name": "Mos Master 1", 58 | "message": "Hello hi tjena", 59 | "approved": true, 60 | "comments": { 61 | "1": { 62 | "date": null, 63 | "ipAddress": null, 64 | "email": "yxan1@hotmail.com", 65 | "uri": null, 66 | "name": "Yxi Kaksi 1", 67 | "message": "Hello hi tjena", 68 | "approved": true, 69 | "comments": null 70 | } 71 | } 72 | }, 73 | "3": { 74 | "date": null, 75 | "ipAddress": null, 76 | "email": "moset2@gmail.com", 77 | "uri": null, 78 | "name": "Mos Master 2", 79 | "message": "Hello hi tjena", 80 | "approved": null, 81 | "comments": { 82 | "1": { 83 | "date": null, 84 | "ipAddress": "127.0.0.1", 85 | "email": "yxan2@hotmail.com", 86 | "uri": null, 87 | "name": "Yxi Kaksi 2", 88 | "message": "Hello hi tjena", 89 | "approved": true, 90 | "comments": null 91 | } 92 | } 93 | } 94 | } 95 | }, 96 | "2": { 97 | "date": null, 98 | "ipAddress": null, 99 | "email": "john@doe.com", 100 | "uri": null, 101 | "name": "John Doe", 102 | "message": "Hello hi tjena", 103 | "approved": null, 104 | "comments": null 105 | } 106 | } -------------------------------------------------------------------------------- /plugins/feedburner.php: -------------------------------------------------------------------------------- 1 | '' 17 | )); 18 | if (!self::$conf['url']) { 19 | gb::log(LOG_WARNING, 'missing "url" in configuration'); 20 | return false; 21 | } 22 | gb::observe('will-handle-request', array(__CLASS__, 'will_handle_req')); 23 | return true; 24 | } 25 | 26 | static function will_handle_req() { 27 | if (!gb::$is_feed) 28 | return; 29 | 30 | $allowed_ips = self::$conf['allowed_ips']; 31 | if (!$allowed_ips) 32 | $allowed_ips = array(); 33 | elseif (!is_array($allowed_ips)) 34 | $allowed_ips = array($allowed_ips); 35 | 36 | $isfb = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'FeedBurner/') !== false; 37 | 38 | if (($isfb === false) 39 | and (in_array($_SERVER['REMOTE_ADDR'], $allowed_ips) === false) 40 | and (!isset($_GET['original-feed'])) # manual override 41 | ) 42 | { 43 | # we send an atom feed as response for clients which do not follow redirects 44 | $site_title = h(gb::$site_title); 45 | $curr_url = h(gb::url()->__toString()); 46 | $site_url = h(gb::$site_url); 47 | $url = h(self::$conf['url']); 48 | $mdate = @filemtime(self::$conf->storage()->file); 49 | $mdate = date('c', $mdate ? $mdate : time()); 50 | $s = << 52 | 53 | $curr_url 54 | $site_title 55 | 56 | 57 | $mdate 58 | 59 | This feed has moved 60 | 61 | $url 62 | $mdate 63 | $mdate 64 | This feed has moved to $url.

66 |

You see this because your feed client does not support automatic redirection.

67 | ]]>
68 |
69 |
70 | 71 | XML; 72 | header('HTTP/1.1 301 Moved Permanently'); 73 | header('Location: '.self::$conf['url']); 74 | header('Content-Length: '.strlen($s)); 75 | header('Content-Type: application/atom+xml; charset=utf-8'); 76 | exit($s); 77 | } 78 | } 79 | } 80 | ?> -------------------------------------------------------------------------------- /admin/res/sha1-min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 3 | * in FIPS 180-1 4 | * Version 2.2 Copyright Paul Johnston 2000 - 2009. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for details. 8 | */ 9 | var hexcase=0;var b64pad="";function hex_sha1(a){return rstr2hex(rstr_sha1(str2rstr_utf8(a)))}function hex_hmac_sha1(a,b){return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(a),str2rstr_utf8(b)))}function sha1_vm_test(){return hex_sha1("abc").toLowerCase()=="a9993e364706816aba3e25717850c26c9cd0d89d"}function rstr_sha1(a){return binb2rstr(binb_sha1(rstr2binb(a),a.length*8))}function rstr_hmac_sha1(c,f){var e=rstr2binb(c);if(e.length>16){e=binb_sha1(e,c.length*8)}var a=Array(16),d=Array(16);for(var b=0;b<16;b++){a[b]=e[b]^909522486;d[b]=e[b]^1549556828}var g=binb_sha1(a.concat(rstr2binb(f)),512+f.length*8);return binb2rstr(binb_sha1(d.concat(g),512+160))}function rstr2hex(c){try{hexcase}catch(g){hexcase=0}var f=hexcase?"0123456789ABCDEF":"0123456789abcdef";var b="";var a;for(var d=0;d>>4)&15)+f.charAt(a&15)}return b}function str2rstr_utf8(c){var b="";var d=-1;var a,e;while(++d>>6)&31),128|(a&63))}else{if(a<=65535){b+=String.fromCharCode(224|((a>>>12)&15),128|((a>>>6)&63),128|(a&63))}else{if(a<=2097151){b+=String.fromCharCode(240|((a>>>18)&7),128|((a>>>12)&63),128|((a>>>6)&63),128|(a&63))}}}}}return b}function rstr2binb(b){var a=Array(b.length>>2);for(var c=0;c>5]|=(b.charCodeAt(c/8)&255)<<(24-c%32)}return a}function binb2rstr(b){var a="";for(var c=0;c>5]>>>(24-c%32))&255)}return a}function binb_sha1(v,o){v[o>>5]|=128<<(24-o%32);v[((o+64>>9)<<4)+15]=o;var y=Array(80);var u=1732584193;var s=-271733879;var r=-1732584194;var q=271733878;var p=-1009589776;for(var l=0;l>16)+(d>>16)+(c>>16);return(b<<16)|(c&65535)}function bit_rol(a,b){return(a<>>(32-b))}; -------------------------------------------------------------------------------- /lib/GBHTTPDigestAuth.php: -------------------------------------------------------------------------------- 1 | realm = $realm; 9 | $this->users = $users; 10 | $this->ttl = $ttl; 11 | $this->domain = $domain; 12 | } 13 | 14 | function authenticate($users=null) { 15 | if (!isset($_SERVER['PHP_AUTH_DIGEST']) || empty($_SERVER['PHP_AUTH_DIGEST'])) 16 | return false; 17 | 18 | # users 19 | if ($users === null) 20 | $users = $this->users ? $this->users : array(); 21 | 22 | # analyze 23 | if (!($data = self::parse($_SERVER['PHP_AUTH_DIGEST']))) { 24 | gb::log('GBHTTPDigestAuth: failed to parse '.var_export($_SERVER['PHP_AUTH_DIGEST'],1)); 25 | return false; 26 | } 27 | elseif (!isset($users[$data['username']])) { 28 | gb::log('GBHTTPDigestAuth: unknown username '.var_export($data['username'],1)); 29 | return false; 30 | } 31 | 32 | # check input 33 | if ($this->ttl > 0 && $data['nonce'] !== $this->nonce()) 34 | return false; 35 | 36 | # generate the valid response 37 | $A1 = $users[$data['username']]; # MD5(username:realm:password) 38 | $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']); # MD5(method:digestURI) 39 | $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); 40 | if ($data['response'] != $valid_response) { 41 | gb::log('GBHTTPDigestAuth: unexpected response '.var_export($data['response'],1)); 42 | return false; 43 | } 44 | 45 | return $data['username']; 46 | } 47 | 48 | function nonce() { 49 | return gb_nonce_make('digest-auth-'.$this->realm, $this->ttl); 50 | } 51 | 52 | function sendHeaders($status='401 Unauthorized') { 53 | if ($status) 54 | header('HTTP/1.1 '.$status); 55 | header('WWW-Authenticate: Digest '. 56 | 'realm="'.$this->realm.'",'. 57 | ($this->domain ? 'domain="'.$this->domain.'",' : ''). 58 | 'qop="auth",'. 59 | 'algorithm="MD5",'. 60 | 'nonce="'.$this->nonce().'",'. 61 | 'opaque="'.md5($this->realm).'"' 62 | ); 63 | } 64 | 65 | static function parse($txt) { 66 | # protect against missing data 67 | $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1); 68 | $data = array(); 69 | $keys = implode('|', array_keys($needed_parts)); 70 | preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER); 71 | foreach ($matches as $m) { 72 | $data[$m[1]] = $m[3] ? $m[3] : $m[4]; 73 | unset($needed_parts[$m[1]]); 74 | } 75 | return $needed_parts ? false : $data; 76 | } 77 | } 78 | 79 | /* 80 | $realm = 'hell'; 81 | $users = array( 82 | 'rasmus' => md5('rasmus:'.$realm.':password') 83 | ); 84 | $d = new GBHTTPDigestAuth($realm); 85 | if (!($username = $d->authenticate($users))) { 86 | $d->sendHeaders(); 87 | exit(0); 88 | } 89 | echo 'authenticated as '.$username; 90 | */ 91 | ?> -------------------------------------------------------------------------------- /lib/GBRebuilder.php: -------------------------------------------------------------------------------- 1 | forceFullRebuild = $forceFullRebuild; 7 | } 8 | 9 | function onObject($name, $id) { 10 | return false; 11 | } 12 | 13 | function finalize() { 14 | } 15 | 16 | #---------------------------------------------------------------------------- 17 | 18 | static public $rebuilders = array(); 19 | 20 | /** Load rebuilders from gb::$dir/rebuilders */ 21 | static function loadRebuilders() { 22 | self::$rebuilders = array(); 23 | foreach (glob(gb::$dir.'/rebuilders/*.php') as $path) { 24 | $n = basename($path); 25 | if (preg_match('/^[a-z_][0-9a-z_]*\.php$/i', $n)) { 26 | $libname = substr($n, 0, -4); 27 | gb::log(LOG_INFO, 'loading rebuilder library "%s" from %s', 28 | $libname, substr($path, strlen(gb::$dir)+1)); 29 | include_once $path; 30 | $initname = 'init_rebuilder_'.$libname; 31 | $initname(self::$rebuilders); 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Rebuild caches, indexes, etc. 38 | */ 39 | static function rebuild($forceFullRebuild=false) { 40 | gb::log(LOG_NOTICE, 'rebuilding cache'.($forceFullRebuild ? ' (forcing full rebuild)':'')); 41 | $time_started = microtime(1); 42 | $failures = array(); 43 | 44 | # Load rebuild plugins 45 | gb::load_plugins('rebuild'); 46 | 47 | # Load rebuilders if needed 48 | if (empty(self::$rebuilders)) 49 | self::loadRebuilders(); 50 | 51 | # Create rebuilder instances 52 | $rebuilders = array(); 53 | foreach (self::$rebuilders as $cls) 54 | $rebuilders[] = new $cls($forceFullRebuild); 55 | 56 | # Load rebuild plugins (2nd offer) 57 | gb::load_plugins('rebuild'); 58 | 59 | # Query ls-tree 60 | $ls = rtrim(git::exec('ls-files --stage')); 61 | 62 | if ($ls) { 63 | # Iterate objects 64 | $ls = explode("\n", $ls); 65 | foreach ($ls as $line) { 66 | try { 67 | # SP SP TAB 68 | if (!$line) 69 | continue; 70 | $line = explode(' ', $line, 3); 71 | $id = $line[1]; 72 | $name = gb_normalize_git_name(substr($line[2], strpos($line[2], "\t")+1)); 73 | 74 | foreach ($rebuilders as $rebuilder) 75 | $rebuilder->onObject($name, $id); 76 | } 77 | catch (RuntimeException $e) { 78 | gb::log(LOG_ERR, 'failed to rebuild object %s %s: %s', 79 | var_export($name,1), $e->getMessage(), $e->getTraceAsString()); 80 | $failures[] = array($rebuilder, $name); 81 | } 82 | } 83 | } 84 | 85 | # Let rebuilders finalize 86 | foreach ($rebuilders as $rebuilder) { 87 | try { 88 | $rebuilder->finalize(); 89 | } 90 | catch (RuntimeException $e) { 91 | gb::log(LOG_ERR, 'rebuilder %s (0x%x) failed to finalize: %s', 92 | get_class($rebuilder), spl_object_hash($rebuilder), 93 | GBException::format($e, true, false, null, 0)); 94 | $failures[] = array($rebuilder, null); 95 | } 96 | } 97 | 98 | gb::log(LOG_NOTICE, 'cache updated -- time spent: %s', 99 | gb_format_duration(microtime(1)-$time_started)); 100 | 101 | return $failures; 102 | } 103 | } 104 | ?> -------------------------------------------------------------------------------- /themes/default/comment.js: -------------------------------------------------------------------------------- 1 | function trim(s) { 2 | return s.replace(/(^[ \t\s\n\r]+|[ \t\s\n\r]+$)/g, ''); 3 | } 4 | 5 | document.getElementById('comment-form').onsubmit = function(e) { 6 | function check_filled(id, default_value) { 7 | var elem = document.getElementById(id); 8 | if (!elem) 9 | return false; 10 | elem.value = trim(elem.value); 11 | if (elem.value == default_value || elem.value == '') { 12 | elem.select(); 13 | return false; 14 | } 15 | return true; 16 | } 17 | if (!check_filled('comment-reply-message', '')) 18 | return false; 19 | if (!check_filled('comment-author-name', 'Name')) 20 | return false; 21 | if (!check_filled('comment-author-email', 'Email')) 22 | return false; 23 | return true; 24 | } 25 | 26 | // reply-to 27 | var reply_to_comment = null; 28 | 29 | function reply(comment_id) { 30 | reply_to_comment = document.getElementById('comment-'+comment_id); 31 | document.getElementById('comment-reply-to').value = comment_id; 32 | } 33 | 34 | var reply_to = document.getElementById('comment-reply-to'); 35 | var reply_to_lastval = ""; 36 | var form_parent = null; 37 | var cancel_button = null; 38 | 39 | reply_to.onchange = function(e) { 40 | reply_to.value = trim(reply_to.value); 41 | var title = document.getElementById('reply-title'); 42 | var form = document.getElementById('comment-form'); 43 | 44 | // remove any cancel button 45 | if (cancel_button != null) { 46 | if (cancel_button.parentNode) 47 | cancel_button.parentNode.removeChild(cancel_button); 48 | cancel_button = null; 49 | } 50 | 51 | if (reply_to.value != "") { 52 | if (reply_to_comment == null) { 53 | reply_to.value = ""; 54 | return; 55 | } 56 | 57 | if (form_parent == null) 58 | form_parent = form.parentNode; 59 | 60 | cancel_button = document.createElement('input'); 61 | cancel_button.setAttribute('type', 'button'); 62 | cancel_button.setAttribute('value', 'Cancel'); 63 | cancel_button.onclick = function(e) { document.getElementById('comment-reply-to').value = ""; }; 64 | 65 | // find submit button and append the form to its parent 66 | var inputs = form.getElementsByTagName("input"); 67 | for (var i=0; i FILTER_REQUIRE_SCALAR, 22 | 'comment' => FILTER_REQUIRE_SCALAR 23 | ); 24 | static $required_fields = array('object','comment'); 25 | 26 | # sanitize and validate input 27 | $input = filter_input_array($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, $fields); 28 | 29 | function exit2($msg, $status='400 Bad Request') { 30 | header('Status: '.$status); 31 | exit($status."\n".$msg."\n"); 32 | } 33 | 34 | # Optimally only allow the DELETE method, but as we live with HTML that's not 35 | # gonna happen very soon unfortunately. 36 | 37 | # assure required fields are OK 38 | $fields_missing = array(); 39 | foreach ($required_fields as $field) { 40 | if (!$input[$field]) 41 | $fields_missing[] = $field; 42 | } 43 | if ($fields_missing) 44 | exit2('missing parameter(s): '.implode(', ', $fields_missing)); 45 | 46 | # sanitize $input['object'] 47 | $input['object'] = trim(str_replace('..', '', $input['object']), '/'); 48 | if (strpos($input['object'], 'content/') !== 0) 49 | exit2('malformed parameter "object"'); 50 | 51 | # sanitize $input['comment'] 52 | $input['comment'] = preg_replace('/[^0-9\.]+/', '', $input['comment']); 53 | 54 | # look up post/page 55 | $post = GBExposedContent::findByCacheName($input['object'].gb::$content_cache_fnext); 56 | 57 | # verify existing content and that comments are enabled 58 | if (!$post) exit2('no such object '.$input['object']); 59 | 60 | # remove from comment db 61 | try { 62 | $cdb = $post->getCommentsDB(); 63 | $removed_comment = $cdb->remove($input['comment']); 64 | $referrer = gb::referrer_url(); 65 | 66 | # comment not found 67 | if (!$removed_comment) { 68 | if ($referrer) { 69 | $referrer['gb-error'] = 'Comment '.$input['comment'].' not found'; 70 | header('HTTP/1.1 303 See Other'); 71 | header('Location: '.$referrer); 72 | } 73 | else { 74 | header('HTTP/1.1 404 Not Found'); 75 | } 76 | exit('no such comment '.$input['comment']); 77 | } 78 | 79 | gb::log(LOG_NOTICE, 'removed comment %s by %s from post %s', 80 | $input['comment'], $removed_comment->name, $post->cachename()); 81 | gb::event('did-remove-comment', $removed_comment); 82 | 83 | # done OK 84 | if ($referrer) { 85 | $referrer->fragment = 'comments'; 86 | header('HTTP/1.1 303 See Other'); 87 | header('Location: '.$referrer); 88 | } 89 | else { 90 | exit2("removed comment: {$removed_comment->id}\n", '200 OK'); 91 | } 92 | } 93 | catch (Exception $e) { 94 | gb::log(LOG_ERR, 'failed to remove comment %s from %s', $input['comment'], $post->cachename()); 95 | header('HTTP/1.1 500 Internal Server Error'); 96 | echo '$input => ';var_export($input);echo "\n"; 97 | gb_flush(); 98 | throw $e; 99 | } 100 | 101 | ?> -------------------------------------------------------------------------------- /plugins/php-content.php: -------------------------------------------------------------------------------- 1 | /Umse', '\'???-\'.base64_encode(\'$0\').\'-???\'', $text); 45 | } 46 | 47 | static function unescape_php($text) { 48 | return preg_replace('/\?\?\?-([a-zA-Z0-9\/+=]+)-\?\?\?/Umse', 'base64_decode(\'$1\')', $text); 49 | } 50 | 51 | static function check_content(GBExposedContent $obj) { 52 | if (!$obj) 53 | return; 54 | # already have meta values? 55 | $eval = null; 56 | foreach ($obj->meta as $k => $v) { 57 | if ($k === 'php-eval') { 58 | $eval = $v; 59 | break; 60 | } 61 | } 62 | if ($eval === null) { 63 | # no php-eval meta, so let's check the body for PHP tags 64 | if (strpos($obj->body, 'body, '?>') !== false) 65 | $eval = 'request'; 66 | } 67 | else { 68 | # normalize custom meta value 69 | if (is_string($eval)) 70 | $eval = strtolower($eval); 71 | if ($eval !== 'request' && $eval !== 'rebuild') { 72 | if (gb_strbool($eval, true) === true) { 73 | $eval = 'request'; 74 | } 75 | else { 76 | unset($obj->meta['php-eval']); 77 | $obj->body = strtr($obj->body, array(''<?','?>'=>'>?')); 78 | $eval = null; 79 | } 80 | } 81 | } 82 | # eval now, at rebuild? 83 | if ($eval === 'rebuild') { 84 | ob_start(); 85 | eval('?>'.$obj->body.'body = ob_get_clean(); 87 | } 88 | } 89 | 90 | static function eval_body($body) { 91 | if (strpos($body, ''.$body.' 100 | -------------------------------------------------------------------------------- /admin/helpers/view-commit.php: -------------------------------------------------------------------------------- 1 | $id.'^'.'..'.$id, 11 | 'names' => $paths 12 | )); 13 | if (!count($commits)) { 14 | header('HTTP/1.1 404 Not Found'); 15 | exit('commit "'.h($_GET['id']).'" not found'); 16 | } 17 | $commit = $commits[key($commits)]; 18 | $patches = $commit->loadPatches($paths); 19 | 20 | gb::$title[] = 'Commit '.$commit->id; 21 | include '../_header.php'; 22 | ?> 23 | 51 |
52 |

Commit id,0,7)?>id,7)?>

53 |

54 | authorDate->condensed() ?> 55 | by 56 | authorName) ?> 57 | 58 |

59 | 60 | 61 |
62 |
63 | action) ?> 64 | 65 | action === GitPatch::DELETE ? $patch->prevname : $patch->currname) ?> 66 |
67 | action === GitPatch::EDIT_IN_PLACE): ?> 68 |
69 | lines as $i => $line): 70 | $fc = $line ? $line{0} : ''; 71 | $cc = ''; 72 | if ($fc === '+') $cc = 'add'; 73 | elseif ($fc === '-') $cc = 'rm'; 74 | elseif ($fc === '@') $cc = 'ctx'; 75 | ?> 76 |
 
77 | 78 |
79 | 80 |
81 | 82 |
83 | 84 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Gitblog have support for plugins written in PHP. These plugins are simple PHP files which will be loaded and a special function called. You can learn more by reading the section "Loading and activation" further down this document. 4 | 5 | There are a few different types of *execution contexts* in which a plugin can be loaded and/or run in. 6 | 7 | ## Execution contexts 8 | 9 | ### rebuild 10 | 11 | Plugins registered in the *rebuild* context are loaded when a rebuild is taking place (i.e. when the repository is changed by for instance a commit or a patch). 12 | 13 | Examples: 14 | 15 | - Adding support for a new input format (e.g. ability to write posts in markdown). 16 | 17 | - Building custom indices (e.g. popular comments). 18 | 19 | 20 | ### request 21 | 22 | Plugins registered in this context are loaded for every public request to the blog interface (theme). Please keep in mind the performance penalty introduced by PHP `require` which is used under the hood. 23 | 24 | Examples: 25 | 26 | - Modifying behaviour based on who is visiting the site (i.e. alternate interface for mobile users). 27 | 28 | - Adding custom HTML and HTTP headers. 29 | 30 | 31 | ### admin 32 | 33 | Plugins registered in this context will be loaded when an administrative task is taking place. For instance when a comment is added or removed. 34 | 35 | Examples: 36 | 37 | - Denying a comment based on some set of parameters. 38 | 39 | 40 | ## Loading and activation 41 | 42 | Plugins are simple PHP files (or possibly other executable files, depending on what kind of plugin it is) which are installed by putting the file into one of the *search paths*. Plugins are enabled by `data/plugins.json`. 43 | 44 | A class of the name "_plugin" is expected to exists after the plugin file has been loaded. Next, the static method `init` is called on that class with a single argument `string $context`. The `init` method should return `true` if the plugin did initialize, otherwise `false` or nothing should be returned. Returning a `false` value might cause the plugin init method to be called again when something in gitblog might have become available. 45 | 46 | > Where these different load points are located, is currently undocumented but is described in-line in the code (most notably in the file [lib/GBRebuilder.php](../lib/GBRebuilder.php)). 47 | > 48 | > Currently *rebuild* plugins will be offered initialization once the rebuild task starts, then again when all rebuilder classes have instantiated to allow for modifying these instances or previously loaded rebuilders. 49 | 50 | ## Configuration and settings 51 | 52 | If a plugin need any sort of configuration and/or keep state it should use a `gb::data()` store, named like *"plugins/name-of-plugin"*. 53 | 54 | ### Example 55 | 56 | `my-example.php`: 57 | 58 | 'default value')); 70 | gb::log('Yay! I can haz loaded in the %s context', $context); 71 | gb::log('key => %s', $conf['key']); 72 | return true; 73 | } 74 | } 75 | 76 | > **Note:** The name of the plugin is constructed as follows: `$name = str_replace(array('-', '.'), '_', substr($filename, 0, -4))` and the class name is constructed like this: `$class = $name . '_plugin'`. 77 | 78 | Have a look at the [built-in plugins](../plugins) as they are pretty good for learn-by-example. 79 | -------------------------------------------------------------------------------- /themes/default/posts.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 |

title) ?>

8 | body() ?> 9 | 10 |
11 |
12 |

Recent posts

13 |
    14 | posts as $rank => $post): 15 | if ($post->published->time > $time_now) continue; 16 | if ($rank === 6) break; ?> 17 |
  1. 18 | title) ?> 19 | published->age() ?> 20 |
  2. 21 | 22 |
23 |
24 |
25 |

Popular tags

26 |
    27 | $popularity): if ($popularity < 0.2) break; ?> 28 |
  1. 29 | 30 |
31 |
32 |
33 |
34 |
35 | 1) || gb::$is_tags): ?> 36 | 49 | 50 |
51 | 52 | 53 | 54 |
55 | 56 |

57 | posts as $post): if ($post->published->time > $time_now) continue; ?> 58 |
59 | commentsLink() ?> 60 |

title) ?>

61 |

62 | published->age() ?> 63 | by author->name) . $post->tagLinks(', tagged ') . $post->categoryLinks(', filed under ') ?> 64 |

65 |
66 | body() ?> 67 | excerpt): ?> 68 |

Continue reading...

69 | 70 |
71 |
72 |
73 | 74 | posts): /* todo: support for scheduled posts */ ?> 75 |

76 | There is no published content here at the moment. Check back later my friend. 77 |

78 | 79 |
80 |
81 |
82 | 83 | 96 | -------------------------------------------------------------------------------- /admin/settings/basics.php: -------------------------------------------------------------------------------- 1 | 8 | 57 |
58 | 59 |
60 |
61 |

Settings

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Default content type for new posts and pages

71 | 76 | 81 | 88 |
89 |
90 |
91 | 92 | 93 |
94 |
95 |

Live preview while editing posts and pages

96 | 101 |
102 |
103 |
104 |
105 | 106 | -------------------------------------------------------------------------------- /admin/helpers/save-post.php: -------------------------------------------------------------------------------- 1 | '', 13 | 'version' => '(work)', 14 | 'commit' => 'bool(false)' 15 | ); 16 | static $state_fields = array( 17 | 'mimeType' => ':trim', 18 | 'title' => ':trim', 19 | 'slug' => ':trim', 20 | 'body' => '', 21 | 'tags' => '[]', 22 | 'categories' => '[]', 23 | 'published' => '@GBDateTime', 24 | 'author' => '@GBAuthor', 25 | 'commentsOpen' => 'bool', 26 | 'pingbackOpen' => 'bool', 27 | 'draft' => 'bool' 28 | ); 29 | $input = gb_input::process(array_merge($spec_fields, $state_fields)); 30 | 31 | # find post 32 | $created = false; 33 | if ($input['name'] !== null) { 34 | if (!($post = GBPost::findByName($input['name'], $input['version']))) 35 | gb_admin::error_rsp('Post '.r($input['name']).' not found'); 36 | } 37 | else { 38 | $post = new GBPost(); 39 | $created = true; 40 | } 41 | 42 | # set post state 43 | $modified_state = array(); 44 | foreach ($state_fields as $k => $discard) { 45 | $v = $input[$k]; 46 | if ($v !== null && $post->$k !== $v) { 47 | if ($k === 'body') { 48 | $post->setRawBody($v); 49 | $modified_state[$k] = $post->rawBody(); 50 | } 51 | else { 52 | $post->$k = $v; 53 | $v = $post->$k; 54 | if ($v instanceof GBDateTime || $v instanceof GBAuthor) 55 | $v = strval($v); 56 | $modified_state[$k] = $v; 57 | } 58 | } 59 | } 60 | 61 | # post-process checks before saving 62 | if ($modified_state) { 63 | $post->modified = new GBDateTime(); 64 | if (!$post->title && !$post->slug) { 65 | throw new UnexpectedValueException( 66 | 'Both title and slug can not both be empty. Please choose a title for this post.'); 67 | } 68 | if (!$post->slug) 69 | $post->slug = gb_cfilter::apply('sanitize-title', $post->title); 70 | elseif ($created && !$post->title) 71 | $post->title = ucfirst($post->slug); 72 | } 73 | 74 | # set newborn properties 75 | if ($created) { 76 | if (!$post->mimeType) { 77 | $post->mimeType = 'text/html'; 78 | gb::log('did force html'); 79 | } 80 | else { 81 | gb::log('mime type is %s', $post->mimeType); 82 | } 83 | if (!$post->published) 84 | $post->published = $post->modified; 85 | $post->name = $post->recommendedName(); 86 | } 87 | else { 88 | gb::log('already exists (OK)'); 89 | } 90 | 91 | # was the state actually modified? 92 | if ($modified_state) { 93 | gb::log('write %s', r($modified_state)); 94 | # write to work area 95 | gb_admin::write_content($post); 96 | } 97 | 98 | # if the post was created, reload it to find appropriate values 99 | if ($created) { 100 | $post = GBPost::findByName($post->name, 'work'); 101 | $modified_state = array(); 102 | foreach ($state_fields as $k => $discard) { 103 | if ($k === 'body') { 104 | $modified_state[$k] = $post->rawBody(); 105 | } 106 | else { 107 | $v = $post->$k; 108 | if ($v instanceof GBDateTime) 109 | $v = strval($v); 110 | $modified_state[$k] = $v; 111 | } 112 | } 113 | } 114 | 115 | # commit? 116 | if ($input['commit']) { 117 | git::add($post->name); 118 | git::commit(($created ? 'Created' : 'Updated').' post '.r($post->title), 119 | gb::$authorized, $post->name); 120 | } 121 | 122 | # build response entity 123 | $rsp = array( 124 | 'name' => $post->name, 125 | 'version' => $post->id, 126 | 'exists' => $post->exists(), 127 | 'isTracked' => $post->isTracked(), 128 | 'isDirty' => $post->isDirty(), 129 | 'state' => $modified_state 130 | ); 131 | 132 | # status 133 | $status = '200 OK'; 134 | if ($created) { 135 | $status = '201 Created'; 136 | } 137 | 138 | # send JSON response 139 | gb_admin::json_rsp($rsp, $status); 140 | gb::log('saved post %s', $post->name); 141 | } 142 | catch (Exception $e) { 143 | gb::log('failed to save post: %s', GBException::format($e, true, false, null, 0)); 144 | gb_admin::json_rsp($e->getMessage(), '400 Bad Request'); 145 | } 146 | 147 | ?> -------------------------------------------------------------------------------- /plugins/email-notification.php: -------------------------------------------------------------------------------- 1 | true, 17 | 'notify_pending_comment' => true, 18 | 'notify_spam_comment' => false, 19 | 'recipient' => 'author' 20 | )); 21 | gb::observe('did-add-comment', array(__CLASS__, 'did_add_comment')); 22 | gb::observe('did-spam-comment', array(__CLASS__, 'did_spam_comment')); 23 | return true; 24 | } 25 | 26 | static function recipient($comment) { 27 | $recipient = self::$data['recipient']; 28 | if (is_array($recipient) || strpos($recipient, '@') !== false) 29 | $recipient = GBMail::normalizeRecipient($recipient); 30 | else 31 | $recipient = GBMail::normalizeRecipient($comment->post->author); 32 | if (!$recipient[0]) 33 | $recipient = GBMail::normalizeRecipient(gb::data('email')->get('admin')); 34 | return $recipient; 35 | } 36 | 37 | static function comment_mkbody($comment, $header='', $footer='') { 38 | $indented_comment_body = "\t".str_replace("\n", "\n\t", trim($comment->body())); 39 | $comments_url = $comment->post->url().'#comments'; 40 | $comment_url = $comment->approved ? $comment->commentURL() : $comments_url; 41 | 42 | $comments = $comment->commentsObject(); 43 | $comments_name = $comments ? $comments->name : '?'; 44 | 45 | $author_origin = $comment->ipAddress; 46 | $hostname = @gethostbyaddr($comment->ipAddress); 47 | if ($hostname && $hostname !== $comment->ipAddress) 48 | $author_origin .= ', '.$hostname; 49 | 50 | if (!is_string($header)) 51 | $header = strval($header); 52 | if (!is_string($footer)) 53 | $footer = strval($footer); 54 | 55 | # add stuff to footer 56 | if ($comment->approved) 57 | $footer .= "\nUnapprove this comment: ".$comment->unapproveURL(null, false)."\n"; 58 | else 59 | $footer .= "\nApprove this comment: ".$comment->approveURL(null, false)."\n"; 60 | 61 | if (!$comment->spam) 62 | $footer .= "\nMark as spam and delete: ".$comment->spamURL(null, false)."\n"; 63 | 64 | $footer .= "\nDelete: ".$comment->removeURL(null, false)."\n"; 65 | 66 | $ipv4_addr = $comment->ipAddress; 67 | if (preg_match('/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/', $comment->ipAddress, $m)) 68 | $ipv4_addr = $m[1]; 69 | 70 | # compile message 71 | $msg = <<post->title}" $comment_url 75 | 76 | Author: $comment->name ($author_origin) 77 | Email: $comment->email 78 | URI: $comment->uri 79 | Whois: http://www.db.ripe.net/whois?searchtext=$ipv4_addr 80 | Date: $comment->date 81 | 82 | View all comments on this post: $comments_url 83 | $footer 84 | -- 85 | $comments_name 86 | MESSAGE; 87 | return $msg; 88 | } 89 | 90 | static function did_add_comment($comment) { 91 | if ($comment->spam) 92 | return; 93 | 94 | # really do this? 95 | if ($comment->approved && !self::$data['notify_new_comment']) 96 | return; 97 | elseif (!$comment->approved && !self::$data['notify_pending_comment']) 98 | return; 99 | 100 | $subject = '['.gb::$site_title.'] ' 101 | .($comment->approved ? 'New' : 'Pending').' comment on "'.$comment->post->title.'"'; 102 | 103 | $body = self::comment_mkbody($comment); 104 | $to = self::recipient($comment); 105 | 106 | if (!$to[0]) { 107 | gb::log(LOG_WARNING, 'failed to deduce recipient -- ' 108 | .'please add your address to "admin" in data/email.json'); 109 | } 110 | else { 111 | GBMail::compose($subject, $body, $to)->send(true); 112 | } 113 | } 114 | 115 | static function did_spam_comment($comment) { 116 | if (!self::$data['notify_spam_comment']) 117 | return; 118 | $subject = '['.gb::$site_title.'] Spam comment on "'.$comment->post->title.'"'; 119 | $body = self::comment_mkbody($comment); 120 | $to = self::recipient($comment); 121 | GBMail::compose($subject, $body, $to)->send(true); 122 | } 123 | } 124 | ?> -------------------------------------------------------------------------------- /lib/gb_input.php: -------------------------------------------------------------------------------- 1 | $filter) { 12 | $isa = is_array($filter); 13 | if ($d[$field] === null 14 | || ($d[$field] === false && ($isa ? $filter['filter'] : $filter) !== FILTER_VALIDATE_BOOLEAN)) 15 | { 16 | if ($strict && isset($required[$field])) { 17 | throw new UnexpectedValueException($field.' is required'); 18 | } 19 | elseif (isset($defaults[$field])) { 20 | if ($filter !== FILTER_DEFAULT) { 21 | if ($isa) 22 | $d[$field] = filter_var($defaults[$field], $filter['filter'], 23 | isset($filter['options']) ? $filter['options'] : null); 24 | else 25 | $d[$field] = filter_var($defaults[$field], $filter); 26 | } 27 | else 28 | $d[$field] = $defaults[$field]; 29 | } 30 | else { 31 | $d[$field] = null; 32 | } 33 | } 34 | } 35 | return $d; 36 | } 37 | 38 | static function parse_filters($filters, $required_by_default) { 39 | static $stdsymbols = array( 40 | 'str' => FILTER_DEFAULT, 41 | 'bool' => FILTER_VALIDATE_BOOLEAN, # aliases: boolean 42 | #'float' => FILTER_VALIDATE_FLOAT, 43 | #'int' => FILTER_VALIDATE_INT, 44 | 'email' => FILTER_VALIDATE_EMAIL, 45 | 'ip' => FILTER_VALIDATE_IP, 46 | 'url' => FILTER_VALIDATE_URL, 47 | 'url+path' => array('filter'=>FILTER_VALIDATE_URL, 'flags'=>FILTER_FLAG_PATH_REQUIRED), 48 | ); 49 | 50 | $required = array(); 51 | $defaults = array(); 52 | 53 | foreach ($filters as $field => $filter) { 54 | if (!$filter) 55 | $filter = FILTER_DEFAULT; 56 | $flags = $required_by_default ? FILTER_REQUIRE_SCALAR : 0; 57 | if (is_string($filter) && $filter) { 58 | if ($filter{0} === '!') { 59 | $filter = substr($filter, 1); 60 | $flags |= FILTER_REQUIRE_SCALAR; 61 | } 62 | elseif ($filter{0} === '?') { 63 | $flags &= ~FILTER_REQUIRE_SCALAR; 64 | } 65 | 66 | if ($filter && $filter{0} === '[') { 67 | $filter = trim($filter, '[]'); 68 | if ($flags & FILTER_REQUIRE_SCALAR) { 69 | $flags &= ~FILTER_REQUIRE_SCALAR; 70 | $flags |= FILTER_REQUIRE_ARRAY; 71 | } 72 | $flags |= FILTER_FORCE_ARRAY; 73 | } 74 | 75 | if ($filter && ($p = strpos($filter, '(')) !== false) { 76 | $defaults[$field] = rtrim(substr($filter, $p+1),')'); 77 | $filter = substr($filter, 0, $p); 78 | } 79 | 80 | if ($filter) { 81 | if (isset($stdsymbols[$filter])) { 82 | $filter = $stdsymbols[$filter]; 83 | } 84 | elseif ($filter{0} === ':') { 85 | $filter = array('filter'=>FILTER_CALLBACK, 'options'=>substr($filter, 1)); 86 | } 87 | elseif ($filter{0} === '@') { 88 | $filter = array('filter'=>FILTER_CALLBACK, 'options'=>create_function( 89 | '$x', 'return '.substr($filter, 1).'::__set_state($x);' 90 | )); 91 | } 92 | elseif ($filter{0} === '/') { 93 | $filter = array('filter'=>FILTER_VALIDATE_REGEXP, 'options'=>array('regexp' => $filter)); 94 | } 95 | else { 96 | $f = filter_id($filter); 97 | if ($f === false || $f === null) 98 | throw new InvalidArgumentException('Unknown filter "'.$filter.'"'); 99 | $filter = $f; 100 | } 101 | } 102 | else { 103 | $filter = FILTER_DEFAULT; 104 | } 105 | } 106 | elseif (is_callable($filter)) { 107 | $filter = array('filter'=>FILTER_CALLBACK, 'options'=>$filter); 108 | } 109 | 110 | if ($flags) { 111 | if (is_int($filter)) { 112 | $filter = array('filter'=>$filter, 'flags'=>$flags); 113 | } 114 | elseif (isset($filter['flags'])) { 115 | $filter['flags'] |= $flags; 116 | $flags = $filter['flags']; 117 | } 118 | else { 119 | $filter['flags'] = $flags; 120 | } 121 | } 122 | else { 123 | $flags = is_array($filter) && isset($filter['flags']) ? $filter['flags'] : 0; 124 | } 125 | 126 | if ($flags & FILTER_REQUIRE_SCALAR || $flags & FILTER_REQUIRE_ARRAY) 127 | $required[$field] = $filter; 128 | 129 | $filters[$field] = $filter; 130 | } 131 | 132 | return array($filters, $required, $defaults); 133 | } 134 | } 135 | ?> -------------------------------------------------------------------------------- /admin/helpers/data.php: -------------------------------------------------------------------------------- 1 | $msg); 55 | if ($bt) 56 | $rsp['bt'] = $bt; 57 | rsp_ok(json::pretty( array('error' => $rsp) ), $status); 58 | } 59 | 60 | function rsp_exc($e) { 61 | rsp_err(GBException::formatPlain($e, false, null, 0), '500 Internal Server Error', 62 | array_filter(array_map('trim', explode("\n",GBException::formatTrace($e, false, null, 0))))); 63 | } 64 | 65 | function stripslashes_deep($value) { 66 | return is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); 67 | } 68 | 69 | # input params 70 | $method = $_SERVER['REQUEST_METHOD']; 71 | $store_id = isset($_SERVER['PATH_INFO']) ? trim($_SERVER['PATH_INFO'],"\r\n\t/ ") : null; 72 | $jsonp_cb = false; 73 | if (isset($_GET['jsoncallback'])) { 74 | $jsonp_cb = $_GET['jsoncallback']; 75 | unset($_GET['jsoncallback']); 76 | header('Content-Type: text/javascript'); 77 | } 78 | else { 79 | header('Content-Type: application/json'); 80 | } 81 | 82 | # store 83 | if (!$store_id) 84 | rsp_err('No store specified in path'); 85 | $store = gb::data($store_id); 86 | if ($method !== 'POST' && !is_readable($store->file)) 87 | rsp_err('No such store "'.$store_id.'"', '404 Not found'); 88 | 89 | try { 90 | # POST 91 | if ($method === 'POST') { 92 | $payload_type = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : ''; 93 | $payload_data = ''; 94 | $payload_struct = null; 95 | 96 | if ($payload_type !== 'application/json') { 97 | rsp_err('Unsupported Media Type. Only accepts "application/json"', 98 | '415 Unsupported Media Type'); 99 | } 100 | 101 | # parse json 102 | $input = fopen('php://input', 'r'); 103 | while ($data = fread($input, 8192)) $payload_data .= $data; 104 | fclose($input); 105 | $pairs = json_decode($payload_data, true); 106 | if ($pairs === null) 107 | rsp_err('Failed to parse JSON payload'); 108 | 109 | # set keys 110 | $store->storage()->begin(); 111 | try { 112 | foreach ($pairs as $k => $v) { 113 | if (($k = trim($k, " \t\r\n/"))) 114 | $store->put($k, $v); 115 | } 116 | $store->storage()->commit(); 117 | } 118 | catch (Exception $e) { 119 | $store->storage()->rollback(); 120 | throw $e; 121 | } 122 | # reply with 200 OK + current document 123 | rsp_ok($store->toJSON()); # OK 124 | } 125 | elseif ($method === 'GET') { 126 | if (get_magic_quotes_gpc()) 127 | $_GET = stripslashes_deep($_GET); 128 | $keys = $_GET; 129 | # fetch specific keys 130 | if ($keys) { 131 | $rsp = array(); 132 | foreach ($keys as $key => $fallback_value) { 133 | if (!$fallback_value) 134 | $fallback_value = null; 135 | $rsp[$key] = $store->get($key, $fallback_value); 136 | } 137 | rsp_ok(json::pretty($rsp)); 138 | } 139 | # fetch complete store 140 | else { 141 | rsp_ok($store->toJSON()); 142 | } 143 | } 144 | else { 145 | rsp_err($method.' method not allowed', '405 Method Not Allowed'); 146 | } 147 | } 148 | /*catch (LogicException $e) { 149 | if (strpos($e->getMessage(), 'Failed to parse') !== false) 150 | rsp_ok('{}'); 151 | rsp_exc($e); 152 | }*/ 153 | catch (Exception $e) { 154 | rsp_exc($e); 155 | } 156 | 157 | ?> -------------------------------------------------------------------------------- /plugins/code-blocks.php: -------------------------------------------------------------------------------- 1 | blocks by using Pygments, if 9 | * available. 10 | * 11 | * Learn more about Pygments: http://pygments.org/ 12 | */ 13 | class code_blocks_plugin { 14 | static public $previous_failure = false; 15 | static public $conf; 16 | 17 | static function init($context) { 18 | if ($context !== 'rebuild') 19 | return false; 20 | self::$conf = gb::data('plugins/'.gb_filenoext(basename(__FILE__)), array( 21 | 'classname' => 'codeblock', 22 | 'tabsize' => 2, 23 | 'pygmentize' => 'pygmentize' 24 | )); 25 | gb_cfilter::add('body.html', array(__CLASS__, 'filter'), 0); 26 | return true; 27 | } 28 | 29 | static function dummy_block($content, $cssclass) { 30 | return '' : '>') 32 | .'
'.h($content).'
'; 33 | } 34 | 35 | static function highlight($content, $lang, $extra_cssclass=null, $input_encoding='utf-8') { 36 | $cssclass = self::$conf['classname']; 37 | if (!$cssclass) 38 | $cssclass = 'codeblock'; 39 | if ($extra_cssclass) 40 | $cssclass .= ' ' . $extra_cssclass; 41 | 42 | if (self::$previous_failure === true || $lang === 'text' || $lang === 'txt') 43 | return self::dummy_block($content, $cssclass); 44 | 45 | $cmd = self::$conf['pygmentize'].' '.($lang ? '-l '.escapeshellarg($lang) : '-g') 46 | .' -f html -O cssclass='.escapeshellarg($cssclass) 47 | .',encoding='.$input_encoding 48 | .(self::$conf['tabsize'] ? ',tabsize='.self::$conf['tabsize'] : ''); 49 | $st = gb::shell($cmd, $content); 50 | if ($st === null || ($st[0] !== 0 && strpos($st[2], 'command not found') !== false)) { 51 | # probably no pygments installed. 52 | # remember failure in order to speed up subsequent calls. 53 | self::$previous_failure = true; 54 | gb::log(LOG_WARNING, 55 | 'unable to highlight code because %s can not be found', 56 | self::$conf['pygmentize']); 57 | return self::dummy_block($content, $cssclass); 58 | } 59 | # $st => array(int status, string out, string err) 60 | if ($st[0] !== 0) { 61 | if (strpos($st[2], 'guess_lexer') !== false) 62 | gb::log(LOG_NOTICE, 'pygments failed to guess language'); 63 | else 64 | gb::log(LOG_WARNING, 'pygments failed to highlight code: '.$st[2]); 65 | return self::dummy_block($content, $cssclass); 66 | } 67 | return $st[1]; 68 | } 69 | 70 | static function _escapeBlockContentCB($m) { 71 | return ''.base64_encode($m[2]).''; 72 | } 73 | 74 | static function filter($text='') { 75 | $text = preg_replace_callback('/]*)>(.*)<\/codeblock>/Usm', 76 | array(__CLASS__, '_escapeBlockContentCB'), $text); 77 | $tokens = gb_tokenize_html($text); 78 | $out = ''; 79 | $depth = 0; 80 | $block = ''; 81 | $tag = ''; 82 | 83 | foreach ($tokens as $token) { 84 | if (substr($token,0,10) === '') { 93 | $depth--; 94 | if ($depth < 0) { 95 | gb::log(LOG_WARNING, 'stray messing up a code block'); 96 | $depth = 0; 97 | } 98 | if ($depth === 0) { 99 | # code block ended 100 | if ($block) { 101 | $block = base64_decode($block); 102 | $lang = ''; 103 | # find lang, if any 104 | if (preg_match('/[\s\t ]+lang=("[^"]*"|\'[^\']*\')[\s\t ]*/', $tag, $m, PREG_OFFSET_CAPTURE)) { 105 | $lang = trim($m[1][0], '"\''); 106 | $end = substr($tag, $m[0][1]+strlen($m[0][0])); 107 | $tag = substr($tag, 0, $m[0][1]).($end === '>' ? '>' : ' '.$end); 108 | } 109 | # add CSS class name 110 | $extra_cssclass = ''; 111 | if (preg_match('/class="([^"]+)"/', $tag, $m)) 112 | $extra_cssclass = $m[1]; 113 | # remove first and last line break if present 114 | if ($block{0} === "\n") 115 | $block = substr($block, 1); 116 | if ($block{strlen($block)-1} === "\n") 117 | $block = substr($block, 0, -1); 118 | # expand tabs 119 | if (self::$conf['tabsize']) 120 | $block = strtr($block, array("\t" => str_repeat(' ', self::$conf['tabsize']))); 121 | # append block to output 122 | $out .= self::highlight($block, $lang, $extra_cssclass); 123 | # clear block 124 | $block = ''; 125 | } 126 | continue; 127 | } 128 | } 129 | 130 | # in codeblock or not? 131 | if ($depth) 132 | $block .= $token; 133 | else 134 | $out .= $token; 135 | } 136 | 137 | return $out; 138 | } 139 | } 140 | ?> -------------------------------------------------------------------------------- /themes/default/index.php: -------------------------------------------------------------------------------- 1 | published->time; 13 | elseif (gb::$is_page) 14 | $context_date = $post->modified->time; 15 | elseif ((gb::$is_posts || gb::$is_tags || gb::$is_categories) && $postspage->posts) { 16 | foreach ($postspage->posts as $post) { 17 | if ($post->published->time <= $time_now) { 18 | $context_date = $post->published->time; 19 | break; 20 | } 21 | } 22 | } 23 | 24 | ?> 25 | 26 | 27 | 28 | 29 | 30 | <?= gb_title() ?> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | X 43 |
44 |
    45 |
  • 46 | 47 |
  • 48 | 49 |
  • 50 | 51 |
52 |
53 |
54 | 55 | 85 |
86 | 90 |
91 |
92 |

404 Not Found

93 | The page toString(false)) ?> does not exist. 94 |
95 |
96 | 106 |
107 | 113 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /lib/GitPatch.php: -------------------------------------------------------------------------------- 1 | action = $action; 23 | $this->lines = array(); 24 | $this->meta = array(); 25 | } 26 | 27 | static function parse($udiff) { 28 | $patches = array(); 29 | $currpatch = null; 30 | $passed_delta = false; 31 | 32 | while ( ($line = gb_sreadline($p, $udiff)) !== null) { 33 | #var_dump($line); 34 | if (!$line) 35 | continue; 36 | 37 | $start3 = substr($line, 0, 3); 38 | 39 | # line 1 -- new set 40 | if ($start3 === 'dif') { 41 | # flush previous 42 | if ($currpatch !== null) 43 | $patches[] = $currpatch; 44 | # new 45 | $currpatch = new GitPatch(GitPatch::EDIT_IN_PLACE); 46 | 47 | $s = explode(' ', $line, 3); 48 | if (isset($s[2])) { 49 | $s = explode(' ', $s[2]); 50 | $n = intval(count($s)/2); 51 | $s = trim(implode(' ', array_slice($s, $n))); 52 | $s = substr($s, strpos($s, '/')+1); 53 | $currpatch->currname = $s; 54 | } 55 | $passed_delta = false; 56 | } 57 | 58 | # content 59 | elseif($passed_delta) { 60 | $currpatch->lines[] = $line; 61 | } 62 | 63 | # prev/curr name 64 | elseif ($start3 === '---' || $start3 === '+++') { 65 | $s = rtrim(substr($line, 4)); 66 | if ($s === '/dev/null') 67 | $s = null; 68 | else 69 | $s = substr($s, 2); # remove trailing "a/" 70 | if ($start3 === '---') 71 | $currpatch->prevname = $s; 72 | else 73 | $currpatch->currname = $s; 74 | } 75 | 76 | # curr name 77 | elseif ($start3 === '@@ ') { 78 | $currpatch->delta = substr($line, 3, -3); 79 | $passed_delta = true; 80 | } 81 | 82 | # header lines 83 | # old old mode 84 | # new new mode 85 | # new file mode 86 | # del deleted file mode 87 | # cop copy from 88 | # copy to 89 | # ren rename from 90 | # rename to 91 | # sim similarity index 92 | # dis dissimilarity index 93 | # ind index .. 94 | 95 | # old mode 96 | elseif ($start3 === 'old') { 97 | $currpatch->meta['old_mode'] = intval(substr($line, 9)); 98 | } 99 | 100 | # new mode 101 | # new file mode 102 | elseif ($start3 === 'new') { 103 | if (substr($line, 4, 4) === 'file') { 104 | $currpatch->mode = intval(substr($line, 14)); 105 | $currpatch->meta['new_file_mode'] = $currpatch->mode; 106 | $currpatch->action = self::CREATE; 107 | } 108 | else { 109 | $currpatch->mode = intval(substr($line, 9)); 110 | $currpatch->meta['new_mode'] = $currpatch->mode; 111 | } 112 | } 113 | 114 | # deleted file mode 115 | elseif ($start3 === 'del') { 116 | $currpatch->meta['deleted_file_mode'] = intval(substr($line, 18)); 117 | $currpatch->action = self::DELETE; 118 | } 119 | 120 | # copy from 121 | # copy to 122 | elseif ($start3 === 'cop') { 123 | $x = strpos($line, ' ', 5); 124 | $currpatch->meta['copy_'.substr($line, 5, $x-5)] = rtrim(substr($line, $x+1)); 125 | $currpatch->action = self::COPY; 126 | } 127 | 128 | # rename from 129 | # rename to 130 | elseif ($start3 === 'ren') { 131 | $x = strpos($line, ' ', 7); 132 | $currpatch->meta['rename_'.substr($line, 7, $x-7)] = rtrim(substr($line, $x+1)); 133 | $currpatch->action = self::RENAME; 134 | } 135 | 136 | # similarity index 137 | elseif ($start3 === 'sim') { 138 | $currpatch->meta['similarity_index'] = intval(substr($line, 17)); 139 | } 140 | 141 | # dissimilarity index 142 | elseif ($start3 === 'dis') { 143 | $currpatch->meta['dissimilarity_index'] = intval(substr($line, 20)); 144 | } 145 | 146 | # index .. 147 | elseif ($start3 === 'ind') { 148 | $v = explode(' ', substr($line, 6)); 149 | $objs = explode('..', $v[0]); 150 | $currpatch->prevobj = $objs[0] === '0000000000000000000000000000000000000000' ? null : $objs[0]; 151 | $currpatch->currobj = $objs[1] === '0000000000000000000000000000000000000000' ? null : $objs[1]; 152 | if (isset($v[1])) 153 | $currpatch->mode = intval($v[1]); 154 | } 155 | 156 | } 157 | # flush 158 | if ($currpatch !== null) 159 | $patches[] = $currpatch; 160 | 161 | return $patches; 162 | } 163 | } 164 | ?> -------------------------------------------------------------------------------- /lib/JSONStore.php: -------------------------------------------------------------------------------- 1 | autocommit = $autocommit; 19 | $this->pretty_output = $pretty_output; 20 | } 21 | 22 | function loadString($s) { 23 | $this->autocommit = false; 24 | $this->data = $s; 25 | $this->parseData(); 26 | } 27 | 28 | function throwJSONError($errno=false, $compatmsg='JSON error') { 29 | if (function_exists('json_last_error')) { 30 | if ($errno === false) 31 | $errno = json_last_error(); 32 | switch ($errno) { 33 | case JSON_ERROR_DEPTH: 34 | throw new OverflowException('json data too deep'); 35 | case JSON_ERROR_CTRL_CHAR: 36 | throw new UnexpectedValueException( 37 | 'json data contains invalid or unparsable control character'); 38 | case JSON_ERROR_SYNTAX: 39 | throw new LogicException('json syntax error'); 40 | } 41 | } 42 | else { 43 | throw new LogicException($compatmsg); 44 | } 45 | } 46 | 47 | function parseData() { 48 | $this->originalData = $this->data; 49 | if ($this->data === '') 50 | $this->data = array(); 51 | elseif ( ($this->data = json_decode($this->data, true)) === null ) 52 | $this->throwJSONError(false, 'Failed to parse '.gb_relpath(gb::$site_dir, $this->file)); 53 | } 54 | 55 | function encodeData() { 56 | $this->data = $this->pretty_output ? json::pretty($this->data)."\n" : json_encode($this->data); 57 | if ($this->data === null) 58 | $this->throwJSONError(false, 'Failed to encode $data ('.gettype($this->data).') as JSON'); 59 | } 60 | 61 | protected function txReadData() { 62 | parent::txReadData(); 63 | $this->parseData(); 64 | } 65 | 66 | protected function txWriteData() { 67 | $this->encodeData(); 68 | if ($this->data !== $this->originalData) 69 | return parent::txWriteData(); 70 | return false; 71 | } 72 | 73 | function set($key, $value=null) { 74 | $return_value = true; 75 | $temptx = $this->txFp === false && $this->autocommit; 76 | if ($temptx) 77 | $this->begin(); 78 | if ($this->data === null) 79 | $this->txReadData(); 80 | if (is_array($key)) { 81 | $this->data = $key; 82 | } 83 | elseif ($value === null) { 84 | if (($return_value = isset($this->data[$key]))) { 85 | $return_value = $this->data[$key]; 86 | unset($this->data[$key]); 87 | } 88 | } 89 | else { 90 | $this->data[$key] = $value; 91 | } 92 | if ($temptx) 93 | $this->commit(); 94 | return $return_value; 95 | } 96 | 97 | function get($key=null, $default=null) { 98 | $temptx = $this->txFp === false && $this->autocommit && $this->data === null; 99 | if ($temptx) 100 | $this->begin(); 101 | if ($this->data === null) 102 | $this->txReadData(); 103 | $v = $key !== null ? (isset($this->data[$key]) ? $this->data[$key] : $default) : $this->data; 104 | if ($temptx) 105 | $this->txEnd(); 106 | return $v; 107 | } 108 | 109 | function offsetGet($k) { 110 | return $this->get($k); 111 | } 112 | 113 | function offsetSet($k, $v) { 114 | $this->set($k, $v); 115 | } 116 | 117 | function offsetExists($k) { 118 | $temptx = $this->txFp === false && $this->autocommit; 119 | if ($temptx) 120 | $this->begin(); 121 | if ($this->data === null) 122 | $this->txReadData(); 123 | $v = isset($this->data[$k]); 124 | if ($temptx) 125 | $this->txEnd(); 126 | return $v; 127 | } 128 | 129 | function offsetUnset($k) { 130 | $this->set($k, null); 131 | } 132 | 133 | function count() { 134 | return count($this->get()); 135 | } 136 | 137 | function __toString() { 138 | return r($this->get()); 139 | } 140 | } 141 | 142 | /*# Test 143 | error_reporting(E_ALL); 144 | $fdb = new JSONStore('/Users/rasmus/Desktop/db.json'); 145 | $fdb->begin(); 146 | try { 147 | assert($fdb->get('mykey') === null); 148 | $fdb->set('mykey', 'myvalue'); 149 | assert($fdb->get('mykey') === 'myvalue'); 150 | assert($fdb->mykey === 'myvalue'); 151 | assert(isset($fdb->mykey) === true); 152 | unset($fdb->mykey); 153 | assert(isset($fdb->mykey) === false); 154 | $fdb->commit(); 155 | } 156 | catch(Exception $e) { 157 | $fdb->rollback(); 158 | throw $e; 159 | } 160 | assert(is_array($fdb->get())); 161 | $v = array('mos' => 'grek', 'hej' => 'bar'); 162 | $fdb->set($v); 163 | assert($fdb->get() == $v);*/ 164 | ?> -------------------------------------------------------------------------------- /lib/FileDB.php: -------------------------------------------------------------------------------- 1 | file = $file; 21 | $this->skeleton_file = $skeleton_file; 22 | $this->createmode = $createmode; 23 | } 24 | 25 | protected $txExclusive = true; 26 | protected $txFp = false; 27 | 28 | function transactionActive() { 29 | return $this->txFp !== false; 30 | } 31 | 32 | function begin($exclusive=true) { 33 | if ($this->txFp !== false) 34 | throw new LogicException('a transaction is already active'); 35 | 36 | $this->txExclusive = $exclusive; 37 | 38 | if (($this->txFp = @fopen($this->file, 'r+')) === false) { 39 | if (($this->txFp = @fopen($this->file, 'r')) !== false) 40 | $this->readOnly = true; 41 | } 42 | 43 | if ($this->txFp === false) { 44 | if (file_exists($this->file)) { 45 | if (is_dir($this->file)) 46 | throw new RuntimeException($this->file.' is a directory'); 47 | throw new RuntimeException($this->file.' is not readable'); 48 | } 49 | if ($this->skeleton_file) { 50 | copy($this->skeleton_file, $this->file); 51 | if ($this->createmode !== false) 52 | chmod($this->file, $this->createmode); 53 | if (($this->txFp = fopen($this->file, 'r+')) === false) 54 | throw new RuntimeException('fopen('.var_export($this->file,1).', "r+") failed'); 55 | } 56 | else { 57 | if ($this->mkdirs) { 58 | $dir = dirname($this->file); 59 | if (!file_exists($dir)) { 60 | $mode = $this->createmode | 0111; 61 | mkdir($dir, $mode, true); 62 | chmod($dir, $mode); 63 | } 64 | } 65 | if (($this->txFp = fopen($this->file, 'x+')) === false) 66 | throw new RuntimeException('fopen('.var_export($this->file,1).', "x+") failed'); 67 | elseif ($this->createmode !== false) 68 | chmod($this->file, $this->createmode); 69 | } 70 | } 71 | 72 | if ($this->txExclusive && !flock($this->txFp, LOCK_EX)) { 73 | fclose($this->txFp); 74 | throw new RuntimeException('flock(, LOCK_EX) failed'); 75 | } 76 | } 77 | 78 | protected function txReadData() { 79 | if ($this->txFp === false) 80 | throw new LogicException('transaction is not active'); 81 | $fz = @fstat($this->txFp); 82 | $fz = $fz ? $fz['size'] : 0; 83 | $this->data = $fz ? fread($this->txFp, $fz) : ''; 84 | } 85 | 86 | protected function txWriteData() { 87 | if ($this->txFp === false) 88 | throw new LogicException('transaction is not active'); 89 | if ($this->data !== null) { 90 | if ($this->readOnly) 91 | return false; 92 | if (rewind($this->txFp) === false) 93 | throw new RuntimeException('rewind() failed'); 94 | if (ftruncate($this->txFp, strlen($this->data)) === false) 95 | throw new RuntimeException('ftruncate(, strlen()) failed'); 96 | if (fwrite($this->txFp, $this->data) === false) 97 | throw new RuntimeException('fwrite(, ) failed'); 98 | } 99 | return true; 100 | } 101 | 102 | function commit() { 103 | if ($this->txFp === false) 104 | throw new LogicException('transaction is not active'); 105 | $ex = null; 106 | try { 107 | $did_write = $this->txWriteData(); 108 | } 109 | catch (Exception $e) { 110 | $ex = $e; 111 | } 112 | $this->txEnd($ex); 113 | if ($ex) 114 | throw $ex; 115 | 116 | # commit to repo 117 | if ($did_write && $this->autocommitToRepo) { 118 | gb::log('committing changes to '.$this->file); 119 | if (!($author = $this->autocommitToRepoAuthor)) 120 | $author = GBUser::findAdmin()->gitAuthor(); 121 | $m = $this->autocommitToRepoMessage ? $this->autocommitToRepoMessage : 'autocommit:'.$this->file; 122 | git::add($this->file); 123 | git::commit($m, $author); 124 | # rollback() will handle gb::reset if needed 125 | } 126 | 127 | return $did_write; 128 | } 129 | 130 | function rollback($strict=true) { 131 | if ($this->txFp === false) { 132 | if ($strict) 133 | throw new LogicException('transaction is not active'); 134 | return false; 135 | } 136 | if ($this->autocommitToRepo) { 137 | try { git::reset($this->file); } catch (Exception $y) {} 138 | } 139 | return $this->txEnd(); 140 | } 141 | 142 | protected function txEnd(Exception $previousex=null) { 143 | if ($this->txExclusive && !flock($this->txFp, LOCK_UN)) 144 | throw new RuntimeException('flock(, LOCK_UN) failed', 0, $previousex); 145 | if (!fclose($this->txFp)) 146 | throw new RuntimeException('fclose() failed', 0, $previousex); 147 | $this->txFp = false; 148 | $this->data = null; 149 | } 150 | } 151 | ?> -------------------------------------------------------------------------------- /lib/CHAP.php: -------------------------------------------------------------------------------- 1 | users = $users; 30 | $this->context = $context; 31 | $this->preshadowed = $preshadowed; 32 | $this->cookie_name = $cookie_name; 33 | } 34 | 35 | static function h($s) { 36 | return hash_hmac('sha1', $s, gb::$secret); 37 | } 38 | 39 | static function shadow($username, $password, $context='') { 40 | return sha1($username.':'.$context.':'.$password); 41 | } 42 | 43 | static function opaque() { 44 | return self::h(gb::$secret); 45 | } 46 | 47 | function nonce() { 48 | $s = strval((int)ceil(time() / $this->ttl)) . $this->context . $_SERVER['REMOTE_ADDR']; 49 | return self::h($s); 50 | } 51 | 52 | function set_cookie($username, $response, $shadow=false, $cookie=false) { 53 | if (!$this->cookie_name) 54 | return; 55 | if ( $cookie !== false && !$this->refresh_cookie 56 | && ($response && isset($cookie['r'])) || ($shadow && isset($cookie['p'])) ) { 57 | # no need to refresh 58 | return; 59 | } 60 | if (headers_sent()) 61 | return; 62 | $cookie = array('u' => $username); 63 | if ($response) 64 | $cookie['r'] = $response; 65 | else 66 | $cookie['s'] = $shadow; 67 | $cookie = base64_encode(serialize($cookie)); 68 | $url = new GBURL(gb::$site_url); 69 | setcookie($this->cookie_name, $cookie, time() + $this->ttl, $url->path, $url->host, $url->secure); 70 | } 71 | 72 | function get_cookie() { 73 | $s = $_COOKIE[$this->cookie_name]; 74 | return unserialize(base64_decode(get_magic_quotes_gpc() ? stripslashes($s) : $s)); 75 | } 76 | 77 | function auth_handshake($username, $response, $cookie=false) { 78 | if (!isset($this->users[$username])) 79 | return self::BAD_USER; 80 | $shadow = $this->preshadowed ? 81 | $this->users[$username] : 82 | self::shadow($username, $this->users[$username], $this->context); 83 | $a = hash_hmac('sha1', $shadow, self::opaque()); 84 | $expected_response = hash_hmac('sha1', $a, $this->nonce()); 85 | if ($response !== $expected_response) 86 | return self::BAD_RESPONSE; 87 | $this->set_cookie($username, $response, null, $cookie); 88 | return $username; 89 | } 90 | 91 | function auth_plain($username, $password, $shadow=false, $cookie=false) { 92 | if (!$this->allow_plain) 93 | return self::UNKNOWN; 94 | if (!isset($this->users[$username])) 95 | return self::BAD_USER; 96 | $expected_shadow = $this->preshadowed ? 97 | $this->users[$username] : 98 | self::shadow($username, $this->users[$username], $this->context); 99 | if ( ($password && $this->users[$username] !== 100 | ($this->preshadowed ? self::shadow($username, $password, $this->context) : $password) ) 101 | || ($shadow && $expected_shadow !== $shadow) ) 102 | return self::BAD_RESPONSE; 103 | $this->set_cookie($username, null, $expected_shadow, $cookie); 104 | return $username; 105 | } 106 | 107 | function authenticate() { 108 | $authed = self::UNKNOWN; 109 | $cookie = false; 110 | 111 | if (isset($_POST['chap-username'])) { 112 | $username = $_POST['chap-username']; 113 | if (get_magic_quotes_gpc()) 114 | $username = stripslashes($username); 115 | if (isset($_POST['chap-response']) && $_POST['chap-response']) 116 | $authed = $this->auth_handshake($username, $_POST['chap-response']); 117 | elseif (isset($_POST['chap-shadow']) && $_POST['chap-shadow']) 118 | $authed = $this->auth_plain($username, false, $_POST['chap-shadow']); 119 | elseif (isset($_POST['chap-password']) && $_POST['chap-password']) { 120 | $passwd = get_magic_quotes_gpc() ? 121 | stripslashes($_POST['chap-password']) : $_POST['chap-password']; 122 | $authed = $this->auth_plain($username, $passwd); 123 | } 124 | } 125 | elseif (isset($_COOKIE[$this->cookie_name]) && ($cookie = $this->get_cookie())) { 126 | if (isset($cookie['r'])) 127 | $authed = $this->auth_handshake($cookie['u'], $cookie['r'], $cookie); 128 | elseif (isset($cookie['s'])) 129 | $authed = $this->auth_plain($cookie['u'], false, $cookie['s'], $cookie); 130 | } 131 | 132 | return $authed; 133 | } 134 | 135 | function deauthorize() { 136 | if (isset($_COOKIE[$this->cookie_name])) { 137 | $url = new GBURL(gb::$site_url); 138 | setcookie($this->cookie_name, '', time()-1000, $url->path, $url->host, $url->secure); 139 | unset($_COOKIE[$this->cookie_name]); 140 | } 141 | } 142 | } 143 | ?> -------------------------------------------------------------------------------- /themes/default/post.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |
7 | commentsLink() ?> 8 |

title ?>

9 |

10 | 11 | published->age() ?> 12 | by author->name) . $post->tagLinks(', tagged ') . $post->categoryLinks(', filed under ') ?> 13 | 14 | tagLinks('tagged '); echo $s; echo ($s ? ', ':'') . $post->categoryLinks('filed under ') ?> 15 | 16 |

17 |
18 | body() ?> 19 |
20 |
21 |
22 |
23 | 24 | commentsOpen || count($post->comments)): ?> 25 |
26 |
27 | 28 |
29 |

30 | You posted a duplicate comment. Naughty! It was rejected. 31 |

32 |
33 | 34 |
35 |

36 | Your comment is being held for approval. 37 |

38 |

39 | It might have failed to be verified as "ham" or the author 40 | wants to manually approve of new comments. 41 |

42 |

43 | Hold in there! 44 |

45 |
46 | 47 | comments)): ?> 48 |

numberOfComments() ?>

49 |
    50 | comments as $level => $comment): 53 | if ($level > $prevlevel) 54 | echo '
      '; 55 | elseif ($level < $prevlevel) 56 | echo str_repeat('
    ', $prevlevel-$level); 57 | $prevlevel = $level; 58 | ?> 59 | 90 |
  • 91 | ', $prevlevel); ?> 92 |
93 | 94 |
95 | comments->countUnapproved()): ?> 96 |

97 | numberOfUnapprovedComments() ?> awaiting approval 98 | comments->countShadowed()): ?> 99 | — numberOfShadowedComments( 100 | 'approved comment is', 'approved comments are', 'no', 'one') ?> 101 | therefore not visible. 102 | 103 |

104 | 105 | 106 | 107 | commentsOpen): ?> 108 |
109 |

Comment

110 |
111 |
112 |

113 | 114 |

115 |

116 | 119 |

120 |

121 | 122 |

123 |
124 |
125 | 126 | 127 |

Comments are closed.

128 | 129 |
130 | 131 |
132 |
133 | 134 | -------------------------------------------------------------------------------- /lib/gb_upgrade.php: -------------------------------------------------------------------------------- 1 | storage()->set($plugins); 33 | unset(gb::$site_state['plugins']); 34 | 35 | # write data/site.json 36 | gb::log('moving %s -> data:site', gb::$site_dir.'/site.json'); 37 | # gb_maint::sync_site_state() will be called after this method returns 38 | @unlink(gb::$site_dir.'/site.json'); 39 | 40 | # remove /site.json from .gitignore 41 | if (gb_maint::gitignore_sub('/(?:\r?\n)\/site\.json([\t\s \r\n]+|^)/m', '$1')) { 42 | gb::log('removed "/site.json" from .gitignore'); 43 | $added[] = git::add('.gitignore'); 44 | } 45 | 46 | # load settings.json 47 | gb::log('loading %s', gb::$site_dir.'/settings.json'); 48 | $settings = is_readable(gb::$site_dir.'/settings.json') ? 49 | json_decode(file_get_contents(gb::$site_dir.'/settings.json'), true) : array(); 50 | 51 | # move settings.json:* to data/plugins/* 52 | foreach ($settings as $pluginn => $d) { 53 | if (!is_array($d)) 54 | $d = $d !== null ? array($d) : array(); 55 | if ($d) { 56 | gb::log('copying %s:%s -> data:plugins/%s', gb::$site_dir.'/settings.json', $pluginn, $pluginn); 57 | gb::data('plugins/'.$pluginn)->storage()->set($d); 58 | } 59 | } 60 | gb::log('removing old %s', gb::$site_dir.'/settings.json'); 61 | @unlink(gb::$site_dir.'/settings.json'); 62 | 63 | # load gb-users.php 64 | $users = array(); 65 | if (is_readable(gb::$site_dir.'/gb-users.php')) { 66 | gb::log('loading %s', gb::$site_dir.'/gb-users.php'); 67 | eval('class GBUserAccount { 68 | static function __set_state($state) { 69 | return GBUser::__set_state($state); 70 | } 71 | }'); 72 | require gb::$site_dir.'/gb-users.php'; 73 | if (isset($db)) { 74 | $admin = isset($db['_admin']) ? $db['_admin'] : ''; 75 | foreach ($db as $email => $user) { 76 | if (is_object($user)) { 77 | $user->admin = ($email === $admin); 78 | $users[$email] = $user; 79 | gb::log('transponded user %s', $email); 80 | } 81 | } 82 | } 83 | } 84 | 85 | # move gb-users.php to data/users.json 86 | gb::log('moving %s -> data:users', gb::$site_dir.'/gb-users.php'); 87 | GBUser::storage()->set($users); 88 | @unlink(gb::$site_dir.'/gb-users.php'); 89 | 90 | # commit any modifications 91 | if ($added) { 92 | try { 93 | git::commit('upgrade 0.1.4 modified ' 94 | . implode(', ',$added), GBUser::findAdmin()->gitAuthor(), $added); 95 | } 96 | catch (GitError $e) { 97 | if (strpos($e->getMessage(), 'no changes added to commit') === false) 98 | throw $e; 99 | } 100 | } 101 | } 102 | 103 | static function _000105($from, $to) { 104 | # remove old hooks, allowing new symlinked ones to appear (which will 105 | # happen after the upgrade is complete, by effect of calling 106 | # gb_main::sync_site_state()). 107 | foreach (array('post-commit', 'post-update') as $name) { 108 | $path = gb::$site_dir.'/.git/hooks/'.$name; 109 | if (is_file($path)) { 110 | gb::log('removing old hook %s', $path); 111 | unlink($path); 112 | } 113 | } 114 | } 115 | 116 | static function build_stages($from, $to) { 117 | $stages = array(); 118 | for ($v=$from+1; $v<=$to; $v++) { 119 | $f = array(__CLASS__, sprintf('_%06x', $v)); 120 | if (method_exists($f[0], $f[1])) 121 | $stages[$v] = $f; 122 | } 123 | return $stages; 124 | } 125 | 126 | static function perform($from, $to) { 127 | $from = gb::version_parse($from); 128 | $to = gb::version_parse($to); 129 | $froms = gb::version_format($from); 130 | $tos = gb::version_format($to); 131 | if ($from === $to) 132 | return null; 133 | $is_upgrade = $from < $to; 134 | $stages = self::build_stages($from, $to); 135 | 136 | gb::log('%s gitblog %s -> %s in %d stages', 137 | ($is_upgrade ? 'upgrading' : 'downgrading'), $froms, $tos, count($stages)); 138 | 139 | # don't break on client abort 140 | ignore_user_abort(true); 141 | 142 | foreach ($stages as $v => $stagefunc) { 143 | $prevvs = gb::version_format($v > 0 ? $v-1 : $v); 144 | gb::log('%s -> %s (%s)', $prevvs, gb::version_format($v), $stagefunc[1], $v); 145 | 146 | # write prev version to site.json so next run will take off where we crashed, if we crash. 147 | $orig_v = gb::$version; 148 | gb::$version = $prevvs; 149 | gb_maint::sync_site_state(); 150 | gb::$version = $orig_v; 151 | 152 | $stages[$v] = call_user_func($stagefunc, $from, $to); 153 | } 154 | 155 | gb_maint::sync_site_state(); 156 | 157 | return $stages; 158 | } 159 | } 160 | ?> -------------------------------------------------------------------------------- /lib/gb_admin.php: -------------------------------------------------------------------------------- 1 | ] array( string title [, string uri [, array( item .. )]] ) 11 | */ 12 | static public $menu = array( 13 | 'h' => array('Dashboard',''), 14 | array('New',null,array( 15 | 'n' => array('Post', 'edit/post.php'), 16 | 'p' => array('Page', '#todo:edit/page.php') 17 | )), 18 | array('Manage',null, array( 19 | array('Posts','manage/posts.php'), 20 | array('Pages','#todo:manage/pages.php'), 21 | array('Comments','manage/comments.php'), 22 | array('Attachments','#todo:manage/attachments.php') 23 | )), 24 | array('Settings',null, array( 25 | array('Basics','settings/basics.php'), 26 | array('Theme','#todo:settings/theme.php'), 27 | array('Plugins','#todo:settings/plugins.php', array( 28 | # This is a good place for plugins to add custom menu items. Example: 29 | # array('Some plugin', '../plugins/some-plugin/ui.php') 30 | )) 31 | )), 32 | array('Maintenance',null,array( 33 | 'r' => array('Rebuild', 'maintenance/rebuild.php'), 34 | array('Import Wordpress site', 'maintenance/import-wordpress.php'), 35 | array('Status', 'maintenance/git-status.php') 36 | )) 37 | ); 38 | 39 | # resolved by render_menu 40 | public static $current_domid = ''; 41 | 42 | function render_menu($menu_disabled=false, $items=null, $baseurl=null, $currurlpath=null, 43 | $liststart='
    ', $listend='
') 44 | { 45 | if ($items === null) 46 | $items = self::$menu; 47 | if ($baseurl === null) 48 | $baseurl = gb_admin::$url; 49 | if ($currurlpath === null) 50 | $currurlpath = gb::url()->path; 51 | $accesskey_prefix = ''; 52 | $is_osx = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Mac OS X') !== false; 53 | if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') !== false) { 54 | if ($is_osx) 55 | $accesskey_prefix = '⌃⌥'; 56 | } 57 | $s = $liststart; 58 | foreach ($items as $k => $item) { 59 | $uri = $url = ''; 60 | $is_curr = $is_todo = false; 61 | $accesskey = is_string($k) ? strtoupper($k) : ''; 62 | if (isset($item[1]) && is_string($item[1])) { 63 | $uri = $item[1]; 64 | $url = ($uri && ($uri{0} === '/' || strpos($uri, '://') !== false)) ? $uri : $baseurl . $uri; 65 | $url_st = GBURL::parse($url); 66 | $actual_currpath = $url_st->path; 67 | $is_todo = strpos($url_st->fragment, 'todo:') === 0; 68 | $is_curr = ( !$is_todo ) && ( $actual_currpath === substr($currurlpath, 0, strlen($actual_currpath)) ); 69 | if ($uri === '') 70 | $is_curr = gb::url()->path === GBURL::parse(gb_admin::$url)->path; 71 | } 72 | $dom_id = $uri ? gb_strtodomid(gb_filenoext($item[1])) : $k; 73 | $s .= ''; 95 | } 96 | $s .= $listend; 97 | return $s; 98 | } 99 | 100 | static function error_rsp($msg, $status='400 Bad Request', $content_type='text/plain; charset=utf-8', $exit=true) { 101 | self::abrupt_rsp($status."\n".$msg."\n", $status, $content_type, $exit); 102 | } 103 | 104 | static function json_rsp($data=null, $status='200 OK', $exit=true, $pretty=true) { 105 | if ($data !== null) 106 | $data = $pretty ? json::pretty($data)."\n" : json_encode($data); 107 | else 108 | $data = ''; 109 | self::abrupt_rsp($data, $status, 'application/json; charset=utf-8', $exit); 110 | } 111 | 112 | static function abrupt_rsp($body='', $status='200 OK', $content_type='text/plain; charset=utf-8', $exit=true) { 113 | if (!$body) 114 | $body = ''; 115 | if (!headers_sent()) { 116 | if ($status) 117 | header('HTTP/1.1 '.$status); 118 | if ($body) { 119 | if ($content_type) 120 | header('Content-Type: '.$content_type); 121 | header('Content-Length: '.strlen($body)); 122 | } 123 | header('Cache-Control: no-cache'); 124 | } 125 | if ($exit) 126 | exit($body); 127 | echo $body; 128 | } 129 | 130 | static function mkdirs($path, $maxdepth=999, $mode=0775) { 131 | if ($maxdepth <= 0) 132 | return; 133 | $parent = dirname($path); 134 | if (!is_dir($parent)) 135 | self::mkdirs($parent, $maxdepth-1, $mode); 136 | mkdir($path, $mode); 137 | @chmod($path, $mode); 138 | } 139 | 140 | /** Write blob version of $obj. Returns path written to or false if write was not needed. */ 141 | static function write_content(GBContent $obj) { 142 | # build blob 143 | $blob = $obj->toBlob(); 144 | 145 | # build destination path 146 | $dstpath = gb::$site_dir.'/'.$obj->name; 147 | 148 | # assure destination dir is prepared 149 | $dstpathdir = dirname($dstpath); 150 | if (!is_dir($dstpathdir)) 151 | self::mkdirs($dstpathdir); 152 | 153 | # write 154 | file_put_contents($dstpath, $blob, LOCK_EX); 155 | @chmod($dstpath, 0664); 156 | 157 | return $dstpath; 158 | } 159 | } 160 | 161 | gb_admin::$url = gb::$site_url.'gitblog/admin/'; 162 | 163 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitblog 2 | 3 | A git-based blog/cms platform for PHP, meant as a replacement for Wordpress. 4 | 5 | Post-action hooks in git are used to manage an intermediate cache which consist only of structured data (no formatting), allowing dynamic presentation. This is one of the biggest differences tech-wise in comparison to Jekyll and similar tools. 6 | 7 | Licensed under MIT means free to use for everyone. See [LICENSE](http://github.com/rsms/gitblog/blob/master/LICENSE) for more information. 8 | 9 | 10 | ## Features 11 | 12 | - Fully git-based -- no mysql or similar involved 13 | - Everything is versioned 14 | - Themes 15 | - No custom file formats for content (only [JSON](http://json.org/), [Markdown](http://daringfireball.net/projects/markdown/) (with [Markdown extra](http://michelf.com/projects/php-markdown/extra/)) and HTML) 16 | - High performance 17 | - Hierarchical comments with optional spam filtering based on [Akismet](http://akismet.com/) 18 | - Remote editing (git push/pull) 19 | - Wordpress import 20 | - [Plugins](http://github.com/rsms/gitblog/blob/master/docs/plugins.md) 21 | 22 | (See section "Future features" for a list of possible future features) 23 | 24 | ## Installing & Getting started 25 | 26 | Clone a copy of gitblog: 27 | 28 | $ cd /path/to/my-blog 29 | $ git clone git://github.com/rsms/gitblog.git 30 | 31 | If your web server is not running as yourself, your group, or the root user, you need to change owner. In this example `www-data` is the web server user. (You will still be able to edit the blog.) 32 | 33 | $ chmod -R g+w . 34 | $ sudo chown -R www-data . 35 | 36 | Open a web browser and point it to your `/my-blog/gitblog`. Enter email and your real name -- these will be used for commit messages. Also choose a good pass phrase which in combination with your email will grant you administration privileges in the web administration interface. 37 | 38 | When you're done you should see a single "Hello world" post. Okay, all good. 39 | 40 | > **What did just happen?** Gitblog initialized a git repository in `/path/to/my-blog` and added a few standard files and directories. If you ever would like to start over, just delete everything except the gitblog directory and visit `/my-blog/gitblog` in a browser again. 41 | 42 | Let's try editing the hello world post: 43 | 44 | $ $EDITOR content/posts/*/*-*-hello-world.html 45 | 46 | Make some changes, be creative! 47 | 48 | To demonstrate that the "working tree" is indeed a working area and not the live stage, reload your web browser and see that the "Hello world" post is still not modified. 49 | 50 | > **Tip:** You can view your work in progress by being signed in and appending `?preview` to the url. 51 | 52 | Now, let's commit the changes, pusing them live: 53 | 54 | $ git commit -m 'Updated my awesome hello-world post' content 55 | 56 | Reload your web browser and... voila! 57 | 58 | > **Warnings when committing?** If you see `error: Could not access 'HEAD@{1}'` on stderr when committing, do not worry. This is an issue that currently do not affect gitblog, but we're looking into what causes it. 59 | 60 | 61 | ### Importing a Wordpress blog 62 | 63 | If you have a Wordpress blog you would like to import, there is a built-in tool which does it for you! Just visit `/my-blog/gitblog/admin/import-wordpress.php` and follow the simple instructions. 64 | 65 | 66 | ## Documentation 67 | 68 | The [docs directory](http://github.com/rsms/gitblog/tree/master/docs) contains a number of documents covering different parts of Gitblog. 69 | 70 | 71 | ## Requirements 72 | 73 | - PHP 5.2 or newer (only standard modules are needed though) 74 | - Git 1.6 or newer 75 | - POSIX system 76 | 77 | 78 | ## Further play 79 | 80 | The gb-config.php file (present in your site root) contains site-specific configuration. A default gb-config.php file, as it looks just after a blog has been setup, contains only the minimum set of paramters. There are a bunch of other paramters which might do something you whish. 81 | 82 | Have a look in the file `gitblog/gitblog.php` -- scroll down a few lines and you'll find a class called `gb` which houses documentation and a list of all available configuration parameters, as well as their default values. 83 | 84 | 85 | ## Known bugs and issues 86 | 87 | - Post-hook system is a bit shaky because of the nature of itself. Running scripts directly instead of POSTing to a URL would be better but many systems does not have CLI PHP or have another version than the web PHP. 88 | 89 | ## Future features 90 | 91 | ### Work in progress 92 | 93 | - Web administration 94 | 95 | ### Planned 96 | 97 | - Pingback 98 | - Search 99 | 100 | ### Under consideration 101 | 102 | - Configure what parts are versioned (e.g. disable versioning of comments) 103 | - Alternate storage 104 | - Caching (memcached, redis, etc) 105 | 106 | 107 | ## Authors 108 | 109 | - Rasmus Andersson <rasmus notion.se> 110 | 111 | 112 | ## History 113 | 114 | A strangely cold morning in june 2009 Mattias Arrelid pressed the "Yeah, upgrade Wordpress". What happened seconds later still brings me down sometimes... *Every file* on our server--removable by the web server--was deleted in an instant. Many years worth of photos, audio recordings and not to mention the 30+ web sites which disappeared into the void of an unrecoverable ext3 file system. 115 | 116 | We swore to never again use Wordpress and to do backups. 117 | 118 | As we all like Git--this pretty little creation of the open source community--the blog tool of our future was of course based on Git. But after giving a few days of research we had not found any tool that suited our taste. (The closest match was [Jekyll](http://github.com/mojombo/jekyll/), however we wanted something more flexible, like Word...euhm). So what the heck, after all we are software engineers so why not write something ourselves? 119 | 120 | Gitblog was born. 121 | -------------------------------------------------------------------------------- /admin/res/gb-admin.js: -------------------------------------------------------------------------------- 1 | if(typeof console=='undefined')console={}; 2 | if(typeof console.log=='undefined')console.log=function(){}; 3 | var c = console; 4 | var gb = {}; 5 | 6 | var ui = { 7 | alert: function(msg) { 8 | var ul = $('#gb-errors ul'); 9 | ul.html(''); 10 | ul.append('
  • An error occured
  • '); 11 | ul.append('
  • '); 12 | $('#gb-errors ul li.msg').text(msg); 13 | $('#gb-errors').slideDown("fast"); 14 | }, 15 | 16 | hideAlert: function() { 17 | $('#gb-errors').slideUp("fast"); 18 | } 19 | }; 20 | 21 | var http = { 22 | postForm: function(kw) { 23 | kw.data = http.toFormParams(kw.data); 24 | return http.post(kw); 25 | }, 26 | 27 | post: function(kw) { 28 | kw.type = "post"; 29 | c.log(kw.type, kw.url, kw.data); 30 | return $.ajax(kw); 31 | }, 32 | 33 | toFormParams: function(params) { 34 | if (typeof params == 'object') 35 | for (var k in params) 36 | http._flattenComplexFormParams(params, k, params[k]); 37 | return params; 38 | }, 39 | 40 | _flattenComplexFormParams: function(params, k, v) { 41 | if (typeof v == 'object') { 42 | for (var x in v) { 43 | var k2 = k+'['+x+']'; 44 | var v2 = v[x]; 45 | if (!http._flattenComplexFormParams(params, k2, v2)) 46 | params[k2] = v2; 47 | } 48 | delete(params[k]); 49 | return true; 50 | } 51 | return false; 52 | } 53 | }; 54 | 55 | /** JS equivalent of gb::data() */ 56 | gb.data = function(doc_id, default_data) { 57 | return new gb.RemoteDict(doc_id, default_data); 58 | }; 59 | 60 | /** JS equivalent of the JSONDict */ 61 | gb.RemoteDict = function(doc_id, default_data) { 62 | var self = this; 63 | this.restServiceURL = '../helpers/data.php/'; 64 | this.docID = doc_id; 65 | this.data = (typeof default_data == 'object' && default_data) ? default_data : {}; 66 | 67 | this.get = function(keypath, fallback) { 68 | var path = keypath.replace(/(^[ \t\r\n]+|[ \t\r\n]+$)/, '').split(/\/+/); 69 | if (path.length == 0 || typeof this.data != 'object') 70 | return fallback; 71 | var v = this.data[path[0]]; 72 | if (path.length == 1) 73 | return v; 74 | for (var i=1; i array('file1', 'file3'), GitPatch::DELETE => array('file2')) 13 | public $previousFiles; # available for GitPatch::RENAME and COPY. array('file1', 'file2', ..) 14 | 15 | /** Tries to resolve author as a GBUser */ 16 | function authorUser($default=null) { 17 | $user = $this->authorEmail ? GBUser::find($this->authorEmail) : null; 18 | if ($user === null) 19 | return $default; 20 | return $user; 21 | } 22 | 23 | /** Tries to resolve committer as a GBUser */ 24 | function committerUser() { 25 | $user = $this->committerEmail ? GBUser::find($this->committerEmail) : null; 26 | if ($user === null) 27 | return $default; 28 | } 29 | 30 | function rawPatch($paths=null) { 31 | $paths = git::escargs($paths); 32 | $cmd = 'log -p --full-index --pretty="format:" --encoding=UTF-8 --date=iso --dense ' 33 | .escapeshellarg($this->id) 34 | .' -1'; 35 | if ($paths) 36 | $cmd .= ' -- '.$paths; 37 | $s = git::exec($cmd); 38 | return $s ? substr($s, 1, -1) : ''; 39 | } 40 | 41 | /** Load a set of GitPatch objects for this commit, optionally restricting to patches affecting $paths */ 42 | function loadPatches($paths=null) { 43 | if (($x = $this->rawPatch($paths))) 44 | return GitPatch::parse($x); 45 | return array(); 46 | } 47 | 48 | static public $fields = array( 49 | 'id','tree', 50 | 'authorEmail','authorName','authorDate', 51 | 'comitterEmail','comitterName', 'comitterDate', 52 | 'message'); 53 | 54 | static public $logFormat = '%H%n%T%n%ae%n%an%n%ai%n%ce%n%cn%n%ci%n%s%x00'; 55 | 56 | /** Returns array($commits, $existing, $ntoc) */ 57 | static function find($kwargs=null) { 58 | static $defaultkwargs = array( 59 | 'names' => null, 60 | 'treeish' => 'HEAD', 61 | 'limit' => -1, 62 | 'sortrcron' => true, 63 | 'detectRC' => true, 64 | 'mapnamestoc' => false # if true, returns a 3rd arg: map[name] => commit 65 | ); 66 | $kwargs = $kwargs ? array_merge($defaultkwargs, $kwargs) : $defaultkwargs; 67 | $commits = array(); 68 | $existing = array(); # tracks existing files 69 | $ntoc = $kwargs['mapnamestoc'] ? array() : null; 70 | 71 | $cmd = "log --name-status --pretty='format:".self::$logFormat."' " 72 | ."--encoding=UTF-8 --date=iso --dense"; 73 | 74 | # do not sort reverse chronological 75 | $rcron = $kwargs['sortrcron']; 76 | if (!$rcron) 77 | $cmd .= ' --reverse'; 78 | 79 | # detect renames and copies 80 | if ($kwargs['detectRC']) 81 | $cmd .= ' -C'; 82 | else 83 | $cmd .= ' --no-renames'; 84 | 85 | # limit 86 | if ($kwargs['limit'] > 0) 87 | $cmd .= " --max-count=".$kwargs['limit']; 88 | 89 | # treeish 90 | $cmd .= " ".$kwargs['treeish']." -- "; 91 | 92 | # filter object names 93 | if ($kwargs['names']) 94 | $cmd .= implode(' ', array_map('escapeshellarg', $kwargs['names'])); 95 | 96 | #var_dump($cmd); 97 | $out = git::exec($cmd); 98 | #var_dump($out); 99 | 100 | $a = 0; 101 | $len = strlen($out); 102 | 103 | while ($a !== false && $a <= $len) { 104 | $c = new self(); 105 | 106 | foreach (self::$fields as $field) { 107 | if ($field == 'message') 108 | $b = strpos($out, "\0", $a); 109 | else 110 | $b = strpos($out, "\n", $a); 111 | 112 | if ($b === false) 113 | break; 114 | 115 | if (substr($field, -4) == 'Date') 116 | $c->$field = new GBDateTime(substr($out, $a, $b-$a)); 117 | else 118 | $c->$field = substr($out, $a, $b-$a); 119 | 120 | $a = $b + 1; 121 | } 122 | 123 | if ($b === false) 124 | break; 125 | 126 | $b = strpos($out, "\n\n", $a); 127 | $files = ($b === false) ? substr($out, $a) : substr($out, $a, $b-$a); 128 | $files = explode("\n", trim($files)); 129 | $c->files = array(); 130 | 131 | foreach ($files as $line) { 132 | $line = explode("\t", $line); 133 | if (count($line) < 2) 134 | continue; 135 | $t = $line[0]{0}; 136 | $name = gb_normalize_git_name($line[1]); 137 | $previousName = null; 138 | 139 | # R|C have two names wherether the last is the new name 140 | if ($t === GitPatch::RENAME || $t === GitPatch::COPY) { 141 | $previousName = $name; 142 | $name = $line[2]; 143 | if ($c->previousFiles === null) 144 | $c->previousFiles = array($previousName); 145 | else 146 | $c->previousFiles[] = $previousName; 147 | } 148 | 149 | # add to files[tag] => [name, ..] 150 | if (isset($c->files[$t])) 151 | $c->files[$t][] = $name; 152 | else 153 | $c->files[$t] = array($name); 154 | 155 | # if kwarg mapnamestoc == true 156 | if ($ntoc !== null) { 157 | if (!isset($ntoc[$name])) 158 | $ntoc[$name] = array($c); 159 | else 160 | $ntoc[$name][] = $c; 161 | } 162 | 163 | # update cached objects 164 | #if (isset($repo->objectCacheByName[$name])) { 165 | # $obj = $repo->objectCacheByName[$name]; 166 | # if ($obj->_commit === null) 167 | # $obj->_commit = $c; 168 | #} 169 | 170 | # update existing 171 | if (!$rcron) { 172 | if ($t === GitPatch::CREATE || $t === GitPatch::COPY) 173 | $existing[$name] = $c; 174 | elseif ($t === GitPatch::DELETE && isset($existing[$name])) 175 | unset($existing[$name]); 176 | elseif ($t === GitPatch::RENAME) { 177 | if (isset($existing[$previousName])) { 178 | # move original CREATE 179 | $existing[$name] = $existing[$previousName]; 180 | unset($existing[$previousName]); 181 | } 182 | else { 183 | $existing[$name] = $c; 184 | } 185 | # move commits from previous file if kwarg mapnamestoc == true 186 | if ($ntoc !== null && isset($ntoc[$previousName])) { 187 | $ntoc[$name] = array_merge($ntoc[$previousName], $ntoc[$name]); 188 | unset($ntoc[$previousName]); 189 | } 190 | } 191 | } 192 | } 193 | 194 | $commits[$c->id] = $c; 195 | 196 | if ($b === false) 197 | break; 198 | $a = $b + 2; 199 | } 200 | 201 | return array($commits, $existing, $ntoc); 202 | } 203 | } 204 | ?> -------------------------------------------------------------------------------- /lib/json.php: -------------------------------------------------------------------------------- 1 | 100) 16 | throw new OverflowException('too deep or recursion'); 17 | 18 | $type = gettype($var); 19 | 20 | switch ($type) { 21 | case 'boolean': 22 | return $var ? 'true' : 'false'; 23 | 24 | case 'NULL': 25 | return 'null'; 26 | 27 | case 'integer': 28 | return (int) $var; 29 | 30 | case 'double': 31 | case 'float': 32 | return (float) $var; 33 | 34 | case 'string': 35 | # STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT 36 | $ascii = ''; 37 | $strlen_var = strlen($var); 38 | 39 | # Iterate over every character in the string, 40 | # escaping with a slash or encoding to UTF-8 where necessary 41 | for ($c = 0; $c < $strlen_var; ++$c) { 42 | $ord_var_c = ord($var{$c}); 43 | switch (true) { 44 | case $ord_var_c === 0x08: 45 | $ascii .= '\b'; 46 | break; 47 | case $ord_var_c === 0x09: 48 | $ascii .= '\t'; 49 | break; 50 | case $ord_var_c === 0x0A: 51 | $ascii .= '\n'; 52 | break; 53 | case $ord_var_c === 0x0C: 54 | $ascii .= '\f'; 55 | break; 56 | case $ord_var_c === 0x0D: 57 | $ascii .= '\r'; 58 | break; 59 | 60 | case $ord_var_c === 0x22: 61 | #case $ord_var_c === 0x2F: # the spec is not 100% clear on "/" 62 | case $ord_var_c === 0x5C: 63 | # double quote, slash, slosh 64 | $ascii .= '\\'.$var{$c}; 65 | break; 66 | 67 | case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): 68 | # characters U-00000000 - U-0000007F (same as ASCII) 69 | $ascii .= $var{$c}; 70 | break; 71 | 72 | case (($ord_var_c & 0xE0) === 0xC0): 73 | # characters U-00000080 - U-000007FF, mask 110XXXXX 74 | # see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 75 | $char = pack('C*', $ord_var_c, ord($var{$c + 1})); 76 | $c += 1; 77 | $utf16 = gb_utf8_to_utf16($char); 78 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 79 | break; 80 | 81 | case (($ord_var_c & 0xF0) === 0xE0): 82 | # characters U-00000800 - U-0000FFFF, mask 1110XXXX 83 | $char = pack('C*', $ord_var_c, 84 | ord($var{$c + 1}), 85 | ord($var{$c + 2})); 86 | $c += 2; 87 | $utf16 = gb_utf8_to_utf16($char); 88 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 89 | break; 90 | 91 | case (($ord_var_c & 0xF8) === 0xF0): 92 | # characters U-00010000 - U-001FFFFF, mask 11110XXX 93 | $char = pack('C*', $ord_var_c, 94 | ord($var{$c + 1}), 95 | ord($var{$c + 2}), 96 | ord($var{$c + 3})); 97 | $c += 3; 98 | $utf16 = gb_utf8_to_utf16($char); 99 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 100 | break; 101 | 102 | case (($ord_var_c & 0xFC) === 0xF8): 103 | # characters U-00200000 - U-03FFFFFF, mask 111110XX 104 | $char = pack('C*', $ord_var_c, 105 | ord($var{$c + 1}), 106 | ord($var{$c + 2}), 107 | ord($var{$c + 3}), 108 | ord($var{$c + 4})); 109 | $c += 4; 110 | $utf16 = gb_utf8_to_utf16($char); 111 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 112 | break; 113 | 114 | case (($ord_var_c & 0xFE) === 0xFC): 115 | # characters U-04000000 - U-7FFFFFFF, mask 1111110X 116 | $char = pack('C*', $ord_var_c, 117 | ord($var{$c + 1}), 118 | ord($var{$c + 2}), 119 | ord($var{$c + 3}), 120 | ord($var{$c + 4}), 121 | ord($var{$c + 5})); 122 | $c += 5; 123 | $utf16 = gb_utf8_to_utf16($char); 124 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 125 | break; 126 | } 127 | } 128 | 129 | return '"'.$ascii.'"'; 130 | 131 | case 'array': 132 | case 'object': 133 | $indent = $level ? str_repeat("\t", $level) : ''; 134 | 135 | if ($force_object && !$var) 136 | return '{}'; 137 | 138 | if ($type === 'object' || ($var && array_keys($var) !== range(0, count($var)-1))) { 139 | if ($type === 'object') { 140 | if (method_exists($var, '__sleep')) 141 | $var = array_intersect_key(get_object_vars($var), array_flip($var->__sleep())); 142 | else 143 | $var = get_object_vars($var); 144 | } 145 | if (!$var) 146 | return '{}'; 147 | $s = ''; 148 | $count = count($var); 149 | $i = 0; 150 | foreach ($var as $k => $v) { 151 | $comma = ++$i === $count ? '':','; 152 | $s .= "\n\t".$indent . json_encode(strval($k)) . ': ' 153 | . self::pretty($v, $compact, $force_object, $level+1) . $comma; 154 | } 155 | $s = '{' . $s . "\n".$indent.'}'; 156 | } 157 | else { 158 | if (!$var) 159 | return '[]'; 160 | 161 | $s = '['; 162 | $count = count($var); 163 | $i = 0; 164 | foreach ($var as $v) { 165 | $comma = ++$i === $count ? '':','; 166 | $s .= "\n\t".$indent . self::pretty($v, $compact, $force_object, $level+1) . $comma; 167 | } 168 | $s .= "\n".$indent.']'; 169 | } 170 | static $m = array("\t"=>'', "\n"=>'', ','=>', '); 171 | return ($compact && strlen($s) < 100 && ($s2 = strtr($s, $m)) && (strlen($s2) + (strlen($indent)*8) < 80)) ? $s2 : $s; 172 | 173 | default: 174 | throw new UnexpectedValueException(gettype($var).' can not be encoded as JSON'); 175 | } 176 | } 177 | } 178 | 179 | if (function_exists('mb_convert_encoding')) { 180 | function gb_utf8_to_utf16($s) { return mb_convert_encoding($s, 'UTF-16', 'UTF-8'); } 181 | } 182 | else { 183 | function gb_utf8_to_utf16($utf8) { 184 | switch(strlen($utf8)) { 185 | case 1: 186 | return $utf8; 187 | case 2: 188 | return chr(0x07 & (ord($utf8{0}) >> 2)) . chr((0xC0 & (ord($utf8{0}) << 6)) | (0x3F & ord($utf8{1}))); 189 | case 3: 190 | return chr((0xF0 & (ord($utf8{0}) << 4)) | (0x0F & (ord($utf8{1}) >> 2))) 191 | . chr((0xC0 & (ord($utf8{1}) << 6)) | (0x7F & ord($utf8{2}))); 192 | } 193 | return '?'; 194 | } 195 | } 196 | 197 | ?> -------------------------------------------------------------------------------- /admin/manage/posts.php: -------------------------------------------------------------------------------- 1 | draft) 27 | $flags .= st::DRAFT; 28 | switch ($status) { 29 | # added'|'modified'|'deleted renamed 30 | case 'added': 31 | $flags .= st::ADDED; 32 | break; 33 | case 'modified': 34 | $flags .= st::MODIFIED; 35 | break; 36 | case 'deleted': 37 | $flags .= st::REMOVED; 38 | break; 39 | case 'renamed': 40 | $flags .= st::RENAMED; 41 | break; 42 | } 43 | return $flags; 44 | } 45 | 46 | function _add_posts_from_status($st, $prefixmatch, $stage, $stageflag) { 47 | global $muxed_posts; 48 | if (isset($st[$stage])) { 49 | foreach ($st[$stage] as $name => $t) { 50 | if (substr($name,0,strlen($prefixmatch)) !== $prefixmatch) 51 | continue; 52 | $status = is_array($t) ? $t['status'] : ''; 53 | $post = GBPost::findByName($name, $status === 'deleted' ? null : 'work', false); 54 | if ($post) { 55 | if (!isset($muxed_posts[$post->name])) 56 | $muxed_posts[$post->name] = array(); 57 | $muxed_posts[$post->name][] = array($post, $stageflag._mkflags($post, $status)); 58 | } 59 | } 60 | } 61 | } 62 | 63 | # Add dirty staged, unstaged and untracked files 64 | _add_posts_from_status($st, 'content/posts/', 'staged', st::STAGED); 65 | _add_posts_from_status($st, 'content/posts/', 'unstaged', st::UNSTAGED); 66 | _add_posts_from_status($st, 'content/posts/', 'untracked', st::UNTRACKED); 67 | 68 | # Add clean drafts 69 | foreach (gb::index('draft-posts') as $post) { 70 | if (!isset($muxed_posts[$post->name])) 71 | $muxed_posts[$post->name] = array(); 72 | $muxed_posts[$post->name][] = array($post, st::DRAFT); 73 | } 74 | 75 | function _post_tuple_sortfunc($a,$b) { 76 | return $b[0]->modified->time - $a[0]->modified->time; 77 | } 78 | function _muxed_posts_sortfunc($a, $b) { 79 | return $b[0][0]->modified->time - $a[0][0]->modified->time; 80 | } 81 | 82 | # Add published and scheduled posts 83 | $pageno = 0; # pages are 0 (zero) indiced 84 | $maxpages = 5; 85 | $num_more_postpages = 0; 86 | do { 87 | $postspage = GBPost::pageByPageno($pageno); 88 | if (!$postspage) 89 | break; 90 | foreach ($postspage->posts as $rank => $post) { 91 | if (!isset($muxed_posts[$post->name])) 92 | $muxed_posts[$post->name] = array(); 93 | if ($post->published->time > time()) { 94 | $muxed_posts[$post->name][] = array($post, st::SCHEDULED.($post->draft ? st::DRAFT : '')); 95 | uasort($muxed_posts[$post->name], '_post_tuple_sortfunc'); 96 | } 97 | else { 98 | #$online[] = $post; 99 | $muxed_posts[$post->name][] = array($post, st::STAGED.($post->draft ? st::DRAFT : '')); 100 | } 101 | } 102 | if ($pageno == $maxpages-1) { 103 | $num_more_postpages = $postspage->numpages - $maxpages; 104 | break; 105 | } 106 | } while ($pageno++ < $postspage->numpages); 107 | 108 | # sort by modified desc 109 | uasort($muxed_posts, '_muxed_posts_sortfunc'); 110 | 111 | ?> 112 | 139 |
    140 |
    141 |

    Posts

    142 |
    143 | Show: 144 | 148 | 152 | 156 | 160 | 164 |
    165 |
    166 | 167 | $posts): $childcount = 0; ?> 168 | 169 | name); ?> 170 | 172 | 191 | 192 | 193 | 194 | 200 | 201 | 202 |
    173 | 174 | title ? $post->title : '('.substr($post->name,strlen('content/posts/')).')') ?> 175 | 176 | 177 | 178 | published->age(null, null, null, '', null, 'a second', 'in ')) ?> 179 | 180 | 181 | 182 | Draft 183 | 184 | 185 | Untracked 186 | 187 | 188 | textBody(), 80));echo $s ? ' – '.$s : '' ?> 189 | 190 | author->shortName()) ?>modified->condensed()) ?>
    203 |
    204 | 205 | Load more pages 206 | 207 |
    208 |
    209 | 210 | -------------------------------------------------------------------------------- /admin/setup.php: -------------------------------------------------------------------------------- 1 | Missing email. 16 | Please supply a valid email address to be used for the administrator account.'; 17 | } 18 | if (!trim($_POST['passphrase'])) { 19 | gb::$errors[] = 'Empty pass phrase. 20 | The pass phrase is empty or contains only spaces.'; 21 | } 22 | if ($_POST['passphrase'] !== $_POST['passphrase2']) { 23 | gb::$errors[] = 'Pass phrases not matching. 24 | You need to type in the same pass phrase in the two input fields below.'; 25 | } 26 | 27 | # ------------------------------------------------------------------------- 28 | # create gb-config.php 29 | if (!gb::$errors) { 30 | $config_path = gb::$site_dir."/gb-config.php"; 31 | $s = file_get_contents(gb::$dir.'/skeleton/gb-config.php'); 32 | # title 33 | $s = preg_replace('/(gb::\$site_title[\t ]*=[\t ]*)\'[^\']*\';/', 34 | '${1}'.var_export($_POST['title'],1).";", $s, 1); 35 | # secret 36 | $secret = ''; 37 | while (strlen($secret) < 62) { 38 | mt_srand(); 39 | $secret .= base_convert(mt_rand(), 10, 36); 40 | } 41 | $s = preg_replace('/^(gb::\$secret[\t ]*=[\t ]*)\'\';/m', 42 | '${1}'.var_export($secret,1).";", $s, 1); 43 | file_put_contents($config_path, $s); 44 | chmod($config_path, 0660); 45 | # reload config 46 | require $config_path; 47 | } 48 | 49 | # ------------------------------------------------------------------------- 50 | # Can git be found and if so, what version? 51 | try { 52 | $version = array_pop(explode(' ', trim(git::exec("--version")))); 53 | $version = array_map('intval', explode('.', $version)); 54 | if ($version[0] < 1 || $version[1] < 6) { 55 | gb::$errors[] = 'To old git version. Gitblog requires git version 1.6 56 | or newer. Please upgrade your git. ('.h(`which git`).')'; 57 | } 58 | } 59 | catch (GitError $e) { 60 | gb::$errors[] = 'git not found in $PATH

    61 | 62 | If git is not installed, please install it. Otherwise you need to update PATH. 63 | Putting something like this in gb-config.php would do it:

    64 | 65 | $_ENV[\'PATH\'] .= \':/opt/local/bin\';

    66 | 67 | /opt/local/bin being the directory in which git is installed. 68 | Alternatively edit PATH in your php.ini file.

    69 | 70 | (Original error from shell: '.h($e->getMessage()).')'; 71 | } 72 | 73 | # ------------------------------------------------------------------------- 74 | # create repository 75 | if (!gb::$errors) { 76 | $add_sample_content = isset($_POST['add-sample-content']) && $_POST['add-sample-content'] === 'true'; 77 | if (!gb::init($add_sample_content)) 78 | gb::$errors[] = 'Failed to create and initialize repository at '.var_export(gb::$site_dir,1); 79 | } 80 | 81 | # ------------------------------------------------------------------------- 82 | # commit changes (done by gb::init()) 83 | if (!gb::$errors) { 84 | try { 85 | if (!git::commit('gitblog created', trim($_POST['name']).' <'.trim($_POST['email']).'>')) 86 | gb::$errors[] = 'failed to commit creation'; 87 | } 88 | catch (Exception $e) { 89 | gb::$errors[] = 'failed to commit creation: '.nl2br(h(strval($e))); 90 | } 91 | } 92 | 93 | # ------------------------------------------------------------------------- 94 | # create admin account 95 | if (!gb::$errors) { 96 | $_POST['email'] = trim($_POST['email']); 97 | $passhash = GBUser::passhash($_POST['email'], $_POST['passphrase']); 98 | $u = new GBUser(trim($_POST['name']), $_POST['email'], $passhash, true); 99 | $u->save(); # issues git add, that's why we do this after init 100 | } 101 | 102 | # ------------------------------------------------------------------------- 103 | # send the client along 104 | if (!gb::$errors) { 105 | header('Location: '.gb::$site_url); 106 | exit(0); 107 | } 108 | } 109 | 110 | # ------------------------------------------------------------------------------------------------ 111 | # Perform a few sanity checks 112 | #else { 113 | # 114 | #} 115 | 116 | # ------------------------------------------------------------------------------------------------ 117 | # prepare for rendering 118 | 119 | gb::$title[] = 'Setup'; 120 | $is_writable = is_writable(gb::$site_dir); 121 | 122 | if (!$is_writable) { 123 | gb::$errors[] = 'Ooops. The directory '.h(gb::$site_dir).' is not writable. 124 | Gitblog need to create a few files in this directory. 125 |

    126 | Please make this directory (highlighted above) writable and then reload this page.'; 127 | # todo: check if the web server user and/or is the same as user and/or group 128 | # on directory. If so, suggest a chmod, otherwise suggest a chown. 129 | } 130 | 131 | if (!isset($_POST['email'])) 132 | $_POST['email'] = git::config('user.email'); 133 | 134 | if (!isset($_POST['name'])) 135 | $_POST['name'] = git::config('user.name'); 136 | 137 | include '_header.php'; 138 | ?> 139 | 144 |
    145 |

    Setup your gitblog

    146 |

    147 | It's time to setup your new gitblog. 148 |

    149 |
    150 | 151 |
    152 |

    Create an administrator account

    153 |

    Email:

    154 | 155 |

    Real name:

    156 | 157 |

    158 | This will be used for commit messages, along with email. 159 | Commit history can not be changed afterwards, so please provide your real name here. 160 |

    161 |

    Pass phrase:

    162 | 163 | 164 |

    165 | Choose a pass phrase used to authenticate as administrator. Type it twice. 166 |

    167 |
    168 | 169 |
    170 |

    Site settings

    171 |

    Title:

    172 | 173 |

    174 | The title of your site can be changed later. 175 |

    176 |

    177 | 181 |

    182 |

    183 | Add some sample content to get you started. 184 |

    185 |
    186 | 187 |
    188 |

    189 | 190 | 191 | 192 | 193 | 194 |

    195 |
    196 |
    197 |
    198 | -------------------------------------------------------------------------------- /plugins/akismet.php: -------------------------------------------------------------------------------- 1 | '', 33 | 'delete_spam' => true 34 | ); 35 | self::$conf = gb::data('plugins/'.gb_filenoext(basename(__FILE__)), $default_conf); 36 | if (!self::$key) 37 | self::$key = self::$conf['api_key']; 38 | if (!self::$key) { 39 | gb::log(LOG_WARNING, 'akismet not loaded since "api_key" is not set in %s', self::$conf->file); 40 | return false; 41 | } 42 | if ($context === 'admin') { 43 | gb_cfilter::add('pre-comment', array(__CLASS__,'check_comment')); 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | static function verify_key($key, $ip=null) { 50 | $blog = urlencode(gb::$site_url); 51 | $response = self::http_post("key=$key&blog=$blog", '/1.1/verify-key', null, null, $ip); 52 | if ( !is_array($response) || !isset($response[1]) || $response[1] != 'valid' && $response[1] != 'invalid' ) 53 | return 'failed'; 54 | return $response[1]; 55 | } 56 | 57 | // Check connectivity between the WordPress blog and Akismet's servers. 58 | // Returns an associative array of server IP addresses, where the key is the 59 | // IP address, and value is true (available) or false (unable to connect). 60 | static function check_server_connectivity() { 61 | $test_host = 'rest.akismet.com'; 62 | 63 | // Some web hosts may disable one or both functions 64 | if ( !is_callable('fsockopen') || !is_callable('gethostbynamel') ) 65 | return array(); 66 | 67 | $ips = gethostbynamel($test_host); 68 | if ( !$ips || !is_array($ips) || !count($ips) ) 69 | return array(); 70 | 71 | $servers = array(); 72 | foreach ( $ips as $ip ) { 73 | $response = self::verify_key(self::$key, $ip); 74 | // even if the key is invalid, at least we know we have connectivity 75 | if ( $response == 'valid' || $response == 'invalid' ) 76 | $servers[$ip] = true; 77 | else 78 | $servers[$ip] = false; 79 | } 80 | 81 | return $servers; 82 | } 83 | 84 | // Check the server connectivity and store the results in an option. 85 | // Cached results will be used if not older than the specified timeout in 86 | // seconds; use $cache_timeout = 0 to force an update. 87 | // Returns the same associative array as akismet_check_server_connectivity() 88 | static function get_server_connectivity( $cache_timeout = 86400 ) { 89 | $servers = self::$conf['available_servers']; 90 | if ( (time() - self::$conf['connectivity_time'] < $cache_timeout) && $servers ) 91 | return $servers; 92 | 93 | // There's a race condition here but the effect is harmless. 94 | $servers = self::check_server_connectivity(); 95 | self::$conf['available_servers'] = $servers; 96 | self::$conf['connectivity_time'] = time(); 97 | return $servers; 98 | } 99 | 100 | // Returns true if server connectivity was OK at the last check, false if there was a problem that needs to be fixed. 101 | static function server_connectivity_ok() { 102 | $servers = self::get_server_connectivity(); 103 | return !( empty($servers) || !count($servers) || count( array_filter($servers) ) < count($servers) ); 104 | } 105 | 106 | static function get_host($host) { 107 | // if all servers are accessible, just return the host name. 108 | // if not, return an IP that was known to be accessible at the last check. 109 | if ( self::server_connectivity_ok() ) { 110 | return $host; 111 | } 112 | else { 113 | $ips = self::get_server_connectivity(); 114 | // a firewall may be blocking access to some Akismet IPs 115 | if ( count($ips) > 0 && count(array_filter($ips)) < count($ips) ) { 116 | // use DNS to get current IPs, but exclude any known to be unreachable 117 | $dns = (array)gethostbynamel( rtrim($host, '.') . '.' ); 118 | $dns = array_filter($dns); 119 | foreach ( $dns as $ip ) { 120 | if ( array_key_exists( $ip, $ips ) && empty( $ips[$ip] ) ) 121 | unset($dns[$ip]); 122 | } 123 | // return a random IP from those available 124 | if ( count($dns) ) 125 | return $dns[ array_rand($dns) ]; 126 | 127 | } 128 | } 129 | // if all else fails try the host name 130 | return $host; 131 | } 132 | 133 | static function http_post($body, $path, $host=null, $port=null, $ip=null) { 134 | $host = $host === null ? self::$host : $host; 135 | $port = $port === null ? self::$port : $port; 136 | 137 | $http_request = "POST $path HTTP/1.0\r\n" 138 | . "Host: $host\r\n" 139 | . "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" 140 | . "Content-Length: " . strlen($body) . "\r\n" 141 | . 'User-Agent: Gitblog/'.gb::$version." , Akismet/2.0\r\n" 142 | . "\r\n" 143 | . $body; 144 | 145 | $http_host = $host; 146 | // use a specific IP if provided - needed by akismet_check_server_connectivity() 147 | if ( $ip && long2ip(ip2long($ip)) ) { 148 | $http_host = $ip; 149 | } else { 150 | $http_host = self::get_host($host); 151 | } 152 | 153 | $response = ''; 154 | if( false != ( $fs = @fsockopen($http_host, $port, $errno, $errstr, 10) ) ) { 155 | fwrite($fs, $http_request); 156 | 157 | while ( !feof($fs) ) 158 | $response .= fgets($fs, 1160); // One TCP-IP packet 159 | fclose($fs); 160 | $response = explode("\r\n\r\n", $response, 2); 161 | } 162 | return $response; 163 | } 164 | 165 | static function check_comment($comment) { 166 | # null? 167 | if (!$comment) 168 | return $comment; 169 | 170 | # already approved? 171 | if ($comment->approved) { 172 | gb::log(LOG_INFO, 'skipping check since comment is already approved'); 173 | return $comment; 174 | } 175 | 176 | $params = array( 177 | # required 178 | 'blog' => gb::$site_url, 179 | 'user_ip' => $comment->ipAddress, 180 | 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 181 | # optional 182 | 'referrer' => $_SERVER['HTTP_REFERER'], 183 | 'blog_charset' => 'utf-8', 184 | 'comment_type' => $comment->type === GBComment::TYPE_COMMENT ? 'comment' : 'pingback', # comment | trackback | pingback 185 | 'comment_author' => $comment->name, 186 | 'comment_author_email' => $comment->email, 187 | 'comment_content' => $comment->body(), 188 | #'blog_lang' => 'en', 189 | #'permalink' => $comment->url() 190 | ); 191 | 192 | if ($comment->uri) 193 | $params['comment_author_url'] = $comment->uri; 194 | 195 | # add HTTP_* server vars (request headers) 196 | static $ignore = array('HTTP_COOKIE'); 197 | foreach ($_SERVER as $key => $value) 198 | if (strpos($key, 'HTTP_') === 0 && !in_array($key, $ignore) && is_string($value)) 199 | $params[$key] = $value; 200 | 201 | # POST 202 | gb::log('checking comment'); 203 | $reqbody = http_build_query($params); 204 | $response = self::http_post($reqbody, '/1.1/comment-check', self::$key.'.'.self::$host); 205 | 206 | # parse response 207 | if ($response[1] === 'true') { 208 | gb::log('comment classed as spam'); 209 | self::$conf['spam_count'] = intval(self::$conf['spam_count']) + 1; 210 | $comment->spam = true; 211 | gb::event('did-spam-comment', $comment); 212 | if (self::$conf['delete_spam']) 213 | $comment = null; 214 | } 215 | elseif ($response[1] === 'false') { 216 | gb::log('comment classed as ham'); 217 | $comment->spam = false; 218 | gb::event('did-ham-comment', $comment); 219 | } 220 | else { 221 | gb::log(LOG_WARNING, 'unexpected response from /1.1/comment-check: '.$response[1]); 222 | } 223 | 224 | # forward 225 | return $comment; 226 | } 227 | } 228 | ?> -------------------------------------------------------------------------------- /helpers/post-comment.php: -------------------------------------------------------------------------------- 1 | filterspec , .. ) 39 | */ 40 | $fields = array( 41 | # ------------------------------------------------------------------- 42 | # required fields 43 | 44 | # Stage name of the object on which to add the comment. 45 | 'reply-post' => FILTER_REQUIRE_SCALAR, 46 | 47 | # The actual comment 48 | 'reply-message' => FILTER_REQUIRE_SCALAR, 49 | 50 | # Authors email address 51 | 'author-email' => FILTER_VALIDATE_EMAIL, 52 | 53 | # Authors name 54 | 'author-name' => FILTER_REQUIRE_SCALAR, 55 | 56 | # ------------------------------------------------------------------- 57 | # optional fields 58 | 59 | # In reply to a supercomment with comment id 60 | 'reply-to' => FILTER_REQUIRE_SCALAR, 61 | 62 | # Authors URL 63 | 'author-url' => FILTER_SANITIZE_URL, 64 | 65 | # Authors URI (shadowed by "author-url" unless author-url === false) 66 | 'author-uri' => FILTER_REQUIRE_SCALAR, 67 | 68 | # client timezone offset in seconds (east of UTC is positive, west of UTC is 69 | # negative). Could be derived from javascript Date object like this: 70 | # -((new Date()).getTimezoneOffset()*60); 71 | 'client-timezone-offset' => array( 72 | 'filter' => FILTER_VALIDATE_INT, 73 | 'options' => array('min_range' => -43200, 'max_range' => 43200) 74 | ), 75 | 76 | # ------------------------------------------------------------------- 77 | # implicit fields 78 | 79 | # Nonce 80 | 'gb-nonce' => array( 81 | 'filter' => FILTER_SANITIZE_STRING, 82 | 'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH 83 | ) 84 | ); 85 | 86 | function exit2($msg, $status='400 Bad Request') { 87 | header('HTTP/1.1 '.$status); 88 | exit($status."\n".$msg."\n"); 89 | } 90 | 91 | # only allow POST 92 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') 93 | exit2('Only POST is allowed', '405 Method Not Allowed'); 94 | 95 | # sanitize and validate input 96 | static $required_fields = array('reply-post', 'reply-message', 'author-email', 'author-name'); 97 | $input = filter_input_array(INPUT_POST, $fields); 98 | 99 | # assure required fields are OK 100 | $fields_missing = array(); 101 | foreach ($required_fields as $field) { 102 | if (!$input[$field]) 103 | $fields_missing[] = $field; 104 | } 105 | if ($fields_missing) 106 | exit2('missing parameter(s): '.implode(', ', $fields_missing)); 107 | elseif (strlen($input['reply-message']) < 2) 108 | exit2('you have to say more than a single character my friend.'); 109 | 110 | # sanitize $input['reply-post'] 111 | $input['reply-post'] = trim(str_replace('..', '', $input['reply-post']), '/'); 112 | if (strpos($input['reply-post'], 'content/') !== 0) 113 | exit2('malformed parameter "reply-post"'); 114 | 115 | # look up post/page 116 | $post = GBExposedContent::findByCacheName($input['reply-post'].gb::$content_cache_fnext); 117 | 118 | # verify existing content and that comments are enabled 119 | if (!$post) exit2('no such reply-post '.$input['reply-post']); 120 | if (!$post->commentsOpen) exit2('commenting not allowed', '403 Forbidden'); 121 | 122 | # verify nonce 123 | if ($input['gb-nonce'] && gb_nonce_verify($input['gb-nonce'], 'post-comment-'.$input['reply-post']) === false) 124 | exit2('nonce verification failure'); 125 | 126 | # adjust date with clients local timezone 127 | $date = new GBDateTime(null, 0); 128 | if ( $input['client-timezone-offset'] !== false 129 | && (($tzoffset = intval($input['client-timezone-offset'])) !== false) 130 | && ($tzoffset < 43200 || $tzoffset > -43200) ) 131 | { 132 | $date->offset = $tzoffset; 133 | } 134 | 135 | # author-url -> author-uri if set 136 | if ($input['author-url'] !== false) 137 | $input['author-uri'] = gb_cfilter::apply('sanitize-url', $input['author-url']); 138 | 139 | # if we are logged in, use the canonical email 140 | if (gb::$authorized) 141 | $input['author-email'] = gb::$authorized->email; 142 | 143 | # set author cookie 144 | gb_author_cookie::set($input['author-email'], $input['author-name'], $input['author-uri']); 145 | 146 | # create comment object 147 | $comment = new GBComment(array( 148 | 'date' => $date->__toString(), 149 | 'ipAddress' => $_SERVER['REMOTE_ADDR'], 150 | 'email' => $input['author-email'], 151 | 'uri' => $input['author-uri'], 152 | 'name' => $input['author-name'], 153 | 'body' => $input['reply-message'], 154 | 'approved' => false, 155 | 156 | # not stored, but used until request has finished 157 | 'post' => $post 158 | )); 159 | 160 | # always approve admin comments 161 | if (gb::$authorized) 162 | $comment->approved = true; 163 | 164 | # apply filters 165 | $comment = gb_cfilter::apply('pre-comment', $comment); 166 | 167 | # aquire referrer 168 | $referrer = gb::referrer_url(); 169 | 170 | # append to comment db 171 | if ($comment) { 172 | try { 173 | $cdb = $post->getCommentsDB(); 174 | $added = $cdb->append($comment, $input['reply-to'] ? $input['reply-to'] : null); 175 | 176 | # duplicate? 177 | if ($added === false) { 178 | gb::log('skipped duplicate comment from '.var_export($comment->email,1)); 179 | gb::event('was-duplicate-comment', $comment); 180 | 181 | if ($referrer) { 182 | $referrer->fragment = 'comments'; 183 | $referrer['comment-status'] = 'duplicate'; 184 | header('HTTP/1.1 304 Not Modified'); 185 | header('Location: '.$referrer); 186 | exit(0); 187 | } 188 | else { 189 | exit2("duplicate comment\n", '200 OK'); 190 | } 191 | } 192 | 193 | gb::log('added comment from %s to %s', 194 | var_export($comment->email,1), gb_filenoext($post->cachename())); 195 | 196 | gb::event('did-add-comment', $comment); 197 | 198 | # done 199 | if ($referrer) { 200 | $referrer->fragment = 'comment-'.$comment->id; 201 | if (!$comment->approved) { 202 | $referrer->fragment = 'comments'; 203 | $referrer['comment-status'] = 'pending'; 204 | } 205 | else { 206 | unset($referrer['comment-status']); 207 | } 208 | header('HTTP/1.1 303 See Other'); 209 | header('Location: '.$referrer); 210 | exit(0); 211 | } 212 | else { 213 | exit2("new comment: {$comment->id}\n", '200 OK'); 214 | } 215 | } 216 | catch (Exception $e) { 217 | if ($e instanceof GitError && strpos($e->getMessage(), 'nothing to commit') !== false) { 218 | gb::log('skipped duplicate comment from ' 219 | .var_export($comment->email,1).' (nothing to commit)'); 220 | gb::event('was-duplicate-comment', $comment); 221 | header('HTTP/1.1 304 Not Modified'); 222 | header('Location: '.$input['gb-referrer'].'#skipped-duplicate-reply'); 223 | exit(0); 224 | } 225 | 226 | gb::log(LOG_ERR, 'failed to add comment '.var_export($comment->body,1) 227 | .' from '.var_export($comment->name,1).' <'.var_export($comment->email,1).'>' 228 | .' to '.$post->cachename()); 229 | 230 | header('HTTP/1.1 500 Internal Server Error'); 231 | echo '$input => ';var_export($input);echo "\n"; 232 | gb_flush(); 233 | throw $e; 234 | } 235 | } 236 | else { 237 | # rejected by filter(s) 238 | if ($referrer) { 239 | $referrer->fragment = 'comments'; 240 | $referrer['comment-status'] = 'rejected'; 241 | header('HTTP/1.1 303 See Other'); 242 | header('Location: '.$referrer); 243 | } 244 | else { 245 | exit2("rejected\n", '200 OK'); 246 | } 247 | } 248 | 249 | ?> -------------------------------------------------------------------------------- /lib/GBCommentDB.php: -------------------------------------------------------------------------------- 1 | post = $post; 13 | } 14 | 15 | function parseData() { 16 | parent::parseData(); 17 | foreach ($this->data as $k => $v) { 18 | $c = new GBComment($v); 19 | $c->post = $this->post; 20 | $this->data[$k] = $c; 21 | } 22 | } 23 | 24 | function encodeData() { 25 | $c = new GBComment(); 26 | $c->comments = $this->data; 27 | $it = new GBCommentsIterator($c); 28 | foreach ($it as $k => $comment) { 29 | if (is_object($comment->date)) 30 | $comment->date = strval($comment->date); 31 | } 32 | parent::encodeData(); 33 | } 34 | 35 | function commit() { 36 | $this->autocommitToRepoAuthor = $this->lastComment ? $this->lastComment->gitAuthor() : null; 37 | $r = parent::commit(); 38 | $this->lastComment = false; 39 | return $r; 40 | } 41 | 42 | protected function txWriteData() { 43 | $r = parent::txWriteData(); 44 | if ($r === false && $this->data != $this->originalData && $this->readOnly) 45 | throw new RuntimeException($this->file.' is not writable'); 46 | return $r; 47 | } 48 | 49 | function rollback($strict=true) { 50 | parent::rollback($strict); 51 | $this->lastComment = false; 52 | } 53 | 54 | function resolveIndexPath($indexpath, $comment, $skipIfSameAsComment=null) { 55 | foreach ($indexpath as $i) { 56 | if (!isset($comment->comments[$i])) 57 | return null; 58 | $comment = $comment->comments[$i]; 59 | if ($skipIfSameAsComment !== null && $skipIfSameAsComment->duplicate($comment)) 60 | return null; 61 | } 62 | return $comment; 63 | } 64 | 65 | function get($index=null) { 66 | if (is_string($index)) { 67 | $temptx = $this->txFp === false && $this->autocommit; 68 | if ($temptx) 69 | $this->begin(); 70 | if ($this->data === null) 71 | $this->txReadData(); 72 | $v = new GBComment(); 73 | $v->post = $this->post; 74 | $v->comments =& $this->data; 75 | $v = $this->resolveIndexPath(explode('.', $index), $v); 76 | if ($temptx) 77 | $this->txEnd(); 78 | return $v; 79 | } 80 | else { 81 | return parent::get($index); 82 | } 83 | } 84 | 85 | /** 86 | * Set or remove a comment. 87 | * 88 | * By not passing the second argument the action will be to remove a comment 89 | * rather than setting it to null. You can also pass an array as the first 90 | * and only argument in which case the whole underlying data set is replaced. 91 | * You have been warned. 92 | * 93 | * Return values: 94 | * 95 | * - After a set operation, true is returned on success, otherwise false. 96 | * 97 | * - After a remove operation, the removed comment is returned (if found and 98 | * removed), otherwise false. 99 | */ 100 | function set($index, GBComment $comment=null) { 101 | $return_value = true; 102 | if ($comment !== null && $comment->post === null) 103 | $comment->post = $this->post; 104 | $this->lastComment = $comment; 105 | if (is_string($index)) { 106 | $indexpath = explode('.', $index); 107 | if (!$indexpath) 108 | return false; 109 | if (count($indexpath) === 1) 110 | return parent::set($indexpath[0], $comment); 111 | $superCommentIndex = intval(array_shift($indexpath)); 112 | $superComment = $this->get($superCommentIndex); 113 | $subCommentIndex = array_pop($indexpath); 114 | $parentComment = $indexpath ? 115 | $this->resolveIndexPath($indexpath, $superComment) : $superComment; 116 | if ($comment === null) { 117 | # delete 118 | if (($return_value = isset($parentComment->comments[$subCommentIndex]))) { 119 | $return_value = $parentComment->comments[$subCommentIndex]; 120 | unset($parentComment->comments[$subCommentIndex]); 121 | } 122 | } 123 | else { 124 | # set 125 | $parentComment->comments[$subCommentIndex] = $comment; 126 | $return_value = true; 127 | } 128 | parent::set($superCommentIndex, $superComment); 129 | } 130 | else { 131 | return parent::set($index, $comment); 132 | } 133 | return $return_value; 134 | } 135 | 136 | function append(GBComment $comment, $index=null, $skipDuplicate=true) { 137 | if ($comment->post === null) 138 | $comment->post = $this->post; 139 | $temptx = $this->txFp === false && $this->autocommit; 140 | # begin if in temporary tx 141 | if ($temptx) 142 | $this->begin(); 143 | try { 144 | # assure data is loaded 145 | if ($this->data === null) 146 | $this->txReadData(); 147 | # add 148 | $newindex = false; 149 | $this->lastComment = $comment; 150 | if ($index !== null) { 151 | if (!$this->data) 152 | throw new OutOfBoundsException('invalid comment index '.$index); 153 | 154 | $indexpath = explode('.', $index); 155 | if (!$indexpath) 156 | throw new InvalidArgumentException('$index is empty'); 157 | if (count($indexpath) === 1) { 158 | if (!isset($this->data[$indexpath[0]])) 159 | throw new OutOfBoundsException('invalid comment index '.$index); 160 | $parentc = $this->data[$indexpath[0]]; 161 | } 162 | else { 163 | $rootc = new GBComment(); 164 | $rootc->comments =& $this->data; 165 | $parentc = $this->resolveIndexPath($indexpath, $rootc, $comment); 166 | } 167 | 168 | if (!$parentc) 169 | throw new OutOfBoundsException('invalid comment index '.$index); 170 | 171 | if ( ($skipDuplicate && !$parentc->duplicate($comment)) || !$skipDuplicate ) 172 | $newindex = $index.'.'.$parentc->append($comment); 173 | } 174 | else { 175 | if (!$this->data) { 176 | $newindex = 1; 177 | $this->data = array(1 => $comment); 178 | } 179 | else { 180 | $newindex = array_pop(array_keys($this->data))+1; 181 | $skip = false; 182 | if ($skipDuplicate) { 183 | # look at previous comments and see if we have a dup 184 | $parentc = new GBComment(array('comments' => $this->data)); 185 | $it = new GBCommentsIterator($parentc); 186 | foreach ($it as $c) { 187 | if ($c->duplicate($comment)) { 188 | $skip = true; 189 | break; 190 | } 191 | } 192 | } 193 | if ($skip) 194 | $newindex = false; 195 | else 196 | $this->data[$newindex] = $comment; 197 | } 198 | } 199 | } 200 | catch (Exception $e) { 201 | if ($temptx) 202 | $this->rollback(); 203 | throw $e; 204 | } 205 | # commit if in temporary tx 206 | if ($temptx) 207 | $this->commit(); 208 | 209 | # set comment->id and return it, unless false 210 | if ($newindex !== false) { 211 | $comment->id = strval($newindex); 212 | return $newindex; 213 | } 214 | return $newindex; 215 | } 216 | 217 | function remove($index) { 218 | return $this->set($index); 219 | } 220 | } 221 | 222 | /* 223 | # tests 224 | $c = new GBComment(array('name'=>'John Doe', 'email'=>'john@doe.com')); 225 | $cdb = new GBCommentDB('/Users/rasmus/Desktop/comments.json'); 226 | 227 | # Dump & clear 228 | var_export($cdb->get()); 229 | $cdb->set(array()); 230 | 231 | # Append super-comment 232 | $cdb->append($c); 233 | $cdb->append($c); 234 | var_export($cdb->get()); 235 | 236 | # Remove super-comment 237 | $cdb->remove(2); 238 | var_export($cdb->get()); 239 | 240 | # Append sub-comment 241 | $c = $cdb->get(1); 242 | for ($i=0;$i<3;$i++) { 243 | $c2 = new GBComment(array('name'=>'Mos Master '.$i, 'email'=>'moset'.$i.'@gmail.com')); 244 | $c->append($c2); 245 | $c3 = new GBComment(array('name'=>'Yxi Kaksi '.$i, 'email'=>'yxan'.$i.'@hotmail.com')); 246 | $c2->append($c3); 247 | } 248 | $cdb->set(1, $c); 249 | var_export($cdb->get()); 250 | 251 | # Work with sub comments and index paths 252 | $c = $cdb->get('1.3'); 253 | var_export($c); 254 | $c2 = new GBComment(array('name'=>'Rolf Von Bulgur', 'email'=>'roffe@intertubes.com')); 255 | $cdb->set('1.3', $c2); 256 | var_export($cdb->get('1.3')); 257 | $cdb->set('1.3', $c); 258 | var_export($cdb->get('1.3')); 259 | 260 | # depth 3 261 | $c = $cdb->get('1.2.1'); 262 | var_export($c); 263 | $c2 = new GBComment(array('name'=>'Henry Lols', 'email'=>'lol@fluff.gr')); 264 | $cdb->set('1.2.1', $c2); 265 | var_export($cdb->get('1.2.1')); 266 | $cdb->set('1.2.1', $c); 267 | var_export($cdb->get('1.2.1')); 268 | */ 269 | ?> -------------------------------------------------------------------------------- /docs/content.md: -------------------------------------------------------------------------------- 1 | # Content 2 | 3 | Posts and pages are classed as *content* and internally represented by the class `GBExposedContent`. 4 | 5 | A content object is compriced of a HTTP-like header and an optional body. Here's a quick example: 6 | 7 | title: Example of a simple post 8 | custom-field: Values can span over 9 | several rows. 10 | 11 | Hello world 12 | 13 | The header is terminated by two linebreaks (`LF LF` or `CR LF CR LF`). 14 | 15 | ## Header fields 16 | 17 | Field names are case-insensitive and values can wrap over several rows (if subsequent rows are indented with at least one space or tab). The field name and value are separated by a colon (`":"`). 18 | 19 | ### Standard header fields 20 | 21 | All of these are optional and replaced by default values if not specified. 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | 139 | 140 | 141 | 142 | 143 | 144 | 154 | 155 | 156 | 157 | 158 | 159 | 162 | 163 | 164 | 165 | 166 | 167 | 170 | 171 |
    NameAliasValueNotes
    TitleTitle of object.If not specified, the title will be deduced from the filename. 32 | Sets the title property.
    AuthorName and/or email of author. 39 | Expects a git-style author format. (e.g. "Name Name <e@ma.il>", 40 | "Name", "e@ma.il", etc). If the author is not given by a 41 | header field, the author will be deduced by finding the initial commit for the 42 | object in question. Sets the author property (an anonymous object with 43 | properties name and email). 44 |
    CategoryCategoriesOne or more categories separated by comma. 51 | Category names are case-insensitive and whitespace is trimmed (not stripped). 52 | If not specified, the object is not filed under any category. 53 | Sets the categories property (an ordered list). 54 |
    TagsTagOne or more tags separated by comma. 61 | Tag names are case-insensitive and whitespace is trimmed (not stripped). 62 | If not specified, the object is not tagged with any tags. 63 | Sets the tags property (an ordered list). 64 |
    PublishPublishedBoolean[1] is published OR date and/or time when the object was or will be published. 71 | Defaults to the date parsed from the object file system path combined with 72 | (date and) time of the initial commit. If a date and/or time is specified without 73 | timezone information, UTC is assumed. Examples: May 4, 2009 14:30 CEST, 74 | 12:47, 2009-05-04 19:03:41 +0400, 12:47 -0700. The date is 75 | parsed using a technique similar to the PHP function `strtotime` thus allowing for a 76 | wide array of different formats and resolutions.

    77 | 78 | Unless a complete (resoluton of a second) date and time is specified, the aforementioned 79 | merge algorithm is used. The logic is a s follows: 80 | 81 |
    <date and/or time parsed from file system path>
     82 |   [ <-- <missing date resolution, time and zone from initial commit> ]
     83 |   [ <-- <date and/or time and/or zone publish header field> ]
    84 | 85 | The later in the list the higher the priority. Commit date does not override file system 86 | path date, but completes it. If the date parsed from the filename expresses year and 87 | month; day, hour, minute, second and zone are added from date of initial commit. 88 | However, the "publish" header field overrides any parts defined.

    89 | 90 | Sets the published property (an instance of GBDateTime). 91 |
    DraftBoolean[1] is draft (not published).Defaults to false if not specified. Sets the draft property.
    CommentsBoolean[1] allow comments.Defaults to true if not specified. Sets the commentsOpen property.
    PingbackBoolean[1] send and receive pingbacks.Defaults to true if not specified. Sets the pingbackOpen property.
    HiddenHide, invisibleBoolean[1] true if the object should not appear in menus.Defaults to false (is visible) if not specified. Only applies to pages. 116 | Sets the hidden property on GBPages.
    OrderSort, priorityInteger value explicitly setting the priority of menu order for a page object.Defaults to undefined if not specified. Menu items are sorted in two phases 123 | -- first on order header field value, then on name/title. 124 | Sets the order property on GBPages.
    Content-typeMIME type or filename extension with optional ;charset=S suffix. 131 | By default, the content type is deduced by trying to map the actual filename extension 132 | to a mime type (done by GBMimeType). If a filename extension (a string not 133 | containing a / character) is specified, 134 | the MIME type will be aquired by looking up that filename extension. 135 | Sets the mimeType property and affects the body 136 | property if charset meta is defined 137 | (see description of Charset field for more info). 138 |
    CharsetEncodingTitle and body character set (text encoding). 145 | Defaults to UTF-8. If specified this overrides any other character set definition 146 | (i.e. any charset set by Content-type or similar). 147 | The internal representation is UTF-8, thus specifying anything else than 148 | UTF-8 or ASCII will 149 | cause a conversion which requires either the 150 | Multibyte String 151 | or the iconv extension. 152 | Does not set any property, but affects the title and body properties. 153 |
    Auto-linebreaksBoolean[1] enable automatic creation of single linebreaks. 160 | True by default. For HTML content, single linebreaks are converted to <br/>. Setting this to false disables automatic <br/>. 161 |
    Auto-paragraphsBoolean[1] enable automatic creation of text paragraphs. 168 | True by default. For HTML content, two or more linebreaks are converted into <p>text before linebreaks</p>. Setting this to false disables implicit creation of paragraphs. 169 |
    172 | 173 | > **[1] Boolean values:** True values are `"true"`, `"yes"`, `"on"`, `"1"` or `""` (empty). 174 | > Anything else is considered a false value. Values are case-insensitive. 175 | 176 | 177 | ### Ancillary header fields 178 | 179 | Ancillary header fields (not defined in "Standard header fields") will be passed through and made 180 | available in the `meta` property. 181 | 182 | Example of presenting ancillary meta fields in a template: 183 | 184 |

    Meta fields

    185 |
      186 | meta as $name => $value): ?> 187 |
    • :
    • 188 | 189 |
    190 | 191 | Plugins can use non-reserved header fields for special purposes. In such case a rebuild 192 | plugin is recommended to remove it's "special" fields from the `meta` map: 193 | 194 | class example_plugin { 195 | static function init($context) { 196 | gb::observe('did-reload-object', __CLASS__.'::did_reload_object'); 197 | } 198 | 199 | static function did_reload_object($post) { 200 | if (isset($post->meta['my-special-field'])) { 201 | $special_field = $post->meta['my-special-field']; 202 | unset($post->meta['my-special-field']); 203 | # do something with $special_field ... 204 | } 205 | } 206 | } 207 | 208 | Plugins can of course also *set* meta fields which later can be read by templates. However, the `meta` map is an *opaque set of metadata* in the eyes of gitblog core, thus no standard templates make use of it at the moment. 209 | --------------------------------------------------------------------------------