02794e565e
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).
98 lines
3.2 KiB
PHP
Executable File
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;
|
|
} |