Source Code for /public/inc/functions.php
<?php
require(__DIR__ . '/../../vendor/autoload.php');
define( 'SITE_TITLE', "Dan Q's PHP Cookery Class" );
require('potion-finder.php');
function add_header( $page_title = null, $hide_navbar = false ) {
$full_page_title = SITE_TITLE;
if( ! empty( $page_title ) ) {
$full_page_title = $page_title . ' - ' . $full_page_title;
}
require('header.php');
}
function add_footer() {
require('footer.php');
}
function add_navbar($full_toc = false) {
$links = [
[
'title' => 'Home',
'href' => '/',
],
[
'title' => 'Introduction',
'href' => '/01-intro/',
],
[
'title' => 'Hello World',
'href' => '/02-hello-world/',
'sections' => [
[
'id' => 'strings-both-ways',
'title' => 'Strings, forwards and backwards'
],
[
'id' => 'today',
'title' => 'What day is it?'
],
[
'id' => 'conditional',
'title' => 'Closed on Mondays!'
],
],
],
[
'title' => 'Lists and Loops',
'href' => '/03-lists-and-loops/',
'sections' => [
[
'id' => 'looping',
'title' => 'The 7-times table'
],
[
'id' => 'looping-through-a-list',
'title' => 'A basic gallery'
],
[
'id' => 'navigation-menu',
'title' => 'Navigation menu'
],
[
'id' => 'looping-through-files',
'title' => 'Looping through files (automatic gallery)'
],
[
'id' => 'selecting-from-an-array',
'title' => 'Showing a random quote'
],
]
],
[
'title' => 'Includes and Functions',
'href' => '/04-includes-and-functions/',
'sections' => [
[
'id' => 'navigation-menu-include',
'title' => 'Sharing your navbar across the site'
],
[
'id' => 'functions',
'title' => 'Reusing common patterns with functions'
],
[
'id' => 'shared-function-file',
'title' => 'Sharing a function across multiple pages'
],
],
],
[
'title' => 'Accepting Input',
'href' => '/05-accepting-input/',
'sections' => [
[
'id' => 'get-and-post',
'title' => 'GET and POST requests'
],
[
'id' => 'whats-your-name',
'title' => "Asking the user's name"
],
[
'id' => 'fruits-and-filters',
'title' => 'Fruits and filters revisited'
],
[
'id' => 'quiz',
'title' => "Let's make a quiz!"
],
],
],
[
'title' => 'Sessions & Cookies',
'href' => '/06-retaining-state/',
'sections' => [
[
'id' => 'what-are-cookies',
'title' => 'What are cookies?'
],
[
'id' => 'detecting-repeat-visitors',
'title' => 'Detecting repeat visitors'
],
[
'id' => 'sessions',
'title' => 'Logging in (and staying logged in)'
],
[
'id' => 'potion-finder',
'title' => 'Potion Finder (game)'
],
],
],
[
'title' => 'Storing Data',
'href' => '/07-storing-data/',
'sections' => [
[
'id' => 'counter',
'title' => 'Counting Clicks'
],
],
],
[
'title' => 'Beyond HTML',
'href' => '/08-beyond-html/',
],
[
'title' => 'Reaching Out',
'href' => '/09-reaching-out/',
],
[
'title' => 'Troubleshooting',
'href' => '/troubleshooting/',
],
[
'title' => 'Full table of contents',
'href' => '/toc/',
'class' => [ 'no-number' ],
],
[
'title' => 'Appendix A: Further Examples',
'href' => '/appendix-samples/',
'class' => [ 'no-number', 'appendix' ],
],
[
'title' => 'Appendix B: Security',
'href' => '/appendix-security/',
'class' => [ 'no-number', 'appendix' ],
],
[
'title' => "Appendix C: This Site's Code",
'href' => '/appendix-quine/',
'class' => [ 'no-number', 'appendix' ],
],
];
?>
<nav>
<ol start="0">
<?php
foreach( $links as $link ) {
$is_active = $_SERVER['REQUEST_URI'] === $link['href'];
$class_list = $link['class'] ?? [];
if( $is_active ) {
$class_list[] = 'active';
}
$class_list = implode( ' ', $class_list );
?>
<li class="<?php echo $class_list; ?>">
<a href="<?php echo $link['href']; ?>"><?php echo $link['title']; ?></a>
<?php if( $full_toc || ( $is_active && ! empty( $link['sections'] ) ) ) { ?>
<ul>
<?php foreach( $link['sections'] as $section ) { ?>
<li><a href="<?php echo $link['href'] . '#' . $section['id']; ?>"><?php echo $section['title']; ?></a></li>
<?php } ?>
</ul>
<?php } ?>
</li>
<?php
}
?>
<?php if( ! str_starts_with($_SERVER['REQUEST_URI'], '/search/') ) { ?>
<li id="nav-search">
<form action="/search/#search-results" method="get">
<input type="search" aria-label="Search the site" autocomplete="off" name="q" placeholder="Search..." value="<?php echo htmlspecialchars($_GET['q'] ?? ''); ?>">
<button type="submit">Search</button>
</form>
</li>
<?php } ?>
</ol>
</nav>
<?php
}
function add_backlink() {
?>
<p class="back"></p>
<script>
const backLink = document.createElement('a');
backLink.href = '#';
backLink.addEventListener('click', (e) => {
e.preventDefault();
window.history.back();
});
backLink.innerText = '🔙 Back to the site';
document.querySelector('.back').appendChild(backLink);
</script>
<?php
}
/**
* Begins a syntax-highlighted code block.
* Accepts the following parameters:
* - $language: Optional, default null. The language of the code. If not provided, no language will be assumed.
*/
function begin_code_block( $language = null ){
global $current_code_block_language;
$current_code_block_language = $language;
ob_start();
}
/**
* Ends a syntax-highlighted code block and renders the output
*/
function end_code_block(){
global $current_code_block_language;
$hl = new \Highlight\Highlighter();
if( ! empty( $current_code_block_language ) ) {
$hl->setAutodetectLanguages(array($current_code_block_language));
} else {
$hl->setAutodetectLanguages(array('php', 'html', 'css', 'javascript'));
}
$code = ob_get_clean();
$highlighted = $hl->highlightAuto( htmlspecialchars_decode( $code ) );
echo '<figure class="code-block-container"><pre class="code-block"><code class="hljs ' . $highlighted->language . '">' . $highlighted->value . '</code></pre></figure>';
}
/**
* Renders a code block from a file.
* Accepts the following parameters:
* - $file: The relative path to the file to render.
* - $language: Optional, default null. The language of the code. If not provided, no language will be assumed.
* - $link: Optional, default true. Should there be a link to the URL of the file (to demo it).
* - $download: Optional, default true. Whether to provide a link to download the file.
* - $demo_in_full_page: Optional, default false. Set to true to wrap the code in a full HTML page when trying/demoing it.
* Set to a string to specify a CSS file to <link>
*/
function code_block_from_file( $file, $language = null, $link = true, $download = true, $demo_in_full_page = false ) {
$code = file_get_contents( __DIR__ . '/../' . $file );
$hl = new \Highlight\Highlighter();
if( ! empty( $language ) ) {
$highlighted = $hl->highlight($language, $code);
} else {
$hl->setAutodetectLanguages(array('php', 'html', 'css', 'javascript'));
$highlighted = $hl->highlightAuto( htmlspecialchars_decode( $code ) );
}
$highlighted = $hl->highlightAuto( htmlspecialchars_decode( $code ) );
$try_link = $demo_in_full_page ?
'/demo-harness.php?file=public/' . $file . '&demo_in_full_page=' . $demo_in_full_page : // 👈 demo link URL if Demo In Full Page is set
'/' . $file; // 👈 raw demo link URL
echo '<figure class="code-block-container"><pre class="code-block"><code class="hljs ' . $highlighted->language . '">' . $highlighted->value . '</code></pre>';
if( $link || $download ) {
echo '<figcaption class="code-block-actions"><ul>';
if( $link ) {
echo '<li><a href="' . $try_link . '">👁️ Try it!</a></li>';
}
if( $download ) {
echo '<li><a href="/source-viewer.php?file=public/' . $file . '&download=true" download>💾 Download</a></li>';
}
echo '</ul></figcaption>';
}
echo '</figure>';
}
/**
* Given a relative or absolute file path, returns a sanitized absolute version of the path.
* Forbids access to files outside of this application's directory structure.
*/
function sanitize_file_path( $file_path ) {
// The allowed directory (from which files can be requested) is the parent directory of this file.
$allowed_directory = realpath( __DIR__ . '/../../' );
// If they didn't specify a file, give them a 404 error:
if( empty( $file_path ) ) {
header('HTTP/1.1 404 Missing file parameter');
die('Missing file parameter');
}
// Move to the allowed directory so that we treat paths as "relative" to that:
chdir( $allowed_directory );
// Determine the real path of the file they're requesting:
$real_file_path = realpath( $file_path );
// If the file doesn't exist, give them a 404 error:
if( $real_file_path === false ) {
header('HTTP/1.1 404 Requested file not found');
die('Requested file not found');
}
// Ensure that the file they're requesting is within the allowed directory so they can't read ANY file on this server!
// If the file they want is not in the allowed directory, give them a 403 error:
if( strpos( $real_file_path, $allowed_directory ) !== 0 ) {
header('HTTP/1.1 403 Requested file forbidden');
die('Requested file forbidden');
}
return $real_file_path;
}
/**
* Some pages (the source viewer and demo harness) will show the source code of or render a specified file.
* This convenience function works out what file they've requested (from the $_GET['file'] parameter),
* checks that it's a legitimate request (a real file, found within this application's directory structure),
* kills the request if it's not, and returns the real path to the file if it is.
*/
function get_requested_file() {
// Find out what file the user wants to view the source code for:
return sanitize_file_path( $_GET['file'] );
}
/**
* Given a PHP file path, returns the path to the associated CSS file if it exists.
*/
function get_associated_css_file( $file ) {
$file = sanitize_file_path( $file );
$css_file_path = str_replace('.php', '.css', $file );
if( file_exists( $css_file_path ) ) {
$public_dir = realpath( __DIR__ . '/../' );
return str_replace( $public_dir, '', $css_file_path );
}
return null;
}