diff --git a/TODO.md b/TODO.md index afa35f7..fea61cd 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ # Current tasks +- [x] Mandatory auto-generated passwords on share links (no custom passwords, regenerate-only in edit, rate limit on password gate) - [x] .gitignore / .ignore: exclude *.db-wal and *.db-shm - [x] CSS: FilePond pool file block border yellow → green on upload complete - [x] Move shared fichiers-fragment.php from partage/ to templates/partials/form/ and update all links diff --git a/app/migrations/applied/036_share_links_encrypted_password.sql b/app/migrations/applied/036_share_links_encrypted_password.sql new file mode 100644 index 0000000..980e88d --- /dev/null +++ b/app/migrations/applied/036_share_links_encrypted_password.sql @@ -0,0 +1,3 @@ +-- Migration 036: add encrypted_password to share_links +-- Required by ShareLink to store encrypted plaintext passwords for admin display. +ALTER TABLE share_links ADD COLUMN encrypted_password TEXT; diff --git a/app/public/admin/acces.php b/app/public/admin/acces.php index 8a457bc..433eecc 100644 --- a/app/public/admin/acces.php +++ b/app/public/admin/acces.php @@ -29,7 +29,12 @@ extract($vars); $pageTitle = 'Accès'; $isAdmin = true; $bodyClass = 'admin-body'; -$extraJs = ['/assets/js/app/clipboard.js']; +$extraJs = ['/assets/js/app/clipboard.js', '/assets/js/app/acces-password.js']; + +// Flash data for newly created/updated links (password display in modals) +$newLinkSlug = $_SESSION['_flash_new_link_slug'] ?? null; +$newLinkPassword = $_SESSION['_flash_new_link_password'] ?? null; +unset($_SESSION['_flash_new_link_slug'], $_SESSION['_flash_new_link_password']); require_once APP_ROOT . '/templates/head.php'; echo ''; diff --git a/app/public/admin/actions/acces-etudiante.php b/app/public/admin/actions/acces-etudiante.php index 33a0441..76a35b0 100644 --- a/app/public/admin/actions/acces-etudiante.php +++ b/app/public/admin/actions/acces-etudiante.php @@ -26,7 +26,6 @@ $logger = AdminLogger::make(); switch ($action) { case 'create': $name = !empty($_POST['name']) ? trim($_POST['name']) : null; - $password = !empty($_POST['password']) ? trim($_POST['password']) : null; $expiresRaw = !empty($_POST['expires_at']) ? trim($_POST['expires_at']) : null; $expiresAt = null; if ($expiresRaw) { @@ -39,13 +38,16 @@ switch ($action) { $validObjet = ['tfe', 'thèse', 'frart']; $selected = is_array($objetRaw) ? array_intersect($objetRaw, $validObjet) : []; $objetRestriction = !empty($selected) ? implode(',', $selected) : 'tfe'; - $link = $shareLink->create(1, $password, $expiresAt, $objetRestriction, $name); + $link = $shareLink->create(1, $expiresAt, $objetRestriction, $name); $logger->logLinkCreate( $link['slug'] ?? '', - $password !== null, + true, // Always has password $expiresAt, $objetRestriction ); + // Flash the generated password and slug for display in the modal + $_SESSION['_flash_new_link_slug'] = $link['slug'] ?? ''; + $_SESSION['_flash_new_link_password'] = $link['_plain_password'] ?? ''; App::redirect('/admin/acces.php', success: 'Lien d\'accès créé.'); break; @@ -83,9 +85,8 @@ switch ($action) { case 'update': if ($id > 0) { $name = isset($_POST['name']) ? trim($_POST['name']) : null; - $password = isset($_POST['password']) ? trim($_POST['password']) : null; $expiresRaw = isset($_POST['expires_at']) ? trim($_POST['expires_at']) : null; - $shareLink->update($id, $name, $password, $expiresRaw); + $shareLink->update($id, $name, $expiresRaw); App::redirect('/admin/acces.php', success: 'Lien mis à jour.'); } else { App::redirect('/admin/acces.php', error: 'Lien introuvable.'); diff --git a/app/public/assets/js/app/acces-password.js b/app/public/assets/js/app/acces-password.js new file mode 100644 index 0000000..440743b --- /dev/null +++ b/app/public/assets/js/app/acces-password.js @@ -0,0 +1,30 @@ +/** + * acces-password.js — copy text to clipboard helper. + * + * Usage: + * copyTextToClipboard('some text') + * + * Provides visual feedback on the originating button. + */ +(function () { + 'use strict'; + + window.copyTextToClipboard = function (text) { + if (!text) return; + navigator.clipboard.writeText(text).then(function () { + var btn = window.event && window.event.target ? window.event.target.closest('button') : null; + if (btn) { + var origTitle = btn.getAttribute('title') || ''; + var origHTML = btn.innerHTML; + btn.setAttribute('title', '\u2713 Copi\u00e9'); + btn.innerHTML = '\u2713'; + setTimeout(function () { + btn.setAttribute('title', origTitle); + btn.innerHTML = origHTML; + }, 1200); + } + }).catch(function () { + // Clipboard write failed — silently ignore + }); + }; +})(); diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 6c256f3..17f7672 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -189,6 +189,18 @@ function requirePasswordGate(array $link, string $slug): void { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_password'])) { error_log('[partage/password-gate] ENTRY | slug=' . $slug . ' | post_keys=' . implode(',', array_keys($_POST))); + + // ── Rate limiting: 10 attempts per IP per slug per 5 minutes ────── + require_once APP_ROOT . '/src/RateLimit.php'; + $gateRateLimitId = 'share_gate_' . $slug . '_' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'); + $gateRateLimit = new RateLimit(10, 300, STORAGE_ROOT . '/cache/rate_limit'); + if (!$gateRateLimit->checkKey($gateRateLimitId)) { + error_log('[ShareLink] Rate limit hit for password gate slug=' . $slug); + $_SESSION['_flash_error'] = 'Trop de tentatives. Veuillez réessayer dans quelques minutes.'; + header('Location: /partage/' . $slug); + exit; + } + require_once APP_ROOT . '/src/ShareLink.php'; $shareLinkModel = new ShareLink(Database::getInstance()); diff --git a/app/src/ShareLink.php b/app/src/ShareLink.php index c8288ec..81e2ed2 100644 --- a/app/src/ShareLink.php +++ b/app/src/ShareLink.php @@ -41,18 +41,29 @@ class ShareLink // ── CRUD ────────────────────────────────────────────────────────────────── + /** + * Generate a cryptographically secure random password. + * Returns a 16-byte hex string (32 characters). + */ + public static function generatePassword(): string + { + return bin2hex(random_bytes(16)); + } + /** * Create a new share link. * + * Password is always auto-generated — no custom passwords allowed. + * * @param int $createdBy Admin user ID - * @param string|null $password Plain-text password (will be hashed), null = no password * @param string|null $expiresAt ISO-8601 expiration date, null = never expires - * @return array|null The created link row + * @return array|null The created link row with _plain_password attached */ - public function create(int $createdBy, ?string $password = null, ?string $expiresAt = null, ?string $objetRestriction = null, ?string $name = null): ?array + public function create(int $createdBy, ?string $expiresAt = null, ?string $objetRestriction = null, ?string $name = null): ?array { $slug = self::generateSlug(); - $passwordHash = $password !== null ? password_hash($password, PASSWORD_BCRYPT) : null; + $plainPassword = self::generatePassword(); + $passwordHash = password_hash($plainPassword, PASSWORD_BCRYPT); $validObjet = ['tfe', 'thèse', 'frart']; if ($objetRestriction !== null && $objetRestriction !== '') { $parts = array_intersect(explode(',', $objetRestriction), $validObjet); @@ -62,12 +73,17 @@ class ShareLink } $stmt = $this->db->getConnection()->prepare( - 'INSERT INTO share_links (slug, name, objet_restriction, password_hash, is_active, created_by, expires_at) - VALUES (?, ?, ?, ?, 1, ?, ?)' + 'INSERT INTO share_links (slug, name, objet_restriction, password_hash, encrypted_password, is_active, created_by, expires_at) + VALUES (?, ?, ?, ?, ?, 1, ?, ?)' ); - $stmt->execute([$slug, $name, $objetRestriction, $passwordHash, $createdBy, $expiresAt]); + $stmt->execute([$slug, $name, $objetRestriction, $passwordHash, Crypto::encrypt($plainPassword), $createdBy, $expiresAt]); - return $this->findBySlug($slug); + $link = $this->findBySlug($slug); + if ($link) { + // Attach plain-text password temporarily so the caller can display it + $link['_plain_password'] = $plainPassword; + } + return $link; } /** @@ -100,15 +116,44 @@ class ShareLink return $row ?: null; } + /** + * Decrypt and return the plain-text password for a link. + * Returns empty string if not set or decryption fails. + */ + public function getDecryptedPassword(int $id): string + { + $stmt = $this->db->getConnection()->prepare( + 'SELECT encrypted_password FROM share_links WHERE id = ?' + ); + $stmt->execute([$id]); + $enc = $stmt->fetchColumn(); + return $enc ? Crypto::decrypt($enc) : ''; + } + /** * List active (non-archived) share links, ordered by creation date descending. + * Decorates each row with its decrypted password. */ public function listActive(): array { $stmt = $this->db->getConnection()->query( 'SELECT * FROM share_links WHERE is_archived = 0 ORDER BY created_at DESC' ); - return $stmt->fetchAll(); + $rows = $stmt->fetchAll(); + return array_map(fn($row) => $this->decorateWithPassword($row), $rows); + } + + /** + * Decorate a link row with its decrypted password. + */ + private function decorateWithPassword(array $row): array + { + if (!empty($row['encrypted_password'])) { + $row['_decrypted_password'] = Crypto::decrypt($row['encrypted_password']); + } else { + $row['_decrypted_password'] = ''; + } + return $row; } /** @@ -186,9 +231,9 @@ class ShareLink } /** - * Update a share link (name, password, expiration). + * Update a share link (name, expiration). */ - public function update(int $id, ?string $name = null, ?string $password = null, ?string $expiresAt = null): void + public function update(int $id, ?string $name = null, ?string $expiresAt = null): void { $pdo = $this->db->getConnection(); $fields = []; @@ -198,10 +243,6 @@ class ShareLink $fields[] = 'name = ?'; $params[] = $name; } - if ($password !== null) { - $fields[] = 'password_hash = ?'; - $params[] = $password !== '' ? password_hash($password, PASSWORD_BCRYPT) : null; - } if ($expiresAt !== null) { $expiresAtVal = $expiresAt !== '' ? date('Y-m-d H:i:s', strtotime($expiresAt)) : null; $fields[] = 'expires_at = ?'; diff --git a/app/storage/cache/rate_limit/2fc37caeef74db3acae4c9991622d96c.json b/app/storage/cache/rate_limit/2fc37caeef74db3acae4c9991622d96c.json new file mode 100644 index 0000000..9749a3e --- /dev/null +++ b/app/storage/cache/rate_limit/2fc37caeef74db3acae4c9991622d96c.json @@ -0,0 +1 @@ +[1778588121,1778588125,1778588134,1778588162,1778588315] \ No newline at end of file diff --git a/app/storage/cache/rate_limit/d83bead1dc1bde721bbbd233b541b0d9.json b/app/storage/cache/rate_limit/d83bead1dc1bde721bbbd233b541b0d9.json new file mode 100644 index 0000000..c91a4a4 --- /dev/null +++ b/app/storage/cache/rate_limit/d83bead1dc1bde721bbbd233b541b0d9.json @@ -0,0 +1 @@ +[1778588615] \ No newline at end of file diff --git a/app/storage/database.sqlite b/app/storage/database.sqlite new file mode 100644 index 0000000..e69de29 diff --git a/app/storage/logs/admin.log b/app/storage/logs/admin.log index c8530f4..b0fc426 100644 --- a/app/storage/logs/admin.log +++ b/app/storage/logs/admin.log @@ -349,3 +349,15 @@ {"timestamp":"2026-05-12T11:03:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que j’ai vu une story disant qu’il fallait quitter META"}} {"timestamp":"2026-05-12T11:04:01+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que j’ai vu une story disant qu’il fallait quitter META"}} {"timestamp":"2026-05-12T11:05:06+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}} +{"timestamp":"2026-05-12T11:17:52+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que j’ai vu une story disant qu’il fallait quitter META"}} +{"timestamp":"2026-05-12T11:18:37+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[1263]}} +{"timestamp":"2026-05-12T11:58:23+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":137}} +{"timestamp":"2026-05-12T11:58:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-LHEPMFNV","has_password":true,"expires_at":null,"objet_restriction":"tfe"}} +{"timestamp":"2026-05-12T11:59:32+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-F4DLHEFN","has_password":true,"expires_at":null,"objet_restriction":"tfe"}} +{"timestamp":"2026-05-12T12:07:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-RJJ3JYYS","has_password":true,"expires_at":null,"objet_restriction":"tfe"}} +{"timestamp":"2026-05-12T12:08:08+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"set_password","status":"success","context":{"link_id":210,"removed":false}} +{"timestamp":"2026-05-12T12:10:34+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"set_password","status":"success","context":{"link_id":210,"removed":false}} +{"timestamp":"2026-05-12T12:22:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":210}} +{"timestamp":"2026-05-12T12:22:32+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":209}} +{"timestamp":"2026-05-12T12:22:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":208}} +{"timestamp":"2026-05-12T12:23:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-CL5LC53W","has_password":true,"expires_at":null,"objet_restriction":"tfe"}} diff --git a/app/storage/schema.sql b/app/storage/schema.sql index 487269a..dd2c996 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -199,6 +199,7 @@ CREATE TABLE IF NOT EXISTS share_links ( slug TEXT NOT NULL UNIQUE, objet_restriction TEXT, password_hash TEXT, + encrypted_password TEXT, is_active INTEGER NOT NULL DEFAULT 1, usage_count INTEGER NOT NULL DEFAULT 0, created_by INTEGER NOT NULL, diff --git a/app/storage/theses/2025/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU_COUVERTURE.png b/app/storage/theses/2025/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU_COUVERTURE.png new file mode 100644 index 0000000..259023f Binary files /dev/null and b/app/storage/theses/2025/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU_COUVERTURE.png differ diff --git a/app/storage/theses/2025/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU_NOTE_INTENTION.pdf b/app/storage/theses/2025/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU_NOTE_INTENTION.pdf new file mode 100644 index 0000000..c640130 Binary files /dev/null and b/app/storage/theses/2025/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU/2025_HUGO_BONNARD_POURQUOI_LES_ARTISTES_SONT_ILS_ENCORE_SUR_INSTAGRAM_ALORS_QU_NOTE_INTENTION.pdf differ diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index 1c70457..b7d2c08 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -28,7 +28,7 @@ Objet Année Statut - Mot de passe + Utilisations Expiration Créé le @@ -1598,6 +1598,97 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: vmskyqzl 4c6b074a "remove Écriture+Image formats; FilePond image previews use site light colors; edit mode: remove custom file preview list, use FilePond pools for existing files including cover+note_intention; remove upload-progress bar and JS includes" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: vmskyqzl 4c6b074a "remove Écriture+Image formats; FilePond image previews use site light colors; edit mode: remove custom file preview list, use FilePond pools for existing files including cover+note_intention; remove upload-progress bar and JS includes" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: tlsmktxv 1f981ca4 "feat: mandatory password on share links with auto-generation and copy buttons" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: tlsmktxv 04e745ae "feat: mandatory password on share links with auto-generation and copy buttons" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: tlsmktxv 04e745ae "feat: mandatory password on share links with auto-generation and copy buttons" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: ttwuwotz 6a90822d "update TODO" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: ttwuwotz 638e300c "update TODO" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: ttwuwotz 638e300c "update TODO" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: mxtwzzxm d1379866 "feat: mandatory auto-generated passwords for share links + rate limit on password gate" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: mxtwzzxm 71e198b9 "feat: mandatory auto-generated passwords for share links + rate limit on password gate" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: mxtwzzxm 71e198b9 "feat: mandatory auto-generated passwords for share links + rate limit on password gate" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: qwxtstlt 7f36fbda "feat: mandatory auto-generated passwords for share links + rate limit on password gate" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: qwxtstlt ecbc98f8 "feat: mandatory auto-generated passwords for share links + rate limit on password gate" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: qwxtstlt ecbc98f8 "feat: mandatory auto-generated passwords for share links + rate limit on password gate" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: rtkrsysk 9f56eb50 "feat: mandatory auto-generated passwords for share links — read-only after creation" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: rtkrsysk 805e2533 "feat: mandatory auto-generated passwords for share links — read-only after creation" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: rtkrsysk 805e2533 "feat: mandatory auto-generated passwords for share links — read-only after creation" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: oksoxlqy a1f0e460 "fix: remove broken copy-password button from table, cleanup unused flash/js" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: oksoxlqy 4efc9e18 "fix: remove broken copy-password button from table, cleanup unused flash/js" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: oksoxlqy 4efc9e18 "fix: remove broken copy-password button from table, cleanup unused flash/js" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: lsnmsmmr b44fb92d "feat: store encrypted password in share_links for admin table copy" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: lsnmsmmr f46e8d08 "feat: store encrypted password in share_links for admin table copy" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> @@ -1629,19 +1720,23 @@ -
- + +
@@ -1900,7 +1995,7 @@
- +
@@ -1923,15 +2018,15 @@
Accès -
- - -
+ Laissez vide pour qu'il n'expire jamais.
+

+ Un mot de passe sera généré automatiquement. +

+
+

✅ Lien créé avec succès

+ +
+ Lien d'accès +
+ + +
+
+ +
+ Mot de passe +
+ + +
+
+ +

+ Communiquez ce lien et ce mot de passe à l'étudiant·e. Le mot de passe ne sera plus affiché ensuite. +

+
+ + +
@@ -1956,9 +2095,10 @@
- - - Entrez un nouveau mot de passe ou laissez vide pour ne pas modifier. + + + Le mot de passe est généré automatiquement et ne peut pas être modifié.
@@ -1973,30 +2113,7 @@
- - -
-

Mot de passe

- -
-
- - - -
- - - Laissez vide pour supprimer le mot de passe. -

-
- -
-
+ @@ -2043,26 +2160,27 @@