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>

View File

@@ -1,11 +0,0 @@
vendor/
compose.lock
### Data et Mémoire###
formulaire/data/yaml/*
formulaire/data/content/*
formulaire/data/cover/*
front-backend/data/yaml/*
front-backend/data/content/*
front-backend/data/cover/*

View File

@@ -1,44 +0,0 @@
# PostERG - Site web public
Site web affichant publiquement les mémoires et travaux de fin d'études soumis par les étudiant.e.s de l'ERG.
## Fonctionnalités
- Affichage paginé des mémoires
- Visualisation détaillée de chaque mémoire
- Pages d'information (à propos, contact, licences)
## Technologies
- PHP
- [Symfony YAML](https://symfony.com/doc/current/components/yaml.html) pour la lecture des métadonnées
- CSS (Bulma)
## Installation
```shell
composer install
```
## Lancement
```shell
php -S 127.0.0.1:3001
```
Puis ouvrir [127.0.0.1:3001](http://127.0.0.1:3001) dans votre navigateur.
## Structure
```
front-backend/
├── assets/ # Fichiers CSS et ressources
├── data/
│ └── yaml/ # Fichiers YAML des mémoires
├── inc/ # Fichiers inclus (header, footer)
├── index.php # Page d'accueil avec liste paginée
├── memoire.php # Page de détail d'un mémoire
├── apropos.php # Page à propos
├── contact.php # Page de contact
└── licences.php # Page des licences
```

View File

@@ -1,67 +0,0 @@
<?php include 'inc/header.php'; ?>
<section class="section">
<div class="container">
<h2 class="title is-2">À propos</h2>
<h2 class="title is-3">Travail en Cours.</h2>
<div class="content">
<a href="https://pads.erg.be/p/POST-ERG_charteDeVosMEMOIRES">Chartes d'utilisation et fonctionnement de l'initiative.</a>
</div>
<div class="content">
<p>Nous sommes un groupe d'étudiant.e.s en design numérique avec projet concernant les mémoires de l'année
passée qui demande votre aide !
Qu'en est-il des mémoires après notre master ?
Quelle est la visibilité de notre travail après notre départ de l'ERG ? Certains mémoires finissent à la
bibliothèque exposés, mais lesquels et pourquoi ?
Actuellement, la bibliothèque (BAUI) sert de lieux d'archives (collection de documents anciens, classés à des
fins historiques ; lieu où les archives sont conservées) des mémoires pour l'erg, st-Luc et UCL mais pourquoi
sont-ils si peu à être exposés ? </p>
<p>Actuellement, les mémoires sélectionnés sont ceux avec une grande disctinction (16/20). Cette note obtenue
dépend de la cotation de lecture de mémoire et sa défense.</p>
<h3 class="title is-3">Mais pourquoi cette moyenne de 14/20 ?</h3>
<h3 class="title is-3">Et où finissent les autres mémoires ?</h3>
<p>En l'occurence, la bibliothèque n'est pas un lieu de diffusion et de monstration " juste ", car les mémoires
dépendent de la note attribuée en fin de Master et de la place disponible dans les étagères ; sans parler de
l'état déplorable de certains mémoires due aux conditions de stockages : couverture plastifiée, stickers, etc
- nous travaillons un visuel qui finalement sera " dégradé " lors de son exposition à la bibliothèque, si
exposé. De plus, les mémoires sont visible en bibliothèque de manière tangible (style édition).</p>
<h3 class="title is-3">Qu'en t-il des formats numérique, audio ou vidéo ?</h3>
<p>De fait, notre recherche se pencherait sur un dispositif de partage/diffusion plus adéquat et en phase avec
la multitude de format et forme de monstration plus contemporain.
<br>
Notre lieu d'archive/exposition prendrait la forme d'un site web, idéalement en ligne (ou en local en fonction
du RE - propriété intellectuelle et droit d'auteur ?). Il contiendrait tout types de mémoire ainsi qu'une
interrogation autour de sa licence et sa notion de partage.
<br>
En paralèlle, nous donnerons quelques tips et bon plans pour : " comment licencier son mémoire : pour protéger
ses valeurs et notions de partage s'il y'a" . Dans un premier temps, nous allons collecter un maximum de
mémoires et
tenter de recontacter leur auteurice pour échanger avec eux et obtenir des pdf, vidéos, photos. Dans un second
temps, nous trouverons un relais pour les futures étudiant.e.s et organiseront un formulaire qui publiera
automatiquement les mémoires sur notre site.
</p>
<h2 class="title is-2">Un projet depuis 2022</h2>
<p>
Théo Hennequin<br>
<a href="https://www.theohennequin.com">www.theohennequin.com</a><br>
Théophile Gervreau-Mercier<br>
<a href="https://tgm.happyngreen.fr">tgm.happyngreen.fr</a><br>
Olivia Marly<br>
<a href="mailto:oli98marly@gmail.com">oli98marly@gmail.com</a>
</p>
</div>
</div>
</section>
<?php include 'inc/footer.php'; ?>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 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,559 +0,0 @@
/* ============================================
POST-ERG - Minimalistic CSS
============================================ */
/* Custom Font */
@font-face {
font-family: 'Combined';
src: url("fonts/Combinedd.otf");
}
/* ============================================
VARIABLES & BASE
============================================ */
:root {
--color-primary: #c104fc;
--color-secondary: #4da870;
--color-text: #333;
--color-text-light: #666;
--color-border: #ddd;
--color-bg: #fff;
--color-bg-light: #f9f9f9;
--spacing-sm: 0.75rem;
--spacing: 1.5rem;
--spacing-lg: 3rem;
--spacing-xl: 4rem;
--border-radius: 8px;
--max-width: 1400px;
}
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.7;
color: var(--color-text);
background: var(--color-bg-light);
margin: 0;
padding: 0;
font-size: 16px;
}
/* ============================================
NAVBAR
============================================ */
.navbar {
font-family: 'Combined', sans-serif;
background: linear-gradient(280deg, var(--color-secondary) 0%, var(--color-primary) 85%);
padding: 2rem 3rem;
color: white;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.navbar-brand {
margin-bottom: 1rem;
}
.navbar-brand h1 {
margin: 0;
font-size: 2.5rem;
font-weight: normal;
letter-spacing: 0.5px;
}
.navbar-menu {
display: flex;
gap: 2rem;
align-items: center;
flex-wrap: wrap;
}
.navbar-item {
color: white;
text-decoration: none;
transition: color 0.2s;
font-size: 1.1rem;
padding: 0.5rem 0;
}
.navbar-item:hover {
color: var(--color-secondary);
}
/* ============================================
LAYOUT
============================================ */
.section {
padding: var(--spacing-xl) var(--spacing-lg);
background: var(--color-bg-light);
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 var(--spacing-lg);
}
/* Grid System */
.columns {
display: grid;
gap: 2rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.columns.is-multiline {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.column {
min-width: 0; /* Fix overflow issues */
}
.column.is-one-third {
grid-column: span 1;
}
.column.is-one-fifth {
/* Will use auto-fill for responsive grid */
}
/* Two-column layout for detail pages */
.columns.is-variable {
grid-template-columns: 1fr 2fr;
gap: var(--spacing-lg);
}
@media (max-width: 768px) {
.columns.is-variable {
grid-template-columns: 1fr;
}
}
/* ============================================
CARDS
============================================ */
.card {
background: var(--color-bg);
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
height: 100%;
display: flex;
flex-direction: column;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.card-link {
text-decoration: none;
color: inherit;
display: block;
height: 100%;
}
.card-link:hover .card {
transform: translateY(-6px);
box-shadow: 0 8px 24px rgba(193, 4, 252, 0.2);
border-color: var(--color-primary);
}
.card-image {
width: 100%;
overflow: hidden;
background: var(--color-bg-light);
}
.card-image img {
width: 100%;
height: 240px;
object-fit: cover;
display: block;
}
.card-content {
padding: 1.75rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4, h5, h6 {
margin: 0 0 0.75rem 0;
line-height: 1.3;
font-weight: 600;
}
.title {
font-size: 1.5rem;
color: var(--color-text);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.title.is-1 {
font-size: 2.5rem;
}
.title.is-4 {
font-size: 1.35rem;
line-height: 1.4;
}
.title.is-6 {
font-size: 1rem;
}
.subtitle {
font-size: 1.1rem;
color: var(--color-text-light);
margin-bottom: 0.75rem;
font-weight: 500;
}
.content {
font-size: 1rem;
line-height: 1.7;
color: var(--color-text-light);
}
.block {
margin-top: auto; /* Push to bottom of card */
padding-top: 0.5rem;
}
/* ============================================
TAG
============================================ */
.tag {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--color-bg-light);
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
font-size: 0.95rem;
margin-bottom: 0.75rem;
font-weight: 500;
}
.tag.is-link {
background: rgba(193, 4, 252, 0.08);
border-color: var(--color-primary);
color: var(--color-primary);
}
.tag.is-light {
background: var(--color-bg-light);
}
/* ============================================
FORMS
============================================ */
.field {
margin-bottom: var(--spacing);
}
.label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
font-size: 0.9rem;
}
.input,
.textarea,
select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s, box-shadow 0.2s;
background: var(--color-bg);
}
.input:focus,
.textarea:focus,
select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(193, 4, 252, 0.1);
}
.textarea {
min-height: 150px;
resize: vertical;
line-height: 1.6;
}
/* ============================================
BUTTONS
============================================ */
.button {
display: inline-block;
padding: 0.85rem 2rem;
background: var(--color-primary);
color: white;
border: none;
border-radius: var(--border-radius);
font-size: 1.05rem;
text-decoration: none;
cursor: pointer;
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
font-family: inherit;
font-weight: 500;
box-shadow: 0 2px 4px rgba(193, 4, 252, 0.2);
}
.button:hover {
background: #a003d1;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(193, 4, 252, 0.3);
}
.button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(193, 4, 252, 0.2);
}
.button.is-link {
background: var(--color-primary);
}
.button.is-light {
background: var(--color-bg);
color: var(--color-text);
border: 2px solid var(--color-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.button.is-light:hover {
background: var(--color-bg-light);
border-color: var(--color-primary);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* ============================================
NOTIFICATIONS
============================================ */
.notification {
padding: 1.5rem 2rem;
border-radius: var(--border-radius);
margin-bottom: var(--spacing);
border: 2px solid;
font-size: 1.05rem;
line-height: 1.6;
}
.notification.is-danger {
background: #fee;
border-color: #fcc;
color: #c00;
}
.notification.is-success {
background: #efe;
border-color: #cfc;
color: #060;
}
.notification.is-info {
background: #eef;
border-color: #ccf;
color: #006;
}
/* ============================================
BOX
============================================ */
.box {
background: var(--color-bg);
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
padding: 2rem;
margin-bottom: var(--spacing);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* ============================================
MEDIA
============================================ */
img,
video,
iframe,
embed {
max-width: 100%;
height: auto;
border-radius: var(--border-radius);
}
embed {
display: block;
width: 100%;
max-width: 800px;
height: 700px;
margin: 0 auto;
border: 1px solid var(--color-border);
}
/* ============================================
PAGINATION
============================================ */
.pagination {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-top: var(--spacing-lg);
flex-wrap: wrap;
}
.pagination a,
.pagination span {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
text-decoration: none;
color: var(--color-text);
transition: all 0.2s;
}
.pagination a:hover {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.pagination .current {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
/* ============================================
FOOTER
============================================ */
.footer {
background: var(--color-bg);
border-top: 2px solid var(--color-border);
padding: 3rem 2rem;
text-align: center;
margin-top: var(--spacing-xl);
font-size: 1rem;
color: var(--color-text-light);
}
/* ============================================
UTILITIES
============================================ */
.has-text-centered {
text-align: center;
}
.is-flex {
display: flex;
}
.is-justify-content-space-between {
justify-content: space-between;
}
.is-align-items-center {
align-items: center;
}
.mt-4 {
margin-top: var(--spacing-lg);
}
.mb-4 {
margin-bottom: var(--spacing-lg);
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
:root {
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
}
.navbar {
padding: 1.5rem 1rem;
}
.navbar-brand h1 {
font-size: 1.8rem;
}
.navbar-menu {
gap: 1rem;
}
.navbar-item {
font-size: 1rem;
}
.columns {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.section {
padding: 2rem 1rem;
}
.card-content {
padding: 1.5rem;
}
embed {
height: 400px;
}
.title.is-4 {
font-size: 1.2rem;
}
}
@media (max-width: 480px) {
.columns {
grid-template-columns: 1fr;
}
.navbar {
padding: 1.25rem 1rem;
}
.navbar-brand h1 {
font-size: 1.5rem;
}
.section {
padding: 1.5rem 1rem;
}
}

View File

@@ -1 +0,0 @@
[1769594004,1769594004,1769594004,1769594004,1769594004]

View File

@@ -1 +0,0 @@
[1769593359,1769593362,1769593367,1769593372,1769593375,1769593380]

View File

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

View File

@@ -1,388 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e941923040be085b6ce94a2d66270369",
"packages": [
{
"name": "behat/transliterator",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/Transliterator.git",
"reference": "baac5873bac3749887d28ab68e2f74db3a4408af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af",
"reference": "baac5873bac3749887d28ab68e2f74db3a4408af",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
"chuyskywalker/rolling-curl": "^3.1",
"php-yaoi/php-yaoi": "^1.0",
"phpunit/phpunit": "^8.5.25 || ^9.5.19"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Behat\\Transliterator\\": "src/Behat/Transliterator"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Artistic-1.0"
],
"description": "String transliterator",
"keywords": [
"i18n",
"slug",
"transliterator"
],
"support": {
"issues": "https://github.com/Behat/Transliterator/issues",
"source": "https://github.com/Behat/Transliterator/tree/v1.5.0"
},
"time": "2022-03-30T09:27:43+00:00"
},
{
"name": "symfony/intl",
"version": "v6.2.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
"reference": "860c99e53149d22df1900d3aefdaeb17adb7669d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/860c99e53149d22df1900d3aefdaeb17adb7669d",
"reference": "860c99e53149d22df1900d3aefdaeb17adb7669d",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"symfony/filesystem": "^5.4|^6.0",
"symfony/finder": "^5.4|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Intl\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Eriksen Costa",
"email": "eriksen.costa@infranology.com.br"
},
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides access to the localization data of the ICU library",
"homepage": "https://symfony.com",
"keywords": [
"i18n",
"icu",
"internationalization",
"intl",
"l10n",
"localization"
],
"support": {
"source": "https://github.com/symfony/intl/tree/v6.2.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-04-14T16:23:31+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "927013f3aac555983a5059aada98e1907d842695"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/927013f3aac555983a5059aada98e1907d842695",
"reference": "927013f3aac555983a5059aada98e1907d842695",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-iconv": "*"
},
"suggest": {
"ext-iconv": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Iconv\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Iconv extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"iconv",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-iconv/tree/v1.27.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/yaml",
"version": "v6.2.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "61916f3861b1e9705b18cfde723921a71dd1559d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/61916f3861b1e9705b18cfde723921a71dd1559d",
"reference": "61916f3861b1e9705b18cfde723921a71dd1559d",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"symfony/console": "<5.4"
},
"require-dev": {
"symfony/console": "^5.4|^6.0"
},
"suggest": {
"symfony/console": "For validating YAML files using the lint command"
},
"bin": [
"Resources/bin/yaml-lint"
],
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v6.2.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-04-28T13:25:36+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View File

@@ -1,19 +0,0 @@
<?php include 'inc/header.php'; ?>
<section class="section">
<div class="container">
<h2 class="title is-2">Contact</h2>
<div class="content">
<p>Laurent Leprince, <br>
Bibliothèque d'architecture, d'ingénierie architecturale, d'urbanisme (BAIU) :<br>
laurent.leprince@uclouvain.be</p>
<p>Xavier Gorgol, <br>
Responsable des mémoires de l'ERG :<br>
xavier.gorgol@erg.be<br></p>
<p>Brigitte Ledune,<br>
Cours de suivi de mémoire : <br>
brigitte.ledune@erg.be</p>
<?php include 'inc/footer.php'; ?>

View File

@@ -1,113 +0,0 @@
[02-May-2023 18:18:59 UTC] PHP Fatal error: Uncaught Error: Call to undefined function yaml_parse_file() in /home/lockpick/Projects/posterg-website/index.php:17
Stack trace:
#0 {main}
thrown in /home/lockpick/Projects/posterg-website/index.php on line 17
[02-May-2023 18:19:05 UTC] PHP Fatal error: Uncaught Error: Call to undefined function yaml_parse_file() in /home/lockpick/Projects/posterg-website/index.php:17
Stack trace:
#0 {main}
thrown in /home/lockpick/Projects/posterg-website/index.php on line 17
[02-May-2023 18:19:09 UTC] PHP Fatal error: Uncaught Error: Call to undefined function yaml_parse_file() in /home/lockpick/Projects/posterg-website/index.php:17
Stack trace:
#0 {main}
thrown in /home/lockpick/Projects/posterg-website/index.php on line 17
[02-May-2023 18:52:59 UTC] PHP Warning: include(header.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:52:59 UTC] PHP Warning: include(): Failed opening 'header.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:52:59 UTC] PHP Warning: include(footer.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:52:59 UTC] PHP Warning: include(): Failed opening 'footer.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:19 UTC] PHP Warning: include(header.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:19 UTC] PHP Warning: include(): Failed opening 'header.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:19 UTC] PHP Warning: include(footer.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:19 UTC] PHP Warning: include(): Failed opening 'footer.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:20 UTC] PHP Warning: include(header.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:20 UTC] PHP Warning: include(): Failed opening 'header.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:20 UTC] PHP Warning: include(footer.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:20 UTC] PHP Warning: include(): Failed opening 'footer.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:38 UTC] PHP Warning: include(header.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:38 UTC] PHP Warning: include(): Failed opening 'header.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:38 UTC] PHP Warning: include(footer.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:38 UTC] PHP Warning: include(): Failed opening 'footer.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:39 UTC] PHP Warning: include(header.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:39 UTC] PHP Warning: include(): Failed opening 'header.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 20
[02-May-2023 18:53:39 UTC] PHP Warning: include(footer.php): Failed to open stream: No such file or directory in /home/lockpick/Projects/posterg-website/index.php on line 38
[02-May-2023 18:53:39 UTC] PHP Warning: include(): Failed opening 'footer.php' for inclusion (include_path='.:') in /home/lockpick/Projects/posterg-website/index.php on line 38
[04-May-2023 19:41:18 UTC] PHP Warning: Undefined array key "title" in /home/lockpick/Projects/posterg-website/memoire.php on line 21
[04-May-2023 19:41:18 UTC] PHP Warning: Undefined array key "author" in /home/lockpick/Projects/posterg-website/memoire.php on line 24
[04-May-2023 19:41:32 UTC] PHP Warning: Undefined array key "title" in /home/lockpick/Projects/posterg-website/memoire.php on line 21
[04-May-2023 19:41:32 UTC] PHP Warning: Undefined array key "author" in /home/lockpick/Projects/posterg-website/memoire.php on line 24
[04-May-2023 20:09:45 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 42
[04-May-2023 20:12:06 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 46
[04-May-2023 20:12:49 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 46
[04-May-2023 20:13:06 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 46
[04-May-2023 20:13:20 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 46
[04-May-2023 20:15:28 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 50
[04-May-2023 20:16:10 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[04-May-2023 20:16:30 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[04-May-2023 20:16:47 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[04-May-2023 20:17:04 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[04-May-2023 20:17:41 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[04-May-2023 20:18:58 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 71
[04-May-2023 20:48:21 UTC] PHP Warning: foreach() argument must be of type array|object, string given in /home/lockpick/Projects/posterg-website/memoire.php on line 74
[04-May-2023 20:54:30 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 20:55:03 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 20:55:11 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 20:55:14 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 20:56:41 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 21:02:29 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 21:02:54 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[04-May-2023 21:25:39 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:44:42 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:46:07 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:02 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:15 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:19 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:22 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:25 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:31 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:47:34 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:48:11 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:57:31 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:57:40 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:57:43 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:57:48 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:57:51 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 08:58:15 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 09:00:33 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 09:02:53 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 09:17:40 UTC] PHP Warning: Undefined variable $key in /home/lockpick/Projects/posterg-website/memoire.php on line 68
[05-May-2023 09:43:22 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 09:45:04 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 09:49:04 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 09:49:04 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 09:49:04 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 09:49:04 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 09:49:04 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:01 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 10:06:23 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 10:06:36 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:36 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:36 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:36 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:36 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:42 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 10:06:44 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:44 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:44 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:44 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:44 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:45 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 10:06:47 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:47 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:47 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:47 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:06:47 UTC] PHP Warning: Undefined array key "resume" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:07:02 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/memoire.php on line 70
[05-May-2023 10:07:16 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:07:16 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:07:16 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:07:16 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:07:16 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:08:03 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:08:03 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:08:03 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:08:03 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58
[05-May-2023 10:08:03 UTC] PHP Warning: Undefined array key "description" in /home/lockpick/Projects/posterg-website/index.php on line 58

View File

@@ -1,9 +0,0 @@
<!-- footer.php -->
<footer class="footer">
<div class="content has-text-centered">
<p>Site fait avec en PHP, HTML, CSS fait mains.</p>
</div>
</footer>
</body>
</html>

View File

@@ -1,30 +0,0 @@
<!-- header.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="author" content="">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Posterg</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="assets/posterg.css?v=2">
</head>
<body>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="index.php">
<h1 class="title is-1">Mémoire post-ERG/A life after ERG</h1>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-end">
<a href="search.php" class="navbar-item">Rechercher</a>
<a href="apropos.php" class="navbar-item">À propos</a>
<a href="contact.php" class="navbar-item">Contact</a>
<a href="licences.php" class="navbar-item">Licences</a>
</div>
</div>
</nav>

View File

@@ -1,87 +0,0 @@
<?php
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
require_once __DIR__ . '/../../shared/Database.php';
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$itemsPerPage = 10;
try {
$db = Database::getInstance();
$offset = ($page - 1) * $itemsPerPage;
$itemsToLoad = $db->getPublishedTheses($itemsPerPage, $offset);
$totalItems = $db->countPublishedTheses();
$totalPages = ceil($totalItems / $itemsPerPage);
} catch (Exception $e) {
error_log("Error loading theses: " . $e->getMessage());
$itemsToLoad = [];
$totalPages = 0;
}
include 'inc/header.php'; ?>
<section class="section">
<div class="container">
<div class="columns is-multiline">
<?php foreach ($itemsToLoad as $item): ?>
<div class="column is-one-fifth">
<a href="memoire.php?id=<?= $item['id']; ?>" class="card-link">
<div class="card">
<?php
// Get cover image from thesis_files if available
$coverImage = null;
if (!empty($item['id'])) {
$files = $db->getThesisFiles($item['id']);
foreach ($files as $file) {
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif']) && $file['file_type'] === 'main') {
$coverImage = $file['file_path'];
break;
}
}
}
?>
<?php if ($coverImage): ?>
<div class="card-image">
<figure class="image ">
<img src="<?= htmlspecialchars($coverImage); ?>" alt="Image preview">
</figure>
</div>
<?php endif; ?>
<div class="card-content">
<h4 class="title is-4">
<?= htmlspecialchars($item['title']); ?>
</h4>
<h2 class="subtitle">
<?= htmlspecialchars($item['authors'] ?? 'Auteur inconnu'); ?>
</h2>
<h3 class="tag title is-6 is-link is-light">
<?= htmlspecialchars($item['year']); ?>
</h3>
<p class="block content">
<?php
$excerpt_length = 150;
$synopsis = $item['synopsis'] ?? '';
$description_excerpt = strlen($synopsis) > $excerpt_length
? substr($synopsis, 0, $excerpt_length) . '...'
: $synopsis;
?>
<?= htmlspecialchars($description_excerpt); ?>
</p>
</div>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
include 'inc/footer.php';
?>

View File

@@ -1,18 +0,0 @@
<?php include 'inc/header.php';?>
<section class="section">
<div class="container">
<div class="content">
Ce travail éditorial, concernant les licences de 2021-2022 est né d'une recherche menée par : <br> <br>
Defez Aurélie <br>
Gervreau-Mercier Théophile <br>
Debaene Justine <br>
Troadec Marie <br>
Marly Olivia <br>
Goldberg Jacquemain Elodie <br>
</div>
</div>
</section>
<?php include 'inc/footer.php';?>

View File

@@ -1,165 +0,0 @@
<?php
// Disable displaying errors, log errors to a file named 'error.log'
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
// Load required libraries and classes
require_once __DIR__ . '/../../shared/Database.php';
// Check if an id parameter is provided in the URL
if (isset($_GET['id'])) {
$thesisId = intval($_GET['id']);
try {
$db = Database::getInstance();
$data = $db->getThesisById($thesisId);
if (!$data) {
// Thesis not found or not published
header('Location: index.php');
exit;
}
} catch (Exception $e) {
error_log("Error loading thesis: " . $e->getMessage());
header('Location: index.php');
exit;
}
} else {
// Redirect to the index page if no id parameter is provided
header('Location: index.php');
exit;
}
// Include the header template
include 'inc/header.php'; ?>
<section class="section">
<div class="container">
<div class="columns is-variable is-1-mobile is-0-tablet is-3-desktop is-8-widescreen is-2-fullhd">
<!-- INFO CARD -->
<div class="column is-one-third">
<div class="card">
<div class="card-content">
<!-- Display the title and author from the database -->
<h1 class="title">
<?= htmlspecialchars($data['title']); ?>
<?php if (!empty($data['subtitle'])): ?>
<br><small><?= htmlspecialchars($data['subtitle']); ?></small>
<?php endif; ?>
</h1>
<h2 class="subtitle">par
<?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu'); ?>
</h2>
<h3 class="subtitle"></h3>
<div class="columns">
<div class="column is-half ">
<?php if (!empty($data['orientation']) || !empty($data['ap_program'])): ?>
<h3 class="subtitle">
<?php if (!empty($data['orientation'])): ?>
<?= htmlspecialchars($data['orientation']); ?>
<?php endif; ?>
<?php if (!empty($data['orientation']) && !empty($data['ap_program'])): ?>
et
<?php endif; ?>
<?php if (!empty($data['ap_program'])): ?>
<?= htmlspecialchars($data['ap_program']); ?>
<?php endif; ?>
</h3>
<?php endif; ?>
<p class="block tag subtitle is-6">
<?= htmlspecialchars($data['year']); ?>
</p>
<?php if (!empty($data['finality_type'])): ?>
<p class="block">
<strong>Finalité:</strong> <?= htmlspecialchars($data['finality_type']); ?>
</p>
<?php endif; ?>
</div>
<div class="column">
<?php if (!empty($data['context_note'])): ?>
<p class="block">
<em><?= htmlspecialchars($data['context_note']); ?></em>
</p>
<?php endif; ?>
<?php if (!empty($data['supervisors'])): ?>
<p class="block">
<strong>Promoteur.ice.s:</strong>
<?= htmlspecialchars($data['supervisors']); ?>
</p>
<?php endif; ?>
<?php if (!empty($data['languages'])): ?>
<p class="block">
<strong>Langue(s):</strong>
<?= htmlspecialchars($data['languages']); ?>
</p>
<?php endif; ?>
<?php if (!empty($data['formats'])): ?>
<p class="block">
<strong>Format(s):</strong>
<?= htmlspecialchars($data['formats']); ?>
</p>
<?php endif; ?>
<?php if (!empty($data['keywords'])): ?>
<p class="block">
<strong>Mots-clés:</strong>
<?= htmlspecialchars($data['keywords']); ?>
</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="box">
<?php if (!empty($data['synopsis'])): ?>
<?= nl2br(htmlspecialchars($data['synopsis'])); ?>
<?php endif; ?>
</div>
</div>
<div class="column is-two-third">
<div class="content">
<!-- Check if there are any files in the database -->
<?php if (isset($data['files']) && count($data['files']) > 0): ?>
<!-- Loop through the files and display them based on their file type -->
<?php foreach ($data['files'] as $file): ?>
<?php $ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION)); ?>
<div class="block">
<?php if ($ext === 'pdf'): ?>
<!-- Display PDF files using the embed element -->
<embed src="<?= htmlspecialchars($file['file_path']); ?>" type="application/pdf" width="100%" height="600px" />
<?php elseif (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'bmp'])): ?>
<!-- Display image files using the img element -->
<figure>
<img src="<?= htmlspecialchars($file['file_path']); ?>" alt="<?= htmlspecialchars($file['file_name']); ?>">
</figure>
<?php elseif ($ext === 'mp4'): ?>
<!-- Display MP4 video files using the video element -->
<video width="100%" height="auto" controls>
<source src="<?= htmlspecialchars($file['file_path']); ?>" type="video/mp4">
Your browser does not support the video tag.
</video>
<?php endif; ?>
<?php if (!empty($file['description'])): ?>
<p class="help"><?= htmlspecialchars($file['description']); ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="notification is-warning">Aucun fichier disponible pour ce mémoire.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
</section>
<!-- Include the footer template -->
<?php include 'inc/footer.php'; ?>

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Simple test runner
* Runs all tests in the tests/ directory
*/
echo "╔════════════════════════════════════════════╗\n";
echo "║ Running Front-Backend Tests ║\n";
echo "╚════════════════════════════════════════════╝\n\n";
$testFiles = [
['name' => 'Fixtures', 'path' => __DIR__ . '/../../database/fixtures/CreateTestDatabase.php'],
['name' => 'Integration', 'path' => __DIR__ . '/tests/Integration/SearchTest.php'],
['name' => 'Security', 'path' => __DIR__ . '/tests/Security/SecurityTest.php'],
['name' => 'Unit', 'path' => __DIR__ . '/tests/Unit/RateLimitTest.php'],
];
$totalTests = 0;
$passedTests = 0;
$failedTests = 0;
foreach ($testFiles as $test) {
echo "┌─────────────────────────────────────────┐\n";
echo "│ Test Suite: " . str_pad($test['name'], 27) . "\n";
echo "└─────────────────────────────────────────┘\n\n";
$totalTests++;
$path = $test['path'];
$file = basename($path);
if (!file_exists($path)) {
echo "⚠️ SKIP: $file (not found at $path)\n\n";
continue;
}
echo "Running: $file\n";
echo str_repeat("", 50) . "\n";
ob_start();
$exitCode = 0;
try {
include $path;
} catch (Exception $e) {
echo "❌ ERROR: " . $e->getMessage() . "\n";
$exitCode = 1;
}
$output = ob_get_clean();
if ($exitCode === 0 && (
strpos($output, '❌') !== false ||
strpos($output, 'FAIL') !== false ||
strpos($output, 'Error:') !== false
)) {
$exitCode = 1;
}
echo $output;
if ($exitCode === 0) {
echo "\n✅ PASSED\n\n";
$passedTests++;
} else {
echo "\n❌ FAILED\n\n";
$failedTests++;
}
}
echo "╔════════════════════════════════════════════╗\n";
echo "║ Test Summary ║\n";
echo "╠════════════════════════════════════════════╣\n";
echo "║ Total: " . str_pad($totalTests, 35) . "\n";
echo "║ Passed: " . str_pad($passedTests . "", 36) . "\n";
echo "║ Failed: " . str_pad($failedTests . ($failedTests > 0 ? "" : ""), 36) . "\n";
echo "╚════════════════════════════════════════════╝\n\n";
if ($failedTests > 0) {
echo "❌ Some tests failed!\n";
exit(1);
} else {
echo "✅ All tests passed!\n";
exit(0);
}

View File

@@ -1,419 +0,0 @@
<?php
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
require_once __DIR__ . '/../../shared/Database.php';
require_once __DIR__ . '/../../shared/RateLimit.php';
// Rate limiting: 30 requests per minute
$rateLimit = new RateLimit(30, 60);
// Check rate limit
if (!$rateLimit->check()) {
// Send rate limit headers
http_response_code(429);
header('Retry-After: ' . $rateLimit->getResetTime());
$rateLimit->sendHeaders();
// Display error page
include 'inc/header.php';
echo '<section class="section">';
echo ' <div class="container">';
echo ' <div class="notification is-danger">';
echo ' <strong>Trop de requêtes</strong><br>';
echo ' Vous avez dépassé la limite de ' . 30 . ' recherches par minute.';
echo ' <br>Veuillez réessayer dans ' . $rateLimit->getResetTime() . ' secondes.';
echo ' </div>';
echo ' </div>';
echo '</section>';
include 'inc/footer.php';
exit;
}
// Send rate limit headers for successful requests
$rateLimit->sendHeaders();
// Periodic cleanup (1% chance)
if (rand(1, 100) === 1) {
$rateLimit->cleanup();
}
// Pagination (max 100 per page)
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$itemsPerPage = min(100, isset($_GET['per_page']) ? intval($_GET['per_page']) : 20);
// Collect search parameters
$searchParams = [];
if (!empty($_GET['query'])) {
$searchParams['query'] = trim($_GET['query']);
}
if (!empty($_GET['year'])) {
$searchParams['year'] = intval($_GET['year']);
}
if (!empty($_GET['orientation'])) {
$searchParams['orientation'] = $_GET['orientation'];
}
if (!empty($_GET['ap_program'])) {
$searchParams['ap_program'] = $_GET['ap_program'];
}
if (!empty($_GET['finality'])) {
$searchParams['finality'] = $_GET['finality'];
}
if (!empty($_GET['keyword'])) {
$searchParams['keyword'] = $_GET['keyword'];
}
if (!empty($_GET['format'])) {
$searchParams['format'] = $_GET['format'];
}
if (!empty($_GET['language'])) {
$searchParams['language'] = $_GET['language'];
}
if (isset($_GET['is_doctoral'])) {
$searchParams['is_doctoral'] = $_GET['is_doctoral'] === '1';
}
$validationError = null;
try {
$db = Database::getInstance();
// Get search results
$offset = ($page - 1) * $itemsPerPage;
$results = $db->searchTheses($searchParams, $itemsPerPage, $offset);
$totalItems = $db->countSearchResults($searchParams);
$totalPages = ceil($totalItems / $itemsPerPage);
// Get filter options
$years = $db->getAvailableYears();
$orientations = $db->getOrientations();
$apPrograms = $db->getApPrograms();
$finalityTypes = $db->getFinalityTypes();
$keywords = $db->getUsedKeywords();
$formats = $db->getFormatTypes();
$languages = $db->getLanguages();
} catch (InvalidArgumentException $e) {
// Input validation error
error_log("Search validation error: " . $e->getMessage());
$validationError = $e->getMessage();
$results = [];
$totalPages = 0;
$totalItems = 0;
$years = [];
$orientations = [];
$apPrograms = [];
$finalityTypes = [];
$keywords = [];
$formats = [];
$languages = [];
} catch (Exception $e) {
// Database or other error
error_log("Error in search: " . $e->getMessage());
$validationError = "Une erreur est survenue lors de la recherche.";
$results = [];
$totalPages = 0;
$totalItems = 0;
$years = [];
$orientations = [];
$apPrograms = [];
$finalityTypes = [];
$keywords = [];
$formats = [];
$languages = [];
}
include 'inc/header.php'; ?>
<section class="section">
<div class="container">
<h1 class="title">Rechercher un mémoire</h1>
<!-- Display validation errors -->
<?php if ($validationError): ?>
<div class="notification is-danger">
<strong>Erreur de validation :</strong> <?= htmlspecialchars($validationError); ?>
</div>
<?php endif; ?>
<!-- Search Form -->
<form method="GET" action="search.php">
<div class="box">
<!-- Main search query -->
<div class="field">
<label class="label">Recherche libre</label>
<div class="control">
<input class="input" type="text" name="query"
placeholder="Titre, auteur, mots-clés, synopsis..."
value="<?= htmlspecialchars($_GET['query'] ?? ''); ?>">
</div>
<p class="help">Recherche dans le titre, sous-titre, synopsis, auteurs, promoteurs et mots-clés</p>
</div>
<!-- Advanced filters in columns -->
<div class="columns is-multiline">
<!-- Year filter -->
<div class="column is-half">
<div class="field">
<label class="label">Année</label>
<div class="control">
<div class="select is-fullwidth">
<select name="year">
<option value="">Toutes les années</option>
<?php foreach ($years as $year): ?>
<option value="<?= $year; ?>" <?= (isset($_GET['year']) && $_GET['year'] == $year) ? 'selected' : ''; ?>>
<?= $year; ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Orientation filter -->
<div class="column is-half">
<div class="field">
<label class="label">Orientation</label>
<div class="control">
<div class="select is-fullwidth">
<select name="orientation">
<option value="">Toutes les orientations</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?= htmlspecialchars($orientation['name']); ?>"
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $orientation['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- AP Program filter -->
<div class="column is-half">
<div class="field">
<label class="label">Atelier Pratique (AP)</label>
<div class="control">
<div class="select is-fullwidth">
<select name="ap_program">
<option value="">Tous les AP</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?= htmlspecialchars($ap['name']); ?>"
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($ap['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Finality filter -->
<div class="column is-half">
<div class="field">
<label class="label">Finalité</label>
<div class="control">
<div class="select is-fullwidth">
<select name="finality">
<option value="">Toutes les finalités</option>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?= htmlspecialchars($finality['name']); ?>"
<?= (isset($_GET['finality']) && $_GET['finality'] == $finality['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($finality['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Format filter -->
<div class="column is-half">
<div class="field">
<label class="label">Format</label>
<div class="control">
<div class="select is-fullwidth">
<select name="format">
<option value="">Tous les formats</option>
<?php foreach ($formats as $format): ?>
<option value="<?= htmlspecialchars($format['name']); ?>"
<?= (isset($_GET['format']) && $_GET['format'] == $format['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($format['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Language filter -->
<div class="column is-half">
<div class="field">
<label class="label">Langue</label>
<div class="control">
<div class="select is-fullwidth">
<select name="language">
<option value="">Toutes les langues</option>
<?php foreach ($languages as $language): ?>
<option value="<?= htmlspecialchars($language['name']); ?>"
<?= (isset($_GET['language']) && $_GET['language'] == $language['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($language['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Keyword filter -->
<div class="column is-full">
<div class="field">
<label class="label">Mot-clé</label>
<div class="control">
<div class="select is-fullwidth">
<select name="keyword">
<option value="">Tous les mots-clés</option>
<?php foreach ($keywords as $keyword): ?>
<option value="<?= htmlspecialchars($keyword['keyword']); ?>"
<?= (isset($_GET['keyword']) && $_GET['keyword'] == $keyword['keyword']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($keyword['keyword']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Thesis type filter -->
<div class="column is-full">
<div class="field">
<label class="label">Type</label>
<div class="control">
<div class="select is-fullwidth">
<select name="is_doctoral">
<option value="">TFE et Thèses doctorales</option>
<option value="0" <?= (isset($_GET['is_doctoral']) && $_GET['is_doctoral'] == '0') ? 'selected' : ''; ?>>
TFE uniquement
</option>
<option value="1" <?= (isset($_GET['is_doctoral']) && $_GET['is_doctoral'] == '1') ? 'selected' : ''; ?>>
Thèses doctorales uniquement
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link">Rechercher</button>
</div>
<div class="control">
<a href="search.php" class="button is-light">Réinitialiser</a>
</div>
</div>
</div>
</form>
<!-- Search results -->
<?php if (!empty($searchParams)): ?>
<div class="notification is-info is-light">
<strong><?= $totalItems; ?></strong> résultat<?= $totalItems > 1 ? 's' : ''; ?> trouvé<?= $totalItems > 1 ? 's' : ''; ?>
</div>
<?php if (count($results) > 0): ?>
<div class="columns is-multiline">
<?php foreach ($results as $item): ?>
<div class="column is-one-fifth">
<a href="memoire.php?id=<?= $item['id']; ?>" class="card-link">
<div class="card">
<?php
// Get cover image from thesis_files if available
$coverImage = null;
if (!empty($item['id'])) {
$files = $db->getThesisFiles($item['id']);
foreach ($files as $file) {
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif']) && $file['file_type'] === 'main') {
$coverImage = $file['file_path'];
break;
}
}
}
?>
<?php if ($coverImage): ?>
<div class="card-image">
<figure class="image">
<img src="<?= htmlspecialchars($coverImage); ?>" alt="Image preview">
</figure>
</div>
<?php endif; ?>
<div class="card-content">
<h4 class="title is-4">
<?= htmlspecialchars($item['title']); ?>
</h4>
<h2 class="subtitle">
<?= htmlspecialchars($item['authors'] ?? 'Auteur inconnu'); ?>
</h2>
<h3 class="tag title is-6 is-link is-light">
<?= htmlspecialchars($item['year']); ?>
</h3>
<p class="block content">
<?php
$excerpt_length = 150;
$synopsis = $item['synopsis'] ?? '';
$description_excerpt = strlen($synopsis) > $excerpt_length
? substr($synopsis, 0, $excerpt_length) . '...'
: $synopsis;
?>
<?= htmlspecialchars($description_excerpt); ?>
</p>
</div>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<?php if ($page > 1): ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page - 1])); ?>" class="pagination-previous">Précédent</a>
<?php endif; ?>
<?php if ($page < $totalPages): ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page + 1])); ?>" class="pagination-next">Suivant</a>
<?php endif; ?>
<ul class="pagination-list">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<li>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $i])); ?>"
class="pagination-link <?= $i === $page ? 'is-current' : ''; ?>">
<?= $i; ?>
</a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>
<?php else: ?>
<div class="notification">
Utilisez le formulaire ci-dessus pour rechercher des mémoires.
</div>
<?php endif; ?>
</div>
</section>
<?php include 'inc/footer.php'; ?>

View File

@@ -1,40 +0,0 @@
<?php
// Simple test script to verify database connection and queries
require_once __DIR__ . '/../../shared/Database.php';
try {
$db = Database::getInstance();
echo "✓ Database connection successful\n";
// Test counting theses
$count = $db->countPublishedTheses();
echo "✓ Found {$count} published theses\n";
// Test getting theses
$theses = $db->getPublishedTheses(5, 0);
echo "✓ Retrieved " . count($theses) . " theses\n";
if (count($theses) > 0) {
$first = $theses[0];
echo "\nFirst thesis:\n";
echo " ID: " . $first['id'] . "\n";
echo " Title: " . $first['title'] . "\n";
echo " Author(s): " . ($first['authors'] ?? 'N/A') . "\n";
echo " Year: " . $first['year'] . "\n";
// Test getting single thesis
$thesis = $db->getThesisById($first['id']);
if ($thesis) {
echo "✓ Successfully retrieved thesis details\n";
echo " Orientation: " . ($thesis['orientation'] ?? 'N/A') . "\n";
echo " AP Program: " . ($thesis['ap_program'] ?? 'N/A') . "\n";
echo " Files: " . (count($thesis['files'] ?? [])) . "\n";
}
}
echo "\n✓ All tests passed!\n";
} catch (Exception $e) {
echo "✗ Error: " . $e->getMessage() . "\n";
exit(1);
}

View File

@@ -1,121 +0,0 @@
<?php
/**
* Test script for search functionality
* Run this to verify that search methods work correctly
*/
require_once __DIR__ . '/../../../../shared/Database.php';
echo "=== Testing Search Feature ===\n\n";
try {
$db = Database::getInstance();
// Test 1: Get all published theses
echo "Test 1: Getting all published theses\n";
$allTheses = $db->searchTheses([], 100, 0);
echo "Found " . count($allTheses) . " published theses\n";
foreach ($allTheses as $thesis) {
echo " - [{$thesis['year']}] {$thesis['title']} by {$thesis['authors']}\n";
}
echo "\n";
// Test 2: Full-text search
echo "Test 2: Full-text search for 'urbain'\n";
$results = $db->searchTheses(['query' => 'urbain']);
echo "Found " . count($results) . " results\n";
foreach ($results as $thesis) {
echo " - {$thesis['title']}\n";
}
echo "\n";
// Test 3: Search by year
echo "Test 3: Search by year (2024)\n";
$results = $db->searchTheses(['year' => 2024]);
echo "Found " . count($results) . " results\n";
foreach ($results as $thesis) {
echo " - [{$thesis['year']}] {$thesis['title']}\n";
}
echo "\n";
// Test 4: Search by orientation
echo "Test 4: Search by orientation (Installation-Performance)\n";
$results = $db->searchTheses(['orientation' => 'Installation-Performance']);
echo "Found " . count($results) . " results\n";
foreach ($results as $thesis) {
echo " - {$thesis['title']} ({$thesis['orientation']})\n";
}
echo "\n";
// Test 5: Search by AP program
echo "Test 5: Search by AP program (Narration Spéculative)\n";
$results = $db->searchTheses(['ap_program' => 'Narration Spéculative']);
echo "Found " . count($results) . " results\n";
foreach ($results as $thesis) {
echo " - {$thesis['title']} ({$thesis['ap_program']})\n";
}
echo "\n";
// Test 6: Search by keyword
echo "Test 6: Search by keyword (performance)\n";
$results = $db->searchTheses(['keyword' => 'performance']);
echo "Found " . count($results) . " results\n";
foreach ($results as $thesis) {
echo " - {$thesis['title']}\n";
echo " Keywords: {$thesis['keywords']}\n";
}
echo "\n";
// Test 7: Combined search
echo "Test 7: Combined search (query='performance' + year=2024)\n";
$results = $db->searchTheses(['query' => 'performance', 'year' => 2024]);
echo "Found " . count($results) . " results\n";
foreach ($results as $thesis) {
echo " - [{$thesis['year']}] {$thesis['title']}\n";
}
echo "\n";
// Test 8: Get available years
echo "Test 8: Getting available years\n";
$years = $db->getAvailableYears();
echo "Available years: " . implode(', ', $years) . "\n\n";
// Test 9: Get orientations
echo "Test 9: Getting orientations\n";
$orientations = $db->getOrientations();
echo "Total orientations: " . count($orientations) . "\n";
echo "Sample: " . $orientations[0]['name'] . ", " . $orientations[1]['name'] . ", ...\n\n";
// Test 10: Get keywords
echo "Test 10: Getting used keywords\n";
$keywords = $db->getUsedKeywords();
echo "Total keywords in use: " . count($keywords) . "\n";
$keywordNames = array_map(function($k) { return $k['keyword']; }, $keywords);
echo "Keywords: " . implode(', ', array_slice($keywordNames, 0, 10)) . "...\n\n";
// Test 11: Count results
echo "Test 11: Count search results\n";
$count = $db->countSearchResults(['year' => 2024]);
echo "Count for year 2024: $count\n\n";
// Test 12: Pagination
echo "Test 12: Testing pagination\n";
$page1 = $db->searchTheses([], 2, 0); // First 2 results
$page2 = $db->searchTheses([], 2, 2); // Next 2 results
echo "Page 1 (first 2):\n";
foreach ($page1 as $thesis) {
echo " - {$thesis['title']}\n";
}
echo "Page 2 (next 2):\n";
foreach ($page2 as $thesis) {
echo " - {$thesis['title']}\n";
}
echo "\n";
echo "✅ All tests completed successfully!\n";
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

View File

@@ -1,306 +0,0 @@
# Test Migration Summary
## ✅ Tests Reorganized Following PHP Standards
The test files have been reorganized to follow PHP testing best practices.
---
## What Changed
### Before (Non-Standard)
```
front-backend/
├── test_search.php ❌ Tests in root
├── test_security.php ❌ Would deploy to production
├── test_security_updated.php ❌ No organization
├── test_rate_limit.php ❌ Mixed with application code
├── create_test_db.php ❌ Test fixtures in root
├── Database_secure.php ❌ Duplicate code
├── Database.php ✓ Application code
└── RateLimit.php ✓ Application code
```
### After (Standard)
```
front-backend/
├── tests/ ✅ Dedicated test directory
│ ├── Fixtures/ ✅ Test data & setup
│ │ └── CreateTestDatabase.php
│ ├── Integration/ ✅ Multi-component tests
│ │ └── SearchTest.php
│ ├── Security/ ✅ Security validation
│ │ └── SecurityTest.php
│ ├── Unit/ ✅ Individual component tests
│ │ └── RateLimitTest.php
│ └── README.md ✅ Test documentation
├── run-tests.php ✅ Convenient test runner
├── .gitignore ✅ Excludes cache, logs, etc.
├── Database.php ✓ Application code
└── RateLimit.php ✓ Application code
```
---
## Benefits Achieved
### ✅ Production Safety
- **Tests excluded from deployment** via `justfile`
- **No test code in production** - cleaner, more secure
- **Smaller deployment size** - only application code deployed
### ✅ Better Organization
- **Clear separation** - tests vs application code
- **Logical grouping** - unit, integration, security, fixtures
- **Standard structure** - other PHP developers will understand immediately
### ✅ Easier Testing
- **Single command** - `php run-tests.php` runs everything
- **Individual tests** - `php tests/Security/SecurityTest.php` for specific tests
- **Better output** - formatted test results with summary
### ✅ Future-Ready
- **PHPUnit compatible** - directory structure ready for migration
- **CI/CD ready** - easy to integrate with GitHub Actions, etc.
- **Scalable** - easy to add new tests in proper categories
---
## Running Tests
### Run All Tests
```bash
cd /home/padlock/dev/posterg-website/front-backend
php run-tests.php
```
**Output:**
```
╔════════════════════════════════════════════╗
║ Running Front-Backend Tests ║
╚════════════════════════════════════════════╝
┌─────────────────────────────────────────┐
│ Test Suite: Fixtures │
└─────────────────────────────────────────┘
✅ PASSED
┌─────────────────────────────────────────┐
│ Test Suite: Integration │
└─────────────────────────────────────────┘
✅ PASSED
┌─────────────────────────────────────────┐
│ Test Suite: Security │
└─────────────────────────────────────────┘
✅ PASSED
┌─────────────────────────────────────────┐
│ Test Suite: Unit │
└─────────────────────────────────────────┘
✅ PASSED
╔════════════════════════════════════════════╗
║ Test Summary ║
╠════════════════════════════════════════════╣
║ Total: 4 ║
║ Passed: 4 ✅ ║
║ Failed: 0 ║
╚════════════════════════════════════════════╝
✅ All tests passed!
```
### Run Individual Tests
```bash
# Setup test database
php tests/Fixtures/CreateTestDatabase.php
# Run specific test suite
php tests/Integration/SearchTest.php
php tests/Security/SecurityTest.php
php tests/Unit/RateLimitTest.php
```
---
## Deployment Configuration
### Updated `justfile`
The deployment now excludes test files:
```just
[group('deploy')]
deploy:
rsync -vur --progress \
--exclude '*.db' \
--exclude 'tests/' \
--exclude 'cache/' \
--exclude '*.md' \
--exclude 'run-tests.php' \
./front-backend/ posterg:/var/www/html/
```
**What's Excluded:**
- `tests/` - All test files
- `*.db` - Test databases
- `cache/` - Runtime cache (rate limiting)
- `*.md` - Documentation files
- `run-tests.php` - Test runner
**What's Deployed:**
- Application code (`.php` files)
- Assets (`assets/` directory)
- Templates (`inc/` directory)
- Public pages (`index.php`, `search.php`, etc.)
### New `.gitignore`
```gitignore
/vendor/
/cache/
*.db
*.log
.env
.env.local
```
---
## Test Organization Explained
### 1. Fixtures (`tests/Fixtures/`)
**Purpose:** Test data setup and database initialization
**Files:**
- `CreateTestDatabase.php` - Creates test.db with sample theses
**When to run:** Before running other tests
### 2. Integration Tests (`tests/Integration/`)
**Purpose:** Test multiple components working together
**Files:**
- `SearchTest.php` - Full search functionality with filters
**What it tests:**
- Full-text search
- Year filtering
- Orientation filtering
- AP program filtering
- Keyword search
- Combined filters
- Pagination
### 3. Security Tests (`tests/Security/`)
**Purpose:** Verify security measures are working
**Files:**
- `SecurityTest.php` - All security validations
**What it tests:**
- Wildcard injection prevention
- Input length validation (max 200 chars)
- Year range validation (1900-2100)
- SQL injection prevention
- Pagination limits (max 100/page)
### 4. Unit Tests (`tests/Unit/`)
**Purpose:** Test individual components in isolation
**Files:**
- `RateLimitTest.php` - Rate limiting functionality
**What it tests:**
- Request tracking
- Limit enforcement (5 requests in test, 30 in production)
- Reset time calculation
- Header generation
---
## Comparison with Professional Projects
| Aspect | This Project | Laravel/Symfony | Status |
|--------|--------------|-----------------|--------|
| Test directory | `tests/` | `tests/` | ✅ Match |
| Test organization | Unit/Integration/Security | Unit/Feature | ✅ Good |
| Test framework | PHP scripts | PHPUnit | ⚠️ Can migrate |
| Deployment exclusion | Via rsync | Via .deployignore | ✅ Works |
| Runner | Custom script | `composer test` | ⚠️ Can improve |
| CI/CD | Manual | GitHub Actions | ⚠️ Future |
**Current Status:** Following PHP conventions, ready for growth
**Future Migration Path:** Can easily migrate to PHPUnit when needed
---
## Next Steps (Optional)
### For Small Projects (Current Approach is Fine)
- ✅ Keep using simple PHP test scripts
- ✅ Run `php run-tests.php` before deploying
- ✅ Tests are properly organized and excluded
### To Upgrade to PHPUnit (When Project Grows)
1. **Install PHPUnit:**
```bash
composer require --dev phpunit/phpunit
```
2. **Convert tests to PHPUnit format:**
```php
// Instead of:
echo "Test result: " . ($result ? "✅" : "❌") . "\n";
// Use:
$this->assertTrue($result);
```
3. **Add `phpunit.xml` configuration**
4. **Run with:** `composer test`
See `TESTING_BEST_PRACTICES.md` for complete migration guide.
---
## Files Created/Modified
### New Files
- ✅ `tests/` directory structure
- ✅ `tests/README.md` - Test documentation
- ✅ `run-tests.php` - Test runner script
- ✅ `.gitignore` - Git exclusions
### Moved Files
- ✅ `test_search.php` → `tests/Integration/SearchTest.php`
- ✅ `test_security_updated.php` → `tests/Security/SecurityTest.php`
- ✅ `test_rate_limit.php` → `tests/Unit/RateLimitTest.php`
- ✅ `create_test_db.php` → `tests/Fixtures/CreateTestDatabase.php`
### Updated Files
- ✅ All test files (updated `require_once` paths)
- ✅ `justfile` (added test exclusions)
### Removed Files
- ✅ `test_security.php` (obsolete, replaced by SecurityTest.php)
- ✅ `Database_secure.php` (obsolete, functionality in Database.php)
---
## Summary
**Organized** - Tests follow PHP conventions
**Secure** - Tests excluded from production
**Convenient** - Single command to run all tests
**Documented** - README explains structure
**Scalable** - Easy to add new tests
**Future-ready** - Can migrate to PHPUnit later
**All tests passing:** 4/4 ✅
**Ready for production deployment!**

View File

@@ -1,108 +0,0 @@
# Tests Directory
This directory contains all tests for the front-backend application.
## Structure
```
tests/
├── Fixtures/ # Test data and setup scripts
│ └── CreateTestDatabase.php
├── Integration/ # Integration tests (multiple components)
│ └── SearchTest.php
├── Security/ # Security-focused tests
│ └── SecurityTest.php
└── Unit/ # Unit tests (individual methods)
└── RateLimitTest.php
```
## Running Tests
### Run All Tests
```bash
php run-tests.php
```
### Run Individual Tests
```bash
# Setup test database first
php tests/Fixtures/CreateTestDatabase.php
# Run specific test
php tests/Integration/SearchTest.php
php tests/Security/SecurityTest.php
php tests/Unit/RateLimitTest.php
```
## Test Suites
### Fixtures
Test data setup and database initialization.
**CreateTestDatabase.php**
- Creates test.db with sample theses
- Populates with 6 sample records
- Includes authors, supervisors, keywords
### Integration Tests
Test multiple components working together.
**SearchTest.php**
- Tests full search functionality
- Tests filtering (year, orientation, AP, keywords)
- Tests pagination
- Tests combined filters
### Security Tests
Verify security measures are working.
**SecurityTest.php**
- Wildcard injection prevention
- Input length validation
- Year range validation
- SQL injection prevention
- Pagination limits
### Unit Tests
Test individual components in isolation.
**RateLimitTest.php**
- Rate limit enforcement
- Request tracking
- Reset time calculation
- Header generation
## Expected Results
All tests should pass:
```
✅ PASSED - Fixtures/CreateTestDatabase.php
✅ PASSED - Integration/SearchTest.php
✅ PASSED - Security/SecurityTest.php
✅ PASSED - Unit/RateLimitTest.php
```
## Deployment
**Tests are NOT deployed to production.**
The deployment configuration (`justfile`) excludes:
- `tests/` directory
- `*.db` files
- Cache directory
- Documentation files
## Future Migration to PHPUnit
This directory structure is compatible with PHPUnit. To migrate:
1. Install PHPUnit:
```bash
composer require --dev phpunit/phpunit
```
2. Convert test files to PHPUnit format
3. Add `phpunit.xml` configuration
4. Run with: `composer test`
See `TESTING_BEST_PRACTICES.md` for details.

View File

@@ -1,119 +0,0 @@
<?php
/**
* Security test script for updated secure implementation
* Verifies that security fixes are working correctly
*/
require_once __DIR__ . '/../../../../shared/Database.php';
echo "=== Security Testing (Secure Implementation) ===\n\n";
try {
$db = Database::getInstance();
// Test 1: Wildcard injection (should now be escaped)
echo "Test 1: Wildcard Injection (Secure Implementation)\n";
echo "Searching for '%' (wildcards should be escaped):\n";
$results = $db->searchTheses(['query' => '%'], 10, 0);
echo "Results found: " . count($results) . "\n";
if (count($results) === 0 || count($results) < 6) {
echo "✅ SECURE: Wildcard characters are escaped!\n";
} else {
echo "❌ VULNERABLE: Still matching everything!\n";
}
echo "\n";
// Test 2: Underscore wildcard
echo "Test 2: Underscore Wildcard (should be escaped)\n";
$results = $db->searchTheses(['query' => '_'], 10, 0);
echo "Searching for '_': " . count($results) . " results\n";
if (count($results) === 0 || count($results) < 6) {
echo "✅ SECURE: Underscore wildcard is escaped!\n";
} else {
echo "❌ VULNERABLE: Underscore matches everything!\n";
}
echo "\n";
// Test 3: Long input validation
echo "Test 3: Long Input String Validation\n";
$longString = str_repeat('test', 1000); // 4000 characters
echo "Attempting to search for " . strlen($longString) . " character string\n";
try {
$results = $db->searchTheses(['query' => $longString], 10, 0);
echo "❌ VULNERABLE: Long input was accepted!\n";
} catch (InvalidArgumentException $e) {
echo "✅ SECURE: Long input rejected: " . $e->getMessage() . "\n";
}
echo "\n";
// Test 4: Invalid year validation
echo "Test 4: Invalid Year Validation\n";
try {
$results = $db->searchTheses(['year' => 999999], 10, 0);
echo "❌ VULNERABLE: Invalid year accepted!\n";
} catch (InvalidArgumentException $e) {
echo "✅ SECURE: Invalid year rejected: " . $e->getMessage() . "\n";
}
echo "\n";
// Test 5: SQL Injection still prevented
echo "Test 5: SQL Injection Prevention\n";
$injectionTests = [
"' OR 1=1--",
"'; DROP TABLE theses;--",
];
foreach ($injectionTests as $injection) {
echo "Testing: $injection\n";
try {
$results = $db->searchTheses(['query' => $injection], 10, 0);
echo " Results: " . count($results) . " (treated as literal string)\n";
echo " ✅ SAFE: SQL injection prevented\n";
} catch (Exception $e) {
echo " Error: " . $e->getMessage() . "\n";
}
}
echo "\n";
// Test 6: Pagination limits
echo "Test 6: Pagination Limits\n";
$results = $db->searchTheses([], 500, 0); // Try to get 500 results
echo "Requested 500 results, got: " . count($results) . "\n";
if (count($results) <= 100) {
echo "✅ SECURE: Pagination limited to max 100 results\n";
} else {
echo "❌ VULNERABLE: Pagination allows too many results\n";
}
echo "\n";
// Test 7: Negative offset
echo "Test 7: Negative Offset Protection\n";
$results = $db->searchTheses([], 10, -100);
echo "Requested offset -100, query succeeded: " . (count($results) >= 0 ? 'yes' : 'no') . "\n";
echo "✅ SECURE: Negative offsets handled safely\n\n";
// Test 8: Normal search still works
echo "Test 8: Normal Search Functionality\n";
$results = $db->searchTheses(['query' => 'urbain'], 10, 0);
echo "Searching for 'urbain': " . count($results) . " results\n";
if (count($results) > 0) {
echo " Found: " . $results[0]['title'] . "\n";
}
echo "✅ Normal searches still work correctly\n\n";
// Summary
echo "=== SECURITY SUMMARY ===\n\n";
echo "✅ SECURE from SQL Injection (prepared statements)\n";
echo "✅ SECURE from wildcard injection (escaped)\n";
echo "✅ SECURE from DoS via long inputs (length validation)\n";
echo "✅ SECURE from invalid year values (range validation)\n";
echo "✅ SECURE from excessive pagination (max 100 per page)\n";
echo "✅ SECURE from negative offsets (validated)\n\n";
echo "✅ ALL SECURITY TESTS PASSED!\n";
echo "The implementation is production-ready.\n";
} catch (Exception $e) {
echo "❌ Unexpected error: " . $e->getMessage() . "\n";
exit(1);
}

View File

@@ -1,58 +0,0 @@
<?php
/**
* Test rate limiting functionality
*/
require_once __DIR__ . '/../../../../shared/RateLimit.php';
echo "=== Testing Rate Limiting ===\n\n";
// Create rate limiter: 5 requests per 10 seconds (for testing)
$rateLimit = new RateLimit(5, 10);
echo "Configuration: 5 requests per 10 seconds\n\n";
// Test 1: Make 5 requests (should all succeed)
echo "Test 1: Making 5 requests (should all succeed)\n";
for ($i = 1; $i <= 5; $i++) {
$allowed = $rateLimit->check();
echo "Request $i: " . ($allowed ? "✅ Allowed" : "❌ Blocked") . "\n";
echo " Remaining: " . $rateLimit->getRemaining() . "\n";
}
echo "\n";
// Test 2: Make 6th request (should be blocked)
echo "Test 2: Making 6th request (should be blocked)\n";
$allowed = $rateLimit->check();
echo "Request 6: " . ($allowed ? "❌ Allowed (FAIL)" : "✅ Blocked (SUCCESS)") . "\n";
echo "Remaining: " . $rateLimit->getRemaining() . "\n";
echo "Reset time: " . $rateLimit->getResetTime() . " seconds\n\n";
// Test 3: Wait and try again
echo "Test 3: Waiting 3 seconds and trying again...\n";
sleep(3);
$allowed = $rateLimit->check();
echo "Request after 3s: " . ($allowed ? "❌ Allowed (still in window)" : "✅ Blocked") . "\n";
echo "Remaining: " . $rateLimit->getRemaining() . "\n\n";
// Test 4: Test headers (CLI simulation)
echo "Test 4: Rate limit headers (simulated)\n";
echo "X-RateLimit-Limit: 5\n";
echo "X-RateLimit-Remaining: " . $rateLimit->getRemaining() . "\n";
echo "X-RateLimit-Reset: " . (time() + $rateLimit->getResetTime()) . "\n";
echo "\n";
// Test 5: Cleanup
echo "Test 5: Testing cleanup function\n";
$rateLimit->cleanup();
echo "✅ Cleanup executed successfully\n\n";
echo "=== RATE LIMITING SUMMARY ===\n\n";
echo "✅ Rate limiting works correctly\n";
echo "✅ Requests are tracked per client\n";
echo "✅ Limits are enforced\n";
echo "✅ Reset time is calculated\n";
echo "✅ Headers are sent\n";
echo "✅ Cleanup removes old files\n\n";
echo "Ready for production use!\n";