252 lines
7.1 KiB
PHP
252 lines
7.1 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/persona.php';
|
|
require_once __DIR__ . '/utils.php';
|
|
|
|
const CHORE_SCHEDULE_ONCE = 'once';
|
|
const CHORE_SCHEDULE_RECURRING = 'recurring';
|
|
const CHORE_NFC_HASH_ALGO = 'sha256';
|
|
|
|
/**
|
|
* @param mixed $raw
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
function normalizeChoresList($raw): array {
|
|
if (!is_array($raw) || !array_is_list($raw)) {
|
|
return [];
|
|
}
|
|
$out = [];
|
|
foreach ($raw as $row) {
|
|
if (is_array($row) && !empty($row['id']) && is_string($row['id'])) {
|
|
$out[] = $row;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $people
|
|
* @return array<int, string>
|
|
*/
|
|
function legacyAssigneeNameToIds(string $name, array $people): array {
|
|
$name = trim($name);
|
|
if ($name === '') {
|
|
return [];
|
|
}
|
|
$lower = familyHubStrLowerUtf8($name);
|
|
$ids = [];
|
|
foreach ($people as $p) {
|
|
$pn = trim((string) ($p['name'] ?? ''));
|
|
if ($pn !== '' && familyHubStrLowerUtf8($pn) === $lower) {
|
|
$ids[] = (string) $p['id'];
|
|
}
|
|
}
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $c
|
|
* @param array<int, array<string, mixed>> $people
|
|
* @return array<string, mixed>
|
|
*/
|
|
function migrateLegacyChoreRow(array $c, array $people): array {
|
|
if (isset($c['name']) && !isset($c['title'])) {
|
|
$c['title'] = (string) $c['name'];
|
|
}
|
|
if (!isset($c['assignee_ids']) || !is_array($c['assignee_ids'])) {
|
|
if (isset($c['assignee'])) {
|
|
$c['assignee_ids'] = legacyAssigneeNameToIds((string) $c['assignee'], $people);
|
|
} else {
|
|
$c['assignee_ids'] = [];
|
|
}
|
|
}
|
|
$ids = [];
|
|
foreach ($c['assignee_ids'] as $id) {
|
|
if (is_string($id) && $id !== '') {
|
|
$ids[] = $id;
|
|
}
|
|
}
|
|
$c['assignee_ids'] = array_values(array_unique($ids));
|
|
if (!isset($c['lists']) || !is_array($c['lists'])) {
|
|
$c['lists'] = [];
|
|
}
|
|
$c['lists'] = normalizeChoreLists($c['lists']);
|
|
if (!isset($c['description'])) {
|
|
$c['description'] = '';
|
|
}
|
|
if (!isset($c['image'])) {
|
|
$c['image'] = '';
|
|
}
|
|
if (!isset($c['author_id'])) {
|
|
$c['author_id'] = '';
|
|
}
|
|
if (!isset($c['value'])) {
|
|
$c['value'] = 0;
|
|
}
|
|
$c['value'] = is_numeric($c['value']) ? (float) $c['value'] : 0.0;
|
|
if (!isset($c['due_date'])) {
|
|
$c['due_date'] = '';
|
|
}
|
|
$c['due_date'] = is_string($c['due_date']) ? trim($c['due_date']) : '';
|
|
if ($c['due_date'] !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $c['due_date'])) {
|
|
$c['due_date'] = '';
|
|
}
|
|
$sched = $c['schedule'] ?? CHORE_SCHEDULE_ONCE;
|
|
$c['schedule'] = $sched === CHORE_SCHEDULE_RECURRING ? CHORE_SCHEDULE_RECURRING : CHORE_SCHEDULE_ONCE;
|
|
if (!isset($c['recurrence_days']) || !is_numeric($c['recurrence_days'])) {
|
|
$c['recurrence_days'] = 7;
|
|
}
|
|
$c['recurrence_days'] = max(1, (int) $c['recurrence_days']);
|
|
if (!isset($c['status'])) {
|
|
$c['status'] = 'active';
|
|
}
|
|
$st = (string) $c['status'];
|
|
$c['status'] = in_array($st, ['active', 'completed'], true) ? $st : 'active';
|
|
if (!array_key_exists('pending_submission', $c)) {
|
|
$c['pending_submission'] = null;
|
|
}
|
|
if ($c['pending_submission'] !== null && !is_array($c['pending_submission'])) {
|
|
$c['pending_submission'] = null;
|
|
}
|
|
$c['anyone_can_complete'] = !empty($c['anyone_can_complete']);
|
|
$c['nfc'] = normalizeChoreNfcMeta($c['nfc'] ?? null);
|
|
return $c;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $raw
|
|
* @return array<string, mixed>
|
|
*/
|
|
function normalizeChoreNfcMeta($raw): array {
|
|
if (!is_array($raw)) {
|
|
return [
|
|
'token_hash' => '',
|
|
'enabled' => false,
|
|
'created_at' => '',
|
|
'last_used_at' => null,
|
|
'last_used_ip' => null,
|
|
];
|
|
}
|
|
$tokenHash = isset($raw['token_hash']) ? trim((string) $raw['token_hash']) : '';
|
|
$createdAt = isset($raw['created_at']) ? trim((string) $raw['created_at']) : '';
|
|
$enabled = !empty($raw['enabled']) && $tokenHash !== '';
|
|
$lastUsedAt = isset($raw['last_used_at']) ? trim((string) $raw['last_used_at']) : '';
|
|
$lastUsedIp = isset($raw['last_used_ip']) ? trim((string) $raw['last_used_ip']) : '';
|
|
return [
|
|
'token_hash' => $tokenHash,
|
|
'enabled' => $enabled,
|
|
'created_at' => $createdAt,
|
|
'last_used_at' => $lastUsedAt !== '' ? $lastUsedAt : null,
|
|
'last_used_ip' => $lastUsedIp !== '' ? $lastUsedIp : null,
|
|
];
|
|
}
|
|
|
|
function generateOpaqueToken(int $bytes = 24): string {
|
|
$raw = random_bytes($bytes);
|
|
return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
|
|
}
|
|
|
|
function hashOpaqueToken(string $token): string {
|
|
return hash(CHORE_NFC_HASH_ALGO, $token);
|
|
}
|
|
|
|
function validateTokenHash(string $token, string $storedHash): bool {
|
|
if ($token === '' || $storedHash === '') {
|
|
return false;
|
|
}
|
|
return hash_equals($storedHash, hashOpaqueToken($token));
|
|
}
|
|
|
|
function requestClientAddress(): ?string {
|
|
$ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
|
return $ip !== '' ? $ip : null;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $lists
|
|
* @return array<int, array{type: string, items: array<int, string>}>
|
|
*/
|
|
function normalizeChoreLists($lists): array {
|
|
if (!is_array($lists)) {
|
|
return [];
|
|
}
|
|
$out = [];
|
|
foreach ($lists as $block) {
|
|
if (!is_array($block)) {
|
|
continue;
|
|
}
|
|
$type = isset($block['type']) ? (string) $block['type'] : 'checkbox';
|
|
if (!in_array($type, ['ordered', 'unordered', 'checkbox'], true)) {
|
|
$type = 'checkbox';
|
|
}
|
|
$items = $block['items'] ?? [];
|
|
if (!is_array($items)) {
|
|
$items = [];
|
|
}
|
|
$clean = [];
|
|
foreach ($items as $it) {
|
|
$s = trim((string) $it);
|
|
if ($s !== '') {
|
|
$clean[] = $s;
|
|
}
|
|
}
|
|
$out[] = ['type' => $type, 'items' => $clean];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $chores
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
function migrateAllChores(array $chores, array $people): array {
|
|
$out = [];
|
|
foreach ($chores as $c) {
|
|
$out[] = migrateLegacyChoreRow($c, $people);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $chores
|
|
*/
|
|
function findChoreById(array $chores, string $id): ?array {
|
|
foreach ($chores as $c) {
|
|
if (($c['id'] ?? '') === $id) {
|
|
return $c;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $chores
|
|
*/
|
|
function findChoreIndexById(array $chores, string $id): ?int {
|
|
foreach ($chores as $i => $c) {
|
|
if (($c['id'] ?? '') === $id) {
|
|
return $i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $assigneeIds
|
|
* @param array<int, array<string, mixed>> $people
|
|
*/
|
|
function choreAssigneeIdsValid(array $assigneeIds, array $people): bool {
|
|
$set = [];
|
|
foreach ($people as $p) {
|
|
if (!empty($p['id'])) {
|
|
$set[(string) $p['id']] = true;
|
|
}
|
|
}
|
|
foreach ($assigneeIds as $id) {
|
|
if (!isset($set[$id])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|