Technical Documentation

Introduction

JuxtaPage is a PHP-based web framework run atop of the Apache web server. Unlike other web frameworks, JuxtaPage does not allow programmatic generation of HTML code. Instead, whole web pages are constructed from HTML element fragments contained in separate files, e.g., "head.htm", "main.htm", "article.htm", etc. These files are stored in a hierarchical directory structure and any required elements that are missing in a leaf directory are obtained from the first parent directory containing the fragment. A special directory called '_site' is considered the root directory of this hierarchy.

Additionally, JuxtaPage has been designed to allow automated publication of websites that are shared with the JuxtaPage server using Dropbox.

Each website is contained within a directory named in reverse domain-name style. For example, 'juxtapage.org' would be stored in a directory called 'org.juxtapage'. Once the shared directory is accepted and populated into the server Dropbox directory, an Apache configuration file is automatically generated, and a Let's Encrypt certificate is retrieved using certbot.

Below is the file file structure of a typical JuxtaPage based website:

org.juxtapage/_site
org.juxtapage/_site/head.title.htm
org.juxtapage/_site/head.meta.htm
org.juxtapage/_site/head.javascript.htm
org.juxtapage/_site/head.styles.htm
org.juxtapage/_site/header.htm
org.juxtapage/_site/nav_start.htm
org.juxtapage/_site/nav_end.htm
org.juxtapage/_site/main_start.htm
org.juxtapage/_site/article_start.htm
org.juxtapage/_site/footer.htm

org.juxtapage/_index/article.htm

History

JuxtaPage is a rewrite/restructure by the same creators of an earlier framework called DropSpace, see:

DropSpace

Configuration

By default, the 'document root' referred to below is '/srv/JUXTAPAGE/', however, it is automatically detected based on the install location. Please note, if you change the default install location you will need to ensure that Apache is appropriate configured to support that location.

Non-site specific configuration

Non-site specific configuration is specified in the 'configuration/conf.php' file. This file is pre-loaded using the 'php_value auto_prepend_file' directive in the Apache configuration file. The only configuration required is to nominate the 'sites path', which is where webiste directories are stored.

configuration/conf.php
<?php

//
//  The SITES_PATH is usually a symbolic link to the Dropbox directory.
//  This indicates where the collection of website directories will be.
//

define( "SITES_PATH", $_SERVER["DOCUMENT_ROOT"] . "/sites" );

Site specific configuration

Site specific configuration is specified in the Apache configuration file, which is automatically generated when a website directory is added to the SITES_PATH directory.

# Last modified: %now
<VirtualHost *:80>

    ServerAdmin webmaster@%domain_name%
    ServerName            %domain_name%
    ServerAlias       www.%domain_name%

    DocumentRoot %document_root%

    <Directory %document_root%/>
    php_value auto_prepend_file "%document_root%/JuxtaPage/latest/juxtapage/configuration/conf.php"
    </Directory>

    Alias /resources %sites_path%/%domain_dir%/_resources
    <Directory "%sites_path%/%domain_dir%/_resources">
    Require all granted
    </Directory>

    <Directory %sites_path%/%domain_dir%/_resources/>
    php_value open_basedir "%sites_path%/%domain_dir%/_resources/"
    </Directory>

    <Directory %document_root%/>
    Options FollowSymLinks

    RewriteEngine On
    RewriteBase /
    #
    #   By default, no redirect occurs. 
    #   To redirect, for example, example.com to www.example.com,
    #   uncomment the appropriate two lines below:
    #
    #   RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]                   #   Redirect www.example.com
    #   RewriteRule ^(.*)$ http://%1/$1 [R=301,L]                   #   to           example.com
    #
    #   RewriteCond %{HTTP_HOST} !^www\. [NC]                       #   Redirect example.com
    #   RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]     #   to   www.example.com
    #
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule /* %document_root%/JuxtaPage/latest/juxtapage/sbin/index.php

    %allow_ip_behind_load_balancer%

    %allow_ip%

    </Directory>

    ErrorLog /var/log/apache2/JUXTAPAGE.%domain_dir%.log

    # Possible values include: debug, info, notice, warn, error, crit,
    # alert, emerg.
    LogLevel warn

    CustomLog /var/log/apache2/access.log combined

    #ErrorDocument 404 /index.php

</VirtualHost>
<IfModule mod_ssl.c>
    <VirtualHost _default_:443>

    SSLEngine on
    SSLCertificateFile    /etc/apache2/ssl/%wildcard%.pem
    SSLCertificateKeyFile /etc/apache2/ssl/%wildcard%.key

    ServerAdmin webmaster@%domain_name%
    ServerName            %domain_name%
    ServerAlias       www.%domain_name%

    DocumentRoot %document_root%

    <Directory %document_root%/>
    php_value auto_prepend_file "%document_root%/JuxtaPage/latest/juxtapage/configuration/conf.php"
    </Directory>

    Alias /resources %dropbox_dir%/%domain_dir%/_resources
    <Directory "%dropbox_dir%/%domain_dir%/_resources">
    Require all granted
    </Directory>

    <Directory %dropbox_dir%/%domain_dir%/_resources/>
    php_value open_basedir "%dropbox_dir%/%domain_dir%/_resources/"
    </Directory>

    <Directory %document_root%/>
    Options FollowSymLinks

    RewriteEngine On
    RewriteBase /
    #
    #   By default, no redirect occurs. 
    #   To redirect, for example, example.com to www.example.com,
    #   uncomment the appropriate two lines below:
    #
    #   RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]                   #   Redirect www.example.com
    #   RewriteRule ^(.*)$ http://%1/$1 [R=301,L]                   #   to           example.com
    #
    #   RewriteCond %{HTTP_HOST} !^www\. [NC]                       #   Redirect example.com
    #   RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]     #   to   www.example.com
    #
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule /* %document_root%/JuxtaPage/latest/juxtapage/sbin/index.php

        <RequireAll>
            Require %allow_ip%
        </RequireAll>
    </Directory>

    ErrorLog /var/log/apache2/JUXTAPAGE.%domain_dir%.log

    # Possible values include: debug, info, notice, warn, error, crit,
    # alert, emerg.
    LogLevel warn

    CustomLog /var/log/apache2/access.log combined

    #ErrorDocument 404 /index.php

    </VirtualHost>
</IfModule>

Implementation

Includes

<?php

include_once(         "strings.php" );
include_once( "HelperFunctions.php" );
include_once(           "Input.php" );
include_once(          "Access.php" );
include_once(             "Alt.php" );
include_once(            "Page.php" );

Main

The main procedure does the following:

1. Obtains a 'Configuration' object that stores important paths, the website domain name, and the path of the websites content.

function main()
{
    $start = microtime( TRUE );

    if ( array_key_exists( "PHP_AUTH_USER", $_SERVER ) )
    {
        SetCookie( "AuthBasicUser", $_SERVER["PHP_AUTH_USER"] );
    }

    $configuration = new Configuration();

    if ( !is_dir( $configuration->sitesPath ) )
    {
        error_log( "Aborting, could not find sites path: [" . $configuration->sitesPath . "]" );
        exit();
    }

    if ( file_exists( $configuration->settingsPath ) )
    {
        $access = Access::IsGranted( $configuration->settingsPath, $configuration->pageID );
    }
    else
    {
        $access = Access::IsGranted( $configuration->restrictedPath, $configuration->pageID );
    }

    if ( false === $access )
    {
        header( "Location: /" );
    }
    else
    {
        header( "Content-Type: text/html" );
        $keys       = $access->keys;
        $csrf_token = $access->csrf_token;

        $page = new Page
        (
            $configuration->websiteContent,
            $configuration->locale,
            $configuration->pageDir,
            $keys,
            $configuration->websiteName
        );
        $page->render( $configuration, $csrf_token );

        //error_log( $page->toString() );
    }

    $delta = microtime( TRUE ) - $start;
    $ms    = ceil( $delta * 1000000 );

    error_log( "Duration: $ms ms ($delta)" );

    Alt::Log( $configuration->clientAddress, $configuration->domain, $configuration->redirectURL );
}
main();
Configuration
class Configuration {

var $base;                  // "/srv/DROPSPACE/Dropspace/latest/dropspace/sbin/.."
var $commonPath;            // "/srv/DROPSPACE/Dropspace/latest/dropspace/sbin/../share/content"
var $sitesPath;             // "/srv/DROPSPACE/sites"

var $documentRoot;          // "/srv/DROPSPACE"
var $domain;                // "example.com"
var $httpUserAgent;         // "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15"

var $redirectURL;           // "/"

var $pageID;                // "index"
var $pageDir;               // "_index"
var $pagePath;              // "/"

var $browser;               // "WEBKIT"
var $isMobile;              // ""

var $websiteName;           // "com.example"
var $websitePath;           // "/srv/DROPSPACE/sites/com.example"
var $websiteContent;        // "/srv/DROPSPACE/sites/com.example/_content"
var $settingsPath;          // "/srv/DROPSPACE/sites/com.example/_content/_site/_SETTINGS.json.txt"
var $restrictedPath;        // "/srv/DROPSPACE/sites/com.example/_content/_site/_RESTRICTED.json.txt"

var $request;               // htmlentity filtered dictionary of $_REQUEST parameters 
var $accessID;              // filtered value from $_COOKIE["accessid"]
var $remoteAddress;         // 192.168.0.1
var $forwardedFor;          // 10.0.0.1
var $clientAddress;         // 10.0.0.1

function __construct()
{
    $this->locale         = self::DetermineLocale();
    $this->base           = __DIR__ . "/..";
    $this->commonPath     = $this->base . "/share/content";
    $this->sitesPath      = getenv( "SITES_PATH" ) ? getenv( "SITES_PATH" ) : SITES_PATH;

    $this->documentRoot   = Input::Filter( array_get( $_SERVER, "DOCUMENT_ROOT"   ) );
    $this->domain         = Input::Filter( array_get( $_SERVER, "SERVER_NAME"     ) );
    $this->httpUserAgent  = Input::Filter( array_get( $_SERVER, "HTTP_USER_AGENT" ) );

    $this->redirectURL    = CanonicalisePath( Input::Filter( array_get( $_SERVER, "REDIRECT_URL" ) ) );

    $this->pageID         = GeneratePageID  ( $this->redirectURL );
    $this->pageDir        = GeneratePageDir ( $this->redirectURL );
    $this->pagePath       = GeneratePagePath( $this->redirectURL );

    $this->browser        = DetermineBrowser ( $this->httpUserAgent );
    $this->isMobile       = DetermineIfMobile( $this->httpUserAgent );

    $this->websiteName    = DetermineSitenameReversed( $this->domain, $this->sitesPath );
    $this->websitePath    = $this->sitesPath . "/" . $this->websiteName;
    $this->websiteContent = self::DetermineWebsiteContentPath( $this->websitePath );
    $this->settingsPath   = $this->websiteContent . "/_site/_SETTINGS.json.txt";
    $this->restrictedPath = $this->websiteContent . "/_site/_RESTRICTED.json.txt";

    $this->request        = Input::FilterInput( $_REQUEST, new Output() );
    $this->accessID       = Input::Filter( array_get( $_COOKIE, "accessid" ) );
    $this->remoteAddress  = $_SERVER['REMOTE_ADDR'];
    $this->forwardedFor   = array_key_exists( "HTTP_X_FORWARDED_FOR", $_SERVER ) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : "";

    $this->clientAddress  = $this->forwardedFor ? $this->forwardedFor : $this->remoteAddress;
}
static function DetermineLocale()
{
    $locale = Input::Filter( array_get( $_COOKIE, "locale" ) );

    if ( ! $locale ) $locale = "default";

    return $locale;
}
static function DetermineWebsiteContentPath( $websitePath )
{
    if ( string_has_suffix( $websitePath, ".test" ) )
    {
        $websitePath = substr( $websitePath, 0, -5 );
    }

    if ( is_dir( $websitePath . "/_content" ) )
    {
        return $websitePath . "/_content";
    }
    else
    if ( is_dir( $websitePath . "/content" ) )
    {
        return $websitePath . "/content";
    }
    else
    if ( is_dir( $websitePath . "/source/content" ) )
    {
        return $websitePath . "/source/content";
    }
}
}

Access

class Access {
static function IsGranted( $restricted_path, $pageid )
{
    if ( ! self::IsAccessDenied( $restricted_path, $pageid ) )
    {
        if ( !defined( "CSRF_TOKEN" ) ) define( "CSRF_TOKEN", "" );
        if ( !defined( "KEYS"       ) ) define( "KEYS",       "" );

        return (object) array
        (
            "csrf_token" => CSRF_TOKEN,
            "keys"       => KEYS
        );
    }
    else
    {
        return false;
    }
}

static function IsAccessDenied( $restricted_path, $pageid )
{
    $is_denied  = false;

    //
    //  Only denies access if "RESTRICTED" file is present in _site dir.
    //

    if ( file_exists( $restricted_path ) )
    {
        $server_name = $_SERVER["SERVER_NAME"];
        $apihost     = "";
        $apikey      = "";
        $list        = null;

        $json = file_get_contents( $restricted_path );
        $json = preg_replace( '/\s+/', '', $json );
        $prov = json_decode( $json );

        if ( is_array( $prov ) )
        {
            $apihost = self::GenerateAPIHostname( $server_name ); 
            $list    = $prov;
        }
        else
        if ( $prov->apihost && $prov->directories )
        {
            $apihost = str_replace( "[SERVER_NAME]", $server_name, $prov->apihost );
            $list    = $prov->directories;

            if ( isset( $prov->{'apikey'} ) )
            {
                $apikey  = $prov->apikey;
            }
            else
            if ( isset( $prov->{'apikeys'} ) )
            {
                $apikeys = $prov->apikeys;

                if ( is_array( $apikeys ) )
                {
                    foreach( $apikeys as $obj )
                    {
                        if ( $server_name == $obj->server_name )
                        {
                            if ( property_exists( $obj, "apikey" )  && ("" != trim( $obj->apikey  )) )
                            {
                                $apikey = $obj->apikey;
                            }

                            if ( property_exists( $obj, "apihost" ) && ("" != trim( $obj->apihost )) )
                            {
                                $apihost = $obj->apihost;
                            }
                            break;
                        }
                    }
                }
            }
        }

        if( ! $list || ! is_array( $list ) )
        {
            $is_denied = true;
            error_log( "ACCESS DENIED: Invalid Configuration: " . $restricted );
        }
        else
        {
            foreach ( $list as $prefix )
            {
                if ( string_has_prefix( $pageid, $prefix ) )
                {
                    error_log( "Checking: $prefix" );

                    $is_denied = self::VerifyAccessID( $apihost, $apikey );
                    break;
                }
            }
        }

        if ( ! $is_denied )
        {
            if ( !defined( "CSRF_TOKEN") && $apihost && $apikey )
            {
                $is_denied = self::RetrieveAndDefineCSRFToken( $apihost, $apikey );
            }

            error_log( "ACCESS PERMITTED" );
        }
    }

    return $is_denied;
}

static function VerifyAccessID( $apihost, $apikey )
{
    $is_denied = true;
    $status    = "";

    if ( ! array_key_exists( "accessid", $_COOKIE ) )
    {
        $status = "Missing Access ID";
    }
    else
    {
        $accessid = $_COOKIE["accessid"];

        if ( 0 == strlen( $accessid ) )
        {
            $status = "Empty Access ID";
        }
        else
        if ( $is_denied = self::IsInvalidAccessID( $apihost, $apikey, $accessid ) )
        {
            $status = "Invalid";
        }
    }

    if ( $is_denied )
    {
        error_log( "ACCESS DENIED: " . $status );
    }
    return $is_denied;
}

function SetCookie( $key, $value )
{
    $is_local = string_has_suffix( $_SERVER['SERVER_NAME'], '.test' );

    $cookie = "Set-Cookie: sid=";

    if ( $is_local )
    {
        // SameSite is causing issues with local API server
        // that uses self-signed certificates.

        $cookie = $cookie . "; path=/; HttpOnly;secure";
    }
    else
    {
        $cookie = $cookie . "; path=/; HttpOnly;secure;SameSite=strict";
    }
        
    header( $cookie );
}

static function IsInvalidAccessID( $apihost, $apikey, $accessid )
{
    $is_invalid   = true;
    $server_name  = 
    $accessid     = urlencode( $accessid );

    $parameters[] = "apikey="      . $apikey;
    $parameters[] = "server_name=" . $_SERVER["SERVER_NAME"];
    $parameters[] = "remote_ip="   . $_SERVER["REMOTE_ADDR"];
    $parameters[] = "Aid="         . $accessid;

    $context      = self::CreateStreamContext( $apihost, join( "&", $parameters ) );

    error_log( "Authenticating using: $apihost ($accessid)" );

    $json     = null;
    $attempts = 5;

    do
    {
        $json = @file_get_contents( $apihost . "/auth/access/", false, $context );

        if ( FALSE === $json )
        {
            $error = error_get_last();

            error_log( "Could not connect to API host: $apihost ($attempts) -" . $error['message'] );

            sleep( 1 );
        }
    }
    while ( (FALSE === $json) && ($attempts-- > 0) );

    if ( FALSE !== $json )
    {
        $obj = json_decode( $json );

        if ( "OK" == $obj->status )
        {
            if ( 1 == count( $obj->results ) )
            {
                if ( "PERMITTED" == $obj->results[0]->access )
                {
                    $is_invalid = false;

                    define( "CSRF_TOKEN", $obj->results[0]->CSRF_Token  );
                    define( "KEYS",       $obj->results[0]->Access_Keys );
                }
            }
        }
        if ( $is_invalid ) error_log( $json );
    }

    return $is_invalid;
}

static function RetrieveAndDefineCSRFToken( $apihost, $apikey )
{
    $is_denied = true;

    $accessid = array_key_exists( "accessid", $_COOKIE ) ? $_COOKIE["accessid"] : "";

    $parameters[] = "apikey="      . $apikey;
    $parameters[] = "server_name=" . $_SERVER["SERVER_NAME"];
    $parameters[] = "remote_ip="   . $_SERVER["REMOTE_ADDR"];
    $parameters[] = "Aid="         . $accessid;

    $context      = self::CreateStreamContext( $apihost, join( "&", $parameters ) );

    error_log( "Authenticating using: " . $apihost );

    $json = @file_get_contents( $apihost . "/auth/access/", false, $context );

    if ( FALSE === $json )
    {
        $error = error_get_last();

        error_log( "Could not connect to API host: $apihost - " . $error['message'] );
    }
    else
    {
        $obj = json_decode( $json );

        if ( $obj && ("OK" == $obj->status) )
        {
            if ( 1 == count( $obj->results ) )
            {
                define( "CSRF_TOKEN", $obj->results[0]->CSRF_Token  );
                define( "KEYS",       $obj->results[0]->Access_Keys );
                
                $is_denied = false;
            }
        }
    }
    
    return $is_denied;
}

static function GenerateAPIHostname( $hostname )
{
    $api_hostname = "api-" . $hostname;

    return ("" !== $_SERVER["HTTPS"]) ? "https://" . $api_hostname : "http://" . $api_hostname;
}   

/*
 *  Copied from BaseSchema Process Places script with permission.
 *  Copyright 2018 Daniel Bradley
 */
static function CreateStreamContext( $hostname, $content )
{
    $verify_server = (false == self::IsLocalServer( $hostname ));

    $NL = "\r\n";

    $content_type   = "Content-type: application/x-www-form-urlencoded" . $NL;
    $content_length = "Content-Length: " . strlen( $content )           . $NL;

    return stream_context_create
    (
        array
        (
            "http" => array
            (
                "method"        => "POST",
                "header"        => $content_type . $content_length,
                "content"       => $content,
                "ignore_errors" => true,
                "timeout"       => (float)2.0,
            ),

            "ssl" => array
            (
                "allow_self_signed" => !$verify_server,
                "verify_peer"       =>  $verify_server,
                "verify_peer_name"  =>  $verify_server,
            ),
        )
    );
}

static function IsLocalServer( $hostname )
{
    return
        string_contains  ( $hostname, ".local:" )
    ||  string_has_suffix( $hostname, ".local"  )
    ||  string_contains  ( $hostname, ".test:"  )
    ||  string_has_suffix( $hostname, ".test"   );
}
}

Page

<?php

class Page {

var $website_content;
var $locale;

var $document;                              //  document.htm

var $html_start;                            //  html_start.htm

var $head;                                  //  head.htm
var $head_title;                            //  head.title.htm
var $head_meta;                             //  head.meta.htm
var $head_csp;                              //  head.csp.htm
var $head_links   = array();                //  head.link.htm
var $head_scripts = array();                //  head.script.htm

var $body;                                  //  body.htm
var $body_header;                           //  body.header.htm
var $body_main;                             //  body.main.htm;
var $body_main_start;                       //  body.main_start.htm;
var $body_main_header;                      //  body.main.header.htm;
var $body_main_article;                     //  body.main.article.htm;
var $body_main_article_start;               //  body.main.article_start.htm;
var $body_main_article_sections = array();  //  body.main.article_start.htm;
var $body_main_aside;                       //  body.main.aside.htm;
var $body_main_footer;                      //  body.main.footer.htm;
var $body_footer;                           //  body.footer.htm;

var $body_navs = array();                   //  body.navs
var $body_nav_start;                        //  body.nav_start.htm;
var $body_nav_end;                          //  body.nav_end.htm;

var $body_nav_breadcrumbs;                  //  body.nav.breadcrumbs.htm;

var $body_menu;                             //  body.menu.htm;
var $body_menu_start;                       //  body.menu_start.htm;
var $body_menus = array();                  //  body.menu{1,2,3,4,5}.htm;
var $body_menu_end;                         //  body.menu_end.htm;

var $body_dialogs;                          //  body.dialogs.htm;
function __construct( $website_content, $locale, $page_dir, $keys, $website_name )
{
    $this->website_content = $website_content;
    $this->locale          = $locale;

    $this->document = self::ResolveFile( $website_content, $page_dir, "document", $keys, $website_name );
    if ( !$this->document )
    {
        $this->html_start = self::ResolveFile( $website_content, $page_dir, "html_start", $keys, $website_name );

        $this->head = self::ResolveFile( $website_content, $page_dir, "head", $keys, $website_name );
        if ( !$this->head )
        {
            $this->head_title  = self::ResolveFile( $website_content, $page_dir, "head.title",  $keys, $website_name );
            $this->head_meta   = self::ResolveFile( $website_content, $page_dir, "head.meta",   $keys, $website_name );
            $this->head_csp    = self::ResolveFile( $website_content, $page_dir, "head.csp",    $keys, $website_name );
            
            if (    ($path = self::ResolveFile( $website_content, $page_dir, "head.link", $keys, $website_name )) )
            {
                $this->head_links[] = $path;
            }

            $i = 1;
            while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.link" . $i . "", $keys, $website_name )) )
            {
                $i++;
                $this->head_links[] = $path;
            }

            if (    ($path = self::ResolveFile( $website_content, $page_dir, "head.styles", $keys, $website_name )) )
            {
                $this->head_links[] = $path;
            }

            $i = 1;
            while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.styles" . $i . "", $keys, $website_name )) )
            {
                $i++;
                $this->head_links[] = $path;
            }

            if (    ($path = self::ResolveFile( $website_content, $page_dir, "head.script", $keys, $website_name )) )
            {
                $this->head_scripts[] = $path;
            }

            $i = 1;
            while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.script" . $i . "", $keys, $website_name )) )
            {
                $i++;
                $this->head_scripts[] = $path;
            }

            if (    ($path = self::ResolveFile( $website_content, $page_dir, "head.javascript", $keys, $website_name )) )
            {
                $this->head_scripts[] = $path;
            }

            $i = 1;
            while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.javascript" . $i . "", $keys, $website_name )) )
            {
                $i++;
                $this->head_scripts[] = $path;
            }
        }

        $this->body = self::ResolveFile( $website_content, $page_dir, "body", $keys, $website_name );
        if ( !$this->body )
        {
            $this->body_header = self::ResolveFile( $website_content, $page_dir, "body.header", $keys, $website_name );
            $this->body_nav    = self::ResolveFile( $website_content, $page_dir, "body.nav",    $keys, $website_name );
            if ( !$this->body_nav )
            {
                $this->body_nav_start  = self::ResolveFile( $website_content, $page_dir, "body.nav_start",  $keys, $website_name );
                $this->body_menu_start = self::ResolveFile( $website_content, $page_dir, "body.menu_start", $keys, $website_name );

                if ( $this->body_nav_start )
                {
                    $this->body_nav_breadcrumbs = self::ResolveFile( $website_content, $page_dir, "body.nav.breadcrumbs", $keys, $website_name );

                    $i = 1;
                    while ( ($nav_line = self::ResolveFile( $website_content, $page_dir, "body.nav" . $i . "", $keys, $website_name )) )
                    {
                        $i++;
                        $this->body_navs[] = $nav_line;
                    }

                    if ( !$this->body_menu_start && empty( $this->body_navs ) )
                    {
                        $i = 1;
                        while ( ($nav_line = self::ResolveFile( $website_content, $page_dir, "body.menu" . $i . "", $keys, $website_name )) )
                        {
                            $i++;
                            $this->body_navs[] = $nav_line;
                        }
                    }

                    $this->body_nav_end = self::ResolveFile( $website_content, $page_dir, "body.nav_end",         $keys, $website_name );
                }
            }

            $this->body_main   = self::ResolveFile( $website_content, $page_dir, "body.main", $keys, $website_name, true );
            if ( !$this->body_main )
            {
                $this->body_main_start   = self::ResolveFile( $website_content, $page_dir, "body.main_start",   $keys, $website_name, true );
                $this->body_main_header  = self::ResolveFile( $website_content, $page_dir, "body.main_header",  $keys, $website_name, true );
                $this->body_main_article = self::ResolveFile( $website_content, $page_dir, "body.main.article", $keys, $website_name, true );
                if ( !$this->body_main_article )
                {
                    $this->body_main_article_start = self::ResolveFile( $website_content, $page_dir, "body.main.article_start", $keys, $website_name, true );
                    $i = 1;
                    while ( ($section = self::ResolveFile( $website_content, $page_dir, "body.main.article.section" . $i, $keys, $website_name, true )) )
                    {
                        $i++;
                        $this->body_main_article_sections[] = $section;
                    }
                }
                $this->body_main_aside   = self::ResolveFile( $website_content, $page_dir, "body.main.aside",   $keys, $website_name, true );
                $this->body_main_footer  = self::ResolveFile( $website_content, $page_dir, "body.main_footer",  $keys, $website_name, true );
            }
            $this->body_footer = self::ResolveFile( $website_content, $page_dir, "body.footer", $keys, $website_name, true );
        }

        $this->body_menu    = self::ResolveFile( $website_content, $page_dir, "body.menu",    $keys, $website_name );

        if ( !$this->body_menu )
        {
            $this->body_menu_start = self::ResolveFile( $website_content, $page_dir, "body.menu_start", $keys, $website_name );

            if ( $this->body_menu_start )
            {
                $i = 1;
                while ( ($menu_line = self::ResolveFile( $website_content, $page_dir, "body.menu" . $i . "", $keys, $website_name )) )
                {
                    $i++;
                    $this->body_menus[] = $menu_line;
                }
                $this->body_menu_end = self::ResolveFile( $website_content, $page_dir, "body.menu_end", $keys, $website_name );
            }
        }            

        $this->body_dialogs = self::ResolveFile( $website_content, $page_dir, "body.dialogs", $keys, $website_name );
    }
}

ResolveFile

Resolve file will determine the appropriate file that is storing a html element fragment. For example, the following call will search for fileds in the following order:

ResolveFile
(
    "/srv/DROPSPACE/sites/com.example/_content",
    "dashboard-settings",
    "body.main.article",
    array( "CLIENT", "COMPANY" ),
    "com.example"
);
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT-COMPANY@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT-COMPANY.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article.htm

/srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT-COMPANY@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/article@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT-COMPANY.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/article.htm

/srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT-COMPANY@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT-COMPANY.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article.htm

/srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT-COMPANY@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/article@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT-COMPANY.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT.htm
/srv/DROPSPACE/sites/com.example/_content/dashboard/article.htm

/srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT-COMPANY@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/_site/body.main.article@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT-COMPANY.htm
/srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT.htm
/srv/DROPSPACE/sites/com.example/_content/_site/body.main.article.htm

/srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT-COMPANY@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/_site/article@com.example.htm
/srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT-COMPANY.htm
/srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT.htm
/srv/DROPSPACE/sites/com.example/_content/_site/article.htm
static function ResolveFile( $website_content, $pageDir, $element, $keys, $website_name, $one_level = false )
{
    $file  = null;
    $files = self::GenerateProvisionalFileList( $website_content, $pageDir, $element, $keys, $website_name, $one_level );

    foreach( $files as $filepath )
    {
        if ( file_exists( $filepath ) )
        {
            $file = $filepath;
            break;
        }

        if ( "body.main.article_start" == $element )
        {
            $yes = $file ? "Yes" : "No ";

            error_log( "$element option: $yes - $filepath" );
        }
    }
    return $file;
}

Generate Provisional File List

Each file can contain the following components:

-]@].htm

static function GenerateProvisionalFileList( $website_content, $pageDir, $element, $keys, $website_name, $one_level )
{
    if ( "body.main_start" == $element )
    {
        error_log( "GenerateProvisionalFileList( '$website_content', '$pageDir', '$element', '$keys', '$website_name', '$one_level' )" );
    }

    $files = array();

    $array_paths = explode( '-', $pageDir );

    while( true )
    {
        if ( !empty( $array_paths ) )
        {
            $p = trim( implode( '-', $array_paths ) );
        }
        else
        {
            $p = "_site";
        }

        $files = array_merge( $files, self::GenerateProvisionalFileListSubset( "$website_content/$p", $element, $keys, "@" . $website_name ) );
        $files = array_merge( $files, self::GenerateProvisionalFileListSubset( "$website_content/$p", $element, $keys                      ) );


        if ( ("_site" == $p) || $one_level )
        {
            break;
        }
        else
        {
            $array_paths = array_slice( $array_paths, 0, -1 );
        }
    }

    return $files;
}

Given:

Returns:

  1. dashboard-clients-client/body.main.article_start-CLIENT-company@example.com.htm
  2. dashboard-clients-client/main.article_start-CLIENT-company@example.com.htm
  3. dashboard-clients-client/article_start-CLIENT-company@example.com.htm
  1. dashboard-clients-client/body.main.article_start-CLIENT@example.com.htm
  2. dashboard-clients-client/main.article_start-CLIENT@example.com.htm
  3. dashboard-clients-client/article_start-CLIENT@example.com.htm
  1. dashboard-clients-client/article_start@example.com.htm
static function GenerateProvisionalFileListSubset( $path, $element, $keys, $website_name = "" )
{
    $files = array();

    $array_keys   = explode( '-', $keys );                          // [ 'CLIENT', 'company'          ]    
    $array_keys[] = "dummy";                                        // [ 'CLIENT', 'company', 'dummy' ]    

    do
    {
        $array_keys = array_slice( $array_keys, 0, -1 );            // [ 'CLIENT', 'company' ]    

        $array_elements = explode( '.', $element );                 // [ 'body', 'main', 'article_start' ]
 
        while( ! empty( $array_elements ) )
        {
            $e = implode( '.', $array_elements );                   //  "body.main.article_start"
            $k = implode( '-', $array_keys     );                   //  "CLIENT-company"

            if ( $k )
            {
                $f = $path . "/" . $e . "-" . $k . $website_name . ".htm";
            }
            else
            {
                $f = $path . "/" . $e . $website_name . ".htm";
            }

            $files[] = $f;

            $array_elements = array_slice( $array_elements, 1 );    // [ 'main', 'article_start' ]
        }
    }
    while( ! empty( $array_keys ) );

    return $files;
}
function FileGetContentsIfExists( $filename, $translate = false )
{
    $locale  = $this->locale ? $this->locale : "default";
    $content = ($filename && file_exists( $filename )) ? file_get_contents( $filename ) : "";

    if ( $translate && $content )
    {
        if ( $locale = $this->RetrieveLocaleJSON( $locale ) )
        {
            $doc = new DOMDocument();
            $doc->loadHTML( $content, LIBXML_HTML_NODEFDTD );

            self::ProcessDOMDocument( $doc, $locale, "a"      );
            self::ProcessDOMDocument( $doc, $locale, "button" );
            self::ProcessDOMDocument( $doc, $locale, "h1"     );
            self::ProcessDOMDocument( $doc, $locale, "span"   );
            self::ProcessDOMDocument( $doc, $locale, "th"     );
            self::ProcessDOMDocument( $doc, $locale, "b"      );
            self::ProcessDOMDocument( $doc, $locale, "i"      );

            // Replace placeholder text.
            self::ProcessDOMDocument( $doc, $locale, "input"  );

            $content = $doc->saveHTML();
        }
    }

    return $content;
}
function RetrieveLocaleJSON( $locale )
{
    $obj = null;

    if ( "default" != $locale )
    {
        $json = file_get_contents( $this->website_content . "/_i18n/$locale.json" );
        try
        {
            $obj  = json_decode( $json );
        }
        catch ( Exception $ex )
        {
            // Ignore exception.
        }
    }

    return $obj;
}
static function ProcessDOMDocument( $dom, $locale, $tag )
{
    $nodes = $dom->getElementsByTagName( $tag );

    foreach ( $nodes as $node )
    {
        $key = trim( $node->nodeValue );

        if ( property_exists( $locale, $key ) )
        {
            if ( $value = $locale->{$key} )
            {
                $node->nodeValue = $value;

                error_log( "Changing $key for $value" );
            }
        }

        if ( $node->hasAttributes() )
        {
            $attributes = $node->attributes;
            $pNode      = $attributes->getNamedItem( "placeholder" );

            if ( $pNode )
            {
                $pKey = trim( $pNode->nodeValue );

                if ( property_exists( $locale, $pKey ) )
                {
                    if ( $pValue = $locale->{$pKey} )
                    {
                        $pNode->nodeValue = $pValue;
                    }
                }
            }
        }
    }
}
function render( $configuration, $csrf_token )
{
    if ( $this->document )
    {
        echo trim( self::FileGetContentsIfExists( $this->document ) );
    }
    else
    {
        $this->headers();
        $this->doctype();
        $this->html_start();
        $this->head();
        $this->debug( $configuration, $csrf_token );
        $this->body ( $configuration, $csrf_token );
        $this->html_end();
    }
}
function headers()
{
}
function doctype()
{
    echo "<!DOCTYPE html>";
}
function html_start()
{
    if ( $this->html_start )
    {
        echo "\n" . trim( self::FileGetContentsIfExists( $this->html_start ) );
    }
    else
    {
        echo "\n" . "<html>";
    }
}
function head()
{
    if ( $this->head )
    {
        echo "\n" . trim( self::FileGetContentsIfExists( $this->head ) );
    }
    else
    {
        echo "\n" . "<head>";

        if ( $this->head_title )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->head_title ) );
        }

        if ( $this->head_meta )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->head_meta ) );
        }

        if ( $this->head_csp )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->head_csp ) );
        }

        foreach ( $this->head_links as $link_line )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $link_line, true ) );
        }

        foreach ( $this->head_scripts as $script_line )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $script_line, true ) );
        }

        echo "\n" . "</head>";
    }
}
function debug( $configuration, $csrf_token )
{
    if ( "TRUE" == getenv( "SHOW_DEBUG" ) )
    {
        echo "\n" . "<!--";

        echo "\n" . "CSRF_TOKEN: " . $csrf_token;
        echo $configuration->toString();
        echo $this->toString();

        echo "\n" . "-->";
    }
}
function body( $configuration, $csrf_token )
{
    if ( $this->body )
    {
        echo "\n" . trim( self::FileGetContentsIfExists( $this->body ) );
    }
    else
    {
        echo "\n" . self::generateBodyStartTag( $configuration, $csrf_token );

        if ( $this->body_main )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main, true ) );
        }
        else
        {
            if ( $this->body_main_start )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_start ) );
            }
            else
            {
                echo "\n" . "<main>";
            }

            if ( $this->body_main_header )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_header, true ) );
            }

            if ( $this->body_main_aside )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_aside, true ) );
            }

            if ( $this->body_main_article )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_article, true ) );
            }
            else
            if ( $this->body_main_article_start )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_article_start, true ) );

                foreach ( $this->body_main_article_sections as $section )
                {
                    echo "\n" . trim( self::FileGetContentsIfExists( $section, true ) );
                }
                echo "\n" . "</article>";
            }

            if ( $this->body_main_footer )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_footer, true ) );
            }
            echo "\n" . "</main>";
        }

        if ( $this->body_footer )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->body_footer, true ) );
        }

        if ( $this->body_nav )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav, true ) );
        }
        else
        {
            $n = count( $this->body_navs );
            if ( 0 == $n )
            {
                if ( $this->body_nav_start )
                {
                    echo "\n" . trim( str_replace( '%nav_id%', 'nav1', self::FileGetContentsIfExists( $this->body_nav_start ) ) );
                }
                else
                {
                    echo "\n" . str_replace( '%nav_id%', 'nav1', "<nav id='%nav_id%'>" );
                }

                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_breadcrumbs, true ) );

                if ( $this->body_nav_end )
                {
                    echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_end ) );
                }
                else
                {
                    echo "\n" . "</nav>";
                }
            }
            else
            {
                $i = 0;
                foreach ( $this->body_navs as $nav_line )
                {
                    $i++;

                    $nav_id = "nav$i";

                    if ( $this->body_nav_start )
                    {
                        echo "\n" . trim( str_replace( '%nav_id%', $nav_id, self::FileGetContentsIfExists( $this->body_nav_start ) ) );
                    }
                    else
                    {
                        echo "\n" . str_replace( '%nav_id%', $nav_id, "<nav id='%nav_id%'>" );
                    }

                    if ( $i == $n )
                    {
                        echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_breadcrumbs, true ) );
                    }

                    echo "\n" . trim( self::FileGetContentsIfExists( $nav_line, true ) );

                    if ( $this->body_nav_end )
                    {
                        echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_end ) );
                    }
                    else
                    {
                        echo "\n" . "</nav>";
                    }
                }
            }
        }

        if ( $this->body_header )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->body_header, true ) );
        }

        if ( $this->body_menu )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->body_menu, true ) );
        }
        else
        {
            if ( $this->body_menu_start )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_menu_start, true ) );
            }

            foreach ( $this->body_menus as $menu_line )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $menu_line, true ) );
            }

            if ( $this->body_menu_end )
            {
                echo "\n" . trim( self::FileGetContentsIfExists( $this->body_menu_end, true ) );
            }
        }

        if ( $this->body_dialogs )
        {
            echo "\n" . trim( self::FileGetContentsIfExists( $this->body_dialogs, true ) );
        }

        echo "\n" . "</body>";
    }
}
function generateBodyStartTag( $configuration, $csrf_token )
{
    $bits = array
    (
        "<body",
        "id='"            . $configuration->pageID  . "'",
        "class='"         . $configuration->browser . "'",
        "data-hostname='" . $configuration->domain  . "'",
        "data-csrf='"     . $csrf_token             . "'",
        ">"
    );

    return implode( ' ', $bits );
}
function html_end()
{
    echo "\n" . "</html>";
}
}

ObjectQueue

<?php
//  Copyright (c) 2022, Daniel Robert Bradley. All rights reserved.
//  This software is distributed under the terms of the GNU Lesser General Public License version 2.1
?>
<?php
class Alt {
static function Log( $address_value, $website_domain, $website_page_path )
{
    $apihost = defined( "ALTLOG_APIHOST" ) ? ALTLOG_APIHOST : Array_Get( $_SERVER, "ALTLOG_APIHOST" );
    $apikey  = defined( "ALTLOG_APIKEY"  ) ? ALTLOG_APIKEY  : Array_Get( $_SERVER, "ALTLOG_APIKEY"  );

    if ( $apihost && $apikey )
    {
        $endpoint   = $apihost . "/api/log/";
        $parameters = "address_value=$address_value"
                    . "&"
                    . "website_domain=$website_domain"
                    . "&"
                    . "website_page_path=$website_page_path"
                    . "&"
                    . "apikey=" . $apikey;

        $context = Access::CreateStreamContext( $apihost, $parameters );

        if ( false === file_get_contents( $endpoint, false, $context ) )
        {
            error_log( "Error while logging to: $apihost" );
        }
        else
        {
            error_log( "Log: $address_value => $website_domain$website_page_path" );
        }
    }
}
}

A -- Strings

<?php
function array_get( $array, $key )
{
    return array_key_exists( $key, $array ) ? $array[$key] : "";
}
function string_contains( $haystack, $needle )
{
    return (0 == strlen($needle)) || (false !== strpos( $haystack, $needle ));
}
function string_has_prefix( $haystack, $needle )
{
    return (0 == strlen($needle)) || (0 === strpos( $haystack, $needle ));
}
function string_has_suffix( $haystack, $needle )
{
    $expected = strlen( $haystack ) - strlen( $needle );

    return (0 == strlen($needle)) || ($expected === strrpos( $haystack, $needle ));
}
function test()
{
    if (    string_contains( "www.example.com", ""                ) ) echo "Passed #1" . "\n";
    if (    string_contains( "www.example.com", "www."            ) ) echo "Passed #2" . "\n";
    if (    string_contains( "www.example.com",    ".example."    ) ) echo "Passed #3" . "\n";
    if (    string_contains( "www.example.com",            ".com" ) ) echo "Passed #4" . "\n";
    if (   !string_contains( "www.example.com", "wwl"             ) ) echo "Passed #5" . "\n";
    if (   !string_contains( "www.example.com",    ".exanple"     ) ) echo "Passed #6" . "\n";
    if (   !string_contains( "www.example.com",            ".con" ) ) echo "Passed #7" . "\n";

    if (  string_has_prefix( "www.example.com", ""                ) ) echo "Passed #8" . "\n";
    if (  string_has_prefix( "www.example.com", "www."            ) ) echo "Passed #9" . "\n";
    if (  string_has_prefix( "www.example.com", "www.example."    ) ) echo "Passed #A" . "\n";
    if (  string_has_prefix( "www.example.com", "www.example.com" ) ) echo "Passed #B" . "\n";
    if ( !string_has_prefix( "www.example.com", "wwl"             ) ) echo "Passed #C" . "\n";
    if ( !string_has_prefix( "www.example.com", "www.exanple"     ) ) echo "Passed #D" . "\n";
    if ( !string_has_prefix( "www.example.com", "www.example.con" ) ) echo "Passed #E" . "\n";

    if (  string_has_suffix( "www.example.com", ""                ) ) echo "Passed #F" . "\n";
    if (  string_has_suffix( "www.example.com",            ".com" ) ) echo "Passed #G" . "\n";
    if (  string_has_suffix( "www.example.com",    ".example.com" ) ) echo "Passed #H" . "\n";
    if (  string_has_suffix( "www.example.com", "www.example.com" ) ) echo "Passed #I" . "\n";
    if ( !string_has_suffix( "www.example.com",            ".con" ) ) echo "Passed #J" . "\n";
    if ( !string_has_suffix( "www.example.com",    ".example.con" ) ) echo "Passed #K" . "\n";
    if ( !string_has_suffix( "www.example.com", "www.example.con" ) ) echo "Passed #L" . "\n";
}
CanonicalisePath
<?php

function CanonicalisePath( $path )
{
        $path = Input::Filter( $path );

        if ( '.php' == substr( $path, -4 ) )
        {
            $path = dirname( $path );
        }

        if ( '/' != substr( $path, -1 ) )
        {
            $path .= "/";
        }

        return $path;
}
function IsHTTPS()
{
    return ( !empty( $_SERVER['HTTPS'] ) && ($_SERVER['HTTPS'] !== 'off') );
}
/**************************************************************************
 *  Below here are private helper methods.
 **************************************************************************/

/*
 *  Converts uri to form 'page-subpage-index', used to uniquely identify pages.
 */
function _generatePageId( $append_index = true )
{
    $uri = REDIRECT_URL;
    $uri = substr( $uri, 1 );
    $uri = str_replace( "/", "-", $uri );

    if ( $append_index )
    {
        $uri = $uri . "index";
    }
    else
    if ( "" == $uri )
    {
        $uri = "index";
    }
    else
    {
        $uri = substr( $uri, 0, -1 );
    }
    
    return $uri;

    //$id  = (0 == stripos( $uri, "/page/" )) ? $uri : substr( $uri, stripos( $uri, "/page/" ) + 5 );

    //$page_id = substr( $uri, 1 );
    //return str_replace( "/", "-", $uri );
}

function GeneratePageId( $uri )
{
    $uri = substr( $uri, 1 );
    $uri = str_replace( "/", "-", $uri );
    $uri = $uri . "index";
    
    return $uri;

    //$id  = (0 == stripos( $uri, "/page/" )) ? $uri : substr( $uri, stripos( $uri, "/page/" ) + 5 );

    //$page_id = substr( $uri, 1 );
    //return str_replace( "/", "-", $uri );
}

function GeneratePageDir( $uri )
{
    $id  = GeneratePageId( $uri );
    $dir = "";

    if ( string_has_suffix( $id, "-index" ) )
    {
        $dir = str_replace( "-index", " ", $id );
    }
    else
    if ( "index" == $id )
    {
        $dir = "_index";
    }

    return $dir;
}

/*
 *  Converts each element of uri to Title Case e.g. 'Page/Subpage'.
 */
function GeneratePagePath( $redirect_url )
{
    $path = "";
    {
        $uri  = substr( $redirect_url, 1, -1 );
    
        $bits = explode( "/", $uri );
        foreach ( $bits as $bit )
        {
            $path .= "/" . ToTitleCase( $bit );
        }
    }
    return $path;
}
    
    function ToTitleCase( $string )
    {
        $ret = "";
    
        $bits = explode( "_", $string );
        foreach ( $bits as $bit )
        {
            if ( !empty( $bit ) ) $ret .= " " . strtoupper( $bit[0] ) . substr( $bit, 1 );
        }
        return substr( $ret, 1 );
    }

function determineIfMobile( $http_user_agent )
{
    //  See:
    //  http://stackoverflow.com/questions/6636306/mobile-browser-detection

    return (bool)preg_match('#\b(ip(hone|od)|android\b.+\bmobile|opera m(ob|in)i|windows (phone|ce)|blackberry'.
                '|s(ymbian|eries60|amsung)|p(alm|rofile/midp|laystation portable)|nokia|fennec|htc[\-_]'.
                '|up\.browser|[1-4][0-9]{2}x[1-4][0-9]{2})\b#i', $http_user_agent );
}

function determineBrowser( $http_user_agent )
{
    $type = "XXX";
    if ( string_contains( $http_user_agent, "Trident" ) )
    {
        $type = "IE";
    }
    else
    if ( string_contains( $http_user_agent, "Chrome" ) )
    {
        if ( string_contains( $http_user_agent, "Windows" ) )
        {
            $type = "CHROME";
        }
        else
        {
            $type = "WEBKIT";
        }
    }
    else
    if ( string_contains( $http_user_agent, "WebKit" ) )
    {
        $type = "WEBKIT";
    }
    else
    {
        $type = "MOZ";
    }
    return $type;
}

function DetermineSitenameReversed( $domain, $sites_path )
{
    //  If .local domain (dropspace.org.local),
    //  will need to rearrange to be 'org.dropspace.local',
    //  so remove '.local', and make 'is_local' true.
    //
    $is_local = string_has_suffix( $domain, ".local" );
    $is_test  = string_has_suffix( $domain, ".test"  );
    $is_next  = string_has_suffix( $domain, ".next"  );

    //error_log( $is_local );

    if ( $is_local ) $domain = preg_replace( "/\.local\z/", "", $domain );
    if ( $is_test  ) $domain = preg_replace(  "/\.test\z/", "", $domain );
    if ( $is_next  ) $domain = preg_replace(  "/\.next\z/", "", $domain );

    //error_log( $domain );

    $sitename = Reverse( $domain );                             // www.dropspace.org --> org.dropspace.www
    $sitename = preg_replace( "/\.www\z/", "", $sitename );     // org.dropspace.www --> org.dropspace

    if ( $is_local ) $sitename = "$sitename.local";             // org.dropspace     --> org.dropspace.local
    if ( $is_test  ) $sitename = "$sitename.test";              // org.dropspace     --> org.dropspace.test
    if ( $is_next  ) $sitename = "$sitename.next";              // org.dropspace     --> org.dropspace.next

    return $sitename;
}

function Reverse( $domain )
{
    $bits = explode( ".", $domain );
    $bits = array_reverse( $bits );

    return implode( ".", $bits );
}

Input

<?php
//  Copyright (c) 2009, 2010 Daniel Robert Bradley. All rights reserved.
//  This software is distributed under the terms of the GNU Lesser General Public License version 2.1
?>
<?php

class Output
{
    function println()
    {}

    function indent()
    {}

    function outdent()
    {}
}

function DBi_escape( $string )
{
    return $string;

    $db = DBi_anon();
    
    if ( $db->connect( new NullPrinter() ) )
    {
        return $db->escape( $string );
    }
}

class Input
{
    static function FilterInput( $request, $debug )
    {
        $debug    = new Output();
        $filtered = array();

        $debug->println( "<!-- FilterInput() start -->" );
        $debug->indent();
        {
            
            $debug->println( "<!-- REQUEST -->" );
            $debug->indent();
            {
                foreach ( $_REQUEST as $key => $val )
                {
                    $filtered_key = Input::Filter( $key );
                    $filtered_val = Input::Filter( $val );

                    $filtered[$filtered_key] = $filtered_val;

                    if ( is_array( $filtered_val ) )
                    {
                        $debug->println( "<!-- \"$filtered_key\" | Array -->" );
                    }
                    else
                    {
                        $debug->println( "<!-- \"$filtered_key\" | \"$filtered_val\" -->" );
                    }
                }
            }
            $debug->outdent();

            $debug->println( "<!-- COOKIE -->" );
            $debug->indent();
            {
                foreach ( $_COOKIE as $key => $val )
                {
                    if ( ! array_key_exists( $key, $filtered ) )
                    {
                        $filtered_key = Input::Filter( $key );
                        $filtered_val = Input::Filter( $val );

                        $filtered[$filtered_key] = $filtered_val;
                        $debug->println( "<!-- \"$filtered_key\" | \"$filtered_val\" -->" );
                    }
                }
            }
            $debug->outdent();
        }
        $debug->outdent();
        $debug->println( "<!-- FilterInput() end -->" );

        return $filtered;
    }

    static function Filter( $value )
    {
        if ( is_array( $value ) )
        {
            $ret = array();
            
            foreach ( $value as $key => $val )
            {
                $filtered_key = Input::Filter( $key );
                $filtered_val = Input::Filter( $val );
            
                $ret[$filtered_key] = $filtered_val;
            }
            
            return $ret;
        }
        else
        if ( is_string( $value ) )
        {
            //$value = Input::unidecode( $value );
            $value = utf8_decode( $value );
            $value = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8', false );
            $value = addslashes( $value );
            $value = DBi_escape( $value );

            $value = str_replace(   "\n",  "<br>", $value );
            $value = str_replace( "\\\\", "\", $value );
            $value = str_replace( "\x09",     " ", $value );

            return $value;
        }
        else
        if ( is_null( $value ) )
        {
            return "";
        }
        else
        {
            error_log( "Input::Filter( $value ): unexpected value!" );
        }
    }

    static function unidecode( $value )
    {
        $str = "";
        $n   = strlen( $value );
        $i   = 0;
        
        while ( $i < $n )
        {
            $ch  = substr( $value, $i, 1 );
            $val = ord( $ch );

            if ( ($val == (0xFC | $val)) && ($i+5 < $n) )       // 6 byte unicode
            {
                $str .= Input::utf2html( substr( $value, $i, 6 ) );
                $i   += 6;
            }
            else
            if ( ($val == (0xF8 | $val)) && ($i+4 < $n) )       // 5 byte unicode
            {
                $str .= Input::utf2html( substr( $value, $i, 5 ) );
                $i   += 5;
            }
            else
            if ( ($val == (0xF0 | $val)) && ($i+3 < $n) )       // 4 byte unicode
            {
                $str .= Input::utf2html( substr( $value, $i, 4 ) );
                $i   += 4;
            }
            else
            if ( ($val == (0xE0 | $val)) && ($i+2 < $n) )       // 3 byte unicode
            {
                $str .= Input::utf2html( substr( $value, $i, 3 ) );
                $i   += 3;
            }
            else
            if ( ($val == (0xC0 | $val)) && ($i+1 < $n) )   // 2 byte unicode
            {
                $str .= Input::utf2html( substr( $value, $i, 2 ) );
                $i   += 2;
            }
            else
            if ( $val == (0x80 | $val) )        // extra byte
            {
                error_log( "Warning detected invalid unicode" );
                $str .= '?';
                $i++;
            }
            else                                // ascii character
            {
                $str .= $ch;
                $i++;
            }
        }
        return $str;
    }

    static function utf2html( $string )
    {
        $array  = Input::utf8_to_unicode( $string );
        $string = Input::unicode_to_entities( $array );
        
        return $string;
    }

    static function utf8_to_unicode( $str )
    {
        $unicode = array();
        $values = array();
        $lookingFor = 1;
        
        for ($i = 0; $i < strlen( $str ); $i++ ) {

            $thisValue = ord( $str[ $i ] );
            
            if ( $thisValue < 128 ) $unicode[] = $thisValue;
            else {
            
                if ( count( $values ) == 0 ) $lookingFor = ( $thisValue < 224 ) ? 2 : 3;
                
                $values[] = $thisValue;
                
                if ( count( $values ) == $lookingFor ) {
            
                    $number = ( $lookingFor == 3 ) ?
                        ( ( $values[0] % 16 ) * 4096 ) + ( ( $values[1] % 64 ) * 64 ) + ( $values[2] % 64 ):
                        ( ( $values[0] % 32 ) * 64 ) + ( $values[1] % 64 );
                        
                    $unicode[] = $number;
                    $values = array();
                    $lookingFor = 1;
            
                } // if
            
            } // if
            
        } // for

        return $unicode;
    
    }

    static function unicode_to_entities( $unicode )
    {
        $entities = '';
        foreach( $unicode as $value ) $entities .= '&#' . $value . ';';
        return $entities;
    }
}

Scripts

G - Generate Apache Configurations

#!/bin/bash
#
#   This script will, for each directory in the Dropspace sites directory,
#   create an Apache2 configuration file called 'JUXTAPAGE.<directory name>.conf',
#   which is a copy of the 'JUXTAPAGE.TEMPLATE.conf' file, that has each occurrance
#   of the pattern %domain_name% replaced with <directory name>.
#
#   It needs to know:
#
#   1)  Where the Document root is - 'sites' is directly under it, and
#   2)  The location of the Apache configuration directory (/etc/apache2 by default).
DATE_FORMAT="+%Y-%m-%dT%H:%M:%S%z"
SITES_DIR=""
APACHE_DOCUMENT_ROOT=""
APACHE_CONFIG_DIR=""
APACHE_TEMPLATE_FILE=""
APPEND=""
DOMAIN=""
ALLOW_IP=""
ALLOW_IP_BEHIND_LB=""
RESTART=""
function Usage()
{
    echo "Usage: generate_apache_configurations.sh [--test] <Dropbox dir>           <Document root> <Apache config dir> [--template <Apache template file>] [--domain <reverse domain> [--allow-ip <ip>,...]]"
    echo "E.g.,  generate_apache_configurations.sh  --test  /home/<account>/Dropbox /srv/JUXTAPAGE  /etc/apache2/sites-enabled --template /etc/apache2/juxtapage.apache.template --domain com.example --allow-ip 192.168.0.1"
}
function main()
{
    Environment "$@" &&
    Generate

    if [ "true" == "$RESTART" ]
    then

        if apachectl configtest
        then
            service apache2 restart

        else
            echo "Configuration error - did not restart Apache"
        fi
    fi
}
function Environment()
{
    if [ "$1" == "--test" ]
    then
        APPEND=".test"
        shift
    fi

    SITES_DIR="$1";            shift
    APACHE_DOCUMENT_ROOT="$1"; shift
    APACHE_CONFIG_DIR="$1";    shift

    while [ -n "$1" ]
    do
        case "$1" in

        "--template")
            shift
            APACHE_TEMPLATE_FILE="$1"
            shift
            ;;

        "--domain")
            shift
            DOMAIN="$1"
            shift

            if [ "$1" == "--allow-ip" ]
            then
                echo "XXX"
                shift
                ALLOW_IP=`GenerateAllowIP $1`
                ALLOW_IP_BEHIND_LB=""
                shift

            elif [ "$1" == "--allow-ip-behind-lb" ]
            then
                shift
                ALLOW_IP="<RequireAny>\n    Require env allowed\n    </RequireAny>"
                ALLOW_IP_BEHIND_LB=`GenerateAllowIPBehindLoadBalander $1`
                shift

            else
                ALLOW_IP="Require all granted"
                ALLOW_IP_BEHIND_LB=""
            fi
            ;;

        "--restart")
            shift
            RESTART="true"
            ;;

        *)
            echo "Unexpected argument"
            Usage
            return -1
            ;;
        esac
    done

    echo "$ALLOW_IP"

    if   [ ! -d "$SITES_DIR" ]
    then
        echo "Invalid Sites directory: $SITES_DIR"
        Usage
        return -1

    elif [ ! -d "$APACHE_DOCUMENT_ROOT" ]
    then
        echo "Invalid Document Root directory: $APACHE_DOCUMENT_ROOT"
        Usage
        return -1

    elif [ ! -d "$APACHE_CONFIG_DIR" -a ! -d "$APACHE_CONFIG_DIR/sites-enabled" ]
    then
        echo "Invalid Apache config dir: $apache_config_dir"
        Usage
        return -1

    fi
}
function GenerateAllowIP()
{
    local ipaddresses=`echo $1 | sed "s|,| |g"`
    local allow_ip="<RequireAny>"

    for i in $ipaddresses
    do
        allow_ip+="\n    Require ip $i"
    done

    allow_ip+="    </RequireAny>"

    echo $allow_ip
}
function GenerateAllowIPBehindLoadBalander()
{
    local ipaddresses=`echo $1 | sed "s|,| |g"`
    local allow_ip=""

    for i in $ipaddresses
    do
        allow_ip+="\n    SetEnvIf X-Forwarded-For ^$i allowed"
    done

    echo $allow_ip
}
function Generate()
{
    echo process_subdirectories "$SITES_DIR" "$APACHE_DOCUMENT_ROOT" "$APACHE_CONFIG_DIR" "$APACHE_TEMPLATE_FILE" "$APPEND"
         process_subdirectories "$SITES_DIR" "$APACHE_DOCUMENT_ROOT" "$APACHE_CONFIG_DIR" "$APACHE_TEMPLATE_FILE" "$APPEND"
}
function process_subdirectories()
{
    local sites_dir=$1                  # /srv/JUXTAPAGE/sites
    local document_root=$2              # /srv/JUXTAPAGE
    local apache_config_target=$3       # /etc/apache2/sites-enabled
    local apache_template_file=$4       # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/configuration/apache2/JUXTAPAGE.TEMPLATE.conf
    local append=$5                     # .test

    if [ -z "$apache_template_file" ]
    then
        local script_name=`scriptname`                                              # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/cron/generate_apache_configurations/generate_apache_configurations.sh
        local script_dir=`dirname "$script_name"`                                   # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/cron/generate_apache_configurations
        local libexec_dir=`dirname "$script_dir"`                                   # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/cron
        local juxtapage_dir=`dirname "$libexec_dir"`                                # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage
        local apache_template_dir="$juxtapage_dir/configuration/apache2"            # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/configuration/apache2
        local apache_config_template="$apache_template_dir/JUXTAPAGE.TEMPLATE.conf" # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/configuration/apache2/JUXTAPAGE.TEMPLATE.conf

        apache_template_file="$apache_config_template"
    fi

    if [ -z "$apache_config_target" ]
    then
        apache_config_target="/etc/apache2/sites-enabled"
    fi

    local target="$apache_config_target"

    if [ ! -f "$apache_template_file" ]
    then

        echo "Internal error: could not find Apache configuration template file at: $apache_template_file"
        exit

    else

        for domain_dir in $( ls "$sites_dir" )
        do
            if [ "" == "$DOMAIN" -o "$DOMAIN" == "$domain_dir" ]
            then

                if [ -d "$sites_dir/$domain_dir" -a "_" != ${domain_dir:0:1} ]
                then
                    if string_contains "$domain_dir" "."
                    then
                        local target_file="$target/JUXTAPAGE.$domain_dir$append.conf"
                        local domain_name="$domain_dir"
                        local tld=`echo "$domain_dir" | cut -f1 -d'.'`
                        local now=`now`

                        if [ "$DOMAIN" == "$domain_dir" ]
                        then
                            echo "$now: Considering: $DOMAIN"
                        fi

                        if is_tld $tld
                        then
                            domain_name=`reverse_domain $domain_dir`

                        fi

                        domain_name="$domain_name$append"

                        if [ "$DOMAIN" == "$domain_dir" ]
                        then

                            rm -rf $target/JUXTAPAGE.$domain_dir$append.ssl.conf $target/JUXTAPAGE.$domain_dir$append-le-ssl.conf

                            echo "$now: Reprocessing directory: $domain_dir for domain: $domain_name"
                            echo "$now: Creating:   $target/JUXTAPAGE.$domain_dir$append.conf"
                            sed "s|%document_root%|$document_root|g" "$apache_template_file" | sed "s|%domain_name%|$domain_name|g" | sed "s|%sites_path%|$sites_dir|g" | sed "s|%domain_dir%|$domain_dir|g" | sed "s|%allow_ip%|$ALLOW_IP|g" | sed "s|%allow_ip_behind_load_balancer%|$ALLOW_IP_BEHIND_LB|g" | sed "s|%now|$now|g" > "$target_file"

                        elif [ ! -f "$target_file" -a ! -f "$target/JUXTAPAGE.$domain_dir$append-le-ssl.conf" -a ! -f "$target/JUXTAPAGE.$domain_dir$append.ssl.conf" ]
                        then

                            echo "$now: Processing directory: $domain_dir for domain: $domain_name"
                            echo "$now: Creating:   $target/JUXTAPAGE.$domain_dir$append.conf"
                            sed "s|%document_root%|$document_root|g" "$apache_template_file" | sed "s|%domain_name%|$domain_name|g" | sed "s|%sites_path%|$sites_dir|g" | sed "s|%domain_dir%|$domain_dir|g" | sed "s|%allow_ip%|$ALLOW_IP|g" | sed "s|%allow_ip_behind_load_balancer%|$ALLOW_IP_BEHIND_LB|g" | sed "s|%now|$now|g" > "$target_file"

                        fi
                    fi
                fi
            fi
        done
    fi
}
#
#   Utility functions
#

function scriptname()
{
    echo $(readlink -f $0)
}

function string_ends_with()
{
    local haystack=$1
    local suffix=$2

    if [ -z "${haystack##*$suffix}" ]
    then
        return  0 # success
    else
        return -1 # failure
    fi
}

function string_contains()
{
    local haystack=$1
    local value=$2

    if [ "${haystack/./}" != "$haystack" ]
    then
        return 0 # success
    else
        return 1 # failure
    fi
}

function reverse_domain()
{
    local dir=$1
    local is_local="FALSE"
    local is_test="FALSE"

    #
    #   If domain has .local suffix remove and remember.
    #
    if string_ends_with $dir .local
    then
        is_local="TRUE"
        dir=${dir/.local/}                      # com.domain.local --> com.domain
    fi

    #
    #   If domain has .test suffix remove and remember.
    #
    if string_ends_with $dir .test
    then
        is_test="TRUE"
        dir=${dir/.test/}                       # com.domain.test  --> com.domain
    fi

    local parts=${dir//./ }                     # com.domain       --> com domain

    local reversed=`reverse_arguments $parts`   # com domain       --> domain com
    local domain=${reversed// /.}               # domain com       --> domain.com

    #
    #   If had .local suffix reattach.
    #
    if [ "TRUE" == $is_local ]
    then
        domain+=".local"
    fi

    #
    #   If had .test suffix reattach.
    #
    if [ "TRUE" == $is_test ]
    then
        domain+=".test"
    fi

    echo $domain
}

function reverse_arguments()
{
        local mine=$1

        if [ $# -gt 1 ]
        then
            shift
            reverse_arguments $@
            printf " $mine"
        else
            printf "$mine"
        fi
}

function is_tld()
{
    case $1 in
        com|dev|info|net|org|info)
            return  0 # true
            ;;

        at|au|nz|uk|us)
            return  0 # true
            ;;

        cloud|directory|online|news|site|software|solutions|training|xyz)
            return  0 # true
            ;;

        *)
            return -1 # false
    esac
}

function now()
{
    date "$DATE_FORMAT"
}


if [ $# -lt 3 ]
then

    Usage
    exit -1

else

    main "$@"

fi

Restart Apache

#!/bin/bash
#
#   This script will determine the most recent modified date of the files in
#   APACHE_CONFIG_DIR and then will restart apache if that modified date is more
#   recent than the last time the script restarted Apache.
#
#   The script stores the most recent restart of Apache in the file TIMESTAMP_FILE
#
#   Locations may be overridden by passing values on command line, e.g.
#
#   ./restart_apache.sh --apache-config-dir <config dir> --timestamp-file <timestamp file>
#
DATE_FORMAT="+%Y-%m-%dT%H:%M:%S%z"
APACHE_CONFIG_DIR="/etc/apache2/sites-enabled"
TIMESTAMP_FILE="/tmp/restart_apache.mtime"
DEV_NULL="/dev/null"

function main()
{
    parse_args "$@"

    local now=`date "$DATE_FORMAT"`
    local last_restart=`last_restart`
    local mtime_full=`last_apache_configuration_mtime                     "${APACHE_CONFIG_DIR}"`
    local mtime_secs=`last_apache_configuration_mtime_seconds_since_epoch "${APACHE_CONFIG_DIR}"`
    local filename=`last_apache_configuration_file                        "${APACHE_CONFIG_DIR}"`
    local user=`whoami`

    if [ "root" != "$user" ]
    then
        echo "Error: restart_apache.sh must be run as root."

    elif (( last_restart < mtime_secs ))
    then
        check_apache_config
        if [ "0" = "$?" ]
        then
            /usr/sbin/service apache2 restart
            if [ "0" = $? ]
            then
                echo "$mtime_secs" > "$TIMESTAMP_FILE"
                echo "$now: Last restart < last modification: $last_restart < $mtime_secs ($mtime_full) - Restarted"

            else
                echo "$now: Last restart < last modification: $last_restart < $mtime_secs ($mtime_full) - Unexpected restart failure"

            fi

        else
            echo "$now: Last restart < last modification: $last_restart < $mtime_secs ($mtime_full) - Config fail ($filename)"

        fi

    else
        echo "$now: Last restart = last modification: $last_restart = $mtime_secs ($mtime_full) - Skipping"

    fi
}

function parse_args()
{
    while [ -n "$1" ]
    do
        if [ "$1" = "--apache-config-dir" ]
        then
            shift
            if [ ! -d "$1" ]
            then
                echo "Aborting, non-existant directory passed with --apache-config-dir"
                exit -1
            else
                APACHE_CONFIG_DIR="$1"
                shift
            fi
        
        elif [ "$1" = "--timestamp-file" ]
        then
            shift
            if [ -d "$1" ]
            then
                echo "Aborting, directory passed with --timestamp-file, file or no file expected"
                exit -1

            else
                TIMESTAMP_FILE="$1"
                shift
            fi
        fi
    done
}

function last_restart()
{
    if [ ! -f "$TIMESTAMP_FILE" ]
    then
        echo "0" > "$TIMESTAMP_FILE"
    fi

    local secs=`cat "$TIMESTAMP_FILE"`

    echo "$secs"
}

function last_apache_configuration_mtime()
{
    local file=`ls -t "$1" | head -n1`
    local info=`ls -lt --time-style "+%Y-%m-%dT%H:%M:%S%z" "$1/$file"`
    local mtime=`echo $info | cut -d ' ' -f6`

    echo "$mtime"
}

function last_apache_configuration_mtime_seconds_since_epoch()
{
    local file=`ls -t "$1" | head -n1`
    local info=`ls -lt --time-style +%s "$1/$file"`
    local mtime=`echo $info | cut -d ' ' -f6`

    echo "$mtime"
}

function last_apache_configuration_file()
{
    local file=`ls -t "$1" | head -n1`
    local info=`ls -lt --time-style +%s "$1/$file"`
    local name=`echo $info | cut -d ' ' -f7`

    echo "$name"
}

function check_apache_config()
{
    sudo apachectl configtest > "$DEV_NULL" 2> "$DEV_NULL"

    return $?
}

main "$@"