Major refactor

- update the structure to have monolithic setup
- updated deployments
- added live-reloading for devops
This commit is contained in:
Théophile Gervreau-Mercier
2026-02-05 20:07:05 +01:00
parent f23fbb481b
commit d2b3c6ca67
75 changed files with 3359 additions and 3987 deletions

31
apps/admin/.gitignore vendored
View File

@@ -1,31 +0,0 @@
# Test database
test.db
# Error logs
error.log
# Uploaded files (for testing)
data/theses/
data/covers/
# Keep the data directories but ignore contents
!data/theses/.gitkeep
!data/covers/.gitkeep
# PHP session files
sessions/
# Composer
vendor/
composer.lock
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
*~

View File

@@ -1,36 +0,0 @@
# Security headers
<IfModule mod_headers.c>
# Prevent clickjacking
Header always set X-Frame-Options "SAMEORIGIN"
# Prevent MIME type sniffing
Header always set X-Content-Type-Options "nosniff"
# Enable XSS protection
Header always set X-XSS-Protection "1; mode=block"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Content Security Policy (adjust as needed)
Header always set Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
</IfModule>
# Prevent directory listing
Options -Indexes
# Protect sensitive files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
<FilesMatch "(composer\.(json|lock)|error\.log)$">
Require all denied
</FilesMatch>
# PHP security settings (if .htaccess can override)
<IfModule mod_php.c>
php_flag display_errors Off
php_flag log_errors On
php_value error_log error.log
</IfModule>

View File

@@ -1,277 +0,0 @@
# PostERG - Formulaire d'ajout de mémoires
Le formulaire permet aux étudiant.e.s sortant de l'ERG en cursus de Master de soumettre leurs mémoires et travaux de fin d'études.
## Fonctionnalités
- Soumission de mémoires avec métadonnées complètes
- Stockage structuré dans base de données SQLite
- Support multi-auteurs, multi-superviseurs, multi-langues
- Gestion des mots-clés (max 10 par TFE)
- Téléversement sécurisé des fichiers
- Protection CSRF et validation complète
- Workflow de publication (soumission → soutenance → publication)
## Technologies
- PHP 7.4+ avec PDO SQLite
- SQLite 3.8+
- CSS fait-main + [Simple.css](https://simplecss.org/)
- [Symfony YAML](https://symfony.com/doc/current/components/yaml.html) (pour migration legacy)
- [Just](https://github.com/casey/just) pour les tâches de développement
## Installation
### Prérequis
```bash
# PHP avec SQLite
php -v # 7.4 ou supérieur
php -m | grep sqlite # Vérifier extension SQLite
# Composer
composer install
# Just (optionnel mais recommandé)
# macOS: brew install just
# Linux: cargo install just
```
### Configuration
1. **Base de données production:**
```bash
cd ../db
sqlite3 posterg.db < schema.sql
```
2. **Base de données de test:**
```bash
just init-test-db
```
## Développement local
### Avec Just (recommandé)
```bash
# Configuration complète et lancement du serveur
just dev
# Ou étape par étape:
just init-test-db # Créer la base de test
just serve # Lancer le serveur (réinitialise la DB)
just serve-only # Lancer sans réinitialiser
# Nettoyage
just cleanup # Supprimer test.db et fichiers uploadés
just reset # Cleanup + réinitialisation
# Statistiques
just stats # Voir les stats de la DB
just recent # Voir les soumissions récentes
just show 1 # Voir le TFE #1
# Autres commandes
just query # Shell SQLite interactif
just dump # Backup de la DB
```
### Sans Just
```bash
# Créer la base de test
sqlite3 test.db < ../db/schema.sql
# Lancer le serveur
php -S 127.0.0.1:3000
# Ouvrir dans le navigateur
open http://127.0.0.1:3000
```
## Structure du projet
```
formulaire/
├── assets/ # CSS et ressources
│ ├── normalize.css
│ ├── simple.css
│ ├── posterg.css
│ └── icon.svg
├── data/ # Données (gitignored)
│ ├── theses/ # Fichiers TFE uploadés
│ ├── covers/ # Images de couverture
│ └── yaml/ # Legacy YAML (migration)
├── Database.php # Classe helper pour DB
├── index.php # Formulaire de soumission
├── formulaire.php # Traitement de soumission
├── thanks.php # Page de confirmation
├── justfile # Tâches de développement
├── .gitignore # Fichiers ignorés
├── MIGRATION.md # Guide de migration YAML → SQLite
├── SECURITY.md # Documentation sécurité
└── README.md # Ce fichier
```
## Workflow de soumission
1. **Étudiant remplit le formulaire** (index.php)
- Informations de base (nom, année, titre)
- Détails académiques (orientation, AP, finalité)
- Contenu (synopsis, mots-clés, langues, formats)
- Upload fichiers (TFE + annexes)
2. **Validation et traitement** (formulaire.php)
- Validation CSRF token
- Sanitization des entrées
- Transaction DB (all-or-nothing)
- Création/liaison entités (auteur, superviseurs, mots-clés)
- Upload sécurisé avec noms aléatoires
- Génération identifiant unique (YYYY-NNN)
3. **Confirmation** (thanks.php)
- Affichage récapitulatif
- Statut: "En attente de publication"
- Liste des fichiers uploadés
4. **Publication** (admin - à venir)
- Après soutenance
- Ajout note contextuelle du jury (optionnel)
- Points du jury
- Publication publique
## Base de données
### Structure
- **19 tables** incluant tables de jonction et vues
- **Normalized 3NF** avec clés étrangères
- **Timestamps automatiques** via triggers
- **Cascade deletes** pour intégrité référentielle
### Tables principales
- `theses` - TFE avec métadonnées
- `authors` - Auteurs (réutilisables)
- `supervisors` - Promoteurs
- `thesis_files` - Métadonnées fichiers
- `keywords` - Mots-clés (extensible)
- Plus tables de référence et jonctions
### Vues
- `v_theses_full` - Vue complète pour admin
- `v_theses_public` - Vue filtrée pour public
Voir `../db/README.md` pour documentation complète.
## Sécurité
✅ **Protection CSRF** - Tokens de session
✅ **SQL Injection** - Prepared statements PDO
✅ **Path Traversal** - Validation stricte des chemins
✅ **File Upload** - Noms aléatoires, validation MIME
✅ **Input Validation** - Sanitization + validation typage
✅ **Error Handling** - Pas d'exposition de chemins système
Voir `SECURITY.md` pour détails complets.
## Tests
### Test manuel
1. Lancer serveur: `just dev`
2. Ouvrir http://127.0.0.1:3000
3. Remplir formulaire avec données test
4. Vérifier confirmation
5. Vérifier DB: `just stats` et `just recent`
### Checklist
- [ ] Form se charge sans erreurs
- [ ] Dropdowns peuplés depuis DB
- [ ] Validation champs requis fonctionne
- [ ] Upload fichiers réussit
- [ ] Transaction rollback sur erreur
- [ ] Page confirmation affiche données
- [ ] Identifiant unique généré (YYYY-NNN)
- [ ] Fichiers stockés avec noms aléatoires
## Migration données legacy
Si vous avez des fichiers YAML existants:
```bash
# Script de migration à créer
php migrate_yaml_to_sqlite.php
```
Voir `MIGRATION.md` pour guide complet.
## Production
### Déploiement
1. **Copier fichiers:**
```bash
rsync -av --exclude='test.db' --exclude='data/' \
formulaire/ user@server:/var/www/posterg/
```
2. **Créer DB production:**
```bash
cd /var/www/posterg/db
sqlite3 posterg.db < schema.sql
```
3. **Permissions:**
```bash
chown -R www-data:www-data /var/www/posterg
chmod 644 db/posterg.db
chmod 755 data/theses data/covers
```
4. **Configuration nginx:**
```nginx
location /formulaire {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ /index.php?$query_string;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
include fastcgi_params;
}
}
```
### Backup
```bash
# Backup automatique quotidien
0 2 * * * sqlite3 /var/www/posterg/db/posterg.db \
.dump > /backups/posterg_$(date +\%Y\%m\%d).sql
```
## Support
- **Schema DB:** `../db/README.md`
- **Setup DB:** `../db/SETUP.md`
- **Sécurité:** `SECURITY.md`
- **Migration:** `MIGRATION.md`
- **Specs techniques:** `../db/posterg_fiche-technique.md`
## Changelog
### v2.0 - 2026-01-27
- Migration vers SQLite
- Support multi-entités (auteurs, superviseurs, etc.)
- Sécurité renforcée
- Workflow de publication
- Justfile pour développement
### v1.0 - Précédent
- Stockage YAML
- Formulaire basique

Binary file not shown.

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-archive-restore"
version="1.1"
id="svg12"
sodipodi:docname="icon.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="24.719275"
inkscape:cx="5.2185997"
inkscape:cy="13.713995"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<rect
width="20"
height="5"
x="2"
y="4"
rx="2"
id="rect2"
style="stroke:#c104fc;stroke-opacity:1" />
<path
d="M12 13v7"
id="path4"
style="stroke:#c104fc;stroke-opacity:1" />
<path
d="m9 16 3-3 3 3"
id="path6"
style="stroke:#c104fc;stroke-opacity:1" />
<path
d="M4 9v9a2 2 0 0 0 2 2h2"
id="path8"
style="stroke:#c104fc;stroke-opacity:1" />
<path
d="M20 9v9a2 2 0 0 1-2 2h-2"
id="path10"
style="stroke:#c104fc;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,349 +0,0 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@@ -1,109 +0,0 @@
@font-face {
font-family: police1;
src: url("./Combinedd.otf");
}
/* Dark theme */
/* UTILE POUR FORCER UN MODE LIGHT */
@media (prefers-color-scheme: dark) {
:root,
::backdrop {
--bg: #fff;
--accent-bg: #f5f7ff;
--text: #212121;
--text-light: #585858;
--border: #898EA4;
--accent: #0d47a1;
--code: #d81b60;
--preformatted: #444;
--marked: #ffdd33;
--disabled: #efefef;
}
}
body{
background-color: white;
}
/* ENTÊTE */
header {
font-family: 'police1';
background: linear-gradient(280deg, rgba(77, 168, 112, 1) 0%, rgba(193, 4, 252, 1) 85%);
text-decoration: none;
outline: none;
font-size: 2rem;
}
body > header h1 {
color: white;
margin: 3rem auto auto auto;
}
/* FORMULAIRE */
form label {
font-family: police1;
font-size: 1rem;
}
form input,
select, textarea {
border-color: #c104fc;
overflow: visible;
outline: none;
background-color: white;
}
form input:focus,
select:focus {
border: 3px solid rgba(77, 168, 112, 1);
}
label{
margin-top: 2rem;
}
input {
/* font-family: police1; */
/* font-weight: bold; */
background-color: none;
color: rgb(193, 4, 252);
border: 1px solid rgb(193, 4, 252);
}
a{
color: rgb(193, 4, 252);
}
a:hover {
text-decoration: none;
}
a, a:visited {
color: rgb(193, 4, 252);
}
input:active {
border-color: rgba(77, 168, 112, 1);
}
button, [role="button"], input[type="submit"], input[type="reset"], input[type="button"], label[type="button"] {
background-color: rgb(193, 4, 252);
margin-top: 2rem;
}
/* For Google Chrome, Safari, and newer versions of Opera */
::placeholder {
/* color: rgb(213, 73, 255); */
font-size: 0.8rem;
}
/* For Mozilla Firefox */
::-moz-placeholder {
/* color: rgb(213, 73, 255); */
font-size: 0.8rem;}

View File

@@ -1,673 +0,0 @@
/* Global variables. */
:root,
::backdrop {
/* Set sans-serif & mono fonts */
--sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir,
"Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica,
"Helvetica Neue", sans-serif;
--mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
--standard-border-radius: 5px;
/* Default (light) theme */
--bg: #fff;
--accent-bg: #f5f7ff;
--text: #212121;
--text-light: #585858;
--border: #898EA4;
--accent: #0d47a1;
--code: #d81b60;
--preformatted: #444;
--marked: #ffdd33;
--disabled: #efefef;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root,
::backdrop {
color-scheme: dark;
--bg: #212121;
--accent-bg: #2b2b2b;
--text: #dcdcdc;
--text-light: #ababab;
--accent: #ffb300;
--code: #f06292;
--preformatted: #ccc;
--disabled: #111;
}
/* Add a bit of transparency so light media isn't so glaring in dark mode */
img,
video {
opacity: 0.8;
}
}
/* Reset box-sizing */
*, *::before, *::after {
box-sizing: border-box;
}
/* Reset default appearance */
textarea,
select,
input,
progress {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
html {
/* Set the font globally */
font-family: var(--sans-font);
scroll-behavior: smooth;
}
/* Make the body a nice central block */
body {
color: var(--text);
background-color: var(--bg);
font-size: 1.15rem;
line-height: 1.5;
display: grid;
grid-template-columns: 1fr min(45rem, 90%) 1fr;
margin: 0;
}
body > * {
grid-column: 2;
}
/* Make the header bg full width, but the content inline with body */
body > header {
background-color: var(--accent-bg);
border-bottom: 1px solid var(--border);
text-align: center;
padding: 0 0.5rem 2rem 0.5rem;
grid-column: 1 / -1;
}
body > header h1 {
max-width: 1200px;
margin: 1rem auto;
}
body > header p {
max-width: 40rem;
margin: 1rem auto;
}
/* Add a little padding to ensure spacing is correct between content and header > nav */
main {
padding-top: 1.5rem;
}
body > footer {
margin-top: 4rem;
padding: 2rem 1rem 1.5rem 1rem;
color: var(--text-light);
font-size: 0.9rem;
text-align: center;
border-top: 1px solid var(--border);
}
/* Format headers */
h1 {
font-size: 3rem;
}
h2 {
font-size: 2.6rem;
margin-top: 3rem;
}
h3 {
font-size: 2rem;
margin-top: 3rem;
}
h4 {
font-size: 1.44rem;
}
h5 {
font-size: 1.15rem;
}
h6 {
font-size: 0.96rem;
}
/* Prevent long strings from overflowing container */
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/* Fix line height when title wraps */
h1,
h2,
h3 {
line-height: 1.1;
}
/* Reduce header size on mobile */
@media only screen and (max-width: 720px) {
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2.1rem;
}
h3 {
font-size: 1.75rem;
}
h4 {
font-size: 1.25rem;
}
}
/* Format links & buttons */
a,
a:visited {
color: var(--accent);
}
a:hover {
text-decoration: none;
}
button,
[role="button"],
input[type="submit"],
input[type="reset"],
input[type="button"],
label[type="button"] {
border: none;
border-radius: var(--standard-border-radius);
background-color: var(--accent);
font-size: 1rem;
color: var(--bg);
padding: 0.7rem 0.9rem;
margin: 0.5rem 0;
}
button[disabled],
[role="button"][aria-disabled="true"],
input[type="submit"][disabled],
input[type="reset"][disabled],
input[type="button"][disabled],
input[type="checkbox"][disabled],
input[type="radio"][disabled],
select[disabled] {
cursor: not-allowed;
}
input:disabled,
textarea:disabled,
select:disabled,
button[disabled] {
cursor: not-allowed;
background-color: var(--disabled);
color: var(--text-light)
}
input[type="range"] {
padding: 0;
}
/* Set the cursor to '?' on an abbreviation and style the abbreviation to show that there is more information underneath */
abbr[title] {
cursor: help;
text-decoration-line: underline;
text-decoration-style: dotted;
}
button:enabled:hover,
[role="button"]:not([aria-disabled="true"]):hover,
input[type="submit"]:enabled:hover,
input[type="reset"]:enabled:hover,
input[type="button"]:enabled:hover,
label[type="button"]:hover {
filter: brightness(1.4);
cursor: pointer;
}
button:focus-visible:where(:enabled, [role="button"]:not([aria-disabled="true"])),
input:enabled:focus-visible:where(
[type="submit"],
[type="reset"],
[type="button"]
) {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* Format navigation */
header > nav {
font-size: 1rem;
line-height: 2;
padding: 1rem 0 0 0;
}
/* Use flexbox to allow items to wrap, as needed */
header > nav ul,
header > nav ol {
align-content: space-around;
align-items: center;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: 0;
padding: 0;
}
/* List items are inline elements, make them behave more like blocks */
header > nav ul li,
header > nav ol li {
display: inline-block;
}
header > nav a,
header > nav a:visited {
margin: 0 0.5rem 1rem 0.5rem;
border: 1px solid var(--border);
border-radius: var(--standard-border-radius);
color: var(--text);
display: inline-block;
padding: 0.1rem 1rem;
text-decoration: none;
}
header > nav a:hover {
border-color: var(--accent);
color: var(--accent);
cursor: pointer;
}
/* Reduce nav side on mobile */
@media only screen and (max-width: 720px) {
header > nav a {
border: none;
padding: 0;
text-decoration: underline;
line-height: 1;
}
}
/* Consolidate box styling */
aside, details, pre, progress {
background-color: var(--accent-bg);
border: 1px solid var(--border);
border-radius: var(--standard-border-radius);
margin-bottom: 1rem;
}
aside {
font-size: 1rem;
width: 30%;
padding: 0 15px;
margin-left: 15px;
float: right;
}
/* Make aside full-width on mobile */
@media only screen and (max-width: 720px) {
aside {
width: 100%;
float: none;
margin-left: 0;
}
}
article, fieldset, dialog {
border: 1px solid var(--border);
padding: 1rem;
border-radius: var(--standard-border-radius);
margin-bottom: 1rem;
}
article h2:first-child,
section h2:first-child {
margin-top: 1rem;
}
section {
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
padding: 2rem 1rem;
margin: 3rem 0;
}
/* Don't double separators when chaining sections */
section + section,
section:first-child {
border-top: 0;
padding-top: 0;
}
section:last-child {
border-bottom: 0;
padding-bottom: 0;
}
details {
padding: 0.7rem 1rem;
}
summary {
cursor: pointer;
font-weight: bold;
padding: 0.7rem 1rem;
margin: -0.7rem -1rem;
word-break: break-all;
}
details[open] > summary + * {
margin-top: 0;
}
details[open] > summary {
margin-bottom: 0.5rem;
}
details[open] > :last-child {
margin-bottom: 0;
}
/* Format tables */
table {
border-collapse: collapse;
margin: 1.5rem 0;
}
td,
th {
border: 1px solid var(--border);
text-align: left;
padding: 0.5rem;
}
th {
background-color: var(--accent-bg);
font-weight: bold;
}
tr:nth-child(even) {
/* Set every other cell slightly darker. Improves readability. */
background-color: var(--accent-bg);
}
table caption {
font-weight: bold;
margin-bottom: 0.5rem;
}
/* Format forms */
textarea,
select,
input {
font-size: inherit;
font-family: inherit;
padding: 0.5rem;
margin-bottom: 0.5rem;
color: var(--text);
background-color: var(--bg);
border: 1px solid var(--border);
border-radius: var(--standard-border-radius);
box-shadow: none;
max-width: 100%;
display: inline-block;
}
label {
display: block;
}
textarea:not([cols]) {
width: 100%;
}
/* Add arrow to drop-down */
select:not([multiple]) {
background-image: linear-gradient(45deg, transparent 49%, var(--text) 51%),
linear-gradient(135deg, var(--text) 51%, transparent 49%);
background-position: calc(100% - 15px), calc(100% - 10px);
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 25px;
}
/* checkbox and radio button style */
input[type="checkbox"],
input[type="radio"] {
vertical-align: middle;
position: relative;
width: min-content;
}
input[type="checkbox"] + label,
input[type="radio"] + label {
display: inline-block;
}
input[type="radio"] {
border-radius: 100%;
}
input[type="checkbox"]:checked,
input[type="radio"]:checked {
background-color: var(--accent);
}
input[type="checkbox"]:checked::after {
/* Creates a rectangle with colored right and bottom borders which is rotated to look like a check mark */
content: " ";
width: 0.18em;
height: 0.32em;
border-radius: 0;
position: absolute;
top: 0.05em;
left: 0.17em;
background-color: transparent;
border-right: solid var(--bg) 0.08em;
border-bottom: solid var(--bg) 0.08em;
font-size: 1.8em;
transform: rotate(45deg);
}
input[type="radio"]:checked::after {
/* creates a colored circle for the checked radio button */
content: " ";
width: 0.25em;
height: 0.25em;
border-radius: 100%;
position: absolute;
top: 0.125em;
background-color: var(--bg);
left: 0.125em;
font-size: 32px;
}
/* Makes input fields wider on smaller screens */
@media only screen and (max-width: 720px) {
textarea,
select,
input {
width: 100%;
}
}
/* Set a height for color input */
input[type="color"] {
height: 2.5rem;
padding: 0.2rem;
}
/* do not show border around file selector button */
input[type="file"] {
border: 0;
}
/* Misc body elements */
hr {
border: none;
height: 1px;
background: var(--border);
margin: 1rem auto;
}
mark {
padding: 2px 5px;
border-radius: var(--standard-border-radius);
background-color: var(--marked);
color: black;
}
img,
video {
max-width: 100%;
height: auto;
border-radius: var(--standard-border-radius);
}
figure {
margin: 0;
display: block;
overflow-x: auto;
}
figcaption {
text-align: center;
font-size: 0.9rem;
color: var(--text-light);
margin-bottom: 1rem;
}
blockquote {
margin: 2rem 0 2rem 2rem;
padding: 0.4rem 0.8rem;
border-left: 0.35rem solid var(--accent);
color: var(--text-light);
font-style: italic;
}
cite {
font-size: 0.9rem;
color: var(--text-light);
font-style: normal;
}
dt {
color: var(--text-light);
}
/* Use mono font for code elements */
code,
pre,
pre span,
kbd,
samp {
font-family: var(--mono-font);
color: var(--code);
}
kbd {
color: var(--preformatted);
border: 1px solid var(--preformatted);
border-bottom: 3px solid var(--preformatted);
border-radius: var(--standard-border-radius);
padding: 0.1rem 0.4rem;
}
pre {
padding: 1rem 1.4rem;
max-width: 100%;
overflow: auto;
color: var(--preformatted);
}
/* Fix embedded code within pre */
pre code {
color: var(--preformatted);
background: none;
margin: 0;
padding: 0;
}
/* Progress bars */
/* Declarations are repeated because you */
/* cannot combine vendor-specific selectors */
progress {
width: 100%;
}
progress:indeterminate {
background-color: var(--accent-bg);
}
progress::-webkit-progress-bar {
border-radius: var(--standard-border-radius);
background-color: var(--accent-bg);
}
progress::-webkit-progress-value {
border-radius: var(--standard-border-radius);
background-color: var(--accent);
}
progress::-moz-progress-bar {
border-radius: var(--standard-border-radius);
background-color: var(--accent);
transition-property: width;
transition-duration: 0.3s;
}
progress:indeterminate::-moz-progress-bar {
background-color: var(--accent-bg);
}
dialog {
max-width: 40rem;
margin: auto;
}
dialog::backdrop {
background-color: var(--bg);
opacity: 0.8;
}
@media only screen and (max-width: 720px) {
dialog {
max-width: 100%;
margin: auto 1em;
}
}
/* Classes for buttons and notices */
.button,
.button:visited {
display: inline-block;
text-decoration: none;
border: none;
border-radius: 5px;
background: var(--accent);
font-size: 1rem;
color: var(--bg);
padding: 0.7rem 0.9rem;
margin: 0.5rem 0;
}
.button:hover,
.button:focus {
filter: brightness(1.4);
cursor: pointer;
}
.notice {
background: var(--accent-bg);
border: 2px solid var(--border);
border-radius: 5px;
padding: 1.5rem;
margin: 2rem 0;
}

View File

@@ -1,8 +0,0 @@
{
"require": {
"symfony/polyfill-iconv": "^1.27",
"symfony/yaml": "^6.2",
"symfony/intl": "^6.2",
"behat/transliterator": "^1.5"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -1,335 +0,0 @@
<?php
// Edit thesis page
session_start();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../shared/Database.php';
$thesisId = isset($_GET['id']) ? intval($_GET['id']) : 0;
$error = null;
$success = null;
if ($thesisId <= 0) {
die("ID invalide");
}
try {
$db = new Database();
$pdo = $db->getPDO();
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['csrf_token'])) {
// Verify CSRF token
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
throw new Exception("Erreur de sécurité : token invalide.");
}
try {
$db->beginTransaction();
// Update thesis basic info
$stmt = $pdo->prepare("
UPDATE theses SET
title = ?,
subtitle = ?,
year = ?,
orientation_id = ?,
ap_program_id = ?,
finality_id = ?,
synopsis = ?,
file_size_info = ?,
baiu_link = ?,
is_published = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
$stmt->execute([
trim($_POST['titre']),
!empty($_POST['subtitle']) ? trim($_POST['subtitle']) : null,
intval($_POST['année']),
intval($_POST['orientation']),
intval($_POST['ap']),
intval($_POST['finality']),
trim($_POST['synopsis']),
!empty($_POST['duration_info']) ? trim($_POST['duration_info']) : null,
!empty($_POST['lien']) ? trim($_POST['lien']) : null,
isset($_POST['is_published']) ? 1 : 0,
$thesisId
]);
// Update authors
$pdo->prepare("DELETE FROM thesis_authors WHERE thesis_id = ?")->execute([$thesisId]);
$authorsRaw = trim($_POST['auteurice'] ?? '');
if (!empty($authorsRaw)) {
$authors = array_map('trim', explode(',', $authorsRaw));
foreach ($authors as $index => $authorName) {
if (!empty($authorName)) {
$authorId = $db->findOrCreateAuthor($authorName, $index === 0 ? ($_POST['mail'] ?? null) : null);
$stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $authorId, $index + 1]);
}
}
}
// Update supervisors
$pdo->prepare("DELETE FROM thesis_supervisors WHERE thesis_id = ?")->execute([$thesisId]);
$supervisorsRaw = trim($_POST['promoteurice'] ?? '');
if (!empty($supervisorsRaw)) {
$supervisors = array_map('trim', explode(',', $supervisorsRaw));
foreach ($supervisors as $index => $supervisorName) {
if (!empty($supervisorName)) {
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
}
}
}
// Update languages
$pdo->prepare("DELETE FROM thesis_languages WHERE thesis_id = ?")->execute([$thesisId]);
if (isset($_POST['languages']) && is_array($_POST['languages'])) {
foreach ($_POST['languages'] as $languageId) {
$stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)");
$stmt->execute([$thesisId, intval($languageId)]);
}
}
// Update formats
$pdo->prepare("DELETE FROM thesis_formats WHERE thesis_id = ?")->execute([$thesisId]);
if (isset($_POST['formats']) && is_array($_POST['formats'])) {
foreach ($_POST['formats'] as $formatId) {
$stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)");
$stmt->execute([$thesisId, intval($formatId)]);
}
}
// Update keywords
$pdo->prepare("DELETE FROM thesis_keywords WHERE thesis_id = ?")->execute([$thesisId]);
$keywordsRaw = trim($_POST['tag'] ?? '');
if (!empty($keywordsRaw)) {
$keywords = array_map('trim', explode(',', $keywordsRaw));
$keywords = array_slice($keywords, 0, 10); // Max 10
foreach ($keywords as $keyword) {
if (!empty($keyword)) {
$keywordId = $db->findOrCreateKeyword($keyword);
if ($keywordId) {
$stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $keywordId]);
}
}
}
}
$db->commit();
$success = "TFE mis à jour avec succès!";
// Regenerate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} catch (Exception $e) {
$db->rollback();
$error = $e->getMessage();
error_log("Edit error: " . $e->getMessage());
}
}
// Load thesis data
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
die("TFE non trouvé");
}
// Load current relationships
$stmt = $pdo->prepare("SELECT language_id FROM thesis_languages WHERE thesis_id = ?");
$stmt->execute([$thesisId]);
$currentLanguages = $stmt->fetchAll(PDO::FETCH_COLUMN);
$stmt = $pdo->prepare("SELECT format_id FROM thesis_formats WHERE thesis_id = ?");
$stmt->execute([$thesisId]);
$currentFormats = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Load reference data
$orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
} catch (Exception $e) {
error_log("Error loading edit page: " . $e->getMessage());
die("Erreur lors du chargement: " . $e->getMessage());
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Éditer TFE - <?php echo htmlspecialchars($thesis['title']); ?></title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" />
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
</head>
<body>
<header>
<h1>Éditer TFE</h1>
<nav>
<a href="list.php">← Liste</a> |
<a href="thanks.php?id=<?php echo $thesisId; ?>">Voir</a>
</nav>
</header>
<main>
<?php if ($error): ?>
<div style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div style="background: #efe; border: 2px solid #0a0; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #0a0;">
<strong>✓ <?php echo htmlspecialchars($success); ?></strong>
</div>
<?php endif; ?>
<form method="post" action="edit.php?id=<?php echo $thesisId; ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<h2>Informations de base</h2>
<fieldset>
<label for="auteurice">Nom/Prénom/Pseudo *</label>
<input type="text" id="auteurice" name="auteurice" value="<?php echo htmlspecialchars($thesis['authors']); ?>" required>
<small>Si plusieurs, séparer par des virgules</small>
</fieldset>
<fieldset>
<label for="mail">Contact</label>
<input type="text" id="mail" name="mail" value="">
</fieldset>
<fieldset>
<label for="année">Année *</label>
<input type="number" id="année" name="année" value="<?php echo $thesis['year']; ?>" required>
</fieldset>
<h2>Informations académiques</h2>
<fieldset>
<label for="orientation">Orientation *</label>
<select id="orientation" name="orientation" required>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo $orientation['id']; ?>" <?php echo ($thesis['orientation'] == $orientation['name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="ap">Atelier Pratique *</label>
<select id="ap" name="ap" required>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?php echo $ap['id']; ?>" <?php echo ($thesis['ap_program'] == $ap['name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($ap['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="finality">Finalité *</label>
<select id="finality" name="finality" required>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?php echo $finality['id']; ?>" <?php echo ($thesis['finality_type'] == $finality['name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($finality['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="promoteurice">Promoteur·ice(s)</label>
<input type="text" id="promoteurice" name="promoteurice" value="<?php echo htmlspecialchars($thesis['supervisors'] ?? ''); ?>">
<small>Si plusieurs, séparer par des virgules</small>
</fieldset>
<h2>À propos du TFE</h2>
<fieldset>
<label for="titre">Titre *</label>
<input type="text" id="titre" name="titre" value="<?php echo htmlspecialchars($thesis['title']); ?>" required>
</fieldset>
<fieldset>
<label for="subtitle">Sous-titre</label>
<input type="text" id="subtitle" name="subtitle" value="<?php echo htmlspecialchars($thesis['subtitle'] ?? ''); ?>">
</fieldset>
<fieldset>
<label for="synopsis">Synopsis *</label>
<textarea id="synopsis" name="synopsis" rows="8" required><?php echo htmlspecialchars($thesis['synopsis'] ?? ''); ?></textarea>
</fieldset>
<fieldset>
<label>Langue(s) *</label>
<?php foreach ($languages as $language): ?>
<label class="checkbox-label">
<input type="checkbox" name="languages[]" value="<?php echo $language['id']; ?>" <?php echo in_array($language['id'], $currentLanguages) ? 'checked' : ''; ?>>
<?php echo htmlspecialchars($language['name']); ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset>
<label>Format(s)</label>
<?php foreach ($formatTypes as $format): ?>
<label class="checkbox-label">
<input type="checkbox" name="formats[]" value="<?php echo $format['id']; ?>" <?php echo in_array($format['id'], $currentFormats) ? 'checked' : ''; ?>>
<?php echo htmlspecialchars($format['name']); ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset>
<label for="tag">Mots-clés (max 10)</label>
<input type="text" id="tag" name="tag" value="<?php echo htmlspecialchars($thesis['keywords'] ?? ''); ?>">
<small>Séparer par des virgules</small>
</fieldset>
<fieldset>
<label for="duration_info">Durée/Taille</label>
<input type="text" id="duration_info" name="duration_info" value="<?php echo htmlspecialchars($thesis['file_size_info'] ?? ''); ?>">
</fieldset>
<fieldset>
<label for="lien">Lien externe</label>
<input type="url" id="lien" name="lien" value="<?php echo htmlspecialchars($thesis['baiu_link'] ?? ''); ?>">
</fieldset>
<h2>Publication</h2>
<fieldset>
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" name="is_published" value="1" <?php echo $thesis['is_published'] ? 'checked' : ''; ?>>
<span>Publier ce TFE sur le site public</span>
</label>
<small>Si coché, ce TFE sera visible sur le site public. Sinon, il restera en attente.</small>
</fieldset>
<button type="submit">Enregistrer les modifications</button>
<a href="thanks.php?id=<?php echo $thesisId; ?>">Annuler</a>
</form>
</main>
<footer>
<p>Édition TFE #<?php echo $thesisId; ?></p>
</footer>
</body>
</html>

View File

@@ -1,328 +0,0 @@
<?php // formulaire.php
// Configure error reporting
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
// Start session for CSRF protection
session_start();
// Verify CSRF token
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
error_log("CSRF token validation failed");
die("Erreur de sécurité : token invalide. Veuillez recharger le formulaire.");
}
// Log the content of the $_FILES array
error_log("FILES array: " . print_r($_FILES, true));
require_once __DIR__ . '/../../shared/Database.php';
// Helper function to sanitize string input
function sanitize_string($input) {
return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
}
// Helper function to validate required field
function validate_required($value, $fieldName) {
if (empty($value)) {
throw new Exception("Le champ '$fieldName' est requis.");
}
return $value;
}
try {
// Initialize database connection
$db = new Database();
$pdo = $db->getPDO();
// Begin transaction - all or nothing
$db->beginTransaction();
// ===== VALIDATE AND SANITIZE INPUT DATA =====
// Author information
$auteurName = validate_required(sanitize_string($_POST["auteurice"] ?? ''), "Nom/Prénom/Pseudo");
$mail = $_POST["mail"] ?? '';
if (!empty($mail)) {
// Could be email or social media handle
$mail = sanitize_string($mail);
}
// Year validation
$annee = filter_var($_POST["année"] ?? '', FILTER_VALIDATE_INT);
if ($annee === false || $annee < 2000 || $annee > (int)date('Y') + 1) {
throw new Exception("Année invalide. Veuillez entrer une année valide.");
}
// Academic details
$orientationId = filter_var($_POST["orientation"] ?? '', FILTER_VALIDATE_INT);
if ($orientationId === false) {
throw new Exception("Veuillez sélectionner une orientation.");
}
$apProgramId = filter_var($_POST["ap"] ?? '', FILTER_VALIDATE_INT);
if ($apProgramId === false) {
throw new Exception("Veuillez sélectionner un Atelier Pratique.");
}
$finalityId = filter_var($_POST["finality"] ?? '', FILTER_VALIDATE_INT);
if ($finalityId === false) {
throw new Exception("Veuillez sélectionner une finalité.");
}
// Thesis content
$titre = validate_required(sanitize_string($_POST["titre"] ?? ''), "Titre du mémoire");
$subtitle = sanitize_string($_POST["subtitle"] ?? '');
$synopsis = validate_required(sanitize_string($_POST["synopsis"] ?? ''), "Synopsis");
$problematique = sanitize_string($_POST["problématique"] ?? '');
$durationInfo = sanitize_string($_POST["duration_info"] ?? '');
// Supervisor(s)
$promoteuriceRaw = sanitize_string($_POST["promoteurice"] ?? '');
$supervisorNames = !empty($promoteuriceRaw) ? array_map('trim', explode(',', $promoteuriceRaw)) : [];
// Keywords (max 10)
$tagRaw = sanitize_string($_POST["tag"] ?? '');
$keywords = !empty($tagRaw) ? array_map('trim', explode(',', $tagRaw)) : [];
if (count($keywords) > 10) {
throw new Exception("Maximum 10 mots-clés autorisés.");
}
// Languages (at least one required)
$languageIds = $_POST["languages"] ?? [];
if (empty($languageIds)) {
throw new Exception("Veuillez sélectionner au moins une langue.");
}
$languageIds = array_map('intval', $languageIds);
// Formats (optional, multiple selection)
$formatIds = isset($_POST["formats"]) ? array_map('intval', $_POST["formats"]) : [];
// External link
$lien = $_POST["lien"] ?? '';
if (!empty($lien)) {
$lien = filter_var($lien, FILTER_VALIDATE_URL);
if ($lien === false) {
throw new Exception("Lien URL invalide.");
}
}
// File uploads
$couverture = $_FILES["couverture"] ?? null;
$files = $_FILES["files"] ?? null;
// ===== CREATE OR FIND AUTHOR =====
$authorId = $db->findOrCreateAuthor($auteurName, $mail);
error_log("Author ID: $authorId");
// ===== INSERT THESIS RECORD =====
// Generate unique identifier (YYYY-NNN format)
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM theses WHERE year = ?");
$stmt->execute([$annee]);
$count = $stmt->fetch()['count'] + 1;
$identifier = sprintf("%d-%03d", $annee, $count);
$stmt = $pdo->prepare("
INSERT INTO theses (
identifier, title, subtitle, year,
orientation_id, ap_program_id, finality_id,
synopsis, file_size_info,
baiu_link,
submitted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
");
$stmt->execute([
$identifier,
$titre,
!empty($subtitle) ? $subtitle : null,
$annee,
$orientationId,
$apProgramId,
$finalityId,
$synopsis,
!empty($durationInfo) ? $durationInfo : null,
!empty($lien) ? $lien : null
]);
$thesisId = $pdo->lastInsertId();
error_log("Thesis ID: $thesisId");
// ===== LINK AUTHOR TO THESIS =====
$stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, 1)");
$stmt->execute([$thesisId, $authorId]);
// ===== LINK SUPERVISORS TO THESIS =====
foreach ($supervisorNames as $index => $supervisorName) {
if (!empty($supervisorName)) {
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
}
}
// ===== LINK LANGUAGES TO THESIS =====
foreach ($languageIds as $languageId) {
$stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $languageId]);
}
// ===== LINK FORMATS TO THESIS =====
foreach ($formatIds as $formatId) {
$stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $formatId]);
}
// ===== LINK KEYWORDS TO THESIS =====
foreach ($keywords as $keyword) {
if (!empty($keyword)) {
$keywordId = $db->findOrCreateKeyword($keyword);
if ($keywordId) {
$stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $keywordId]);
}
}
}
// ===== HANDLE FILE UPLOADS =====
// Create necessary directories
$uploadBaseDir = __DIR__ . "/data/theses/{$annee}/{$identifier}/";
$coverDir = __DIR__ . "/data/covers/";
if (!file_exists($uploadBaseDir)) {
mkdir($uploadBaseDir, 0755, true);
}
if (!file_exists($coverDir)) {
mkdir($coverDir, 0755, true);
}
// Define security constraints
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip'];
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip'];
$maxFileSize = 50 * 1024 * 1024; // 50 MB
// Process cover image
$coverPath = null;
if ($couverture && isset($couverture["error"]) && $couverture["error"] === UPLOAD_ERR_OK) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($couverture["tmp_name"]);
$fileExtension = strtolower(pathinfo($couverture["name"], PATHINFO_EXTENSION));
// Only allow image files for cover
if (in_array($mimeType, ['image/jpeg', 'image/png']) &&
in_array($fileExtension, ['jpg', 'jpeg', 'png'])) {
// Generate random filename
$randomName = bin2hex(random_bytes(16));
$safeFileName = $randomName . "." . $fileExtension;
$targetFile = $coverDir . $safeFileName;
if (move_uploaded_file($couverture["tmp_name"], $targetFile)) {
chmod($targetFile, 0644);
$coverPath = "data/covers/" . $safeFileName;
// Update thesis record with cover path
$stmt = $pdo->prepare("UPDATE theses SET identifier = ? WHERE id = ?");
// Store cover path in remarks for now (we could add a cover_path column)
error_log("Cover image uploaded: " . $safeFileName);
}
} else {
error_log("Invalid cover image type: " . $mimeType);
}
}
// Process thesis files
if ($files && is_array($files["name"])) {
for ($i = 0; $i < count($files["name"]); $i++) {
// Skip if no file was uploaded for this slot
if ($files["error"][$i] === UPLOAD_ERR_NO_FILE) {
continue;
}
if ($files["error"][$i] !== UPLOAD_ERR_OK) {
error_log("File upload error code " . $files["error"][$i] . ": " . $files["name"][$i]);
continue;
}
// Validate file
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($files["tmp_name"][$i]);
$fileExtension = strtolower(pathinfo($files["name"][$i], PATHINFO_EXTENSION));
if (!in_array($mimeType, $allowedMimeTypes) || !in_array($fileExtension, $allowedExtensions)) {
error_log("Invalid file type: " . $files["name"][$i] . " (MIME: $mimeType)");
continue;
}
if ($files["size"][$i] > $maxFileSize) {
error_log("File too large: " . $files["name"][$i]);
continue;
}
// Generate random filename
$randomName = bin2hex(random_bytes(16));
$safeFileName = $randomName . "." . $fileExtension;
$targetFile = $uploadBaseDir . $safeFileName;
if (move_uploaded_file($files["tmp_name"][$i], $targetFile)) {
chmod($targetFile, 0644);
// Determine file type (simplified - could be enhanced)
$fileType = 'other';
if (strpos(strtolower($files["name"][$i]), 'annex') !== false) {
$fileType = 'annex';
} else if ($fileExtension === 'pdf') {
$fileType = 'main';
}
// Insert file record
$db->insertThesisFile(
$thesisId,
$fileType,
"data/theses/{$annee}/{$identifier}/" . $safeFileName,
basename($files["name"][$i]),
$files["size"][$i],
$mimeType
);
error_log("File uploaded: " . $safeFileName);
} else {
error_log("Failed to move file: " . $files["name"][$i]);
}
}
}
// ===== COMMIT TRANSACTION =====
$db->commit();
error_log("Thesis submission completed successfully: $identifier");
// Clear CSRF token
unset($_SESSION['csrf_token']);
// Redirect to thank you page
header('Location: thanks.php?id=' . urlencode($thesisId));
exit();
} catch (Exception $e) {
// Rollback transaction on error
if (isset($db)) {
$db->rollback();
}
error_log("Form processing error: " . $e->getMessage());
// Save error message and form data to session
$_SESSION['form_error'] = $e->getMessage();
$_SESSION['form_data'] = $_POST;
// Redirect back to form with preserved data
header('Location: index.php');
exit();
}

View File

@@ -1,366 +0,0 @@
<?php
// CSV Import page for Post-ERG thesis database
// This page allows importing thesis data from CSV files
session_start();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../shared/Database.php';
$message = '';
$errors = [];
$importedCount = 0;
$skippedCount = 0;
$importResults = [];
// Handle CSV upload and import
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Verify CSRF token
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$errors[] = "Erreur de sécurité : token invalide.";
} else {
try {
$db = new Database();
$pdo = $db->getPDO();
// Check file upload
if ($_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
throw new Exception("Erreur lors du téléversement du fichier.");
}
// Read CSV file
$csvFile = $_FILES['csv_file']['tmp_name'];
$handle = fopen($csvFile, 'r');
if (!$handle) {
throw new Exception("Impossible d'ouvrir le fichier CSV.");
}
// Skip first two rows (empty and headers)
fgetcsv($handle); // Empty row
$headers = fgetcsv($handle); // Header row
fgetcsv($handle); // Description row
$headers = fgetcsv($handle); // Actual column names
// Map CSV columns
$columnMap = [
0 => 'identifier', // Identifiant
1 => 'title', // Titre
2 => 'subtitle', // Sous-titre
3 => 'authors', // Auteur·ice(s)
4 => 'contact', // Contact
5 => 'supervisors', // Promoteur·ice(s)
6 => 'formats', // Format
7 => 'year', // Année
8 => 'ap', // AP
9 => 'orientation', // Orientation
10 => 'finality', // Finalité
11 => 'keywords', // Mots-clés
12 => 'synopsis', // Synopsis
13 => 'context', // Contexte
14 => 'remarks', // Remarques
15 => 'language', // Langue
16 => 'access', // Autorisation
17 => 'license', // License
18 => 'size_info', // taille
19 => 'jury_points', // Points sur 20
20 => 'baiu_link', // lien BAIU
];
// Orientation abbreviation mapping
$orientationMap = [
'SC' => 'Sculpture',
'VI' => 'Vidéographie',
'CA' => 'Cinéma d\'animation',
'IP' => 'Installation-Performance',
'PE' => 'Peinture',
'PH' => 'Photographie',
'DE' => 'Dessin',
'AN' => 'Arts Numériques',
'GR' => 'Graphisme',
'TY' => 'Typographie',
'DN' => 'Design Numérique',
'IL' => 'Illustration',
'BD' => 'Bande-Dessinée',
'SE' => 'Sérigraphie',
'GV' => 'Gravure',
];
// Process each row
$lineNumber = 5; // Start after headers
while (($row = fgetcsv($handle)) !== false) {
$lineNumber++;
// Skip empty rows
if (empty($row[0]) && empty($row[1])) {
continue;
}
try {
$db->beginTransaction();
// Extract data
$identifier = trim($row[0] ?? '');
$title = trim($row[1] ?? '');
$subtitle = trim($row[2] ?? '');
$authorsRaw = trim($row[3] ?? '');
$contact = trim($row[4] ?? '');
$supervisorsRaw = trim($row[5] ?? '');
$formatsRaw = trim($row[6] ?? '');
$year = intval($row[7] ?? 0);
$apCode = trim($row[8] ?? '');
$orientationCode = trim($row[9] ?? '');
$finalityName = trim($row[10] ?? '');
$keywordsRaw = trim($row[11] ?? '');
$synopsis = trim($row[12] ?? '');
$context = trim($row[13] ?? '');
$remarks = trim($row[14] ?? '');
$languageRaw = trim($row[15] ?? '');
$access = trim($row[16] ?? '');
$license = trim($row[17] ?? '');
$sizeInfo = trim($row[18] ?? '');
$juryPoints = !empty($row[19]) ? floatval($row[19]) : null;
$baiuLink = trim($row[20] ?? '');
// Validate required fields
if (empty($title) || empty($year)) {
throw new Exception("Ligne $lineNumber: Titre et année requis.");
}
// Map orientation
$orientationName = isset($orientationMap[$orientationCode]) ? $orientationMap[$orientationCode] : null;
$orientationId = null;
if ($orientationName) {
$orientationId = $db->getOrientationId($orientationName);
}
// Map AP program
$apProgramId = null;
if (!empty($apCode)) {
$stmt = $pdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$stmt->execute([$apCode]);
$result = $stmt->fetch();
if ($result) {
$apProgramId = $result['id'];
}
}
// Map finality
$finalityId = null;
if (!empty($finalityName)) {
$finalityId = $db->getFinalityId($finalityName);
}
// Insert thesis
$stmt = $pdo->prepare("
INSERT INTO theses (
identifier, title, subtitle, year,
orientation_id, ap_program_id, finality_id,
synopsis, context_note, remarks,
file_size_info, jury_points, baiu_link,
submitted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
");
$stmt->execute([
!empty($identifier) ? $identifier : null,
$title,
!empty($subtitle) ? $subtitle : null,
$year,
$orientationId,
$apProgramId,
$finalityId,
!empty($synopsis) ? $synopsis : null,
!empty($context) ? $context : null,
!empty($remarks) ? $remarks : null,
!empty($sizeInfo) ? $sizeInfo : null,
$juryPoints,
!empty($baiuLink) ? $baiuLink : null
]);
$thesisId = $pdo->lastInsertId();
// Add authors
if (!empty($authorsRaw)) {
$authors = array_map('trim', explode(',', $authorsRaw));
foreach ($authors as $index => $authorName) {
if (!empty($authorName)) {
$authorId = $db->findOrCreateAuthor($authorName, $index === 0 ? $contact : null);
$stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $authorId, $index + 1]);
}
}
}
// Add supervisors
if (!empty($supervisorsRaw)) {
$supervisors = array_map('trim', explode(',', $supervisorsRaw));
foreach ($supervisors as $index => $supervisorName) {
if (!empty($supervisorName)) {
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
}
}
}
// Add keywords
if (!empty($keywordsRaw)) {
$keywords = array_map('trim', explode(',', $keywordsRaw));
$keywords = array_slice($keywords, 0, 10); // Max 10
foreach ($keywords as $keyword) {
if (!empty($keyword)) {
$keywordId = $db->findOrCreateKeyword($keyword);
if ($keywordId) {
$stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $keywordId]);
}
}
}
}
// Add language
if (!empty($languageRaw)) {
$languageId = $db->getLanguageId(ucfirst(strtolower($languageRaw)));
if ($languageId) {
$stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $languageId]);
}
}
// Add formats
if (!empty($formatsRaw)) {
$formats = array_map('trim', explode(',', $formatsRaw));
foreach ($formats as $formatName) {
if (!empty($formatName)) {
$formatId = $db->getFormatId(ucfirst(strtolower($formatName)));
if ($formatId) {
$stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $formatId]);
}
}
}
}
$db->commit();
$importedCount++;
$importResults[] = "✓ Ligne $lineNumber: \"$title\" importé (ID: $thesisId)";
} catch (Exception $e) {
$db->rollback();
$skippedCount++;
$importResults[] = " Ligne $lineNumber: " . $e->getMessage();
error_log("Import error on line $lineNumber: " . $e->getMessage());
}
}
fclose($handle);
$message = "Import terminé : $importedCount TFE importés, $skippedCount ignorés.";
} catch (Exception $e) {
$errors[] = $e->getMessage();
error_log("CSV import error: " . $e->getMessage());
}
}
// Regenerate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Import CSV - Post-ERG</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" />
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
</head>
<body>
<header>
<h1>Import CSV - Post-ERG</h1>
<nav>
<a href="index.php">← Nouveau TFE</a> |
<a href="list.php">📋 Liste des TFE</a>
</nav>
</header>
<main>
<h2>Importer des TFE depuis un fichier CSV</h2>
<?php if (!empty($errors)): ?>
<div style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreurs:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($message): ?>
<div style="background: #efe; border: 2px solid #0a0; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #0a0;">
<strong>✓ <?php echo htmlspecialchars($message); ?></strong>
</div>
<?php endif; ?>
<form action="import.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<fieldset>
<legend>Sélectionner un fichier CSV</legend>
<p><strong>Format attendu:</strong></p>
<ul>
<li>Colonnes: Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU</li>
<li>Les deux premières lignes seront ignorées (entête)</li>
<li>Séparateur: virgule</li>
<li>Encodage: UTF-8</li>
</ul>
<label for="csv_file">Fichier CSV:</label>
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
<button type="submit">Importer</button>
</fieldset>
</form>
<?php if (!empty($importResults)): ?>
<h3>Résultats de l'import</h3>
<div style="background: #f5f5f5; padding: 1rem; border-radius: 4px; max-height: 400px; overflow-y: auto;">
<pre style="margin: 0; font-size: 0.9em;"><?php
foreach ($importResults as $result) {
echo htmlspecialchars($result) . "\n";
}
?></pre>
</div>
<?php endif; ?>
<hr>
<h3>Notes importantes</h3>
<ul>
<li><strong>Codes orientation:</strong> SC (Sculpture), VI (Vidéographie), CA (Cinéma d'animation), IP (Installation-Performance), etc.</li>
<li><strong>Codes AP:</strong> DPM, LIENS, APS (comme dans la base)</li>
<li><strong>Auteurs multiples:</strong> Séparer par des virgules</li>
<li><strong>Mots-clés:</strong> Maximum 10, séparés par des virgules</li>
<li><strong>Formats:</strong> Séparer par des virgules</li>
<li>Les lignes avec erreurs seront ignorées et loggées</li>
</ul>
<h3>Exemple de fichier CSV</h3>
<p>Voir: <code>../db/Database_TFE_test.csv</code></p>
</main>
<footer>
<p>Import CSV - Post-ERG Database</p>
</footer>
</body>
</html>

View File

@@ -1,301 +0,0 @@
<?php
// Start session and generate CSRF token
session_start();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
// Load database helper
require_once __DIR__ . '/../../shared/Database.php';
try {
$db = new Database();
$orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
} catch (Exception $e) {
error_log("Failed to load form data: " . $e->getMessage());
die(
"Erreur lors du chargement du formulaire. Veuillez réessayer plus tard."
);
}
// Get error message and preserved form data from session (if redirected back from error)
$error = isset($_SESSION["form_error"]) ? $_SESSION["form_error"] : null;
$formData = isset($_SESSION["form_data"]) ? $_SESSION["form_data"] : [];
// Clear session data after retrieving
unset($_SESSION["form_error"]);
unset($_SESSION["form_data"]);
// Helper function to get old form value
function old($key, $default = "")
{
global $formData;
return isset($formData[$key])
? htmlspecialchars($formData[$key])
: $default;
}
// Helper function to check if value was previously selected
function wasSelected($key, $value)
{
global $formData;
if (!isset($formData[$key])) {
return false;
}
if (is_array($formData[$key])) {
return in_array($value, $formData[$key]);
}
return $formData[$key] == $value;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formulaire</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" />
<!-- <link rel="stylesheet" href="assets/simple.css"> -->
<!--<link rel="stylesheet" href="assets/posterg.css"> -->
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
</head>
<body>
<header>
<h1>Formulaire Posterg</h1>
<nav style="margin-top: 1rem;">
<a href="list.php" style="font-size: 0.9em;">📋 Liste des TFE</a> |
<a href="import.php" style="font-size: 0.9em;">📥 Importer CSV</a>
</nav>
</header>
<main>
<?php if ($error): ?>
<div class="error-message" style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form action="formulaire.php" method="post" enctype="multipart/form-data">
<!-- CSRF Protection -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(
$_SESSION["csrf_token"],
); ?>">
<h2>Informations de base</h2>
<fieldset>
<label for="auteurice">Nom/Prénom/Pseudo *</label>
<input type="text" id="auteurice" name="auteurice" placeholder="Nom de l'auteur·ice" value="<?php echo old(
"auteurice",
); ?>" required>
</fieldset>
<fieldset>
<label for="mail">Contact (email, site web, insta, ...)</label>
<input type="text" id="mail" name="mail" placeholder="votre.email@example.com ou @instagram" value="<?php echo old(
"mail",
); ?>">
</fieldset>
<fieldset>
<label for="année">Année diplômante *</label>
<input type="number" id="année" name="année" min="2000" max="<?php echo date(
"Y",
) + 1; ?>" placeholder="<?php echo date(
"Y",
); ?>" value="<?php echo old("année"); ?>" required>
</fieldset>
<h2>Informations académiques</h2>
<fieldset>
<label for="orientation">Orientation principale *</label>
<select id="orientation" name="orientation" required>
<option value="">-- Sélectionner une orientation --</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo htmlspecialchars(
$orientation["id"],
); ?>" <?php echo wasSelected(
"orientation",
$orientation["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars(
$orientation["name"],
); ?>
</option>
<?php endforeach; ?>
</select>
<fieldset>
<label for="ap">Atelier Pratique (AP) *</label>
<select id="ap" name="ap" required>
<option value="">-- Sélectionner un AP --</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?php echo htmlspecialchars(
$ap["id"],
); ?>" <?php echo wasSelected("ap", $ap["id"])
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($ap["name"]); ?>
<?php if (
$ap["code"]
): ?> (<?php echo htmlspecialchars(
$ap["code"],
); ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="finality">Finalité du master *</label>
<select id="finality" name="finality" required>
<option value="">-- Sélectionner une finalité --</option>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?php echo htmlspecialchars(
$finality["id"],
); ?>" <?php echo wasSelected(
"finality",
$finality["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($finality["name"]); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="promoteurice">Promoteur·ice(s)</label>
<input type="text" id="promoteurice" name="promoteurice" placeholder="Nom du/de la promoteur·ice (si plusieurs, séparer par des virgules)" value="<?php echo old(
"promoteurice",
); ?>">
</fieldset>
<h2>À propos du TFE</h2>
<fieldset>
<label for="titre">Titre du mémoire *</label>
<input type="text" id="titre" name="titre" placeholder="Titre de votre TFE" value="<?php echo old(
"titre",
); ?>" required>
</fieldset>
<fieldset>
<label for="subtitle">Sous-titre (si applicable)</label>
<input type="text" id="subtitle" name="subtitle" placeholder="Sous-titre de votre TFE" value="<?php echo old(
"subtitle",
); ?>">
</fieldset>
<fieldset>
<label for="synopsis">Synopsis (environ 200 mots) *</label>
<textarea id="synopsis" name="synopsis" rows="8" placeholder="Décrivez votre TFE en quelques paragraphes..." required><?php echo old(
"synopsis",
); ?></textarea>
</fieldset>
<fieldset>
<label for="problématique">Problématique</label>
<textarea id="problématique" name="problématique" rows="4" placeholder="La problématique principale de votre mémoire..."><?php echo old(
"problématique",
); ?></textarea>
</fieldset>
<fieldset>
<label>Langue(s) du TFE * (sélection multiple possible)</label>
<?php foreach ($languages as $language): ?>
<label class="checkbox-label">
<input type="checkbox" name="languages[]" value="<?php echo htmlspecialchars(
$language["id"],
); ?>" <?php echo wasSelected(
"languages",
$language["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($language["name"]); ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset>
<label>Format(s) (sélection multiple possible)</label>
<?php foreach ($formatTypes as $format): ?>
<label class="checkbox-label">
<input type="checkbox" name="formats[]" value="<?php echo htmlspecialchars(
$format["id"],
); ?>" <?php echo wasSelected(
"formats",
$format["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($format["name"]); ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset>
<label for="tag">Mots-clés (max 10, séparés par des virgules)</label>
<input type="text" id="tag" name="tag" placeholder="typographie, photographie, outils libre, post-colonial..." value="<?php echo old(
"tag",
); ?>">
<small>Séparez les mots-clés par des virgules. Maximum 10 mots-clés.</small>
</fieldset>
<fieldset>
<label for="duration_info">Durée/Taille (si applicable)</label>
<input type="text" id="duration_info" name="duration_info" placeholder="Ex: 68 minutes, 128 pages, 78 pages + 15 minutes" value="<?php echo old(
"duration_info",
); ?>">
<small>Indiquez la durée (en minutes) ou le nombre de pages de votre TFE.</small>
</fieldset>
<fieldset>
<label for="lien">Lien vers un site web ou ressource en ligne</label>
<input type="url" id="lien" name="lien" placeholder="https://monmemoire.erg.be/..." value="<?php echo old(
"lien",
); ?>">
</fieldset>
<h2>Fichiers</h2>
<fieldset>
<label for="couverture">Importer une image de couverture</label>
<small>Formats acceptés : JPG, PNG. Taille max : 10MB.</small>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png">
</fieldset>
<fieldset>
<label for="files">Importer le TFE et les fichiers annexes</label>
<small>Formats acceptés : PDF, JPG, PNG, MP4, ZIP. Taille max par fichier : 50MB.</small>
<small>Si vous voulez importer un dossier, créez une archive ZIP.</small>
<input type="file" id="files" name="files[]" multiple accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip">
</fieldset>
<br>
<input type="submit" name="go" value="Soumettre mon TFE">
</form>
</main>
<footer>
<p>Formulaire fait avec ❤ en PHP et <a href="https://github.com/kevquirk/simple.css">SimpleCSS</a>.</p>
</footer>
</body>
</html>

View File

@@ -1,451 +0,0 @@
<?php
// List all theses in the database
session_start();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../shared/Database.php';
try {
$db = new Database();
$pdo = $db->getPDO();
// Get filter parameters
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
// Build query
$sql = "SELECT
t.id, t.identifier, t.title, t.subtitle, t.year,
o.name as orientation,
ap.name as ap_program,
GROUP_CONCAT(DISTINCT a.name) as authors,
t.submitted_at,
t.is_published
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
LEFT JOIN authors a ON ta.author_id = a.id
WHERE 1=1";
$params = [];
if ($searchQuery) {
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
$searchParam = "%$searchQuery%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($yearFilter) {
$sql .= " AND t.year = ?";
$params[] = $yearFilter;
}
if ($orientationFilter) {
$sql .= " AND t.orientation_id = ?";
$params[] = $orientationFilter;
}
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$theses = $stmt->fetchAll();
// Get unique years for filter
$yearsStmt = $pdo->query("SELECT DISTINCT year FROM theses ORDER BY year DESC");
$years = $yearsStmt->fetchAll(PDO::FETCH_COLUMN);
// Get orientations for filter
$orientations = $db->getAllOrientations();
} catch (Exception $e) {
error_log("Error loading theses list: " . $e->getMessage());
die("Erreur lors du chargement de la liste.");
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liste des TFE - Post-ERG</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" />
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
<style>
.filters {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 4px;
}
.filters form {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: end;
}
.filters fieldset {
margin: 0;
padding: 0;
border: none;
min-width: 200px;
}
.thesis-table {
width: 100%;
border-collapse: collapse;
}
.thesis-table th,
.thesis-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.thesis-table th {
background: #f0f0f0;
font-weight: bold;
}
.thesis-table tr:hover {
background: #f9f9f9;
}
.thesis-title {
font-weight: bold;
}
.thesis-subtitle {
font-style: italic;
color: #666;
font-size: 0.9em;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 0.85em;
}
.status-pending {
background: #ffd700;
color: #000;
}
.status-published {
background: #90ee90;
color: #000;
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.35rem 0.75rem;
border-radius: 3px;
text-decoration: none;
font-size: 0.9em;
display: inline-block;
}
.btn-view {
background: #4a90e2;
color: white;
}
.btn-edit {
background: #f39c12;
color: white;
}
.btn-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
}
.btn-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
}
.publish-form {
display: inline;
margin: 0;
}
.stats {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.stat-card {
background: #f5f5f5;
padding: 1rem;
border-radius: 4px;
min-width: 150px;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #4a90e2;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.bulk-actions {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
display: flex;
gap: 1rem;
align-items: center;
}
.bulk-actions-buttons {
display: flex;
gap: 0.5rem;
}
.btn-bulk-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.btn-bulk-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.select-checkbox {
cursor: pointer;
}
.select-all-checkbox {
cursor: pointer;
}
</style>
<script>
function toggleAll(source) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]');
checkboxes.forEach(checkbox => {
checkbox.checked = source.checked;
});
updateBulkActionsVisibility();
}
function updateBulkActionsVisibility() {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
if (checkboxes.length > 0) {
bulkActions.style.display = 'flex';
selectedCount.textContent = checkboxes.length;
} else {
bulkActions.style.display = 'none';
}
}
function bulkAction(action) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
if (checkboxes.length === 0) {
alert('Veuillez sélectionner au moins un TFE.');
return false;
}
const actionText = action === 'publish' ? 'publier' : 'dépublier';
if (!confirm(`Voulez-vous vraiment ${actionText} ${checkboxes.length} TFE(s) ?`)) {
return false;
}
// Set action
document.getElementById('bulk-action-input').value = action;
// Copy selected thesis IDs to hidden form
const bulkCheckboxesContainer = document.getElementById('bulk-checkboxes');
bulkCheckboxesContainer.innerHTML = '';
checkboxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selected_theses[]';
input.value = checkbox.value;
bulkCheckboxesContainer.appendChild(input);
});
// Submit the form
document.getElementById('bulk-form').submit();
return false;
}
document.addEventListener('DOMContentLoaded', function() {
// Add change listeners to all checkboxes
document.querySelectorAll('input[name="selected_theses[]"]').forEach(checkbox => {
checkbox.addEventListener('change', updateBulkActionsVisibility);
});
});
</script>
</head>
<body>
<header>
<h1>Liste des TFE</h1>
<nav>
<a href="index.php">← Nouveau TFE</a> |
<a href="import.php">📥 Importer CSV</a>
</nav>
</header>
<main>
<?php if (isset($_SESSION['error'])): ?>
<div style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div style="background: #efe; border: 2px solid #0a0; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #0a0;">
<strong>✓ <?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?></strong>
</div>
<?php endif; ?>
<div id="bulk-actions" class="bulk-actions" style="display: none;">
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
<div class="bulk-actions-buttons">
<button type="button" class="btn-bulk-publish" onclick="bulkAction('publish')">Publier la sélection</button>
<button type="button" class="btn-bulk-unpublish" onclick="bulkAction('unpublish')">Dépublier la sélection</button>
</div>
</div>
<form id="bulk-form" method="post" action="publish.php" style="display: none;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" id="bulk-action-input" name="action" value="">
<input type="hidden" name="bulk" value="1">
<div id="bulk-checkboxes"></div>
</form>
<div class="stats">
<div class="stat-card">
<div class="stat-number"><?php echo count($theses); ?></div>
<div class="stat-label">TFE total</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => $t['is_published'])); ?></div>
<div class="stat-label">Publiés</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => !$t['is_published'])); ?></div>
<div class="stat-label">En attente</div>
</div>
</div>
<div class="filters">
<form method="get" action="list.php">
<fieldset>
<label for="search">Rechercher</label>
<input type="text" id="search" name="search" placeholder="Titre, auteur..." value="<?php echo htmlspecialchars($searchQuery); ?>">
</fieldset>
<fieldset>
<label for="year">Année</label>
<select id="year" name="year">
<option value="">Toutes</option>
<?php foreach ($years as $year): ?>
<option value="<?php echo $year; ?>" <?php echo $yearFilter == $year ? 'selected' : ''; ?>>
<?php echo $year; ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="orientation">Orientation</label>
<select id="orientation" name="orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo $orientation['id']; ?>" <?php echo $orientationFilter == $orientation['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<button type="submit">Filtrer</button>
<?php if ($searchQuery || $yearFilter || $orientationFilter): ?>
<a href="list.php">Réinitialiser</a>
<?php endif; ?>
</form>
</div>
<?php if (empty($theses)): ?>
<p>Aucun TFE trouvé.</p>
<?php else: ?>
<table class="thesis-table">
<thead>
<tr>
<th><input type="checkbox" class="select-all-checkbox" onchange="toggleAll(this)" title="Tout sélectionner"></th>
<th>ID</th>
<th>Titre</th>
<th>Auteur(s)</th>
<th>Année</th>
<th>Orientation</th>
<th>AP</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($theses as $thesis): ?>
<tr>
<td><input type="checkbox" class="select-checkbox" name="selected_theses[]" value="<?php echo $thesis['id']; ?>"></td>
<td><?php echo htmlspecialchars($thesis['identifier'] ?? $thesis['id']); ?></td>
<td>
<div class="thesis-title"><?php echo htmlspecialchars($thesis['title']); ?></div>
<?php if ($thesis['subtitle']): ?>
<div class="thesis-subtitle"><?php echo htmlspecialchars($thesis['subtitle']); ?></div>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($thesis['authors'] ?? 'N/A'); ?></td>
<td><?php echo $thesis['year']; ?></td>
<td><?php echo htmlspecialchars($thesis['orientation'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($thesis['ap_program'] ?? 'N/A'); ?></td>
<td>
<?php if ($thesis['is_published']): ?>
<span class="status-badge status-published">Publié</span>
<?php else: ?>
<span class="status-badge status-pending">En attente</span>
<?php endif; ?>
</td>
<td>
<div class="actions">
<a href="thanks.php?id=<?php echo $thesis['id']; ?>" class="btn btn-view">Voir</a>
<a href="edit.php?id=<?php echo $thesis['id']; ?>" class="btn btn-edit">Éditer</a>
<form method="post" action="publish.php" class="publish-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" name="thesis_id" value="<?php echo $thesis['id']; ?>">
<?php if ($thesis['is_published']): ?>
<input type="hidden" name="action" value="unpublish">
<button type="submit" class="btn btn-unpublish" onclick="return confirm('Retirer ce TFE de la publication ?');">Dépublier</button>
<?php else: ?>
<input type="hidden" name="action" value="publish">
<button type="submit" class="btn btn-publish">Publier</button>
<?php endif; ?>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</main>
<footer>
<p>Post-ERG - <?php echo count($theses); ?> TFE dans la base de données</p>
</footer>
</body>
</html>

View File

@@ -1,95 +0,0 @@
<?php
/**
* Handle publish/unpublish actions for theses
*/
session_start();
require_once __DIR__ . '/../../shared/Database.php';
// Verify CSRF token
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$_SESSION['error'] = "Erreur de sécurité : token invalide.";
header('Location: list.php');
exit;
}
$action = isset($_POST['action']) ? $_POST['action'] : '';
$isBulk = isset($_POST['bulk']) && $_POST['bulk'] == '1';
if (!in_array($action, ['publish', 'unpublish'])) {
$_SESSION['error'] = "Action invalide.";
header('Location: list.php');
exit;
}
try {
$db = new Database();
$pdo = $db->getPDO();
$isPublished = ($action === 'publish') ? 1 : 0;
if ($isBulk) {
// Handle bulk action
$thesisIds = isset($_POST['selected_theses']) ? $_POST['selected_theses'] : [];
if (empty($thesisIds)) {
$_SESSION['error'] = "Aucun TFE sélectionné.";
header('Location: list.php');
exit;
}
// Validate all IDs are integers
$thesisIds = array_map('intval', $thesisIds);
$thesisIds = array_filter($thesisIds, fn($id) => $id > 0);
if (empty($thesisIds)) {
$_SESSION['error'] = "IDs invalides.";
header('Location: list.php');
exit;
}
// Prepare placeholders for IN clause
$placeholders = str_repeat('?,', count($thesisIds) - 1) . '?';
$sql = "UPDATE theses SET is_published = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$params = array_merge([$isPublished], $thesisIds);
$stmt->execute($params);
$count = count($thesisIds);
if ($action === 'publish') {
$_SESSION['success'] = "$count TFE(s) publié(s) avec succès!";
} else {
$_SESSION['success'] = "$count TFE(s) retiré(s) de la publication.";
}
} else {
// Handle single action
$thesisId = isset($_POST['thesis_id']) ? intval($_POST['thesis_id']) : 0;
if ($thesisId <= 0) {
$_SESSION['error'] = "ID invalide.";
header('Location: list.php');
exit;
}
$stmt = $pdo->prepare("UPDATE theses SET is_published = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt->execute([$isPublished, $thesisId]);
if ($action === 'publish') {
$_SESSION['success'] = "TFE publié avec succès!";
} else {
$_SESSION['success'] = "TFE retiré de la publication.";
}
}
} catch (Exception $e) {
error_log("Publish error: " . $e->getMessage());
$_SESSION['error'] = "Erreur lors de la modification: " . $e->getMessage();
}
// Regenerate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
header('Location: list.php');
exit;

View File

@@ -1,14 +0,0 @@
.
├── assets
├── composer.json
├── composer.lock
├── data
├── error.log
├── formulaire.php
├── index.php
├── README.md
├── struct.txt
├── thanks.php
└── vendor
4 directories, 8 files

Binary file not shown.

View File

@@ -1,295 +0,0 @@
<?php
// Configure error reporting
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
require __DIR__ . '/../../shared/Database.php';
// Security: Validate thesis ID parameter
$thesisId = null;
$thesis = null;
$files = [];
$error = null;
if (isset($_GET['id'])) {
$thesisId = filter_var($_GET['id'], FILTER_VALIDATE_INT);
if ($thesisId !== false && $thesisId > 0) {
try {
$db = new Database();
$pdo = $db->getPDO();
// Get thesis data
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
$error = "TFE non trouvé.";
} else {
// Get associated files
$stmt = $pdo->prepare("
SELECT file_type, file_name, file_size, mime_type, uploaded_at
FROM thesis_files
WHERE thesis_id = ?
ORDER BY file_type, uploaded_at
");
$stmt->execute([$thesisId]);
$files = $stmt->fetchAll();
}
} catch (Exception $e) {
error_log("Error loading thesis: " . $e->getMessage());
$error = "Erreur lors de la lecture des données.";
}
} else {
error_log("Invalid thesis ID: " . $_GET['id']);
$error = "Identifiant invalide.";
}
} else {
$error = "Aucun identifiant spécifié.";
}
// Helper function to format file size
function formatFileSize($bytes) {
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' bytes';
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Merci - Post-ERG</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="assets/simple.css">
<link rel="stylesheet" href="assets/posterg.css">
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
</head>
<body>
<header>
<h1>Merci</h1>
<?php if ($thesis): ?>
<nav style="margin-top: 1rem;">
<a href="list.php">Liste des TFE</a> |
<a href="edit.php?id=<?php echo $thesisId; ?>">✏️ Modifier ce TFE</a>
</nav>
<?php endif; ?>
</header>
<main>
<?php if ($error): ?>
<div class="error">
<p>⚠️ <?php echo htmlspecialchars($error); ?></p>
<p><a href="index.php">Retour au formulaire</a></p>
</div>
<?php elseif ($thesis): ?>
<p>d'avoir soumis votre TFE. Les informations ont été enregistrées et sont en attente de traitement.</p>
<div class="thesis-info">
<h2>Récapitulatif de votre soumission</h2>
<h3>Informations de base</h3>
<dl>
<dt>Identifiant:</dt>
<dd><strong><?php echo htmlspecialchars($thesis['identifier']); ?></strong></dd>
<dt>Titre:</dt>
<dd><?php echo htmlspecialchars($thesis['title']); ?></dd>
<?php if ($thesis['subtitle']): ?>
<dt>Sous-titre:</dt>
<dd><?php echo htmlspecialchars($thesis['subtitle']); ?></dd>
<?php endif; ?>
<dt>Auteur·ice(s):</dt>
<dd><?php echo htmlspecialchars($thesis['authors']); ?></dd>
<dt>Année:</dt>
<dd><?php echo htmlspecialchars($thesis['year']); ?></dd>
</dl>
<h3>Détails académiques</h3>
<dl>
<dt>Orientation:</dt>
<dd><?php echo htmlspecialchars($thesis['orientation'] ?? 'Non spécifié'); ?></dd>
<dt>Atelier Pratique:</dt>
<dd><?php echo htmlspecialchars($thesis['ap_program'] ?? 'Non spécifié'); ?></dd>
<dt>Finalité:</dt>
<dd><?php echo htmlspecialchars($thesis['finality_type'] ?? 'Non spécifié'); ?></dd>
<?php if ($thesis['supervisors']): ?>
<dt>Promoteur·ice(s):</dt>
<dd><?php echo htmlspecialchars($thesis['supervisors']); ?></dd>
<?php endif; ?>
</dl>
<h3>Contenu</h3>
<dl>
<?php if ($thesis['synopsis']): ?>
<dt>Synopsis:</dt>
<dd><?php echo nl2br(htmlspecialchars($thesis['synopsis'])); ?></dd>
<?php endif; ?>
<?php if ($thesis['languages']): ?>
<dt>Langue(s):</dt>
<dd><?php echo htmlspecialchars($thesis['languages']); ?></dd>
<?php endif; ?>
<?php if ($thesis['formats']): ?>
<dt>Format(s):</dt>
<dd><?php echo htmlspecialchars($thesis['formats']); ?></dd>
<?php endif; ?>
<?php if ($thesis['keywords']): ?>
<dt>Mots-clés:</dt>
<dd><?php echo htmlspecialchars($thesis['keywords']); ?></dd>
<?php endif; ?>
<?php if ($thesis['file_size_info']): ?>
<dt>Durée/Taille:</dt>
<dd><?php echo htmlspecialchars($thesis['file_size_info']); ?></dd>
<?php endif; ?>
<?php if ($thesis['baiu_link']): ?>
<dt>Lien:</dt>
<dd><a href="<?php echo htmlspecialchars($thesis['baiu_link']); ?>" target="_blank" rel="noopener">
<?php echo htmlspecialchars($thesis['baiu_link']); ?>
</a></dd>
<?php endif; ?>
</dl>
<?php if (!empty($files)): ?>
<h3>Fichiers téléversés</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Nom du fichier</th>
<th>Taille</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($files as $file): ?>
<tr>
<td><?php echo htmlspecialchars($file['file_type']); ?></td>
<td><?php echo htmlspecialchars($file['file_name']); ?></td>
<td><?php echo formatFileSize($file['file_size']); ?></td>
<td><?php echo date('d/m/Y H:i', strtotime($file['uploaded_at'])); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<h3>Statut de publication</h3>
<p><strong>⏳ En attente</strong> - Votre TFE ne sera publié qu'après la soutenance et l'ajout éventuel d'une note contextuelle par le jury.</p>
<p class="submitted-date">
Soumis le <?php echo date('d/m/Y à H:i', strtotime($thesis['submitted_at'])); ?>
</p>
</div>
<p><a href="index.php">Soumettre un autre TFE</a></p>
<?php else: ?>
<p>Aucune donnée à afficher.</p>
<p><a href="index.php">Retour au formulaire</a></p>
<?php endif; ?>
</main>
<footer>
<p>Formulaire Post-ERG</p>
</footer>
</body>
</html>
<style>
.thesis-info {
background: #f5f5f5;
padding: 2rem;
border-radius: 8px;
margin: 2rem 0;
}
.thesis-info h2 {
margin-top: 0;
border-bottom: 2px solid #333;
padding-bottom: 0.5rem;
}
.thesis-info h3 {
margin-top: 2rem;
margin-bottom: 1rem;
color: #555;
}
.thesis-info dl {
display: grid;
grid-template-columns: 200px 1fr;
gap: 0.5rem 1rem;
margin-bottom: 1.5rem;
}
.thesis-info dt {
font-weight: bold;
color: #666;
}
.thesis-info dd {
margin: 0;
}
.thesis-info table {
width: 100%;
margin-top: 1rem;
}
.thesis-info table th {
text-align: left;
background: #ddd;
padding: 0.5rem;
}
.thesis-info table td {
padding: 0.5rem;
border-bottom: 1px solid #ddd;
}
.submitted-date {
margin-top: 2rem;
font-style: italic;
color: #666;
}
.error {
background: #fee;
border: 2px solid #c00;
padding: 1.5rem;
border-radius: 8px;
color: #c00;
}
@media (max-width: 768px) {
.thesis-info dl {
grid-template-columns: 1fr;
gap: 0.25rem;
}
.thesis-info dt {
margin-top: 1rem;
}
}
</style>