Skip to content

Commit 3376a2f

Browse files
authored
Merge pull request #24 from SproutPHP/dev_yanik
Dev yanik
2 parents 4251541 + 36132e5 commit 3376a2f

File tree

12 files changed

+353
-107
lines changed

12 files changed

+353
-107
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,26 @@ These were single-shot development releases with no progressive alpha/beta cycle
5858
---
5959

6060
**From v0.1.7-alpha.2 onward, all releases will follow a structured, progressive SemVer pre-release cycle.**
61+
62+
## [v0.1.7-beta.1] - 2024-06-09
63+
64+
### Added
65+
- Dynamic route parameter support (e.g., `/user/{id}`, `/blog/{slug}`) for CRUD and flexible routing
66+
- Robust CSRF protection via middleware and helpers (works for forms, AJAX, and HTMX)
67+
- SPA-like file upload and form handling with HTMX (including indicators and grid UI)
68+
- Secure private file upload/download (no direct links, internal access only)
69+
- Consistent CSRF token management (single session key, helpers, and middleware)
70+
71+
### Improved
72+
- UI/UX for validation and file upload forms (two-column grid, spinner, SPA feel)
73+
- Path resolution for storage (public/private separation, symlink support)
74+
- Code structure: CSRF logic moved to helpers/middleware, no raw PHP in entry
75+
76+
### Fixed
77+
- Issues with file download on PHP built-in server (now uses query param for compatibility)
78+
- Consistency in CSRF token usage across the framework
79+
80+
### Removed
81+
- Exposed raw CSRF logic from entry point
82+
83+
---

DOCS.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# SproutPHP Documentation
22

3+
## [v0.1.7-beta.1] - 2024-06-09
4+
5+
### New Features & Improvements
6+
7+
- **Dynamic Route Parameters:** Define routes with parameters (e.g., `/user/{id}`, `/blog/{slug}`) for flexible CRUD and API endpoints.
8+
- **Robust CSRF Protection:** Middleware-based CSRF protection for all state-changing requests (forms, AJAX, HTMX). Use `{{ csrf_field()|raw }}` in forms and `{{ csrf_token() }}` for AJAX/HTMX headers. All tokens use the `_csrf_token` session key.
9+
- **SPA-like Interactivity with HTMX:** Use HTMX attributes for seamless, partial updates (e.g., form submissions, file uploads) and request indicators (spinners). Example:
10+
```html
11+
<form
12+
hx-post="/validation-test"
13+
hx-target="#form-container"
14+
hx-swap="innerHTML"
15+
hx-indicator="#spinner"
16+
hx-trigger="submit delay:500ms"
17+
>
18+
...
19+
</form>
20+
```
21+
- **Private File Handling:** Upload files to private storage (not web-accessible). Download private files only via internal controller methods (no direct links). Example:
22+
```php
23+
// Upload
24+
Storage::put($file, '', 'private');
25+
// Download (controller)
26+
$path = Storage::path($filename, 'private');
27+
```
28+
- **Storage Improvements:** Storage paths are now always resolved relative to the project root. Public/private separation, symlink support for serving public files, and compatibility with the PHP built-in server for downloads.
29+
- **UI/UX:** Two-column grid for validation and file upload forms, spinner/indicator support, and SPA feel for user interactions.
30+
31+
---
32+
333
## Included by Default
434

535
- **HTMX** for modern, interactive UIs (already loaded in your base template)
@@ -543,6 +573,7 @@ To make uploaded files accessible via the web, create a symlink:
543573
```bash
544574
php sprout symlink:create
545575
```
576+
546577
- This links `public/storage` to `storage/app/public`.
547578
- On Windows, a directory junction is created for compatibility.
548579

@@ -577,6 +608,7 @@ if ($request->hasFile('avatar')) {
577608
```
578609

579610
### Notes
611+
580612
- Always use the `Storage` helper for uploads and URLs.
581613
- The storage root is now absolute for reliability.
582614
- No need to set or override the storage root in `.env` unless you have a custom setup.

RELEASE_NOTES.md

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
1-
# SproutPHP v0.1.7-alpha.3 Release Notes
1+
# Release Notes: v0.1.7-beta.1 (2024-06-09)
22

3-
## 🎉 New Features & Improvements
3+
## Highlights
44

5-
- **Absolute Storage Path:** Storage root is now set to an absolute path by default for reliability; no need to set in .env for most use cases.
6-
- **Updated Storage Helper:** Improved documentation and usage for file uploads and URL generation.
7-
- **Symlink Command:** Enhanced for better cross-platform compatibility (Windows junctions, Linux/macOS symlinks).
8-
- **Documentation:** DOCS.md updated to reflect new storage system, usage, and best practices.
9-
- **Bugfix:** Prevented duplicate/nested storage paths in uploads.
10-
- **General Improvements:** Codebase and documentation refinements.
5+
- **First Beta Release!** SproutPHP is now feature-complete and ready for broader testing and feedback.
6+
- **Dynamic Routing:** Support for route parameters (e.g., `/user/{id}`, `/file/{filename:.+}`) enables full CRUD and flexible APIs.
7+
- **CSRF Protection:** Robust, middleware-based CSRF protection for all state-changing requests (forms, AJAX, HTMX).
8+
- **SPA-like UX:** HTMX-powered forms and file uploads for a modern, seamless user experience.
9+
- **Private File Handling:** Secure upload/download of private files, accessible only via internal methods.
10+
- **Cleaner Codebase:** All CSRF logic is now in helpers/middleware, not exposed in entry scripts.
1111

12-
## 🛠️ Upgrade Guide
12+
## Upgrade Notes
1313

14-
- Use the new absolute storage root (no .env override needed).
15-
- Run `php sprout symlink:create` to ensure correct symlink/junction for uploads.
16-
- See DOCS.md for updated usage and examples.
14+
- All CSRF tokens now use the `_csrf_token` session key. Update any custom code to use the new helpers.
15+
- File downloads now use a query parameter (`?file=...`) for compatibility with the PHP built-in server.
16+
- If you use custom routes, you can now use `{param}` and `{param:regex}` patterns.
1717

18-
## 📅 Release Date
18+
## What's New
1919

20-
2024-06-13
20+
- Two-column grid UI for validation and file upload forms
21+
- SPA feel with HTMX indicators and partial updates
22+
- Consistent and secure CSRF handling everywhere
23+
- Improved storage path resolution and symlink support
2124

22-
## 📦 Framework Version
25+
## Thank You!
2326

24-
v0.1.7-alpha.3
25-
26-
---
27-
28-
**Release Date**: 2024-06-13
29-
**Framework Version**: v0.1.7-alpha.3
30-
**PHP Version**: 8.1+
31-
**Composer**: 2.0+
27+
Thank you for testing and contributing to SproutPHP. Please report any issues or feedback as we move toward a stable release.

app/Controllers/ValidationTestController.php

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,36 @@ public function index()
1818
]);
1919
}
2020

21-
return view('validation-test', ['title' => 'ValidationTestController Index']);
21+
// List private files for the right grid
22+
$privateFiles = $this->getPrivateFiles();
23+
24+
return view('validation-test', [
25+
'title' => 'ValidationTestController Index',
26+
'privateFiles' => $privateFiles,
27+
]);
28+
}
29+
30+
/**
31+
* Helper to get private files list
32+
*/
33+
protected function getPrivateFiles()
34+
{
35+
$privateDir = Storage::path('', 'private');
36+
$privateFiles = [];
37+
38+
if (is_dir($privateDir)) {
39+
foreach (scandir($privateDir) as $file) {
40+
if ($file !== '.' && $file !== '..' && is_file($privateDir . '/' . $file)) {
41+
$privateFiles[] = $file;
42+
}
43+
}
44+
}
45+
return $privateFiles;
2246
}
2347

2448
public function handleForm()
2549
{
50+
sleep(1); // 1 second delay
2651
$request = Request::capture();
2752

2853
$data = [
@@ -70,4 +95,76 @@ public function handleForm()
7095
'avatar_url' => isset($path) ? Storage::url($path) : null,
7196
]);
7297
}
98+
99+
/**
100+
* Handle private file upload
101+
*/
102+
public function handlePrivateUpload()
103+
{
104+
$request = Request::capture();
105+
$error = null;
106+
$path = null;
107+
if ($request->hasFile('private_file')) {
108+
$file = $request->file('private_file');
109+
$path = Storage::put($file, '', 'private');
110+
if (!$path) {
111+
$error = 'Private file upload failed.';
112+
}
113+
} else {
114+
$error = 'Please select a file to upload.';
115+
}
116+
117+
// HTMX: return only the private files list fragment
118+
if (isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true') {
119+
$privateFiles = $this->getPrivateFiles();
120+
return view('partials/private-files-list', [
121+
'privateFiles' => $privateFiles,
122+
'error' => $error,
123+
]);
124+
}
125+
126+
// Fallback: redirect back
127+
header('Location: ' . $_SERVER['HTTP_REFERER']);
128+
exit;
129+
}
130+
131+
/**
132+
* Render private files list fragment (for HTMX)
133+
*/
134+
public function privateFilesListFragment()
135+
{
136+
$privateFiles = $this->getPrivateFiles();
137+
return view('partials/private-files-list', [
138+
'privateFiles' => $privateFiles,
139+
]);
140+
}
141+
142+
/**
143+
* Securely download a private file
144+
*/
145+
public function downloadPrivateFile()
146+
{
147+
$filename = isset($_GET['file']) ? urldecode($_GET['file']) : null;
148+
if (!$filename) {
149+
http_response_code(400);
150+
echo 'Missing file parameter.';
151+
exit;
152+
}
153+
$privatePath = Storage::path($filename, 'private');
154+
if (!is_file($privatePath)) {
155+
http_response_code(404);
156+
echo 'File not found.';
157+
exit;
158+
}
159+
// Set headers for download
160+
header('Content-Description: File Transfer');
161+
header('Content-Type: application/octet-stream');
162+
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
163+
header('Expires: 0');
164+
header('Cache-Control: must-revalidate');
165+
header('Pragma: public');
166+
header('Content-Length: ' . filesize($privatePath));
167+
readfile($privatePath);
168+
exit;
169+
}
73170
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<h3 style="margin-top:2rem;">Private Files</h3>
2+
{% if privateFiles and privateFiles|length > 0 %}
3+
<ul style="list-style: none; padding-left: 0;">
4+
{% for file in privateFiles %}
5+
<li style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
6+
<span>{{ file|e }}</span>
7+
<a href="/validation-test/private-download?file={{ file|url_encode }}" target="_blank">Download</a>
8+
</li>
9+
{% endfor %}
10+
</ul>
11+
{% else %}
12+
<p>No private files uploaded yet.</p>
13+
{% endif %}
Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,43 @@
11
{% include "components/spinner.twig" %}
2-
<form
3-
hx-post="/validation-test"
4-
hx-target="#form-container"
5-
hx-swap="innerHTML"
6-
hx-indicator="#spinner"
7-
method="POST"
8-
autocomplete="off"
9-
enctype="multipart/form-data"
10-
>
11-
{# CSRF TOKEN #}
12-
{{ csrf_field()|raw }}
13-
<div>
14-
<label for="name">Name:</label>
15-
<input type="text" name="name" id="name" value="{{ old.name|e }}">
16-
{% if errors.name %}
17-
<div class="error" for="name" style="color: red;">{{ errors.name }}</div>
18-
{% endif %}
19-
</div>
20-
<div>
21-
<label for="email">Email:</label>
22-
<input type="email" name="email" id="email" value="{{ old.email|e }}">
23-
{% if errors.email %}
24-
<div class="error" for="email" style="color: red;">{{ errors.email }}</div>
25-
{% endif %}
26-
</div>
27-
<div>
28-
<label for="avatar">Avatar:</label>
29-
<input type="file" name="avatar" id="avatar" accept="image/*" />
30-
{% if errors.avatar %}
31-
<div class="error" for="avatar" style="color: red;">{{ errors.avatar }}</div>
32-
{% endif %}
33-
</div>
34-
<button type="submit">Submit
35-
</button>
2+
<form
3+
hx-post="/validation-test" hx-target="#form-container" hx-swap="innerHTML" hx-indicator="#spinner" method="POST" autocomplete="off" enctype="multipart/form-data">
4+
{# CSRF TOKEN #}
5+
{{ csrf_field()|raw }}
6+
<div>
7+
<label for="name">Name:</label>
8+
<input type="text" name="name" id="name" value="{{ old.name|e }}">
9+
{% if errors.name %}
10+
<div class="error" for="name" style="color: red;">{{ errors.name }}</div>
11+
{% endif %}
12+
</div>
13+
<div>
14+
<label for="email">Email:</label>
15+
<input type="email" name="email" id="email" value="{{ old.email|e }}">
16+
{% if errors.email %}
17+
<div class="error" for="email" style="color: red;">{{ errors.email }}</div>
18+
{% endif %}
19+
</div>
20+
<div>
21+
<label for="avatar">Avatar:</label>
22+
<input type="file" name="avatar" id="avatar" accept="image/*"/>
23+
{% if errors.avatar %}
24+
<div class="error" for="avatar" style="color: red;">{{ errors.avatar }}</div>
25+
{% endif %}
26+
</div>
27+
<button type="submit">Submit
28+
</button>
3629
</form>
3730
<script>
38-
document.addEventListener('focusin', function(e) {
39-
if (e.target.form && e.target.name) {
40-
const error = e.target.form.querySelector(`.error[for="${e.target.name}"]`);
41-
if (error) error.remove();
42-
}
31+
document.addEventListener('focusin', function (e) {
32+
if (e.target.form && e.target.name) {
33+
const error = e.target.form.querySelector(`.error[for="${
34+
e.target.name
35+
}"]`);
36+
if (error)
37+
error.remove();
38+
39+
40+
41+
}
4342
});
44-
</script>
43+
</script>
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<div class="success">
2-
<h2>Form Submitted Successfully!</h2>
3-
<p>Name: {{ name|e }}</p>
4-
<p>Email: {{ email|e }}</p>
5-
{% if avatar_url %}
6-
<p>Avatar: <img src="{{ avatar_url }}" alt="Avatar" style="max-width:100px;max-height:100px;"></p>
7-
{% endif %}
8-
<button hx-get="/validation-test" hx-target="#main-content" hx-push-url="true">Submit Another</button>
9-
</div>
2+
<h2>Form Submitted Successfully!</h2>
3+
<p>Name:
4+
{{ name|e }}</p>
5+
<p>Email:
6+
{{ email|e }}</p>
7+
{% if avatar_url %}
8+
<p>Avatar:
9+
<img src="{{ avatar_url }}" alt="Avatar" style="max-width:100px;max-height:100px;"></p>
10+
{% endif %}
11+
<button hx-get="/validation-test" hx-target="#form-container" hx-push-url="true">Submit Another</button>
12+
</div>

app/Views/validation-test.twig

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
{% extends "layouts/base.twig" %}
2-
{% block title %}validation-test{% endblock %}
2+
{% block title %}validation-test
3+
{% endblock %}
34

45
{% block content %}
5-
<h1>Validation Test Form</h1>
6-
<div id="form-container">
7-
{% include "partials/validation-form.twig" %}
8-
</div>
6+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start;">
7+
<div id="form-container">
8+
<h1>Validation Test Form</h1>
9+
{% include "partials/validation-form.twig" %}
10+
</div>
11+
<div id="private-upload-container" style="border-left: 1px solid #eee; padding-left: 2rem;">
12+
<h2>Private File Upload</h2>
13+
<form method="POST" action="/validation-test/private-upload" enctype="multipart/form-data" hx-post="/validation-test/private-upload" hx-target="#private-files-list" hx-swap="outerHTML">
14+
{{ csrf_field()|raw }}
15+
<div>
16+
<label for="private_file">Select file:</label>
17+
<input type="file" name="private_file" id="private_file" required/>
18+
</div>
19+
<button type="submit">Upload Private File</button>
20+
</form>
21+
<div id="private-files-list">
22+
{% include "partials/private-files-list.twig" %}
23+
</div>
24+
</div>
25+
</div>
926
{% endblock %}

0 commit comments

Comments
 (0)