Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b1ebdd77f | ||
|
|
3726e2423d |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## changes 11/18/2025 (v1.9.10)
|
||||
|
||||
release(v1.9.10): add Pro bundle installer and admin panel polish
|
||||
|
||||
- Add FileRise Pro section in admin panel with license management and bundle upload
|
||||
- Persist Pro bundle under users/pro and sync public/api/pro endpoints on container startup
|
||||
- Improve admin config API: Pro metadata, license file handling, hardened auth/CSRF helpers
|
||||
- Update Pro badge/version UI with “update available” hint and link to filerise.net
|
||||
- Change Pro bundle installer to always overwrite existing bundle files for clean upgrades
|
||||
|
||||
---
|
||||
|
||||
## Changes 11/16/2025 (v1.9.9)
|
||||
|
||||
release(v1.9.9): fix(branding): sanitize custom logo URL preview
|
||||
|
||||
@@ -240,30 +240,57 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
// Final: env var wins, else fallback
|
||||
define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare);
|
||||
|
||||
// --------------------------------
|
||||
// FileRise Pro (optional add-on)
|
||||
// --------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// FileRise Pro bootstrap wiring
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Where the Pro license JSON lives
|
||||
// Inline license (optional; usually set via Admin UI and PRO_LICENSE_FILE)
|
||||
if (!defined('FR_PRO_LICENSE')) {
|
||||
$envLicense = getenv('FR_PRO_LICENSE');
|
||||
define('FR_PRO_LICENSE', $envLicense !== false ? trim((string)$envLicense) : '');
|
||||
}
|
||||
|
||||
// JSON license file used by AdminController::setLicense()
|
||||
if (!defined('PRO_LICENSE_FILE')) {
|
||||
define('PRO_LICENSE_FILE', PROJECT_ROOT . '/users/proLicense.json');
|
||||
}
|
||||
|
||||
// Inline/env license strings (optional)
|
||||
if (!defined('FR_PRO_LICENSE')) {
|
||||
define('FR_PRO_LICENSE', getenv('FR_PRO_LICENSE') ?: '');
|
||||
}
|
||||
// Optional plain-text license file (used as fallback in bootstrap)
|
||||
if (!defined('FR_PRO_LICENSE_FILE')) {
|
||||
define('FR_PRO_LICENSE_FILE', getenv('FR_PRO_LICENSE_FILE') ?: '');
|
||||
$lf = getenv('FR_PRO_LICENSE_FILE');
|
||||
if ($lf === false || $lf === '') {
|
||||
$lf = PROJECT_ROOT . '/users/proLicense.txt';
|
||||
}
|
||||
define('FR_PRO_LICENSE_FILE', $lf);
|
||||
}
|
||||
|
||||
// Optional Pro bootstrap (shipped only with Pro bundle)
|
||||
$proBootstrap = PROJECT_ROOT . '/src/pro/bootstrap_pro.php';
|
||||
if (is_file($proBootstrap)) {
|
||||
// Where Pro code lives by default → inside users volume
|
||||
$proDir = getenv('FR_PRO_BUNDLE_DIR');
|
||||
if ($proDir === false || $proDir === '') {
|
||||
$proDir = PROJECT_ROOT . '/users/pro';
|
||||
}
|
||||
$proDir = rtrim($proDir, "/\\");
|
||||
if (!defined('FR_PRO_BUNDLE_DIR')) {
|
||||
define('FR_PRO_BUNDLE_DIR', $proDir);
|
||||
}
|
||||
|
||||
// Try to load Pro bootstrap if enabled + present
|
||||
$proBootstrap = FR_PRO_BUNDLE_DIR . '/bootstrap_pro.php';
|
||||
if (@is_file($proBootstrap)) {
|
||||
require_once $proBootstrap;
|
||||
}
|
||||
|
||||
// Safe default so the rest of the app always has the constant
|
||||
// If bootstrap didn’t define these, give safe defaults
|
||||
if (!defined('FR_PRO_ACTIVE')) {
|
||||
define('FR_PRO_ACTIVE', false);
|
||||
}
|
||||
if (!defined('FR_PRO_INFO')) {
|
||||
define('FR_PRO_INFO', [
|
||||
'valid' => false,
|
||||
'error' => null,
|
||||
'payload' => null,
|
||||
]);
|
||||
}
|
||||
if (!defined('FR_PRO_BUNDLE_VERSION')) {
|
||||
define('FR_PRO_BUNDLE_VERSION', null);
|
||||
}
|
||||
8
public/api/admin/installProBundle.php
Normal file
8
public/api/admin/installProBundle.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$controller = new AdminController();
|
||||
$controller->installProBundle();
|
||||
@@ -14,6 +14,9 @@ function normalizeLogoPath(raw) {
|
||||
}
|
||||
|
||||
const version = window.APP_VERSION || "dev";
|
||||
// Hard-coded *FOR NOW* latest FileRise Pro bundle version for UI hints only.
|
||||
// Update this when I cut a new Pro ZIP.
|
||||
const PRO_LATEST_BUNDLE_VERSION = 'v1.0.0';
|
||||
|
||||
function getAdminTitle(isPro, proVersion) {
|
||||
const corePill = `
|
||||
@@ -22,6 +25,23 @@ function getAdminTitle(isPro, proVersion) {
|
||||
</span>
|
||||
`;
|
||||
|
||||
// Normalize versions so "v1.0.1" and "1.0.1" compare cleanly
|
||||
const norm = (v) => String(v || '').trim().replace(/^v/i, '');
|
||||
|
||||
const latestRaw = (typeof PRO_LATEST_BUNDLE_VERSION !== 'undefined'
|
||||
? PRO_LATEST_BUNDLE_VERSION
|
||||
: ''
|
||||
);
|
||||
|
||||
const currentRaw = (proVersion && proVersion !== 'not installed')
|
||||
? String(proVersion)
|
||||
: '';
|
||||
|
||||
const hasCurrent = !!norm(currentRaw);
|
||||
const hasLatest = !!norm(latestRaw);
|
||||
const hasUpdate = isPro && hasCurrent && hasLatest &&
|
||||
norm(currentRaw) !== norm(latestRaw);
|
||||
|
||||
if (!isPro) {
|
||||
// Free/core only
|
||||
return `
|
||||
@@ -30,18 +50,32 @@ function getAdminTitle(isPro, proVersion) {
|
||||
`;
|
||||
}
|
||||
|
||||
const pv = proVersion ? `Pro v${proVersion}` : 'Pro';
|
||||
const pvLabel = hasCurrent ? `Pro v${norm(currentRaw)}` : 'Pro';
|
||||
|
||||
const proPill = `
|
||||
<span class="badge badge-pill badge-warning admin-pro-badge">
|
||||
${pv}
|
||||
${pvLabel}
|
||||
</span>
|
||||
`;
|
||||
|
||||
const updateHint = hasUpdate
|
||||
? `
|
||||
<a
|
||||
href="https://filerise.net/pro/update.php"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="badge badge-pill badge-warning admin-pro-badge"
|
||||
style="cursor:pointer; text-decoration:none; margin-left:4px;">
|
||||
Pro update available
|
||||
</a>
|
||||
`
|
||||
: '';
|
||||
|
||||
return `
|
||||
${t("admin_panel")}
|
||||
${corePill}
|
||||
${proPill}
|
||||
${updateHint}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -450,6 +484,81 @@ function toggleSection(id) {
|
||||
}
|
||||
}
|
||||
|
||||
export function initProBundleInstaller() {
|
||||
try {
|
||||
const fileInput = document.getElementById('proBundleFile');
|
||||
const btn = document.getElementById('btnInstallProBundle');
|
||||
const statusEl = document.getElementById('proBundleStatus');
|
||||
|
||||
if (!fileInput || !btn || !statusEl) return;
|
||||
|
||||
// Allow names like: FileRisePro_v1.0.0.zip or FileRisePro-1.0.0.zip
|
||||
const PRO_ZIP_NAME_RE = /^FileRisePro[_-]v?[0-9]+\.[0-9]+\.[0-9]+\.zip$/i;
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
statusEl.textContent = 'Choose a FileRise Pro .zip bundle first.';
|
||||
statusEl.className = 'small text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
const name = file.name || '';
|
||||
if (!PRO_ZIP_NAME_RE.test(name)) {
|
||||
statusEl.textContent = 'Bundle must be named like "FileRisePro_v1.0.0.zip".';
|
||||
statusEl.className = 'small text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('bundle', file);
|
||||
|
||||
statusEl.textContent = 'Uploading and installing Pro bundle...';
|
||||
statusEl.className = 'small text-muted';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/admin/installProBundle.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': window.csrfToken || ''
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
let data = null;
|
||||
try {
|
||||
data = await resp.json();
|
||||
} catch (_) {
|
||||
// ignore JSON parse errors; handled below
|
||||
}
|
||||
|
||||
if (!resp.ok || !data || !data.success) {
|
||||
const msg = data && data.error
|
||||
? data.error
|
||||
: `HTTP ${resp.status}`;
|
||||
statusEl.textContent = 'Install failed: ' + msg;
|
||||
statusEl.className = 'small text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
const versionText = data.proVersion ? ` (version ${data.proVersion})` : '';
|
||||
statusEl.textContent = 'Pro bundle installed' + versionText + '. Reload the page to apply changes.';
|
||||
statusEl.className = 'small text-success';
|
||||
|
||||
if (typeof loadAdminConfigFunc === 'function') {
|
||||
loadAdminConfigFunc();
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Install failed: ' + (e && e.message ? e.message : String(e));
|
||||
statusEl.className = 'small text-danger';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to init Pro bundle installer', e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadShareLinksSection() {
|
||||
const container = document.getElementById("shareLinksContent");
|
||||
if (!container) return;
|
||||
@@ -1266,18 +1375,32 @@ async function ooProbeFrame(docsOrigin, timeoutMs = 4000) {
|
||||
// --- FileRise Pro / License section ---
|
||||
const proContent = document.getElementById("proContent");
|
||||
if (proContent) {
|
||||
// Normalize versions so "v1.0.1" and "1.0.1" compare cleanly
|
||||
const norm = (v) => (String(v || '').trim().replace(/^v/i, ''));
|
||||
|
||||
const currentVersionRaw = (proVersion && proVersion !== 'not installed') ? String(proVersion) : '';
|
||||
const latestVersionRaw = PRO_LATEST_BUNDLE_VERSION || '';
|
||||
const hasCurrent = !!norm(currentVersionRaw);
|
||||
const hasLatest = !!norm(latestVersionRaw);
|
||||
const hasUpdate = hasCurrent && hasLatest && norm(currentVersionRaw) !== norm(latestVersionRaw);
|
||||
|
||||
const proMetaHtml =
|
||||
isPro && (proType || proEmail || proVersion)
|
||||
? `
|
||||
<div class="pro-license-meta">
|
||||
<div class="pro-license-meta" style="margin-top:8px;font-size:12px;color:#777;">
|
||||
<div>
|
||||
✅ ${proType ? `License type: ${proType}` : 'License active'}
|
||||
${proType && proEmail ? ' • ' : ''}
|
||||
${proEmail ? `Licensed to: ${proEmail}` : ''}
|
||||
</div>
|
||||
${hasCurrent ? `
|
||||
<div>
|
||||
Pro bundle version: v${proVersion}
|
||||
</div>
|
||||
Installed Pro bundle: v${norm(currentVersionRaw)}
|
||||
</div>` : ''}
|
||||
${hasLatest ? `
|
||||
<div>
|
||||
Latest Pro bundle (UI hint): ${latestVersionRaw}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
@@ -1308,8 +1431,13 @@ if (proContent) {
|
||||
href="https://filerise.net/pro/update.php"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
Download latest Pro bundle
|
||||
class="btn btn-sm btn-pro-admin d-inline-flex align-items-center"
|
||||
>
|
||||
<span>Download latest Pro bundle</span>
|
||||
${hasUpdate ? `
|
||||
<span class="badge badge-light" style="margin-left:6px;">
|
||||
Update available
|
||||
</span>` : ''}
|
||||
</a>
|
||||
<small class="text-muted d-block" style="margin-top:4px;">
|
||||
Opens filerise.net in a new tab where you can enter your Pro license
|
||||
@@ -1322,7 +1450,8 @@ if (proContent) {
|
||||
href="https://filerise.net/pro/checkout.php"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
class="btn btn-sm btn-pro-admin"
|
||||
>
|
||||
Buy FileRise Pro
|
||||
</a>
|
||||
<small class="text-muted d-block" style="margin-top:4px;">
|
||||
@@ -1332,7 +1461,16 @@ if (proContent) {
|
||||
`}
|
||||
|
||||
<div class="form-group" style="margin-top:10px;">
|
||||
<label for="proLicenseInput" style="font-size:12px;">License key</label>
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<label for="proLicenseInput" style="font-size:12px; margin-bottom:0;">License key</label>
|
||||
${isPro && proLicense ? `
|
||||
<button type="button"
|
||||
class="btn btn-link btn-sm p-0"
|
||||
id="proCopyLicenseBtn">
|
||||
Copy current license
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<textarea
|
||||
id="proLicenseInput"
|
||||
class="form-control"
|
||||
@@ -1344,30 +1482,14 @@ if (proContent) {
|
||||
</small>
|
||||
</div>
|
||||
|
||||
${isPro && proLicense ? `
|
||||
<div style="margin-top:6px;">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="proCopyLicenseBtn">
|
||||
Copy current license
|
||||
</button>
|
||||
<small class="text-muted d-block" style="margin-top:4px;">
|
||||
Copies the saved license so you can reuse it for upgrades or downloads on filerise.net.
|
||||
</small>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="form-group" style="margin-top:6px;">
|
||||
<label style="font-size:12px;">Or upload license file</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="file"
|
||||
id="proLicenseFile"
|
||||
class="form-control"
|
||||
accept=".lic,.json,.txt,.filerise-lic"
|
||||
/>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="proLoadLicenseFileBtn">
|
||||
Load from file
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
id="proLicenseFile"
|
||||
class="form-control-file"
|
||||
accept=".lic,.json,.txt,.filerise-lic"
|
||||
/>
|
||||
<small class="text-muted">
|
||||
Supported: FileRise.lic, plain text with FRP1... or JSON containing a <code>license</code> field.
|
||||
</small>
|
||||
@@ -1376,26 +1498,44 @@ if (proContent) {
|
||||
<button type="button" class="btn btn-primary btn-sm" id="proSaveLicenseBtn" style="margin-top:8px;">
|
||||
Save license
|
||||
</button>
|
||||
|
||||
<div class="mt-3 border-top pt-3" style="margin-top:14px;">
|
||||
<h6 class="mb-1">Install / update Pro bundle</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
Upload the <code>.zip</code> bundle you downloaded from <a href="https://filerise.net" target="_blank" rel="noopener noreferrer">filerise.net</a>.
|
||||
This runs locally on your server and never contacts an external update service.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2" style="margin-top:4px;">
|
||||
<input type="file"
|
||||
id="proBundleFile"
|
||||
accept=".zip"
|
||||
class="form-control-file mb-2 mb-sm-0" />
|
||||
<button type="button"
|
||||
id="btnInstallProBundle"
|
||||
class="btn btn-sm btn-pro-admin">
|
||||
Install Pro bundle
|
||||
</button>
|
||||
</div>
|
||||
<div id="proBundleStatus" class="small mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Wire up local Pro bundle installer (upload .zip into core)
|
||||
initProBundleInstaller();
|
||||
|
||||
// Pre-fill textarea with saved license if present
|
||||
const licenseTextarea = document.getElementById('proLicenseInput');
|
||||
if (licenseTextarea && proLicense) {
|
||||
licenseTextarea.value = proLicense;
|
||||
}
|
||||
|
||||
// File upload → fill textarea (unchanged)
|
||||
// Auto-load license when a file is selected
|
||||
const fileInput = document.getElementById('proLicenseFile');
|
||||
const fileBtn = document.getElementById('proLoadLicenseFileBtn');
|
||||
|
||||
if (fileInput && fileBtn && licenseTextarea) {
|
||||
fileBtn.addEventListener('click', () => {
|
||||
if (fileInput && licenseTextarea) {
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) {
|
||||
showToast('Please choose a license file first.');
|
||||
return;
|
||||
}
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
@@ -1428,7 +1568,7 @@ if (proContent) {
|
||||
});
|
||||
}
|
||||
|
||||
// Copy current license button
|
||||
// Copy current license button (now inline next to the label)
|
||||
const proCopyBtn = document.getElementById('proCopyLicenseBtn');
|
||||
if (proCopyBtn && proLicense) {
|
||||
proCopyBtn.addEventListener('click', async () => {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// generated by CI
|
||||
window.APP_VERSION = 'v1.9.9';
|
||||
window.APP_VERSION = 'v1.9.10';
|
||||
|
||||
@@ -272,6 +272,265 @@ public function setLicense(): void
|
||||
}
|
||||
}
|
||||
|
||||
public function installProBundle(): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
// Guard rails: method + auth + CSRF
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
return;
|
||||
}
|
||||
|
||||
self::requireAuth();
|
||||
self::requireAdmin();
|
||||
self::requireCsrf();
|
||||
|
||||
// Ensure ZipArchive is available
|
||||
if (!class_exists('\\ZipArchive')) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'ZipArchive extension is required on the server.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic upload validation
|
||||
if (empty($_FILES['bundle']) || !is_array($_FILES['bundle'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing uploaded bundle (field "bundle").']);
|
||||
return;
|
||||
}
|
||||
|
||||
$f = $_FILES['bundle'];
|
||||
|
||||
if (!empty($f['error']) && $f['error'] !== UPLOAD_ERR_OK) {
|
||||
$msg = 'Upload error.';
|
||||
switch ($f['error']) {
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
$msg = 'Uploaded file exceeds size limit.';
|
||||
break;
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
$msg = 'Uploaded file was only partially received.';
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
$msg = 'No file was uploaded.';
|
||||
break;
|
||||
default:
|
||||
$msg = 'Upload failed with error code ' . (int)$f['error'];
|
||||
break;
|
||||
}
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
$tmpName = $f['tmp_name'] ?? '';
|
||||
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid uploaded file.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against unexpectedly large bundles (e.g., >100MB)
|
||||
$size = isset($f['size']) ? (int)$f['size'] : 0;
|
||||
if ($size <= 0 || $size > 100 * 1024 * 1024) {
|
||||
http_response_code(413);
|
||||
echo json_encode(['success' => false, 'error' => 'Bundle size is invalid or too large (max 100MB).']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: require .zip extension by name (best-effort)
|
||||
$origName = (string)($f['name'] ?? '');
|
||||
if ($origName !== '' && !preg_match('/\.zip$/i', $origName)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Bundle must be a .zip file.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare temp working dir
|
||||
$tempRoot = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
|
||||
$workDir = $tempRoot . DIRECTORY_SEPARATOR . 'filerise_pro_' . bin2hex(random_bytes(8));
|
||||
if (!@mkdir($workDir, 0700, true)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to prepare temp dir.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$zipPath = $workDir . DIRECTORY_SEPARATOR . 'bundle.zip';
|
||||
if (!@move_uploaded_file($tmpName, $zipPath)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to move uploaded bundle.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipPath) !== true) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to open ZIP bundle.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$installed = [
|
||||
'src' => [],
|
||||
'public' => [],
|
||||
'docs' => [],
|
||||
];
|
||||
|
||||
$projectRoot = rtrim(PROJECT_ROOT, DIRECTORY_SEPARATOR);
|
||||
|
||||
// Where Pro bundle code lives (defaults to PROJECT_ROOT . '/users/pro')
|
||||
$bundleRoot = defined('FR_PRO_BUNDLE_DIR')
|
||||
? rtrim(FR_PRO_BUNDLE_DIR, DIRECTORY_SEPARATOR)
|
||||
: ($projectRoot . DIRECTORY_SEPARATOR . 'users' . DIRECTORY_SEPARATOR . 'pro');
|
||||
|
||||
// Put README-Pro.txt / LICENSE-Pro.txt inside the bundle dir as well
|
||||
$proDocsDir = $bundleRoot;
|
||||
if (!is_dir($proDocsDir)) {
|
||||
@mkdir($proDocsDir, 0755, true);
|
||||
}
|
||||
|
||||
$allowedTopLevel = ['LICENSE-Pro.txt', 'README-Pro.txt'];
|
||||
|
||||
// Iterate entries and selectively extract/copy expected files only
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if ($name === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalise and guard
|
||||
$name = ltrim($name, "/\\");
|
||||
if ($name === '' || substr($name, -1) === '/') {
|
||||
continue; // skip directories
|
||||
}
|
||||
if (strpos($name, '../') !== false || strpos($name, '..\\') !== false) {
|
||||
continue; // path traversal guard
|
||||
}
|
||||
|
||||
// Ignore macOS Finder junk: __MACOSX and "._" resource forks
|
||||
$base = basename($name);
|
||||
if (
|
||||
str_starts_with($name, '__MACOSX/') ||
|
||||
str_contains($name, '/__MACOSX/') ||
|
||||
str_starts_with($base, '._')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetPath = null;
|
||||
$category = null;
|
||||
|
||||
if (in_array($name, $allowedTopLevel, true)) {
|
||||
// Docs → bundle dir (under /users/pro)
|
||||
$targetPath = $proDocsDir . DIRECTORY_SEPARATOR . $name;
|
||||
$category = 'docs';
|
||||
|
||||
} elseif (strpos($name, 'src/pro/') === 0) {
|
||||
// e.g. src/pro/bootstrap_pro.php -> FR_PRO_BUNDLE_DIR/bootstrap_pro.php
|
||||
$relative = substr($name, strlen('src/pro/'));
|
||||
if ($relative === '' || substr($relative, -1) === '/') {
|
||||
continue;
|
||||
}
|
||||
$targetPath = $bundleRoot . DIRECTORY_SEPARATOR . $relative;
|
||||
$category = 'src';
|
||||
|
||||
} elseif (strpos($name, 'public/api/pro/') === 0) {
|
||||
// e.g. public/api/pro/uploadBrandLogo.php
|
||||
$relative = substr($name, strlen('public/api/pro/'));
|
||||
if ($relative === '' || substr($relative, -1) === '/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Persist under bundle dir so it survives image rebuilds:
|
||||
// users/pro/public/api/pro/...
|
||||
$targetPath = $bundleRoot
|
||||
. DIRECTORY_SEPARATOR . 'public'
|
||||
. DIRECTORY_SEPARATOR . 'api'
|
||||
. DIRECTORY_SEPARATOR . 'pro'
|
||||
. DIRECTORY_SEPARATOR . $relative;
|
||||
$category = 'public';
|
||||
} else {
|
||||
// Skip anything outside these prefixes
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$targetPath || !$category) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track whether we're overwriting an existing file (for reporting only)
|
||||
$wasExisting = is_file($targetPath);
|
||||
|
||||
// Read from ZIP entry
|
||||
$stream = $zip->getStream($name);
|
||||
if (!$stream) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dir = dirname($targetPath);
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||||
fclose($stream);
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to create destination directory for ' . $name]);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = stream_get_contents($stream);
|
||||
fclose($stream);
|
||||
if ($data === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to read data for ' . $name]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Always overwrite target file on install/upgrade
|
||||
if (@file_put_contents($targetPath, $data) === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to write ' . $name]);
|
||||
return;
|
||||
}
|
||||
|
||||
@chmod($targetPath, 0644);
|
||||
|
||||
// Track what we installed (and whether it was overwritten)
|
||||
if (!isset($installed[$category])) {
|
||||
$installed[$category] = [];
|
||||
}
|
||||
$installed[$category][] = $targetPath . ($wasExisting ? ' (overwritten)' : '');
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
// Best-effort cleanup; ignore failures
|
||||
@unlink($zipPath);
|
||||
@rmdir($workDir);
|
||||
|
||||
// Reflect current Pro status in response if bootstrap was loaded
|
||||
$proActive = defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE;
|
||||
$proPayload = defined('FR_PRO_INFO') && is_array(FR_PRO_INFO)
|
||||
? (FR_PRO_INFO['payload'] ?? null)
|
||||
: null;
|
||||
$proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Pro bundle installed.',
|
||||
'installed' => $installed,
|
||||
'proActive' => (bool)$proActive,
|
||||
'proVersion' => $proVersion,
|
||||
'proPayload' => $proPayload,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Exception during bundle install: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
17
start.sh
17
start.sh
@@ -72,6 +72,23 @@ for d in uploads users metadata; do
|
||||
chmod 775 "${tgt}"
|
||||
done
|
||||
|
||||
# 2.4) Sync FileRise Pro public endpoints from persistent bundle
|
||||
BUNDLE_PRO_PUBLIC="/var/www/users/pro/public/api/pro"
|
||||
LIVE_PRO_PUBLIC="/var/www/public/api/pro"
|
||||
|
||||
if [ -d "${BUNDLE_PRO_PUBLIC}" ]; then
|
||||
echo "[startup] Syncing FileRise Pro public endpoints..."
|
||||
mkdir -p "${LIVE_PRO_PUBLIC}"
|
||||
|
||||
# Copy files from bundle to live api/pro (overwrite for upgrades)
|
||||
cp -R "${BUNDLE_PRO_PUBLIC}/." "${LIVE_PRO_PUBLIC}/" || echo "[startup] Pro sync copy failed (continuing)"
|
||||
|
||||
# Normalize ownership/permissions
|
||||
chown -R www-data:www-data "${LIVE_PRO_PUBLIC}" || echo "[startup] chown api/pro failed (continuing)"
|
||||
find "${LIVE_PRO_PUBLIC}" -type d -exec chmod 755 {} \; 2>/dev/null || true
|
||||
find "${LIVE_PRO_PUBLIC}" -type f -exec chmod 644 {} \; 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 3) Ensure PHP conf dir & set upload limits
|
||||
mkdir -p /etc/php/8.3/apache2/conf.d
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
|
||||
Reference in New Issue
Block a user