<?php
/*
  (c) 2005 Vincent Caron <v.caron@zerodeux.net>

  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')").".</p>");
  } 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 = <<<EOF
    <form method="post" action="" id="logform">
      <p>
        <b>Password:</b> <input type="password" name="passwd" />
        <input type="hidden" name="a" value="login" />
        <input type="submit" value=" Login " />
      </p>
    </form>
    <script type="text/javascript"><!--
      document.forms["logform"].passwd.focus();
      // -->
    </script> 
    <p>
$info</p>
    <p>
$hint</p>
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("    <p><b>Error</b>: wrong password.</p>\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 = <<<EOF
$from said :

$text

--
$url
EOF;

  
mail($email$title$message);
}


/*
 * Index (ie. list of thread_id's or comment_id's)
 * The global blof_index holds the complete sorted list of threads.
 */

function blof_scandir($path) {
  global 
$blof_opt;

  
$scan = array();
  
$path $blof_opt['store'].($path != '' "/".$path "");

  if (!(
$dh opendir($path)))
    return 
$scan;
  while ((
$fname readdir($dh)) !== false)
    
$scan[] = $fname;
  
closedir($dh);

  return 
$scan;
}

function 
blof_is_thread_id($id) {
  return 
preg_match('/^\d{4}-\d{2}-\d{2}-\d+$/'$id);
}

function 
blof_sort_index() {
  global 
$blof_index;

  if (!isset(
$blof_index))
    return;

  
natsort($blof_index); // To properly sort threads posted the same day
  
$blof_index array_reverse($blof_index);
}

function 
blof_get_index() {
  global 
$blof_index;

  if (!isset(
$blof_index)) {
    
$blof_index array_filter(blof_scandir(''), "blof_is_thread_id");
    
blof_sort_index();
  }
  return 
$blof_index;
}

function 
blof_add_index($thread_id) {
  
$blof_index blof_get_index();

  
$blof_index[] = $thread_id;
  
blof_sort_index();
}

function 
blof_is_comment_id($id) {
  return 
preg_match('/^comment-\d+$/'$id);
}

function 
blof_get_comments($thread_id) {
  
$comments array_filter(blof_scandir($thread_id), "blof_is_comment_id");
  
natsort($comments);
  return 
$comments;
}

function 
blof_rss() {
  
header('Content-Type: text/xml');
  echo 
"RSS test ...";
  exit;
}


/*
 * Common store operations
 */

function blof_store_is_writable() {
  global 
$blof_opt;

  
$store $blof_opt['store'];
  if (!
is_writable($store)) {
    
blof_print_store_error(
      
"The folder <code>'$store'</code> 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($line0$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 
"<code>$func</code>";

  return 
"<a href=\"http://php.net/manual/function.$ref\"><code>$func</code></a>";
}

function 
blof_print_error($error$explain) {
  echo <<<EOF
<div class="blof_error">
  <p>Blof error</b>: 
$error.</p>
  <p>
$explain</p>
</div>

EOF;
}

function 
blof_print_store_error($text) {
  
$func blof_phpman("blof_set('store', 'your/folder/here')");
  
$explain = <<<EOF
$text
Please either fix the appropriate file permissions, or use 
$func to set your desired location.
In any case, the web server must be able to write into this folder.
EOF;
  
blof_print_error('could not create data store'$explain);
}

function 
blof_print_block($class$title$header$body$actions NULL) {
  echo 
"<div class=\"blof_$class\">\n";
  if (
$title)  echo "  <div class=\"blof_title\">$title</div>\n";
  if (
$header) echo "  <div class=\"blof_header\">$header</div>\n";
  if (
$body)   echo "  <div class=\"blof_body\">\n$body\n  </div>\n";
  if (
$actions) {
    
$actionbar $actions implode(" |\n    "$actions) : "";
    echo 
"  <div class=\"blof_action\">\n$actionbar\n  </div>\n";
  }
  echo 
"</div>\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[] = "<a href=\"?a=about\">Powered by Blof</a>";
  if (
$validate) {
                     
$actions[] = "<a href=\"http://validator.w3.org/check?uri=referer\">Valid XHTML</a>";
                     
$actions[] = "<a href=\"http://jigsaw.w3.org/css-validator/check/referer\">Valid CSS</a>";
  }
  if (
blof_is_author()) {
                     
$actions[] = "<a href=\"?a=tidform\">Post a new thread</a>";
                     
$actions[] = "<a href=\"?a=logout\">Logout</a>";
  } else
                     
$actions[] = "<a href=\"?a=logform\">Login</a>";
  
$actions[] = "<a href=\"?a=rss\">RSS</a>";

  if (
is_array($more))
                     
$actions array_merge ($actions$more);
  else if (
$more != '')
                     
$actions[] = $more;

  echo 
"<div class=\"blof_footer\">\n  ";
  echo 
implode(" |\n  "$actions);
  echo 
"\n</div>\n";
}

function 
blof_home() {
  return 
'<a href="?a=none">Home</a>';
}


/*
 * 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''"<span class=\"blof_".$markup["\1"]."\">\2</span>"'$line);

    
// Make paragraphs
    
$text .= ($text == "" "" "\n")."    <p>$line</p>";
  }

  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($text0$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 <span class=\"blof_from\">$from</span> on <span class=\"blof_date\">$date</span>$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"<a href=\"?tid=$thread_id\">Read more ...</a>");
  else
    
array_unshift($actionsblof_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 "<a name=\"$cid\">$header</a>";
    
$body   blof_enhance_text($data);

    
blof_print_block('comment'.($count '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("<a href=\"?tid=$id&amp;a=cidform\">Post a comment</a> ($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[] = "<a href=\"?to=".$index[0]."\">&lt;&lt;</a>";
    
$actions[] = "<a href=\"?to=".$index[$order]."\">&lt; See newer threads</a>";
  }
  if (
$more) {
    
$max count($index);
    
$end $index[max(0$max $pager)];
    
$actions[] = "<a href=\"?to=$id\">See older threads &gt;</a>";
    
$actions[] = "<a href=\"?to=$end\">&gt;&gt;</a>";
  }
  
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 " (<u>Hint:</u> call blof_set('author', 'your_name') to prefill this field)";

  
$body = <<<EOF
<form method="post" action="" id="tform">
  <table>
    <tr>
      <td><b>Title:</b> </td>
      <td><input name="title" size="80" /></td>
    </tr>
    <tr>
      <td><b>From:</b> </td>
      <td><input type="text" name="from" value="
$from" />$hint</td>
    </tr>
    <tr>
      <td><b>Date:</b> </td>
      <td>around 
$date</td>
    </tr>
    <tr>
      <td></td>
      <td><textarea name="text" cols="80" rows="25"></textarea></td>
    </tr>
    <tr>
      <td colspan="2" align="center">
        <input type="hidden" name="a" value="tidadd" />
        <input type="hidden" name="ctime" value="
$now" />
        <input type="submit" value="  Post new thread  " />
      </td>
    </tr>
  </table>
</form>
<script type="text/javascript"><!--
  document.forms["tform"].title.focus();
  // -->
</script> 
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($tidTRUE, array());

  
$from  htmlspecialchars($_SESSION['blof_from']);
  
$focus = ($from == '') ? 'from' 'text';
  
$body  = <<<EOF
<form method="post" action="" id="cform">
  <table>
    <tr>
      <td><b>From:</b> </td>
      <td><input type="text" name="from" value="
$from" /></td>
    </tr>
    <tr>
      <td></td>
      <td><textarea name="text" cols="80" rows="15"></textarea></td>
    </tr>
    <tr>
      <td colspan="2" align="center">
        <input type="hidden" name="a" value="cidadd" />
        <input type="hidden" name="tid" value="
$tid" />
        <input type="submit" value="  Post comment " />
      </td>
    </tr>
  </table>
</form>
<script type="text/javascript"><!--
  document.forms["cform"].
$focus.focus();
  // -->
</script> 
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 = <<<EOF
<p>Blof is a <em>very</em> simple blog implementation, for the real thing see
   <a href="http://www.dotclear.net">DotClear</a> or
   <a href="http://www.geeklog.net">Geeklog</a>.</p>
<p>Features:</p>
<ul>
  <li>Single PHP file - trivial install and upgrade</li>
  <li>No database - stores all data in flat files</li>
  <li>Simple setup - follow the embedded tutorial</li>
  <li>Conforms to XHTML 1.0 strict and CSS 2.1</li>
  <li>GPL license</li>
</ul>

<p>Limitations (by design):</p>
<ul>
  <li>Single author  - no login, only a password</li>
  <li>Flat threading - no nested comments</li>
</ul>

<p>You can <a href="?a=doc">browse the documentation</a>,
<a href="?a=src">browse the source code</a> or
<a href="?a=dl">download it</a>.</p>
EOF;

  
$xhtml = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html 
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
EOF;

  
$example = <<<EOF
<?php
require 'blof.php';
blof_set('passwd', 'mypass');
blof_init();
?>
$xhtml
  <head>
    <title>My blog</title>
    <link rel="stylesheet" href="blof.css" />
  </head>
  <body>
    <?php blof_main(); ?>
  </body>
</html>
EOF;
  
$example highlight_string($exampleTRUE);

  
$css = <<<EOF
div.blof_thread     { margin-top: 1em; margin-bottom: 2em; }
div.blof_thread
  div.blof_title    { font-size: 1.25em; font-weight: bold; border-bottom: solid black 1px; }
div.blof_thread
  div.blof_header   { }
div.blof_comment    { margin-bottom: 0.5em; padding: 0.25em; background-color: #e0e0e0; border: solid black 1px; }
div.blof_commentalt { margin-bottom: 0.5em; padding: 0.25em; background-color: #d0d0d0; border: solid black 1px; }
div.blof_comment
  div.blof_header,
div.blof_commentalt
  div.blof_header   { font-weight: bold; }
div.blof_body   { text-align: justify; margin-top: 1em; }
div.blof_body p { margin: 0.5em; }
div.blof_action { text-align: center; }
div.blof_footer { text-align: center; margin-top: 2em; border-top: dotted gray 1px; }
div.blof_error  { background-color: #f0b0b0; }
span.blof_from  { font-style: italic; }
span.blof_date  { }
span.blof_u     { text-decoration: underline; }
span.blof_i     { font-style: italic; }
span.blof_b     { font-weight: bold; }
pre.blof_code   { margin: 1em; background-color: #d0d0d0; }
EOF;

  
$doc = <<<EOF
<p>Here is a simple and complete example, let's call it <code>demo.php</code>:</p>
<pre class="blof_code">
$example
</pre>

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

<p>Here is a default stylesheet (referred as <code>blof.css</code> in the previous example):</p>
<pre class="blof_code">
$css
</pre>

<p>Blof can be customised with <code>blof_set('param', 'value')</code> where <code>param</code> is:</p>
<ul>
  <li><code>store</code>: folder where thread data is stored. Defaults to the name of the embedder
      with its file extension removed (ie. embedding in <code>demo.php</code> will store data
      in the <code>demo/</code> folder).</li>
  <li><code>author</code>: sets the default author's name, can be changed while posting.</li>
  <li><code>passwd</code>: sets the author's password, mandatory.</li>
  <li><code>email</code>: set the author's email for posting notifications.</li>
  <li><code>brag</code>: add a 'powered by Blof' link in the footer (set to 0 or 1).</li>
  <li><code>validate</code>: add 'Valid XHTML' and 'Valid CSS' links to W3C validator in the footer (set to 0 or 1).</li>
  <li><code>footer</code>: custom footer note, appears on the left.</li>
  <li><code>per_page</code>: number of threads per page.</li>
  <li><code>cap_fold</code>: number of characters displayed by a folded thread.</li>
</ul>
EOF;

  
$title "Blof $blof_version";

  if (
$action != 'about' && $action != 'doc' && $action != 'src')
    return 
0;

  if (!
$embedded)
    echo <<<EOF
$xhtml
  <head>
    <title>
$title</title>
    <style type="text/css">
$css
    </style>
  </head>
  <body>

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
  </body>
</html>
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'   => '&copy; 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 <code>'$store'</code> 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($actionTRUE)) return;

  
blof_print_threads();
}

if (
$_SERVER['SCRIPT_FILENAME'] == __FILE__) {
  
$action $_REQUEST['a'];
  
$action = isset($action) ? $action 'about';
  if (
blof_handle_info($actionFALSE))
    return;
  if (
$action == 'dl') { blof_sendfile(__FILE__); }
}
?>