188 lines
8.6 KiB
PHP
188 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
final class PrivacyService
|
|
{
|
|
public static function logAccess(
|
|
string $type,
|
|
?int $id,
|
|
?int $animalId,
|
|
string $name,
|
|
string $action = 'view',
|
|
): void {
|
|
DB::pdo()
|
|
->prepare(
|
|
'INSERT INTO private_file_access_log(user_id,resource_type,resource_id,animal_id,action,original_name,ip_address,user_agent) VALUES(?,?,?,?,?,?,?,?)',
|
|
)
|
|
->execute([
|
|
Auth::id(),
|
|
$type,
|
|
$id,
|
|
$animalId,
|
|
$action,
|
|
mb_substr($name, 0, 240),
|
|
$_SERVER['REMOTE_ADDR'] ?? null,
|
|
mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 500) ?: null,
|
|
]);
|
|
}
|
|
public static function accessLog(int $limit = 500): array
|
|
{
|
|
return DB::pdo()
|
|
->query(
|
|
'SELECT l.*,COALESCE(u.display_name,u.username) actor,a.name animal_name FROM private_file_access_log l LEFT JOIN users u ON u.id=l.user_id LEFT JOIN animals a ON a.id=l.animal_id ORDER BY l.id DESC LIMIT ' .
|
|
max(1, min(2000, $limit)),
|
|
)
|
|
->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
public static function retentionReview(int $years): array
|
|
{
|
|
$rows = DB::pdo()
|
|
->query(
|
|
"SELECT dc.id,dc.name,dc.email,dc.phone,dc.updated_at,group_concat(DISTINCT r.role) roles,MAX(COALESCE(ad.adoption_date,ap.event_date,dc.updated_at)) last_activity,(SELECT COUNT(*) FROM animal_placements active JOIN animals a ON a.id=active.animal_id WHERE active.contact_id=dc.id AND active.event_type='foster' AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status)='fa') active_links FROM directory_contacts dc LEFT JOIN directory_contact_roles r ON r.contact_id=dc.id LEFT JOIN adoptions ad ON ad.adopter_contact_id=dc.id LEFT JOIN animal_placements ap ON ap.contact_id=dc.id WHERE dc.deleted_at IS NULL AND dc.kind='person' AND dc.anonymized_at IS NULL GROUP BY dc.id ORDER BY date(last_activity),dc.name COLLATE NOCASE",
|
|
)
|
|
->fetchAll(PDO::FETCH_ASSOC);
|
|
$out = [];
|
|
foreach ($rows as $row) {
|
|
$roles = array_filter(explode(',', (string) $row['roles']));
|
|
$limits = [];
|
|
foreach ($roles as $role) {
|
|
$limits[] = match ($role) {
|
|
'adoptant' => AppSettings::int('privacy_retention_adopter_years', 1, 30),
|
|
'fa' => AppSettings::int('privacy_retention_foster_years', 1, 30),
|
|
'benevole' => AppSettings::int('privacy_retention_volunteer_years', 1, 30),
|
|
default => $years,
|
|
};
|
|
}
|
|
$limit = $limits ? max($limits) : $years;
|
|
$last = strtotime((string) $row['last_activity']) ?: time();
|
|
if ($last > strtotime('-' . $limit . ' years')) {
|
|
continue;
|
|
}
|
|
$row['policy_years'] = $limit;
|
|
$row['blocked'] = (int) $row['active_links'] > 0;
|
|
$out[] = $row;
|
|
}
|
|
return $out;
|
|
}
|
|
public static function exportContact(int $id): string
|
|
{
|
|
$db = DB::pdo();
|
|
$s = $db->prepare(
|
|
"SELECT dc.*,group_concat(r.role,',') roles,org.name organization_name FROM directory_contacts dc LEFT JOIN directory_contact_roles r ON r.contact_id=dc.id LEFT JOIN directory_contacts org ON org.id=dc.organization_id WHERE dc.id=? AND dc.deleted_at IS NULL GROUP BY dc.id",
|
|
);
|
|
$s->execute([$id]);
|
|
$contact = $s->fetch(PDO::FETCH_ASSOC);
|
|
if (!$contact) {
|
|
throw new RuntimeException(t('privacy.contact_not_found'));
|
|
}
|
|
$queries = [
|
|
'adoptions' =>
|
|
'SELECT a.internal_code,a.name animal,ad.adoption_date,ad.notes FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE ad.adopter_contact_id=? ORDER BY ad.adoption_date',
|
|
'placements' =>
|
|
'SELECT a.internal_code,a.name animal,ap.event_type,ap.event_date,ap.reason,ap.notes FROM animal_placements ap JOIN animals a ON a.id=ap.animal_id WHERE ap.contact_id=? ORDER BY ap.event_date,ap.id',
|
|
'deposited_animals' =>
|
|
'SELECT internal_code,name,intake_date,intake_type,intake_reason,intake_circumstances FROM animals WHERE depositor_contact_id=? ORDER BY intake_date',
|
|
];
|
|
$payload = ['generated_at' => (new DateTimeImmutable())->format(DATE_ATOM), 'contact' => $contact];
|
|
foreach ($queries as $key => $sql) {
|
|
$q = $db->prepare($sql);
|
|
$q->execute([$id]);
|
|
$payload[$key] = $q->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
$tmp = tempnam(sys_get_temp_dir(), 'globinours-contact-');
|
|
if ($tmp === false) {
|
|
throw new RuntimeException(t('privacy.export_failed'));
|
|
}
|
|
@unlink($tmp);
|
|
$path = $tmp . '.zip';
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
|
throw new RuntimeException(t('privacy.export_failed'));
|
|
}
|
|
$zip->addFromString(
|
|
'contact.json',
|
|
json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
);
|
|
foreach (['adoptions', 'placements', 'deposited_animals'] as $key) {
|
|
$zip->addFromString($key . '.csv', self::csv($payload[$key]));
|
|
}
|
|
$zip->addFromString(
|
|
'README.txt',
|
|
"Export de portabilité Globinours\nGénéré le " .
|
|
date('d/m/Y H:i') .
|
|
"\nDonnées à conserver dans un emplacement sécurisé.\n",
|
|
);
|
|
$zip->close();
|
|
return $path;
|
|
}
|
|
public static function anonymize(int $id, string $reason): void
|
|
{
|
|
$reason = trim($reason);
|
|
if ($reason === '') {
|
|
throw new RuntimeException(t('privacy.reason_required'));
|
|
}
|
|
$db = DB::pdo();
|
|
$s = $db->prepare(
|
|
"SELECT * FROM directory_contacts WHERE id=? AND deleted_at IS NULL AND kind='person' AND anonymized_at IS NULL",
|
|
);
|
|
$s->execute([$id]);
|
|
$contact = $s->fetch(PDO::FETCH_ASSOC);
|
|
if (!$contact) {
|
|
throw new RuntimeException(t('privacy.contact_not_found'));
|
|
}
|
|
$backup = BackupService::create('avant-anonymisation-contact-' . $id);
|
|
$label = t('privacy.anonymous_contact') . ' #' . $id;
|
|
$db->beginTransaction();
|
|
try {
|
|
$db->prepare(
|
|
"UPDATE directory_contacts SET name=?,phone=NULL,email=NULL,address=NULL,postal_code=NULL,city=NULL,country='France',organization_id=NULL,notes=NULL,anonymized_at=datetime('now'),anonymization_reason=?,updated_at=datetime('now') WHERE id=?",
|
|
)->execute([$label, $reason, $id]);
|
|
$db->prepare(
|
|
'UPDATE adoptions SET adopter_name=?,adopter_phone=NULL,adopter_email=NULL,adopter_address=NULL,adopter_postal_code=NULL,adopter_city=NULL WHERE adopter_contact_id=?',
|
|
)->execute([$label, $id]);
|
|
foreach ([(string) $contact['name']] as $old) {
|
|
$db->prepare(
|
|
'UPDATE animal_movements SET contact_name=? WHERE lower(trim(contact_name))=lower(trim(?))',
|
|
)->execute([$label, $old]);
|
|
}
|
|
$db->commit();
|
|
AuditService::log(
|
|
'contact_anonymized',
|
|
'/settings/privacy/anonymize',
|
|
'Contact anonymisé',
|
|
'contact',
|
|
$id,
|
|
['reason' => $reason, 'backup' => $backup['name'] ?? null],
|
|
);
|
|
} catch (Throwable $e) {
|
|
if ($db->inTransaction()) {
|
|
$db->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
public static function maintenance(): array
|
|
{
|
|
$months = AppSettings::int('privacy_access_log_months', 1, 120);
|
|
$s = DB::pdo()->prepare("DELETE FROM private_file_access_log WHERE datetime(accessed_at)<datetime('now',?)");
|
|
$s->execute(['-' . $months . ' months']);
|
|
return ['access_logs_removed' => $s->rowCount(), 'retention_months' => $months];
|
|
}
|
|
private static function csv(array $rows): string
|
|
{
|
|
if (!$rows) {
|
|
return "\xEF\xBB\xBF";
|
|
}
|
|
$stream = fopen('php://temp', 'r+');
|
|
fwrite($stream, "\xEF\xBB\xBF");
|
|
fputcsv($stream, array_keys($rows[0]), ';');
|
|
foreach ($rows as $row) {
|
|
fputcsv($stream, array_values($row), ';');
|
|
}
|
|
rewind($stream);
|
|
$out = stream_get_contents($stream);
|
|
fclose($stream);
|
|
return (string) $out;
|
|
}
|
|
}
|