Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
188
app/Services/PrivacyService.php
Normal file
188
app/Services/PrivacyService.php
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue