Files
xamxam/docs/assessments.md
Théophile Gervreau-Mercier 2cb5436647 Added Claude assessements
2026-02-02 18:56:12 +01:00

1099 lines
29 KiB
Markdown

# POSTERG Project Assessment & Migration Plan
*A practical guide to improving the ERG thesis archive system*
---
## Project Overview
POSTERG is a student-led initiative to democratize access to ERG masters theses. Unlike traditional library systems that limit visibility based on grades (only theses scoring 70%+ are displayed), POSTERG aims to make all theses accessible regardless of academic scoring. The project aligns with libre/open-source values, questioning institutional power structures around knowledge dissemination.
**Current Implementation:**
- Two PHP repositories: submission form + public website
- YAML files for metadata storage (13 theses currently)
- File-based architecture with no database
- Simple, student-hackable codebase (~567 lines of PHP)
- Uses Composer for dependencies (Symfony YAML parser)
---
## Migration Summary: YAML → SQLite
### Why Migrate?
**Current System Limitations:**
- Loads ALL YAML files on every single page request
- No search, filtering, or advanced browsing
- Performance degrades linearly (unusable beyond ~100 theses)
- No data validation or integrity checks
- Difficult to implement new features
**SQLite Benefits:**
- Fast queries with indexes (10-50x performance improvement)
- Built-in search and filtering capabilities
- Scalable to 10,000+ theses
- Single-file database (no server configuration needed)
- Still simple and student-friendly
- Works with cheap shared hosting
### What Stays the Same
- File storage structure unchanged (same directories)
- PHP codebase (no framework required)
- Simple.css and Bulma for styling
- Minimal JavaScript approach
- Student-hackable architecture
- No external dependencies beyond SQLite
---
## Proposed Data Structure
### Database Schema
**theses table** (core metadata)
```
id → INTEGER PRIMARY KEY AUTOINCREMENT
author → TEXT NOT NULL
year → INTEGER NOT NULL
email → TEXT
title → TEXT NOT NULL
supervisor → TEXT
problem_statement → TEXT
description → TEXT NOT NULL
orientation → TEXT
ap → TEXT (atelier pratique)
cover_path → TEXT
external_link → TEXT
created_at → DATETIME
updated_at → DATETIME
```
**tags table** (normalized tag storage)
```
id → INTEGER PRIMARY KEY AUTOINCREMENT
name → TEXT UNIQUE NOT NULL
```
**thesis_tags table** (many-to-many relationship)
```
thesis_id → INTEGER (foreign key to theses.id)
tag_id → INTEGER (foreign key to tags.id)
PRIMARY KEY (thesis_id, tag_id)
```
**files table** (associated content files)
```
id → INTEGER PRIMARY KEY AUTOINCREMENT
thesis_id → INTEGER (foreign key to theses.id)
file_path → TEXT NOT NULL
file_type → TEXT (pdf, image, video, archive)
mime_type → TEXT
file_size → INTEGER
uploaded_at → DATETIME
```
### Performance Indexes
```sql
-- Speed up common queries
CREATE INDEX idx_year ON theses(year DESC);
CREATE INDEX idx_author ON theses(author);
CREATE INDEX idx_orientation ON theses(orientation);
CREATE INDEX idx_tag_name ON tags(name);
-- Full-text search (optional, for advanced search)
CREATE VIRTUAL TABLE search_index USING fts5(
title, description, content=theses
);
```
---
## Improvements Over Current Method
### 1. Performance
**Before (YAML):**
- Load time: ~50ms for 13 theses
- Projected: ~400ms for 100 theses (slow)
- Projected: ~4000ms for 1000 theses (unusable)
**After (SQLite):**
- Load time: ~5ms for 13 theses
- Projected: ~8ms for 100 theses
- Projected: ~15ms for 1000 theses
- Projected: ~30ms for 10,000 theses
### 2. New Capabilities
**Search & Discovery:**
- Full-text search across titles and descriptions
- Filter by year, program, supervisor, tags
- Tag cloud visualization
- "Related theses" suggestions
- Alphabetical author index
**Data Quality:**
- Required field validation
- Duplicate detection
- Email format validation
- Year range checking
- Automatic tag normalization
**Administrative:**
- Edit metadata after submission
- Merge duplicate tags
- View submission statistics
- Export data (CSV, YAML backup)
### 3. Usability
**For Students Submitting:**
- Tag suggestions from existing tags
- Duplicate title warnings
- Instant validation feedback
- Preview before submission
**For Visitors:**
- Fast browsing experience
- Discoverable content via search
- Multiple navigation paths (year, tag, author)
- Related thesis recommendations
**For Future Developers:**
- Clear database structure
- Easy to understand SQL queries
- No complex framework magic
- Well-commented code
---
## Overall Project Assessment
### Strengths
**Philosophy & Mission:**
- Strong ethical foundation (democratizing access)
- Challenges institutional hierarchies
- Aligns with libre software values
- Student-empowered initiative
**Technical Approach:**
- Appropriately simple for the scale
- No over-engineering
- Easy for students to understand and modify
- Uses standard, well-documented tools
**User Experience:**
- Clean, readable interface
- Accessible forms
- Works without JavaScript
- Progressive enhancement possible
### Current Issues
**Code Quality:**
- Missing input validation in critical places
- No error handling for edge cases
- Security concerns (detailed below)
- Inconsistent variable naming
- Limited code comments
**Architecture:**
- No separation of concerns (business logic mixed with presentation)
- Duplicate code between repositories
- No shared configuration file
- Hard-coded paths
**Documentation:**
- README exists but minimal
- No code comments
- No deployment guide
- No backup/recovery procedures
**Security:**
- SQL injection vulnerabilities if migrating without prepared statements
- Path traversal risks in file handling
- No CSRF protection on forms
- Uploaded files not fully validated
- Email addresses exposed publicly
---
## Recommendations for Improvement
### Priority 1: Critical Security Fixes
**Input Validation & Sanitization:**
```php
// Current: Unsafe
$auteurice = filter_var($_POST["auteurice"], FILTER_SANITIZE_STRING);
// Better: Validate and escape
$auteurice = trim($_POST["auteurice"] ?? '');
if (strlen($auteurice) < 2 || strlen($auteurice) > 100) {
die("Invalid author name");
}
$auteurice = htmlspecialchars($auteurice, ENT_QUOTES, 'UTF-8');
```
**Prepared Statements (for SQLite migration):**
```php
// NEVER do this
$sql = "SELECT * FROM theses WHERE author = '$author'"; // DANGEROUS!
// ALWAYS use prepared statements
$stmt = $db->prepare("SELECT * FROM theses WHERE author = ?");
$stmt->execute([$author]);
```
**File Upload Security:**
```php
// Validate file types by content, not just extension
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpFile);
$allowed = ['application/pdf', 'image/jpeg', 'image/png'];
if (!in_array($mime, $allowed)) {
die("Invalid file type");
}
// Generate random filenames (prevent path traversal)
$newName = bin2hex(random_bytes(16)) . '.' . $extension;
// Store outside web root if possible
$uploadDir = __DIR__ . '/../data/uploads/';
```
**CSRF Protection:**
```php
// In form:
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
echo '<input type="hidden" name="csrf_token" value="' . $token . '">';
// In processing:
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die("Invalid request");
}
```
### Priority 2: Database Migration
**Migration Script Structure:**
```php
// migrate.php
require 'vendor/autoload.php';
// Create database connection
$db = new PDO('sqlite:data/posterg.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create schema
$schema = file_get_contents('schema.sql');
$db->exec($schema);
// Migrate YAML files
$yamlFiles = glob('data/yaml/*.yaml');
foreach ($yamlFiles as $file) {
$data = Yaml::parseFile($file);
// Insert thesis
$stmt = $db->prepare("
INSERT INTO theses (author, year, title, description, ...)
VALUES (?, ?, ?, ?, ...)
");
$stmt->execute([
$data['auteurice'],
$data['année'],
$data['titre'],
$data['description'],
// ... other fields
]);
$thesisId = $db->lastInsertId();
// Insert tags
foreach ($data['tag'] as $tagName) {
// Insert tag if doesn't exist
$db->exec("INSERT OR IGNORE INTO tags (name) VALUES ('$tagName')");
$tagId = $db->query("SELECT id FROM tags WHERE name = '$tagName'")->fetch()[0];
// Link thesis to tag
$db->exec("INSERT INTO thesis_tags VALUES ($thesisId, $tagId)");
}
}
echo "Migration complete!\n";
```
**Database Helper Class:**
```php
// db.php - Simple database wrapper
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
$this->pdo = new PDO('sqlite:' . __DIR__ . '/data/posterg.db');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function lastInsertId() {
return $this->pdo->lastInsertId();
}
}
// Usage:
$db = Database::getInstance();
$theses = $db->query("SELECT * FROM theses WHERE year = ?", [2024])->fetchAll();
```
### Priority 3: Code Organization
**Recommended File Structure:**
```
posterg-website/
├── config.php ← Shared configuration
├── db.php ← Database helper
├── functions.php ← Reusable functions
├── index.php ← Gallery view
├── thesis.php ← Detail view (renamed from memoire.php)
├── search.php ← Search interface
├── about.php ← About page (renamed from apropos.php)
├── inc/
│ ├── header.php
│ └── footer.php
├── assets/
│ ├── css/
│ └── img/
└── data/
├── posterg.db ← SQLite database
├── content/ ← Uploaded files
└── backups/ ← Database backups
posterg-formulaire/
├── config.php ← Same config as website
├── db.php ← Same database helper
├── functions.php ← Shared functions
├── index.php ← Form display
├── submit.php ← Form processing (renamed from formulaire.php)
└── data/ ← Symlink to website/data
```
**Shared Configuration:**
```php
// config.php (same file in both repos)
define('DB_PATH', __DIR__ . '/data/posterg.db');
define('UPLOAD_DIR', __DIR__ . '/data/content/');
define('COVER_DIR', __DIR__ . '/data/covers/');
define('MAX_FILE_SIZE', 50 * 1024 * 1024); // 50MB
define('ITEMS_PER_PAGE', 20);
// Allowed file types
const ALLOWED_MIMES = [
'application/pdf',
'image/jpeg',
'image/png',
'video/mp4',
'application/zip'
];
// Current year for validation
define('CURRENT_YEAR', (int)date('Y'));
```
**Reusable Functions:**
```php
// functions.php
function sanitize_input($data) {
return htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8');
}
function validate_year($year) {
return is_numeric($year) && $year >= 1950 && $year <= CURRENT_YEAR + 1;
}
function validate_email($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
function format_file_size($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$i = floor(log($bytes, 1024));
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
}
function get_file_type_icon($mime) {
$icons = [
'application/pdf' => '📄',
'image/jpeg' => '🖼️',
'video/mp4' => '🎬',
'application/zip' => '📦'
];
return $icons[$mime] ?? '📎';
}
```
### Priority 4: Feature Enhancements
**1. Search Interface (search.php):**
```php
<?php
require 'db.php';
$db = Database::getInstance();
$query = $_GET['q'] ?? '';
$year = $_GET['year'] ?? '';
$orientation = $_GET['orientation'] ?? '';
$tag = $_GET['tag'] ?? '';
// Build search query
$sql = "SELECT DISTINCT t.* FROM theses t
LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
LEFT JOIN tags tg ON tt.tag_id = tg.id
WHERE 1=1";
$params = [];
if ($query) {
$sql .= " AND (t.title LIKE ? OR t.description LIKE ?)";
$params[] = "%$query%";
$params[] = "%$query%";
}
if ($year) {
$sql .= " AND t.year = ?";
$params[] = $year;
}
if ($orientation) {
$sql .= " AND t.orientation = ?";
$params[] = $orientation;
}
if ($tag) {
$sql .= " AND tg.name = ?";
$params[] = $tag;
}
$sql .= " ORDER BY t.year DESC, t.title ASC";
$results = $db->query($sql, $params)->fetchAll();
?>
```
**2. Tag Cloud:**
```php
<?php
// Get tags with counts
$tags = $db->query("
SELECT t.name, COUNT(*) as count
FROM tags t
JOIN thesis_tags tt ON t.id = tt.tag_id
GROUP BY t.id
ORDER BY count DESC, t.name ASC
LIMIT 50
")->fetchAll();
// Display with size based on frequency
foreach ($tags as $tag) {
$size = min(2, 0.8 + ($tag['count'] / 10));
echo "<a href='search.php?tag={$tag['name']}'
style='font-size: {$size}em; margin: 0.3em;'>
{$tag['name]} ({$tag['count']})
</a>";
}
?>
```
**3. CSV Export (admin/export.php):**
```php
<?php
// Simple CSV export for analysis
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="posterg_export.csv"');
$db = Database::getInstance();
$theses = $db->query("SELECT * FROM theses ORDER BY year DESC")->fetchAll();
$output = fopen('php://output', 'w');
fputcsv($output, ['ID', 'Author', 'Year', 'Title', 'Orientation', 'AP']);
foreach ($theses as $thesis) {
fputcsv($output, [
$thesis['id'],
$thesis['author'],
$thesis['year'],
$thesis['title'],
$thesis['orientation'],
$thesis['ap']
]);
}
?>
```
**4. CSV Import (admin/import.php):**
```php
<?php
// Import theses from CSV (useful for bulk additions)
if ($_FILES['csv']['error'] === UPLOAD_ERR_OK) {
$file = fopen($_FILES['csv']['tmp_name'], 'r');
$header = fgetcsv($file); // Skip header
$db = Database::getInstance();
while ($row = fgetcsv($file)) {
$stmt = $db->query("
INSERT INTO theses (author, year, title, orientation, ap, description)
VALUES (?, ?, ?, ?, ?, ?)
", $row);
}
echo "Import complete!";
}
?>
```
### Priority 5: User Experience
**Better Form Validation (with minimal JS):**
```html
<form action="submit.php" method="post" enctype="multipart/form-data">
<label for="author">Nom/Prénom/Pseudo</label>
<input type="text"
id="author"
name="author"
required
minlength="2"
maxlength="100"
pattern="[A-Za-zÀ-ÿ\s\-']+"
title="Lettres, espaces et tirets uniquement">
<label for="year">Année diplômante</label>
<input type="number"
id="year"
name="year"
required
min="2000"
max="2030"
value="<?= date('Y') ?>">
<label for="email">Contact</label>
<input type="email"
id="email"
name="email"
placeholder="nom@exemple.com">
<!-- File size preview (tiny bit of JS, progressive enhancement) -->
<label for="files">Fichiers du mémoire</label>
<input type="file"
id="files"
name="files[]"
multiple
accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip"
onchange="showFileInfo(this)">
<div id="file-info"></div>
<script>
// Optional, enhances UX but works without JS
function showFileInfo(input) {
let info = '';
for (let file of input.files) {
let size = (file.size / 1024 / 1024).toFixed(2);
info += file.name + ' (' + size + ' MB)\n';
}
document.getElementById('file-info').innerText = info;
}
</script>
</form>
```
**Pagination Component:**
```php
<?php
// functions.php
function render_pagination($current_page, $total_items, $per_page, $url) {
$total_pages = ceil($total_items / $per_page);
if ($total_pages <= 1) return;
echo '<nav class="pagination">';
// Previous
if ($current_page > 1) {
$prev = $current_page - 1;
echo "<a href='$url?page=$prev'>← Précédent</a>";
}
// Page numbers
for ($i = 1; $i <= $total_pages; $i++) {
if ($i == $current_page) {
echo "<strong>$i</strong>";
} else {
echo "<a href='$url?page=$i'>$i</a>";
}
}
// Next
if ($current_page < $total_pages) {
$next = $current_page + 1;
echo "<a href='$url?page=$next'>Suivant →</a>";
}
echo '</nav>';
}
?>
```
### Priority 6: Maintenance & Operations
**Automated Database Backup:**
```php
// cron/backup.php (run daily via cron)
<?php
$dbFile = __DIR__ . '/../data/posterg.db';
$backupDir = __DIR__ . '/../data/backups/';
$backupFile = $backupDir . 'posterg_' . date('Y-m-d_H-i-s') . '.db';
// Create backup directory if needed
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
}
// Copy database
copy($dbFile, $backupFile);
// Compress
exec("gzip $backupFile");
// Delete backups older than 30 days
$files = glob($backupDir . '*.gz');
foreach ($files as $file) {
if (time() - filemtime($file) > 30 * 24 * 3600) {
unlink($file);
}
}
echo "Backup created: " . basename($backupFile) . ".gz\n";
?>
```
**Database Maintenance:**
```php
// cron/maintain.php (run weekly)
<?php
$db = Database::getInstance();
// Vacuum database (reclaim space)
$db->query("VACUUM");
// Analyze for query optimization
$db->query("ANALYZE");
// Check integrity
$result = $db->query("PRAGMA integrity_check")->fetch();
if ($result[0] !== 'ok') {
error_log("Database integrity check failed!");
}
echo "Database maintenance complete\n";
?>
```
**Simple Admin Dashboard (admin/index.php):**
```php
<?php
require '../db.php';
$db = Database::getInstance();
// Statistics
$stats = [
'total_theses' => $db->query("SELECT COUNT(*) FROM theses")->fetchColumn(),
'total_tags' => $db->query("SELECT COUNT(*) FROM tags")->fetchColumn(),
'total_files' => $db->query("SELECT COUNT(*) FROM files")->fetchColumn(),
'recent_year' => $db->query("SELECT MAX(year) FROM theses")->fetchColumn(),
'oldest_year' => $db->query("SELECT MIN(year) FROM theses")->fetchColumn(),
];
// Theses per year
$per_year = $db->query("
SELECT year, COUNT(*) as count
FROM theses
GROUP BY year
ORDER BY year DESC
")->fetchAll();
// Most used tags
$popular_tags = $db->query("
SELECT t.name, COUNT(*) as count
FROM tags t
JOIN thesis_tags tt ON t.id = tt.tag_id
GROUP BY t.id
ORDER BY count DESC
LIMIT 10
")->fetchAll();
// Database size
$db_size = filesize(__DIR__ . '/../data/posterg.db');
$db_size_mb = round($db_size / 1024 / 1024, 2);
?>
<!DOCTYPE html>
<html>
<head>
<title>POSTERG Admin</title>
<link rel="stylesheet" href="../assets/normalize.css">
<link rel="stylesheet" href="../assets/simple.css">
</head>
<body>
<h1>POSTERG Dashboard</h1>
<section>
<h2>Statistiques</h2>
<ul>
<li>Total mémoires: <?= $stats['total_theses'] ?></li>
<li>Total tags: <?= $stats['total_tags'] ?></li>
<li>Total fichiers: <?= $stats['total_files'] ?></li>
<li>Années: <?= $stats['oldest_year'] ?> - <?= $stats['recent_year'] ?></li>
<li>Taille base de données: <?= $db_size_mb ?> MB</li>
</ul>
</section>
<section>
<h2>Mémoires par année</h2>
<table>
<tr><th>Année</th><th>Nombre</th></tr>
<?php foreach ($per_year as $row): ?>
<tr><td><?= $row['year'] ?></td><td><?= $row['count'] ?></td></tr>
<?php endforeach; ?>
</table>
</section>
<section>
<h2>Tags populaires</h2>
<ol>
<?php foreach ($popular_tags as $tag): ?>
<li><?= $tag['name'] ?> (<?= $tag['count'] ?>)</li>
<?php endforeach; ?>
</ol>
</section>
<section>
<h2>Actions</h2>
<ul>
<li><a href="export.php">Exporter CSV</a></li>
<li><a href="import.php">Importer CSV</a></li>
<li><a href="backup.php">Créer backup</a></li>
</ul>
</section>
</body>
</html>
```
### Priority 7: Documentation
**Comprehensive README.md:**
```markdown
# POSTERG - Mémoire post-ERG
Archive libre et démocratique des mémoires de l'ERG.
## Installation
### Prérequis
- PHP 7.4+
- SQLite3
- Composer
### Configuration
1. Cloner le dépôt
2. Installer les dépendances: `composer install`
3. Créer la base de données: `php setup/create_database.php`
4. Importer les données existantes: `php setup/migrate_yaml.php`
5. Configurer le serveur web (voir docs/deployment.md)
## Structure du projet
- `index.php` - Page d'accueil / galerie
- `thesis.php` - Page détail d'un mémoire
- `search.php` - Interface de recherche
- `db.php` - Couche d'accès base de données
- `data/` - Base de données et fichiers uploadés
## Développement
### Ajouter une fonctionnalité
1. Modifier le schema si nécessaire (`setup/schema.sql`)
2. Créer un script de migration (`setup/migrate_XXX.php`)
3. Implémenter la fonctionnalité
4. Tester avec `php -S localhost:8000`
5. Documenter dans ce README
### Contribuer
Ce projet appartient aux étudiant·e·s de l'ERG.
N'hésitez pas à le modifier, l'améliorer, le hacker.
```
**Inline Code Comments:**
```php
<?php
/**
* POSTERG Thesis Detail Page
*
* Displays a single thesis with all associated files and metadata.
* URL format: thesis.php?id=123
*
* @requires db.php Database connection
* @requires functions.php Helper functions
*/
require_once 'db.php';
require_once 'functions.php';
// Get thesis ID from URL (validate as integer)
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!$id) {
header('Location: index.php');
exit;
}
// Fetch thesis data
$db = Database::getInstance();
$thesis = $db->query("SELECT * FROM theses WHERE id = ?", [$id])->fetch();
if (!$thesis) {
header('HTTP/1.0 404 Not Found');
echo "Mémoire non trouvé";
exit;
}
// Fetch associated tags
$tags = $db->query("
SELECT t.name
FROM tags t
JOIN thesis_tags tt ON t.id = tt.tag_id
WHERE tt.thesis_id = ?
ORDER BY t.name
", [$id])->fetchAll(PDO::FETCH_COLUMN);
// Fetch associated files
$files = $db->query("
SELECT * FROM files
WHERE thesis_id = ?
ORDER BY file_type, uploaded_at
", [$id])->fetchAll();
// Include header template
include 'inc/header.php';
?>
<!-- Rest of the template -->
```
---
## Keeping It Student-Friendly
### Principles for Future Development
**1. No Framework Lock-In**
- Avoid Laravel, Symfony, or other large frameworks
- Keep dependencies minimal (just Composer + SQLite PDO)
- Everything should be understandable by reading PHP docs
**2. Hackability First**
- Clear file structure (each file does one thing)
- No magic methods or complex abstractions
- Direct SQL queries (no ORM complexity)
- Vanilla CSS with optional Bulma for layout
**3. Educational Value**
- Code should teach good practices
- Comments explain WHY, not just WHAT
- Examples of proper security (prepared statements, validation)
- Show different approaches in comments
**4. Low Barrier to Entry**
- Works with PHP's built-in server (`php -S`)
- Single SQLite file (no MySQL setup needed)
- Can run on cheap shared hosting
- No build process or compilation
**5. Libre Philosophy**
- Code is the documentation
- No proprietary dependencies
- Export functionality (CSV, YAML)
- Easy to fork and modify
### Example: Teaching Moment in Code
```php
<?php
/**
* BAD: This is vulnerable to SQL injection
* $sql = "SELECT * FROM theses WHERE year = " . $_GET['year'];
*
* WHY IT'S BAD: An attacker could send year=2023 OR 1=1
* This would return ALL theses, ignoring the year filter.
* Worse: year=2023; DROP TABLE theses-- would delete everything!
*
* GOOD: Use prepared statements
*/
$stmt = $db->prepare("SELECT * FROM theses WHERE year = ?");
$stmt->execute([$_GET['year']]);
/**
* The ? is a placeholder. PDO automatically escapes the value,
* making SQL injection impossible. Always use prepared statements
* when dealing with user input!
*/
?>
```
---
## Implementation Roadmap
### Phase 1: Foundation (Week 1-2)
- [ ] Create database schema (schema.sql)
- [ ] Write migration script (YAML → SQLite)
- [ ] Test migration with existing data
- [ ] Create Database helper class
- [ ] Set up shared configuration
### Phase 2: Security (Week 2-3)
- [ ] Implement prepared statements throughout
- [ ] Add CSRF protection to forms
- [ ] Improve file upload validation
- [ ] Add input sanitization functions
- [ ] Security audit of all user inputs
### Phase 3: Core Features (Week 3-5)
- [ ] Refactor index.php to use database
- [ ] Refactor thesis.php (memoire.php) to use database
- [ ] Update form submission to write to database
- [ ] Add pagination with SQL LIMIT/OFFSET
- [ ] Implement basic search
### Phase 4: Enhancements (Week 5-7)
- [ ] Tag cloud visualization
- [ ] Advanced filtering interface
- [ ] Related theses suggestions
- [ ] Statistics dashboard
- [ ] CSV import/export
### Phase 5: Polish (Week 7-8)
- [ ] Comprehensive testing
- [ ] Documentation (README, code comments)
- [ ] Deployment guide
- [ ] Backup procedures
- [ ] Performance optimization
### Phase 6: Student Handoff
- [ ] Code walkthrough session
- [ ] Transfer ownership to ERG
- [ ] Set up maintenance procedures
- [ ] Document common tasks
- [ ] Create troubleshooting guide
---
## Long-Term Sustainability
### For Future Student Maintainers
**Annual Tasks:**
- Review and merge tag duplicates (typographie vs typo)
- Check for broken file links
- Update year options in form dropdown
- Backup database offsite
**When Things Break:**
1. Check error.log file first
2. Test database: `sqlite3 data/posterg.db "PRAGMA integrity_check;"`
3. Restore from backup if corrupted
4. Check file permissions (uploads need 755)
**Adding New Features:**
- Start simple, add complexity only if needed
- Test with a copy of the database first
- Document what you changed in README
- Add comments explaining your code
**Getting Help:**
- PHP documentation: php.net
- SQLite documentation: sqlite.org
- Simple CSS: simplecss.org
- Bulma CSS: bulma.io
### Passing It On
This project is meant to be maintained by ERG students, for ERG students. When you graduate:
1. Train at least one person to take over
2. Document any custom modifications you made
3. Ensure backups are accessible
4. Update the README with current procedures
The best code is code that others can understand and modify. Keep it simple, keep it documented, keep it libre.
---
## Philosophy: Why This Matters
The POSTERG project challenges traditional academic hierarchies. Libraries archive only "excellent" work (70%+), relegating the rest to obscurity or basement storage. This creates a power structure where institutional gatekeepers decide what knowledge is worth preserving.
By making ALL theses accessible regardless of grade, POSTERG asserts that:
- Student work has intrinsic value beyond academic scoring
- Knowledge should be freely shared, not gatekept
- Digital formats deserve equal status with printed editions
- Students should control how their work is shared
This aligns with libre software principles:
- **Freedom to use**: Anyone can access theses
- **Freedom to study**: Full metadata and search capabilities
- **Freedom to share**: No artificial access restrictions
- **Freedom to modify**: Open source, hackable codebase
The technical decisions (SQLite, PHP, no framework) support this philosophy by ensuring the project remains:
- **Accessible**: Runs on cheap hosting, no special infrastructure
- **Understandable**: Code can be read and modified by students
- **Sustainable**: Not dependent on proprietary services or complex setups
- **Empowering**: Students maintain control, not dependent on external developers
Technology serves the mission. The mission is democratizing knowledge.
---
## Conclusion
The migration from YAML to SQLite is not just a technical upgrade—it's an investment in the project's ability to fulfill its mission at scale. The current system works for a dozen theses but will fail as the archive grows.
By following these recommendations, POSTERG can:
- Scale to thousands of theses with excellent performance
- Offer powerful search and discovery features
- Maintain security and data integrity
- Remain simple enough for students to maintain and improve
- Stay true to libre/open-source values
The proposed approach keeps the spirit of the original project—simple, student-built, non-hierarchical—while building a foundation that can last for years and serve hundreds of future ERG graduates.
Most importantly: this project belongs to students. These recommendations are suggestions, not requirements. Hack it, improve it, make it yours. The code is libre, the mission is democratization, and the future is in your hands.
*Pour une vie après l'ERG. Pour tous les mémoires, pas seulement les "excellents".*
---
*Assessment prepared January 2026*
*Based on POSTERG project state as of current commit*