mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
1099 lines
29 KiB
Markdown
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*
|