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:
- If configured to do so, sets an auth cookie.
- Obtains a 'Configuration' object that stores important paths, the website domain name, and the path of the websites content.
- Checks if the sites path exists.
- Checks if the settings path exists, and if so checks user authorisation using it.
- Else, checks if the restricted file exists, and if so checks user authorisation using it.
- If access is denied, the user is redirected to the home page.
- 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:
- Calls 'header' to set the content type to 'text/html'.
- Instantiates a Page object.
- 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:
- 'apihost' - API host used to determine access
- 'apihkey' - API key passed to the API host
- 'list' - a list of restricted parent directories
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:
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:
- 'path' = 'dashboard-clients-client'
- 'element' = 'body.main.article_start'
- 'keys' = 'CLIENT-company'
- 'website_name' = '@example.com'
Returns:
- dashboard-clients-client/body.main.article_start-CLIENT-company@example.com.htm
- dashboard-clients-client/main.article_start-CLIENT-company@example.com.htm
- dashboard-clients-client/article_start-CLIENT-company@example.com.htm
- dashboard-clients-client/body.main.article_start-CLIENT@example.com.htm
- dashboard-clients-client/main.article_start-CLIENT@example.com.htm
- dashboard-clients-client/article_start-CLIENT@example.com.htm
- 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 "$@"