Technical Documentation

Introduction

JuxtaPage is a PHP-based web framework that runs 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.

Websites are stored in sub-directories underneath the '/srv/JUXTAPAGE/sites' directory. 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'.

JuxtaPage also comes with support scripts that can automatically create a new Apache configuration file for websites as they are added to the 'sites' directory; and, if desired, use Let's Encrypt to obtain SSL certificates.

This has been used to automatically publish websites using Dropbox.

Below is the file file structure of a typical JuxtaPage based website. Page fragments can either be named using a definitive name such as 'head.title.htm', or can be named using the short-form 'title.htm'.

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. If configured to do so, sets an auth cookie.
  2. Obtains a 'Configuration' object that stores important paths, the website domain name, and the path of the websites content.
  3. Checks if the sites path exists.
  4. Checks if the settings path exists, and if so checks user authorisation using it.
  5. Else, checks if the restricted file exists, and if so checks user authorisation using it.
  6. If access is denied, the user is redirected to the home page.
  7. Otherwise, renders the page.

Note: on each page load, you need to very careful that you confirm the authorisation of the user before redirecting them to another page, otherwise you may encounter redirect loops.

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

    if ( array_key_exists( "PHP_AUTH_USER", $_SERVER ) )
    {
        SetCustomCookie( "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
    {
        render_page( $access, $configuration );
    }

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

    error_log( "Duration: $microseconds microseconds ($delta seconds)" );

    Alt::Log( $configuration->clientAddress, $configuration->domain, $configuration->redirectURL );
}
Render Page

The render page function performs the folloing important steps:

  1. Calls 'header' to set the content type to 'text/html'.
  2. Instantiates a Page object.
  3. Calls $page->render to render the page out to std out.
function render_page( $access, $configuration )
{
    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 );
}
main();

Configuration

The Configuration class is responsible for interrogating the enviornment to determine where important directories are.

Below are each of the class members of the Configuration class, with the typical values they might have.

class Configuration {

var $base;                  // "/srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/sbin/.."
var $commonPath;            // "/srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/sbin/../share/content"
var $sitesPath;             // "/srv/JUXTAPAGE/sites"

var $documentRoot;          // "/srv/JUXTAPAGE"
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/JUXTAPAGE/sites/com.example"
var $websiteContent;        // "/srv/JUXTAPAGE/sites/com.example/_content"
var $settingsPath;          // "/srv/JUXTAPAGE/sites/com.example/_content/_site/_SETTINGS.json.txt"
var $restrictedPath;        // "/srv/JUXTAPAGE/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;
}
Determine Locale

The following function is used to determine the locale as requested by the browser. The locale may be used to perform automated language translation using a mapping file.

static function DetermineLocale()
{
    $locale = Input::Filter( array_get( $_COOKIE, "locale" ) );

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

    return $locale;
}
Determine Website Content Path

The following function is used to determine where a website's content is located. Originally, the '_content' directory was used, however, later it was decided to also support the use of either the 'content' or 'source/content' directories.

As a special case, if 'websitePath' ends with '.test', and a directory corresponding to the path doesn't exist, it will also try removing that suffix.

This is to allow the same development folder to be shared between a public server and private '.test' server.

juxtapage.com       → public server with content at /srv/JUXTAPAGE/sites/com.juxtapage
juxtapage.com.test  → private server with content at /srv/JUXTAPAGE/sites/com.juxtapage
                                                       or /srv/JUXTAPAGE/sites/com.juxtapage.test

If neither directory exists, it will log an error message and exit.

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

    if ( !is_dir( $websitePath ) )
    {
        error_log( "Cannot find website path: [" . $websitePath . "]" );

        exit( -1 );
    }
    else
    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";
    }
}
to String

The following returns a string containing the values of all configuration fields.

function toString()
{
    $output  = "\n";
    $output .= "Configuration";
    $output .= "\n\t". sprintf( "%20s: %s",           "base", $this->base           );
    $output .= "\n\t". sprintf( "%20s: %s",     "commonPath", $this->commonPath     );
    $output .= "\n\t". sprintf( "%20s: %s",      "sitesPath", $this->sitesPath      );

    $output .= "\n\t". sprintf( "%20s: %s",   "documentRoot", $this->documentRoot   );
    $output .= "\n\t". sprintf( "%20s: %s",         "domain", $this->domain         );
    $output .= "\n\t". sprintf( "%20s: %s",  "httpUserAgent", $this->httpUserAgent  );
    $output .= "\n\t". sprintf( "%20s: %s",    "redirectURL", $this->redirectURL    );

    $output .= "\n\t". sprintf( "%20s: %s",         "pageID", $this->pageID         );
    $output .= "\n\t". sprintf( "%20s: %s",        "pageDir", $this->pageDir        );
    $output .= "\n\t". sprintf( "%20s: %s",       "pagePath", $this->pagePath       );

    $output .= "\n\t". sprintf( "%20s: %s",        "browser", $this->browser        );
    $output .= "\n\t". sprintf( "%20s: %s",       "isMobile", $this->isMobile       );

    $output .= "\n\t". sprintf( "%20s: %s",    "websiteName", $this->websiteName    );
    $output .= "\n\t". sprintf( "%20s: %s",    "websitePath", $this->websitePath    );
    $output .= "\n\t". sprintf( "%20s: %s", "websiteContent", $this->websiteContent );
    $output .= "\n\t". sprintf( "%20s: %s", "restrictedPath", $this->restrictedPath );

    $output .= "\n";
    $output .= "Request";

    foreach ( $this->request as $name => $value )
    {
        $output .= "\n\t". sprintf( "%20s: %s", $name, $value );
    }

    return $output;
}

Access

The Access class determines whether the users should be able to access the current page. The 'IsGranted' static function is called, passing in the path to either the settings file or the restricted file - '/_content/_site/_RESTRICTED.txt' - which contains both a list of restricted paths and a set of zero or more API servers that are queried to determine whether access should be allowed.

class Access {
Is Granted

'IsGranted' is called from 'main'. If the call to 'RequestAccess' returns false, the function returns false; otherwise, it will return an object 'access' containing 'csrf_token' and 'keys'.

static function IsGranted( $restricted_path, $pageid )
{
    $access = false;

    if ( !file_exists( $restricted_path ) )
    {
        $access               = array();
        $access["csrf_token"] = "";
        $access["keys"      ] = "";
    }
    else
    {
        if ( ($credentials = self::DetermineCredentials( $restricted_path )) )
        {
            $access = self::RequestAccess( $credentials, $pageid );
        }
    }

    return (object) $access;
}
Determine Credentials

Determine credentials returns a 'credentials' object that with the following members:

This information is extracted from the JSON file stored at 'restricted_path'.

static function DetermineCredentials( $restricted_path )
{
    $credentials = false;

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

    $server_name = $_SERVER["SERVER_NAME"];
    $apihost     = "";
    $apikey      = "";
    $list        = null;

    //  1.  Parse JSON formatted settings/restricted file.

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

    //  2.  Retrieve api host/key and list of restricted directories.

    if ( is_array( $prov ) )
    {
        //  2a.  Handle case (a) just an array of restricted directories

        $apihost = self::GenerateAPIHostname( $server_name );
        $list    = $prov;
    }
    else
    if ( $prov->apihost && $prov->directories )
    {
        //  2b.  Handle case (b) where JSON contains separate members.

        $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( !$apihost || !$apikey || !$list || !is_array( $list ) )
    {
        error_log( "ACCESS DENIED: Invalid Configuration: " . $restricted );
        exit( -1 );
    }
    else
    {
        $credentials["apihost"] = $apihost;
        $credentials["apikey" ] = $apikey;
        $credentials["list"   ] = $list;
    }

    return $credentials;
}
Request Access

Iterates through 'credentials->list', which is the list of restricted directories; if the current 'pageid' corresponds to one of those directories, VerifyAccessID is called.

static function RequestAccess( $credentials, $pageid )
{
    $access  = false;
    $list    = $credentials["list"   ];
    $apihost = $credentials["apihost"];
    $apikey  = $credentials["apikey" ];

    foreach ( $list as $prefix )
    {
        if ( string_has_prefix( $pageid, $prefix ) )
        {
            if ( !array_key_exists( "accessid", $_COOKIE ) )
            {
                error_log( "ACCESS DENIED: Missing Access ID" );
            }
            else
            {
                $access = self::VerifyAccess( $apihost, $apikey );
            }
            break;
        }
    }

    if ( ! $access )
    {
        $access = self::RetrieveCSRFToken( $apihost, $apikey );
    }

    if ( $access )
    {
        error_log( "ACCESS PERMITTED" );
    }

    return $access;
}

Verfity Access

The following function will attempt to retrieve the "accessid" cookie, which will contain a subset of the users sessionid, and will then use 'IsValidAccessID' to validate it with the API server. If successful, the 'access' object is returned including the 'csrf_token' and 'keys'. CSRF_TOKEN will be used to populate the "data-csrf" attribute, while KEYS will be used to determine access to page variants.

static function VerifyAccess( $apihost, $apikey )
{
    $access = self::RetrieveAccess( $apihost, $apikey, $_COOKIE["accessid"] );
    {
        $status = "Invalid";
    }

    if ( !$access )
    {
        error_log( "ACCESS DENIED: Invalid accessid" );
    }

    return $access;
}

Retrieve Access

Calls the '/auth/access/' endpoint passing the 'accessid' as 'Aid', and, if valid, receives back access keys and a CSRF token.

If the internet connection fails, the function will retry the call five further times, sleeping one second after each unsuccessful attempt.

static function RetrieveAccess( $apihost, $apikey, $accessid )
{
    $access       = false;
    $accessid     = urlencode( $accessid );

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

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

    $responseText = self::Call( $apihost, "/auth/access/", $parameters );

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

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

                    $access["csrf_token"] = $obj->results[0]->CSRF_Token;
                    $access["keys"      ] = $obj->results[0]->Access_Keys;
                }
            }
        }

        if ( !$access )
        {
            error_log( $responseText );
        }
    }

    return $access;
}

Retrieve CSRF Token

static function RetrieveCSRFToken( $apihost, $apikey )
{
    $access = false;

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

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

    $responseText = self::Call( $apihost, "/api/auth/access/", $parameters );

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

        if ( $obj && ("OK" == $obj->status) )
        {
            if ( 1 == count( $obj->results ) )
            {
                $access = array();
                $access["csrf_token"] = $obj->results[0]->CSRF_Token;
                $access["keys"      ] = "";
            }
        }
    }
    
    return $access;
}
Call
$responseText = self::Call( $apihost, $endpoint, $parameters );
static function Call( $apihost, $endpoint, $parameters )
{
    $context       = self::CreateStreamContext( $apihost, join( "&", $parameters ) );
    $response_text = FALSE;
    $attempts      = 5;

    do
    {
        $response_text = @file_get_contents( $apihost . $endpoint, false, $context );

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

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

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

    return $response_text;
}
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 );
            if ( !$this->body_main )
            {
                $this->body_main_start   = self::ResolveFile( $website_content, $page_dir, "body.main_start",   $keys, $website_name );
                $this->body_main_header  = self::ResolveFile( $website_content, $page_dir, "body.main_header",  $keys, $website_name );
                $this->body_main_article = self::ResolveFile( $website_content, $page_dir, "body.main.article", $keys, $website_name );
                if ( !$this->body_main_article )
                {
                    $this->body_main_article_start = self::ResolveFile( $website_content, $page_dir, "body.main.article_start", $keys, $website_name );
                    $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 );
                $this->body_main_footer  = self::ResolveFile( $website_content, $page_dir, "body.main_footer",  $keys, $website_name );
            }
            $this->body_footer = self::ResolveFile( $website_content, $page_dir, "body.footer", $keys, $website_name );
        }

        $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:

static function ResolveFile( $website_content, $pageDir, $element, $keys, $website_name, $one_level = false )
{
    if ( "TRUE" == getenv( "DEBUG" ) )
    {
        error_log( "Searching for: $element (one_level=$one_level)" );
    }

    $files = self::GenerateProvisionalFileList( $website_content, $pageDir, $element, $keys, $website_name, $one_level );

    //  The following will iterator through every provisional profile until the file is found.
    //

    foreach( $files as $filepath )
    {
        $file  = null;

        if ( file_exists( $filepath ) )
        {
            $file = $filepath;

            if ( "TRUE" == getenv( "DEBUG" ) )
            {
                error_log( "Yes - $filepath" );
            }
            break;
        }

        if ( "TRUE" == getenv( "DEBUG" ) )
        {
            error_log( "No - $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 )
{
    $files = array();
    $loop  = !$one_level;

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

    do
    {
        if ( !empty( $array_paths ) )
        {
            if ( count( $array_paths ) > 1 )
            {
                $p = trim( implode( '-', $array_paths ) );
            }
            else
            {
                $p = $array_paths[0];
            }
        }
        else
        {
            $loop = false;
            $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 ( count( $array_paths ) )
        {
            $array_paths = array_slice( $array_paths, 0, -1 );
        }
    }
    while( $loop );

    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' ]    
        $lamb           = substr( $element, 0 );
        $array_elements = explode( '.', $lamb );                        // [ 'body', 'main', 'article_start' ]
 
        while( ! empty( $array_elements ) )
        {
            if ( count( $array_elements ) > 1 )
            {
                $e = implode( '.', $array_elements );                   //  "body.main.article_start"

                $array_elements = array_slice( $array_elements, 1 );    // [ 'main', 'article_start' ]
            }
            else
            {
                $e = $array_elements[0];

                $array_elements = array();
            }
            $k = implode( '-', $array_keys     );                       //  "CLIENT-company"

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

            $files[] = $f;
        }
    }
    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>";
}
}

Alt Log

<?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" );
        }
    }
}
}

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";
}

Helper Functions

CanonicalisePath

Called from Configuration constructor.

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

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

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

        return $path;
}

IsHTTPS

Unused.

function IsHTTPS()
{
    return ( !empty( $_SERVER['HTTPS'] ) && ($_SERVER['HTTPS'] !== 'off') );
}

GeneratePageId

Called from Configuration constructor.

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

GeneratePageDir

Called from Configuration constructor.

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;
}

GeneratePagePath

Called from Configuration constructor.

/*
 *  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 );
    }

Determine If Mobile

Called from Configuration constructor.

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 );
}

Determine Browser

Called from Configuration constructor.

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;
}

Determine Sitename Reversed

Called from Configuration constructor.

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;
}
Reverse

Called from DetermineSitenameReversed.

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

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

Set Custom Cookie

Called from main.

function SetCustomCookie( $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 );
}

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 "$@"