Skip to content

Commit ba5fd49

Browse files
authored
release(v3.2.0): share pages revamp + portals browse/download-all + Pro branding upgrades
- shares: modern Dropbox-like share UI (file + folder), safe inline previews, and optional subfolder access - portals: subfolder browsing + pagination, list/gallery toggle, download-all zip, resumable uploads, submission IDs - branding (Pro): meta description + favicons + theme colors + login/app backgrounds + share/portal branding - security: sanitize footer HTML; tighten shared uploads with per-share upload token; validate share/portal paths
1 parent f205a82 commit ba5fd49

41 files changed

Lines changed: 7994 additions & 828 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,100 @@
11
# Changelog
22

3+
## Changes 01/28/2026 (v3.2.0)
4+
5+
`release(v3.2.0): share pages revamp + portals browse/download-all + Pro branding upgrades`
6+
7+
**Commit message**
8+
9+
```text
10+
release(v3.2.0): share pages revamp + portals browse/download-all + Pro branding upgrades
11+
12+
- shares: modern Dropbox-like share UI (file + folder), safe inline previews, and optional subfolder access
13+
- portals: subfolder browsing + pagination, list/gallery toggle, download-all zip, resumable uploads, submission IDs
14+
- branding (Pro): meta description + favicons + theme colors + login/app backgrounds + share/portal branding
15+
- security: sanitize footer HTML; tighten shared uploads with per-share upload token; validate share/portal paths
16+
```
17+
18+
**Added**
19+
20+
- **Shares (Core)**
21+
- **Folder shares** can optionally **include subfolders** (`allowSubfolders`) when creating the link.
22+
- Shared folder browsing supports `path=` for subfolder navigation (when enabled).
23+
- New public endpoint: **`GET /api/folder/downloadSharedFolder.php`** to download a shared folder (or subfolder) as a ZIP **(local storage only)**.
24+
- Shared downloads support `inline=1` for safe types (images/video/audio/pdf) and **never inline SVG**.
25+
26+
- **Share UI revamp (Core)**
27+
- New modern share layout + styles in `public/css/share.css` (folder + file share views).
28+
- Shared folder now supports:
29+
- **Download all**
30+
- **List/Gallery toggle**
31+
- **Search within the shared folder**
32+
- **Breadcrumbs** when subfolder browsing is enabled
33+
- Optional XHR upload progress UI for shared-folder uploads
34+
- File shares now generate a link that defaults to a landing page (`&view=1`) with metadata + preview.
35+
36+
- **Portals (Pro)**
37+
- New API: **`GET /api/pro/portals/listEntries.php`** (folders + files, pagination, optional “all files” mode).
38+
- Portal UI now supports:
39+
- **Subfolder browsing** (optional, per portal) using `?path=...`
40+
- **Breadcrumbs + pagination**
41+
- **List/Gallery toggle**
42+
- **Download all** (queues a ZIP via `/api/file/downloadZip.php`)
43+
- **Resumable uploads** for portals (with standard upload fallback)
44+
- Optional **Submission ID** tracking + show in thank-you screen
45+
- **5 New preset templates**
46+
47+
- **Branding upgrades (Pro)**
48+
- Admin branding now supports:
49+
- **Meta description**
50+
- **Favicons** (SVG/PNG/ICO), **Apple touch icon**, **Safari pinned mask icon + color**
51+
- **Theme color** (light/dark) for browser UI
52+
- **Login background** (light/dark) and **App background** (light/dark)
53+
- Optional **login tagline**
54+
- New `public/js/shareBranding.js` applies Pro branding to share pages (logo, accents, footer, icons, theme-color).
55+
- New `public/index.php` can serve `index.html` with branding meta/favicons applied (via `.htaccess` DirectoryIndex).
56+
57+
**Changed**
58+
59+
- **Shared folder data model**
60+
- Shared folder listing now returns a unified `entries[]` array (folders + files), plus `shareRoot`, `path`, and `allowSubfolders`.
61+
- Shared file download supports `path=subfolder/file.ext` (with subfolder gating).
62+
63+
- **Shared uploads hardening**
64+
- Shared-folder upload POST now supports `pass` + `path` and includes a per-share **`share_upload_token`** guard (HMAC) to reduce abuse.
65+
66+
- **Portal uploads enforcement**
67+
- Portal uploads are enforced server-side:
68+
- Must stay within the portal’s configured folder
69+
- Subfolder uploads are blocked unless the portal enables them
70+
- Portal sourceId must match (when configured)
71+
72+
- **Portals admin UX**
73+
- Adds portal theme presets (new industries), per-portal theme override fields, and portal logo field.
74+
- Adds “portal user” controls (optional per-portal user + password, preset modes).
75+
76+
- **Branding plumbing**
77+
- `main.js` now applies branding meta + icons + theme color + backgrounds, and **sanitizes footer HTML** before injecting.
78+
79+
**Fixed**
80+
81+
- Shared folder password form and file share password form now use the unified share UI and preserve `path` when prompting.
82+
- `downloadZip` now supports passing an explicit `sourceId` (local sources) by running inside a source context.
83+
- Various base-path issues resolved for share/portal JS/CSS includes by using `withBase()` and versioned assets.
84+
85+
**Security**
86+
87+
- Share and portal subpaths are normalized/validated (no `..`, invalid segments).
88+
- Shared downloads: SVG/SVGZ are always attachment-only (defense in depth).
89+
- Footer branding HTML is sanitized (allowlist) before inserting into DOM.
90+
91+
**Notes**
92+
93+
- `downloadSharedFolder.php` only supports **local** storage; remote adapters return a clear error.
94+
- Portals “download all” depends on ZIP being enabled for the account + server having the needed tooling for ZIP/7z where applicable.
95+
96+
---
97+
398
## Changes 01/24/2026 (v3.1.7)
499

5100
`release(v3.1.7): fix table header select-all checkbox + Pro bundle install progress UI (closes #99)`

public/.htaccess

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# FileRise portable .htaccess
33
# --------------------------------
44
Options -Indexes -Multiviews
5-
DirectoryIndex index.html
5+
DirectoryIndex index.php index.html
66

77
# Allow PATH_INFO for routes like /webdav.php/foo/bar
88
AcceptPathInfo On
@@ -37,6 +37,7 @@ RewriteRule ^portal/([A-Za-z0-9_-]+)$ portal.html?slug=$1 [L,QSA]
3737
RewriteCond %{REQUEST_URI} !^/api/ [NC]
3838
RewriteCond %{REQUEST_URI} !^/api\.php$ [NC]
3939
RewriteCond %{REQUEST_URI} !^/webdav\.php$ [NC]
40+
RewriteCond %{REQUEST_URI} !^/index\.php$ [NC]
4041
RewriteRule \.php$ - [F,L]
4142

4243
# 3) Never redirect local/dev hosts

public/api/file/share.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* tags={"Shares"},
1010
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string"), description="Share token"),
1111
* @OA\Parameter(name="pass", in="query", required=false, @OA\Schema(type="string"), description="Share password"),
12+
* @OA\Parameter(name="view", in="query", required=false, @OA\Schema(type="integer", enum={0,1}), description="Render share landing page when set to 1"),
13+
* @OA\Parameter(name="inline", in="query", required=false, @OA\Schema(type="integer", enum={0,1}), description="Allow inline rendering for safe types"),
1214
* @OA\Response(
1315
* response=200,
1416
* description="File stream or password prompt",

public/api/folder/createShareFolderLink.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
* @OA\Property(property="expirationValue", type="integer", example=60),
1919
* @OA\Property(property="expirationUnit", type="string", enum={"seconds","minutes","hours","days"}, example="minutes"),
2020
* @OA\Property(property="password", type="string", example=""),
21-
* @OA\Property(property="allowUpload", type="integer", enum={0,1}, example=0)
21+
* @OA\Property(property="allowUpload", type="integer", enum={0,1}, example=0),
22+
* @OA\Property(property="allowSubfolders", type="integer", enum={0,1}, example=0)
2223
* )
2324
* ),
2425
* @OA\Response(
@@ -27,7 +28,7 @@
2728
* @OA\JsonContent(
2829
* type="object",
2930
* @OA\Property(property="token", type="string", example="sf_abc123"),
30-
* @OA\Property(property="url", type="string", example="/api/folder/shareFolder.php?token=sf_abc123"),
31+
* @OA\Property(property="link", type="string", example="/api/folder/shareFolder.php?token=sf_abc123"),
3132
* @OA\Property(property="expires", type="integer", example=1700000000)
3233
* )
3334
* ),
@@ -41,4 +42,4 @@
4142
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
4243

4344
$folderController = new FolderController();
44-
$folderController->createShareFolderLink();
45+
$folderController->createShareFolderLink();

public/api/folder/downloadSharedFile.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
* operationId="downloadSharedFile",
1010
* tags={"Shared Folders"},
1111
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string")),
12-
* @OA\Parameter(name="file", in="query", required=true, @OA\Schema(type="string"), example="report.pdf"),
12+
* @OA\Parameter(name="pass", in="query", required=false, @OA\Schema(type="string")),
13+
* @OA\Parameter(name="file", in="query", required=false, @OA\Schema(type="string"), example="report.pdf"),
14+
* @OA\Parameter(name="path", in="query", required=false, @OA\Schema(type="string"), example="subfolder/report.pdf"),
15+
* @OA\Parameter(name="inline", in="query", required=false, @OA\Schema(type="integer", enum={0,1}), description="Allow inline rendering for safe types"),
1316
* @OA\Response(
1417
* response=200,
1518
* description="Binary file",
@@ -21,6 +24,7 @@
2124
* }
2225
* ),
2326
* @OA\Response(response=400, description="Invalid input"),
27+
* @OA\Response(response=403, description="Password required"),
2428
* @OA\Response(response=404, description="Not found")
2529
* )
2630
*/
@@ -29,4 +33,4 @@
2933
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
3034

3135
$folderController = new FolderController();
32-
$folderController->downloadSharedFile();
36+
$folderController->downloadSharedFile();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
// public/api/folder/downloadSharedFolder.php
3+
4+
/**
5+
* @OA\Get(
6+
* path="/api/folder/downloadSharedFolder.php",
7+
* summary="Download a shared folder as a ZIP",
8+
* description="Public endpoint; validates token/path and streams a ZIP archive.",
9+
* operationId="downloadSharedFolder",
10+
* tags={"Shared Folders"},
11+
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string")),
12+
* @OA\Parameter(name="pass", in="query", required=false, @OA\Schema(type="string")),
13+
* @OA\Parameter(name="path", in="query", required=false, @OA\Schema(type="string"), description="Subfolder path within the shared folder"),
14+
* @OA\Response(
15+
* response=200,
16+
* description="ZIP archive",
17+
* content={
18+
* "application/zip": @OA\MediaType(
19+
* mediaType="application/zip",
20+
* @OA\Schema(type="string", format="binary")
21+
* )
22+
* }
23+
* ),
24+
* @OA\Response(response=400, description="Invalid input"),
25+
* @OA\Response(response=403, description="Password required"),
26+
* @OA\Response(response=404, description="Not found")
27+
* )
28+
*/
29+
30+
require_once __DIR__ . '/../../../config/config.php';
31+
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
32+
33+
$folderController = new FolderController();
34+
$folderController->downloadSharedFolder();

public/api/folder/shareFolder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* tags={"Shared Folders"},
1111
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string")),
1212
* @OA\Parameter(name="pass", in="query", required=false, @OA\Schema(type="string")),
13+
* @OA\Parameter(name="path", in="query", required=false, @OA\Schema(type="string"), description="Subfolder path within the shared folder"),
1314
* @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", minimum=1), example=1),
1415
* @OA\Response(
1516
* response=200,
@@ -25,4 +26,4 @@
2526
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
2627

2728
$folderController = new FolderController();
28-
$folderController->shareFolder();
29+
$folderController->shareFolder();

public/api/folder/uploadToSharedFolder.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* type="object",
1818
* required={"token","fileToUpload"},
1919
* @OA\Property(property="token", type="string", description="Share token"),
20+
* @OA\Property(property="pass", type="string", description="Share password (if required)"),
21+
* @OA\Property(property="path", type="string", description="Optional subfolder path within the shared folder"),
2022
* @OA\Property(property="fileToUpload", type="string", format="binary", description="File to upload")
2123
* )
2224
* )
@@ -32,4 +34,4 @@
3234
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
3335

3436
$folderController = new FolderController();
35-
$folderController->uploadToSharedFolder();
37+
$folderController->uploadToSharedFolder();
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
// public/api/pro/portals/listEntries.php
3+
/**
4+
* List portal entries (folders + files) with pagination.
5+
*/
6+
declare(strict_types=1);
7+
8+
header('Content-Type: application/json; charset=utf-8');
9+
10+
require_once __DIR__ . '/../../../../config/config.php';
11+
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
12+
require_once PROJECT_ROOT . '/src/controllers/PortalController.php';
13+
14+
try {
15+
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET') {
16+
http_response_code(405);
17+
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
18+
return;
19+
}
20+
21+
if (session_status() !== PHP_SESSION_ACTIVE) {
22+
session_start();
23+
}
24+
25+
AdminController::requireAuth();
26+
27+
$slug = isset($_GET['slug']) ? trim((string)$_GET['slug']) : '';
28+
$path = isset($_GET['path']) ? (string)$_GET['path'] : '';
29+
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
30+
$perPage = isset($_GET['perPage']) ? (int)$_GET['perPage'] : 50;
31+
$all = !empty($_GET['all']);
32+
33+
$data = PortalController::listPortalEntries($slug, $path, $page, $perPage, $all);
34+
if (isset($data['error'])) {
35+
http_response_code((int)($data['status'] ?? 400));
36+
echo json_encode([
37+
'success' => false,
38+
'error' => $data['error'],
39+
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
40+
return;
41+
}
42+
43+
echo json_encode(
44+
['success' => true] + $data,
45+
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
46+
);
47+
} catch (Throwable $e) {
48+
http_response_code(500);
49+
echo json_encode([
50+
'success' => false,
51+
'error' => $e->getMessage(),
52+
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
53+
}

public/api/pro/portals/save.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@
6161
}
6262

6363
$ctrl = new AdminController();
64-
$ctrl->saveProPortals($portals);
64+
$result = $ctrl->saveProPortals($portals);
6565

66-
echo json_encode(['success' => true], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
66+
$payload = ['success' => true];
67+
if (is_array($result) && !empty($result['portalUsers'])) {
68+
$payload['portalUsers'] = $result['portalUsers'];
69+
}
70+
71+
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
6772
} catch (Throwable $e) {
6873
$code = $e instanceof InvalidArgumentException ? 400 : 500;
6974
http_response_code($code);

0 commit comments

Comments
 (0)