-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmouse_move.php
More file actions
125 lines (108 loc) · 5.25 KB
/
Copy pathmouse_move.php
File metadata and controls
125 lines (108 loc) · 5.25 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
<?php
/**
* Move Mouse to Cage
*
* Two-step write that maintains the cage history invariant:
* 1. Close the currently-open history interval (the row with moved_out_at IS NULL).
* 2. Open a new interval pointing at the target cage (or NULL for "remove from cage").
* 3. Update mice.current_cage_id to match.
*
* Wrapped in a transaction so we never end up with two open intervals or with
* current_cage_id out of sync with the open history row. CSRF-protected.
*/
require 'session_config.php';
require 'dbcon.php';
require_once 'log_activity.php';
require_once 'services/roles.php';
require_once 'includes/cage_access.php';
if (!isset($_SESSION['username'])) { header('Location: index.php'); exit; }
if (!role_can_write($_SESSION['role'] ?? null)) { $_SESSION['message'] = 'Your role has view-only access and cannot move mice.'; header('Location: mouse_dash.php'); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: mouse_dash.php'); exit; }
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) { die('CSRF token validation failed'); }
$mouse_id = trim($_POST['mouse_id'] ?? '');
$target_cage_raw = trim($_POST['target_cage_id'] ?? '');
$reason = trim($_POST['reason'] ?? '') ?: null;
$user_id = $_SESSION['user_id'] ?? null;
// Optional. Pages that initiate a transfer from their own context (cage
// view, mouse_edit) pass this so we return there instead of bouncing
// the user to mouse_view. Restricted to a small allow-list to prevent
// open-redirect via a forged form input.
$redirectTo = $_POST['redirect_to'] ?? '';
$allowedRedirect = ['mouse_view.php', 'hc_view.php', 'bc_view.php', 'mouse_edit.php', 'mouse_dash.php'];
$redirectBase = $allowedRedirect[0];
foreach ($allowedRedirect as $cand) {
if (strpos($redirectTo, $cand) === 0) { $redirectBase = $redirectTo; break; }
}
// "__none__" is the explicit "remove from cage" sentinel from the form
$target_cage = ($target_cage_raw === '__none__' || $target_cage_raw === '') ? null : $target_cage_raw;
if ($mouse_id === '') {
$_SESSION['message'] = 'Missing mouse_id.';
header('Location: mouse_dash.php'); exit;
}
// Load mouse
$stmt = $con->prepare("SELECT current_cage_id, status FROM mice WHERE mouse_id = ?");
$stmt->bind_param("s", $mouse_id);
$stmt->execute();
$res = $stmt->get_result();
if ($res->num_rows !== 1) {
$_SESSION['message'] = 'Mouse not found.';
header('Location: mouse_dash.php'); exit;
}
$row = $res->fetch_assoc();
$stmt->close();
// Per-cage authorization: a non-admin may only move a mouse out of a cage they
// are assigned to (matches the cage pages and the API). Without this, any
// write-capable user could move mice in cages they have no access to.
if (!cage_user_can_write_mouse($con, $user_id, $_SESSION['role'] ?? null, $row['current_cage_id'])) {
$_SESSION['message'] = 'Access denied. You can only move mice in cages you are assigned to.';
header("Location: mouse_view.php?id=" . urlencode($mouse_id)); exit;
}
if (in_array($row['status'], ['sacrificed','archived'], true)) {
$_SESSION['message'] = 'Cannot move a sacrificed or archived mouse.';
header("Location: mouse_view.php?id=" . urlencode($mouse_id)); exit;
}
if ($row['current_cage_id'] === $target_cage) {
$_SESSION['message'] = 'Mouse is already in that cage.';
header("Location: mouse_view.php?id=" . urlencode($mouse_id)); exit;
}
// Validate target cage exists & is active (skip when removing-from-cage)
if ($target_cage !== null) {
$chk = $con->prepare("SELECT 1 FROM cages WHERE cage_id = ? AND status = 'active'");
$chk->bind_param("s", $target_cage);
$chk->execute();
if ($chk->get_result()->num_rows === 0) {
$_SESSION['message'] = "Target cage doesn't exist or is archived.";
header("Location: mouse_view.php?id=" . urlencode($mouse_id)); exit;
}
$chk->close();
}
mysqli_begin_transaction($con);
try {
// Close currently-open interval
$close = $con->prepare("UPDATE mouse_cage_history SET moved_out_at = CURRENT_TIMESTAMP WHERE mouse_id = ? AND moved_out_at IS NULL");
$close->bind_param("s", $mouse_id);
$close->execute();
$close->close();
// Open new interval
$open = $con->prepare("INSERT INTO mouse_cage_history (mouse_id, cage_id, reason, moved_by) VALUES (?, ?, ?, ?)");
$open->bind_param("sssi", $mouse_id, $target_cage, $reason, $user_id);
$open->execute();
$open->close();
// Update denormalized pointer + status if removing from cage
$newStatus = $target_cage === null ? 'transferred_out' : 'alive';
$upd = $con->prepare("UPDATE mice SET current_cage_id = ?, status = ? WHERE mouse_id = ?");
$upd->bind_param("sss", $target_cage, $newStatus, $mouse_id);
$upd->execute();
$upd->close();
mysqli_commit($con);
log_activity($con, 'move', 'mouse', $mouse_id,
"Moved " . ($row['current_cage_id'] ?? '(no cage)') . " → " . ($target_cage ?? '(no cage)') . ($reason ? ": $reason" : ''));
$_SESSION['message'] = "Mouse moved to " . ($target_cage ?? 'no cage') . ".";
} catch (Exception $e) {
mysqli_rollback($con);
$_SESSION['message'] = 'Failed to move mouse: ' . $e->getMessage();
}
header("Location: " . ($redirectBase === 'mouse_view.php'
? 'mouse_view.php?id=' . urlencode($mouse_id)
: $redirectBase));
exit;