2019-03-02 16:45:34 +00:00
< ? php
/**
* Renders the HTML page that is sent to the client .
* @ package core
*/
class page_renderer
{
/**
* The root HTML template that all pages are built from .
* @ var string
* @ package core
*/
public static $html_template = " <!DOCTYPE html>
< html >
< head >
< meta charset = 'utf-8' />
< title > { title } </ title >
< meta name = 'viewport' content = 'width=device-width, initial-scale=1' />
< meta name = 'generator' content = 'Pepperminty Wiki {version}' />
< link rel = 'shortcut-icon' href = '{favicon-url}' />
< link rel = 'icon' href = '{favicon-url}' />
{ header - html }
</ head >
< body >
{ body }
<!-- Took { generation - time - taken } ms to generate -->
</ body >
</ html >
" ;
/**
* The main content template that is used to render normal wiki pages .
* @ var string
* @ package core
*/
public static $main_content_template = " { navigation-bar}
< h1 class = 'sitename' > { sitename } </ h1 >
< main >
{ content }
</ main >
{ extra }
< footer >
< p > { footer - message } </ p >
< p > Powered by Pepperminty Wiki { version }, which was built by < a href = '//starbeamrainbowlabs.com/' > Starbeamrainbowlabs </ a >. Send bugs to 'bugs at starbeamrainbowlabs dot com' or < a href = '//github.com/sbrl/Pepperminty-Wiki' title = 'Github Issue Tracker' > open an issue </ a >.</ p >
< p > Your local friendly moderators are { admins - name - list } .</ p >
< p > This wiki is managed by < a href = 'mailto:{admin-details-email}' > { admin - details - name } </ a >.</ p >
</ footer >
{ navigation - bar - bottom }
{ all - pages - datalist } " ;
/**
* A specially minified content template that doesn ' t include the navbar and
* other elements not suitable for printing .
* @ var string
* @ package core
*/
public static $minimal_content_template = " <main class='printable'> { content}</main>
< footer class = 'printable' >
< hr class = 'footerdivider' />
< p >< em > From { sitename }, which is managed by { admin - details - name } .</ em ></ p >
< p > { footer - message } </ p >
< p >< em > Timed at { generation - date } </ em ></ p >
< p >< em > Powered by Pepperminty Wiki { version } .</ em ></ p >
</ footer > " ;
/**
* An array of items indicating the resources to ask the web server to push
* down to the client with HTTP / 2.0 server push .
* Format : [ [ type , path ], [ type , path ], .... ]
* @ var array []
*/
protected static $http2_push_items = [];
/**
* A string of extrar HTML that should be included at the bottom of the page < head >.
* @ var string
*/
private static $extraHeaderHTML = " " ;
/**
* The javascript snippets that will be included in the page .
* @ var string []
* @ package core
*/
private static $jsSnippets = [];
/**
* The urls of the external javascript files that should be referenced
* by the page .
* @ var string []
* @ package core
*/
private static $jsLinks = [];
/**
* The navigation bar divider .
* @ package core
* @ var string
*/
public static $nav_divider = " <span class='nav-divider inflexible'> | </span> " ;
/**
* An array of functions that have been registered to process the
* find / replace array before the page is rendered . Note that the function
* should take a * reference * to an array as its only argument .
* @ var array
* @ package core
*/
protected static $part_processors = [];
/**
* Registers a function as a part post processor .
* This function ' s use is more complicated to explain . Pepperminty Wiki
* renders pages with a very simple templating system . For example , in the
* template a page ' s content is denoted by `{content}` . A function
* registered here will be passed all the components of a page _just_
* before they are dropped into the template . Note that the function you
* pass in here should take a * reference * to the components , as the return
* value of the function passed is discarded .
* @ package core
* @ param callable $function The part preprocessor to register .
*/
public static function register_part_preprocessor ( $function ) {
global $settings ;
// Make sure that the function we are about to register is valid
if ( ! is_callable ( $function ))
{
http_response_code ( 500 );
$admin_email = hide_email ( $settings -> admindetails_email );
exit ( page_renderer :: render ( " $settings->sitename - Module Error " , " <p> $settings->sitename has got a misbehaving module installed that tried to register an invalid HTML handler with the page renderer. Please contact $settings->sitename 's administrator { $settings -> admindetails_name } at <a href='mailto: $admin_email '> $admin_email </a>. " ));
}
self :: $part_processors [] = $function ;
return true ;
}
/**
* Renders a HTML page with the content specified .
* @ package core
* @ param string $title The title of the page .
* @ param string $content The ( HTML ) content of the page .
* @ param bool $body_template The HTML content template to use .
* @ return string The rendered HTML , ready to send to the client :- )
*/
public static function render ( $title , $content , $body_template = false )
{
global $settings , $start_time , $version ;
if ( $body_template === false )
$body_template = self :: $main_content_template ;
if ( strlen ( $settings -> logo_url ) > 0 ) {
// A logo url has been specified
2019-08-30 22:13:16 +00:00
$logo_html = " <img aria-hidden='true' class='logo " . ( isset ( $_GET [ " printable " ]) ? " small " : " " ) . " ' src=' $settings->logo_url ' /> " ;
2019-03-02 16:45:34 +00:00
switch ( $settings -> logo_position ) {
case " left " :
$logo_html = " $logo_html $settings->sitename " ;
break ;
case " right " :
$logo_html .= " $settings->sitename " ;
break ;
default :
throw new Exception ( " Invalid logo_position ' $settings->logo_position '. Valid values are either \" left \" or \" right \" and are case sensitive. " );
}
}
// Push the logo via HTTP/2.0 if possible
if ( $settings -> favicon [ 0 ] === " / " ) self :: $http2_push_items [] = [ " image " , $settings -> favicon ];
$parts = [
" { body} " => $body_template ,
" { sitename} " => $logo_html ,
" { version} " => $version ,
" { favicon-url} " => $settings -> favicon ,
" { header-html} " => self :: get_header_html (),
" { navigation-bar} " => self :: render_navigation_bar ( $settings -> nav_links , $settings -> nav_links_extra , " top " ),
" { navigation-bar-bottom} " => self :: render_navigation_bar ( $settings -> nav_links_bottom , [], " bottom " ),
" { admin-details-name} " => $settings -> admindetails_name ,
" { admin-details-email} " => $settings -> admindetails_email ,
" { admins-name-list} " => implode ( " , " , array_map ( function ( $username ) { return page_renderer :: render_username ( $username ); }, $settings -> admins )),
" { generation-date} " => date ( " l jS \ of F Y \ a \\ t h:ia T " ),
" { all-pages-datalist} " => self :: generate_all_pages_datalist (),
" { footer-message} " => $settings -> footer_message ,
/// Secondary Parts ///
" { content} " => $content ,
" { extra} " => " " ,
" { title} " => $title ,
];
// Pass the parts through the part processors
foreach ( self :: $part_processors as $function ) {
$function ( $parts );
}
$result = self :: $html_template ;
$result = str_replace ( array_keys ( $parts ), array_values ( $parts ), $result );
$result = str_replace ( " { generation-time-taken} " , round (( microtime ( true ) - $start_time ) * 1000 , 2 ), $result );
// Send the HTTP/2.0 server push indicators if possible - but not if we're sending a redirect page
if ( ! headers_sent () && ( http_response_code () < 300 || http_response_code () >= 400 )) self :: send_server_push_indicators ();
return $result ;
}
/**
* Renders a normal HTML page .
* @ package core
* @ param string $title The title of the page .
* @ param string $content The content of the page .
* @ return string The rendered page .
*/
public static function render_main ( $title , $content ) {
return self :: render ( $title , $content , self :: $main_content_template );
}
/**
* Renders a minimal HTML page . Useful for printable pages .
* @ package core
* @ param string $title The title of the page .
* @ param string $content The content of the page .
* @ return string The rendered page .
*/
public static function render_minimal ( $title , $content ) {
return self :: render ( $title , $content , self :: $minimal_content_template );
}
/**
* Sends the currently registered HTTP2 server push items to the client .
* @ return int | false The number of resource hints included in the link : header , or false if server pushing is disabled .
*/
public static function send_server_push_indicators () {
global $settings ;
if ( ! $settings -> http2_server_push )
return false ;
// Render the preload directives
$link_header_parts = [];
foreach ( self :: $http2_push_items as $push_item )
$link_header_parts [] = " < { $push_item [ 1 ] } >; rel=preload; as= { $push_item [ 0 ] } " ;
// Send them in a link: header
if ( ! empty ( $link_header_parts ))
header ( " link: " . implode ( " , " , $link_header_parts ));
return count ( self :: $http2_push_items );
}
/**
* Renders the header HTML .
* @ package core
* @ return string The rendered HTML that goes in the header .
*/
public static function get_header_html ()
{
global $settings ;
$result = self :: $extraHeaderHTML ;
$result .= self :: get_css_as_html ();
$result .= self :: _get_js ();
// We can't use module_exists here because sometimes global $modules
// hasn't populated yet when we get called O.o
if ( class_exists ( " search " ))
$result .= " \t \t <link rel='search' type='application/opensearchdescription+xml' href='?action=opensearch-description' title=' $settings->sitename Search' /> \n " ;
if ( ! empty ( $settings -> enable_math_rendering )) {
$result .= " <script type='text/x-mathjax-config'>
MathJax . Hub . Config ({
tex2jax : {
inlineMath : [ [ '$' , '$' ], [ '\\\\(' , '\\\\)' ] ],
processEscapes : true ,
skipTags : [ 'script' , 'noscript' , 'style' , 'textarea' , 'pre' , 'code' ]
}
});
</ script > " ;
}
return $result ;
}
/**
* Figures out whether $settings -> css is a url , or a string of css .
* A url is something starting with " protocol:// " or simply a " / " .
* @ return bool True if it 's a url - false if we assume it' s a string of css .
*/
public static function is_css_url () {
global $settings ;
2019-08-25 20:38:28 +00:00
return preg_match ( " /^[^ \ /]* \ / \ /|^ \ /[^ \ *]/ " , $settings -> css );
2019-03-02 16:45:34 +00:00
}
/**
* Renders all the CSS as HTML .
* @ package core
* @ return string The css as HTML , ready to be included in the HTML header .
*/
public static function get_css_as_html ()
{
global $settings , $defaultCSS ;
if ( self :: is_css_url ()) {
if ( $settings -> css [ 0 ] === " / " ) // Push it if it's a relative resource
self :: add_server_push_indicator ( " style " , $settings -> css );
return " <link rel='stylesheet' href=' $settings->css ' /> \n " ;
} else {
$css = $settings -> css == " auto " ? $defaultCSS : $settings -> css ;
if ( ! empty ( $settings -> optimize_pages )) {
// CSS Minification ideas by Jean from catswhocode.com
// Link: http://www.catswhocode.com/blog/3-ways-to-compress-css-files-using-php
// Remove comments
$css = preg_replace ( '!/\*[^*]*\*+([^/][^*]*\*+)*/!' , " " , $css );
// Cut down whitespace
$css = preg_replace ( '/\s+/' , " " , $css );
// Remove whitespace after colons and semicolons
$css = str_replace ([
" : " ,
" : " ,
" ; " ,
" { " ,
" } "
], [
" : " ,
" : " ,
" ; " ,
" { " ,
" } "
], $css );
}
return " <style> $css </style> \n " ;
}
}
/**
* Adds the specified url to a javascript file as a reference to the page .
* @ package core
* @ param string $scriptUrl The url of the javascript file to reference .
*/
2019-09-11 21:11:13 +00:00
public static function add_js_link ( string $scriptUrl ) {
2019-03-02 16:45:34 +00:00
static :: $jsLinks [] = $scriptUrl ;
}
/**
* Adds a javascript snippet to the page .
* @ package core
* @ param string $script The snippet of javascript to add .
*/
2019-09-11 21:11:13 +00:00
public static function add_js_snippet ( string $script ) {
2019-03-02 16:45:34 +00:00
static :: $jsSnippets [] = $script ;
}
/**
* Renders the included javascript header for inclusion in the final
* rendered page .
* @ package core
* @ return string The rendered javascript ready for inclusion in the page .
*/
private static function _get_js () {
$result = " <!-- Javascript --> \n " ;
foreach ( static :: $jsSnippets as $snippet )
$result .= " <script defer> \n $snippet\n </script> \n " ;
foreach ( static :: $jsLinks as $link ) {
// Push it via HTTP/2.0 if it's relative
if ( $link [ 0 ] === " / " ) self :: add_server_push_indicator ( " script " , $link );
$result .= " <script src=' " . $link . " ' defer></script> \n " ;
}
return $result ;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* Adds a string of HTML to the header of the rendered page .
* @ param string $html The string of HTML to add .
*/
public static function add_header_html ( $html ) {
self :: $extraHeaderHTML .= $html ;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* Adds a resource to the list of items to indicate that the web server should send via HTTP / 2.0 Server Push .
* Note : Only specify static files here , as you might end up with strange ( and possibly dangerous ) results !
* @ param string $type The resource type . See https :// fetch . spec . whatwg . org / #concept-request-destination for more information.
* @ param string $path The * relative url path * to the resource .
*/
public static function add_server_push_indicator ( $type , $path ) {
self :: $http2_push_items [] = [ $type , $path ];
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* Renders a navigation bar from an array of links . See
* $settings -> nav_links for format information .
* @ package core
* @ param array $nav_links The links to add to the navigation bar .
* @ param array $nav_links_extra The extra nav links to add to
* the " More... " menu .
* @ param string $class The class ( es ) to assign to the rendered
* navigation bar .
*/
public static function render_navigation_bar ( $nav_links , $nav_links_extra , $class = " " ) {
global $settings , $env ;
2019-08-29 16:19:57 +00:00
$mega_menu = false ;
if ( is_object ( $nav_links )) {
$mega_menu = true ;
$class = trim ( " $class mega-menu " );
$links_list = [];
$keys = array_keys ( get_object_vars ( $nav_links ));
foreach ( $keys as $key ) {
$links_list [] = " category \0 $key " ;
$links_list = array_merge (
$links_list ,
$nav_links -> $key
);
}
$nav_links = $links_list ;
}
2019-03-02 16:45:34 +00:00
$result = " <nav class=' $class '> \n " ;
2019-08-29 19:57:14 +00:00
$is_first_category = true ;
2019-03-02 16:45:34 +00:00
// Loop over all the navigation links
foreach ( $nav_links as $item ) {
if ( ! is_string ( $item )) {
// Output the item as a link to a url
$result .= " <span><a href=' " . str_replace ( " { page} " , rawurlencode ( $env -> page ), $item [ 1 ]) . " '> $item[0] </a></span> " ;
continue ;
}
2019-08-29 16:19:57 +00:00
// Extract the item key - a null character can be used to separate extra data from an item type
$item_key = $item ;
if ( strpos ( $item_key , " \0 " ) !== false )
$item_key = substr ( $item_key , 0 , strpos ( $item_key , " \0 " ));
2019-03-02 16:45:34 +00:00
// The item is a string
2019-08-29 16:19:57 +00:00
switch ( $item_key ) {
2019-03-02 16:45:34 +00:00
//keywords
case " user-status " : // Renders the user status box
if ( $env -> is_logged_in ) {
$result .= " <span class='inflexible logged-in " . ( $env -> is_logged_in ? " moderator " : " normal-user " ) . " '> " ;
if ( module_exists ( " feature-user-preferences " )) {
2019-08-30 20:31:14 +00:00
$result .= " <a href='?action=user-preferences' aria-label='Change user preferences'> $settings->user_preferences_button_text </a> " ;
2019-03-02 16:45:34 +00:00
}
$result .= self :: render_username ( $env -> user );
$result .= " <small>(<a href='index.php?action=logout'>Logout</a>)</small> " ;
$result .= " </span> " ;
//$result .= page_renderer::$nav_divider;
}
else {
$returnto_url = $env -> action !== " logout " ? $_SERVER [ " REQUEST_URI " ] : " ?action=view&page= " . rawurlencode ( $settings -> defaultpage );
$result .= " <span class='not-logged-in'><a href='index.php?action=login&returnto= " . rawurlencode ( $returnto_url ) . " '>Login</a></span> " ;
}
break ;
case " search " : // Renders the search bar
2019-08-30 20:31:14 +00:00
$result .= " <span class='inflexible'><form method='get' action='index.php' style='display: inline;'><input type='search' name='page' list='allpages' placeholder='🔎 Type a page name here and hit enter' /><input type='hidden' name='search-redirect' value='true' /></form></span> " ;
2019-03-02 16:45:34 +00:00
break ;
case " divider " : // Renders a divider
$result .= page_renderer :: $nav_divider ;
break ;
case " menu " : // Renders the "More..." menu
$result .= " <span class='inflexible nav-more'><label for='more-menu-toggler'>More...</label>
< input type = 'checkbox' class = 'off-screen' id = 'more-menu-toggler' /> " ;
$result .= page_renderer :: render_navigation_bar ( $nav_links_extra , [], " nav-more-menu " );
$result .= " </span> " ;
break ;
2019-08-29 16:19:57 +00:00
case " category " : // Renders a category header
2019-08-29 19:57:14 +00:00
if ( ! $is_first_category ) $result .= " </span> " ;
$result .= " <span class='category'><strong> " . substr ( $item , 9 ) . " </strong> " ;
$is_first_category = false ;
2019-08-29 16:19:57 +00:00
break ;
2019-03-02 16:45:34 +00:00
// It isn't a keyword, so just output it directly
default :
$result .= " <span> $item </span> " ;
}
}
2019-08-29 16:19:57 +00:00
if ( $mega_menu ) $result .= " </span> " ;
2019-03-02 16:45:34 +00:00
$result .= " </nav> " ;
return $result ;
}
/**
* Renders a username for inclusion in a page .
* @ package core
* @ param string $name The username to render .
* @ return string The username rendered in HTML .
*/
public static function render_username ( $name ) {
global $settings ;
$result = " " ;
$result .= " <a href='?page= " . rawurlencode ( get_user_pagename ( $name )) . " '> " ;
if ( $settings -> avatars_show )
2019-08-30 17:23:17 +00:00
$result .= " <img class='avatar' aria-hidden='true' src='?action=avatar&user= " . urlencode ( $name ) . " &size= $settings->avatars_size ' /> " ;
2019-03-02 16:45:34 +00:00
if ( in_array ( $name , $settings -> admins ))
$result .= $settings -> admindisplaychar ;
$result .= htmlentities ( $name );
$result .= " </a> " ;
return $result ;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* Renders the datalist for the search box as HTML .
* @ package core
* @ return string The search box datalist as HTML .
*/
public static function generate_all_pages_datalist () {
global $settings , $pageindex ;
$arrayPageIndex = get_object_vars ( $pageindex );
2019-09-03 17:16:01 +00:00
$sorter = new Collator ( " " );
uksort ( $arrayPageIndex , function ( $a , $b ) use ( $sorter ) : int {
return $sorter -> compare ( $a , $b );
});
2019-03-02 16:45:34 +00:00
$result = " <datalist id='allpages'> \n " ;
// If dynamic page sugggestions are enabled, then we should send a loading message instead.
if ( $settings -> dynamic_page_suggestion_count > 0 ) {
$result .= " <option value='Loading suggestions...' /> " ;
} else {
foreach ( $arrayPageIndex as $pagename => $pagedetails ) {
$escapedPageName = str_replace ( '"' , '"' , $pagename );
$result .= " \t \t \t <option value= \" $escapedPageName\ " /> \n " ;
}
}
$result .= " \t \t </datalist> " ;
return $result ;
}
}
// HTTP/2.0 Server Push static items
foreach ( $settings -> http2_server_push_items as $push_item ) {
page_renderer :: add_server_push_indicator ( $push_item [ 0 ], $push_item [ 1 ]);
}
// Math rendering support
if ( ! empty ( $settings -> enable_math_rendering ))
{
page_renderer :: add_js_link ( " https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML " );
}
// alt+enter support in the search box
page_renderer :: add_js_snippet ( ' // Alt + Enter support in the top search box
window . addEventListener ( " load " , function ( event ) {
document . querySelector ( " input[type=search] " ) . addEventListener ( " keyup " , function ( event ) {
// Listen for Alt + Enter
if ( event . keyCode == 13 && event . altKey ) {
event . stopPropagation ();
event . preventDefault ();
event . cancelBubble = true ;
event . target . form . setAttribute ( " target " , " _blank " );
event . target . form . submit ();
event . target . form . removeAttribute ( " target " );
return false ; // Required by some browsers
}
});
});
' );