#!/bin/bash # ========================================================== # QuickSite — Post-Clone Setup # ========================================================== # # Run this after cloning, before your first page load. # # Usage: # chmod +x setup.sh # ./setup.sh # menu # ./setup.sh # pre-answer the public-folder rename, # # then show the menu # PHP=/usr/bin/php8.3 ./setup.sh # check that PHP's version rather than # # the php on the PATH # # A MENU, NOT A WALK. Every item is independent and re-runnable: come back # later to change one thing without answering the other five again. The # settings you choose are remembered in .quicksite.conf and read back the next # time you run this. # # 1. Public folder name — match your vhost DocumentRoot (www, public_html…) # 2. Secure folder name — rename/nest the engine folder for obscurity # 3. URL space — serve from a subdirectory (http://domain/web/) # 4. Environment — production (default) or development # 5. Self-registration — may visitors create their own accounts? # 6. Setup token — read the first-run credential off disk # 7. Storage quotas — per-account size and upload-rate ceilings # 8. Command console — offer the raw command runner at /admin/command? # 9. Default language — the language a new project starts in # 10. Idle sign-out — how long the panel may go unused before it signs # you out # 11. Embed security page — show the embed policy's page in the panel? # # Items 1-3 rewrite init.php constants and the .htaccess routing. Items 4-5 and # 7-11 write config files under the secure folder. Item 6 only reads. # # Before anything else, it checks the PHP version: older than 8.3, it stops. # # WHAT THIS SCRIPT CANNOT DO: create your account. No account exists yet, so # there is nobody to be one. That happens in the browser at /admin/, and the # setup token (item 6) is what authorises it — see the notes there. # ========================================================== set -euo pipefail # Colors GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' DIM='\033[2m' BOLD='\033[1m' NC='\033[0m' # ========================================================== # The PHP floor — checked before anything is written # ========================================================== # QuickSite needs PHP 8.3 or later. This script runs no PHP of its own, so it asks # the php on the PATH — or the one the PHP variable names — for its version, and # stops at once when it is older, before the ownership fix or any file below. # That is the command-line PHP: on some servers the web server's own (mod_php, # php-fpm) is a separate install, which is why the README asks for a patched # release of the branch either way. Only the floor is checked here: patch numbers # change with every advisory, so they live in the README. check_php_floor() { local php="${PHP:-}" out="" [ -n "$php" ] || php="$(command -v php 2>/dev/null || true)" if [ -n "$php" ]; then # -n: no php.ini, so an extension that fails to load prints nothing here out="$("$php" -n -r 'echo PHP_VERSION_ID, " ", PHP_VERSION;' 2>/dev/null || true)" fi if [[ ! "$out" =~ ^([0-9]+)\ ([0-9][^[:space:]]*)$ ]]; then echo "" if [ -z "$php" ]; then echo -e " ${YELLOW}⚠ The PHP version could not be checked: no php command was found${NC}" echo -e " ${YELLOW} (name one with PHP=/path/to/php). QuickSite needs PHP 8.3 or later.${NC}" else echo -e " ${YELLOW}⚠ The PHP version could not be checked: $php answered no version.${NC}" echo -e " ${YELLOW} QuickSite needs PHP 8.3 or later.${NC}" fi return 0 fi if [ "${BASH_REMATCH[1]}" -lt 80300 ]; then echo "" echo -e " ${RED}✗ QuickSite needs PHP 8.3 or later; the PHP found here ($php, ${BASH_REMATCH[2]}) is older.${NC}" echo " Install a supported PHP, a patched release of its branch (see the README), and run setup again." exit 1 fi } check_php_floor SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PUBLIC_DIR="$SCRIPT_DIR/public" SECURE_DIR="$SCRIPT_DIR/secure" PUBLIC_FOLDER_NAME="public" SECURE_FOLDER_NAME="secure" PUBLIC_SPACE="" # Read existing config if available (re-run detection) CONF_FILE="$SCRIPT_DIR/.quicksite.conf" # Has setup ever run here? Captured BEFORE save_conf writes the file, because # the landing-page drop below is a first-launch-only action. FIRST_LAUNCH=yes [ -f "$CONF_FILE" ] && FIRST_LAUNCH=no if [ -f "$CONF_FILE" ]; then source "$CONF_FILE" # Backward compat: old conf format used PUBLIC_FOLDER_SPACE PUBLIC_SPACE="${PUBLIC_SPACE:-${PUBLIC_FOLDER_SPACE:-}}" PUBLIC_DIR="$SCRIPT_DIR/$PUBLIC_FOLDER_NAME" SECURE_DIR="$SCRIPT_DIR/$SECURE_FOLDER_NAME" fi # Helper: save current state to conf (crash recovery + re-run detection) save_conf() { cat > "$CONF_FILE" << CONFEOF PUBLIC_FOLDER_NAME=$PUBLIC_FOLDER_NAME SECURE_FOLDER_NAME=$SECURE_FOLDER_NAME PUBLIC_SPACE=$PUBLIC_SPACE CONFEOF } # Where init.php lives right now (it moves with the URL space). init_file() { if [ -n "$PUBLIC_SPACE" ]; then echo "$PUBLIC_DIR/$PUBLIC_SPACE/init.php" else echo "$PUBLIC_DIR/init.php" fi } # The directory holding the served .htaccess files (also moves with the space). served_dir() { if [ -n "$PUBLIC_SPACE" ]; then echo "$PUBLIC_DIR/$PUBLIC_SPACE" else echo "$PUBLIC_DIR" fi } CONFIG_DIR() { echo "$SECURE_DIR/management/config"; } # First capture group of a BRE, or '' when the file or the pattern is absent. # # POSIX sed, deliberately: `grep -P` is a GNU extension that ALSO refuses to run # under a non-UTF-8 multibyte locale ("-P supports only unibyte and UTF-8 # locales"), which is a property of the server this lands on, not of the # pattern. A setup script that reads its own config back wrongly on somebody # else's box is worse than one that never reads it at all — it reports settings # that are not in effect. first_capture() { local file="$1" pattern="$2" out [ -f "$file" ] || { echo ""; return 0; } out=$(sed -n "s/.*${pattern}.*/\\1/p" "$file" 2>/dev/null) || out="" echo "${out%%$'\n'*}" } pause_for_menu() { echo "" read -r -p " Press Enter to return to the menu… " _ || true } # ========================================================== # Crash recovery — locate folders that were renamed without us # ========================================================== if [ ! -d "$PUBLIC_DIR" ]; then for d in "$SCRIPT_DIR"/*/; do if [ -f "$d/init.php" ] || ls "$d"/*/init.php >/dev/null 2>&1; then PUBLIC_FOLDER_NAME="$(basename "$d")" PUBLIC_DIR="$SCRIPT_DIR/$PUBLIC_FOLDER_NAME" break fi done fi if [ ! -d "$SECURE_DIR" ]; then INIT_PROBE="$(init_file)" if [ -f "$INIT_PROBE" ]; then DETECTED=$(first_capture "$INIT_PROBE" "define('SECURE_FOLDER_NAME',[[:space:]]*'\\([^']*\\)')") if [ -n "$DETECTED" ] && [ -d "$SCRIPT_DIR/$DETECTED" ]; then SECURE_DIR="$SCRIPT_DIR/$DETECTED" SECURE_FOLDER_NAME="$DETECTED" fi fi fi # ========================================================== # File ownership — PHP needs write access to the secure folder # ========================================================== # Runs once, before the menu: a clone made as root leaves every later item # writing files the web server cannot then read back. OWNERSHIP_WARNING="" if [ "$(whoami)" = "root" ]; then OWNERSHIP_WARNING="yes" WEB_USER="" # CloudPanel: the site user matches the htdocs parent folder name HTDOCS_PARENT="$(basename "$(dirname "$SCRIPT_DIR")" 2>/dev/null || true)" if id "$HTDOCS_PARENT" &>/dev/null 2>&1; then WEB_USER="$HTDOCS_PARENT" elif id "www-data" &>/dev/null 2>&1; then WEB_USER="www-data" elif id "nginx" &>/dev/null 2>&1; then WEB_USER="nginx" elif id "apache" &>/dev/null 2>&1; then WEB_USER="apache" fi if [ -n "$WEB_USER" ]; then echo "" echo -e " ${BOLD}Fixing file ownership…${NC}" chown -R "$WEB_USER:$WEB_USER" "$SCRIPT_DIR" echo -e " ${GREEN}✓${NC} Ownership set to ${BOLD}$WEB_USER${NC}" OWNERSHIP_WARNING="" fi fi # ========================================================== # Landing page — first launch only, and only into an empty root # ========================================================== # QuickSite serves nothing at the web root by design, so a brand-new install has # nothing there and answers 403. That is correct and it looks broken, which is # the worst combination — hence a placeholder that says where the panel is. # # TWO CONDITIONS, both required. It runs only on the FIRST setup launch, so # deleting the page is permanent rather than something the next run undoes; and # only when the root holds no index file of its own, so cloning QuickSite onto a # server that already has a site never overwrites that site's front page. maybe_place_landing_page() { [ "$FIRST_LAUNCH" = yes ] || return 0 local dir; dir="$(served_dir)" local template="$SECURE_DIR/deploy/index.html.example" [ -d "$dir" ] || return 0 [ -f "$template" ] || return 0 # Any index.* at all — .php, .html, .htm, anything the server might pick as # a DirectoryIndex. If something is already there, it is not ours to touch. if compgen -G "$dir/index.*" > /dev/null 2>&1; then return 0 fi cp "$template" "$dir/index.html" || return 0 echo "" echo -e " ${GREEN}✓${NC} Added a placeholder page at the web root" echo -e " ${DIM}The root serves nothing by default, so it would answer 403.${NC}" echo -e " ${DIM}Delete or replace $PUBLIC_FOLDER_NAME/index.html when you put${NC}" echo -e " ${DIM}your own site there — nothing depends on it.${NC}" } # ========================================================== # Embed policy — the install-wide iframe sandbox rules # ========================================================== # One file for the whole installation (embed-policy.json) decides which hosts an #