App: apps - Page: browse
PHP
browse.php
|
HTML
browse.pad
|
Result
|
<?php
/** * Redact sensitive values from source code before syntax highlighting. * Replaces values of variables containing sensitive keywords with *REDACTED* */ function padColorsFileRedacted($file) { $source = padFileGet($file);
// Pattern matches: $varName = 'value' or $varName = "value" // For variables containing sensitive keywords (case-insensitive) $sensitiveKeywords = [ 'password', 'passwd', 'pwd', 'pass', 'secret', 'token', 'apikey', 'api_key', 'appkey', 'app_key', 'credential', 'cred', 'auth', 'private', 'salt', 'hash', 'cipher', 'encrypt', ];
$keywordPattern = implode('|', $sensitiveKeywords);
// Match: $varName = 'value' or $varName = "value" (with optional spaces) // Captures: 1=variable with $, 2=equals and spaces, 3=quote char, 4=value, 5=closing quote $pattern = '/(\$\w*(?:' . $keywordPattern . ')\w*)\s*(=\s*)([\'"])(.+?)\3/i';
$source = preg_replace_callback($pattern, function($matches) { return $matches[1] . $matches[2] . $matches[3] . '*REDACTED*' . $matches[3]; }, $source);
// Also handle array definitions like 'password'=> '*REDACTED*' $arrayPattern = '/([\'"](?:' . $keywordPattern . ')[\'"])\s*(=>\s*)([\'"])(.+?)\3/i';
$source = preg_replace_callback($arrayPattern, function($matches) { return $matches[1] . $matches[2] . $matches[3] . '*REDACTED*' . $matches[3]; }, $source);
// Apply syntax highlighting to the redacted source. A highlighted PHP source keeps // its braces, and spliced into a level they read as tags - so they travel escaped, // the way the .pad walk escapes its own, and the exit puts them back. if (substr($file, -4) == '.pad') { return padColorsString($source); } else { return str_replace(['{', '}'], ['{', '}'], padColorsHighLight($source)); } }
$title = 'Browse Application';
// Get the apps directory $appsDir = dirname(APP) . '/';
// Get requested app, directory and file from query string $app = padRequest ( 'app', '' ); $dir = padRequest ( 'dir', '' ); $file = padRequest ( 'file', '' );
// A list from the query string - app[]=x - names nothing: preg_match got the array and // the page ended on a TypeError, a 500. $app = is_string ( $app ) ? $app : ''; $dir = is_string ( $dir ) ? $dir : ''; $file = is_string ( $file ) ? $file : '';
$appPath = ''; $appDirs = []; $appFiles = []; $appExts = ['php', 'pad', 'json', 'xml', 'html', 'js', 'css']; $source = ''; $currentFile = ''; $parentDir = '';
// Validate app name (security: prevent directory traversal). A nested application - // regression/main, which the index links - is name segments joined by /, still with no // dot and no empty segment, so it can not climb out. if ($app && preg_match('/^[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)*$/', $app)) {
$appPath = $appsDir . $app . '/';
if (is_dir($appPath)) {
$title = "Browse: $app";
// Validate and set current directory $currentDir = $appPath; if ($dir && preg_match('/^[a-zA-Z0-9_\-\/]+$/', $dir)) { $testPath = $appPath . $dir . '/'; if (is_dir($testPath) && strpos(realpath($testPath), realpath($appPath)) === 0) { $currentDir = $testPath; $title = "Browse: $app/$dir";
// Calculate parent directory if (strpos($dir, '/') !== false) { $parentDir = dirname($dir); } else { $parentDir = ''; } } } elseif ($dir) { // Invalid dir, reset $dir = ''; }
// Scan current directory for files and subdirectories $items = scandir($currentDir);
foreach ($items as $item) { // Skip hidden files and . / .. if ($item[0] === '.') continue;
$itemPath = $currentDir . $item; $relativePath = ($dir ? $dir . '/' : '') . $item;
if (is_dir($itemPath)) { $appDirs[] = [ 'name' => $item, 'path' => $relativePath ]; } else { $ext = pathinfo($item, PATHINFO_EXTENSION); // Only show source files if (in_array($ext, $appExts)) { $appFiles[] = [ 'path' => $relativePath, 'name' => $item, 'ext' => $ext ]; } } }
// Sort directories and files by name $appDirs = padArrSortBy ( $appDirs, fn ( $one ) => strtolower ( $one ['name'] ) ); $appFiles = padArrSortBy ( $appFiles, fn ( $one ) => strtolower ( $one ['name'] ) );
// If a file is requested, load its source - a file the listing shows, and no other: // a name with a part that starts with a dot is never listed, yet .env - where padEnv // finds an application's passwords and keys - was shown in full to anyone who // typed it, as was a file of any other kind. if ($file && preg_match('/^[a-zA-Z0-9_\-\/\.]+$/', $file) && !preg_match('#(^|/)\.#', $file) && in_array(pathinfo($file, PATHINFO_EXTENSION), $appExts)) { $filePath = $appPath . $file;
if (file_exists($filePath) && strpos(realpath($filePath), realpath($appPath)) === 0) { $currentFile = $file; $source = padColorsFileRedacted($filePath); } } } }
$dirCount = count($appDirs); $fileCount = count($appFiles); $hasParent = ($dir !== '');
?>
|
<style> .browse-page { text-align: left; display: inline-block; } .browse-container { display: flex; gap: 20px; } .file-list { min-width: 150px; } .file-list ul { list-style: none; padding: 0; margin: 0; } .file-list li { padding: 2px 0; } .file-list a { text-decoration: none; } .file-list a:hover { text-decoration: underline; } .file-list .current { font-weight: bold; } .file-list .dir { font-weight: bold; } .file-list .parent a { font-size: 1.4em; font-weight: bold; } .source-view { overflow-x: auto; } .source-view pre { background: #f8f8f8; padding: 10px; border: 1px solid #ddd; margin: 0; } .back-link { margin-bottom: 10px; } .path-info { color: #666; margin-bottom: 5px; } </style>
<div class="browse-page">
{if $app ne ''}
<div class="browse-container">
<div class="file-list"> <ul> {if $hasParent} <li class="parent"> {if $parentDir ne ''} <a href="?browse&app={$app}&dir={$parentDir}">..</a> @else@ <a href="?browse&app={$app}">..</a> {/if} </li> {/if} {appDirs} <li class="dir"><a href="?browse&app={$app}&dir={$path}">{$name}/</a></li> {/appDirs} {appFiles} {if $currentFile eq $path} <li class="current">{$name}</li> {/if} {if $currentFile ne $path} <li><a href="?browse&app={$app}&dir={$dir}&file={$path}">{$name}</a></li> {/if} {/appFiles} </ul> </div>
{if $currentFile ne ''} <div class="source-view"> <h3>{$currentFile}</h3> {!source} </div> {/if}
</div>
{/if}
{if $app eq ''} <p>No application specified. <a href="?index">Return to applications list</a>.</p> {/if} </div>
|
|
App: demo - Page: clock
PHP
|
HTML
clock.pad
|
Result
|
|
{# This page prints the clock, so it says something different every second and can never match a stored copy - and not only in its digits: AM turns to PM and the weekday changes name, which digit masking cannot cover. The crawl marks this page uncomparable by name and only checks that it still answers. #} <p>Current date and time information.</p>
<h2>Current Time</h2>
<div class="counter" style="font-size: 36px;"> {clock 'l, F j, Y'}<br> {clock 'g:i:s A'} </div>
<h2>Time Formats</h2>
<table> <tr> <th>Format</th> <th>Value</th> </tr> <tr> <td>12-hour format</td> <td>{clock 'g:i:s A'}</td> </tr> <tr> <td>24-hour format</td> <td>{clock 'H:i:s'}</td> </tr> <tr> <td>ISO 8601</td> <td>{clock 'c'}</td> </tr> <tr> <td>RFC 2822</td> <td>{clock 'r'}</td> </tr> <tr> <td>Unix Timestamp</td> <td>{clock 'U'}</td> </tr> </table>
<h2>Calendar Information</h2>
<table> <tr> <th>Property</th> <th>Value</th> </tr> <tr> <td>Day of Week</td> <td>{clock 'l'}</td> </tr> <tr> <td>Month</td> <td>{clock 'F'}</td> </tr> <tr> <td>Year</td> <td>{clock 'Y'}</td> </tr> <tr> <td>Day of Year</td> <td>{clock 'z' | + 1} of {if {clock 'L'} eq 1}366@else@365{/if}</td> </tr> <tr> <td>Week of Year</td> <td>{clock 'W'}</td> </tr> <tr> <td>Days in This Month</td> <td>{clock 't'}</td> </tr> <tr> <td>Days Remaining in Year</td> <td>{if {clock 'L'} eq 1}{echo 365 - {clock 'z'}}@else@{echo 364 - {clock 'z'}}{/if}</td> </tr> <tr> <td>Leap Year</td> <td>{if {clock 'L'} eq 1}Yes@else@No{/if}</td> </tr> <tr> <td>Timezone</td> <td>{date_default_timezone_get}</td> </tr> </table>
|
|
App: demo - Page: guestbook
PHP
guestbook.php
|
HTML
guestbook.pad
|
Result
|
<?php
$title = 'Guestbook'; $dataFile = 'demo/guestbook.json'; // under DATA/, for padFileGet and padFilePut $error = '';
$entries = json_decode ( padFileGet ( $dataFile ), TRUE ) ?: [];
if ( padRequestIs ( 'POST' ) and padRequest ( 'action' ) == 'add' ) { $name = padRequest ( 'name', '' ); $comment = padRequest ( 'comment', '' );
// A field posted as a list - name[]=x - is no text: it is asked for again, where // htmlspecialchars() was handed the array and the post ended on a 500.
if ( is_string ( $name ) and is_string ( $comment ) and padFilled ( $name ) and padFilled ( $comment ) ) { $entry = [ 'name' => htmlspecialchars ( $name ), 'comment' => htmlspecialchars ( $comment ), 'date' => padNow ( 'Y-m-d H:i:s' ) ]; array_unshift ( $entries, $entry ); padFilePut ( $dataFile, json_encode ( $entries, JSON_PRETTY_PRINT ) );
// Post, redirect, get: the browser is sent on to a plain GET of this page, so a // refresh shows the guestbook again instead of signing it a second time. The thanks // travel as a flash message, shown on that page once.
padFlash ( 'Thank you for signing the guestbook!' );
padRedirect ( 'guestbook' ); } else $error = 'Please fill in both name and message.'; }
$hasEntries = count ( $entries ) > 0;
?>
|
<p>Sign our guestbook and leave a message for other visitors!</p>
{flash} <div class="message">{$message}</div> {/flash}
{if $error} <div class="message error">{$error}</div> {/if}
<h2>Sign the Guestbook</h2>
<form method="post"> <input type="hidden" name="action" value="add"> <p> <label for="guestbook-name">Your Name:</label><br> <input type="text" id="guestbook-name" name="name" required> </p> <p> <label for="guestbook-comment">Your Message:</label><br> <textarea id="guestbook-comment" name="comment" rows="4" required></textarea> </p> <p> <input type="submit" value="Sign Guestbook"> </p> </form>
<h2>Guestbook Entries</h2>
{if $hasEntries} {entries} <div class="guest-entry"> <div class="guest-name">{$name}</div> <div class="guest-date">{$date}</div> <p>{$comment}</p> </div> {/entries} @else@ <p>No entries yet. Be the first to sign!</p> {/if}
|
|
App: demo - Page: highlight
PHP
highlight.php
|
HTML
highlight.pad
|
Result
|
<?php
$title = 'Syntax Highlighting';
// A query as it would come from a database or a log - a value, coloured by the pipe.
$query = "SELECT name, COUNT(*) AS orders FROM customers JOIN orders USING (customer_id) WHERE country = 'NL' GROUP BY name";
?>
|
<p>The <code>{ignore}{highlight}{/ignore}</code> tag colours source code on the server - PAD, PHP, HTML, CSS, JavaScript, JSON, YAML, SQL and Bash. No JavaScript library, and the braces inside need no <code>{ignore}</code>.</p>
<h2>PHP, with line numbers and a marked line</h2>
{highlight 'php', lines, mark='4'} <?php
$orders = db ( "ARRAY * FROM orders WHERE status = 'open'" ); $total = padArrSum ( $orders, 'amount' );
if ( ! $orders ) padRedirect ( 'empty' );
?> {/highlight}
<p><code>{ignore}{highlight 'php', lines, mark='4'}...{/highlight}{/ignore}</code></p>
<h2>PAD itself</h2>
{highlight 'pad'} {orders sort='date', first=10} <li class="{even@orders ? 'even' : 'odd'}"> {$number}: {$total | money} </li> @else@ <li>No orders yet</li> {/orders} {/highlight}
<h2>CSS and JavaScript</h2>
{highlight 'css'} .card { padding: 12px 16px; border-radius: 8px; color: #333; } @media (prefers-color-scheme: dark) { .card { color: #eee; } } {/highlight}
{highlight 'js'} // Counts the clicks on every button with a data-count attribute document.querySelectorAll('[data-count]').forEach(button => { let clicks = 0; button.addEventListener('click', () => button.textContent = `Clicked ${++clicks} times`); }); {/highlight}
<h2>A file of this application</h2>
{highlight file='qr.php'}
<p><code>{ignore}{highlight file='qr.php'}{/ignore}</code> - the language follows the extension.</p>
<h2>A value, through the pipe</h2>
{$query | highlight('sql')}
<p><code>{ignore}{$query | highlight('sql')}{/ignore}</code> - a query kept in a database or a log.</p>
|
|
App: demo - Page: todo
PHP
todo.php
|
HTML
todo.pad
|
Result
|
<?php
$title = 'Todo List'; $dataFile = 'demo/todos.json'; // under DATA/, for padFileGet and padFilePut $message = $message ?? ''; // todoPost sets it, then restarts this page
// A notice from the request - ?todo&message[]=x - is no text: {if $message} ended the // page on a 500 over the list.
if ( ! is_string ( $message ) ) $message = '';
$todos = json_decode ( padFileGet ( $dataFile ), TRUE ) ?: [];
$hasTodos = count ( $todos ) > 0; $doneCount = count ( padArrWhere ( $todos, 'done' ) ); $pendingCount = count ( $todos ) - $doneCount;
?>
|
<p>Manage your tasks with this simple todo list.</p>
{if $message} <div class="message">{$message}</div> {/if}
<h2>Add New Task</h2>
{todo 'add', 'Add Task'} <input type="text" name="task" aria-label="New task" placeholder="Enter a new task..." required style="flex: 1;"> {/todo}
<h2>Tasks ({$pendingCount} pending, {$doneCount} done)</h2>
{todos}
<table>
<tr> <th>Task</th> <th style="width: 120px;">Added</th> <th style="width: 5%; text-align: center;" colspan="2">Actions</th> </tr>
@start@
<tr{if $done} class="todo-item done"{/if}> <td>{$task}</td> <td style="font-size: 12px; color: #999;">{$date | substr (0, 10)}</td> <td> {if ! $done} {todo 'done', 'Done'} <input type="hidden" name="id" value="{$id}"> {/todo} {/if} </td> <td> {todo 'delete', 'Delete'} <input type="hidden" name="id" value="{$id}"> {/todo} </td> </tr>
@end@
</table>
{if $doneCount > 0} {todo 'clear', 'Clear Completed Tasks'}{/todo} {/if} @else@
<p>No tasks yet. Add one above!</p>
{/todos}
|
|
App: examples - Page: show
PHP
show.php
|
HTML
show.pad
|
Result
|
<?php
// One example, three panels: the .php and .pad sources coloured by the _common colours // library, the .html as what it is - the example's rendered output. The name is taken // from the query, so it is held to the shape a harvest writes and to the store itself.
$exDir = DATA . 'examples/';
// The query travels along on every link, so "back to the search" lands on the results // the visitor came from - the index runs the same search for a GET as for the form.
$q = padMakeSafe ( $q ?? '', 200 );
$item = padMakeSafe ( $item ?? '', 200 ); $show = '';
if ( $item and preg_match ( '#^[a-zA-Z0-9_/-]+$#', $item ) ) {
$exHtml = realpath ( $exDir . "$item.html" );
if ( $exHtml and str_starts_with ( $exHtml, realpath ( $exDir ) . '/' ) ) $show = $item;
}
$hasPhp = ( $show and file_exists ( $exDir . "$show.php" ) ) ? 1 : 0; $hasPad = ( $show and file_exists ( $exDir . "$show.pad" ) ) ? 1 : 0;
$srcPhp = ( $hasPhp ) ? padColorsFile ( $exDir . "$show.php" ) : ''; $srcPad = ( $hasPad ) ? padColorsFile ( $exDir . "$show.pad" ) : '';
// The stored render is output as-is, so its braces are escaped the way the colours // library escapes its own - the exit unescapes them, the level loop never sees them.
$result = ( $show ) ? padEscape ( padFileGet ( $exDir . "$show.html" ) ) : '';
// The example's name is the title, exactly as it is - the common header would space its // underscores and capitalise it, so the page writes its own.
if ( $show ) $skipTitle = 1;
$title = ( $show ) ? $show : 'Examples';
?>
|
{if $show eq ''}
<p>Pick an example from the <a href="?index{?q}">search</a>.</p>
@else@
<h1>{$show}</h1>
<p><a href="?index{?q}">back to the search</a></p>
<div class="example-view">
{if $hasPhp eq 1} <div class="example-panel"> <h4>PHP</h4> <div class="example-body">{!srcPhp}</div> </div> {/if}
{if $hasPad eq 1} <div class="example-panel"> <h4>PAD</h4> <div class="example-body">{!srcPad}</div> </div> {/if}
<div class="example-panel"> <h4>Result</h4> <div class="example-body">{!result}</div> </div>
</div>
{/if}
|
|
App: ledger - Page: index
PHP
index.php
|
HTML
index.pad
|
Result
|
<?php
// The month at a glance: what came in, what went out, what is left; six months of it as a // chart, this month's spending by category, the budgets, the accounts, the latest entries // - and a quick entry that ledger.js posts as JSON, answered by quick.php.
$totals = ledgerTotals ( $ledgerMonth ); $history = ledgerHistory ( $ledgerMonth ); $spending = ledgerSpending ( $ledgerMonth ); $budgets = ledgerBudgets ( $ledgerMonth ); $accounts = ledgerAccounts (); $latest = array_slice ( array_values ( array_filter ( ledgerRows ( $ledgerMonth ), fn ( $r ) => $r ['kind'] != 'transfer' ) ), 0, 8 ); $spendable = array_values ( array_filter ( ledgerCategories (), fn ( $c ) => $c ['kind'] == 'expense' ) ); $monthName = ledgerMonthName ( $ledgerMonth ); $today = date ( 'Y-m-d' );
$ledgerPage = $monthName;
?>
|
<div class="head"> <h1>{$monthName}</h1> <span class="dim">{$totals.entries} entries</span> </div>
<section class="kpis" aria-label="This month"> <div class="kpi in"><span>Income</span><b data-kpi="income">{$totals.income | money}</b></div> <div class="kpi out"><span>Spending</span><b data-kpi="spent">{$totals.spent | money}</b></div> <div class="kpi"><span>Left</span><b data-kpi="left">{$totals.left | money}</b></div> <div class="kpi"><span>Kept</span><b data-kpi="kept">{$totals.kept}%</b></div> </section>
<div class="columns">
<div class="main">
<section class="card"> <h2>Six months</h2> {chart 'bar', data='history', label='month', value='income, spending', title='Income and spending, six months'} </section>
<section class="card"> <h2>Where it went</h2> {if $totals.spent gt 0} {chart 'donut', data='spending', label='category', value='spent', title='Spending by category'} {else} <p class="dim">Nothing spent this month.</p> {/if} </section>
<section class="card"> <h2>Budgets <a class="more" href="{$padGo}budgets{?month}">Change</a></h2> <ul class="budgets"> {budgets} <li class="{$state}"> <div class="line"><span class="dot" style="background: {$color}"></span><b>{$name}</b><span>{$spent | money} of {$monthly | money}</span></div> {progress $share, max=100, label=$name, color=$tone} </li> @else@ <li class="dim">No budgets yet.</li> {/budgets} </ul> </section>
</div>
<aside class="side">
<section class="card quick"> <h2>Quick entry</h2> {form 'quick', action='?quick', class='quick-form'} <input type="hidden" name="month" value="{$ledgerMonth}"> <label for="q-amount">Amount</label> <input id="q-amount" name="amount" inputmode="decimal" placeholder="12.50" required pattern="[0-9]+([.,][0-9][0-9]?)?"> <label for="q-category">Category</label> <select id="q-category" name="category">{spendable}<option value="{$id}">{$name}</option>{/spendable}</select> <label for="q-note">Note</label> <input id="q-note" name="note" placeholder="Coffee" maxlength="120"> <button class="button primary">Spend it</button> <p class="said" role="status"></p> {/form} </section>
<section class="card"> <h2>Accounts</h2> <ul class="accounts"> {accounts}<li><span>{$name}</span><b class="{if $balance lt 0}neg{/if}">{$balance | money}</b></li>{/accounts} </ul> </section>
<section class="card"> <h2>Latest <a class="more" href="{$padGo}transactions{?month}">All</a></h2> <ul class="latest"> {latest}<li><span class="day">{$day | date('j M')}</span><span class="what">{$note}<small>{$category}</small></span><b class="{if $amount lt 0}neg{else}pos{/if}">{$amount | money}</b></li> @else@<li class="dim">No entries this month.</li>{/latest} </ul> </section>
</aside>
</div>
|
|
App: manual - Page: constructs/else_1
App: manual - Page: constructs/else_2
App: manual - Page: constructs/else_4
PHP
constructs/else_4.php
|
HTML
constructs/else_4.pad
|
Result
|
<?php
$myVar = FALSE;
?>
|
{myVar} true @else@ false {/myVar}
|
|
App: manual - Page: content/5
App: manual - Page: content/7
App: manual - Page: fragments/collections_2
PHP
fragments/collections_2.php
|
HTML
fragments/collections_2.pad
|
Result
|
<?php
$slug = 'markdown';
?>
|
{collection 'news', slug=$slug} <h3>{$title}</h3> {!body} @else@ No such post. {/collection}
|
|
App: manual - Page: fragments/content_3
PHP
|
HTML
fragments/content_3.pad
|
Result
|
|
{content 'verdict'}Found: @else@Nothing there.{/content}
{data 'hits'}["first","second"]{/data} {data 'none'}[]{/data}
<p>{hits content='verdict'}{$hits} {/hits}</p> <p>{none content='verdict'}{$none} {/none}</p>
|
|
App: manual - Page: fragments/database_2
PHP
fragments/database_2.php
|
HTML
fragments/database_2.pad
|
Result
|
<?php
$earners = db ( "array name, salary from staff where salary >= 2000" );
?>
|
<table border="1"> {earners} <tr> <td>{$name}</td> <td>{$salary}</td> </tr> @else@ <tr> <td>nobody earns that much</td> </tr> {/earners} </table>
|
|
|