-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_data_issue.php
More file actions
140 lines (126 loc) · 5.76 KB
/
api_data_issue.php
File metadata and controls
140 lines (126 loc) · 5.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<?php
// api_data_issue.php — dedicated endpoint for "report data inaccuracy" on
// judge/attorney/jurisdiction entries. Kept separate from api_submit.php
// because:
// (a) data issues are private operator triage, NOT a public feed;
// (b) different rate limit + size bounds;
// (c) different storage (no release-at randomization; no token issuance).
//
// Storage: /var/www/fca/data/data_issues.db, single table `data_issues`,
// WAL mode, append-only from this endpoint. Operator reads via CLI / admin.
define('DB_PATH', '/var/www/fca/data/data_issues.db');
define('MAX_BODY', 65536); // 64 KB — plenty for a correction note
define('RATE_LIMIT', 10); // 10 submissions/hour per IP
// CLI: `php api_data_issue.php --init` creates the schema.
if (php_sapi_name() === 'cli') {
if (in_array('--init', $argv ?? [])) { initDB(); echo "data_issues DB initialized.\n"; }
else { echo "Usage: php api_data_issue.php --init\n"; }
exit(0);
}
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if ($method === 'OPTIONS') { http_response_code(200); exit; }
if ($method !== 'POST') {
http_response_code(405);
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
exit;
}
require_once __DIR__ . '/api_keys.php';
if (!fca_rate_ok('data_issue', RATE_LIMIT)) {
http_response_code(429);
echo json_encode(['ok' => false, 'error' => 'Rate limit — try again later.']);
exit;
}
// Accept both application/json and multipart/form-data. The frontend sends
// FormData (multipart) today; keeping JSON supported makes the endpoint
// usable from scripts and curl.
$ctype = strtolower($_SERVER['CONTENT_TYPE'] ?? '');
$data = [];
if (strpos($ctype, 'application/json') !== false) {
$raw = file_get_contents('php://input', false, null, 0, MAX_BODY);
if (!$raw) { http_response_code(400); echo json_encode(['ok'=>false,'error'=>'Empty body']); exit; }
$j = json_decode($raw, true);
if (!is_array($j)) { http_response_code(400); echo json_encode(['ok'=>false,'error'=>'Invalid JSON']); exit; }
$data = $j;
} else {
$data = $_POST;
}
// Pull + length-clamp each field. Never trust client length.
function _clip($v, int $max): string {
return substr((string)($v ?? ''), 0, $max);
}
$entity_type = _clip($data['entity_type'] ?? 'judge', 20);
$entity_id = _clip($data['judgeId'] ?? ($data['entity_id'] ?? ''), 100);
$entity_name = _clip($data['judge'] ?? ($data['entity_name'] ?? ''), 200);
$issue_type = _clip($data['issue'] ?? '', 30);
$desc = _clip($data['desc'] ?? '', 4000);
$email = _clip($data['email'] ?? '', 120);
// Validation
if (strlen($desc) < 20) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Description too short — include the error + a source.']);
exit;
}
$ALLOWED_ISSUES = ['name','court','county','status','moved','duplicate','missing','other'];
if (!in_array($issue_type, $ALLOWED_ISSUES, true)) { $issue_type = 'other'; }
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $email = ''; /* silently drop invalid */ }
try {
$db = getDB();
$stmt = $db->prepare('INSERT INTO data_issues
(entity_type, entity_id, entity_name, issue_type, description, contact_email, status, created_at, ip_hash)
VALUES (:et, :eid, :en, :it, :d, :em, :st, :ts, :ih)');
$stmt->bindValue(':et', $entity_type, SQLITE3_TEXT);
$stmt->bindValue(':eid', $entity_id, SQLITE3_TEXT);
$stmt->bindValue(':en', $entity_name, SQLITE3_TEXT);
$stmt->bindValue(':it', $issue_type, SQLITE3_TEXT);
$stmt->bindValue(':d', $desc, SQLITE3_TEXT);
$stmt->bindValue(':em', $email, SQLITE3_TEXT);
$stmt->bindValue(':st', 'pending', SQLITE3_TEXT);
$stmt->bindValue(':ts', gmdate('Y-m-d H:i:s'), SQLITE3_TEXT);
// IP hash (not raw IP) — same HMAC pattern used by the rate limiter.
// Lets the operator spot abuse without storing a reverse-identifiable IP.
$ip = fca_client_ip();
$stmt->bindValue(':ih', fca_rate_hash($ip), SQLITE3_TEXT);
$stmt->execute();
} catch (Throwable $e) {
error_log('api_data_issue INSERT: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Database error — please try again.']);
exit;
}
echo json_encode([
'ok' => true,
'message' => 'Correction received. It will be reviewed against primary sources before any change is applied.',
]);
function getDB(): SQLite3 {
$dir = dirname(DB_PATH);
if (!is_dir($dir)) mkdir($dir, 0750, true);
$db = new SQLite3(DB_PATH);
$db->exec('PRAGMA journal_mode = WAL');
$db->exec('PRAGMA foreign_keys = ON');
initDB($db);
return $db;
}
function initDB(?SQLite3 $db = null): void {
$own = false;
if ($db === null) { $db = new SQLite3(DB_PATH); $db->exec('PRAGMA journal_mode = WAL'); $own = true; }
$db->exec('CREATE TABLE IF NOT EXISTS data_issues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL,
entity_id TEXT,
entity_name TEXT,
issue_type TEXT NOT NULL,
description TEXT NOT NULL,
contact_email TEXT,
status TEXT NOT NULL DEFAULT "pending",
created_at TEXT NOT NULL,
ip_hash TEXT,
resolved_at TEXT,
resolution TEXT
)');
$db->exec('CREATE INDEX IF NOT EXISTS ix_data_issues_entity ON data_issues(entity_type, entity_id)');
$db->exec('CREATE INDEX IF NOT EXISTS ix_data_issues_status ON data_issues(status, created_at)');
if ($own) $db->close();
}