This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ $blof_version = '0.1.0'; /* TODO - blof_set(opt, val): name: blog name (for RSS) var_prefix: prefix CGI vars - enhanced text (links, images, lists) - enhanced text: online quickref for author - thread mod+versionning - comments delete(by author) - locking: have a big 'write lock' for mod/del (steal lock_ex code from misc/lock.php) - RSS feed - translations (minimal) */ /* * Auth/prefs */ function blof_login() { $_SESSION['blof_auth'] = 'is_author'; } function blof_logout() { $_SESSION['blof_auth'] = ''; } function blof_is_author() { return ($_SESSION['blof_auth'] == 'is_author'); } function blof_print_login_form($info = '') { global $blof_opt; if (!session_id()) { blof_print_error( "session not started", "You must call ".blof_phpman("session_start()")." in order authentification works."); } else if (!isset($blof_opt['passwd'])) { blof_print_error( "no password has been set", "You must call ".blof_phpman("blof_set('passwd', 'your_password')").".

"); } else { $cookie = session_get_cookie_params(); if ($cookie['lifetime']) { $date = strftime('%c', time() + $cookie['lifetime']); $hint = "You will be automatically logged in until $date."; } else $hint = "You will be automatically logged out when you close your browser."; $hint .= " Use ".blof_phpman("session_set_cookie_params()")." to change this."; $body = <<

Password:

$info

$hint

EOF; blof_print_block('thread', 'Author login', '', $body); } blof_print_footer(blof_home()); } function blof_handle_login_form() { global $blof_opt; if (!isset($blof_opt['passwd'])) return 0; if ($_REQUEST['passwd'] != $blof_opt['passwd']) { blof_print_login_form("

Error: wrong password.

\n"); return 0; } blof_login(); return 1; } /* * Email notification */ function blof_notify($id, $meta, $text) { global $blof_opt; $email = $blof_opt['email']; if ($email == '') return; $title = $meta['Title']; $from = $meta['From']; if ($title == '') { $id = dirname($id); if (!blof_read_block($id, $parent_meta, $parent_text)) return; $title = "Re: ".$parent_meta['Title']; } $url = "http://".$_SERVER['HTTP_HOST'].preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI'])."?tid=$id"; $message = <<'$store' cannot be written to by your web server."); return FALSE; } return TRUE; } /* Only mkdir() is known to be atomic and race-free over networked fs. */ function blof_store_alloc_id($base) { global $blof_opt; $store = $blof_opt['store']; for ($i = 1; $i <= 999; $i++) { // This is a safety max, can be pushed up $id = "$base-$i"; $path = "$store/$id"; if (@mkdir($path)) return $id; } return ''; } function blof_write_block($id, $mode, $meta, $text) { global $blof_opt; $file = fopen($blof_opt['store']."/$id/content", $mode); if (!$file) return 0; $head = ''; foreach($meta as $key => $val) { /* It is important to strip off EOL values to make header parsing * work in blof_read_block(see fgets() doc, depends on platform and * auto_detect_line_endings setting). */ $head .= "$key: ".str_replace(array("\r", "\n"), " ", $val)."\n"; } $head .= "\n"; $ok = fwrite($file, $head) && fwrite($file, $text); fclose($file); return $ok; } function blof_read_block($id, &$meta, &$text) { global $blof_opt; $meta = array(); $text = ''; $file = fopen($blof_opt['store']."/$id/content", 'r'); if (!$file) return 0; $state = 0; while (!feof($file)) { $line = fgets($file); if ($state == 0) { $line = rtrim($line); if ($line == '') { $state = 1; continue; } // We're doing it the old way to spare a preg_split() $colon = strpos($line, ':'); $key = substr($line, 0, $colon); $val = substr($line, $colon+2); $meta[$key] = $val; } else if ($state == 1) { $text .= $line; } } fclose($file); return 1; } /* * Display */ function blof_phpman($func) { $ref = preg_replace('/\(.*$/', '', $func); $ref = str_replace('_', '-', $ref); if ($ref == '' || preg_match('/^blof-/', $ref)) return "$func"; return "$func"; } function blof_print_error($error, $explain) { echo <<

Blof error: $error.

$explain

EOF; } function blof_print_store_error($text) { $func = blof_phpman("blof_set('store', 'your/folder/here')"); $explain = <<\n"; if ($title) echo "
$title
\n"; if ($header) echo "
$header
\n"; if ($body) echo "
\n$body\n
\n"; if ($actions) { $actionbar = $actions ? implode(" |\n ", $actions) : ""; echo "
\n$actionbar\n
\n"; } echo "\n\n"; } function blof_print_footer($more) { global $blof_opt; $footer = $blof_opt['footer']; $brag = $blof_opt['brag']; $validate = $blof_opt['validate']; $actions = array(); if ($footer != '') $actions[] = $footer; if ($brag) $actions[] = "Powered by Blof"; if ($validate) { $actions[] = "Valid XHTML"; $actions[] = "Valid CSS"; } if (blof_is_author()) { $actions[] = "Post a new thread"; $actions[] = "Logout"; } else $actions[] = "Login"; $actions[] = "RSS"; if (is_array($more)) $actions = array_merge ($actions, $more); else if ($more != '') $actions[] = $more; echo "
\n "; echo implode(" |\n ", $actions); echo "\n
\n"; } function blof_home() { return 'Home'; } /* * Thread display UI */ /* Interpret text enhancements in a Wiki-style syntax. * The order of the different transformations is important! */ function blof_enhance_text($text) { $fixed = htmlspecialchars($text); $text = ''; foreach(explode("\n", $fixed) as $line) { $line = rtrim($line); if ($line == '') continue; // Font styles $markup = array('*' => 'b', '_' => 'u', '/' => 'i'); $line = preg_replace('/(\*|_|\/)([^\*_\/]+)\1/e', '"\2"', $line); // Make paragraphs $text .= ($text == "" ? "" : "\n")."

$line

"; } return $text; } /* Caps $text to $charcount chars, rounding to the nearest word boundary. * If capping really occurs, append the '[...]' string. * Must be applied on raw text, otherwise can break HTML tags. */ function blof_cap_text($text, $charcount) { $len = strlen($text); if ($len <= $charcount) return $text; $text = substr($text, 0, $charcount); $text = preg_replace('/\s+[^\s]*$/', '', $text); // kill any leftover piece of word if (strlen($text) < $len) $text .= ' [...]'; return $text; } function blof_format_header($from, $date, $extra) { return "Posted by $from on $date$extra"; } /* Print a formatted thread (title, header, body, actions - no comments). */ function blof_print_thread($thread_id, $folded, $actions) { global $blof_opt; if (!blof_read_block($thread_id, $meta, $text)) return; // Fix meta data for HTML output $title = htmlspecialchars($meta['Title']); $from = htmlspecialchars($meta['From']); $date = htmlspecialchars($meta['Date']); // Compose header $delay = isset($meta['Delay']) ? " (typed in ".$meta['Delay']." seconds)" : ""; $header = blof_format_header($from, $date, $delay); // Compose body and acton (ie. navigation) if ($folded) $text = blof_cap_text($text, $blof_opt['cap_fold']); $body = blof_enhance_text($text); // Compose actions if ($folded) array_unshift($actions, "Read more ..."); else array_unshift($actions, blof_home()); blof_print_block('thread', $title, $header, $body, $actions); } /* Print a formatted comment */ function blof_print_comments($tid) { $count = 0; foreach(blof_get_comments($tid) as $cid) { if (!blof_read_block("$tid/$cid", $meta, $data)) continue; // Fix meta data for HTML output $from = htmlspecialchars($meta['From']); $date = htmlspecialchars($meta['Date']); $header = blof_format_header($from, $date, ''); $header = "$header"; $body = blof_enhance_text($data); blof_print_block('comment'.($count & 1 ? 'alt' : ''), '', $header, $body); $count++; } } /* This is the default view. * We must handle navigation (newer, older, offset). Threads are displayed folded. * Algorithm designed to do a single pass over the $index array. */ function blof_print_threads() { global $blof_opt; $tid = $_REQUEST['tid']; if (blof_is_thread_id($tid)) { // Single thread (full) view $fold = 0; $pager = 1; $offset = $tid; } else { // Multiple thread (folded) view $fold = 1; $pager = $blof_opt['per_page']; $offset = $_REQUEST['to']; $offset = blof_is_thread_id($offset) ? $offset : ''; } $index = blof_get_index(); $todo = $pager; $order = 0; $more = 0; foreach($index as $id) { if ($todo <= 0) { $more = 1; // All threads displayed, and there are more break; } if ($offset != '' && $offset != $id) { $order++; continue; // First displayed thread not found yet } $offset = ''; // Found, turn off thread search // Retrieve comments index (cheap lookup) for display count $thread_nb = count(blof_get_comments($id)); $thread_nb = $thread_nb ? $thread_nb : "none"; $actions = array("Post a comment ($thread_nb)"); blof_print_thread($id, $fold, $actions); $todo--; } if (!$folded) blof_print_comments($tid); $actions = array(); if ($order) { $order = max(0, $order - $pager); $actions[] = "<<"; $actions[] = "< See newer threads"; } if ($more) { $max = count($index); $end = $index[max(0, $max - $pager)]; $actions[] = "See older threads >"; $actions[] = ">>"; } blof_print_footer($actions); } /* * Thread posting UI and handler */ function blof_print_thread_form() { global $blof_opt; blof_store_is_writable(); // Better warn the user early $now = time(); $date = strftime('%c', $now); $from = $blof_opt['author']; if (!isset($from)) $hint = " (Hint: call blof_set('author', 'your_name') to prefill this field)"; $body = <<
Title:
From: $hint
Date: around $date
EOF; blof_print_block("thread", "Post a new thread", "", $body); blof_print_footer(blof_home()); } function blof_handle_thread_form() { if (!blof_store_is_writable()) return; // Retrieve input, check if we have enough data $title = $_REQUEST['title']; $from = $_REQUEST['from']; $text = $_REQUEST['text']; $ctime = $_REQUEST['ctime']; if (!isset($title) || !isset($text)) return 0; // Allocate new thread_id $now = time(); $base = strftime('%Y-%m-%d', $now); $thread_id = blof_store_alloc_id($base); if ($thread_id == '') return 0; // Parse/fix input if (get_magic_quotes_gpc()) { // What a stupid hack, fix the APIs first! $title = stripslashes($title); $from = stripslashes($from); $text = stripslashes($text); } $ctime = is_numeric($ctime) ? $ctime : 0; // Fill in thread meta values $meta = array(); $meta['Title'] = $title; $meta['From'] = $from; $meta['Date'] = strftime('%Y-%m-%d %H:%M %Z', $now); $meta['Delay'] = $ctime ? ($now - $ctime) : 0; $meta['IP-Address'] = $_SERVER['REMOTE_ADDR']; if (!blof_write_block($thread_id, "w", $meta, $text)) return 0; blof_add_index($thread_id); blof_notify($thread_id, $meta, $text); return 1; } /* * Comment posting UI and handler */ function blof_print_comment_form() { global $blof_opt; $tid = $_REQUEST['tid']; if (!blof_is_thread_id($tid)) return 0; blof_store_is_writable(); // Better warn the user early blof_print_thread($tid, TRUE, array()); $from = htmlspecialchars($_SESSION['blof_from']); $focus = ($from == '') ? 'from' : 'text'; $body = <<
From:
EOF; blof_print_block('thread', 'Post a comment', '', $body); blof_print_footer(blof_home()); return 1; } function blof_handle_comment_form() { if (!blof_store_is_writable()) return 0; // Retrieve input, check if we have enough data $tid = $_REQUEST['tid']; $from = $_REQUEST['from']; $text = $_REQUEST['text']; if (!isset($tid) || !isset($text) || !blof_is_thread_id($tid)) return 0; // Allocate new thread_id $now = time(); $base = "$tid/comment"; $comment_id = blof_store_alloc_id($base); if ($comment_id == '') return 0; // Parse/fix input if (get_magic_quotes_gpc()) { $from = stripslashes($from); $text = stripslashes($text); } if ($from == '') $from = 'Anonymous'; else $_SESSION['blof_from'] = $from; // Fill in comment meta values $meta = array(); $meta['From'] = $from; $meta['Date'] = strftime('%Y-%m-%d %H:%M %Z', $now); $meta['IP-Address'] = $_SERVER['REMOTE_ADDR']; $meta['Referer'] = $tid; if (!blof_write_block($comment_id, "w", $meta, $text)) return 0; blof_notify($comment_id, $meta, $text); return 1; } /* * Embedded info and doc */ function blof_handle_info($action, $embedded) { global $blof_version; $about = <<Blof is a very simple blog implementation, for the real thing see DotClear or Geeklog.

Features:

  • Single PHP file - trivial install and upgrade
  • No database - stores all data in flat files
  • Simple setup - follow the embedded tutorial
  • Conforms to XHTML 1.0 strict and CSS 2.1
  • GPL license

Limitations (by design):

  • Single author - no login, only a password
  • Flat threading - no nested comments

You can browse the documentation, browse the source code or download it.

EOF; $xhtml = << EOF; $example = << $xhtml My blog EOF; $example = highlight_string($example, TRUE); $css = <<Here is a simple and complete example, let's call it demo.php:

$example

Explanations for every item:

  • First, you need to link to the Blof code whith require. In this example, blof.php is in the same folder as demo.php.
  • Then you must set a password, otherwise you won't be able to login and post new threads.
  • Then you call blof_init() before sending any header.
  • Finally, call blof_main() to embed the blog anywhere in the HTML body.

Here is a default stylesheet (referred as blof.css in the previous example):

$css

Blof can be customised with blof_set('param', 'value') where param is:

  • store: folder where thread data is stored. Defaults to the name of the embedder with its file extension removed (ie. embedding in demo.php will store data in the demo/ folder).
  • author: sets the default author's name, can be changed while posting.
  • passwd: sets the author's password, mandatory.
  • email: set the author's email for posting notifications.
  • brag: add a 'powered by Blof' link in the footer (set to 0 or 1).
  • validate: add 'Valid XHTML' and 'Valid CSS' links to W3C validator in the footer (set to 0 or 1).
  • footer: custom footer note, appears on the left.
  • per_page: number of threads per page.
  • cap_fold: number of characters displayed by a folded thread.
EOF; $title = "Blof $blof_version"; if ($action != 'about' && $action != 'doc' && $action != 'src') return 0; if (!$embedded) echo << $title EOF; if ($action == 'about') blof_print_block("thread", $title, "", $about); if ($action == 'doc') blof_print_block("thread", $title, "", $doc); if ($action == 'src') highlight_file(__FILE__); if ($embedded) blof_print_footer(blof_home()); else echo << EOF; return 1; } /* Use to send the script itself for self-download */ function blof_sendfile($fname) { $file = fopen($fname, 'r'); if (!$file) return; header('Content-Type: text/plain'); fpassthru($file); fclose($file); exit; } /* * Main routine */ function blof_set($opt, $val) { global $blof_opt; $blof_opt[$opt] = $val; } function blof_init() { global $blof_opt; if (!session_id()) session_start(); $action = $_REQUEST['a']; $brag = $blof_opt['brag']; if (!isset($brag) || $brag) { if ($action == 'dl') { blof_sendfile(__FILE__); } } if ($action == 'rss') { blof_rss(); } } function blof_main() { global $blof_opt; // Set some sane defaults were value are not defined $defaults = array( 'store' => preg_replace('/\.[^\.]+$/', '', $_SERVER['SCRIPT_FILENAME']), 'cap_fold' => 500, 'per_page' => 5, 'footer' => '© 2005 - All rights reserved', 'brag' => 1, 'validate' => 0 ); foreach($defaults as $key => $val) if (!isset($blof_opt[$key])) $blof_opt[$key] = $val; // Check data store, build if necessary $store = $blof_opt['store']; if (!file_exists($store)) { if (!mkdir($store)) { blof_print_store_error( "The folder '$store' does not exist on your web server ". "and could not be automatically created for you."); return; } } // Handle various actions $action = $_REQUEST['a']; if ($action == 'logout') { blof_logout(); } if (blof_is_author()) { if ($action == 'tidform') { blof_print_thread_form(); return; } if ($action == 'tidadd') { blof_handle_thread_form(); } //if ($action == 'tidmod') {} //if ($action == 'tiddel') {} } else { if ($action == 'logform') { blof_print_login_form(); return; } if ($action == 'login') { if (!blof_handle_login_form()) return; } } if ($action == 'cidform') { if (blof_print_comment_form()) return; } if ($action == 'cidadd') { blof_handle_comment_form(); } if ($blof_opt['brag'] && blof_handle_info($action, TRUE)) return; blof_print_threads(); } if ($_SERVER['SCRIPT_FILENAME'] == __FILE__) { $action = $_REQUEST['a']; $action = isset($action) ? $action : 'about'; if (blof_handle_info($action, FALSE)) return; if ($action == 'dl') { blof_sendfile(__FILE__); } } ?>