feat: initial release of Protocol Droid v0.2.0

A tool for authoring, sharing, and curating social protocols.

Features:
- Protocol library with search, tag filtering, and sort by date/relevance
- Protocol authoring with structured fields (title, description, steps, outcome, practice, source, tags)
- Protocol forking with provenance tracking
- Collection creation with searchable protocol picker and ordering
- User accounts with roles (admin, member, viewer)
- YAML import/export for portability
- Self-hostable on LAMP/Cloudron, works in subdirectories
- Responsive design with hamburger menu on mobile
- About page

Security:
- CSRF protection via Origin/Referer validation
- Session regeneration on login/register
- Secure session cookie params (HttpOnly, SameSite, Secure)
- Visibility enforcement on private/unlisted items
- YAML object injection hardening
- Login rate limiting
- Path traversal protection
- Input validation and length clamping
- Vote value constraining

Stack: PHP 8.x + MySQL/MariaDB, vanilla JS frontend, no external dependencies.

Hippocratic License (HL3-CORE).
This commit is contained in:
Protocolbot
2026-07-06 13:18:08 -06:00
commit 02794e565e
23 changed files with 3346 additions and 0 deletions
Executable
+67
View File
@@ -0,0 +1,67 @@
<?php
// Root router — serve frontend or API
// Works at web root or in a subdirectory (e.g. /protocol-droid/)
// Detect the base path (for subdirectory deployments)
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
$basePath = rtrim(str_replace('\\', '/', dirname($scriptName)), '/');
// If basePath is just / or . , clear it
if ($basePath === '/' || $basePath === '.') $basePath = '';
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Strip base path from URI for routing
if ($basePath && strpos($uri, $basePath) === 0) {
$uri = substr($uri, strlen($basePath));
}
if (preg_match('#^/api(/|$)#', $uri)) {
require __DIR__ . '/api/index.php';
exit;
}
// Everything else: serve frontend files
// Normalize the path to prevent traversal
$uri = preg_replace('#/+#', '/', $uri);
$file = __DIR__ . '/frontend' . $uri;
// Resolve real path and verify it's within the frontend directory
$real_file = realpath($file);
$frontend_dir = realpath(__DIR__ . '/frontend');
if ($real_file === false || strpos($real_file, $frontend_dir) !== 0) {
$index = __DIR__ . '/frontend/index.html';
if (file_exists($index)) {
header('Content-Type: text/html');
readfile($index);
exit;
}
http_response_code(404);
echo 'Frontend not found';
exit;
}
if (!is_file($real_file)) {
$index = __DIR__ . '/frontend/index.html';
if (file_exists($index)) {
header('Content-Type: text/html');
readfile($index);
exit;
}
http_response_code(404);
echo 'Frontend not found';
exit;
}
$ext = pathinfo($real_file, PATHINFO_EXTENSION);
$types = [
'css' => 'text/css',
'js' => 'application/javascript',
'html' => 'text/html',
'svg' => 'image/svg+xml',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'json' => 'application/json',
];
if (isset($types[$ext])) header('Content-Type: ' . $types[$ext]);
readfile($real_file);