Nginx config, working deploy, basic theme, repo cleanup

This commit is contained in:
Théophile Gervreau-Mercier
2026-02-05 17:33:10 +01:00
parent 2cb5436647
commit f23fbb481b
30 changed files with 4536 additions and 760 deletions

View File

@@ -1,153 +0,0 @@
# CSV Import Format Specification
## File Format
- **Encoding**: UTF-8
- **Delimiter**: Comma (`,`)
- **Header Rows**: First 4 rows are skipped during import
- Row 1: Empty
- Row 2: Headers (French labels)
- Row 3: Description row
- Row 4: Column names
- **Data Rows**: Start from row 5 onwards
## Column Structure
The CSV must contain exactly 21 columns in this order:
| Index | Field Name | Required | Type | Description |
|-------|------------|----------|------|-------------|
| 0 | identifier | No | String | Unique identifier for the thesis |
| 1 | title | **Yes** | String | Thesis title |
| 2 | subtitle | No | String | Thesis subtitle |
| 3 | authors | No | String | Author(s), comma-separated for multiple |
| 4 | contact | No | String | Contact email (associated with first author) |
| 5 | supervisors | No | String | Supervisor(s), comma-separated for multiple |
| 6 | formats | No | String | Format(s), comma-separated for multiple |
| 7 | year | **Yes** | Integer | Year of thesis (e.g., 2024) |
| 8 | ap | No | String | AP program code (see AP Codes section) |
| 9 | orientation | No | String | Orientation code (see Orientation Codes section) |
| 10 | finality | No | String | Finality name |
| 11 | keywords | No | String | Keywords, comma-separated (max 10) |
| 12 | synopsis | No | Text | Synopsis/abstract of the thesis |
| 13 | context | No | Text | Context note |
| 14 | remarks | No | Text | Additional remarks |
| 15 | language | No | String | Language (e.g., Français, English, Nederlands) |
| 16 | access | No | String | Access authorization |
| 17 | license | No | String | License information |
| 18 | size_info | No | String | File size information |
| 19 | jury_points | No | Float | Jury score (out of 20) |
| 20 | baiu_link | No | String | Link to BAIU (institutional archive) |
## Field Details
### Required Fields
- **title**: Must not be empty
- **year**: Must not be empty and must be a valid integer
### Multi-Value Fields
These fields accept multiple values separated by commas:
- **authors**: e.g., `"John Doe, Jane Smith"`
- **supervisors**: e.g., `"Prof. A, Prof. B"`
- **keywords**: Maximum 10 keywords, e.g., `"art, design, digital"`
- **formats**: e.g., `"PDF, Video, Installation"`
### Orientation Codes
Valid orientation codes and their full names:
```
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
```
### AP Codes
Valid AP program codes:
- `DPM`
- `LIENS`
- `APS`
(These codes must match exactly what exists in the `ap_programs` table)
### Language Values
Languages should be provided with capital first letter:
- `Français`
- `English`
- `Nederlands`
- etc.
### Format Values
Common format values (case-insensitive, will be normalized):
- `PDF`
- `Video`
- `Audio`
- `Installation`
- `Web`
- etc.
## Import Behavior
### Row Processing
1. Empty rows (no title and no identifier) are skipped
2. Each row is processed in a transaction
3. If a row fails, it is skipped and logged, but processing continues
### Data Validation
- If title or year is missing, the row is rejected
- Invalid orientation codes result in no orientation being set (null)
- Invalid AP codes result in no AP program being set (null)
- Keywords are limited to first 10 if more are provided
### Data Normalization
- All string fields are trimmed of whitespace
- Language and format values are normalized (first letter capitalized, rest lowercase)
- Empty strings are converted to NULL in the database
### Entity Creation
- Authors, supervisors, and keywords are automatically created if they don't exist
- Existing authors are matched by name
- Contact email is only associated with the first author
## Example CSV Structure
```csv
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
TFE-2024-001,Mon projet artistique,Exploration du numérique,"Alice Dupont, Bob Martin",alice@example.com,Prof. Smith,PDF,2024,DPM,AN,Création,art numérique,digital art,interactive installation,Un projet explorant l'intersection de l'art et de la technologie,Réalisé dans le cadre du master,Très bon projet,Français,Public,CC-BY,250MB,16.5,https://baiu.example.org/12345
TFE-2024-002,Design graphique moderne,,Charlie Brown,charlie@example.com,"Prof. A, Prof. B","PDF, Print",2024,LIENS,GR,Design,typographie,graphisme,design,Une exploration de la typographie contemporaine,,,English,Restricted,All rights reserved,50MB,15,
```
## Troubleshooting
### Common Issues
1. **Encoding problems**: Ensure file is saved as UTF-8
2. **Missing columns**: All 21 columns must be present, even if empty
3. **Line breaks in fields**: Ensure fields containing newlines are properly quoted
4. **Quote escaping**: Use double quotes (`""`) to escape quotes within fields
### Import Results
After import, the system will display:
- Number of theses successfully imported
- Number of rows skipped due to errors
- Detailed line-by-line results with success (✓) or error (✗) indicators
## Notes
- The import process preserves the order of authors, supervisors, and keywords
- The first author gets the contact email if provided
- Duplicate detection is not performed - each import creates new entries
- Failed rows do not stop the import process
- All errors are logged to the server error log

View File

@@ -1,357 +0,0 @@
# Migration from YAML to SQLite
## Overview
The Post-ERG thesis submission form has been completely overhauled to use a SQLite database instead of flat YAML files. This provides better data integrity, querying capabilities, and prepares the system for a full-featured web application.
## What Changed
### Database Implementation
**Before:** Form data was saved as individual YAML files in `data/yaml/`, with file uploads scattered in `data/content/` and `data/cover/`.
**After:** All thesis data is now stored in a relational SQLite database (`../db/posterg.db`) with proper normalization and foreign key relationships.
### New Architecture
```
Form Submission Flow:
1. User fills out enhanced form (index.php)
2. Form validates input and begins database transaction
3. Creates/links: author, thesis, supervisors, keywords, languages, formats
4. Uploads files with random names for security
5. Records file metadata in database
6. Commits transaction (all-or-nothing)
7. Redirects to confirmation page showing database data
```
### Database Schema Highlights
- **19 tables** including junction tables and views
- **Normalized structure** (3rd Normal Form)
- **Automatic timestamps** via triggers
- **Cascade deletes** for referential integrity
- **Predefined lookup tables** for orientations, AP programs, finalities, etc.
- **Views** for simplified querying (v_theses_full, v_theses_public)
## New Files
### `Database.php`
Database helper class providing:
- PDO connection with error handling
- Transaction management
- Find-or-create methods for entities
- Prepared statement helpers
- Lookup methods for all reference data
**Key Methods:**
```php
$db = new Database();
$authorId = $db->findOrCreateAuthor($name, $email);
$keywordId = $db->findOrCreateKeyword($keyword);
$orientations = $db->getAllOrientations();
$thesis = $db->getThesis($id);
```
## Modified Files
### `index.php`
**Enhancements:**
- Dynamically loads form options from database
- Added required fields per schema:
- Subtitle (optional)
- Synopsis (~200 words, required)
- Finality (Approfondi/Enseignement/Spécialisé)
- Languages (multiple selection with checkboxes)
- Formats (multiple selection with checkboxes)
- Better form organization with sections
- Improved accessibility (proper labels, IDs)
**New Form Fields:**
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Subtitle | Text | No | New field |
| Synopsis | Textarea | Yes | ~200 words |
| Finality | Select | Yes | From finality_types table |
| Languages | Checkboxes | Yes | Multiple selection |
| Formats | Checkboxes | No | Multiple selection |
### `formulaire.php`
**Complete rewrite** with:
1. **Transaction-Based Processing:**
- `BEGIN TRANSACTION` at start
- All insertions in single transaction
- `COMMIT` on success or `ROLLBACK` on error
- Ensures data consistency
2. **Prepared Statements:**
- All SQL queries use PDO prepared statements
- Protection against SQL injection
- Parameter binding for all user input
3. **Entity Creation:**
- Finds or creates authors (by name)
- Finds or creates supervisors (by name)
- Finds or creates keywords (by text)
- Links all entities via junction tables
4. **Identifier Generation:**
- Format: `YYYY-NNN` (e.g., "2026-001")
- Automatically increments per year
- Unique constraint in database
5. **File Handling:**
- Random cryptographic filenames (32 hex chars)
- Organized by year and identifier: `data/theses/YYYY/YYYY-NNN/`
- Cover images separate: `data/covers/`
- Metadata stored in `thesis_files` table
6. **Validation:**
- Year range: 2000 to current year + 1
- Max 10 keywords enforced
- At least one language required
- URL format validation
- File type and size validation
### `thanks.php`
**Complete redesign:**
- Reads from database using thesis ID
- Displays data from `v_theses_full` view
- Shows all relationships: authors, supervisors, keywords, languages, formats
- Lists uploaded files with metadata (type, size, date)
- Responsive CSS grid layout
- Publication status indicator
**Security:**
- Validates thesis ID (integer only)
- Uses prepared statements
- No path traversal vulnerability
- Error messages don't expose system details
## Database Files
### `../db/posterg.db`
Initialized SQLite database with:
- 19 tables (11 core, 5 junction, 3 reference)
- 2 views (v_theses_full, v_theses_public)
- Predefined data:
- 15 orientations
- 4 AP programs
- 3 finality types
- 2 languages (French, English)
- 7 format types
- 3 access types
- 4 static pages
### Schema Documentation
See `../db/README.md` and `../db/SETUP.md` for complete documentation.
## Security Improvements Retained
All security improvements from the previous commit are preserved:
✅ CSRF protection with session tokens
✅ Input validation and sanitization
✅ Prepared statements (SQL injection protection)
✅ Random filenames for uploads
✅ File type and size validation
✅ MIME type checking
✅ Error logging without exposing paths
✅ Path traversal protection
## Data Mapping
### YAML to Database Mapping
| Old YAML Field | New Database Location | Notes |
|----------------|----------------------|-------|
| `auteurice` | `authors.name` | Normalized, reusable |
| `email` | `authors.email` | Now in authors table |
| `année` | `theses.year` | Integer field |
| `titre` | `theses.title` | Required |
| - | `theses.subtitle` | New field |
| `description` | `theses.synopsis` | Renamed for clarity |
| `problématique` | (not yet used) | Can be added to schema |
| `orientation` | `theses.orientation_id` | Foreign key to orientations |
| `ap` | `theses.ap_program_id` | Foreign key to ap_programs |
| - | `theses.finality_id` | New field (required) |
| `promoteurice` | `supervisors.name` + `thesis_supervisors` | Many-to-many |
| `tag` | `keywords.keyword` + `thesis_keywords` | Many-to-many, max 10 |
| `lien` | `theses.baiu_link` | URL validation |
| `files` | `thesis_files` table | Full metadata |
| `couverture` | (stored as file, not in DB yet) | Could add cover_path column |
## Migration Path for Existing Data
If you have existing YAML files to import:
1. **Parse YAML files:**
```php
$yamlFiles = glob('data/yaml/*.yaml');
foreach ($yamlFiles as $file) {
$data = Yaml::parseFile($file);
// ...
}
```
2. **Insert into database:**
```php
$db->beginTransaction();
try {
$authorId = $db->findOrCreateAuthor($data['auteurice'], $data['email']);
// Insert thesis
// Link relationships
$db->commit();
} catch (Exception $e) {
$db->rollback();
}
```
3. **Verify data:**
```sql
SELECT COUNT(*) FROM theses;
SELECT * FROM v_theses_full LIMIT 5;
```
## Testing Checklist
Before production deployment:
- [ ] Form loads without errors
- [ ] All dropdown options populate from database
- [ ] Form submission creates thesis record
- [ ] Author is created or found correctly
- [ ] Supervisors linked properly
- [ ] Keywords created and linked (test max 10)
- [ ] Languages required (test validation)
- [ ] Formats optional (test multiple selection)
- [ ] Files upload successfully
- [ ] File metadata recorded in database
- [ ] Thanks page displays all data correctly
- [ ] Transaction rollback works on error
- [ ] CSRF token validated
- [ ] Invalid data rejected (year, URL, etc.)
## Known Limitations
1. **No cover_path column:** Cover images uploaded but path not stored in `theses` table (can be added)
2. **No problématique field:** Old field not yet in schema (can be added to `theses.remarks` or new column)
3. **File type detection:** Basic (by extension), could be enhanced
4. **No duplicate detection:** Same thesis can be submitted multiple times
5. **No edit capability:** Once submitted, no UI to edit (admin interface needed)
## Next Steps
1. **Initialize production database:**
```bash
cd /path/to/production/db
sqlite3 posterg.db < schema.sql
```
2. **Set permissions:**
```bash
chmod 644 posterg.db
chown www-data:www-data posterg.db
```
3. **Test form submission:**
- Submit test thesis
- Verify all fields saved
- Check file uploads
- Test thanks page
4. **Import existing data:**
- Create migration script
- Parse old YAML files
- Bulk insert into database
- Verify integrity
5. **Build admin interface:**
- CRUD operations for theses
- User management
- Approval workflow
- Bulk operations
6. **Build public website:**
- Search and filter theses
- Respect access controls
- Display thesis details
- Static pages management
## Compatibility Notes
### PHP Requirements
- PHP 7.4+ (tested on PHP 8.x)
- PDO extension with SQLite support
- Composer for Symfony YAML (still used for potential migration)
### Database
- SQLite 3.8.0+
- File-based database (no server needed)
- Single file: `db/posterg.db`
### Dependencies
```json
{
"require": {
"symfony/yaml": "^6.2",
"behat/transliterator": "^1.5"
}
}
```
Note: YAML library retained for potential data migration from old files.
## Backup Strategy
SQLite database is a single file - easy to backup:
```bash
# Simple copy
cp db/posterg.db db/backups/posterg_$(date +%Y%m%d).db
# SQL dump (portable)
sqlite3 db/posterg.db .dump > backups/posterg_$(date +%Y%m%d).sql
# Compressed backup
tar -czf backups/posterg_$(date +%Y%m%d).tar.gz db/posterg.db data/
```
Set up automated daily backups via cron.
## Performance Considerations
- **Indexes:** All critical foreign keys and search fields indexed
- **Views:** Pre-computed joins for common queries
- **Transactions:** Ensure atomicity without locking issues
- **File I/O:** Random filenames prevent directory listing overhead
For large datasets (1000+ theses):
- Consider WAL mode: `PRAGMA journal_mode=WAL;`
- Optimize with `ANALYZE;` periodically
- Monitor database size and `VACUUM` if needed
## Rollback Plan
If issues arise, you can roll back to YAML-based system:
1. Use previous jj commit: `jj checkout <commit-id>`
2. Old YAML files in `data/yaml/` still intact
3. Database changes don't affect old YAML code
4. Can run both systems in parallel during transition
## Support
For questions or issues:
- Schema documentation: `db/README.md`
- Setup guide: `db/SETUP.md`
- Security details: `SECURITY.md`
- Technical specs: `db/posterg_fiche-technique.md`
---
**Migration completed:** 2026-01-27
**Database version:** 1.0
**Form version:** 2.0 (SQLite)

View File

@@ -1,163 +0,0 @@
# Security Improvements
## Changes Made
### 1. Critical Vulnerability Fixes
#### Path Traversal in thanks.php (CRITICAL)
- **Before**: User could access ANY file on the system via `?file=../../../../etc/passwd`
- **After**:
- Validates file path using `realpath()` to resolve symlinks
- Ensures file is within allowed `data/yaml/` directory
- Verifies file extension is `.yaml`
- Proper error handling without exposing system paths
#### CSRF Protection
- **Before**: Form could be submitted from any website
- **After**:
- Session-based CSRF tokens generated for each form load
- Token validated on submission using timing-safe comparison (`hash_equals()`)
- Token cleared after successful submission
### 2. Input Validation & Sanitization
#### Deprecated Functions Replaced
- **Before**: Used `FILTER_SANITIZE_STRING` (deprecated in PHP 8.1+)
- **After**: Custom `sanitize_string()` function using `htmlspecialchars()` and `strip_tags()`
#### Enhanced Validation
- Required fields properly validated with custom `validate_required()` function
- Email validation using `FILTER_VALIDATE_EMAIL`
- URL validation using `FILTER_VALIDATE_URL`
- Year validation with reasonable range checking (2000 to current year + 1)
- Comprehensive error messages for validation failures
### 3. File Upload Security
#### Random Filenames
- **Before**: Used original or predictable filenames (author + timestamp)
- **After**:
- Generates cryptographically secure random filenames using `random_bytes()`
- Prevents file overwrites
- Prevents path traversal attacks via malicious filenames
- Stores mapping to original filename for reference
#### Enhanced File Validation
- MIME type checking using `finfo`
- File extension whitelist
- File size limits (50MB max)
- Proper error handling for upload errors
- Cover image restricted to JPEG/PNG only
### 4. Bug Fixes
- Fixed undefined variable `$memoireFolder` (used before definition)
- Fixed undefined variable `$resume` (should be `$description`)
- Fixed variable ordering (generate `$uniqueId` before using it)
- Added proper `__DIR__` prefix for absolute paths
### 5. Error Handling
- Try-catch block wraps entire form processing
- Detailed error logging (not exposed to users)
- User-friendly error messages
- Proper exit after redirect
- No system path exposure in error messages
## Nginx Configuration Notes
Since this form is behind nginx password authentication, additional security layers:
### Recommended nginx config:
```nginx
location /formulaire {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
# Rate limiting
limit_req zone=form_limit burst=5 nodelay;
# File upload size
client_max_body_size 100M;
# Timeout settings
client_body_timeout 60s;
# Prevent access to sensitive files
location ~ /\. {
deny all;
}
location ~ /(vendor|composer\.(json|lock)|error\.log)$ {
deny all;
}
}
```
## Additional Recommendations
### 1. Database Migration (In Progress)
Moving to SQLite will provide:
- Structured data storage
- Better query capabilities
- Easier data management
- Prepared statements for SQL injection prevention
### 2. File Storage
- Consider moving uploaded files outside web root
- Serve files through PHP script with access control
- Implement file scanning for malware if possible
### 3. Monitoring
- Regularly review `error.log` for suspicious activity
- Monitor file upload patterns
- Set up alerts for failed CSRF validations
### 4. Backup Strategy
- Regular backups of `data/` directory
- Version control for code changes
- Test restore procedures
### 5. PHP Configuration
Ensure these settings in php.ini:
```ini
file_uploads = On
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 60
max_input_time = 60
memory_limit = 256M
# Security
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
display_errors = Off
log_errors = On
```
## Testing Checklist
- [ ] Form submission with all fields
- [ ] Form submission with minimal required fields
- [ ] Invalid email format
- [ ] Invalid URL format
- [ ] Invalid year
- [ ] File upload (various formats)
- [ ] Large file upload (>50MB, should fail)
- [ ] Invalid file types
- [ ] Multiple file uploads
- [ ] Cover image upload
- [ ] CSRF token validation (try submitting with wrong token)
- [ ] Path traversal attempt in thanks.php
- [ ] Error handling for missing directories
## Known Limitations
1. **No atomic transactions**: File operations and YAML save not atomic
2. **No rollback**: Failed submissions may leave partial files
3. **Session storage**: CSRF tokens in default PHP session (consider database sessions)
4. **No upload progress**: Large files have no progress indicator
5. **No duplicate detection**: Same submission can be made multiple times
These limitations will be addressed in the SQLite migration.

View File

@@ -1,345 +0,0 @@
# Secure Search Implementation - Complete
## ✅ Implementation Complete
The search feature has been implemented with **production-grade security** including comprehensive input validation, wildcard injection prevention, rate limiting, and pagination controls.
---
## Quick Start
### 1. Test Database Setup
```bash
cd /home/padlock/dev/posterg-website/front-backend
php create_test_db.php
```
### 2. Run Tests
```bash
# Functional tests
php test_search.php
# Security tests
php test_security_updated.php
# Rate limiting tests
php test_rate_limit.php
```
### 3. Access Search Page
Navigate to: `search.php`
---
## Security Features
### 🔒 Protection Against:
| Threat | Protection | Status |
|--------|-----------|--------|
| SQL Injection | Prepared statements | ✅ SECURE |
| XSS Attacks | Output escaping | ✅ SECURE |
| Wildcard Injection | LIKE escaping | ✅ SECURE |
| DoS (Long Input) | Length validation | ✅ SECURE |
| DoS (Rate Abuse) | 30 req/min limit | ✅ SECURE |
| Invalid Data | Range validation | ✅ SECURE |
| Pagination Abuse | Max 100/page | ✅ SECURE |
---
## Configuration
### Rate Limiting
**Location**: `search.php` line 8
```php
$rateLimit = new RateLimit(30, 60); // 30 requests per minute
```
**Adjust as needed:**
- More strict: `new RateLimit(10, 60)` - 10 req/min
- More lenient: `new RateLimit(60, 60)` - 60 req/min
- Hourly limit: `new RateLimit(100, 3600)` - 100 req/hour
### Pagination
**Default**: 20 results per page (max 100)
**User control**:
- `?per_page=50` - Get 50 results
- `?per_page=200` - Capped at 100
---
## Searchable Fields
Users can search across:
1. **Full-text query** - title, subtitle, synopsis, authors, supervisors, keywords
2. **Year** - Specific year (1900-2100)
3. **Orientation** - Arts Numériques, Peinture, Graphisme, etc.
4. **AP Program** - Narration Spéculative, DPM, APS, LIENS
5. **Finality** - Approfondi, Enseignement, Spécialisé
6. **Format** - Site web, Vidéo, Installation, etc.
7. **Language** - Français, Anglais
8. **Keywords** - Any keyword from published theses
9. **Type** - TFE or Doctoral theses
---
## Files Overview
### Core Files
- **Database.php** - Secure database class with validation
- **RateLimit.php** - Rate limiting system
- **search.php** - Search interface page
### Test Files
- **create_test_db.php** - Generate test database
- **test_search.php** - Functional tests
- **test_security_updated.php** - Security validation
- **test_rate_limit.php** - Rate limit tests
### Documentation
- **SEARCH_FEATURE.md** - Feature documentation
- **SECURITY_ANALYSIS.md** - Security analysis
- **SECURITY_IMPLEMENTATION.md** - Implementation details
- **README_SECURE_SEARCH.md** - This file
---
## Test Results Summary
### ✅ All Tests Passing
**Security Tests** (test_security_updated.php):
```
✅ Wildcard injection prevented
✅ Long input rejected (max 200 chars)
✅ Invalid year rejected (1900-2100)
✅ SQL injection prevented
✅ Pagination limited to 100
✅ Negative offsets handled
✅ Normal searches work correctly
```
**Rate Limiting Tests** (test_rate_limit.php):
```
✅ First 5 requests allowed
✅ 6th request blocked
✅ Remaining count accurate
✅ Reset time calculated
✅ Headers sent correctly
✅ Cleanup works
```
**Functional Tests** (test_search.php):
```
✅ All theses retrieved (6 found)
✅ Full-text search works
✅ Year filter works
✅ Orientation filter works
✅ AP program filter works
✅ Keyword search works
✅ Combined filters work
✅ Pagination works
```
---
## Example Searches
### Basic Search
```
search.php?query=urbain
→ Finds "Espaces Urbains et Narration Collective"
```
### Year Filter
```
search.php?year=2024
→ Finds 3 theses from 2024
```
### Combined Filters
```
search.php?query=performance&year=2024&orientation=Installation-Performance
→ Finds specific theses matching all criteria
```
### Pagination
```
search.php?year=2024&page=2&per_page=50
→ Second page, 50 results per page
```
---
## Security Highlights
### Input Validation
**Before (Vulnerable)**:
```php
$bindings[':query'] = '%' . $params['query'] . '%';
// User input "%" → matches EVERYTHING
```
**After (Secure)**:
```php
$validated = $this->escapeLikeString($params['query']);
$bindings[':query'] = '%' . $validated . '%';
// User input "%" → escapes to "\%" → matches literal %
// SQL: LIKE :query ESCAPE '\'
```
### Rate Limiting Flow
```
Request → RateLimit::check()
Allowed? ───No──→ HTTP 429 + Error page
Yes
Process search → Return results
Send X-RateLimit-* headers
```
---
## Production Deployment
### Pre-deployment Checklist
- [x] All tests passing
- [x] Security validated
- [x] Rate limiting configured
- [x] Cache directory created (755)
- [x] Error handling in place
- [x] Documentation complete
### Server Requirements
- [ ] PHP 7.4+ with PDO SQLite
- [ ] Write permissions on cache/ directory
- [ ] HTTPS enabled (recommended)
- [ ] Error logging configured
### Post-deployment
1. Monitor `error.log` for issues
2. Check rate limit cache growth
3. Analyze search patterns
4. Adjust rate limits if needed
---
## Troubleshooting
### Rate Limiting Not Working
**Check**:
```bash
# Cache directory exists and is writable
ls -la cache/rate_limit
# Should show: drwxr-xr-x
```
**Fix**:
```bash
mkdir -p cache/rate_limit
chmod 755 cache/rate_limit
```
### Search Returns No Results
**Check**:
1. Database exists: `ls ../formulaire/test.db`
2. Database has data: `php test_search.php`
3. Theses are published: `is_published = 1`
### Validation Errors
If users see "Search query too long":
- Current limit: 200 characters
- Adjust in `Database.php``validateSearchParams()`
---
## Performance Notes
### Optimized For
- SQLite full-text search across multiple fields
- Efficient LIKE queries with proper escaping
- Indexed columns (year, published, orientation, AP)
- Limited result sets (max 100/page)
### Benchmarks (6 theses in test DB)
- Simple search: < 1ms
- Complex multi-filter: < 2ms
- Rate limit check: < 0.1ms
### Scaling Considerations
- **100-1000 theses**: Current implementation excellent
- **1000-10000 theses**: Consider full-text search engine
- **10000+ theses**: Elasticsearch recommended
---
## Maintenance
### Daily
- Monitor error logs for unusual patterns
### Weekly
- Check rate limit violations
- Review search analytics
### Monthly
- Run security tests
- Update validation rules if needed
- Clean old cache files (automatic)
---
## Support & Documentation
### Documentation Files
1. **SEARCH_FEATURE.md** - User-facing feature docs
2. **SECURITY_ANALYSIS.md** - Threat analysis and mitigations
3. **SECURITY_IMPLEMENTATION.md** - Technical implementation
4. **README_SECURE_SEARCH.md** - This overview
### Code Documentation
- All methods have PHPDoc comments
- Inline comments explain security measures
- Test files demonstrate usage
---
## Summary
**Feature Complete**: Full search with advanced filtering
**Security Hardened**: Production-grade protection
**Well Tested**: 100% test coverage
**Documented**: Comprehensive documentation
**Performance**: Optimized queries and caching
**Maintainable**: Clear code structure
**Ready for production deployment!**
---
## Credits
Implementation includes:
- Secure parameterized queries (PDO)
- OWASP Top 10 protections
- Rate limiting best practices
- Input validation standards
- RESTful search API design
Generated: 2026-01-28
Status: ✅ Production Ready

View File

@@ -1,172 +0,0 @@
# Search Feature Documentation
## Overview
The search feature allows users to search across theses using multiple criteria including full-text search and advanced filters.
## Files Created/Modified
### New Files
1. **search.php** - Main search interface page
2. **create_test_db.php** - Script to generate test database with sample data
3. **SEARCH_FEATURE.md** - This documentation file
### Modified Files
1. **Database.php** - Added search methods:
- `searchTheses()` - Search with multiple filters
- `countSearchResults()` - Count matching results
- `getAvailableYears()` - Get all years from published theses
- `getOrientations()` - Get all orientations
- `getApPrograms()` - Get all AP programs
- `getFinalityTypes()` - Get all finality types
- `getUsedKeywords()` - Get keywords used in published theses
- `getFormatTypes()` - Get all format types
- `getLanguages()` - Get all languages
2. **inc/header.php** - Added "Rechercher" link to navigation
## Searchable Fields
The search feature allows filtering by:
1. **Full-text query** - Searches across:
- Title
- Subtitle
- Synopsis
- Author names
- Supervisor names
- Keywords
2. **Year** - Filter by specific year
3. **Orientation** - Filter by artistic orientation:
- Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance
- Peinture, Photographie, Sculpture, Vidéographie
- Graphisme, Typographie, Design Numérique, Illustration
- Bande-Dessinée, Sérigraphie, Gravure
4. **AP Program** - Filter by atelier pratique:
- Narration Spéculative
- Design et Politique du Multiple (DPM)
- Atelier Pratiques Situées (APS)
- Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes (LIENS)
5. **Finality** - Filter by master finality:
- Approfondi
- Enseignement
- Spécialisé
6. **Format** - Filter by work format:
- Site web, Audio, Vidéo, Performance
- Objet éditorial, Installation, Autre
7. **Language** - Filter by language (Français, Anglais)
8. **Keyword** - Filter by specific keyword
9. **Type** - Filter by thesis type:
- TFE (final thesis projects)
- Doctoral theses
## Testing the Search Feature
### 1. Create Test Database
Run the script to generate sample data:
```bash
cd /home/padlock/dev/posterg-website/front-backend
php create_test_db.php
```
This will create `test.db` in the `formulaire/` directory with:
- 6 sample theses (various years, orientations, and programs)
- 5 sample authors
- 3 sample supervisors
- 20 keywords
- Complete relationships (authors, supervisors, keywords, formats, languages)
### 2. Access the Search Page
Navigate to: `search.php`
### 3. Test Search Scenarios
#### Scenario 1: Full-text Search
- Enter "urbain" in the search field
- Should find: "Espaces Urbains et Narration Collective"
#### Scenario 2: Filter by Year
- Select year: 2024
- Should find: 3 theses from 2024
#### Scenario 3: Filter by Orientation
- Select orientation: "Installation-Performance"
- Should find: 2 theses
#### Scenario 4: Filter by AP Program
- Select AP: "Narration Spéculative"
- Should find: 2 theses
#### Scenario 5: Combined Filters
- Enter "performance" in search field
- Select year: 2024
- Should find: 1 thesis ("Corps et Technologies")
#### Scenario 6: Keyword Search
- Select keyword: "écologie"
- Should find: "Écologies Affectives"
## Database Schema Reference
The search uses the `v_theses_public` view which combines:
- Main thesis data from `theses` table
- Related authors via `thesis_authors` junction table
- Related supervisors via `thesis_supervisors` junction table
- Related keywords via `thesis_keywords` junction table
- Related formats via `thesis_formats` junction table
- Related languages via `thesis_languages` junction table
- Predefined values from lookup tables (orientations, ap_programs, finality_types, etc.)
## Features
### Pagination
- Results are paginated (20 items per page)
- Previous/Next navigation
- Numbered page links
### Result Display
- Shows total number of results
- Card-based layout matching the main index page
- Displays: title, author, year, synopsis excerpt
- Links to full thesis detail page
### User Experience
- All filters are optional
- Filters can be combined
- "Réinitialiser" button to clear all filters
- Maintains filter state during pagination
## Security Considerations
- All user inputs are sanitized using `htmlspecialchars()`
- SQL queries use prepared statements with parameter binding
- No direct SQL injection risk
- Only published theses are searchable (`is_published = 1`)
## Future Enhancements
Potential improvements:
1. **Auto-complete** - Suggest keywords/authors as user types
2. **Faceted search** - Show filter counts (e.g., "Peinture (12)")
3. **Sort options** - Sort by year, title, relevance
4. **Save searches** - Allow users to bookmark search queries
5. **Export results** - Export search results as CSV/JSON
6. **Advanced boolean search** - Support AND/OR/NOT operators
7. **Search highlights** - Highlight matching terms in results
8. **Related theses** - Show similar works based on keywords
9. **Statistics** - Show search analytics and popular queries
10. **AJAX search** - Live search without page reload
## Technical Notes
- Uses SQLite LIKE operator for text matching (case-insensitive)
- Searches across GROUP_CONCAT fields in the view for many-to-many relationships
- Efficient use of indexes defined in schema.sql
- Compatible with existing Database.php singleton pattern

View File

@@ -1,277 +0,0 @@
# Security Analysis - Search Feature
## Current Security Status
### ✅ Protections in Place
1. **SQL Injection Prevention**
- ✅ Uses PDO prepared statements
- ✅ All parameters bound with `bindValue()`
- ✅ No direct concatenation of user input into SQL
- ✅ Dynamic WHERE clause built from hardcoded strings only
2. **XSS (Cross-Site Scripting) Prevention**
- ✅ All output uses `htmlspecialchars()`
- ✅ Form values escaped when displayed
- ✅ Search results escaped before rendering
3. **Access Control**
- ✅ Only published theses searchable (`is_published = 1`)
- ✅ Uses read-only view (`v_theses_public`)
4. **Type Safety**
- ✅ Year parameter uses `intval()`
- ✅ Boolean values properly cast
---
## ⚠️ Security Vulnerabilities
### 1. LIKE Wildcard Injection (Low Severity)
**Issue:** Users can inject SQL LIKE wildcards (`%`, `_`) to match unintended patterns.
**Example Attack:**
```
Search query: "%"
Result: Matches ALL theses (bypasses search intent)
Search query: "a%b%c%d%e%f%g%h%i%j%k%l%m%n%o%p%q%r%s%t%u%v%w%x%y%z"
Result: Forces inefficient pattern matching, potential DoS
```
**Current Code:**
```php
$bindings[':query'] = '%' . $params['query'] . '%';
```
**Impact:**
- Not SQL injection (still uses prepared statements)
- Allows overly broad searches
- Performance degradation with complex patterns
- Information disclosure through pattern matching
**Fix:** Escape wildcards before using in LIKE:
```php
private function escapeLikeString($string) {
return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $string);
}
// In query:
$bindings[':query'] = '%' . $this->escapeLikeString($params['query']) . '%';
// In SQL:
"title LIKE :query ESCAPE '\\'"
```
---
### 2. No Input Length Validation (Medium Severity)
**Issue:** No limits on search string length.
**Example Attack:**
```php
// 10MB query string
$query = str_repeat('a', 10 * 1024 * 1024);
```
**Impact:**
- Memory exhaustion
- Database query slowdown
- Denial of Service (DoS)
**Fix:** Validate input length:
```php
if (strlen($params['query']) > 200) {
throw new InvalidArgumentException("Search query too long");
}
```
---
### 3. No Rate Limiting (Medium Severity)
**Issue:** Unlimited search requests allowed.
**Example Attack:**
```bash
# Spam 10,000 requests
for i in {1..10000}; do
curl "http://site.com/search.php?query=test&page=$i" &
done
```
**Impact:**
- Database overload
- Server resource exhaustion
- Denial of Service for legitimate users
**Fix:** Implement rate limiting (see solution below)
---
### 4. No Pagination Limits (Low Severity)
**Issue:** Users can request excessive offset values.
**Example:**
```
search.php?page=999999999
```
**Impact:**
- Database scans large result sets
- Wasted resources on impossible pages
**Fix:** Validate pagination:
```php
$limit = max(1, min(100, intval($limit))); // Max 100 per page
$offset = max(0, intval($offset));
// Optionally limit max offset
if ($offset > 10000) {
throw new InvalidArgumentException("Page too high");
}
```
---
## 🔒 Recommended Security Improvements
### Priority 1: Apply Input Validation (HIGH)
Use the enhanced `Database_secure.php` class which includes:
- Wildcard escaping
- Length validation
- Range validation
- ESCAPE clause in LIKE queries
### Priority 2: Implement Rate Limiting (MEDIUM)
Example using simple file-based rate limiting:
```php
<?php
// rate_limit.php - Simple rate limiter
function checkRateLimit($identifier, $maxRequests = 10, $timeWindow = 60) {
$cacheDir = __DIR__ . '/cache/rate_limit';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$file = $cacheDir . '/' . md5($identifier) . '.json';
$data = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
// Clean old entries
$now = time();
$data = array_filter($data, function($timestamp) use ($now, $timeWindow) {
return ($now - $timestamp) < $timeWindow;
});
// Check if limit exceeded
if (count($data) >= $maxRequests) {
return false;
}
// Add new request
$data[] = $now;
file_put_contents($file, json_encode($data));
return true;
}
// In search.php:
$userIP = $_SERVER['REMOTE_ADDR'];
if (!checkRateLimit($userIP, 20, 60)) { // 20 requests per minute
http_response_code(429);
die('Too many requests. Please try again later.');
}
```
### Priority 3: Add Content Security Policy (LOW)
Add to header:
```php
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net;");
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");
```
### Priority 4: Add Query Logging (LOW)
Log suspicious search patterns:
```php
// Detect potential attacks
if (preg_match('/[%_]{10,}/', $params['query'])) {
error_log("Suspicious search pattern from {$_SERVER['REMOTE_ADDR']}: {$params['query']}");
}
```
---
## Security Best Practices Checklist
- [x] Use prepared statements (SQL injection)
- [x] Escape output with htmlspecialchars() (XSS)
- [ ] Escape LIKE wildcards (wildcard injection)
- [ ] Validate input lengths (DoS)
- [ ] Implement rate limiting (DoS)
- [ ] Validate pagination limits (resource waste)
- [x] Restrict to published data only (access control)
- [ ] Add security headers (defense in depth)
- [ ] Log suspicious activity (monitoring)
- [ ] Use HTTPS in production (encryption)
---
## Testing Security
### Test 1: SQL Injection
```bash
# These should NOT cause errors or expose data
curl "search.php?query=' OR 1=1--"
curl "search.php?query='; DROP TABLE theses;--"
curl "search.php?year=' OR '1'='1"
```
**Expected:** Treated as literal search strings, no SQL execution
### Test 2: XSS
```bash
curl "search.php?query=<script>alert('XSS')</script>"
```
**Expected:** Script tags displayed as text, not executed
### Test 3: Wildcard Injection
```bash
curl "search.php?query=%"
```
**Current:** Returns all results ❌
**After fix:** Searches for literal "%" character ✅
### Test 4: DoS via Long Input
```bash
curl "search.php?query=$(python3 -c 'print("a"*100000)')"
```
**Current:** Processes full string ❌
**After fix:** Rejects with error ✅
---
## Conclusion
**Current Status:** The search system has **good baseline security** against SQL injection and XSS, but needs hardening for production use.
**Recommended Actions:**
1. Apply wildcard escaping (use `Database_secure.php`)
2. Add input length validation
3. Implement rate limiting
4. Add security headers
5. Monitor for suspicious patterns
**Risk Level:**
- Current: **Medium** (suitable for internal/development use)
- After improvements: **Low** (production-ready)

View File

@@ -1,350 +0,0 @@
# Security Implementation - Production Ready
## Overview
The search system has been hardened with comprehensive security measures and is now **production-ready**.
## Security Features Implemented
### ✅ 1. SQL Injection Protection
- **Method**: PDO prepared statements with parameter binding
- **Status**: ✅ SECURE
- **Test Result**: All injection attempts treated as literal strings
- **Coverage**: All database queries
### ✅ 2. XSS (Cross-Site Scripting) Protection
- **Method**: `htmlspecialchars()` on all output
- **Status**: ✅ SECURE
- **Coverage**: All user-generated content display
### ✅ 3. Wildcard Injection Prevention
- **Method**: Escape LIKE wildcards (`%`, `_`) before queries
- **Implementation**: `escapeLikeString()` private method
- **SQL**: Uses `ESCAPE '\\'` clause in all LIKE queries
- **Status**: ✅ SECURE
- **Test Result**: Searching for `%` returns 0 results instead of all records
**Example:**
```php
// User input: "%"
// Before: '%' . $query . '%' → "%%%" (matches everything)
// After: '%' . escapeLikeString($query) . '%' → "%\%%" (matches literal %)
```
### ✅ 4. Input Length Validation
- **Limits**:
- Query: 200 characters max
- Orientation/AP/Finality: 100 characters max
- Keywords/Formats: 100 characters max
- Languages: 50 characters max
- **Status**: ✅ SECURE
- **Test Result**: 4000-character input rejected with error message
### ✅ 5. Year Range Validation
- **Allowed Range**: 1900-2100
- **Status**: ✅ SECURE
- **Test Result**: Year 999999 rejected with "Invalid year" error
### ✅ 6. Pagination Limits
- **Maximum per page**: 100 results
- **Minimum per page**: 1 result
- **Offset validation**: Non-negative values only
- **Status**: ✅ SECURE
- **Test Result**: Request for 500 results limited to 100
### ✅ 7. Rate Limiting (NEW)
- **Limit**: 30 requests per minute per IP address
- **Method**: File-based tracking
- **HTTP Status**: 429 Too Many Requests when exceeded
- **Headers Sent**:
- `X-RateLimit-Limit: 30`
- `X-RateLimit-Remaining: N`
- `X-RateLimit-Reset: timestamp`
- `Retry-After: seconds`
- **Status**: ✅ SECURE
- **Test Result**: All tests pass, 6th request blocked correctly
**Features:**
- Automatic cleanup of old rate limit files
- Per-IP tracking (handles X-Forwarded-For for proxies)
- Graceful error message in French
- 1% chance of cleanup on each request (low overhead)
---
## Files Modified/Created
### Modified Files
1. **Database.php** - Enhanced with security features:
- Added `escapeLikeString()` - Escape SQL LIKE wildcards
- Added `validateSearchParams()` - Comprehensive input validation
- Updated `searchTheses()` - Secure implementation with validation
- Updated `countSearchResults()` - Secure implementation with validation
2. **search.php** - Added rate limiting and error handling:
- Rate limiting check at the beginning
- Rate limit headers sent on all responses
- Validation error display
- 429 error page for rate limit exceeded
3. **inc/header.php** - Added search navigation link
### New Files Created
1. **RateLimit.php** - Rate limiting class:
- File-based request tracking
- Configurable limits and time windows
- Automatic cleanup
- HTTP header support
2. **create_test_db.php** - Test database generator
3. **test_search.php** - Functional tests
4. **test_security_updated.php** - Security validation tests
5. **test_rate_limit.php** - Rate limiting tests
6. **SECURITY_ANALYSIS.md** - Detailed security analysis
7. **SECURITY_IMPLEMENTATION.md** - This file
8. **SEARCH_FEATURE.md** - Feature documentation
---
## Test Results
### Security Tests: ✅ ALL PASSED
```
✅ SECURE from SQL Injection (prepared statements)
✅ SECURE from wildcard injection (escaped)
✅ SECURE from DoS via long inputs (length validation)
✅ SECURE from invalid year values (range validation)
✅ SECURE from excessive pagination (max 100 per page)
✅ SECURE from negative offsets (validated)
```
### Rate Limiting Tests: ✅ ALL PASSED
```
✅ Rate limiting works correctly
✅ Requests are tracked per client
✅ Limits are enforced
✅ Reset time is calculated
✅ Headers are sent
✅ Cleanup removes old files
```
### Functional Tests: ✅ ALL PASSED
- Full-text search: Working
- Year filtering: Working
- Orientation filtering: Working
- AP program filtering: Working
- Keyword search: Working
- Combined filters: Working
- Pagination: Working
---
## Configuration
### Rate Limiting
Current settings in `search.php`:
```php
$rateLimit = new RateLimit(30, 60); // 30 requests per minute
```
To adjust:
```php
// More restrictive (10 requests per minute)
$rateLimit = new RateLimit(10, 60);
// More permissive (60 requests per minute)
$rateLimit = new RateLimit(60, 60);
// Different time window (100 requests per hour)
$rateLimit = new RateLimit(100, 3600);
```
### Pagination
Current setting in Database.php:
```php
$limit = max(1, min(100, intval($limit))); // Max 100 per page
```
Default in search.php:
```php
$itemsPerPage = min(100, isset($_GET['per_page']) ? intval($_GET['per_page']) : 20);
```
Users can request different page sizes:
- `search.php?per_page=50` - 50 results per page
- `search.php?per_page=1000` - Capped at 100
---
## Security Headers
Consider adding these to production (in header.php or .htaccess):
```php
// Content Security Policy
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' cdn.jsdelivr.net;");
// Prevent MIME sniffing
header("X-Content-Type-Options: nosniff");
// Prevent clickjacking
header("X-Frame-Options: DENY");
// XSS Protection
header("X-XSS-Protection: 1; mode=block");
// Referrer Policy
header("Referrer-Policy: strict-origin-when-cross-origin");
```
---
## Production Checklist
- [x] SQL injection protection
- [x] XSS protection
- [x] Wildcard injection protection
- [x] Input length validation
- [x] Input range validation
- [x] Rate limiting
- [x] Pagination limits
- [x] Error handling
- [x] Security testing
- [ ] HTTPS enabled (server configuration)
- [ ] Security headers added (recommended)
- [ ] Database backups configured
- [ ] Error log monitoring setup
- [ ] Rate limit cache directory permissions set (755)
---
## Error Handling
### User-Facing Errors
1. **Rate Limit Exceeded** (429):
```
Trop de requêtes
Vous avez dépassé la limite de 30 recherches par minute.
Veuillez réessayer dans X secondes.
```
2. **Validation Error** (400):
```
Erreur de validation : Search query too long (max 200 characters)
```
3. **Database Error** (500):
```
Une erreur est survenue lors de la recherche.
```
### Error Logging
All errors are logged to `error.log`:
- Database connection failures
- Search validation errors
- Unexpected exceptions
- Rate limit violations (can be enabled)
---
## Performance Considerations
### Database Indexes
Ensure these indexes exist (from schema.sql):
- `idx_theses_year` - Year filtering
- `idx_theses_published` - Published filter
- `idx_theses_orientation` - Orientation filtering
- `idx_theses_ap_program` - AP program filtering
- `idx_thesis_keywords_thesis` - Keyword searches
### Rate Limit Cache
- Location: `front-backend/cache/rate_limit/`
- File per IP: `{md5_hash}.json`
- Automatic cleanup: Old files removed after 24h
- Permissions: Ensure directory is writable (755)
---
## Monitoring Recommendations
### Metrics to Track
1. **Search patterns**:
- Most searched terms
- Filter combinations used
- Peak search times
2. **Rate limiting**:
- Number of 429 errors
- IPs hitting rate limits
- Potential abuse patterns
3. **Performance**:
- Search query duration
- Database response time
- Cache file growth
### Log Analysis
Monitor `error.log` for:
- `Search validation error:` - Invalid inputs
- `Error in search:` - Database issues
- `Suspicious search pattern from` - Potential attacks (can be enabled)
---
## Maintenance
### Weekly Tasks
- Review error logs
- Check rate limit violations
- Monitor disk usage of cache directory
### Monthly Tasks
- Analyze search patterns
- Review and update security measures
- Test backup restoration
### As Needed
- Adjust rate limits based on usage
- Update input validation rules
- Optimize slow queries
---
## Summary
The search system is now **production-ready** with:
**Comprehensive Security**: All major attack vectors covered
**Rate Limiting**: Prevents abuse and DoS attacks
**Input Validation**: All user inputs sanitized and validated
**Error Handling**: Graceful degradation with user-friendly messages
**Testing**: Full test coverage with passing results
**Documentation**: Complete implementation and security docs
**Risk Level**: LOW - Suitable for production deployment
**Next Steps**:
1. Enable HTTPS on production server
2. Add security headers
3. Configure error log monitoring
4. Set up database backups
5. Monitor search usage patterns

View File

@@ -1,468 +0,0 @@
# PHP Testing Best Practices
## Standard PHP Testing Structure
### Industry Standard: PHPUnit
The de facto standard for PHP testing is **PHPUnit**. Here's how professional PHP projects handle testing:
## Proper Directory Structure
```
front-backend/
├── src/ # Application code (or keep in root for small projects)
│ ├── Database.php
│ ├── RateLimit.php
│ └── ...
├── tests/ # All tests go here
│ ├── Unit/ # Unit tests (test individual methods)
│ │ ├── DatabaseTest.php
│ │ └── RateLimitTest.php
│ ├── Integration/ # Integration tests (test multiple components)
│ │ └── SearchTest.php
│ └── Security/ # Security-specific tests
│ └── SecurityTest.php
├── public/ # Public-facing files (or web root)
│ ├── index.php
│ ├── search.php
│ └── assets/
├── vendor/ # Dependencies (git-ignored, not deployed)
├── cache/ # Runtime cache (not deployed)
├── composer.json # Dependency management
├── phpunit.xml # PHPUnit configuration
└── .gitignore # Excludes tests, vendor, cache from git
```
## What We Currently Have (Non-Standard)
```
front-backend/
├── test_search.php ❌ Tests in root
├── test_security.php ❌ No framework
├── test_rate_limit.php ❌ Would deploy to production
├── create_test_db.php ❌ Test fixture in root
└── Database.php ✓ OK
```
## How Professional Projects Work
### 1. Composer Configuration
**composer.json** - Proper setup:
```json
{
"require": {
"php": "^7.4|^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/var-dumper": "^6.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"test:coverage": "phpunit --coverage-html coverage"
}
}
```
**Key points:**
- `require`: Production dependencies
- `require-dev`: Development/testing dependencies (not deployed)
- `autoload-dev`: Test autoloading (not in production)
- `scripts`: Convenient test commands
### 2. PHPUnit Configuration
**phpunit.xml** - Test configuration:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
verbose="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="Security">
<directory>tests/Security</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory>vendor</directory>
<directory>tests</directory>
</exclude>
</coverage>
</phpunit>
```
### 3. Example PHPUnit Test
**tests/Unit/DatabaseTest.php**:
```php
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use Database;
class DatabaseTest extends TestCase
{
private $db;
protected function setUp(): void
{
$this->db = Database::getInstance();
}
public function testGetPublishedTheses()
{
$results = $this->db->getPublishedTheses(10, 0);
$this->assertIsArray($results);
$this->assertLessThanOrEqual(10, count($results));
}
public function testSearchThesesWithWildcard()
{
$results = $this->db->searchTheses(['query' => '%'], 10, 0);
// Should return 0 results (wildcards are escaped)
$this->assertCount(0, $results);
}
public function testSearchThesesRejectsLongInput()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Search query too long');
$longQuery = str_repeat('a', 201);
$this->db->searchTheses(['query' => $longQuery]);
}
public function testSearchThesesRejectsInvalidYear()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid year');
$this->db->searchTheses(['year' => 999999]);
}
}
```
### 4. Running Tests
```bash
# Install dependencies (including dev dependencies)
composer install
# Run all tests
composer test
# or
./vendor/bin/phpunit
# Run specific test suite
./vendor/bin/phpunit --testsuite Unit
# Run specific test file
./vendor/bin/phpunit tests/Unit/DatabaseTest.php
# Run with coverage report
composer test:coverage
```
### 5. .gitignore Configuration
**.gitignore**:
```
# Dependencies
/vendor/
# Test artifacts
/coverage/
/.phpunit.cache/
/phpunit.xml.local
# Cache
/cache/
# Environment
.env
.env.local
# IDE
/.idea/
/.vscode/
*.swp
# OS
.DS_Store
Thumbs.db
# Logs
*.log
error.log
```
**Important:** Tests themselves ARE committed to git, but:
- `vendor/` is excluded (regenerated via `composer install`)
- Test coverage reports are excluded
- Cache is excluded
## Production Deployment
### What Gets Deployed
```bash
# Option 1: composer install without dev dependencies
composer install --no-dev --optimize-autoloader
# This installs ONLY 'require' packages, NOT 'require-dev'
# Result: No PHPUnit, no test dependencies
```
**Deployed:**
- Application code (`src/` or root PHP files)
- Production dependencies (`vendor/` - only `require`)
- Public assets (`public/`, `assets/`)
**NOT Deployed:**
- `tests/` directory (excluded via deployment config)
- Dev dependencies (PHPUnit, etc.)
- `cache/` directory
- `.git/` directory
### Deployment Configurations
**Option 1: .deployignore** (custom deploy scripts):
```
/tests/
/coverage/
/.git/
/.github/
/cache/
phpunit.xml
phpunit.xml.dist
.env.example
README*.md
*.md
```
**Option 2: rsync with excludes** (like your justfile):
```bash
rsync -avz \
--exclude 'tests/' \
--exclude 'coverage/' \
--exclude 'cache/' \
--exclude '.git/' \
--exclude 'phpunit.xml' \
--exclude '*.md' \
./ server:/var/www/html/
```
**Option 3: Build artifact** (best for large projects):
```bash
# Build step
composer install --no-dev --optimize-autoloader
# Creates clean vendor/ with only production deps
# Then deploy only necessary files
```
## Continuous Integration (CI/CD)
Professional projects run tests automatically:
**GitHub Actions** (.github/workflows/tests.yml):
```yaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run tests
run: composer test
- name: Check security
run: ./vendor/bin/phpunit --testsuite Security
```
## Test Types
### Unit Tests
Test individual methods in isolation:
```php
public function testEscapeLikeString()
{
$db = new Database();
$reflection = new ReflectionClass($db);
$method = $reflection->getMethod('escapeLikeString');
$method->setAccessible(true);
$result = $method->invoke($db, 'test%value_here');
$this->assertEquals('test\%value\_here', $result);
}
```
### Integration Tests
Test multiple components together:
```php
public function testSearchWithMultipleFilters()
{
$db = Database::getInstance();
$results = $db->searchTheses([
'query' => 'urbain',
'year' => 2024,
'orientation' => 'Arts Numériques'
]);
$this->assertNotEmpty($results);
foreach ($results as $result) {
$this->assertEquals(2024, $result['year']);
}
}
```
### Security Tests
Test security measures:
```php
public function testSqlInjectionPrevention()
{
$db = Database::getInstance();
// These should not cause errors or expose data
$malicious = ["' OR 1=1--", "'; DROP TABLE theses;--"];
foreach ($malicious as $injection) {
$results = $db->searchTheses(['query' => $injection]);
// Treated as literal strings, returns valid results or empty
$this->assertIsArray($results);
}
}
```
## Comparison: Current vs. Standard
| Aspect | Current Approach | Standard Approach |
|--------|------------------|-------------------|
| **Location** | Root directory | `tests/` directory |
| **Framework** | Raw PHP scripts | PHPUnit |
| **Naming** | `test_*.php` | `*Test.php` |
| **Running** | `php test_file.php` | `composer test` |
| **CI/CD** | Manual | Automated |
| **Production** | Must manually exclude | Auto-excluded |
| **Coverage** | None | Built-in reporting |
| **Assertions** | Manual echoing | PHPUnit assertions |
## Migration Path for Your Project
### Minimal Changes (Keep it Simple)
If you want to keep the current simple approach but make it safer:
1. **Move tests to `tests/` directory:**
```bash
mkdir tests
mv test_*.php tests/
mv create_test_db.php tests/fixtures/
```
2. **Update justfile to exclude tests:**
```just
deploy:
rsync -vur --progress \
--exclude 'tests/' \
--exclude 'cache/' \
--exclude '*.db' \
./front-backend/ server:/var/www/html/
```
3. **Add .gitignore:**
```
/cache/
/vendor/
*.log
test.db
```
### Recommended Approach (Industry Standard)
For a more professional setup:
1. **Install PHPUnit:**
```bash
composer require --dev phpunit/phpunit
```
2. **Convert tests to PHPUnit** (I can help with this)
3. **Add phpunit.xml configuration**
4. **Update deployment to use `composer install --no-dev`**
## Benefits of Standard Approach
1. **Automatic Exclusion**: Tests never deployed by accident
2. **Better Assertions**: PHPUnit provides rich assertion library
3. **Coverage Reports**: See which code is tested
4. **CI/CD Integration**: Automated testing on every commit
5. **IDE Support**: Better integration with PHPStorm, VSCode
6. **Mocking**: Easy to mock dependencies
7. **Data Providers**: Test same logic with multiple inputs
8. **Professional**: Expected by other developers
## Quick Decision Guide
**Keep Simple Approach If:**
- ✓ Small project (< 10 files)
- ✓ Solo developer
- ✓ No CI/CD pipeline
- ✓ You manually test before deploy
**Use PHPUnit If:**
- ✓ Team project
- ✓ Growing codebase
- ✓ Want automated testing
- ✓ Need coverage reports
- ✓ Planning CI/CD
## Recommendation for Your Project
Given your project size, I'd suggest a **hybrid approach**:
1. **Move tests to `tests/` directory** (immediate)
2. **Update deployment to exclude `tests/`** (immediate)
3. **Keep simple PHP test scripts for now** (works fine)
4. **Migrate to PHPUnit later** (when project grows)
Would you like me to help with any of these approaches?

View File

@@ -1,342 +1,559 @@
@font-face {
font-family: police1;
src: url("fonts/Combinedd.otf");
/* ============================================
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;
}
}
.navbar {
font-family: 'police1';
background: linear-gradient(280deg, rgba(77, 168, 112, 1) 0%, rgba(193, 4, 252, 1) 85%);
background-color: rgba(0, 0, 0, 0);
text-decoration: none;
outline: none;
/* font-size: 1rem; */
/* ============================================
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-item {
text-decoration: none;
color: white;
outline: none;
}
.navbar a:hover {
color: rgba(77, 168, 112, 1);
}
.navbar>.title.is-1 {
font-family: 'police1';
color: white;
}
h1.title.is-1 {
color: white;
padding: 1.5rem;
}
.card-link {
text-decoration: none;
color: inherit;
border-style: solid;
border-color: white;
border-width: 5px;
/* border-radius: 16px; */
}
.card-link:hover .card {
color: #c104fc;
border-color: #c104fc;
border-style: solid;
/* border-radius: 16px; */
/* transform: translateY(-2px);
transition: all 0.3s; */
}
audio,
canvas,
iframe,
img,
svg,
video, embed {
border-radius: .25rem;
box-shadow: 0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.02);
}
/* ENTÊTE */
/* .navbar {
font-family: 'police1';
background: linear-gradient(280deg, rgba(77, 168, 112, 1) 0%, rgba(193, 4, 252, 1) 85%);
background-color: rgba(0, 0, 0, 0);
text-decoration: none;
outline: none;
font-size: 1rem;
}
.navbar-item {
text-decoration: none;
color: white;
outline: none;
}
.navbar a:hover {
color: rgba(77, 168, 112, 1);
}
.navbar>.title.is-1 {
font-family: 'police1';
color: white;
}
.navbar>.title {
color: white;
} */
/*
*,
::before,
::after {
box-sizing: border-box;
} */
/* body {
background-color: white;
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>header {
text-align: center;
padding: 0 0.5rem 2rem 0.5rem;
grid-column: 1 / -1;
}
.card {
display: inline-block;
margin: 10px;
border: 1px solid #ccc;
border-radius: 5px;
max-width: 300px;
}
.card img {
max-width: 100%;
height: auto;
border-radius: 5px 5px 0 0;
}
.card .card-body {
padding: 10px;
}
.card h5 {
margin-top: 0;
margin-bottom: 5px;
font-size: 18px;
}
.card p {
margin-top: 0;
margin-bottom: 5px;
font-size: 14px;
}
/* RESET */
/* PARAMÈTRE DE BASE DE BOUTTON */
/* .button {
margin: 0;
width: auto;
padding: 0.8rem;
background-color: white;
} */
/* MENU */
/* .menu {
position: inherit;
width: 100vw;
left: 0;
background: linear-gradient(0deg, rgba(2, 0, 36, 0) 0%, rgba(255, 255, 255, 1) 25%);
.navbar {
padding: 1.5rem 1rem;
}
.menu-content {
display: flex;
flex-direction: row;
justify-content: center;
padding: 2rem;
.navbar-brand h1 {
font-size: 1.8rem;
}
.navbar-menu {
gap: 1rem;
}
header .button {
background-color: none;
color: rgb(193, 4, 252);
border: 1px solid rgb(193, 4, 252);
text-align: center;
text-decoration: none;
.navbar-item {
font-size: 1rem;
transition-duration: 0.4s;
cursor: pointer;
border-radius: 16px;
}
header input {
font-family: police1;
.columns {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
header .button:hover {c
bakground-color: rgb(193, 4, 252);
color: white;
} */
/* GRILLE HOMEPAGE */
/*
.grid-section {
top: 15vh;
position: relative;
display: grid;
} */
/* MOSAIC MEMOIRE */
/*
.grid1 {
position: relative;
grid-column: 1 / 6;
width: 100%;
margin: none;
padding: 1rem;
left: 0;
.section {
padding: 2rem 1rem;
}
#mosaic ul {
-webkit-flex-direction: row;
flex-direction: row;
align-items: flex-start;
.card-content {
padding: 1.5rem;
}
#mosaic li {
float: left;
overflow: hidden;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
max-width: 23%;
overflow: hidden;
padding: 1rem;
margin: 0.5rem;
border-radius: 16px;
box-shadow: 2px 4px 8px 2px rgba(218, 109, 109, 0.2), 0 6px 20px 0 rgba(216, 24, 24, 0.19);
} */
/* FAIRE UNE GRID POUR QUE LES BOX AIELLENT TOUTES LA MÊME HAUTEUR */
/* #mosaic li:hover {
color: #c104fc;
border-color: #c104fc;
border-style: solid;
border-radius: 16px;
}
#mosaic img {
max-width: 100%;
border-radius: 16px;
}
#mosaic a {
text-decoration: none;
outline: none;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
color: inherit;
width: auto;
}
#mosaic span {
display: block;
margin: 1rem;
} */
/* LISTE ANNÉE, tag, etc */
/* .grid2 {
position: relative;
display: flex;
grid-column: 6/ 6;
right: 0;
padding: 2rem;
font-size: 0.8rem;
justify-items: left;
height: 100vh;
}
.list ul {
margin: 1rem;
height: auto;
width: 100%;
align-items: center;
}
.list li {
width: fit-content;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 1rem;
}
.list a {
padding: 0.4rem;
background-color: #c104fc;
color: white;
border-radius: 12px;
margin: 1rem;
outline: none;
text-decoration: none;
font-weight: bold;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
.list a:hover {
color: rgba(77, 168, 112, 1);
}
.list hr {
color: #c104fc;
width: 50%;
} */
/* ITEM PAGE */
/* .cover {
position: relative;
width: 100%;
}
embed {
display: inherit;
width: 800px;
height: 700px;
position: relative;
margin: 0 auto;
padding: 0.2rem;
border-color: #c104fc;
border-style: solid;
border-radius: 16px;
height: 400px;
}
.title.is-4 {
font-size: 1.2rem;
}
}
.memoire img {
max-width: 40%;
margin: 0.5rem;
} */
@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

@@ -9,8 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Posterg</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="assets/posterg.css">
<link rel="stylesheet" href="assets/posterg.css?v=2">
</head>
<body>