mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
Nginx config, working deploy, basic theme, repo cleanup
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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?
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user