mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
update: move server handling to a new file.
update: tooltip things. remove: unused var.
This commit is contained in:
@@ -12,7 +12,6 @@ $version = $this->getParam('version', '');
|
||||
$organization = $this->getParam('organization', '');
|
||||
$image = $this->getParam('image', '');
|
||||
$dbService = $this->getParam('database');
|
||||
$hostPath = $this->getParam('hostPath', '');
|
||||
|
||||
?>services:
|
||||
traefik:
|
||||
|
||||
@@ -7,10 +7,12 @@ $vars = $vars ?? [];
|
||||
$defaultHttpPort = $defaultHttpPort ?? '80';
|
||||
$defaultHttpsPort = $defaultHttpsPort ?? '443';
|
||||
$defaultAppDomain = $vars['_APP_DOMAIN']['default'] ?? 'localhost';
|
||||
$defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAppDomain;
|
||||
$defaultEmailCertificates = $vars['_APP_EMAIL_CERTIFICATES']['default'] ?? '';
|
||||
$defaultSecretKey = $vars['_APP_OPENSSL_KEY_V1']['default'] ?? '';
|
||||
$defaultAssistantOpenAIKey = $vars['_APP_ASSISTANT_OPENAI_API_KEY']['default'] ?? '';
|
||||
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
|
||||
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
|
||||
$isLocalInstall = !empty(getenv('APPWRITE_INSTALLER_LOCAL'));
|
||||
if ($installMode === 'mock' && $defaultEmailCertificates === '') {
|
||||
$defaultEmailCertificates = 'walterobrien@example.com';
|
||||
@@ -38,6 +40,7 @@ if (!is_file($stepFile)) {
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
|
||||
}
|
||||
$cssVersion = @filemtime(__DIR__ . '/installer/css/styles.css') ?: time();
|
||||
$tooltipsVersion = @filemtime(__DIR__ . '/installer/js/tooltips.js') ?: time();
|
||||
$constantsVersion = @filemtime(__DIR__ . '/installer/js/constants.js') ?: time();
|
||||
$stepsVersion = @filemtime(__DIR__ . '/installer/js/steps.js') ?: time();
|
||||
$installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time();
|
||||
@@ -50,6 +53,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
<title><?php echo $isUpgrade ? 'Appwrite Update' : 'Appwrite Installation'; ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="installer/icons/appwrite-mark.svg">
|
||||
<link rel="stylesheet" href="installer/css/styles.css?v=<?php echo $cssVersion; ?>">
|
||||
<script src="installer/js/tooltips.js?v=<?php echo $tooltipsVersion; ?>" defer></script>
|
||||
<script src="installer/js/constants.js?v=<?php echo $constantsVersion; ?>" defer></script>
|
||||
<script src="installer/js/steps.js?v=<?php echo $stepsVersion; ?>" defer></script>
|
||||
<script src="installer/js/installer.js?v=<?php echo $installerVersion; ?>" defer></script>
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
|
||||
/* Border radius */
|
||||
--border-radius-s: 8px;
|
||||
--border-radius-xs: 6px;
|
||||
--border-radius-m: 12px;
|
||||
--border-radius-l: 16px;
|
||||
|
||||
@@ -263,57 +264,14 @@ body {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.step-layout.is-locked .selector-card,
|
||||
.step-layout.is-locked .accordion-toggle {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.step-layout.is-locked .selector-card {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.selector-card.is-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.selector-group.is-locked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(250, 250, 251, 0.8);
|
||||
border-radius: var(--border-radius-s);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.selector-lock {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--gap-xs);
|
||||
color: var(--fgcolor-neutral-secondary);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.selector-lock svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.selector-lock {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--border-radius-s);
|
||||
border: var(--border-width-s) solid var(--border-neutral);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
margin: auto;
|
||||
.selector-card.is-disabled .selector-content,
|
||||
.selector-card.is-disabled .selector-icon {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.installer-step {
|
||||
@@ -473,7 +431,7 @@ body {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--gap-xxxs);
|
||||
transition: height var(--duration-medium) var(--ease-standard);
|
||||
transition: height var(--duration-slow) var(--ease-standard);
|
||||
}
|
||||
|
||||
.install-header {
|
||||
@@ -503,7 +461,7 @@ body {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity var(--duration-medium) var(--ease-standard),
|
||||
transform var(--duration-slow) var(--ease-emphasized);
|
||||
transform var(--duration-extended) var(--ease-standard);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@@ -580,7 +538,7 @@ body {
|
||||
|
||||
.install-row.is-entering {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.install-icon {
|
||||
@@ -949,6 +907,7 @@ body {
|
||||
gap: var(--gap-m);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selector-card {
|
||||
@@ -968,6 +927,10 @@ body {
|
||||
transition: all var(--duration-fast) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.selector-card.has-tooltip {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selector-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -982,6 +945,49 @@ body {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selector-card.is-disabled::before,
|
||||
.selector-card.is-disabled:hover::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.selector-group.is-locked .selector-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selector-group.is-locked .selector-card:hover::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.selector-card.has-tooltip .tooltip {
|
||||
top: calc(100% + 6px);
|
||||
bottom: auto;
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
}
|
||||
|
||||
.selector-card.has-tooltip {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.selector-card.has-tooltip:hover {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.selector-card.has-tooltip:hover .tooltip,
|
||||
.selector-card.has-tooltip:focus-within .tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
transition: opacity var(--duration-short) var(--ease-standard),
|
||||
transform var(--duration-short) var(--ease-standard),
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
.tooltip-db-locked {
|
||||
width: 193px;
|
||||
max-width: none;
|
||||
min-width: 193px;
|
||||
text-align: center;
|
||||
}
|
||||
.selector-card.selected {
|
||||
box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral-stronger);
|
||||
}
|
||||
@@ -1303,6 +1309,23 @@ body {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tooltip.is-open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
transition: opacity var(--duration-short) var(--ease-standard),
|
||||
transform var(--duration-short) var(--ease-standard),
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
.tooltip-portal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.tooltip-assistant {
|
||||
width: 246px;
|
||||
max-width: 246px;
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
installFallbackDelayMs: 12000,
|
||||
redirectDelayMs: 500,
|
||||
mockStepDelayMs: 1800,
|
||||
progressTransitionDelayMs: 200,
|
||||
progressTransitionDelayMs: 320,
|
||||
progressCompleteDelayMs: 140,
|
||||
mockErrorDetails: {
|
||||
output: [
|
||||
'Failed to start containers: appwrite-worker-webhooks',
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
installFallbackDelay: CONSTANTS.installFallbackDelayMs ?? 12000,
|
||||
redirectDelay: CONSTANTS.redirectDelayMs ?? 500,
|
||||
mockStepDelay: CONSTANTS.mockStepDelayMs ?? 1800,
|
||||
progressTransitionDelay: CONSTANTS.progressTransitionDelayMs ?? 140
|
||||
progressTransitionDelay: CONSTANTS.progressTransitionDelayMs ?? 140,
|
||||
progressCompleteDelay: CONSTANTS.progressCompleteDelayMs ?? 120
|
||||
};
|
||||
|
||||
const formState = {
|
||||
@@ -423,21 +424,20 @@
|
||||
};
|
||||
|
||||
const lockDatabaseSelection = (root, lockedDatabase) => {
|
||||
const cards = root.querySelectorAll('.selector-card');
|
||||
cards.forEach((card) => {
|
||||
card.classList.add('is-disabled');
|
||||
const input = card.querySelector('input[name="database"]');
|
||||
if (input) {
|
||||
input.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (lockedDatabase) {
|
||||
const radio = root.querySelector(`input[name="database"][value="${lockedDatabase}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
updateDatabaseSelection(radio, root);
|
||||
}
|
||||
const radios = root.querySelectorAll('input[name="database"]');
|
||||
radios.forEach((radio) => {
|
||||
const isLockedChoice = radio.value === lockedDatabase;
|
||||
const card = radio.closest('.selector-card');
|
||||
radio.disabled = !isLockedChoice;
|
||||
if (card) {
|
||||
card.classList.toggle('is-disabled', !isLockedChoice);
|
||||
}
|
||||
if (isLockedChoice) {
|
||||
radio.checked = true;
|
||||
updateDatabaseSelection(radio, root);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -548,15 +548,6 @@
|
||||
bindDatabaseSelection(root);
|
||||
}
|
||||
|
||||
if (isUpgradeMode()) {
|
||||
const radios = root.querySelectorAll('input[name="database"]');
|
||||
radios.forEach((radio) => {
|
||||
radio.checked = false;
|
||||
});
|
||||
const cards = root.querySelectorAll('.selector-card');
|
||||
cards.forEach((card) => card.classList.remove('selected'));
|
||||
}
|
||||
|
||||
const hostname = root.querySelector('#hostname');
|
||||
const httpPort = root.querySelector('#http-port');
|
||||
const httpsPort = root.querySelector('#https-port');
|
||||
@@ -582,6 +573,7 @@
|
||||
|
||||
setupResetButtons(root);
|
||||
setupAccordion(root);
|
||||
Tooltips?.setupTooltipPortals?.(root);
|
||||
};
|
||||
|
||||
const generateSecretKey = () => {
|
||||
@@ -631,6 +623,8 @@
|
||||
setTooltipText(wrapper, defaultText);
|
||||
};
|
||||
|
||||
const Tooltips = window.InstallerTooltips || null;
|
||||
|
||||
const hydrateStep2State = (root) => {
|
||||
const value = root.querySelector('#secret-key')?.value;
|
||||
if (formState.opensslKey) return;
|
||||
@@ -911,12 +905,15 @@
|
||||
const hasPort = rawDomain.includes(':') || rawDomain.startsWith('[');
|
||||
let host = rawDomain;
|
||||
const hostForProtocol = extractHostname(rawDomain);
|
||||
const normalizedHost = hostForProtocol.toLowerCase();
|
||||
if (hostForProtocol === '0.0.0.0') {
|
||||
host = rawDomain.replace('0.0.0.0', 'localhost');
|
||||
} else if (normalizedHost === 'traefik') {
|
||||
host = rawDomain.replace(hostForProtocol, 'localhost');
|
||||
}
|
||||
let protocol = 'http';
|
||||
let port = httpPort;
|
||||
if (httpsPort && httpsPort !== '0' && !isLocalHost(hostForProtocol)) {
|
||||
if (httpsPort && httpsPort !== '0' && !isLocalHost(normalizedHost)) {
|
||||
protocol = 'https';
|
||||
port = httpsPort;
|
||||
}
|
||||
@@ -1071,6 +1068,7 @@
|
||||
const progressState = new Map();
|
||||
let pendingProgressTimer = null;
|
||||
let pendingProgressStep = null;
|
||||
let pendingCompletionTimer = null;
|
||||
syncInstallLockFlag();
|
||||
applyLockPayload();
|
||||
applyBodyDefaults();
|
||||
@@ -1199,6 +1197,10 @@
|
||||
pendingProgressTimer = null;
|
||||
pendingProgressStep = null;
|
||||
}
|
||||
if (pendingCompletionTimer) {
|
||||
clearTimeout(pendingCompletionTimer);
|
||||
pendingCompletionTimer = null;
|
||||
}
|
||||
const step = getStepDefinition(payload.step) || {
|
||||
id: payload.step,
|
||||
inProgress: payload.message || payload.step,
|
||||
@@ -1206,24 +1208,32 @@
|
||||
};
|
||||
if (payload.status === 'in-progress') {
|
||||
const currentIndex = INSTALLATION_STEPS.findIndex((candidate) => candidate.id === step.id);
|
||||
let didAutoComplete = false;
|
||||
const completionTargets = [];
|
||||
if (currentIndex > 0) {
|
||||
for (let i = 0; i < currentIndex; i += 1) {
|
||||
const previousStep = INSTALLATION_STEPS[i];
|
||||
const previousState = progressState.get(previousStep.id);
|
||||
if (previousState && previousState.status !== 'completed') {
|
||||
progressState.set(previousStep.id, {
|
||||
status: 'completed',
|
||||
message: previousStep.done,
|
||||
completionTargets.push({
|
||||
step: previousStep,
|
||||
details: previousState.details
|
||||
});
|
||||
didAutoComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (didAutoComplete && currentIndex > 0) {
|
||||
renderProgress();
|
||||
if (completionTargets.length && currentIndex > 0) {
|
||||
pendingProgressStep = payload.step;
|
||||
pendingCompletionTimer = setTimeout(() => {
|
||||
pendingCompletionTimer = null;
|
||||
completionTargets.forEach(({ step: previousStep, details }) => {
|
||||
progressState.set(previousStep.id, {
|
||||
status: 'completed',
|
||||
message: previousStep.done,
|
||||
details
|
||||
});
|
||||
});
|
||||
renderProgress();
|
||||
}, TIMINGS.progressCompleteDelay);
|
||||
pendingProgressTimer = setTimeout(() => {
|
||||
pendingProgressTimer = null;
|
||||
pendingProgressStep = null;
|
||||
@@ -1558,6 +1568,7 @@
|
||||
if (!container) return;
|
||||
const root = container.querySelector('.step-layout') || container;
|
||||
const normalized = clampStep(step);
|
||||
Tooltips?.cleanupTooltipPortals?.();
|
||||
if (normalized !== 3 && reviewListener) {
|
||||
document.removeEventListener('installer:state-change', reviewListener);
|
||||
reviewListener = null;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
(() => {
|
||||
const tooltipPortals = new Set();
|
||||
|
||||
const positionTooltipPortal = (tooltip, anchor) => {
|
||||
if (!tooltip || !anchor) return;
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const offset = Number(tooltip.dataset.tooltipOffset || 6);
|
||||
const padding = 8;
|
||||
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
left = Math.max(padding, Math.min(left, window.innerWidth - tooltipRect.width - padding));
|
||||
const top = rect.bottom + offset;
|
||||
tooltip.style.left = `${left}px`;
|
||||
tooltip.style.top = `${top}px`;
|
||||
};
|
||||
|
||||
const attachTooltipPortal = (tooltip) => {
|
||||
if (!tooltip || tooltip.dataset.portalInitialized === 'true') return;
|
||||
const anchor = tooltip.parentElement;
|
||||
if (!anchor) return;
|
||||
|
||||
tooltip.dataset.portalInitialized = 'true';
|
||||
tooltip.classList.add('tooltip-portal');
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
const show = () => {
|
||||
tooltip.classList.add('is-open');
|
||||
positionTooltipPortal(tooltip, anchor);
|
||||
};
|
||||
const hide = () => {
|
||||
tooltip.classList.remove('is-open');
|
||||
};
|
||||
const refresh = () => {
|
||||
if (tooltip.classList.contains('is-open')) {
|
||||
positionTooltipPortal(tooltip, anchor);
|
||||
}
|
||||
};
|
||||
|
||||
anchor.addEventListener('mouseenter', show);
|
||||
anchor.addEventListener('mouseleave', hide);
|
||||
anchor.addEventListener('focusin', show);
|
||||
anchor.addEventListener('focusout', hide);
|
||||
window.addEventListener('scroll', refresh, true);
|
||||
window.addEventListener('resize', refresh);
|
||||
|
||||
tooltipPortals.add({
|
||||
tooltip,
|
||||
cleanup: () => {
|
||||
anchor.removeEventListener('mouseenter', show);
|
||||
anchor.removeEventListener('mouseleave', hide);
|
||||
anchor.removeEventListener('focusin', show);
|
||||
anchor.removeEventListener('focusout', hide);
|
||||
window.removeEventListener('scroll', refresh, true);
|
||||
window.removeEventListener('resize', refresh);
|
||||
if (tooltip.parentElement) {
|
||||
tooltip.parentElement.removeChild(tooltip);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setupTooltipPortals = (root) => {
|
||||
if (!root) return;
|
||||
const portalTooltips = root.querySelectorAll('.tooltip[data-tooltip-portal]');
|
||||
portalTooltips.forEach((tooltip) => attachTooltipPortal(tooltip));
|
||||
};
|
||||
|
||||
const cleanupTooltipPortals = () => {
|
||||
tooltipPortals.forEach((entry) => entry.cleanup());
|
||||
tooltipPortals.clear();
|
||||
};
|
||||
|
||||
window.InstallerTooltips = {
|
||||
setupTooltipPortals,
|
||||
cleanupTooltipPortals
|
||||
};
|
||||
})();
|
||||
@@ -7,7 +7,10 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443';
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? '';
|
||||
$defaultDatabase = $defaultDatabase ?? 'mongodb';
|
||||
$selectedDatabase = $isUpgrade ? '' : ($lockedDatabase ?: $defaultDatabase);
|
||||
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
|
||||
$isDatabaseLocked = !empty($lockedDatabase);
|
||||
$mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb';
|
||||
$mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb';
|
||||
$hostnameValue = htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8');
|
||||
$httpPortValue = htmlspecialchars((string) $defaultHttpPort, ENT_QUOTES, 'UTF-8');
|
||||
$httpsPortValue = htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8');
|
||||
@@ -38,31 +41,30 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
|
||||
<div class="input-group stack-xs">
|
||||
<label class="label-text typography-text-m-500 text-neutral-secondary">Database</label>
|
||||
<div class="selector-group<?php echo $lockedDatabase ? ' is-locked' : ''; ?>">
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $lockedDatabase ? ' is-disabled' : ''; ?>">
|
||||
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $lockedDatabase ? 'disabled' : ''; ?>>
|
||||
<div class="selector-group<?php echo $isDatabaseLocked ? ' is-locked' : ''; ?>">
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $mongoDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mongoDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mongoDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
<div class="typography-text-m-500 text-neutral-primary">MongoDB</div>
|
||||
<div class="typography-text-xs-400 text-neutral-secondary">Document based database</div>
|
||||
</div>
|
||||
<img src="installer/icons/mongodb.svg" alt="MongoDB" class="selector-icon">
|
||||
<?php if ($mongoDisabled) { ?>
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $lockedDatabase ? ' is-disabled' : ''; ?>">
|
||||
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $lockedDatabase ? 'disabled' : ''; ?>>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $mariaDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mariaDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mariaDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
<div class="typography-text-m-500 text-neutral-primary">MariaDB</div>
|
||||
<div class="typography-text-xs-400 text-neutral-secondary">Relational SQL database</div>
|
||||
</div>
|
||||
<img src="installer/icons/mariadb.svg" alt="MariaDB" class="selector-icon">
|
||||
<?php if ($mariaDisabled) { ?>
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
|
||||
<?php if ($lockedDatabase) { ?>
|
||||
<div class="selector-lock" aria-hidden="true">
|
||||
<?php include __DIR__ . '/../../icons/lock.svg'; ?>
|
||||
<span class="typography-caption-500 text-neutral-secondary">Database cannot be changed after the initial setup.</span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/src/Appwrite/Platform/Installer/Server.php';
|
||||
|
||||
use Appwrite\Platform\Installer\Server as InstallerServer;
|
||||
|
||||
const INSTALLER_DEFAULT_HOST = 'localhost';
|
||||
const INSTALLER_DEFAULT_PORT = '8080';
|
||||
const INSTALLER_DEFAULT_IMAGE = 'appwrite-dev';
|
||||
const INSTALLER_DEFAULT_CONTAINER = 'appwrite-installer';
|
||||
const DEV_SERVER_START_PATTERN = '/PHP\s+\d+\.\d+\.\d+\s+Development Server .* started/';
|
||||
@@ -146,7 +149,7 @@ function startDockerInstaller(array $opts): void
|
||||
buildDockerInstallerImage($image);
|
||||
}
|
||||
ensureLocalInstallerTag($image, 'appwrite/appwrite:local');
|
||||
$port = $opts['port'] ?? INSTALLER_DEFAULT_PORT;
|
||||
$port = (string) InstallerServer::INSTALLER_WEB_PORT;
|
||||
$entrypoint = isset($opts['upgrade']) ? 'upgrade' : 'install';
|
||||
|
||||
removeDockerInstallerContainer($container);
|
||||
@@ -168,7 +171,7 @@ function startDockerInstaller(array $opts): void
|
||||
'-it',
|
||||
'--rm',
|
||||
'--name', $container,
|
||||
'-p', "$port:8080",
|
||||
'-p', "$port:" . InstallerServer::INSTALLER_WEB_PORT_INTERNAL,
|
||||
'--volume', '/var/run/docker.sock:/var/run/docker.sock',
|
||||
'--volume', "$volumePath:/usr/src/code/appwrite:rw",
|
||||
];
|
||||
@@ -226,9 +229,9 @@ function handleInstallerRequest(): bool
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$opts = getopt('', ['host::', 'port::', 'mock', 'upgrade', 'locked-database::', 'docker', 'container::', 'clean']);
|
||||
$opts = getopt('', ['host::', 'mock', 'upgrade', 'locked-database::', 'docker', 'container::', 'clean']);
|
||||
$host = $opts['host'] ?? INSTALLER_DEFAULT_HOST;
|
||||
$port = $opts['port'] ?? INSTALLER_DEFAULT_PORT;
|
||||
$port = (string) InstallerServer::INSTALLER_WEB_PORT;
|
||||
$isMock = isset($opts['mock']);
|
||||
if (isset($opts['clean'])) {
|
||||
removeDockerInstallerContainer($opts['container'] ?? INSTALLER_DEFAULT_CONTAINER);
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@
|
||||
"bench": "vendor/bin/phpbench run --report=benchmark",
|
||||
"check": "./vendor/bin/phpstan analyse -c phpstan.neon",
|
||||
"installer:clean": "php app/views/install/router.php --clean",
|
||||
"installer:dev": "composer installer:clean && php app/views/install/router.php --docker",
|
||||
"installer:dev-upgrade": "composer installer:clean && php app/views/install/router.php --docker --upgrade",
|
||||
"installer:dev": "docker compose build && composer installer:clean && php app/views/install/router.php --docker",
|
||||
"installer:dev-upgrade": "docker compose build && composer installer:clean && php app/views/install/router.php --docker --upgrade",
|
||||
"installer:mock": "composer installer:clean && php app/views/install/router.php --mock",
|
||||
"installer:mock-upgrade": "composer installer:clean && php app/views/install/router.php --mock --upgrade --locked-database=mongodb"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,767 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Installer;
|
||||
|
||||
class Server
|
||||
{
|
||||
public const int INSTALLER_WEB_PORT = 20080;
|
||||
public const int INSTALLER_WEB_PORT_INTERNAL = 20081;
|
||||
|
||||
public static function writeRouterScript(
|
||||
string $path,
|
||||
string $installPhpPath,
|
||||
string $appViewsPath,
|
||||
string $publicPath,
|
||||
string $vendorPath,
|
||||
string $appwritePath,
|
||||
string $defaultHTTPPort,
|
||||
string $defaultHTTPSPort,
|
||||
string $organization,
|
||||
string $image,
|
||||
bool $noStart,
|
||||
array $vars,
|
||||
bool $isUpgrade = false,
|
||||
?string $lockedDatabase = null
|
||||
): void {
|
||||
$script = self::renderRouterScript(
|
||||
$installPhpPath,
|
||||
$appViewsPath,
|
||||
$publicPath,
|
||||
$vendorPath,
|
||||
$appwritePath,
|
||||
$defaultHTTPPort,
|
||||
$defaultHTTPSPort,
|
||||
$organization,
|
||||
$image,
|
||||
$noStart,
|
||||
$vars,
|
||||
$isUpgrade,
|
||||
$lockedDatabase
|
||||
);
|
||||
|
||||
if (file_put_contents($path, $script) === false) {
|
||||
throw new \RuntimeException('Failed to write installer router script.');
|
||||
}
|
||||
}
|
||||
|
||||
private static function renderRouterScript(
|
||||
string $installPhpPath,
|
||||
string $appViewsPath,
|
||||
string $publicPath,
|
||||
string $vendorPath,
|
||||
string $appwritePath,
|
||||
string $defaultHTTPPort,
|
||||
string $defaultHTTPSPort,
|
||||
string $organization,
|
||||
string $image,
|
||||
bool $noStart,
|
||||
array $vars,
|
||||
bool $isUpgrade,
|
||||
?string $lockedDatabase
|
||||
): string {
|
||||
$script = <<<'PHP'
|
||||
<?php
|
||||
require_once '%s';
|
||||
require_once '%s';
|
||||
|
||||
define('INSTALL_PHP_PATH', '%s');
|
||||
define('APP_VIEWS_PATH', '%s');
|
||||
define('PUBLIC_PATH', '%s');
|
||||
define('DEFAULT_HTTP_PORT', '%s');
|
||||
define('DEFAULT_HTTPS_PORT', '%s');
|
||||
define('ORGANIZATION', '%s');
|
||||
define('IMAGE', '%s');
|
||||
define('NO_START', %s);
|
||||
define('VARS_JSON', '%s');
|
||||
define('IS_UPGRADE', %s);
|
||||
define('LOCKED_DATABASE', %s);
|
||||
define('INSTALLER_VIEW', 'installer.phtml');
|
||||
define('INSTALL_LOCK_TTL', 3600);
|
||||
|
||||
$uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
|
||||
|
||||
function sanitizeInstallId($value): string
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$clean = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
|
||||
if (!is_string($clean)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr($clean, 0, 64);
|
||||
}
|
||||
|
||||
function progressFilePath(string $installId): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/appwrite-install-' . $installId . '.json';
|
||||
}
|
||||
|
||||
function globalLockPath(): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/appwrite-install-lock.json';
|
||||
}
|
||||
|
||||
function isGlobalLockActive(?array $lock): bool
|
||||
{
|
||||
if (!$lock || !isset($lock['updatedAt'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($lock['status']) && in_array($lock['status'], ['completed', 'error'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (time() - (int) $lock['updatedAt'] > INSTALL_LOCK_TTL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function withGlobalLock(callable $callback)
|
||||
{
|
||||
$path = globalLockPath();
|
||||
$handle = fopen($path, 'c+');
|
||||
if ($handle === false) {
|
||||
return $callback(null, null);
|
||||
}
|
||||
if (!flock($handle, LOCK_EX)) {
|
||||
fclose($handle);
|
||||
return $callback(null, null);
|
||||
}
|
||||
|
||||
$contents = stream_get_contents($handle);
|
||||
$lock = null;
|
||||
if ($contents !== false && $contents !== '') {
|
||||
$decoded = json_decode($contents, true);
|
||||
if (is_array($decoded)) {
|
||||
$lock = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $callback($handle, $lock);
|
||||
|
||||
fflush($handle);
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function reserveGlobalLock(string $installId): string
|
||||
{
|
||||
return (string) withGlobalLock(function ($handle, $lock) use ($installId) {
|
||||
if (!$handle) {
|
||||
return 'unavailable';
|
||||
}
|
||||
if (isGlobalLockActive($lock) && ($lock['installId'] ?? '') !== $installId) {
|
||||
return 'locked';
|
||||
}
|
||||
$payload = [
|
||||
'installId' => $installId,
|
||||
'status' => 'in-progress',
|
||||
'updatedAt' => time(),
|
||||
];
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
fwrite($handle, json_encode($payload));
|
||||
return 'ok';
|
||||
});
|
||||
}
|
||||
|
||||
function updateGlobalLock(string $installId, string $status): void
|
||||
{
|
||||
withGlobalLock(function ($handle, $lock) use ($installId, $status) {
|
||||
if (!$handle) {
|
||||
return;
|
||||
}
|
||||
if (isGlobalLockActive($lock) && ($lock['installId'] ?? '') !== $installId) {
|
||||
return;
|
||||
}
|
||||
$payload = [
|
||||
'installId' => $installId,
|
||||
'status' => $status,
|
||||
'updatedAt' => time(),
|
||||
];
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
fwrite($handle, json_encode($payload));
|
||||
});
|
||||
}
|
||||
|
||||
function readProgressFile(string $installId): array
|
||||
{
|
||||
$path = progressFilePath($installId);
|
||||
if (!file_exists($path)) {
|
||||
return [
|
||||
'installId' => $installId,
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
if ($contents === false) {
|
||||
return [
|
||||
'installId' => $installId,
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$data = json_decode($contents, true);
|
||||
if (!is_array($data)) {
|
||||
return [
|
||||
'installId' => $installId,
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function writeProgressFile(string $installId, array $payload): void
|
||||
{
|
||||
$data = readProgressFile($installId);
|
||||
if (!isset($data['steps']) || !is_array($data['steps'])) {
|
||||
$data['steps'] = [];
|
||||
}
|
||||
|
||||
if (!empty($payload['step'])) {
|
||||
$data['steps'][$payload['step']] = [
|
||||
'status' => $payload['status'] ?? 'in-progress',
|
||||
'message' => $payload['message'] ?? '',
|
||||
'updatedAt' => $payload['updatedAt'] ?? time(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($payload['status']) && $payload['status'] === 'error') {
|
||||
$data['error'] = $payload['message'] ?? 'Installation failed';
|
||||
}
|
||||
|
||||
if (isset($payload['details']) && is_array($payload['details'])) {
|
||||
$data['details'][$payload['step']] = $payload['details'];
|
||||
}
|
||||
|
||||
if (isset($payload['payload']) && is_array($payload['payload'])) {
|
||||
$data['payload'] = $payload['payload'];
|
||||
if (!isset($data['startedAt'])) {
|
||||
$data['startedAt'] = $payload['updatedAt'] ?? time();
|
||||
}
|
||||
}
|
||||
|
||||
$data['updatedAt'] = $payload['updatedAt'] ?? time();
|
||||
|
||||
file_put_contents(progressFilePath($installId), json_encode($data));
|
||||
}
|
||||
|
||||
function sendSseEvent(string $event, array $payload): void
|
||||
{
|
||||
echo "event: {$event}\n";
|
||||
echo 'data: ' . json_encode($payload) . "\n\n";
|
||||
@ob_flush();
|
||||
@flush();
|
||||
}
|
||||
|
||||
function respondBadRequest(string $message, bool $wantsStream): void
|
||||
{
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('error', ['message' => $message, 'step' => 'config-files']);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => $message]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
function hashSensitiveValue(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed === '') {
|
||||
return '';
|
||||
}
|
||||
return hash('sha256', $trimmed);
|
||||
}
|
||||
|
||||
function isValidPort($value): bool
|
||||
{
|
||||
$string = (string) $value;
|
||||
if ($string === '' || !preg_match('/^\d+$/', $string)) {
|
||||
return false;
|
||||
}
|
||||
$port = (int) $string;
|
||||
return $port >= 1 && $port <= 65535;
|
||||
}
|
||||
|
||||
function isValidEmailAddress(string $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
function isValidAppDomain(string $value): bool
|
||||
{
|
||||
if ($value === 'localhost') {
|
||||
return true;
|
||||
}
|
||||
if (filter_var($value, FILTER_VALIDATE_IP) !== false) {
|
||||
return true;
|
||||
}
|
||||
return filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
|
||||
}
|
||||
|
||||
function isValidAppDomainInput(string $value): bool
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = $value;
|
||||
$port = null;
|
||||
|
||||
if (str_starts_with($value, '[')) {
|
||||
if (!preg_match('/^\[(.+)\](?::(\d+))?$/', $value, $matches)) {
|
||||
return false;
|
||||
}
|
||||
$host = $matches[1] ?? '';
|
||||
$port = $matches[2] ?? null;
|
||||
} else {
|
||||
$parts = explode(':', $value);
|
||||
if (count($parts) > 2) {
|
||||
return false;
|
||||
}
|
||||
if (count($parts) === 2) {
|
||||
[$host, $port] = $parts;
|
||||
}
|
||||
}
|
||||
|
||||
if ($port !== null && $port !== '' && !isValidPort($port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isValidAppDomain($host);
|
||||
}
|
||||
|
||||
function isValidDatabaseAdapter(string $value): bool
|
||||
{
|
||||
return in_array($value, ['mongodb', 'mariadb'], true);
|
||||
}
|
||||
|
||||
function sendInstallerHtmlHeaders(): void
|
||||
{
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'");
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
if ($uri !== '/' && $uri !== '') {
|
||||
$requestPath = $uri;
|
||||
$publicBase = realpath(PUBLIC_PATH);
|
||||
$viewsBase = realpath(APP_VIEWS_PATH);
|
||||
$filePath = null;
|
||||
|
||||
if ($publicBase !== false) {
|
||||
$candidate = realpath($publicBase . $requestPath);
|
||||
if ($candidate !== false && str_starts_with($candidate, $publicBase . DIRECTORY_SEPARATOR)) {
|
||||
$filePath = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (($filePath === null || !is_file($filePath)) && str_starts_with($uri, '/installer/') && $viewsBase !== false) {
|
||||
$candidate = realpath($viewsBase . $requestPath);
|
||||
if ($candidate !== false && str_starts_with($candidate, $viewsBase . DIRECTORY_SEPARATOR)) {
|
||||
$filePath = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filePath && is_file($filePath)) {
|
||||
// Determine content type
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'json' => 'application/json',
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'svg' => 'image/svg+xml',
|
||||
'ico' => 'image/x-icon',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
'ttf' => 'font/ttf',
|
||||
'eot' => 'application/vnd.ms-fontobject',
|
||||
];
|
||||
|
||||
$contentType = $mimeTypes[$extension] ?? 'application/octet-stream';
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $uri === '/install/status') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$installId = sanitizeInstallId($_GET['installId'] ?? '');
|
||||
if ($installId === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Missing installId']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = progressFilePath($installId);
|
||||
if (!file_exists($path)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Install not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = readProgressFile($installId);
|
||||
echo json_encode(['success' => true, 'progress' => $data]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle POST request (completion)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uri === '/install/complete') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$installId = sanitizeInstallId($input['installId'] ?? '');
|
||||
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'completed');
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
}
|
||||
|
||||
if (function_exists('posix_getpid')) {
|
||||
$pid = posix_getpid();
|
||||
if ($pid) {
|
||||
$delay = 5;
|
||||
$command = 'sh -c ' . escapeshellarg("sleep {$delay}; kill {$pid} >/dev/null 2>&1");
|
||||
@exec($command . ' >/dev/null 2>&1 &');
|
||||
}
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle POST request (installation)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uri === '/install') {
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
$acceptsStream = stripos($acceptHeader, 'text/event-stream') !== false;
|
||||
$wantsStream = $acceptsStream;
|
||||
|
||||
if ($wantsStream) {
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
@ini_set('output_buffering', 'off');
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
@ini_set('implicit_flush', '1');
|
||||
if (function_exists('ob_get_level')) {
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_flush();
|
||||
}
|
||||
}
|
||||
@ob_implicit_flush(true);
|
||||
} else {
|
||||
header('Content-Type: application/json');
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$appDomain = trim((string) ($input['appDomain'] ?? ''));
|
||||
if ($appDomain === '' || !isValidAppDomainInput($appDomain)) {
|
||||
respondBadRequest('Please enter a valid hostname', $wantsStream);
|
||||
}
|
||||
$input['appDomain'] = $appDomain;
|
||||
|
||||
$httpPort = $input['httpPort'] ?? '';
|
||||
if (!isValidPort($httpPort)) {
|
||||
respondBadRequest('Please enter a valid HTTP port (1-65535)', $wantsStream);
|
||||
}
|
||||
|
||||
$httpsPort = $input['httpsPort'] ?? '';
|
||||
if (!isValidPort($httpsPort)) {
|
||||
respondBadRequest('Please enter a valid HTTPS port (1-65535)', $wantsStream);
|
||||
}
|
||||
|
||||
$emailCertificates = trim((string) ($input['emailCertificates'] ?? ''));
|
||||
if ($emailCertificates === '' || !isValidEmailAddress($emailCertificates)) {
|
||||
respondBadRequest('Please enter a valid email address', $wantsStream);
|
||||
}
|
||||
$input['emailCertificates'] = $emailCertificates;
|
||||
|
||||
$opensslKey = trim((string) ($input['opensslKey'] ?? ''));
|
||||
if ($opensslKey === '' || strlen($opensslKey) > 64) {
|
||||
respondBadRequest('Secret API key must be 1-64 characters', $wantsStream);
|
||||
}
|
||||
$input['opensslKey'] = $opensslKey;
|
||||
|
||||
$assistantOpenAIKey = trim((string) ($input['assistantOpenAIKey'] ?? ''));
|
||||
$input['assistantOpenAIKey'] = $assistantOpenAIKey;
|
||||
|
||||
if (!LOCKED_DATABASE) {
|
||||
$database = strtolower(trim((string) ($input['database'] ?? '')));
|
||||
if (!isValidDatabaseAdapter($database)) {
|
||||
respondBadRequest('Please select a supported database', $wantsStream);
|
||||
}
|
||||
$input['database'] = $database;
|
||||
}
|
||||
|
||||
$installId = sanitizeInstallId($input['installId'] ?? '');
|
||||
if ($installId === '') {
|
||||
$installId = bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
$lockResult = reserveGlobalLock($installId);
|
||||
if ($lockResult !== 'ok') {
|
||||
if ($lockResult === 'locked') {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'Installation already in progress']);
|
||||
} else {
|
||||
http_response_code(503);
|
||||
echo json_encode(['success' => false, 'message' => 'Installer lock unavailable']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$retryStep = $input['retryStep'] ?? null;
|
||||
$allowedRetrySteps = ['docker-compose', 'env-vars', 'docker-containers'];
|
||||
if (!is_string($retryStep) || !in_array($retryStep, $allowedRetrySteps, true)) {
|
||||
$retryStep = null;
|
||||
}
|
||||
|
||||
$existingPath = progressFilePath($installId);
|
||||
$existing = null;
|
||||
if (file_exists($existingPath)) {
|
||||
$existing = readProgressFile($installId);
|
||||
if (!empty($existing['steps']) && $retryStep === null) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'Installation already started']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ignore_user_abort(true);
|
||||
require_once INSTALL_PHP_PATH;
|
||||
$installer = new \Appwrite\Platform\Tasks\Install();
|
||||
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('install-id', ['installId' => $installId]);
|
||||
}
|
||||
|
||||
updateGlobalLock($installId, 'in-progress');
|
||||
|
||||
// Prepare user inputs - use locked database if in upgrade mode
|
||||
$payloadInput = [
|
||||
'_APP_ENV' => 'production',
|
||||
'_APP_OPENSSL_KEY_V1' => $input['opensslKey'] ?? '',
|
||||
'_APP_DOMAIN' => $input['appDomain'] ?? 'localhost',
|
||||
'_APP_DOMAIN_TARGET' => $input['appDomain'] ?? 'localhost',
|
||||
'_APP_EMAIL_CERTIFICATES' => $input['emailCertificates'] ?? '',
|
||||
'_APP_DB_ADAPTER' => LOCKED_DATABASE ?? ($input['database'] ?? 'mongodb'),
|
||||
'_APP_ASSISTANT_OPENAI_API_KEY' => $input['assistantOpenAIKey'] ?? '',
|
||||
];
|
||||
|
||||
if (is_array($existing) && isset($existing['payload']) && is_array($existing['payload'])) {
|
||||
$stored = $existing['payload'];
|
||||
$fieldsToCompare = [
|
||||
'httpPort',
|
||||
'httpsPort',
|
||||
'database',
|
||||
'appDomain',
|
||||
'emailCertificates',
|
||||
];
|
||||
foreach ($fieldsToCompare as $field) {
|
||||
if (isset($stored[$field]) && isset($input[$field]) && (string) $stored[$field] !== (string) $input[$field]) {
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
respondBadRequest('Installation payload mismatch', $wantsStream);
|
||||
}
|
||||
}
|
||||
|
||||
$sensitiveFields = [
|
||||
'opensslKey' => 'opensslKeyHash',
|
||||
'assistantOpenAIKey' => 'assistantOpenAIKeyHash',
|
||||
];
|
||||
foreach ($sensitiveFields as $field => $hashField) {
|
||||
if (!isset($stored[$hashField]) && !isset($stored[$field])) {
|
||||
continue;
|
||||
}
|
||||
$incomingHash = hashSensitiveValue((string) ($input[$field] ?? ''));
|
||||
if (isset($stored[$hashField])) {
|
||||
if ((string) $stored[$hashField] !== $incomingHash) {
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
respondBadRequest('Installation payload mismatch', $wantsStream);
|
||||
}
|
||||
} elseif (isset($stored[$field]) && isset($input[$field]) && (string) $stored[$field] !== (string) $input[$field]) {
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
respondBadRequest('Installation payload mismatch', $wantsStream);
|
||||
}
|
||||
}
|
||||
|
||||
$payloadInput['_APP_DOMAIN'] = $stored['appDomain'] ?? $payloadInput['_APP_DOMAIN'];
|
||||
$payloadInput['_APP_DOMAIN_TARGET'] = $stored['appDomain'] ?? $payloadInput['_APP_DOMAIN_TARGET'];
|
||||
$payloadInput['_APP_EMAIL_CERTIFICATES'] = $stored['emailCertificates'] ?? $payloadInput['_APP_EMAIL_CERTIFICATES'];
|
||||
$payloadInput['_APP_DB_ADAPTER'] = LOCKED_DATABASE ?? ($stored['database'] ?? $payloadInput['_APP_DB_ADAPTER']);
|
||||
$input['httpPort'] = $stored['httpPort'] ?? $input['httpPort'] ?? DEFAULT_HTTP_PORT;
|
||||
$input['httpsPort'] = $stored['httpsPort'] ?? $input['httpsPort'] ?? DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
|
||||
// Use the prepareEnvironmentVariables method to merge with defaults
|
||||
$vars = json_decode(VARS_JSON, true);
|
||||
$envVars = $installer->prepareEnvironmentVariables($payloadInput, $vars);
|
||||
|
||||
writeProgressFile($installId, [
|
||||
'payload' => [
|
||||
'httpPort' => $input['httpPort'] ?? DEFAULT_HTTP_PORT,
|
||||
'httpsPort' => $input['httpsPort'] ?? DEFAULT_HTTPS_PORT,
|
||||
'database' => $input['database'] ?? 'mongodb',
|
||||
'appDomain' => $input['appDomain'] ?? 'localhost',
|
||||
'emailCertificates' => $input['emailCertificates'] ?? '',
|
||||
'opensslKey' => $input['opensslKey'] ?? '',
|
||||
'opensslKeyHash' => hashSensitiveValue((string) ($input['opensslKey'] ?? '')),
|
||||
'assistantOpenAIKey' => $input['assistantOpenAIKey'] ?? '',
|
||||
'assistantOpenAIKeyHash' => hashSensitiveValue((string) ($input['assistantOpenAIKey'] ?? '')),
|
||||
],
|
||||
'step' => 'start',
|
||||
'status' => 'in-progress',
|
||||
'message' => 'Installation started',
|
||||
'updatedAt' => time(),
|
||||
]);
|
||||
|
||||
$progress = function (string $step, string $status, string $message, array $details = []) use ($installId, $wantsStream) {
|
||||
$payload = [
|
||||
'installId' => $installId,
|
||||
'step' => $step,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'updatedAt' => time(),
|
||||
];
|
||||
if (!empty($details)) {
|
||||
$payload['details'] = $details;
|
||||
}
|
||||
writeProgressFile($installId, $payload);
|
||||
updateGlobalLock($installId, 'in-progress');
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('progress', $payload);
|
||||
}
|
||||
};
|
||||
|
||||
// Call performInstallation method
|
||||
$installer->performInstallation(
|
||||
$input['httpPort'] ?? DEFAULT_HTTP_PORT,
|
||||
$input['httpsPort'] ?? DEFAULT_HTTPS_PORT,
|
||||
ORGANIZATION,
|
||||
IMAGE,
|
||||
$envVars,
|
||||
NO_START,
|
||||
$progress,
|
||||
$retryStep,
|
||||
IS_UPGRADE
|
||||
);
|
||||
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('done', ['installId' => $installId, 'success' => true]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully'
|
||||
]);
|
||||
}
|
||||
updateGlobalLock($installId, 'completed');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
if ($installId !== '') {
|
||||
$details = ['trace' => $e->getTraceAsString()];
|
||||
$previous = $e->getPrevious();
|
||||
if ($previous instanceof \Throwable && $previous->getMessage() !== '') {
|
||||
$details['output'] = $previous->getMessage();
|
||||
}
|
||||
writeProgressFile($installId, [
|
||||
'step' => 'error',
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'details' => $details,
|
||||
'updatedAt' => time(),
|
||||
]);
|
||||
}
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
if ($wantsStream) {
|
||||
$details = ['trace' => $e->getTraceAsString()];
|
||||
$previous = $e->getPrevious();
|
||||
if ($previous instanceof \Throwable && $previous->getMessage() !== '') {
|
||||
$details['output'] = $previous->getMessage();
|
||||
}
|
||||
sendSseEvent('error', ['message' => $e->getMessage(), 'details' => $details]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve installer UI
|
||||
if ($uri === '/' || $uri === '') {
|
||||
sendInstallerHtmlHeaders();
|
||||
|
||||
$vars = json_decode(VARS_JSON, true);
|
||||
$defaultHttpPort = DEFAULT_HTTP_PORT;
|
||||
$defaultHttpsPort = DEFAULT_HTTPS_PORT;
|
||||
$isUpgrade = IS_UPGRADE;
|
||||
$lockedDatabase = LOCKED_DATABASE;
|
||||
|
||||
include APP_VIEWS_PATH . '/' . INSTALLER_VIEW;
|
||||
exit;
|
||||
}
|
||||
|
||||
// 404
|
||||
http_response_code(404);
|
||||
echo '404 Not Found';
|
||||
PHP;
|
||||
|
||||
$varsJson = json_encode($vars, JSON_UNESCAPED_SLASHES);
|
||||
return sprintf(
|
||||
$script,
|
||||
$vendorPath,
|
||||
$appwritePath,
|
||||
$installPhpPath,
|
||||
$appViewsPath,
|
||||
$publicPath,
|
||||
$defaultHTTPPort,
|
||||
$defaultHTTPSPort,
|
||||
$organization,
|
||||
$image,
|
||||
var_export($noStart, true),
|
||||
str_replace("'", "\\'", $varsJson),
|
||||
var_export($isUpgrade, true),
|
||||
var_export($lockedDatabase, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Docker\Compose;
|
||||
use Appwrite\Docker\Env;
|
||||
use Appwrite\Platform\Installer\Server as InstallerServer;
|
||||
use Appwrite\Utopia\View;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
@@ -15,9 +16,9 @@ use Utopia\Validator\Text;
|
||||
|
||||
class Install extends Action
|
||||
{
|
||||
private const INSTALL_STEP_DELAY_SECONDS = 2;
|
||||
private const int INSTALL_STEP_DELAY_SECONDS = 2;
|
||||
|
||||
protected string $path = '/usr/src/code/appwrite';
|
||||
protected ?string $hostPath = null;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
@@ -36,8 +37,6 @@ class Install extends Action
|
||||
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
|
||||
->callback($this->action(...));
|
||||
|
||||
$this->hostPath = $this->detectHostPath($this->path);
|
||||
$this->ensureHostPathLink();
|
||||
}
|
||||
|
||||
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart, bool $isUpgrade = false): void
|
||||
@@ -145,7 +144,7 @@ class Install extends Action
|
||||
// If interactive and web mode enabled, start web server
|
||||
if ($interactive === 'Y' && Console::isInteractive()) {
|
||||
Console::success('Starting web installer...');
|
||||
Console::info('Open your browser at: http://localhost:8080');
|
||||
Console::info('Open your browser at: http://localhost:' . InstallerServer::INSTALLER_WEB_PORT);
|
||||
Console::info('Press Ctrl+C to cancel installation');
|
||||
|
||||
$this->startWebServer($defaultHTTPPort, $defaultHTTPSPort, $organization, $image, $noStart, $vars);
|
||||
@@ -153,6 +152,30 @@ class Install extends Action
|
||||
}
|
||||
|
||||
// Fall back to CLI mode
|
||||
$enableAssistant = false;
|
||||
$assistantExistsInOldCompose = false;
|
||||
if ($existingInstallation && isset($compose)) {
|
||||
try {
|
||||
$assistantService = $compose->getService('appwrite-assistant');
|
||||
$assistantExistsInOldCompose = $assistantService !== null;
|
||||
} catch (\Throwable) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
if ($interactive === 'Y' && Console::isInteractive()) {
|
||||
$prompt = 'Add Appwrite Assistant? (Y/n)' . ($assistantExistsInOldCompose ? ' [Currently enabled]' : '');
|
||||
$answer = Console::confirm($prompt);
|
||||
|
||||
if (empty($answer)) {
|
||||
$enableAssistant = $assistantExistsInOldCompose;
|
||||
} else {
|
||||
$enableAssistant = \strtolower($answer) === 'y';
|
||||
}
|
||||
} elseif ($assistantExistsInOldCompose) {
|
||||
$enableAssistant = true;
|
||||
}
|
||||
|
||||
if (empty($httpPort)) {
|
||||
$httpPort = Console::confirm('Choose your server HTTP port: (default: ' . $defaultHTTPPort . ')');
|
||||
$httpPort = ($httpPort) ? $httpPort : $defaultHTTPPort;
|
||||
@@ -163,9 +186,33 @@ class Install extends Action
|
||||
$httpsPort = ($httpsPort) ? $httpsPort : $defaultHTTPSPort;
|
||||
}
|
||||
|
||||
$password = new Password();
|
||||
$token = new Token();
|
||||
$userInput = [];
|
||||
foreach ($vars as $var) {
|
||||
if ($var['name'] === '_APP_ASSISTANT_OPENAI_API_KEY') {
|
||||
if (!$enableAssistant) {
|
||||
$userInput[$var['name']] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($var['default'])) {
|
||||
$userInput[$var['name']] = $var['default'];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Console::isInteractive() && $interactive === 'Y') {
|
||||
$userInput[$var['name']] = Console::confirm('Enter your OpenAI API key for Appwrite Assistant:');
|
||||
if (empty($userInput[$var['name']])) {
|
||||
Console::warning('No API key provided. Assistant will be disabled.');
|
||||
$enableAssistant = false;
|
||||
$userInput[$var['name']] = '';
|
||||
}
|
||||
} else {
|
||||
$userInput[$var['name']] = '';
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$var['required'] || !Console::isInteractive() || $interactive !== 'Y') {
|
||||
continue;
|
||||
}
|
||||
@@ -189,112 +236,13 @@ class Install extends Action
|
||||
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade);
|
||||
}
|
||||
|
||||
private function detectHostPath(string $path): ?string
|
||||
{
|
||||
if (!is_file('/proc/self/mountinfo')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bestMatch = null;
|
||||
$bestLength = 0;
|
||||
|
||||
foreach (@file('/proc/self/mountinfo') as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode(' - ', $line, 2);
|
||||
if (count($parts) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$left = preg_split('/\s+/', $parts[0]);
|
||||
$right = preg_split('/\s+/', $parts[1]);
|
||||
|
||||
if (count($left) < 5 || count($right) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mountPoint = $left[4];
|
||||
$root = $left[3];
|
||||
$fsType = $right[0];
|
||||
$source = $right[1];
|
||||
|
||||
if (!str_starts_with($path, $mountPoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mountLength = strlen($mountPoint);
|
||||
if ($mountLength < $bestLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relative = substr($path, $mountLength);
|
||||
$relative = ltrim($relative, '/');
|
||||
|
||||
$hostBase = null;
|
||||
|
||||
if ($fsType === 'fakeowner' && str_starts_with($source, '/run/host_mark/')) {
|
||||
$share = basename($source);
|
||||
$hostBase = '/' . $share . rtrim($root, '/');
|
||||
} elseif ($root !== '/' && $root !== '.') {
|
||||
$hostBase = $root;
|
||||
} elseif ($source !== 'none') {
|
||||
$hostBase = $source;
|
||||
}
|
||||
|
||||
if (!$hostBase || $hostBase === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($hostBase[0] !== '/') {
|
||||
$hostBase = '/' . $hostBase;
|
||||
}
|
||||
|
||||
$candidate = rtrim($hostBase, '/');
|
||||
if ($relative !== '') {
|
||||
$candidate .= '/' . $relative;
|
||||
}
|
||||
|
||||
$candidate = preg_replace('#//+#', '/', $candidate);
|
||||
|
||||
$bestMatch = $candidate;
|
||||
$bestLength = $mountLength;
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
private function ensureHostPathLink(): void
|
||||
{
|
||||
if (empty($this->hostPath) || $this->hostPath === $this->path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_link($this->hostPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_dir($this->hostPath)) {
|
||||
@rmdir($this->hostPath);
|
||||
}
|
||||
|
||||
$parent = dirname($this->hostPath);
|
||||
if (!is_dir($parent)) {
|
||||
@mkdir($parent, 0755, true);
|
||||
}
|
||||
|
||||
if (!file_exists($this->hostPath)) {
|
||||
@symlink($this->path, $this->hostPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected function startWebServer(string $defaultHTTPPort, string $defaultHTTPSPort, string $organization, string $image, bool $noStart, array $vars, bool $isUpgrade = false, ?string $lockedDatabase = null): void
|
||||
{
|
||||
$port = 8080;
|
||||
$port = getenv('APPWRITE_INSTALLER_LOCAL')
|
||||
? InstallerServer::INSTALLER_WEB_PORT_INTERNAL
|
||||
: InstallerServer::INSTALLER_WEB_PORT;
|
||||
$host = '0.0.0.0';
|
||||
$url = "http://localhost:$port";
|
||||
|
||||
// Create a router script for handling requests
|
||||
$routerScript = \sys_get_temp_dir() . '/appwrite-installer-router.php';
|
||||
@@ -355,712 +303,22 @@ class Install extends Action
|
||||
$vendorPath = __DIR__ . '/../../../../vendor/autoload.php';
|
||||
$appwritePath = __DIR__ . '/../../../../app/init.php';
|
||||
|
||||
$script = <<<'PHP'
|
||||
<?php
|
||||
require_once '%s';
|
||||
require_once '%s';
|
||||
|
||||
define('INSTALL_PHP_PATH', '%s');
|
||||
define('APP_VIEWS_PATH', '%s');
|
||||
define('PUBLIC_PATH', '%s');
|
||||
define('DEFAULT_HTTP_PORT', '%s');
|
||||
define('DEFAULT_HTTPS_PORT', '%s');
|
||||
define('ORGANIZATION', '%s');
|
||||
define('IMAGE', '%s');
|
||||
define('NO_START', %s);
|
||||
define('VARS_JSON', '%s');
|
||||
define('IS_UPGRADE', %s);
|
||||
define('LOCKED_DATABASE', %s);
|
||||
define('INSTALLER_VIEW', 'installer.phtml');
|
||||
define('INSTALL_LOCK_TTL', 3600);
|
||||
|
||||
$uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
|
||||
|
||||
function sanitizeInstallId($value): string
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$clean = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
|
||||
if (!is_string($clean)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr($clean, 0, 64);
|
||||
}
|
||||
|
||||
function progressFilePath(string $installId): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/appwrite-install-' . $installId . '.json';
|
||||
}
|
||||
|
||||
function globalLockPath(): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/appwrite-install-lock.json';
|
||||
}
|
||||
|
||||
function isGlobalLockActive(?array $lock): bool
|
||||
{
|
||||
if (!$lock || !isset($lock['updatedAt'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($lock['status']) && in_array($lock['status'], ['completed', 'error'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (time() - (int) $lock['updatedAt'] > INSTALL_LOCK_TTL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function withGlobalLock(callable $callback)
|
||||
{
|
||||
$path = globalLockPath();
|
||||
$handle = fopen($path, 'c+');
|
||||
if ($handle === false) {
|
||||
return $callback(null, null);
|
||||
}
|
||||
if (!flock($handle, LOCK_EX)) {
|
||||
fclose($handle);
|
||||
return $callback(null, null);
|
||||
}
|
||||
|
||||
$contents = stream_get_contents($handle);
|
||||
$lock = null;
|
||||
if ($contents !== false && $contents !== '') {
|
||||
$decoded = json_decode($contents, true);
|
||||
if (is_array($decoded)) {
|
||||
$lock = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $callback($handle, $lock);
|
||||
|
||||
fflush($handle);
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function reserveGlobalLock(string $installId): string
|
||||
{
|
||||
return (string) withGlobalLock(function ($handle, $lock) use ($installId) {
|
||||
if (!$handle) {
|
||||
return 'unavailable';
|
||||
}
|
||||
if (isGlobalLockActive($lock) && ($lock['installId'] ?? '') !== $installId) {
|
||||
return 'locked';
|
||||
}
|
||||
$payload = [
|
||||
'installId' => $installId,
|
||||
'status' => 'in-progress',
|
||||
'updatedAt' => time(),
|
||||
];
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
fwrite($handle, json_encode($payload));
|
||||
return 'ok';
|
||||
});
|
||||
}
|
||||
|
||||
function updateGlobalLock(string $installId, string $status): void
|
||||
{
|
||||
withGlobalLock(function ($handle, $lock) use ($installId, $status) {
|
||||
if (!$handle) {
|
||||
return;
|
||||
}
|
||||
if (isGlobalLockActive($lock) && ($lock['installId'] ?? '') !== $installId) {
|
||||
return;
|
||||
}
|
||||
$payload = [
|
||||
'installId' => $installId,
|
||||
'status' => $status,
|
||||
'updatedAt' => time(),
|
||||
];
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
fwrite($handle, json_encode($payload));
|
||||
});
|
||||
}
|
||||
|
||||
function readProgressFile(string $installId): array
|
||||
{
|
||||
$path = progressFilePath($installId);
|
||||
if (!file_exists($path)) {
|
||||
return [
|
||||
'installId' => $installId,
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
if ($contents === false) {
|
||||
return [
|
||||
'installId' => $installId,
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$data = json_decode($contents, true);
|
||||
if (!is_array($data)) {
|
||||
return [
|
||||
'installId' => $installId,
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function writeProgressFile(string $installId, array $payload): void
|
||||
{
|
||||
$data = readProgressFile($installId);
|
||||
if (!isset($data['steps']) || !is_array($data['steps'])) {
|
||||
$data['steps'] = [];
|
||||
}
|
||||
|
||||
if (!empty($payload['step'])) {
|
||||
$data['steps'][$payload['step']] = [
|
||||
'status' => $payload['status'] ?? 'in-progress',
|
||||
'message' => $payload['message'] ?? '',
|
||||
'updatedAt' => $payload['updatedAt'] ?? time(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($payload['status']) && $payload['status'] === 'error') {
|
||||
$data['error'] = $payload['message'] ?? 'Installation failed';
|
||||
}
|
||||
|
||||
if (isset($payload['details']) && is_array($payload['details'])) {
|
||||
$data['details'][$payload['step']] = $payload['details'];
|
||||
}
|
||||
|
||||
if (isset($payload['payload']) && is_array($payload['payload'])) {
|
||||
$data['payload'] = $payload['payload'];
|
||||
if (!isset($data['startedAt'])) {
|
||||
$data['startedAt'] = $payload['updatedAt'] ?? time();
|
||||
}
|
||||
}
|
||||
|
||||
$data['updatedAt'] = $payload['updatedAt'] ?? time();
|
||||
|
||||
file_put_contents(progressFilePath($installId), json_encode($data));
|
||||
}
|
||||
|
||||
function sendSseEvent(string $event, array $payload): void
|
||||
{
|
||||
echo "event: {$event}\n";
|
||||
echo 'data: ' . json_encode($payload) . "\n\n";
|
||||
@ob_flush();
|
||||
@flush();
|
||||
}
|
||||
|
||||
function respondBadRequest(string $message, bool $wantsStream): void
|
||||
{
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('error', ['message' => $message, 'step' => 'config-files']);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => $message]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
function hashSensitiveValue(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed === '') {
|
||||
return '';
|
||||
}
|
||||
return hash('sha256', $trimmed);
|
||||
}
|
||||
|
||||
function isValidPort($value): bool
|
||||
{
|
||||
$string = (string) $value;
|
||||
if ($string === '' || !preg_match('/^\d+$/', $string)) {
|
||||
return false;
|
||||
}
|
||||
$port = (int) $string;
|
||||
return $port >= 1 && $port <= 65535;
|
||||
}
|
||||
|
||||
function isValidEmailAddress(string $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
function isValidAppDomain(string $value): bool
|
||||
{
|
||||
if ($value === 'localhost') {
|
||||
return true;
|
||||
}
|
||||
if (filter_var($value, FILTER_VALIDATE_IP) !== false) {
|
||||
return true;
|
||||
}
|
||||
return filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
|
||||
}
|
||||
|
||||
function isValidAppDomainInput(string $value): bool
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = $value;
|
||||
$port = null;
|
||||
|
||||
if (str_starts_with($value, '[')) {
|
||||
if (!preg_match('/^\[(.+)\](?::(\d+))?$/', $value, $matches)) {
|
||||
return false;
|
||||
}
|
||||
$host = $matches[1] ?? '';
|
||||
$port = $matches[2] ?? null;
|
||||
} else {
|
||||
$parts = explode(':', $value);
|
||||
if (count($parts) > 2) {
|
||||
return false;
|
||||
}
|
||||
if (count($parts) === 2) {
|
||||
[$host, $port] = $parts;
|
||||
}
|
||||
}
|
||||
|
||||
if ($port !== null && $port !== '' && !isValidPort($port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isValidAppDomain($host);
|
||||
}
|
||||
|
||||
function isValidDatabaseAdapter(string $value): bool
|
||||
{
|
||||
return in_array($value, ['mongodb', 'mariadb'], true);
|
||||
}
|
||||
|
||||
function sendInstallerHtmlHeaders(): void
|
||||
{
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'");
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
if ($uri !== '/' && $uri !== '') {
|
||||
$requestPath = $uri;
|
||||
$publicBase = realpath(PUBLIC_PATH);
|
||||
$viewsBase = realpath(APP_VIEWS_PATH);
|
||||
$filePath = null;
|
||||
|
||||
if ($publicBase !== false) {
|
||||
$candidate = realpath($publicBase . $requestPath);
|
||||
if ($candidate !== false && str_starts_with($candidate, $publicBase . DIRECTORY_SEPARATOR)) {
|
||||
$filePath = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (($filePath === null || !is_file($filePath)) && str_starts_with($uri, '/installer/') && $viewsBase !== false) {
|
||||
$candidate = realpath($viewsBase . $requestPath);
|
||||
if ($candidate !== false && str_starts_with($candidate, $viewsBase . DIRECTORY_SEPARATOR)) {
|
||||
$filePath = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filePath && is_file($filePath)) {
|
||||
// Determine content type
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'json' => 'application/json',
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'svg' => 'image/svg+xml',
|
||||
'ico' => 'image/x-icon',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
'ttf' => 'font/ttf',
|
||||
'eot' => 'application/vnd.ms-fontobject',
|
||||
];
|
||||
|
||||
$contentType = $mimeTypes[$extension] ?? 'application/octet-stream';
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $uri === '/install/status') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$installId = sanitizeInstallId($_GET['installId'] ?? '');
|
||||
if ($installId === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Missing installId']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = progressFilePath($installId);
|
||||
if (!file_exists($path)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Install not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = readProgressFile($installId);
|
||||
echo json_encode(['success' => true, 'progress' => $data]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle POST request (completion)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uri === '/install/complete') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$installId = sanitizeInstallId($input['installId'] ?? '');
|
||||
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'completed');
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
}
|
||||
|
||||
if (function_exists('posix_getpid') && PHP_OS_FAMILY !== 'Windows') {
|
||||
$pid = posix_getpid();
|
||||
if ($pid) {
|
||||
$delay = 3;
|
||||
$command = 'sh -c ' . escapeshellarg("sleep {$delay}; kill {$pid} >/dev/null 2>&1");
|
||||
@exec($command . ' >/dev/null 2>&1 &');
|
||||
}
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle POST request (installation)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uri === '/install') {
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
$acceptsStream = stripos($acceptHeader, 'text/event-stream') !== false;
|
||||
$wantsStream = $acceptsStream;
|
||||
|
||||
if ($wantsStream) {
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
@ini_set('output_buffering', 'off');
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
@ini_set('implicit_flush', '1');
|
||||
if (function_exists('ob_get_level')) {
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_flush();
|
||||
}
|
||||
}
|
||||
@ob_implicit_flush(true);
|
||||
} else {
|
||||
header('Content-Type: application/json');
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$appDomain = trim((string) ($input['appDomain'] ?? ''));
|
||||
if ($appDomain === '' || !isValidAppDomainInput($appDomain)) {
|
||||
respondBadRequest('Please enter a valid hostname', $wantsStream);
|
||||
}
|
||||
$input['appDomain'] = $appDomain;
|
||||
|
||||
$httpPort = $input['httpPort'] ?? '';
|
||||
if (!isValidPort($httpPort)) {
|
||||
respondBadRequest('Please enter a valid HTTP port (1-65535)', $wantsStream);
|
||||
}
|
||||
|
||||
$httpsPort = $input['httpsPort'] ?? '';
|
||||
if (!isValidPort($httpsPort)) {
|
||||
respondBadRequest('Please enter a valid HTTPS port (1-65535)', $wantsStream);
|
||||
}
|
||||
|
||||
$emailCertificates = trim((string) ($input['emailCertificates'] ?? ''));
|
||||
if ($emailCertificates === '' || !isValidEmailAddress($emailCertificates)) {
|
||||
respondBadRequest('Please enter a valid email address', $wantsStream);
|
||||
}
|
||||
$input['emailCertificates'] = $emailCertificates;
|
||||
|
||||
$opensslKey = trim((string) ($input['opensslKey'] ?? ''));
|
||||
if ($opensslKey === '' || strlen($opensslKey) > 64) {
|
||||
respondBadRequest('Secret API key must be 1-64 characters', $wantsStream);
|
||||
}
|
||||
$input['opensslKey'] = $opensslKey;
|
||||
|
||||
$assistantOpenAIKey = trim((string) ($input['assistantOpenAIKey'] ?? ''));
|
||||
$input['assistantOpenAIKey'] = $assistantOpenAIKey;
|
||||
|
||||
if (!LOCKED_DATABASE) {
|
||||
$database = strtolower(trim((string) ($input['database'] ?? '')));
|
||||
if (!isValidDatabaseAdapter($database)) {
|
||||
respondBadRequest('Please select a supported database', $wantsStream);
|
||||
}
|
||||
$input['database'] = $database;
|
||||
}
|
||||
|
||||
$installId = sanitizeInstallId($input['installId'] ?? '');
|
||||
if ($installId === '') {
|
||||
$installId = bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
$lockResult = reserveGlobalLock($installId);
|
||||
if ($lockResult !== 'ok') {
|
||||
if ($lockResult === 'locked') {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'Installation already in progress']);
|
||||
} else {
|
||||
http_response_code(503);
|
||||
echo json_encode(['success' => false, 'message' => 'Installer lock unavailable']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$retryStep = $input['retryStep'] ?? null;
|
||||
$allowedRetrySteps = ['docker-compose', 'env-vars', 'docker-containers'];
|
||||
if (!is_string($retryStep) || !in_array($retryStep, $allowedRetrySteps, true)) {
|
||||
$retryStep = null;
|
||||
}
|
||||
|
||||
$existingPath = progressFilePath($installId);
|
||||
$existing = null;
|
||||
if (file_exists($existingPath)) {
|
||||
$existing = readProgressFile($installId);
|
||||
if (!empty($existing['steps']) && $retryStep === null) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'Installation already started']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ignore_user_abort(true);
|
||||
require_once INSTALL_PHP_PATH;
|
||||
$installer = new \Appwrite\Platform\Tasks\Install();
|
||||
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('install-id', ['installId' => $installId]);
|
||||
}
|
||||
|
||||
updateGlobalLock($installId, 'in-progress');
|
||||
|
||||
// Prepare user inputs - use locked database if in upgrade mode
|
||||
$payloadInput = [
|
||||
'_APP_ENV' => 'production',
|
||||
'_APP_OPENSSL_KEY_V1' => $input['opensslKey'] ?? '',
|
||||
'_APP_DOMAIN' => $input['appDomain'] ?? 'localhost',
|
||||
'_APP_DOMAIN_TARGET' => $input['appDomain'] ?? 'localhost',
|
||||
'_APP_EMAIL_CERTIFICATES' => $input['emailCertificates'] ?? '',
|
||||
'_APP_DB_ADAPTER' => LOCKED_DATABASE ?? ($input['database'] ?? 'mongodb'),
|
||||
'_APP_ASSISTANT_OPENAI_API_KEY' => $input['assistantOpenAIKey'] ?? '',
|
||||
];
|
||||
|
||||
if (is_array($existing) && isset($existing['payload']) && is_array($existing['payload'])) {
|
||||
$stored = $existing['payload'];
|
||||
$fieldsToCompare = [
|
||||
'httpPort',
|
||||
'httpsPort',
|
||||
'database',
|
||||
'appDomain',
|
||||
'emailCertificates',
|
||||
];
|
||||
foreach ($fieldsToCompare as $field) {
|
||||
if (isset($stored[$field]) && isset($input[$field]) && (string) $stored[$field] !== (string) $input[$field]) {
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
respondBadRequest('Installation payload mismatch', $wantsStream);
|
||||
}
|
||||
}
|
||||
|
||||
$sensitiveFields = [
|
||||
'opensslKey' => 'opensslKeyHash',
|
||||
'assistantOpenAIKey' => 'assistantOpenAIKeyHash',
|
||||
];
|
||||
foreach ($sensitiveFields as $field => $hashField) {
|
||||
if (!isset($stored[$hashField]) && !isset($stored[$field])) {
|
||||
continue;
|
||||
}
|
||||
$incomingHash = hashSensitiveValue((string) ($input[$field] ?? ''));
|
||||
if (isset($stored[$hashField])) {
|
||||
if ((string) $stored[$hashField] !== $incomingHash) {
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
respondBadRequest('Installation payload mismatch', $wantsStream);
|
||||
}
|
||||
} elseif (isset($stored[$field]) && isset($input[$field]) && (string) $stored[$field] !== (string) $input[$field]) {
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
respondBadRequest('Installation payload mismatch', $wantsStream);
|
||||
}
|
||||
}
|
||||
|
||||
$payloadInput['_APP_DOMAIN'] = $stored['appDomain'] ?? $payloadInput['_APP_DOMAIN'];
|
||||
$payloadInput['_APP_DOMAIN_TARGET'] = $stored['appDomain'] ?? $payloadInput['_APP_DOMAIN_TARGET'];
|
||||
$payloadInput['_APP_EMAIL_CERTIFICATES'] = $stored['emailCertificates'] ?? $payloadInput['_APP_EMAIL_CERTIFICATES'];
|
||||
$payloadInput['_APP_DB_ADAPTER'] = LOCKED_DATABASE ?? ($stored['database'] ?? $payloadInput['_APP_DB_ADAPTER']);
|
||||
$input['httpPort'] = $stored['httpPort'] ?? $input['httpPort'] ?? DEFAULT_HTTP_PORT;
|
||||
$input['httpsPort'] = $stored['httpsPort'] ?? $input['httpsPort'] ?? DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
|
||||
// Use the prepareEnvironmentVariables method to merge with defaults
|
||||
$vars = json_decode(VARS_JSON, true);
|
||||
$envVars = $installer->prepareEnvironmentVariables($payloadInput, $vars);
|
||||
|
||||
writeProgressFile($installId, [
|
||||
'payload' => [
|
||||
'httpPort' => $input['httpPort'] ?? DEFAULT_HTTP_PORT,
|
||||
'httpsPort' => $input['httpsPort'] ?? DEFAULT_HTTPS_PORT,
|
||||
'database' => $input['database'] ?? 'mongodb',
|
||||
'appDomain' => $input['appDomain'] ?? 'localhost',
|
||||
'emailCertificates' => $input['emailCertificates'] ?? '',
|
||||
'opensslKey' => $input['opensslKey'] ?? '',
|
||||
'opensslKeyHash' => hashSensitiveValue((string) ($input['opensslKey'] ?? '')),
|
||||
'assistantOpenAIKey' => $input['assistantOpenAIKey'] ?? '',
|
||||
'assistantOpenAIKeyHash' => hashSensitiveValue((string) ($input['assistantOpenAIKey'] ?? '')),
|
||||
],
|
||||
'step' => 'start',
|
||||
'status' => 'in-progress',
|
||||
'message' => 'Installation started',
|
||||
'updatedAt' => time(),
|
||||
]);
|
||||
|
||||
$progress = function (string $step, string $status, string $message, array $details = []) use ($installId, $wantsStream) {
|
||||
$payload = [
|
||||
'installId' => $installId,
|
||||
'step' => $step,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'updatedAt' => time(),
|
||||
];
|
||||
if (!empty($details)) {
|
||||
$payload['details'] = $details;
|
||||
}
|
||||
writeProgressFile($installId, $payload);
|
||||
updateGlobalLock($installId, 'in-progress');
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('progress', $payload);
|
||||
}
|
||||
};
|
||||
|
||||
// Call performInstallation method
|
||||
$installer->performInstallation(
|
||||
$input['httpPort'] ?? DEFAULT_HTTP_PORT,
|
||||
$input['httpsPort'] ?? DEFAULT_HTTPS_PORT,
|
||||
ORGANIZATION,
|
||||
IMAGE,
|
||||
$envVars,
|
||||
NO_START,
|
||||
$progress,
|
||||
$retryStep,
|
||||
IS_UPGRADE
|
||||
);
|
||||
|
||||
if ($wantsStream) {
|
||||
sendSseEvent('done', ['installId' => $installId, 'success' => true]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully'
|
||||
]);
|
||||
}
|
||||
updateGlobalLock($installId, 'completed');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
if ($installId !== '') {
|
||||
$details = ['trace' => $e->getTraceAsString()];
|
||||
$previous = $e->getPrevious();
|
||||
if ($previous instanceof \Throwable && $previous->getMessage() !== '') {
|
||||
$details['output'] = $previous->getMessage();
|
||||
}
|
||||
writeProgressFile($installId, [
|
||||
'step' => 'error',
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'details' => $details,
|
||||
'updatedAt' => time(),
|
||||
]);
|
||||
}
|
||||
if ($installId !== '') {
|
||||
updateGlobalLock($installId, 'error');
|
||||
}
|
||||
if ($wantsStream) {
|
||||
$details = ['trace' => $e->getTraceAsString()];
|
||||
$previous = $e->getPrevious();
|
||||
if ($previous instanceof \Throwable && $previous->getMessage() !== '') {
|
||||
$details['output'] = $previous->getMessage();
|
||||
}
|
||||
sendSseEvent('error', ['message' => $e->getMessage(), 'details' => $details]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve installer UI
|
||||
if ($uri === '/' || $uri === '') {
|
||||
sendInstallerHtmlHeaders();
|
||||
|
||||
$vars = json_decode(VARS_JSON, true);
|
||||
$defaultHttpPort = DEFAULT_HTTP_PORT;
|
||||
$defaultHttpsPort = DEFAULT_HTTPS_PORT;
|
||||
$isUpgrade = IS_UPGRADE;
|
||||
$lockedDatabase = LOCKED_DATABASE;
|
||||
|
||||
include APP_VIEWS_PATH . '/' . INSTALLER_VIEW;
|
||||
exit;
|
||||
}
|
||||
|
||||
// 404
|
||||
http_response_code(404);
|
||||
echo '404 Not Found';
|
||||
PHP;
|
||||
|
||||
$varsJson = json_encode($vars, JSON_UNESCAPED_SLASHES);
|
||||
$script = sprintf(
|
||||
$script,
|
||||
$vendorPath,
|
||||
$appwritePath,
|
||||
InstallerServer::writeRouterScript(
|
||||
$path,
|
||||
$installPhpPath,
|
||||
$appViewsPath,
|
||||
$publicPath,
|
||||
$vendorPath,
|
||||
$appwritePath,
|
||||
$defaultHTTPPort,
|
||||
$defaultHTTPSPort,
|
||||
$organization,
|
||||
$image,
|
||||
var_export($noStart, true),
|
||||
str_replace("'", "\\'", $varsJson),
|
||||
var_export($isUpgrade, true),
|
||||
var_export($lockedDatabase, true)
|
||||
$noStart,
|
||||
$vars,
|
||||
$isUpgrade,
|
||||
$lockedDatabase
|
||||
);
|
||||
|
||||
file_put_contents($path, $script);
|
||||
}
|
||||
|
||||
public function prepareEnvironmentVariables(array $userInput, array $vars): array
|
||||
@@ -1082,7 +340,7 @@ PHP;
|
||||
|
||||
// Override with user inputs
|
||||
foreach ($userInput as $key => $value) {
|
||||
if ($value !== null && $value !== '') {
|
||||
if ($value !== null && ($value !== '' || $key === '_APP_ASSISTANT_OPENAI_API_KEY')) {
|
||||
$input[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -1156,8 +414,7 @@ PHP;
|
||||
->setParam('version', $version)
|
||||
->setParam('organization', $organization)
|
||||
->setParam('image', $image)
|
||||
->setParam('database', $database)
|
||||
->setParam('hostPath', $this->hostPath);
|
||||
->setParam('database', $database);
|
||||
|
||||
$templateForEnv->setParam('vars', $input);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user