Technical Documentation
Introduction
JuxtaPage is a PHP-based web framework run atop of the Apache web server. Unlike other web frameworks, JuxtaPage does not allow programmatic generation of HTML code. Instead, whole web pages are constructed from HTML element fragments contained in separate files, e.g., "head.htm", "main.htm", "article.htm", etc. These files are stored in a hierarchical directory structure and any required elements that are missing in a leaf directory are obtained from the first parent directory containing the fragment. A special directory called '_site' is considered the root directory of this hierarchy.
Additionally, JuxtaPage has been designed to allow automated publication of websites that are shared with the JuxtaPage server using Dropbox.
Each website is contained within a directory named in reverse domain-name style. For example, 'juxtapage.org' would be stored in a directory called 'org.juxtapage'. Once the shared directory is accepted and populated into the server Dropbox directory, an Apache configuration file is automatically generated, and a Let's Encrypt certificate is retrieved using certbot.
Below is the file file structure of a typical JuxtaPage based website:
org.juxtapage/_site org.juxtapage/_site/head.title.htm org.juxtapage/_site/head.meta.htm org.juxtapage/_site/head.javascript.htm org.juxtapage/_site/head.styles.htm org.juxtapage/_site/header.htm org.juxtapage/_site/nav_start.htm org.juxtapage/_site/nav_end.htm org.juxtapage/_site/main_start.htm org.juxtapage/_site/article_start.htm org.juxtapage/_site/footer.htm org.juxtapage/_index/article.htm
History
JuxtaPage is a rewrite/restructure by the same creators of an earlier framework called DropSpace, see:
DropSpace
Configuration
By default, the 'document root' referred to below is '/srv/JUXTAPAGE/', however, it is automatically detected based on the install location. Please note, if you change the default install location you will need to ensure that Apache is appropriate configured to support that location.
Non-site specific configuration
Non-site specific configuration is specified in the 'configuration/conf.php' file. This file is pre-loaded using the 'php_value auto_prepend_file' directive in the Apache configuration file. The only configuration required is to nominate the 'sites path', which is where webiste directories are stored.
configuration/conf.php
<?php
//
// The SITES_PATH is usually a symbolic link to the Dropbox directory.
// This indicates where the collection of website directories will be.
//
define( "SITES_PATH", $_SERVER["DOCUMENT_ROOT"] . "/sites" );
Site specific configuration
Site specific configuration is specified in the Apache configuration file, which is automatically generated when a website directory is added to the SITES_PATH directory.
# Last modified: %now
<VirtualHost *:80>
ServerAdmin webmaster@%domain_name%
ServerName %domain_name%
ServerAlias www.%domain_name%
DocumentRoot %document_root%
<Directory %document_root%/>
php_value auto_prepend_file "%document_root%/JuxtaPage/latest/juxtapage/configuration/conf.php"
</Directory>
Alias /resources %sites_path%/%domain_dir%/_resources
<Directory "%sites_path%/%domain_dir%/_resources">
Require all granted
</Directory>
<Directory %sites_path%/%domain_dir%/_resources/>
php_value open_basedir "%sites_path%/%domain_dir%/_resources/"
</Directory>
<Directory %document_root%/>
Options FollowSymLinks
RewriteEngine On
RewriteBase /
#
# By default, no redirect occurs.
# To redirect, for example, example.com to www.example.com,
# uncomment the appropriate two lines below:
#
# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] # Redirect www.example.com
# RewriteRule ^(.*)$ http://%1/$1 [R=301,L] # to example.com
#
# RewriteCond %{HTTP_HOST} !^www\. [NC] # Redirect example.com
# RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L] # to www.example.com
#
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule /* %document_root%/JuxtaPage/latest/juxtapage/sbin/index.php
%allow_ip_behind_load_balancer%
%allow_ip%
</Directory>
ErrorLog /var/log/apache2/JUXTAPAGE.%domain_dir%.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access.log combined
#ErrorDocument 404 /index.php
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/%wildcard%.pem
SSLCertificateKeyFile /etc/apache2/ssl/%wildcard%.key
ServerAdmin webmaster@%domain_name%
ServerName %domain_name%
ServerAlias www.%domain_name%
DocumentRoot %document_root%
<Directory %document_root%/>
php_value auto_prepend_file "%document_root%/JuxtaPage/latest/juxtapage/configuration/conf.php"
</Directory>
Alias /resources %dropbox_dir%/%domain_dir%/_resources
<Directory "%dropbox_dir%/%domain_dir%/_resources">
Require all granted
</Directory>
<Directory %dropbox_dir%/%domain_dir%/_resources/>
php_value open_basedir "%dropbox_dir%/%domain_dir%/_resources/"
</Directory>
<Directory %document_root%/>
Options FollowSymLinks
RewriteEngine On
RewriteBase /
#
# By default, no redirect occurs.
# To redirect, for example, example.com to www.example.com,
# uncomment the appropriate two lines below:
#
# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] # Redirect www.example.com
# RewriteRule ^(.*)$ http://%1/$1 [R=301,L] # to example.com
#
# RewriteCond %{HTTP_HOST} !^www\. [NC] # Redirect example.com
# RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L] # to www.example.com
#
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule /* %document_root%/JuxtaPage/latest/juxtapage/sbin/index.php
<RequireAll>
Require %allow_ip%
</RequireAll>
</Directory>
ErrorLog /var/log/apache2/JUXTAPAGE.%domain_dir%.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access.log combined
#ErrorDocument 404 /index.php
</VirtualHost>
</IfModule>
Implementation
Includes
<?php
include_once( "strings.php" );
include_once( "HelperFunctions.php" );
include_once( "Input.php" );
include_once( "Access.php" );
include_once( "Alt.php" );
include_once( "Page.php" );
Main
The main procedure does the following:
1. Obtains a 'Configuration' object that stores important paths, the website domain name, and the path of the websites content.
function main()
{
$start = microtime( TRUE );
if ( array_key_exists( "PHP_AUTH_USER", $_SERVER ) )
{
SetCookie( "AuthBasicUser", $_SERVER["PHP_AUTH_USER"] );
}
$configuration = new Configuration();
if ( !is_dir( $configuration->sitesPath ) )
{
error_log( "Aborting, could not find sites path: [" . $configuration->sitesPath . "]" );
exit();
}
if ( file_exists( $configuration->settingsPath ) )
{
$access = Access::IsGranted( $configuration->settingsPath, $configuration->pageID );
}
else
{
$access = Access::IsGranted( $configuration->restrictedPath, $configuration->pageID );
}
if ( false === $access )
{
header( "Location: /" );
}
else
{
header( "Content-Type: text/html" );
$keys = $access->keys;
$csrf_token = $access->csrf_token;
$page = new Page
(
$configuration->websiteContent,
$configuration->locale,
$configuration->pageDir,
$keys,
$configuration->websiteName
);
$page->render( $configuration, $csrf_token );
//error_log( $page->toString() );
}
$delta = microtime( TRUE ) - $start;
$ms = ceil( $delta * 1000000 );
error_log( "Duration: $ms ms ($delta)" );
Alt::Log( $configuration->clientAddress, $configuration->domain, $configuration->redirectURL );
}
main();
Configuration
class Configuration {
var $base; // "/srv/DROPSPACE/Dropspace/latest/dropspace/sbin/.."
var $commonPath; // "/srv/DROPSPACE/Dropspace/latest/dropspace/sbin/../share/content"
var $sitesPath; // "/srv/DROPSPACE/sites"
var $documentRoot; // "/srv/DROPSPACE"
var $domain; // "example.com"
var $httpUserAgent; // "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15"
var $redirectURL; // "/"
var $pageID; // "index"
var $pageDir; // "_index"
var $pagePath; // "/"
var $browser; // "WEBKIT"
var $isMobile; // ""
var $websiteName; // "com.example"
var $websitePath; // "/srv/DROPSPACE/sites/com.example"
var $websiteContent; // "/srv/DROPSPACE/sites/com.example/_content"
var $settingsPath; // "/srv/DROPSPACE/sites/com.example/_content/_site/_SETTINGS.json.txt"
var $restrictedPath; // "/srv/DROPSPACE/sites/com.example/_content/_site/_RESTRICTED.json.txt"
var $request; // htmlentity filtered dictionary of $_REQUEST parameters
var $accessID; // filtered value from $_COOKIE["accessid"]
var $remoteAddress; // 192.168.0.1
var $forwardedFor; // 10.0.0.1
var $clientAddress; // 10.0.0.1
function __construct()
{
$this->locale = self::DetermineLocale();
$this->base = __DIR__ . "/..";
$this->commonPath = $this->base . "/share/content";
$this->sitesPath = getenv( "SITES_PATH" ) ? getenv( "SITES_PATH" ) : SITES_PATH;
$this->documentRoot = Input::Filter( array_get( $_SERVER, "DOCUMENT_ROOT" ) );
$this->domain = Input::Filter( array_get( $_SERVER, "SERVER_NAME" ) );
$this->httpUserAgent = Input::Filter( array_get( $_SERVER, "HTTP_USER_AGENT" ) );
$this->redirectURL = CanonicalisePath( Input::Filter( array_get( $_SERVER, "REDIRECT_URL" ) ) );
$this->pageID = GeneratePageID ( $this->redirectURL );
$this->pageDir = GeneratePageDir ( $this->redirectURL );
$this->pagePath = GeneratePagePath( $this->redirectURL );
$this->browser = DetermineBrowser ( $this->httpUserAgent );
$this->isMobile = DetermineIfMobile( $this->httpUserAgent );
$this->websiteName = DetermineSitenameReversed( $this->domain, $this->sitesPath );
$this->websitePath = $this->sitesPath . "/" . $this->websiteName;
$this->websiteContent = self::DetermineWebsiteContentPath( $this->websitePath );
$this->settingsPath = $this->websiteContent . "/_site/_SETTINGS.json.txt";
$this->restrictedPath = $this->websiteContent . "/_site/_RESTRICTED.json.txt";
$this->request = Input::FilterInput( $_REQUEST, new Output() );
$this->accessID = Input::Filter( array_get( $_COOKIE, "accessid" ) );
$this->remoteAddress = $_SERVER['REMOTE_ADDR'];
$this->forwardedFor = array_key_exists( "HTTP_X_FORWARDED_FOR", $_SERVER ) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : "";
$this->clientAddress = $this->forwardedFor ? $this->forwardedFor : $this->remoteAddress;
}
static function DetermineLocale()
{
$locale = Input::Filter( array_get( $_COOKIE, "locale" ) );
if ( ! $locale ) $locale = "default";
return $locale;
}
static function DetermineWebsiteContentPath( $websitePath )
{
if ( string_has_suffix( $websitePath, ".test" ) )
{
$websitePath = substr( $websitePath, 0, -5 );
}
if ( is_dir( $websitePath . "/_content" ) )
{
return $websitePath . "/_content";
}
else
if ( is_dir( $websitePath . "/content" ) )
{
return $websitePath . "/content";
}
else
if ( is_dir( $websitePath . "/source/content" ) )
{
return $websitePath . "/source/content";
}
}
}
Access
class Access {
static function IsGranted( $restricted_path, $pageid )
{
if ( ! self::IsAccessDenied( $restricted_path, $pageid ) )
{
if ( !defined( "CSRF_TOKEN" ) ) define( "CSRF_TOKEN", "" );
if ( !defined( "KEYS" ) ) define( "KEYS", "" );
return (object) array
(
"csrf_token" => CSRF_TOKEN,
"keys" => KEYS
);
}
else
{
return false;
}
}
static function IsAccessDenied( $restricted_path, $pageid )
{
$is_denied = false;
//
// Only denies access if "RESTRICTED" file is present in _site dir.
//
if ( file_exists( $restricted_path ) )
{
$server_name = $_SERVER["SERVER_NAME"];
$apihost = "";
$apikey = "";
$list = null;
$json = file_get_contents( $restricted_path );
$json = preg_replace( '/\s+/', '', $json );
$prov = json_decode( $json );
if ( is_array( $prov ) )
{
$apihost = self::GenerateAPIHostname( $server_name );
$list = $prov;
}
else
if ( $prov->apihost && $prov->directories )
{
$apihost = str_replace( "[SERVER_NAME]", $server_name, $prov->apihost );
$list = $prov->directories;
if ( isset( $prov->{'apikey'} ) )
{
$apikey = $prov->apikey;
}
else
if ( isset( $prov->{'apikeys'} ) )
{
$apikeys = $prov->apikeys;
if ( is_array( $apikeys ) )
{
foreach( $apikeys as $obj )
{
if ( $server_name == $obj->server_name )
{
if ( property_exists( $obj, "apikey" ) && ("" != trim( $obj->apikey )) )
{
$apikey = $obj->apikey;
}
if ( property_exists( $obj, "apihost" ) && ("" != trim( $obj->apihost )) )
{
$apihost = $obj->apihost;
}
break;
}
}
}
}
}
if( ! $list || ! is_array( $list ) )
{
$is_denied = true;
error_log( "ACCESS DENIED: Invalid Configuration: " . $restricted );
}
else
{
foreach ( $list as $prefix )
{
if ( string_has_prefix( $pageid, $prefix ) )
{
error_log( "Checking: $prefix" );
$is_denied = self::VerifyAccessID( $apihost, $apikey );
break;
}
}
}
if ( ! $is_denied )
{
if ( !defined( "CSRF_TOKEN") && $apihost && $apikey )
{
$is_denied = self::RetrieveAndDefineCSRFToken( $apihost, $apikey );
}
error_log( "ACCESS PERMITTED" );
}
}
return $is_denied;
}
static function VerifyAccessID( $apihost, $apikey )
{
$is_denied = true;
$status = "";
if ( ! array_key_exists( "accessid", $_COOKIE ) )
{
$status = "Missing Access ID";
}
else
{
$accessid = $_COOKIE["accessid"];
if ( 0 == strlen( $accessid ) )
{
$status = "Empty Access ID";
}
else
if ( $is_denied = self::IsInvalidAccessID( $apihost, $apikey, $accessid ) )
{
$status = "Invalid";
}
}
if ( $is_denied )
{
error_log( "ACCESS DENIED: " . $status );
}
return $is_denied;
}
function SetCookie( $key, $value )
{
$is_local = string_has_suffix( $_SERVER['SERVER_NAME'], '.test' );
$cookie = "Set-Cookie: sid=";
if ( $is_local )
{
// SameSite is causing issues with local API server
// that uses self-signed certificates.
$cookie = $cookie . "; path=/; HttpOnly;secure";
}
else
{
$cookie = $cookie . "; path=/; HttpOnly;secure;SameSite=strict";
}
header( $cookie );
}
static function IsInvalidAccessID( $apihost, $apikey, $accessid )
{
$is_invalid = true;
$server_name =
$accessid = urlencode( $accessid );
$parameters[] = "apikey=" . $apikey;
$parameters[] = "server_name=" . $_SERVER["SERVER_NAME"];
$parameters[] = "remote_ip=" . $_SERVER["REMOTE_ADDR"];
$parameters[] = "Aid=" . $accessid;
$context = self::CreateStreamContext( $apihost, join( "&", $parameters ) );
error_log( "Authenticating using: $apihost ($accessid)" );
$json = null;
$attempts = 5;
do
{
$json = @file_get_contents( $apihost . "/auth/access/", false, $context );
if ( FALSE === $json )
{
$error = error_get_last();
error_log( "Could not connect to API host: $apihost ($attempts) -" . $error['message'] );
sleep( 1 );
}
}
while ( (FALSE === $json) && ($attempts-- > 0) );
if ( FALSE !== $json )
{
$obj = json_decode( $json );
if ( "OK" == $obj->status )
{
if ( 1 == count( $obj->results ) )
{
if ( "PERMITTED" == $obj->results[0]->access )
{
$is_invalid = false;
define( "CSRF_TOKEN", $obj->results[0]->CSRF_Token );
define( "KEYS", $obj->results[0]->Access_Keys );
}
}
}
if ( $is_invalid ) error_log( $json );
}
return $is_invalid;
}
static function RetrieveAndDefineCSRFToken( $apihost, $apikey )
{
$is_denied = true;
$accessid = array_key_exists( "accessid", $_COOKIE ) ? $_COOKIE["accessid"] : "";
$parameters[] = "apikey=" . $apikey;
$parameters[] = "server_name=" . $_SERVER["SERVER_NAME"];
$parameters[] = "remote_ip=" . $_SERVER["REMOTE_ADDR"];
$parameters[] = "Aid=" . $accessid;
$context = self::CreateStreamContext( $apihost, join( "&", $parameters ) );
error_log( "Authenticating using: " . $apihost );
$json = @file_get_contents( $apihost . "/auth/access/", false, $context );
if ( FALSE === $json )
{
$error = error_get_last();
error_log( "Could not connect to API host: $apihost - " . $error['message'] );
}
else
{
$obj = json_decode( $json );
if ( $obj && ("OK" == $obj->status) )
{
if ( 1 == count( $obj->results ) )
{
define( "CSRF_TOKEN", $obj->results[0]->CSRF_Token );
define( "KEYS", $obj->results[0]->Access_Keys );
$is_denied = false;
}
}
}
return $is_denied;
}
static function GenerateAPIHostname( $hostname )
{
$api_hostname = "api-" . $hostname;
return ("" !== $_SERVER["HTTPS"]) ? "https://" . $api_hostname : "http://" . $api_hostname;
}
/*
* Copied from BaseSchema Process Places script with permission.
* Copyright 2018 Daniel Bradley
*/
static function CreateStreamContext( $hostname, $content )
{
$verify_server = (false == self::IsLocalServer( $hostname ));
$NL = "\r\n";
$content_type = "Content-type: application/x-www-form-urlencoded" . $NL;
$content_length = "Content-Length: " . strlen( $content ) . $NL;
return stream_context_create
(
array
(
"http" => array
(
"method" => "POST",
"header" => $content_type . $content_length,
"content" => $content,
"ignore_errors" => true,
"timeout" => (float)2.0,
),
"ssl" => array
(
"allow_self_signed" => !$verify_server,
"verify_peer" => $verify_server,
"verify_peer_name" => $verify_server,
),
)
);
}
static function IsLocalServer( $hostname )
{
return
string_contains ( $hostname, ".local:" )
|| string_has_suffix( $hostname, ".local" )
|| string_contains ( $hostname, ".test:" )
|| string_has_suffix( $hostname, ".test" );
}
}
Page
<?php
class Page {
var $website_content;
var $locale;
var $document; // document.htm
var $html_start; // html_start.htm
var $head; // head.htm
var $head_title; // head.title.htm
var $head_meta; // head.meta.htm
var $head_csp; // head.csp.htm
var $head_links = array(); // head.link.htm
var $head_scripts = array(); // head.script.htm
var $body; // body.htm
var $body_header; // body.header.htm
var $body_main; // body.main.htm;
var $body_main_start; // body.main_start.htm;
var $body_main_header; // body.main.header.htm;
var $body_main_article; // body.main.article.htm;
var $body_main_article_start; // body.main.article_start.htm;
var $body_main_article_sections = array(); // body.main.article_start.htm;
var $body_main_aside; // body.main.aside.htm;
var $body_main_footer; // body.main.footer.htm;
var $body_footer; // body.footer.htm;
var $body_navs = array(); // body.navs
var $body_nav_start; // body.nav_start.htm;
var $body_nav_end; // body.nav_end.htm;
var $body_nav_breadcrumbs; // body.nav.breadcrumbs.htm;
var $body_menu; // body.menu.htm;
var $body_menu_start; // body.menu_start.htm;
var $body_menus = array(); // body.menu{1,2,3,4,5}.htm;
var $body_menu_end; // body.menu_end.htm;
var $body_dialogs; // body.dialogs.htm;
function __construct( $website_content, $locale, $page_dir, $keys, $website_name )
{
$this->website_content = $website_content;
$this->locale = $locale;
$this->document = self::ResolveFile( $website_content, $page_dir, "document", $keys, $website_name );
if ( !$this->document )
{
$this->html_start = self::ResolveFile( $website_content, $page_dir, "html_start", $keys, $website_name );
$this->head = self::ResolveFile( $website_content, $page_dir, "head", $keys, $website_name );
if ( !$this->head )
{
$this->head_title = self::ResolveFile( $website_content, $page_dir, "head.title", $keys, $website_name );
$this->head_meta = self::ResolveFile( $website_content, $page_dir, "head.meta", $keys, $website_name );
$this->head_csp = self::ResolveFile( $website_content, $page_dir, "head.csp", $keys, $website_name );
if ( ($path = self::ResolveFile( $website_content, $page_dir, "head.link", $keys, $website_name )) )
{
$this->head_links[] = $path;
}
$i = 1;
while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.link" . $i . "", $keys, $website_name )) )
{
$i++;
$this->head_links[] = $path;
}
if ( ($path = self::ResolveFile( $website_content, $page_dir, "head.styles", $keys, $website_name )) )
{
$this->head_links[] = $path;
}
$i = 1;
while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.styles" . $i . "", $keys, $website_name )) )
{
$i++;
$this->head_links[] = $path;
}
if ( ($path = self::ResolveFile( $website_content, $page_dir, "head.script", $keys, $website_name )) )
{
$this->head_scripts[] = $path;
}
$i = 1;
while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.script" . $i . "", $keys, $website_name )) )
{
$i++;
$this->head_scripts[] = $path;
}
if ( ($path = self::ResolveFile( $website_content, $page_dir, "head.javascript", $keys, $website_name )) )
{
$this->head_scripts[] = $path;
}
$i = 1;
while ( ($path = self::ResolveFile( $website_content, $page_dir, "head.javascript" . $i . "", $keys, $website_name )) )
{
$i++;
$this->head_scripts[] = $path;
}
}
$this->body = self::ResolveFile( $website_content, $page_dir, "body", $keys, $website_name );
if ( !$this->body )
{
$this->body_header = self::ResolveFile( $website_content, $page_dir, "body.header", $keys, $website_name );
$this->body_nav = self::ResolveFile( $website_content, $page_dir, "body.nav", $keys, $website_name );
if ( !$this->body_nav )
{
$this->body_nav_start = self::ResolveFile( $website_content, $page_dir, "body.nav_start", $keys, $website_name );
$this->body_menu_start = self::ResolveFile( $website_content, $page_dir, "body.menu_start", $keys, $website_name );
if ( $this->body_nav_start )
{
$this->body_nav_breadcrumbs = self::ResolveFile( $website_content, $page_dir, "body.nav.breadcrumbs", $keys, $website_name );
$i = 1;
while ( ($nav_line = self::ResolveFile( $website_content, $page_dir, "body.nav" . $i . "", $keys, $website_name )) )
{
$i++;
$this->body_navs[] = $nav_line;
}
if ( !$this->body_menu_start && empty( $this->body_navs ) )
{
$i = 1;
while ( ($nav_line = self::ResolveFile( $website_content, $page_dir, "body.menu" . $i . "", $keys, $website_name )) )
{
$i++;
$this->body_navs[] = $nav_line;
}
}
$this->body_nav_end = self::ResolveFile( $website_content, $page_dir, "body.nav_end", $keys, $website_name );
}
}
$this->body_main = self::ResolveFile( $website_content, $page_dir, "body.main", $keys, $website_name, true );
if ( !$this->body_main )
{
$this->body_main_start = self::ResolveFile( $website_content, $page_dir, "body.main_start", $keys, $website_name, true );
$this->body_main_header = self::ResolveFile( $website_content, $page_dir, "body.main_header", $keys, $website_name, true );
$this->body_main_article = self::ResolveFile( $website_content, $page_dir, "body.main.article", $keys, $website_name, true );
if ( !$this->body_main_article )
{
$this->body_main_article_start = self::ResolveFile( $website_content, $page_dir, "body.main.article_start", $keys, $website_name, true );
$i = 1;
while ( ($section = self::ResolveFile( $website_content, $page_dir, "body.main.article.section" . $i, $keys, $website_name, true )) )
{
$i++;
$this->body_main_article_sections[] = $section;
}
}
$this->body_main_aside = self::ResolveFile( $website_content, $page_dir, "body.main.aside", $keys, $website_name, true );
$this->body_main_footer = self::ResolveFile( $website_content, $page_dir, "body.main_footer", $keys, $website_name, true );
}
$this->body_footer = self::ResolveFile( $website_content, $page_dir, "body.footer", $keys, $website_name, true );
}
$this->body_menu = self::ResolveFile( $website_content, $page_dir, "body.menu", $keys, $website_name );
if ( !$this->body_menu )
{
$this->body_menu_start = self::ResolveFile( $website_content, $page_dir, "body.menu_start", $keys, $website_name );
if ( $this->body_menu_start )
{
$i = 1;
while ( ($menu_line = self::ResolveFile( $website_content, $page_dir, "body.menu" . $i . "", $keys, $website_name )) )
{
$i++;
$this->body_menus[] = $menu_line;
}
$this->body_menu_end = self::ResolveFile( $website_content, $page_dir, "body.menu_end", $keys, $website_name );
}
}
$this->body_dialogs = self::ResolveFile( $website_content, $page_dir, "body.dialogs", $keys, $website_name );
}
}
ResolveFile
Resolve file will determine the appropriate file that is storing a html element fragment. For example, the following call will search for fileds in the following order:
ResolveFile ( "/srv/DROPSPACE/sites/com.example/_content", "dashboard-settings", "body.main.article", array( "CLIENT", "COMPANY" ), "com.example" );
/srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT-COMPANY@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT-COMPANY.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article-CLIENT.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/body.main.article.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT-COMPANY@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/article@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT-COMPANY.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/article-CLIENT.htm /srv/DROPSPACE/sites/com.example/_content/dashboard-example/article.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT-COMPANY@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT-COMPANY.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article-CLIENT.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/body.main.article.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT-COMPANY@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/article@com.example.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT-COMPANY.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/article-CLIENT.htm /srv/DROPSPACE/sites/com.example/_content/dashboard/article.htm /srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT-COMPANY@com.example.htm /srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT@com.example.htm /srv/DROPSPACE/sites/com.example/_content/_site/body.main.article@com.example.htm /srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT-COMPANY.htm /srv/DROPSPACE/sites/com.example/_content/_site/body.main.article-CLIENT.htm /srv/DROPSPACE/sites/com.example/_content/_site/body.main.article.htm /srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT-COMPANY@com.example.htm /srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT@com.example.htm /srv/DROPSPACE/sites/com.example/_content/_site/article@com.example.htm /srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT-COMPANY.htm /srv/DROPSPACE/sites/com.example/_content/_site/article-CLIENT.htm /srv/DROPSPACE/sites/com.example/_content/_site/article.htm
static function ResolveFile( $website_content, $pageDir, $element, $keys, $website_name, $one_level = false )
{
$file = null;
$files = self::GenerateProvisionalFileList( $website_content, $pageDir, $element, $keys, $website_name, $one_level );
foreach( $files as $filepath )
{
if ( file_exists( $filepath ) )
{
$file = $filepath;
break;
}
if ( "body.main.article_start" == $element )
{
$yes = $file ? "Yes" : "No ";
error_log( "$element option: $yes - $filepath" );
}
}
return $file;
}
Generate Provisional File List
Each file can contain the following components:
static function GenerateProvisionalFileList( $website_content, $pageDir, $element, $keys, $website_name, $one_level )
{
if ( "body.main_start" == $element )
{
error_log( "GenerateProvisionalFileList( '$website_content', '$pageDir', '$element', '$keys', '$website_name', '$one_level' )" );
}
$files = array();
$array_paths = explode( '-', $pageDir );
while( true )
{
if ( !empty( $array_paths ) )
{
$p = trim( implode( '-', $array_paths ) );
}
else
{
$p = "_site";
}
$files = array_merge( $files, self::GenerateProvisionalFileListSubset( "$website_content/$p", $element, $keys, "@" . $website_name ) );
$files = array_merge( $files, self::GenerateProvisionalFileListSubset( "$website_content/$p", $element, $keys ) );
if ( ("_site" == $p) || $one_level )
{
break;
}
else
{
$array_paths = array_slice( $array_paths, 0, -1 );
}
}
return $files;
}
Given:
- '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' ]
$array_elements = explode( '.', $element ); // [ 'body', 'main', 'article_start' ]
while( ! empty( $array_elements ) )
{
$e = implode( '.', $array_elements ); // "body.main.article_start"
$k = implode( '-', $array_keys ); // "CLIENT-company"
if ( $k )
{
$f = $path . "/" . $e . "-" . $k . $website_name . ".htm";
}
else
{
$f = $path . "/" . $e . $website_name . ".htm";
}
$files[] = $f;
$array_elements = array_slice( $array_elements, 1 ); // [ 'main', 'article_start' ]
}
}
while( ! empty( $array_keys ) );
return $files;
}
function FileGetContentsIfExists( $filename, $translate = false )
{
$locale = $this->locale ? $this->locale : "default";
$content = ($filename && file_exists( $filename )) ? file_get_contents( $filename ) : "";
if ( $translate && $content )
{
if ( $locale = $this->RetrieveLocaleJSON( $locale ) )
{
$doc = new DOMDocument();
$doc->loadHTML( $content, LIBXML_HTML_NODEFDTD );
self::ProcessDOMDocument( $doc, $locale, "a" );
self::ProcessDOMDocument( $doc, $locale, "button" );
self::ProcessDOMDocument( $doc, $locale, "h1" );
self::ProcessDOMDocument( $doc, $locale, "span" );
self::ProcessDOMDocument( $doc, $locale, "th" );
self::ProcessDOMDocument( $doc, $locale, "b" );
self::ProcessDOMDocument( $doc, $locale, "i" );
// Replace placeholder text.
self::ProcessDOMDocument( $doc, $locale, "input" );
$content = $doc->saveHTML();
}
}
return $content;
}
function RetrieveLocaleJSON( $locale )
{
$obj = null;
if ( "default" != $locale )
{
$json = file_get_contents( $this->website_content . "/_i18n/$locale.json" );
try
{
$obj = json_decode( $json );
}
catch ( Exception $ex )
{
// Ignore exception.
}
}
return $obj;
}
static function ProcessDOMDocument( $dom, $locale, $tag )
{
$nodes = $dom->getElementsByTagName( $tag );
foreach ( $nodes as $node )
{
$key = trim( $node->nodeValue );
if ( property_exists( $locale, $key ) )
{
if ( $value = $locale->{$key} )
{
$node->nodeValue = $value;
error_log( "Changing $key for $value" );
}
}
if ( $node->hasAttributes() )
{
$attributes = $node->attributes;
$pNode = $attributes->getNamedItem( "placeholder" );
if ( $pNode )
{
$pKey = trim( $pNode->nodeValue );
if ( property_exists( $locale, $pKey ) )
{
if ( $pValue = $locale->{$pKey} )
{
$pNode->nodeValue = $pValue;
}
}
}
}
}
}
function render( $configuration, $csrf_token )
{
if ( $this->document )
{
echo trim( self::FileGetContentsIfExists( $this->document ) );
}
else
{
$this->headers();
$this->doctype();
$this->html_start();
$this->head();
$this->debug( $configuration, $csrf_token );
$this->body ( $configuration, $csrf_token );
$this->html_end();
}
}
function headers()
{
}
function doctype()
{
echo "<!DOCTYPE html>";
}
function html_start()
{
if ( $this->html_start )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->html_start ) );
}
else
{
echo "\n" . "<html>";
}
}
function head()
{
if ( $this->head )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->head ) );
}
else
{
echo "\n" . "<head>";
if ( $this->head_title )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->head_title ) );
}
if ( $this->head_meta )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->head_meta ) );
}
if ( $this->head_csp )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->head_csp ) );
}
foreach ( $this->head_links as $link_line )
{
echo "\n" . trim( self::FileGetContentsIfExists( $link_line, true ) );
}
foreach ( $this->head_scripts as $script_line )
{
echo "\n" . trim( self::FileGetContentsIfExists( $script_line, true ) );
}
echo "\n" . "</head>";
}
}
function debug( $configuration, $csrf_token )
{
if ( "TRUE" == getenv( "SHOW_DEBUG" ) )
{
echo "\n" . "<!--";
echo "\n" . "CSRF_TOKEN: " . $csrf_token;
echo $configuration->toString();
echo $this->toString();
echo "\n" . "-->";
}
}
function body( $configuration, $csrf_token )
{
if ( $this->body )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body ) );
}
else
{
echo "\n" . self::generateBodyStartTag( $configuration, $csrf_token );
if ( $this->body_main )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main, true ) );
}
else
{
if ( $this->body_main_start )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_start ) );
}
else
{
echo "\n" . "<main>";
}
if ( $this->body_main_header )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_header, true ) );
}
if ( $this->body_main_aside )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_aside, true ) );
}
if ( $this->body_main_article )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_article, true ) );
}
else
if ( $this->body_main_article_start )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_article_start, true ) );
foreach ( $this->body_main_article_sections as $section )
{
echo "\n" . trim( self::FileGetContentsIfExists( $section, true ) );
}
echo "\n" . "</article>";
}
if ( $this->body_main_footer )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_main_footer, true ) );
}
echo "\n" . "</main>";
}
if ( $this->body_footer )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_footer, true ) );
}
if ( $this->body_nav )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav, true ) );
}
else
{
$n = count( $this->body_navs );
if ( 0 == $n )
{
if ( $this->body_nav_start )
{
echo "\n" . trim( str_replace( '%nav_id%', 'nav1', self::FileGetContentsIfExists( $this->body_nav_start ) ) );
}
else
{
echo "\n" . str_replace( '%nav_id%', 'nav1', "<nav id='%nav_id%'>" );
}
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_breadcrumbs, true ) );
if ( $this->body_nav_end )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_end ) );
}
else
{
echo "\n" . "</nav>";
}
}
else
{
$i = 0;
foreach ( $this->body_navs as $nav_line )
{
$i++;
$nav_id = "nav$i";
if ( $this->body_nav_start )
{
echo "\n" . trim( str_replace( '%nav_id%', $nav_id, self::FileGetContentsIfExists( $this->body_nav_start ) ) );
}
else
{
echo "\n" . str_replace( '%nav_id%', $nav_id, "<nav id='%nav_id%'>" );
}
if ( $i == $n )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_breadcrumbs, true ) );
}
echo "\n" . trim( self::FileGetContentsIfExists( $nav_line, true ) );
if ( $this->body_nav_end )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_nav_end ) );
}
else
{
echo "\n" . "</nav>";
}
}
}
}
if ( $this->body_header )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_header, true ) );
}
if ( $this->body_menu )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_menu, true ) );
}
else
{
if ( $this->body_menu_start )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_menu_start, true ) );
}
foreach ( $this->body_menus as $menu_line )
{
echo "\n" . trim( self::FileGetContentsIfExists( $menu_line, true ) );
}
if ( $this->body_menu_end )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_menu_end, true ) );
}
}
if ( $this->body_dialogs )
{
echo "\n" . trim( self::FileGetContentsIfExists( $this->body_dialogs, true ) );
}
echo "\n" . "</body>";
}
}
function generateBodyStartTag( $configuration, $csrf_token )
{
$bits = array
(
"<body",
"id='" . $configuration->pageID . "'",
"class='" . $configuration->browser . "'",
"data-hostname='" . $configuration->domain . "'",
"data-csrf='" . $csrf_token . "'",
">"
);
return implode( ' ', $bits );
}
function html_end()
{
echo "\n" . "</html>";
}
}
ObjectQueue
<?php
// Copyright (c) 2022, Daniel Robert Bradley. All rights reserved.
// This software is distributed under the terms of the GNU Lesser General Public License version 2.1
?>
<?php
class Alt {
static function Log( $address_value, $website_domain, $website_page_path )
{
$apihost = defined( "ALTLOG_APIHOST" ) ? ALTLOG_APIHOST : Array_Get( $_SERVER, "ALTLOG_APIHOST" );
$apikey = defined( "ALTLOG_APIKEY" ) ? ALTLOG_APIKEY : Array_Get( $_SERVER, "ALTLOG_APIKEY" );
if ( $apihost && $apikey )
{
$endpoint = $apihost . "/api/log/";
$parameters = "address_value=$address_value"
. "&"
. "website_domain=$website_domain"
. "&"
. "website_page_path=$website_page_path"
. "&"
. "apikey=" . $apikey;
$context = Access::CreateStreamContext( $apihost, $parameters );
if ( false === file_get_contents( $endpoint, false, $context ) )
{
error_log( "Error while logging to: $apihost" );
}
else
{
error_log( "Log: $address_value => $website_domain$website_page_path" );
}
}
}
}
A -- Strings
<?php
function array_get( $array, $key )
{
return array_key_exists( $key, $array ) ? $array[$key] : "";
}
function string_contains( $haystack, $needle )
{
return (0 == strlen($needle)) || (false !== strpos( $haystack, $needle ));
}
function string_has_prefix( $haystack, $needle )
{
return (0 == strlen($needle)) || (0 === strpos( $haystack, $needle ));
}
function string_has_suffix( $haystack, $needle )
{
$expected = strlen( $haystack ) - strlen( $needle );
return (0 == strlen($needle)) || ($expected === strrpos( $haystack, $needle ));
}
function test()
{
if ( string_contains( "www.example.com", "" ) ) echo "Passed #1" . "\n";
if ( string_contains( "www.example.com", "www." ) ) echo "Passed #2" . "\n";
if ( string_contains( "www.example.com", ".example." ) ) echo "Passed #3" . "\n";
if ( string_contains( "www.example.com", ".com" ) ) echo "Passed #4" . "\n";
if ( !string_contains( "www.example.com", "wwl" ) ) echo "Passed #5" . "\n";
if ( !string_contains( "www.example.com", ".exanple" ) ) echo "Passed #6" . "\n";
if ( !string_contains( "www.example.com", ".con" ) ) echo "Passed #7" . "\n";
if ( string_has_prefix( "www.example.com", "" ) ) echo "Passed #8" . "\n";
if ( string_has_prefix( "www.example.com", "www." ) ) echo "Passed #9" . "\n";
if ( string_has_prefix( "www.example.com", "www.example." ) ) echo "Passed #A" . "\n";
if ( string_has_prefix( "www.example.com", "www.example.com" ) ) echo "Passed #B" . "\n";
if ( !string_has_prefix( "www.example.com", "wwl" ) ) echo "Passed #C" . "\n";
if ( !string_has_prefix( "www.example.com", "www.exanple" ) ) echo "Passed #D" . "\n";
if ( !string_has_prefix( "www.example.com", "www.example.con" ) ) echo "Passed #E" . "\n";
if ( string_has_suffix( "www.example.com", "" ) ) echo "Passed #F" . "\n";
if ( string_has_suffix( "www.example.com", ".com" ) ) echo "Passed #G" . "\n";
if ( string_has_suffix( "www.example.com", ".example.com" ) ) echo "Passed #H" . "\n";
if ( string_has_suffix( "www.example.com", "www.example.com" ) ) echo "Passed #I" . "\n";
if ( !string_has_suffix( "www.example.com", ".con" ) ) echo "Passed #J" . "\n";
if ( !string_has_suffix( "www.example.com", ".example.con" ) ) echo "Passed #K" . "\n";
if ( !string_has_suffix( "www.example.com", "www.example.con" ) ) echo "Passed #L" . "\n";
}
CanonicalisePath
<?php
function CanonicalisePath( $path )
{
$path = Input::Filter( $path );
if ( '.php' == substr( $path, -4 ) )
{
$path = dirname( $path );
}
if ( '/' != substr( $path, -1 ) )
{
$path .= "/";
}
return $path;
}
function IsHTTPS()
{
return ( !empty( $_SERVER['HTTPS'] ) && ($_SERVER['HTTPS'] !== 'off') );
}
/**************************************************************************
* Below here are private helper methods.
**************************************************************************/
/*
* Converts uri to form 'page-subpage-index', used to uniquely identify pages.
*/
function _generatePageId( $append_index = true )
{
$uri = REDIRECT_URL;
$uri = substr( $uri, 1 );
$uri = str_replace( "/", "-", $uri );
if ( $append_index )
{
$uri = $uri . "index";
}
else
if ( "" == $uri )
{
$uri = "index";
}
else
{
$uri = substr( $uri, 0, -1 );
}
return $uri;
//$id = (0 == stripos( $uri, "/page/" )) ? $uri : substr( $uri, stripos( $uri, "/page/" ) + 5 );
//$page_id = substr( $uri, 1 );
//return str_replace( "/", "-", $uri );
}
function GeneratePageId( $uri )
{
$uri = substr( $uri, 1 );
$uri = str_replace( "/", "-", $uri );
$uri = $uri . "index";
return $uri;
//$id = (0 == stripos( $uri, "/page/" )) ? $uri : substr( $uri, stripos( $uri, "/page/" ) + 5 );
//$page_id = substr( $uri, 1 );
//return str_replace( "/", "-", $uri );
}
function GeneratePageDir( $uri )
{
$id = GeneratePageId( $uri );
$dir = "";
if ( string_has_suffix( $id, "-index" ) )
{
$dir = str_replace( "-index", " ", $id );
}
else
if ( "index" == $id )
{
$dir = "_index";
}
return $dir;
}
/*
* Converts each element of uri to Title Case e.g. 'Page/Subpage'.
*/
function GeneratePagePath( $redirect_url )
{
$path = "";
{
$uri = substr( $redirect_url, 1, -1 );
$bits = explode( "/", $uri );
foreach ( $bits as $bit )
{
$path .= "/" . ToTitleCase( $bit );
}
}
return $path;
}
function ToTitleCase( $string )
{
$ret = "";
$bits = explode( "_", $string );
foreach ( $bits as $bit )
{
if ( !empty( $bit ) ) $ret .= " " . strtoupper( $bit[0] ) . substr( $bit, 1 );
}
return substr( $ret, 1 );
}
function determineIfMobile( $http_user_agent )
{
// See:
// http://stackoverflow.com/questions/6636306/mobile-browser-detection
return (bool)preg_match('#\b(ip(hone|od)|android\b.+\bmobile|opera m(ob|in)i|windows (phone|ce)|blackberry'.
'|s(ymbian|eries60|amsung)|p(alm|rofile/midp|laystation portable)|nokia|fennec|htc[\-_]'.
'|up\.browser|[1-4][0-9]{2}x[1-4][0-9]{2})\b#i', $http_user_agent );
}
function determineBrowser( $http_user_agent )
{
$type = "XXX";
if ( string_contains( $http_user_agent, "Trident" ) )
{
$type = "IE";
}
else
if ( string_contains( $http_user_agent, "Chrome" ) )
{
if ( string_contains( $http_user_agent, "Windows" ) )
{
$type = "CHROME";
}
else
{
$type = "WEBKIT";
}
}
else
if ( string_contains( $http_user_agent, "WebKit" ) )
{
$type = "WEBKIT";
}
else
{
$type = "MOZ";
}
return $type;
}
function DetermineSitenameReversed( $domain, $sites_path )
{
// If .local domain (dropspace.org.local),
// will need to rearrange to be 'org.dropspace.local',
// so remove '.local', and make 'is_local' true.
//
$is_local = string_has_suffix( $domain, ".local" );
$is_test = string_has_suffix( $domain, ".test" );
$is_next = string_has_suffix( $domain, ".next" );
//error_log( $is_local );
if ( $is_local ) $domain = preg_replace( "/\.local\z/", "", $domain );
if ( $is_test ) $domain = preg_replace( "/\.test\z/", "", $domain );
if ( $is_next ) $domain = preg_replace( "/\.next\z/", "", $domain );
//error_log( $domain );
$sitename = Reverse( $domain ); // www.dropspace.org --> org.dropspace.www
$sitename = preg_replace( "/\.www\z/", "", $sitename ); // org.dropspace.www --> org.dropspace
if ( $is_local ) $sitename = "$sitename.local"; // org.dropspace --> org.dropspace.local
if ( $is_test ) $sitename = "$sitename.test"; // org.dropspace --> org.dropspace.test
if ( $is_next ) $sitename = "$sitename.next"; // org.dropspace --> org.dropspace.next
return $sitename;
}
function Reverse( $domain )
{
$bits = explode( ".", $domain );
$bits = array_reverse( $bits );
return implode( ".", $bits );
}
Input
<?php
// Copyright (c) 2009, 2010 Daniel Robert Bradley. All rights reserved.
// This software is distributed under the terms of the GNU Lesser General Public License version 2.1
?>
<?php
class Output
{
function println()
{}
function indent()
{}
function outdent()
{}
}
function DBi_escape( $string )
{
return $string;
$db = DBi_anon();
if ( $db->connect( new NullPrinter() ) )
{
return $db->escape( $string );
}
}
class Input
{
static function FilterInput( $request, $debug )
{
$debug = new Output();
$filtered = array();
$debug->println( "<!-- FilterInput() start -->" );
$debug->indent();
{
$debug->println( "<!-- REQUEST -->" );
$debug->indent();
{
foreach ( $_REQUEST as $key => $val )
{
$filtered_key = Input::Filter( $key );
$filtered_val = Input::Filter( $val );
$filtered[$filtered_key] = $filtered_val;
if ( is_array( $filtered_val ) )
{
$debug->println( "<!-- \"$filtered_key\" | Array -->" );
}
else
{
$debug->println( "<!-- \"$filtered_key\" | \"$filtered_val\" -->" );
}
}
}
$debug->outdent();
$debug->println( "<!-- COOKIE -->" );
$debug->indent();
{
foreach ( $_COOKIE as $key => $val )
{
if ( ! array_key_exists( $key, $filtered ) )
{
$filtered_key = Input::Filter( $key );
$filtered_val = Input::Filter( $val );
$filtered[$filtered_key] = $filtered_val;
$debug->println( "<!-- \"$filtered_key\" | \"$filtered_val\" -->" );
}
}
}
$debug->outdent();
}
$debug->outdent();
$debug->println( "<!-- FilterInput() end -->" );
return $filtered;
}
static function Filter( $value )
{
if ( is_array( $value ) )
{
$ret = array();
foreach ( $value as $key => $val )
{
$filtered_key = Input::Filter( $key );
$filtered_val = Input::Filter( $val );
$ret[$filtered_key] = $filtered_val;
}
return $ret;
}
else
if ( is_string( $value ) )
{
//$value = Input::unidecode( $value );
$value = utf8_decode( $value );
$value = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8', false );
$value = addslashes( $value );
$value = DBi_escape( $value );
$value = str_replace( "\n", "<br>", $value );
$value = str_replace( "\\\\", "\", $value );
$value = str_replace( "\x09", " ", $value );
return $value;
}
else
if ( is_null( $value ) )
{
return "";
}
else
{
error_log( "Input::Filter( $value ): unexpected value!" );
}
}
static function unidecode( $value )
{
$str = "";
$n = strlen( $value );
$i = 0;
while ( $i < $n )
{
$ch = substr( $value, $i, 1 );
$val = ord( $ch );
if ( ($val == (0xFC | $val)) && ($i+5 < $n) ) // 6 byte unicode
{
$str .= Input::utf2html( substr( $value, $i, 6 ) );
$i += 6;
}
else
if ( ($val == (0xF8 | $val)) && ($i+4 < $n) ) // 5 byte unicode
{
$str .= Input::utf2html( substr( $value, $i, 5 ) );
$i += 5;
}
else
if ( ($val == (0xF0 | $val)) && ($i+3 < $n) ) // 4 byte unicode
{
$str .= Input::utf2html( substr( $value, $i, 4 ) );
$i += 4;
}
else
if ( ($val == (0xE0 | $val)) && ($i+2 < $n) ) // 3 byte unicode
{
$str .= Input::utf2html( substr( $value, $i, 3 ) );
$i += 3;
}
else
if ( ($val == (0xC0 | $val)) && ($i+1 < $n) ) // 2 byte unicode
{
$str .= Input::utf2html( substr( $value, $i, 2 ) );
$i += 2;
}
else
if ( $val == (0x80 | $val) ) // extra byte
{
error_log( "Warning detected invalid unicode" );
$str .= '?';
$i++;
}
else // ascii character
{
$str .= $ch;
$i++;
}
}
return $str;
}
static function utf2html( $string )
{
$array = Input::utf8_to_unicode( $string );
$string = Input::unicode_to_entities( $array );
return $string;
}
static function utf8_to_unicode( $str )
{
$unicode = array();
$values = array();
$lookingFor = 1;
for ($i = 0; $i < strlen( $str ); $i++ ) {
$thisValue = ord( $str[ $i ] );
if ( $thisValue < 128 ) $unicode[] = $thisValue;
else {
if ( count( $values ) == 0 ) $lookingFor = ( $thisValue < 224 ) ? 2 : 3;
$values[] = $thisValue;
if ( count( $values ) == $lookingFor ) {
$number = ( $lookingFor == 3 ) ?
( ( $values[0] % 16 ) * 4096 ) + ( ( $values[1] % 64 ) * 64 ) + ( $values[2] % 64 ):
( ( $values[0] % 32 ) * 64 ) + ( $values[1] % 64 );
$unicode[] = $number;
$values = array();
$lookingFor = 1;
} // if
} // if
} // for
return $unicode;
}
static function unicode_to_entities( $unicode )
{
$entities = '';
foreach( $unicode as $value ) $entities .= '' . $value . ';';
return $entities;
}
}
Scripts
G - Generate Apache Configurations
#!/bin/bash
#
# This script will, for each directory in the Dropspace sites directory,
# create an Apache2 configuration file called 'JUXTAPAGE.<directory name>.conf',
# which is a copy of the 'JUXTAPAGE.TEMPLATE.conf' file, that has each occurrance
# of the pattern %domain_name% replaced with <directory name>.
#
# It needs to know:
#
# 1) Where the Document root is - 'sites' is directly under it, and
# 2) The location of the Apache configuration directory (/etc/apache2 by default).
DATE_FORMAT="+%Y-%m-%dT%H:%M:%S%z"
SITES_DIR=""
APACHE_DOCUMENT_ROOT=""
APACHE_CONFIG_DIR=""
APACHE_TEMPLATE_FILE=""
APPEND=""
DOMAIN=""
ALLOW_IP=""
ALLOW_IP_BEHIND_LB=""
RESTART=""
function Usage()
{
echo "Usage: generate_apache_configurations.sh [--test] <Dropbox dir> <Document root> <Apache config dir> [--template <Apache template file>] [--domain <reverse domain> [--allow-ip <ip>,...]]"
echo "E.g., generate_apache_configurations.sh --test /home/<account>/Dropbox /srv/JUXTAPAGE /etc/apache2/sites-enabled --template /etc/apache2/juxtapage.apache.template --domain com.example --allow-ip 192.168.0.1"
}
function main()
{
Environment "$@" &&
Generate
if [ "true" == "$RESTART" ]
then
if apachectl configtest
then
service apache2 restart
else
echo "Configuration error - did not restart Apache"
fi
fi
}
function Environment()
{
if [ "$1" == "--test" ]
then
APPEND=".test"
shift
fi
SITES_DIR="$1"; shift
APACHE_DOCUMENT_ROOT="$1"; shift
APACHE_CONFIG_DIR="$1"; shift
while [ -n "$1" ]
do
case "$1" in
"--template")
shift
APACHE_TEMPLATE_FILE="$1"
shift
;;
"--domain")
shift
DOMAIN="$1"
shift
if [ "$1" == "--allow-ip" ]
then
echo "XXX"
shift
ALLOW_IP=`GenerateAllowIP $1`
ALLOW_IP_BEHIND_LB=""
shift
elif [ "$1" == "--allow-ip-behind-lb" ]
then
shift
ALLOW_IP="<RequireAny>\n Require env allowed\n </RequireAny>"
ALLOW_IP_BEHIND_LB=`GenerateAllowIPBehindLoadBalander $1`
shift
else
ALLOW_IP="Require all granted"
ALLOW_IP_BEHIND_LB=""
fi
;;
"--restart")
shift
RESTART="true"
;;
*)
echo "Unexpected argument"
Usage
return -1
;;
esac
done
echo "$ALLOW_IP"
if [ ! -d "$SITES_DIR" ]
then
echo "Invalid Sites directory: $SITES_DIR"
Usage
return -1
elif [ ! -d "$APACHE_DOCUMENT_ROOT" ]
then
echo "Invalid Document Root directory: $APACHE_DOCUMENT_ROOT"
Usage
return -1
elif [ ! -d "$APACHE_CONFIG_DIR" -a ! -d "$APACHE_CONFIG_DIR/sites-enabled" ]
then
echo "Invalid Apache config dir: $apache_config_dir"
Usage
return -1
fi
}
function GenerateAllowIP()
{
local ipaddresses=`echo $1 | sed "s|,| |g"`
local allow_ip="<RequireAny>"
for i in $ipaddresses
do
allow_ip+="\n Require ip $i"
done
allow_ip+=" </RequireAny>"
echo $allow_ip
}
function GenerateAllowIPBehindLoadBalander()
{
local ipaddresses=`echo $1 | sed "s|,| |g"`
local allow_ip=""
for i in $ipaddresses
do
allow_ip+="\n SetEnvIf X-Forwarded-For ^$i allowed"
done
echo $allow_ip
}
function Generate()
{
echo process_subdirectories "$SITES_DIR" "$APACHE_DOCUMENT_ROOT" "$APACHE_CONFIG_DIR" "$APACHE_TEMPLATE_FILE" "$APPEND"
process_subdirectories "$SITES_DIR" "$APACHE_DOCUMENT_ROOT" "$APACHE_CONFIG_DIR" "$APACHE_TEMPLATE_FILE" "$APPEND"
}
function process_subdirectories()
{
local sites_dir=$1 # /srv/JUXTAPAGE/sites
local document_root=$2 # /srv/JUXTAPAGE
local apache_config_target=$3 # /etc/apache2/sites-enabled
local apache_template_file=$4 # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/configuration/apache2/JUXTAPAGE.TEMPLATE.conf
local append=$5 # .test
if [ -z "$apache_template_file" ]
then
local script_name=`scriptname` # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/cron/generate_apache_configurations/generate_apache_configurations.sh
local script_dir=`dirname "$script_name"` # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/cron/generate_apache_configurations
local libexec_dir=`dirname "$script_dir"` # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/cron
local juxtapage_dir=`dirname "$libexec_dir"` # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage
local apache_template_dir="$juxtapage_dir/configuration/apache2" # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/configuration/apache2
local apache_config_template="$apache_template_dir/JUXTAPAGE.TEMPLATE.conf" # /srv/JUXTAPAGE/JuxtaPage/latest/juxtapage/configuration/apache2/JUXTAPAGE.TEMPLATE.conf
apache_template_file="$apache_config_template"
fi
if [ -z "$apache_config_target" ]
then
apache_config_target="/etc/apache2/sites-enabled"
fi
local target="$apache_config_target"
if [ ! -f "$apache_template_file" ]
then
echo "Internal error: could not find Apache configuration template file at: $apache_template_file"
exit
else
for domain_dir in $( ls "$sites_dir" )
do
if [ "" == "$DOMAIN" -o "$DOMAIN" == "$domain_dir" ]
then
if [ -d "$sites_dir/$domain_dir" -a "_" != ${domain_dir:0:1} ]
then
if string_contains "$domain_dir" "."
then
local target_file="$target/JUXTAPAGE.$domain_dir$append.conf"
local domain_name="$domain_dir"
local tld=`echo "$domain_dir" | cut -f1 -d'.'`
local now=`now`
if [ "$DOMAIN" == "$domain_dir" ]
then
echo "$now: Considering: $DOMAIN"
fi
if is_tld $tld
then
domain_name=`reverse_domain $domain_dir`
fi
domain_name="$domain_name$append"
if [ "$DOMAIN" == "$domain_dir" ]
then
rm -rf $target/JUXTAPAGE.$domain_dir$append.ssl.conf $target/JUXTAPAGE.$domain_dir$append-le-ssl.conf
echo "$now: Reprocessing directory: $domain_dir for domain: $domain_name"
echo "$now: Creating: $target/JUXTAPAGE.$domain_dir$append.conf"
sed "s|%document_root%|$document_root|g" "$apache_template_file" | sed "s|%domain_name%|$domain_name|g" | sed "s|%sites_path%|$sites_dir|g" | sed "s|%domain_dir%|$domain_dir|g" | sed "s|%allow_ip%|$ALLOW_IP|g" | sed "s|%allow_ip_behind_load_balancer%|$ALLOW_IP_BEHIND_LB|g" | sed "s|%now|$now|g" > "$target_file"
elif [ ! -f "$target_file" -a ! -f "$target/JUXTAPAGE.$domain_dir$append-le-ssl.conf" -a ! -f "$target/JUXTAPAGE.$domain_dir$append.ssl.conf" ]
then
echo "$now: Processing directory: $domain_dir for domain: $domain_name"
echo "$now: Creating: $target/JUXTAPAGE.$domain_dir$append.conf"
sed "s|%document_root%|$document_root|g" "$apache_template_file" | sed "s|%domain_name%|$domain_name|g" | sed "s|%sites_path%|$sites_dir|g" | sed "s|%domain_dir%|$domain_dir|g" | sed "s|%allow_ip%|$ALLOW_IP|g" | sed "s|%allow_ip_behind_load_balancer%|$ALLOW_IP_BEHIND_LB|g" | sed "s|%now|$now|g" > "$target_file"
fi
fi
fi
fi
done
fi
}
#
# Utility functions
#
function scriptname()
{
echo $(readlink -f $0)
}
function string_ends_with()
{
local haystack=$1
local suffix=$2
if [ -z "${haystack##*$suffix}" ]
then
return 0 # success
else
return -1 # failure
fi
}
function string_contains()
{
local haystack=$1
local value=$2
if [ "${haystack/./}" != "$haystack" ]
then
return 0 # success
else
return 1 # failure
fi
}
function reverse_domain()
{
local dir=$1
local is_local="FALSE"
local is_test="FALSE"
#
# If domain has .local suffix remove and remember.
#
if string_ends_with $dir .local
then
is_local="TRUE"
dir=${dir/.local/} # com.domain.local --> com.domain
fi
#
# If domain has .test suffix remove and remember.
#
if string_ends_with $dir .test
then
is_test="TRUE"
dir=${dir/.test/} # com.domain.test --> com.domain
fi
local parts=${dir//./ } # com.domain --> com domain
local reversed=`reverse_arguments $parts` # com domain --> domain com
local domain=${reversed// /.} # domain com --> domain.com
#
# If had .local suffix reattach.
#
if [ "TRUE" == $is_local ]
then
domain+=".local"
fi
#
# If had .test suffix reattach.
#
if [ "TRUE" == $is_test ]
then
domain+=".test"
fi
echo $domain
}
function reverse_arguments()
{
local mine=$1
if [ $# -gt 1 ]
then
shift
reverse_arguments $@
printf " $mine"
else
printf "$mine"
fi
}
function is_tld()
{
case $1 in
com|dev|info|net|org|info)
return 0 # true
;;
at|au|nz|uk|us)
return 0 # true
;;
cloud|directory|online|news|site|software|solutions|training|xyz)
return 0 # true
;;
*)
return -1 # false
esac
}
function now()
{
date "$DATE_FORMAT"
}
if [ $# -lt 3 ]
then
Usage
exit -1
else
main "$@"
fi
Restart Apache
#!/bin/bash
#
# This script will determine the most recent modified date of the files in
# APACHE_CONFIG_DIR and then will restart apache if that modified date is more
# recent than the last time the script restarted Apache.
#
# The script stores the most recent restart of Apache in the file TIMESTAMP_FILE
#
# Locations may be overridden by passing values on command line, e.g.
#
# ./restart_apache.sh --apache-config-dir <config dir> --timestamp-file <timestamp file>
#
DATE_FORMAT="+%Y-%m-%dT%H:%M:%S%z"
APACHE_CONFIG_DIR="/etc/apache2/sites-enabled"
TIMESTAMP_FILE="/tmp/restart_apache.mtime"
DEV_NULL="/dev/null"
function main()
{
parse_args "$@"
local now=`date "$DATE_FORMAT"`
local last_restart=`last_restart`
local mtime_full=`last_apache_configuration_mtime "${APACHE_CONFIG_DIR}"`
local mtime_secs=`last_apache_configuration_mtime_seconds_since_epoch "${APACHE_CONFIG_DIR}"`
local filename=`last_apache_configuration_file "${APACHE_CONFIG_DIR}"`
local user=`whoami`
if [ "root" != "$user" ]
then
echo "Error: restart_apache.sh must be run as root."
elif (( last_restart < mtime_secs ))
then
check_apache_config
if [ "0" = "$?" ]
then
/usr/sbin/service apache2 restart
if [ "0" = $? ]
then
echo "$mtime_secs" > "$TIMESTAMP_FILE"
echo "$now: Last restart < last modification: $last_restart < $mtime_secs ($mtime_full) - Restarted"
else
echo "$now: Last restart < last modification: $last_restart < $mtime_secs ($mtime_full) - Unexpected restart failure"
fi
else
echo "$now: Last restart < last modification: $last_restart < $mtime_secs ($mtime_full) - Config fail ($filename)"
fi
else
echo "$now: Last restart = last modification: $last_restart = $mtime_secs ($mtime_full) - Skipping"
fi
}
function parse_args()
{
while [ -n "$1" ]
do
if [ "$1" = "--apache-config-dir" ]
then
shift
if [ ! -d "$1" ]
then
echo "Aborting, non-existant directory passed with --apache-config-dir"
exit -1
else
APACHE_CONFIG_DIR="$1"
shift
fi
elif [ "$1" = "--timestamp-file" ]
then
shift
if [ -d "$1" ]
then
echo "Aborting, directory passed with --timestamp-file, file or no file expected"
exit -1
else
TIMESTAMP_FILE="$1"
shift
fi
fi
done
}
function last_restart()
{
if [ ! -f "$TIMESTAMP_FILE" ]
then
echo "0" > "$TIMESTAMP_FILE"
fi
local secs=`cat "$TIMESTAMP_FILE"`
echo "$secs"
}
function last_apache_configuration_mtime()
{
local file=`ls -t "$1" | head -n1`
local info=`ls -lt --time-style "+%Y-%m-%dT%H:%M:%S%z" "$1/$file"`
local mtime=`echo $info | cut -d ' ' -f6`
echo "$mtime"
}
function last_apache_configuration_mtime_seconds_since_epoch()
{
local file=`ls -t "$1" | head -n1`
local info=`ls -lt --time-style +%s "$1/$file"`
local mtime=`echo $info | cut -d ' ' -f6`
echo "$mtime"
}
function last_apache_configuration_file()
{
local file=`ls -t "$1" | head -n1`
local info=`ls -lt --time-style +%s "$1/$file"`
local name=`echo $info | cut -d ' ' -f7`
echo "$name"
}
function check_apache_config()
{
sudo apachectl configtest > "$DEV_NULL" 2> "$DEV_NULL"
return $?
}
main "$@"