Files
protocol-droid/api/db.php
T
Protocolbot 02794e565e 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).
2026-07-06 13:18:08 -06:00

98 lines
3.2 KiB
PHP
Executable File

<?php
/**
* Database connection and helpers.
* Uses MySQL/MariaDB via PDO (Cloudron-compatible).
*/
function db(): PDO {
static $pdo = null;
if ($pdo === null) {
$host = getenv('MYSQL_HOST') ?: 'localhost';
$port = getenv('MYSQL_PORT') ?: '3306';
$name = getenv('MYSQL_DATABASE') ?: 'protocol_droid';
$user = getenv('MYSQL_USER') ?: 'root';
$pass = getenv('MYSQL_PASSWORD') ?: '';
$dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4";
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Database connection failed']);
exit;
}
}
return $pdo;
}
function db_init(): void {
$sql = file_get_contents(__DIR__ . '/../schema.sql');
if (!$sql) return;
// Split on semicolons at end of statements (simple split — schema has no complex strings with semicolons)
$statements = array_filter(array_map('trim', preg_split('/;\s*(?=\n|$)/', $sql)));
foreach ($statements as $stmt) {
if (empty($stmt)) continue;
try {
db()->exec($stmt);
} catch (PDOException $e) {
// Ignore "already exists" errors
if (strpos($e->getMessage(), 'already exists') === false) {
error_log("Schema init: " . $e->getMessage());
}
}
}
}
function db_query(string $sql, array $params = []): PDOStatement {
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt;
}
function db_one(string $sql, array $params = []): ?array {
$stmt = db_query($sql, $params);
$row = $stmt->fetch();
return $row ?: null;
}
function db_all(string $sql, array $params = []): array {
$stmt = db_query($sql, $params);
return $stmt->fetchAll();
}
function db_insert(string $table, array $data): int {
$cols = implode(', ', array_map(fn($c) => "`$c`", array_keys($data)));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO {$table} ({$cols}) VALUES ({$placeholders})";
db_query($sql, array_values($data));
return (int) db()->lastInsertId();
}
function db_update(string $table, array $data, string $where, array $whereParams): int {
$set = implode(', ', array_map(fn($c) => "`$c` = ?", array_keys($data)));
$sql = "UPDATE {$table} SET {$set} WHERE {$where}";
db_query($sql, array_merge(array_values($data), $whereParams));
return db_query("SELECT ROW_COUNT()")->fetchColumn();
}
function slugify(string $text): string {
$slug = strtolower($text);
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
$slug = trim($slug, '-');
$slug = substr($slug, 0, 128);
return $slug ?: 'untitled';
}
function ensure_unique_slug(string $base): string {
$slug = $base;
$i = 1;
while (db_one("SELECT slug FROM protocols WHERE slug = ?", [$slug]) ||
db_one("SELECT slug FROM collections WHERE slug = ?", [$slug])) {
$slug = $base . '-' . $i++;
}
return $slug;
}