ABOUT

About This Code

There are a few philosophies within. In terms of adventure, there is no one road to take, there are stops and side paths along the way. In terms of architecture, there are only artist's drawings of the exterior to go by. What matters is the destination, the end result, not how you get there.

"Anything and everything may change."

Code Is Not Sacred

We have no emotional attachment to this code. The code frequently is redesigned and rewritten. Write code in a manner as if it will be rewritten in the future.

"Be prepared to be deleted."

Code Will Tell

First versions, proofs of concept, new ideas, are always implemented without consideration to performance or efficiency or even to understandability. "Sloppy" code that works is code that works. Eventually, the code itself will "tell the way" for improvement.

"Let it happen in it's own good time."

Time To Redesign

If some portion of code is frequently being adjusted, redesign. If some code is confusing, redesign. If a file has become very large, redesign. If something is just not clear enough, or if the code suddenly "shows a new way", stop development and redesign. Code is not poetry.

"Code is meant to be rewritten."

Yet Another Motto

Many little phrases all alike have been coined, and occasionally one seemed like a motto. But a truer motto has arisen:

"We are the Sloppy Perfectionists."

CODESTYLE

Our Coding Style

The code has a very particular coding style that is unique enough to have to explain. Partly because of being dyslectic, the basic format style is so that the code can be easily read and understood — we call this:

Visual Clarity

Brace position and indent width and spaces before or after arguments and expressions, or all the rest of what most people call "programming style", actually has nothing to do with code being easily followed as one "reads" it.

No need to provide examples here — just look at the code. But here are a few reasons for the choices.

Line length is limited to 80 characters because we use a terminal and command line tools such as grep. This also helps reduce code complexity by the discipline of keeping expressions short and as simple as possible. Which brings us to this aphorism:

Simplicity is the cornerstone of good code.

Line length is short. Identifier names are short. Function names are short. Function length is short. File length is short. Statement block length is short. Indents are kept to a minimum.

This is all simply and only about clarity and about being able to quickly "see" what the code is doing. Not everybody uses an IDE or has a display that can show long lines. Long lines that are word-wrapped are hard to follow. And having to scroll the display left and right is bothersome.

We format the way we do simply because it makes it clearer to see what the code is doing. Which brings us to what we call our:

Five Minute Rule

If a feature can not be added in a clear way within five minutes, stop and adapt the code so that the feature could have been added in a clear way within five minutes.

Tab Stops

This is, perhaps, annoying to some. We use real tabs and pretend they are eight spaces. Real tabs reduce file size and eight spaces force us to keep indents to a minimum. That is all.

Comment Style

Our code commenting style has evolved over time and we have settled on a particular style that we think helps to understand the code — but in a unique way.

  • Comments do not describe what the codes does but why the code is.
  • Comments should be positioned to better highlight the code.

We use both /* */ and // style comments but in a particular way. We use the former for top-level code blocks (top of file, before functions), and the latter for code-right comments (for the most practical reason that we save three characters) and sometimes to introduce inside level code blocks.

We comment functions like this:

/* functionname - one line function description */

// option line here perhaps explaining something not obvious

function functionname() {

    ...
}


We comment within functions mostly like this:

                                                   
    Comments to the right                           
// always line up
             
of code should be,                     // like this to be
                   
no matter what,                 // more easily read
                           
kept short              // and be to the point


so that code and comments have two distinct "columns", like so:

    +-----------------------------------------------+---------------------+
    | Code Goes Here                                | Comments Go Here    |
    +-----------------------------------------------+---------------------+
 

We use the "breaking" of this rule as way to highlight something, like a change that is going to happen, by starting a comment at column one:

    if (debug()) {
// I don't like this here! truncation needs to be moved into debug()
       
...
    }


Sometimes a block starts with a "header" comment:

    // this does that weird thing

   
if (isset($weird)) {
        ...
    }


The latest addition is using # at column one to mark something that will change:

# this is too noisy
   
if ($music === TRUE) {
        ...
    }


(Note that the judicial use of a blank line can be just as important as a comment.)

Comment Essays

The number of comments and their scope has also changed over time. What is done now is to start off a source file with a large comment block explaining what the API is to do. And at the end of the file there can be essay like comments limited to explaining/reasoning about anything not obvious from just reading the code. If the source code were to be actually deployed, the essay comments are easily deleted.

Next Step

What is needed though, is documentation in the format of PHP's function documentation. Then, comments in the sources are reduced, these document files are reduced to only explain the architecture and the data formats, and there is PHP-like, indexed and cross-referenced HTML that everyone is familiar with.

PHPDoc Comments

PHPDoc style comments are not used. This was tried to be explained previously but not explained properly. For a large and popular code base maintained by many (distributed) people? Of course they should be used. But nobody else uses this code, and the code is small and therefore PHPDoc style comments brings it no benefit.

Notes
  1. As the design is that each source file defines an API (or at least a number of related functions or functionality).

CONFIG

Site Configuration

The main configuration of the site is the file CONFIG.INI. Here is an excerpt:

    sitetitle = This is the Cool Website
    copyright = "© 2013 Cool Inc."
    debug = 0
    postlimit = 10000

Since the PHP parse_ini_file() function is used to parse the file some values may need to be delimited by quotation marks.

The CONFIG module parses this file into an associative array (see file MOD/CONFIG.PHP). This array is accessed by the function config() which is passed the name of the variable to read: config('themedir'). If a variable is not set (does not exist) an empty string is returned. If config() is called with no arguments the entire array is returned.

During runtime a variable can be set by passing a value in the function call:

    config('foobar',"foo");

The value can be any PHP type.

During runtime some settings are set according to the site section being viewed (see SECTIONS). For example, viewing the base site:

    setting         value
    section         "root"
    pagedir         "pages/root/"
    importdir       "import/root/"

Viewing the section "about" (?about) they are:

    setting         value
    section         "about"
    pagedir         "pages/about/"
    importdir       "import/about/"

The CONFIG.INI file is heavily commented.

Directory Names

There are several directories that the code expects to exist which are named in CONFIG.INI. They all end in dir. They need to exist (and do in the archive of course). (Some of them need to be writeable for full functionality.)

The names do not need a trailing / but can have one (the code checks for it, adding one if needed).

Three of the directory names are aliased by functions:

    function	alias
    htmdir()	config('htmdir')
    imgdir()	config('imgdir')
    themedir()	config('themedir')

Sensitive Data

No sensitive data is stored in the configuration array.

Modifying The Code

For anyone interested in modifying the code, this section explains how to use a new configuration setting. Anything placed into CONFIG.INI will end up in the configuration array.

Say you are looking at a block of code and want to change it so that it becomes conditional. All you have to do is put the block within a "config test".

Say there is this, and you don't particularly like it:

    print "Fooby Looby!";

To conditionalize it, step one is to add the config test to the code:

    if (!config('nofoobylooby')) {
    	print "Fooby Looby!";
    }

And then add the setting in CONFIG.INI:

    nofoobylooby = 1

That is all.

Because config() returns "" (empty string) if a config variable is not defined, we chose to use an example with a double negative on purpose — this way the default operation remains the same if you (or someone else) do not define the new setting in the ini file. (You obviously do not have to do things that way if you do not want to.)

Also, let's say you are looking at the code and see something like:

    $test = preg_match("/[a-zA-Z0-9]/",$name);

and see that it is used twice in the same block! And you go, "Ya know, it'd be way cool to fix that!"

To do so, step one is to make the code something like:

    $namematch = config('namematch');
    $test = preg_match($namematch,$name);

and the new INI setting is:

    namematch = "/[a-zA-Z0-9]/"

The example is trivial, but when much of the application is configured this way it becomes, well, trivial to configure and to make changes.

Caveats

The root section's configuration values are common to all sections (unless overridden by a section value; see SECTIONS). This means that if the root section was wanted to be the only section with a unique value, say reversesort = 1, all other defined sections must explicitly have reversesort = 0. (There may be a way to change that.)

Most settings are not defaulted, which means there will be errors if some settings are not defined.

Disallowed section names (currently root, users and visitors) are not checked and will causes errors if defined.

For a real application the latter two would be a real problem — in that an Admin edit with a single misspelling could possibly break the code, with the potential of preventing Admin from fixing the error (requiring an external configuration edit to fix it). Although We can live with that other people may not want to. There are solutions which will eventually be put into place.

DATABASE

Post Database

We designed the database code to be "Data Ignorant". All the code does is read/write/delete records. The code is just an API and knows nothing about the content of the records. Just like the fopen/fread/fwrite/fclose API. It is the "higher up" code that knows, or cares, about the content.

The database design is as simple as possible:

    +----+------+
    | id | body |
    +----+------+

Which is:

    CREATE TABLE data (id INT AUTO_INCREMENT UNIQUE, body MEDIUMTEXT)

See POSTFORMAT for how the body text is formatted (it is several headers separated by newlines, with the post text after two newlines). Like this:

    +----+---------------------------------------------------------------+
    | id | title:First Post\ndate:Jan-01, 2013\n\nWelcome...\nThis is... |
    +----+---------------------------------------------------------------+

which is exactly how a post is stored in the database. This data gets turned in to a record as:

    'id' => ID,
    'title' => 'First Post',
    'date' => 'Jan-01, 2013',
    'body' => 'Welcome...\nThis is...'

The code that turns a post into a record turns each header into an array member with the name/value set to each text:text\n found, with the \n\n as the marker for body; then it adds in the ID.

Although this design makes for really simple database, it does not allow for direct queries of a post's header data — that was not a requirement of the design.

Querying and managing post header data is done in a separate module that knows nothing about the database design.

Since the database cannot be queried on the post header data directly, as each record must be read in full, the headers extracted and then queried. With a small blog, with no more than several thousands of medium sized posts this is not really an issue because the overall code base is very small. However, with tens of thousands or more and with much larger post data sizes, the performance hit will be seen.

We do not plan to change the database design. However, we do speculate about this from time to time (more below).

The database has a separate table for each "section" (what is otherwise known as a "category").

Comments Database

The user comments database is just like the posts database with the addition on a column of entryid, which contains the id of the post that the comment is for:

    +----+---------+-----------------------------------------------------+
    | id | entryid | from:Joe\ntitle:Snow\ndate:Jan-01, 2013\n\n<p>It's..|
    +----+---------+-----------------------------------------------------+

This data gets turned into a record as:

    'id' => ID,
    'from' => 'Joe',
    'title' => 'Snow',
    'date' => 'Jan-01, 2013',
    'body' => '<p>It\'s..'

Currently comments are not editable and there is no comment header data beyond from, title and date. body is formatted for displaying before the data is stored. id is the MySQL record ID and not entryid, which gets lost — that may change.

Visitors Database

The Visitors database (see VISITORS) is similar to the POST DATABASE but with name instead of id.

    CREATE TABLE visitor (name VARCHAR(32) UNIQUE, body MEDIUMTEXT)
    +------+-------------------------------------------------------------+
    | name | from:Joe\ncode:$5$nHr7d\n\nip:127.0.0.1\ntime:1375296768... |
    +------+-------------------------------------------------------------+

This data gets turned into a record as:

    'from' => Joe',
    'code' => '$5$nHr7d',
    'body' => 'ip:127.0.0.1\ntime:1375296768'

The Visitors database is editable and is in the process of expansion. Headers and body data will be changing through the next several versions as we are adding features as we think of them. Note that body currently is like header data. We do not do anything with that data... yet.

Performance

Adding of two columns for the post title and date (while leaving room for a variable number of "sub-headers"), as all posts require a date and title header, might be an improvement.

The code was designed to be a single (Admin) user website and post records do not have a "name" header, and no other header fields beyond title and date lends itself to queries.

Complex queries such as date ranges and sorting are probably better turned over to the database server — certainly so for millions of rows or distributed databases — but this ain't that. PHP does what we want to do just fine.

To us, the current design has the direct advantage that if we want to add, remove or otherwise change the post header data there are not hundreds of SQL strings strewn throughout the code that would have to be changed.

Notes
  1. But with a configuration setting a few User Agent strings are added.
  2. This is unlike posts, which are formatted for display each time they are read out of the database. This is because comments are not editable as posts are. (Admin can edit posts but it edits them in their formatted state.)

DEBUGGING

About Debugging

THIS has a handy, nifty, cool thing called "debugging by print statements."

We are, of course, only serious.

Debugging is turned on by setting debug in CONFIG.INI to be non-zero. When non-zero, diagnostic messages are displayed in one of three ways:

  • Directly inline where is.
  • Directly inline where is delimited by HTML comments.
  • As a list upon program termination (with or without HTML comments).

Quite simple, yet quite effective.

Messages are stored (or displayed) by the function debug(), which takes any argument. If no arguments are given debug() returns the current debug setting.

A debug setting of 1 enables diagnostic messages which all get listed upon program termination. A setting of -1 turns on the immediate displaying of all debug messages. A setting of 100 is like -1 but all messages are wrapping in HTML comments. Debug summary:

    debug setting	result
    1		display messages at end of program
    100		all messages are displayed within <!-- -->
    200		debug messages saved to $GLOBALS['debug']

For the last case, there can be the string in the close web template:

    <div id="debug">{&#036;GLOBALS['debug']}</div>

so that they can be displayed however the CSS for the id #debug is defined.

For debugging output to be visible though, you must be logged in as Admin; this way, while you are debugging something no other visitor will see the diagnostic messages/data. The setting of 100 is an exception to this and is available only if for some reason (as has happened) there was an error logging into Admin — it can only be set in CONFIG.INI.

The debug setting can also be controlled by debug=<value> in the URL if you are logged in as Admin.

If CONFIGNODUMP is defined then no data is displayed regardless of the debug setting (the Admin code does this).

Message Format

The messages displayed by debug() have the calling file and function name prepended:

    (mysqli.php,560,query) SELECT body FROM root WHERE id = 39

with the CONFIG.INI setting of nodebugfunc to eliminate the function name (for just a little bit more clarity), and the setting of debugmod to show only messages from any one file:

    debugmod = mysqli.php

And to skip messages from modules:

    debugnomod = mysqli.php display.php

With the setting of debugcaller = 1 the function/location that called the function that called debug() will be displayed:

    (display.php,112)(mysqli.php,560,query) SELECT ...

All messages are filtered through htmlentities(), and can be truncated by the optional second argument to debug() (used when displaying post data which may be large).

As we progress with the code more features are added as we think of them. See CONFIG.INI for other settings.

Notes
  1. Resources and objects are displayed with get_resource_type() and get_class(), and arrays are imploded (handling associative arrays correctly).

ERROR.INI

The ERROR.INI File

ERROR.INI is a minor INI file and there are plans to simply turn it into a PHP array. For now it's importance is for rejecting comment spam.

Ban Words

There in an array that contains strings that will cause a submitted comment to be rejected:

    [BAN_WORDS]
    1 = "[url="
    2 = "[link="
    3 = "http://"
    4 = "href="

These strings being the sole reason comment spammers exist. (Our code also strips out HTML.)

It also supports regular expressions (between '//'s):

    5 = "/[a-zA-Z\.]+@[a-zA-Z]+\.(com|org|net|info|biz)/"

GMLP

GMLP Markup Language Processor

Post text is "processed" based on a small array of regular expressions and a few replacement strings — a "translation" table — in a separate, user-defined file, TRANSLATE.PHP, which is editable by Admin.

The purpose of the translation table is that it should be customized.

The GMLP code is now a separate API in INC/ and is also available separately GMLP API.

I thought of using Markdown or Textile, but Markdown is almost ten times as large as GMLP and Textile is almost as large as this entire code base.

HTML

HTML Configuration

The main theme configuration of the site is the directory HTM/ along with the files HTM/HTML.INI and HTM/TEMPLATES.PHP.

The HTML module parses the HTML.INI file into an associative array. This array is accessed by the function html() which is passed the name of the variable to read: html('menu'). If a variable is not set (does not exist) an empty string is returned. If html() is called with no arguments the entire array is returned.

During runtime a variable can be set by passing a value in the function call:

    html('foobar',"foo");

The value can be any PHP type but only strings and integers will make sense.

The TEMPLATES.PHP file is included by the html() function during start-up and that file simply creates the HTML "templates" by 'heredoc's:

$html['open'] = <<<HTML
<!DOCTYPE html>
<html>
<head>
<title>{\$html['title']}</title>
<link href="{\$html['themedir']}default.css" rel="stylesheet" type="text/css">
</head>
<body>
HTML;

That example defines the site's opening HTML template. As can be seen, any other $html variable can be referenced within the $html array (self referencing is undefined).

In the above example title and themedir are set during start-up as the section being viewed and the theme name set by CONFIG.INI (or SECTIONS.INI). See SECTIONS, see CONFIG.

Anything placed into the HTML.INI file will end up in the $html array. In addition to the $html array the site's configuration data is in scope in the array $config. Anything placed into CONFIG.INI or SECTIONS.INI end up in $config.

If you look at the code you will see that extra data can be referenced by a template when the data is passed to the display HTML function. For example, the post data is in the array named $record. The template $html['entry'] demonstrates this.

PHP's super globals can also be referenced. If any variable referenced does not exist it will be silently ignored (warnings can be allowed to be issued by the shownotices configuration setting).

The templates must be valid PHP or they will not be displayed and the code silently continues. (There is no configuration setting for these kind of errors but if debugging is enabled, see DEBUGGING, a diagnostic will be issued.)

Error Messages

This INI file also defines several HTML error messages. Here is our template not found error:

    TEMPLATE = The HTML template <tt>{$html['template']}</tt> not found.

Just like our web templates, any $html data can be referenced.

Comment Error Messages

Messages can occur when submitting comments with bad or missing data. These messages are placed into a special $html variable which is referenced in the comment form HTML template:

    {$html['comment_message']}

Normally, the message is blank (and does not display). If the code detects an error condition it calls a function to set one of the global error messages. For example, if a user comment is submitted without data, the code is similar to:

    if ($data == "")
    	htmlseterror('SUBMIT');

The variable $html['comment_message'] is then set to the message defined in HTM/HTML.INI by SUBMIT and the message gets displayed with the HTML template, the HTML automatically "adapting" (so to speak).

Themes

A theme is a subdirectory of HTM/ and is set by themedir in either CONFIG.INI or SECTIONS.INI.

The default HTML.INI and TEMPLATES.PHP files are always read first so a theme need only override what it needs (and can add anything extra) by creating it's own versions of those two files — see THEMES.

Although the default theme has a .CSS file such a file is not required for a theme.

Notes
  1. The file is parsed by the PHP function parse_ini_file().
  2. The only real concern here is that array references must be within {}.

INI FILES

Why INI Files

THIS uses INI files for many aspects of configuration. Currently, there are least three. All INI files are described in detail within them or within their corresponding module.

This document provides an overview of why we decided to use INI files for configuration.

An INI file is one of the simplest ways to store user configuration data; so common is INI file usage that we need not provide any examples beyond stating that PHP provides a function to convert INI files directly into an array.

While an SQL based database is incredibly useful, it is as ill-suited for handling only a few dozen configuration values, as an INI file is ill-suited for storing large amounts of distributed data.

An INI file can be a good choice for many reasons:

  • Human readable.
  • A universally understood format.
  • Editable by any text editor and/or command line tools.
  • Transferable through any medium.
  • Easily stored within any medium — even a database.

An INI file can be considered a self-creating database. One can use the simplest of text editors to write many lines of data, and with one function call turn that data into an associative array — something that cannot be done with any sort of database. JSON encoding has similar traits. XML does not.

An INI file is also suitable for remote configuration. Storing basic configuration data in a database requires a large "front end" application to support it, whereas with basic configuration in a text file one can use many simple ways to change the data — SSH, FTP, cPanel, etc.

Add in the ability to configure the core (or "start up") code by remote, without having to use the program itself, an INI file for basic configuration is a good choice.

Notes
  1. We do have, somewhere, an XML encode/decode API which converts PHP associative arrays to XML and back.

MODULES

A This Module

THIS calls some PHP source files "modules". These files have some internal data that it, and it alone, manages. Modules do not use global data but have a single function that has static data. (A few do not use any data but are not quite standalone APIs.)

Module data initializes in one of three ways:

  • When the module is included.
  • Right after INDEX.PHP has included all files.
  • When the module's entry point is first called.

The net result is that there are no include file dependencies. INDEX.PHP simply includes all files in the MOD/ directory.

Initialization

A module can self-initialize when it's main entry point is first called. This is the case of the system data functions config() and html().

A module can self-initialize on inclusion by calling it's own initialize function itself, which is usually the module name preceded by an underscore, _module(). Keeping the code in a function is simply to not have to worry about polluting the global namespace.

A module can also have a function with the name _config_module(). A list of these functions is created as the modules are included. After all source files have been included, these initialization functions are called.

Since all files have been included before a modules' initialization function gets called, each initialization function has the full code base available to it. This is not perfect, however, in that no initialization function can be aware of the state of any other module.

MYSQL

The Mysql Module

The MYSQL.PHP module is the only source file that has SQL strings. The code chooses the mysqli functions if available else it will use the mysql functions.

This module has gone through the most changes of any other we have written for this code — and it can be improved still. We believe this code to be pretty good (though odd).

There are four groups of database functions, each representing a separate database table: records (aka posts), comments, users and visitors. They each have common names:

    dblistrecords($id)
    dbreadrecord($id)
    dbnewrecord($data)
    dblistcomments($entryid)
    dbreadcomment($id)
    dbnewcomment($entryid, $record)
    dbreaduser($userid)
    dbnewuser($userid, $record)
    dbreadvisitor($from)
    dbnewvisitor($from, $record)

In one of the previous versions of the code each of those functions had it's own SQL query string, query call, result checking and row processing — we saw that as too redundant and too complex. So now there is something odd. There is one function for each database table:

    dbrecord();
    dbcomment();
    dbuser();
    dbvisitor();

But we left all those other functions is as they are called from several files. What we did was to change all the previous functions to be something like these:

    function dblistrecords($id NULL) {
        return 
dbrecord('list',$id);
    }

    function 
dbreadrecord($id) {
        return 
dbrecord('read',$id);
    }

The function dbrecord() actually does the work, and it is a bit like:

    function dbrecord($cmd$id NULL$data NULL) {

        if (
$data)
           
$data mysql_real_escape_string($data);

       
$db_table get_stored_table_name();

       
$sql = array(
       
'list' => "SELECT id FROM `$db_table` ORDER BY id",
       
'read' => "SELECT body FROM `$db_table` WHERE id = $id",
       
'new' => "INSERT INTO `$db_table` (body) VALUES ('$data')",
        }

       
$res mysql_query($sql[$cmd]);
        ...
    }

The change to a common function that contains all the SQL query strings is just an easy first step to simplify things in the MYSQL code without having to change any of the other code — and we think that is a good thing.

And herein lies the basis for our thinking: It is not only inefficient but also problematic to have dozens of SQL query strings (and the code to fetch the rows each time they are used) strewn throughout dozens of files.

We like having all the code's SQL query strings in one file, with one call to the mysql_query() function, with common code in one function and not duplicated throughout the file.

The next step will be to replace all the database calls to their "command" equivalents, further reducing code size and complexity.

Notes
  1. For now. See next note.
  2. Making too many changes throughout the code at one time is the number one source of bugs. This is a step by step process.

PERMISSIONS

Files and File Permissions

Admin expects many files and directories to have write permissions. None of these write permissions are necessary for THIS to work once fully set-up and configured, except that some functionality will be limited.

Also, some files and directories are only used while "learning" the application and will not be needed after set-up.

THIS is meant to be set-up and configured locally before being placed on an Internet server. Setting up Apache, MySQL and PHP on a computer is relatively easy to do for almost every operating system.

Setup File

The one file that must have write permission for set-up is:

    dat/defines.php

Out of the archive that file is empty (except for the PHP start-tag). During set-up some PHP defines are written to it.

Configuration Files

Admin can be used to edit all of the configuration files and the HTML templates file — or not. These files are:

    config.ini
    error.ini
    rules.ini
    sections.ini
    translate.php
    htm/html.ini
    htm/usercode.ini
    htm/templates.php

These files need write permissions only if they are wanted to be editable via Admin. Otherwise they can be read-only. No sensitive data is stored in any of these files.

Because the first two INI files are required for Admin to run, if there is a syntax error in one of them Admin will fail. The Admin editor does have a syntax check function and will not save a file if there are any errors. One still must be careful to make sure that settings are spelled properly and that all required settings are present.

The main program does have evaluations in place to survive syntax errors in the other configuration files. Still, one must be careful when remotely editing a live site — but again, doing so is not a requirement and we are working on a more robust Admin editor.

Sitemap

Admin has a sitemap.xml generator (it's kind of lame, though). The initial file is in the base directory and is empty and needs to be writeable. Using the Admin command sitemap for the first time will create the sitemap file. Any time a post is added the command can be used to up date the lastmod value for that section.

Directories

There are some directories that need to be writeable to support the functionality of uploading images, for adding/editing "pages" and for exporting and importing posts. These directories are:

    files
    pages
    import

The directory for the documentation and the directories for the highlighted source files are writeable because we use a PHP application to generate them. None of those files are at all required to be used (see TEMPLATES for how to remove the links to them from the navigation menu).

POSTFORMAT

Internal Post Format

A THIS post (also called an entry or a record) is stored in the database as text, which is weird, we know, but there is a reason. The database table is simply an INT named id and a TEXT named body.

See DATABASE for an explanation of the database design (which includes a discussion of some of it's shortcomings).

The text format is just like the RFC for Mail Transport Something or Other, one or more colon delimited word identifiers and text in a "header", followed by a blank line, followed by the text body. Here is an example:

    date: Jun-17, 2010
    title: Post Format
    This post format is too weird!

Immediately, several people just emailed at us to put title, date in the database like normal people! Well, we do things differently than everybody else for a reason. The Admin code to create a post is a TEXTAREA like this:

    +-----------------------------+
    | date: Jun-17, 2010          |
    | title: Title                |
    |                             |
    | Place content here.         |
    +-----------------------------+

The code puts in that default text (the date is automatically generated to the current date). And that is exactly how the data is stored in the database.

This is to be able to customize the database without customizing the database.

If you want to add a field to the header, say, um, color — you want to add a color to all your posts. To do so requires about three steps:

  1. Add a new line in the header of something like color: red.
  2. Modify your web template that displays a post to use the color.

Wait, that's only two. That's right. Two steps. That's all.

"Wait a minute," someone shouts from down the hall. "It can't be that simple!" Well, let's see.

First, a post is written to the database as the text from the TEXTAREA:

    $data = "date: Jun-17, 2010\ntitle: Title\n\nPlace content here\n";

Here is our write to the database function:

    mysql_query("INSERT INTO data (body) VALUES ('$data')");

This is our read from the database function:

    mysql_query("SELECT body FROM data WHERE id = $id");

Which reads that string back. This string is then turned into an array:

    $record['date'] = 'Jun-17, 2010';
    $record['title'] = 'Title';
    $record['body'] = 'Place content here';

(The code handles things like colons in a header, empty fields, etc.)

The HTML web template used to display the record is something like this:

    <div>
    <span>{$record['title']}</span>
    <span>{$record['date']}</span>
    <div>{$record['body']}</div>
    </div>

(Which actually has class identifiers and really nice CSS.)

To add color, simply modify the template, say, something like this:

    <div>
    <span>{$record['title']}</span>
    <span>{$record['date']}</span>
    <div style="color:{$record['color']};">{$record['body']}</div>
    </div>

That is all.

One of our mottos is: design your data well and your code will follow.

Of course, you probably see the flaw. There actually are three steps. Which leads to the next section.

Functionality

In the case of the colorizing we need to support older posts that would not have the color field. Currently, the HTML template display code will display non-existent record data as empty strings, so records without a color: header will be like so:

    <div>
    <span>Title</span>
    <span>Jun-17, 2010</span>
    <div style="color:">Place content here.</div>
    </div>

A harmless "flaw" actually. But what to do is to add a section at the end of CONFIG.INI file like this:

    [headers]
    color = red

The code to read a record uses that data to make up defaults for any missing data.

There are other places in the code that tests for certain post headers. There is nocomments: to not display comments for that post; and draft: so that the post will not be displayed. The code was designed to be changed to handle new post headers.

Notes
  1. The actual implementation is:
      CREATE TABLE data (id INT AUTO_INCREMENT UNIQUE, body MEDIUMTEXT).
  2. Eric S. Raymond: "Under Unix, this is the traditional and preferred textual metaformat for attributed messages or anything that can be closely analogized to electronic mail. More generally, it's appropriate for records with a varying set of fields in which the hierarchy of data is flat (no recursion or tree structure)."
  3. You do need to make sure that the headers are properly formatted and the the body is separated by two newlines (as the record will not be parsed properly without them). We will eventually put in some code to check for that.
  4. With the data properly escaped.
  5. We just put that in and the values are strings. We may add more functionality in the future.

RESHAPING

Wherein we define the term: Reshaping.

    Simplicity is the ultimate sophistication.
      -- Leonardo da Vinci

Many times — as many people do — when we start writing code we go through an iterative process of redesign. Every once in a while, after the code becomes stable, we rewrite the code.

As in introduction of what we mean, see MYSQL about our MySQL code modules.

We developed the mysqli version over a few months — i.e. an iterative process of starting with a "proof of concept" (a "quick hack" if you will), not caring too much about efficiency or duplication etc., just to get things working satisfactorily.

Once stable, we revisited it with an eye toward code reduction; consolidating duplicate code into sub-functions for example. Then, once stable again, we looked at code efficiency. (Others will have their own steps to improvement, of course.)

Each iteration came when "their time was due", when something "became apparent", and not on a fixed schedule. We write code with the idea in mind that everything and anything can change at some point if we can improve the code in some manner. But we tried not to have to change it all at once — only a little bit at a time when it seemed right to do so.

We are using the terms "Shaping" and "Reshaping" for this process (not to say that no one else works like this or that we have "first use"). The code is like a blob of clay and we shape it into something over time. Unlike "building", "shaping" is a slower process, the tools are not saws and hammers, but carvers and smoothers, one can use fingers and hands. And one can still carve out whole parts and stick-on more blobs.

It is, though, a tricky balancing act to produce something really elaborate, but, perhaps, that is the point.

Notes
  1. It's not always a forward process as there have been times when changes had to be undone.

RULES

Site Rules

This site operates through "rules" defined in an INI encoded file, RULES.INI. Here is an excerpt:

    default = read
    [read]
    displayhtml open
    displayentries
    displayhtml close
    [post]
    displayhtml open
    displayentry
    displayhtml close
    [submit]
    displaysubmit

That data, parsed by a really small algorithm, is the PHP equivalent of:

    $op = (isset($_GET['op'])) ? $_GET['op'] : 'read';

    if (
$op == 'read') {
       
displayhtml('open');
       
displayentries();
       
displayhtml('close');
    }
    elseif (
$op == 'post') {
       
displayhtml('open');
       
displayentry();
       
displayhtml('close');
    }
    elseif (
$op == 'submit') {
       
displaysubmit();
    }

Internally a rule is known as op, actually converted to $_GET['op'], and read throughout the code by getvar('op'). This is a backward compatible, or "legacy" thing explained in the code.

Changing The Rules

Rules are just functions. By adding this to RULES.INI:

    [phpinfo]
    phpinfo

You have now modified the site to call phpinfo() with a URL of ?phpinfo.

Currently, function arguments are stringized; i.e:

    [vars]
    var_dump a b c

with ?vars, would result in:

    string(1) "a" string(1) "b" string(1) "c"

Arguments with PHP like variables are compared against GET variables and set it them if they exist, else are set to empty strings. For example:

    [vars]
    var_dump $a $b $c

with ?vars&a=1&c=foo, would result in:

    string(1) "1" string(0) "" string(3) "foo"

Arguments can reference super globals within {}. This is a version of the logout rule, where the logout form has a hidden input of a redirect URL:

    [logout]
    cookie_unset from
    redirect {$_POST['redirect']}

Rule Conditionals

Rule functions can be conditional. If a function returns a value it is stored. A following function can have an indicator to base it's execution on. The post rule is actually preceded by:

    [post]
    validate
    !notfound

where validate() checks the value of ?post and if it returns FALSE notfound() will be executed else it will not. The other conditionals are ? and :. The ? is for functions that return TRUE, and : is for an "if else" construct:

    [test]
    is_numeric $a
    ?display $a is a number
    :display $a is not a number

Since echo() and print() are not functions they cannot be used as rules; display() is a defined function that prints all of it's arguments and so does the equivalent.

Summary

The RULES.PHP file is only 250 lines long, and the rules algorithm is only 60 lines, and this is the best introduction to this code: small and efficient, simple and versatile. And we try real hard to make it so.

Notes
  1. A possible change may be to enclose arguments in quotes: display "this is a string".

SECTIONS

Site Sections

THIS uses "sections" as a way to categorize posts — but this is optional as explained below. Site sections are defined in SECTIONS.INI. For example:

    name = Home
    subtitle = Main Page
    description = This website offers products and services
    [products]
    name = Products
    subtitle = Buy our stuff
    description = These are the products that we offer
    allowcomments = 0
    [services]
    name = Services
    subtitle = We do things
    description = These are the services that we offer
    allowcomments = 0

The default section is outside [] and must exist (but there is an alternative described below).

Note how the other sections have commenting turned off. A section can have it's own settings, including themedir so that sections can have different themes. (Some configuration settings won't make sense in a section — a complete list is forthcoming.)

The name and subtitle values get used in the menu created by the default theme and in the <title> HTML tag. The description value will be used for a <meta> description tag.

A section is identified in the URL as the first argument with a blank indicating the default section:

    ?about
    ?post=1
    ?about&post=1

Each "section" gets it's own database table, of the same name, created automatically at start-up — the default section is internally named root. There is also a related comments table created, by the name of {section}_comments. (The comments table always gets created regardless of the allowcomments configuration setting.)

There is an "import" directory for posts that (optionally) get read into the database upon first run of the program. There exists several posts in import/root for example as you may have already seen.

Each section can have it's own "pages". Pages are like posts but are stored as files in the pages/{section} directory. Pages are formatted identically to posts and they can be created, edited and deleted through Admin.

Configuration

During runtime configuration, the MOD/CONFIG.PHP module reads the SECTION.INI file into the configuration array (see CONFIG) as the setting sections and is an associative array. For example:

    var_dump(config('sections'));
    array(2) {
      [""]=>
      array(3) {
        ["name"]=>
        string(4) "Home"
        ["subtitle"]=>
        string(9) "Main Page"
        ["description"=>
        string(41) "This website offers products and services"
      }
      ["products"]=>
      array(4) {
        ["name"]=>
        string(8) "Products"
        ["subtitle"]=>
        string(13) "Buy our stuff"
        ["description"=>
        string(36) "These are the products that we offer"
        ["allowcomments"]=>
        string(1) "1"
      }
    }

Template Menus

The sections data are used to dynamically create the HTML template data for the main menu and the navigation (sidebar) menus (see TEMPLATES, see HTML).

Sections Need Not Be Used

If the SECTIONS.INI file does not exist the default section is created from the SITE section of CONFIG.INI. The HTM/HTML.INI and HTM/TEMPLATES.PHP can then be changed (or a new theme created) to eliminate the "main menu" and the "navigation bar".

Notes
  1. The file is parsed by the PHP function parse_ini_file().

SECURITY

Important Notice

This code passes user submitted data to eval — but only sort of. The HTML templates are "passed" through eval() before being displayed. The code is like:

    eval("\$text = \"$data\";"); 
    print 
$text;

And the data can contain a user submitted string in a POST variable — in one location in the code — that has been htmlentitie-ized.

If you do not like this idea then you should not use this code.

But it is safe.

Editing Live

The Administration code configures the site's core data via INI/PHP files, and the code is not 100% bulletproof — the potential exists (though very small) that a misspelling could possibly break the code, requiring an external configuration edit to fix it. This will be corrected at some undefined future date. Editing the site live is not a requirement.

Security

Web application security involves more than just passwords. But this document focuses on passwords and their related salts and cookies. (We hope to add "part two" soon about validating GET and POST data.)

This text is written for three reasons. One, to make sure that we understand what we are doing; two to inform others what we are doing with this code; three, in hopes that people contact us about any flaws or any improvements that can be made. (We can almost claim that our code is fully secure.)

Salts

For protecting the Admin and Visitor passwords there are different salts for them. The salts are randomly generated, one for each, and will be different for each setup. (There is a way to manually override this for the user to chose his/her own salts.) The default salts are for SHA_512.

There are three salts, one for Admins and one for Visitors, which are in SHA_512 format, and a cookie salt is just a mix of several random upper and lowercase letters and/or numbers and is used for encrypting cookies.

Admin Setup

During the Setup process the user enters a name and a password, and the three salts are generated. These are then written as PHP defines (as AD_NAME, AD_PASS, AD_SALT, VIS_SALT, COOKIE_SALT) to a PHP file. The password becomes the output of the function:

    $encryptedpassword = crypt($password,AD_SALT);

When the Admin code is next run it creates a Users database and stores in it a "user record" consisting of "name:encryptedpassword".

Admin Login

The Admin submits his/her name and password, ill-formatted names are rejected, then the name is looked-up in the database, if it exists it's record is read. The user password is then compared with:

    if (crypt($password,$encryptedpassword) == $encryptedpassword)

and success is if they match.

If name and password are accepted two cookies get set. The cookies are created using the following data:

    $name = [user name]
    $extra = [user IP address]
    $salt = COOKIE_SALT;

The two cookies are set then as:

    setcookie('userid',$name);
    setcookie('userdat',base64encode(md5($name.$extra.$salt,TRUE)));

Each time the Admin code runs it checks for the two cookies and compares them (if they exist):

    $name = getcookie('userid');
    $data = getcookie('userdat');
    
    if (base64decode($data) == md5($name.$extra.$salt,TRUE))

Access is allowed if they compare true.

If a user accesses the site by a different network that provides a different IP address he/or will either have to have previously logged out or has to delete their cookies (which is what logging out does) and login again.

See the section on SECURITY for why there are two cookies.

Visitors

THIS code does not "register users" for posting comments. It provides a way to remember a visitor's name by allowing the visitor to provide a "code" (which is just like a password). There is a visitor database to store a visitor's name and code, like for admin users, just using the visitor salt:

    $encryptedcode = crypt($code,VIS_SALT);

And when logging in the test is:

    if (crypt($code,VIS_SALT) == $encryptedcode)

(We are still working on "Visitor Code" and plan to add more features. See VISITORS.)

Visitor Cookies

Visitor cookies are encrypted/encoded similarly to Admin users except, for us, we chose to not include the IP address in the data (so visitors can roam from network to network without having to log out/in), and different names are used. The data are:

    $name = [visitor name]
    $salt = VIS_SALT;

The two cookies are:

    setcookie('from',$name);
    setcookie('fromdat',base64encode(md5($name.$salt,TRUE)));

Each time the code runs it checks for the two cookies and compares them (if they exist):

    $name = getcookie('from');
    $data = getcookie('fromdat');
    if (base64decode($data) == md5($name.$salt,TRUE))

The visitor is considered logged in if they compare true. (A visitor does not have to have cookies enabled. See VISITORS.)

Account Security

This section is about Admin account and cookie security.

If a user's computer is in the hands of another, and it's local password is compromised, and the cookies exist and are not expired and the person is using the same network connection, they have admin access to the site. (If their web browser is set to "save passwords" and the admin password has been saved...)

If a user's cookies are compromised (stolen), and somehow inserted into a different machine that has the same network connection, they have admin access to the site.

The reason for two cookies is that cookies can be forged. We had at one point just set a cookie of userid = name after verifying a user's password, and then just checked that cookie for existence for subsequent visits. But if someone knew that that was all we did — and the code is available so anyone could have — all one had to do was forge a cookie to be a logged in as an Admin. That is not good.

With a cookie pair, the name and the encrypted name, which are always encoded/decoded with a randomized secret known only to us, no one can forge the encrypted cookie — unless they get a copy of the secret. Adding an IP address into the encrypted data just, in theory, slows down a cracker if they get someone's cookies.

If somehow the PHP file containing the Admin name and password and salts was stolen, the thief would have to do some work to gain access. The user name is known. The password is encrypted and the salt is known, so a dictionary could be used to try and find the password. But could a cookie be forged to gain Admin access? Unlikely if an IP address was extra data in the hash.

Preventing web access to the PHP file can be achieved by .htaccess, but even that is unnecessary if the PHP file contains just a number of defined constants. The PHP file could be placed below DocumentRoot to prevent the file being downloaded directly if FTP access to the site was compromised (social engineering to get the password or finding a password written down, however unlikely).

The leaking of the secret (salts) seems to be the main concern here.

Code Errors

Code can have errors that leave possibilities for "leaks" if there is any code that could possibly display data (by design or by exploit) that happened to contain usernames, passwords (encrypted or clear) or salts. All of these can be prevented by never storing any sensitive data as variables, and only using defined constants where appropriate in the code.

The having of viewable highlighted or PHPDoc created source code is another possible leak. Any such source code documentation must be configured to never process any file with sensitive defined constants.

Defaults

Having defaults for any sensitive data leaves open the possibility for people to just use them as is. If possible, and ideally, no sensitive data should have defaults stored anywhere. Users must have to create them themselves, either by instruction of how and where to edit a file or through an HTML form. And these data should then be stored in a protected file (or, for example, in a PHP file as defined constants).

If there has to be a default password, like a router for example — although I am speaking about website applications in general here — the code should have a "You must change this password before continuing," mode.

In a related area, files too have defaults that should be able to be changed. Our website logs show constant fishing attempts for known default file names such as wp-login.php, signup.php, wp-admin/, etc. The list is a long one. Being able to change your website's administration page name is just one more preventative measure to take (unfortunately, many large and popular web applications have hard-coded file names throughout many files).

Of Course There Is More

This is just a "first draft" and we plan to expand it as mentioned in the introduction.

If you see anything stupid or faulty here, please contact us. Thank you.

Notes
1. I took this from a note on PHP.NET:

<?php
function randomstring($len) {
$s 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    return 
substr(str_shuffle($s),0,$len);
}
?>

2. The two cookies can be combined into one, actually.
3. Getting the SALT of course, barring any software errors that could leak it or allow it to be stolen, would be near impossible if correctly stored on the server. However, humans can make mistakes. We thought of putting the encrypted password (less the salt portion) into the cookie which would prevent the cookie forgery, but that means transmitting it back and forth over the Internet and we have to think more about the ramifications of that.
4. Pretty standard stuff to many, of course, but we all had/have to learn this stuff at some point.
5. That one small thing — forcing router installers, or other hardware installers, to override the default password long ago — would have prevented a great many server cracks over the years!

TEMPLATES

True HTML Templates

All "templates" are in one PHP file and are created by use of heredoc. Not quite as easy as editing .html files, but the performance increase over multiple files loaded and parsed as the code runs is dramatic.

A template is straight HTML within which PHP variables can be referenced. This is a snippet of a template:

$html['template'] = <<<HTML
<h1>{\$config['section']}</h1>
<div style="{\$html['style']}">
The header name and style attributes are PHP data.
</div>
HTML;

The code is designed to ignore undefined variables and indexes in templates. There still will be a PHP warning generated, which will be seen in debug output. Such warnings are harmless. These warnings will be issued for any referenced variable that has not been defined (which is often enough to be noticed).

No Code

There are no code constructs in the templates (there is no template "engine" whatsoever) but there is a "loop support" function to supplement the creation of repetitive HTML.

Examples

Each template is named and these names are used in the main display code, MOD/DISPLAY.PHP. To display the opening HTML there is a function call like:

    displayhtml('open');

How the templates are used during a website hit is very simple:

1. The open template is displayed.
2. The entries template is displayed for each post.
3. The close template is displayed.

The main menu and navigation menu (sidebar) both have their own templates but are incorporated into the open template. (Example below.)

When viewing a single post the output is simply:

1. The open template is displayed.
2. The entry template is displayed for the post.
3. The close template is displayed.

When viewing a single post there may be comments and a comment form:

1. The open template is displayed.
2. The entry template is displayed for the post.
3. The comment template is displayed for each comment.
4. The commentform template is displayed for the comment form.
5. The close template is displayed.

Note how totally different this all is from a WordPress theme (if you are familiar with a WordPress theme). The order of and names of templates are hard-coded in the code. That is a drawback or sorts and will eventually be changed.

There is an HTML data array named $html that holds all of the templates as well as many HTML related strings, all of which are customizable and extendable. Any template can refer to this data in the $html array, and even other templates.

Default HTML Strings

There is a default HTML configuration file, HTML.INI, that has several most likely to be changed strings that the templates reference, and any of these can be changed, and any template can be changed to not use them or to use others. Everything placed in the INI file gets placed into the $html array (which is accessed through the function html(); see HTML). Here is an excerpt:

    commentstext = comments ($N)
    commentslink = &bull; $1
    addcommentlink = Add Comment

The strings beginning with $ get substituted during runtime; in the above excerpt $N is replaced with the number of comments a post has, and the $1 is replaced by the value of commentstext. A result may be:

    $html['commentslink'] = "&bull; comments (2)";

Some of these INI strings are referenced by the code only, and change during runtime — the commentslink value is calculated for each post for example. And some of them are not (meant to be) referenced by templates.

Some $html values are created only during runtime depending on context. These data, like for menus, are usually referenced just by the templates. Some of these dynamic data are formed with the help of INI values. And all of these can be directly overridden with a template.

The Data Connection

Templates refer directly to $html (and the configuration data, $config; see CONFIG). For example, the open template is:

<!DOCTYPE html>
<html>
<head>
<title>{$html['title']}</title>
<link href="{$html['htmdir']}default.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="header">
<a href="{$config['siteurl']}" title="{$html['title']}">{$html['subtitle']}</a>
</div>
<div id="main">
<div id="content">
{$html['menu']}

Note that $html['menu'] is dynamically created during runtime depending on how the site is configured. It also can be another template if it is defined as a template. There are currently four of these dynamically created "sub-templates".

The post data is in an array named $record, which is referred to by the entries template:

<div class="entry">
<span class="title">{$record['title']}</span>
<span class="date">{$record['date']}</span>
<div class="body">
{$record['body']}
</div>
</div>

And the close HTML file is:

</div>
</div>
<div id="footer">
<span id="copyright">{$html['copyright']}</span>
</div>
</body>
</html>

(If we could make it simpler than that, we would. Well, we can, of course, but that is the beauty of CSS.)

Themes

A theme is nothing than secondary TEMPLATES.PHP and HTML.INI files in a subdirectory of the theme's name. If a theme is set — globally or per section — a theme's data simply overrides (replaces) the default theme's data.

The template names and the $record array are all that is required of a theme. The $html array is a benefit but need not be used. The open HTML could very well be:

<!DOCTYPE html>
<title>Welcome to my world!</title>
<link href="htm/mytheme/max.css" rel="stylesheet" type="text/css">
<a href="http://myworld.com" title="excellent!">My World!</a>
<br>

or whatever one wants. Missing or empty templates are silently ignored. (See THEMES.)

As a tutorial on editing the templates, here is how to remove a section of HTML from the left side navigation menu.

In Admin, click on the edit command and then on the link to htm/templates.php. Scroll down or search for the navmenu template, which will be like this:

$html['navmenu'] = <<<'HTML'
<div id="navmenu">
Sections
<ul>
{$html['navstra']}
</ul>
Other
<ul>
<li><a href="{$html['helpurl']}" target="_blank" title="help files">{$html[help]}</a></li>
<li><a href="doc/" target="_blank" title="documentation">Documentation</a></li>
</ul>
</div>
HTML;

The reference to $html['navstra'] is the dynamically created section links (as described in the templates file itself).

The "Other" section of the navigation menu will not be needed after set-up and should be deleted and then the templates file be "saved".

If your host is using ModSecurity it will probably not allow the post and will issue "403 Access Denied". If that happens you can edit the file offline and use the Admin editor's put command to directly upload the file (which still might be blocked!). Many hosts have cPanel which has a file manager that will work to edit all of your files. Just be careful of syntax errors.

A LOCALHOST installation would be highly recommended to test all your changes.

Templates Within Templates

Templates can "nest" three levels deep (a template can reference another template which can reference another template):

$html['open'] = <<<'HTML'
<header>
{$html['navmenu']}
</header>
HTML;

$html['navmenu'] = <<<'HTML'
<ul>
{$html['navstra']}
</ul>
HTML;

$html['navstra'] = <<<'HTML'
<li>Index</li>
HTML;

Which will ultimately display as:

<header>
<ul>
<li>Index</li>
</ul>
</header>

The caveat is that third-level templates — which are typically dynamically created HTML based on configuration data — cannot reference any PHP variable (they would be printed as plain text).

The Display Function

The "web template" display code is a single function that uses eval() to resolve the referenced variables, doing so twice in order to resolve nested data. The eval() is only an assignment and there is no possibility of code being executed, however the use of eval() does have a potential "problem" mentioned later. print() is directly used to actually print the data.

Warnings against eval are valid, and the code that uses it has had it's share of mistakes (which is mostly because the code is confusing and will not make any sense with just a casual look).

The only real problem to watch out for is making sure referenced PHP array variables are properly formatted — which if they are not the template will fail to print ad the code will continue (with optionally a printed error notice).

There are two types of data that templates can reference, the sitewide data arrays $config and $html, and data, either an array or a string, passed to the display function.

There are three basic calls to displayhtml(): with just a defined template name (defined by HTM/TEMPLATES.PHP); with an array of data that the template can reference; and with a string that will be evaluated and referenced by the template:

    displayhtml('template')
    displayhtml('template','$record',$record)
    displayhtml('template','$str',"template is {\$html['template']}")

With the example templates (one per line):

    <p>template is: {$html['template']}</p>
    <p>passed array: {$record['member']}<p>
    <p>$str</p>

The first and the last result in the same output.

To accomplish the templates within templates there are two evaluations — the first evaluation can evaluate a string, say, $html['foo'], into the string $html['bar'] and then the second evaluates it.

But not all templates are like that, so $html['direct'] can hold the names of templates that can skip that second evaluation.

It is this second evaluation that can cause a problem with a template that refers to $_POST input data if the input data contains a $. To protect against this the template name is entered in the direct HTML data (an array).

The default theme has one template with a $_POST reference, commentform.

Admin Templates

The Admin code has it's own templates and CSS and is implemented in a wholly different manner.

Notes
1. A full list of these strings and their uses is forthcoming.
2. A full list of these values is forthcoming.
3. This is the tricky part (and actually took us a while to get right). Here is a simplified example. The INI string menustr is like this:

    <a class="$selected" href="?arg=$key" title="$subtitle">$name</a>

And the $html['menu'] result is made out of that HTML for each section of the site with the $ designated strings appropriately replaced during runtime. The really nice thing about all this is that a template does not have to reference the menu data, or a menu template can be defined and it will be used — basically a template can be anything it wants to be.
4. Again, better documentation is forthcoming.
5. I have fudged some of the newlines for this example.

TESTING

The Admin code does not require a login/password until set-up.

THIS has two modes. File and MySQL. The default is File Mode and there are some pre-created posts that describe the code (they are in the import directory). This means that the code works exactly as it would if it were fully setup. No MySQL is used in this mode, all posts are files.

File mode is a fully functional site, and the code can be deployed in this mode. If your web-host has a File Manager interface, such as cPanel, or if you were to just FTP new posts and pages, you would not even need the Admin code.

To enable the default of File Mode there is extra file, TESTING.PHP. Once set-up this file is no longer necessary and can be deleted. Then, either FILEMODE.PHP or MYSQL.PHP could be moved into the MOD/ directory depending on how you want to run the site.

THEMES

Themes Are Just HTML

Themes here are only an associative array of HTML strings. The array names are hard-coded but that may change. The array is formed from two files, an INI file and a PHP file (see TEMPLATES).

And One Code File

Themes do have a code component. If there exists a file, HTM/THEMEDIR/FUNCTIONS.PHP it will be included (by INDEX.PHP). Such a file can do anything as there are no constraints, it is simply included.

Any Fatal Error in a theme's FUNCTIONS.PHP will not display any diagnostic as error reporting is disabled. Admin includes the file too, but that will catch syntax errors only. We are working on a way to mitigate this.

Creating A Theme

An additional theme is simply a new directory in htmdir. Creating a directory and assigning the themedir setting to the new theme directory is the first step to create a theme. Here is an example of how to create a new theme for the "About" section (see SECTIONS).

In the program base directory, create a theme directory (any name will do except one that exists):

    mkdir htm/theme

Add the following line in the SECTIONS.INI file in the [about] section:

    themedir = theme

Viewing the "About" pages will look no different. This is because the default theme's files are used in the absence of any theme's data. To use new HTML for the theme a new file, HTM/THEME/TEMPLATES.PHP needs to be created. In this example the open html will be changed.

Create the template file in the new theme directory with this in it:

<?php

$html
['open'] = <<<'HTML'
<!DOCTYPE html>
<html>
<head>
<title>{$html['title']}</title>
<link href="{$html['htmdir']}default.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="header">
New Theme
</div>
<div id="main">
<div id="content">
HTML;

?>

When you next view the "About" section you will see the changes. If you want Admin to be able to edit the file use chmod 646 htm/theme/templates.php.

This example demonstrates how the default templates are always read first and then the theme templates are read to override or add to the default $html array (see HTML).

In the above example, the open HTML explicitly refers to the default theme's CSS file:

    {$html['htmdir']}default.css

For a theme to use it's own CSS the line would be:

    {$html['themedir']}default.css

or it could be:

    htm/theme/style.css

whatever.

Other hardcoded references to templates are in MOD/DISPLAY.PHP and in ERROR.PHP and will eventually go away some how.

Another aspect of the display code is that if a template does not exist it will not be displayed (and issues no error). So adding this to the theme templates:

    $html['close'] = '';

will cause the footer to not be displayed.

More Later

That's just the basics. In time there will be more documentation.

Notes
  1. This may change, but only slightly.
  2. All configuration variables for directories automatically get a trailing / if needed.
  3. We try really hard to make this code simple. But, it is at times confusing to us, and we wrote it.

USERCODE

User Defined Code

There is a way to modify the code without modifying the code by adding code in an odd way called "Usercode".

Usercode is a way of fairly easily and quickly adding code to a theme that can modify posts or even run amok, that is, code can modify all posts, and, late in the initialization process after the site has been configured, code can be run in the global scope.

The code does parse the Usercode to catch potential parse and fatal errors. That code might not be perfect (but it is pretty good). If an error gets missed — a remote but possible condition — and does cause a fatal error, the Admin code and the Website will no longer function.

It is recommended to not use this feature on a live site but to simply experiment with it to see if it has any merit.

The only way to recover from a fatal error is to either add nousercode = 1 to CONFIG.INI or to otherwise remotely undo the changes. This will change.

The Admin edit code will not allow Usercode to be saved if it detects an error in the code.

There are four types of Usercode.

1. Applied to all posts.
2. Applied to a single post.
3. A single function to run in limited scope once during start-up.
4. A PHP file that is included in the global scope to support the first three points.

First, by "user" we do not mean people visiting the website. We mean people "using" this code. Usercode can only be created and installed by an Administrator.

Second, the Usercode is in an INI file which is read into an array and the data is used to create functions. The INI file is editable by the Admin code so an Administrator can modify the code at anytime. As an INI file there is only a slightly less chance of typographical errors, but it provides a way to parse the code and catch errors while maintaining the overall security of the site.

Third, there can exist a PHP source file that will simply be included in global scope if it exists (see THEMES). This allows Administrators to have total control to modify the running of the code as much as possible without having to modify the core code.

There is no "Plugin API", no "hook functions" and no classes. Just code. Knowledge of how the core code works and how it operates is the fundamental requirement for adding additional functionally via a theme (more about this later).

Usercode Is In A Theme

This is all going to change to a simple API. It's current form should be considered experimental.

Usercode is associated with a theme and is in the USERCODE.INI file. The default theme's Usercode file is empty. Here is how to add some Usercode.

In Admin, click on the edit command. To the right, click on htm/usercode.ini and then add the following and save:

    [signature]
    $r = $record['body']."<br>&mdash; All's well, that ends well.";
    return $r;

It's straight PHP code. There are some limitations like some constructs cannot be used such as goto, eval, echo and include. It is recommened that the code be small and simply modify the post body. The code runs in function scope.

There can be multiple Usercode sections. The return value later overwrites $record['body'] if it loosely tests true (but is expected to be a string—more on that later).

To have Usercode applied to a post, in Admin, create a post with the line, right after the title: line, and before the body of text:

    usercode: signature

When previewed the post will end with the "signature" defined above.

Perhaps you now see why our Admin post form is so spartan. What was just done in one minute, the editing of one file to modify the program, would have taken an hour and many files, perhaps having to "implement a plugin class extension", in just about every other Blog application.

More Details

There can be any number of Usercode sections. And a certain two have special function. A section with the name [allposts] will have it's code applied to all posts. A section with the name [global] will have it's code executed once during start-up, after the site has been initialized and after any theme's FUNCTIONS.PHP file has been included — more about that later.

The Usercode has a copy of the post record. And if the Usercode returns a string that string will then be copied into the record body. If the Usercode decides to not change the post body it should return 0 (or NULL or FALSE).

The Usercode ID can have arguments, separated by commas, and they will be in an array variable $user_args, with [0] the Usercode ID and other members any arguments.

The allposts Usercode can be bypassed for any post which has a header of noall:.

The post body is run through the "post translation" code (as described in one of the default posts when the program is run for the first time) before the Usercode is run.

The [global] Usercode runs with only the super globals in scope, but of course can call any existing function. There are no arguments to the global code.

Implementation

The code to support Usercode is really small and is in the MOD/USERCODE.PHP file, has no global data and has a single entry point, usercode(), which is called by only one function, displayrecord(), in the MOD/DISPLAY.PHP file.

There are two ways to disable Usercode, 1) by a CONFIG.INI setting of nousercode = 1; 2) by removing the MOD/USERCODE.PHP file.

FUNCTIONS

If a FUNCTIONS.PHP file exists in the theme directory it will automatically be included. This file has no constraints and no requirements (other than being valid PHP code). We do not provide an example but may do so soon.

The default theme templates and the theme data file (HTML.INI) are always included even if the default theme is overridden (see THEMES). However, only the set theme's Usercode and FUNCTIONS.PHP will used — i.e. if you create a theme and use the themedir = newtheme configuration setting, the default theme's Usercode will not be used.

Notes
  1. But, since in the global scope, can do anything.
  2. We will eventually convert the INI file to a PHP file. But the reason we have an INI file is, 1) we were not too smart when we started; 2) we want a minimalist Admin text editor to remotely edit the code. We actually do not want a PHP file filled with defined functions for this, as that would not work. An associative array of code is how to make this work.
  3. If there is a parse error in the Usercode that code section will be discarded. (A function not found error will stop execution — we are working on a fix.)
  4. The file should have write permissions.
  5. We are going to incrementally change the interface. The next step will be to use a PHP formatted file (an associative array of code). Then we will see what comes next.

VISITORS

About Visitor Code

We have a way of registering users without user registration which we are calling Visitor Code.

Visitor code is enabled by configuration setting. When disabled the visitor code is still included, just all of it's functions do nothing.

This code will look strange or even dumb to people, and I admit that it does not bring much of a benefit to the code, however, that is because the code is not fully implemented. This is just the first step. Visitor control of many things will be added — it will just be done in a way different than what people geerally think of User Registration.

This first step is an API 300 lines long; an Admin interface 300 lines long (including the HTML); and database code of only 60 lines and 6 SQL query strings.

Preamble

The term "Visitor" is used instead of "User" with regard to comment submissions because there is no "User Registration" just to submit a comment — we do not want to require anything like "User Registration" just to allow a visitor to submit a comment.

Preventing comment spam is, of course, the main reason websites force casual visitors to become registered users in order to submit comments. But there are simple ways to prevent comment spam (see ERROR).

(There are, of course, other reasons for requiring users to register, but there is no need to discuss them here. We simply choose to do things this way.)

Implementation

Our visitor handling code will not be obvious, trust us on that. But we do what we do because, as we said, we do not want to impose any user registration or even any required data on visitors, yet, we want to give visitors the option of being "remembered" and to provide a way to allow a visitor to have exclusivity of his or her chosen user name. And later, customization of what the site looks like and how it operates.

There are two visitor checkpoints: 1) displaying the comment form, and 2) parsing a comment submission.

We use what we call a name/code pair — which is not unlike a username and password, just that there is no registration process. The comment form simply has an extra text input for a code.

There are two states for the form display:

    state one => no cookie
    state two => cookie set

The form inputs are set accordingly:

    state one => inputs for name and code displayed
    state two => input for name = name stored in cookie, code = hidden

A first time visitor would see the two inputs, of course.

Parsing a submitted comment is:

    state one => name entered
    state two => name and code entered
    state three => name entered and cookie is set

(The case of a code entered but not a name is ignored.)

The acceptance of the comment is:

    state one => name is not in the visitor database
    state two => name is not in the visitor database
    state two => name and code are in the visitor database and correct
    state three => name matches cookie

State one does not save the name in the visitor database. The first case of state two saves the name/code pair in the visitor database. For state three the cookie is the name encrypted, but it is tested just in case the form name input had been modified.

Here is the important part: The first case of state two is the new user registration.

If a repeat visitor does not enable cookies she will have to enter the code for each comment.

Comment rejection is when a name is entered which is in the database but the code is not entered, the code is wrong or the cookie is wrong.

Each name/code pair value must be non-blank. (The name is restricted to alpha-numeric, spaces and periods and 32 characters; the code is only limited to 32 characters.) Each name stored must be unique, of course. (The character set is UFT8 and a collation of UTF8_BIN.)

WHITESCREEN

Errors Disabled

Sometimes during development the program will err and leave a white screen as error reporting is turned off.

But as administrator, you need to know what the error is. So Admin enables all error reporting. And there is an error in the main site, simply login to Admin and the error will be seen. Usually.

The main code starts with INDEX.PHP which Admin does not use. The main code has a few include statements that Admin does not have. Errors in any of those cannot be detected by Admin. A Whitescreen with debug output enabled will indicate a location close to or even the file, but not the error.

Any Fatal Error in a theme's FUNCTIONS.PHP will not display any diagnostic as error reporting is disabled. Admin includes the file too, but that will catch syntax errors only. We are working on a way to mitigate this.

Warning And Notice Display

An error handler is installed and can be controlled in a number of ways in how to display errors (in ERROR.PHP). It always issues a debug diagostic, but can display them to the screen with these CONFIG.INI settings, initially set to 0:

    showerrors
    shownotices
    shownotfounds

The shownotices of 1 displays E_NOTICE messages, like for undefined variables. Undefined variables are actually common and harmless in some of the HTML templates.

The showerrors setting can turn on the display of web template parse errors. Web templates are are loaded simply as data and are parsed when displayed. If a template has a parse error it will not be displayed.

The shownotfounds setting can enable messages for missing web templates.

Editing The Site Live

The Admin editor prevents syntax errors in a template from being saved, but cannot detect spelling errors and typos that do not cause a parse error. Turning on debugging is recommended when editing files.

Notes
  1. See the top of INDEX.PHP for how to turn on "maintenance mode".
  2. In the previous releases there were a great many of these due to our excessive use of the PHP error operator, @, on accessing possibly undefined variables — we have fixed all of these.
  3. As mentioned in the message above we will have a template editor that prevents errors from being saved.