commit 004a18f09c003277a4a458f2f9e6d2572c7f535c Author: Guillermo Pages Date: Wed Dec 10 16:27:37 2025 +0100 Initial commit from template diff --git a/.deploy.yml b/.deploy.yml new file mode 100644 index 0000000..3a94331 --- /dev/null +++ b/.deploy.yml @@ -0,0 +1,12 @@ +sn48: + vault: + hydrate: + branches: + - master + + copy_files: + - docker/wordpress/php.ini + - docker/wordpress/public_dir/to-bool.php + - docker/filebrowser/settings.json + + deploy: 4 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..1c3bb3d --- /dev/null +++ b/.drone.yml @@ -0,0 +1,147 @@ +kind: pipeline +type: docker +name: default +trigger: + branch: + - master + +steps: +- name: debug-secrets + image: alpine + environment: + VAULT_API_URL: + from_secret: VAULT_API_URL + commands: + - 'echo "Docker Registry URL: $${VAULT_API_URL}"' + when: + event: + - push + - tag + +# Make the image available for next step +- name: cron + image: plugins/docker + settings: + dockerfile: docker/wp-cron/Dockerfile + context: . + registry: registry.sn48.zivili.ch + repo: registry.sn48.zivili.ch/meow/wp-cron + tags: + - "amd64-1.0.0" + - "latest" + username: + from_secret: PORTUS_USER + password: + from_secret: PORTUS_PASSWORD + debug: true + launch_debug: true + # make sure to replace image with same tag + force_tag: true + when: + event: + - push + - tag + +- name: deploy + image: registry.sn48.zivili.ch/meow/drone-deploy:amd64-1.0.0 + pull: never + settings: + ssh_port: + from_secret: SSH_PORT + # this is required for the moment to generate the .docker/config.json + # drone is failing to do it on its own at the moment + dockerconfigjson: + from_secret: dockerconfigjson + # use portus or directly docker logins + portus_user: + from_secret: PORTUS_USER + portus_password: + from_secret: PORTUS_PASSWORD + # used by deploy to login to deploy server + ssh_host: + from_secret: SSH_HOST + ssh_user: + from_secret: SSH_USER + ssh_key: + from_secret: SSH_KEY + ssh_fingerprint: + from_secret: SSH_FINGERPRINT + # used by the deploy script to gather all project's .env values from vault + drone_agent1_token: + from_secret: DRONE_AGENT1_TOKEN + # used by deploy script to know where to gather secrets from + vault_api_url: + from_secret: VAULT_API_URL + +--- +kind: secret +name: SSH_HOST +get: + path: kv/data/__drone-admin-secrets + name: SSH_HOST + +--- +kind: secret +name: SSH_USER +get: + path: kv/data/__drone-admin-secrets + name: SSH_USER + +--- +kind: secret +name: SSH_KEY +get: + path: kv/data/__drone-admin-secrets + name: SSH_KEY + +--- +kind: secret +name: DRONE_AGENT1_TOKEN +get: + path: kv/data/__drone-admin-secrets + name: DRONE_AGENT1_TOKEN + +--- +kind: secret +name: VAULT_API_URL +get: + path: kv/data/__drone-admin-secrets + name: VAULT_API_URL + +--- +kind: secret +name: PORTUS_USER +get: + path: kv/data/__drone-admin-secrets + name: PORTUS_USER + +--- +kind: secret +name: PORTUS_PASSWORD +get: + path: kv/data/__drone-admin-secrets + name: PORTUS_PASSWORD + +--- +kind: secret +name: dockerconfigjson +get: + path: kv/data/__drone-admin-secrets + name: dockerconfigjson + +image_pull_secrets: + from_secret: dockerconfigjson + +--- +kind: secret +name: SSH_PORT +get: + path: kv/data/__drone-admin-secrets + name: SSH_PORT + +--- +kind: secret +name: SSH_FINGERPRINT +get: + path: kv/data/__drone-admin-secrets + name: SSH_FINGERPRINT \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d41ef2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env* +themeforest-* +display/ +.DS_Store \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..814f84d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,142 @@ +version: "3.9" + +services: + wp_db: + image: mariadb:11 + container_name: "${REVERSE_DOMAIN}_wp_db" + environment: + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + MYSQL_DATABASE: "${DB_NAME}" + MYSQL_USER: "${DB_USER}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + expose: + - 3306 + volumes: + - wp_mysql:/var/lib/mysql + restart: always + networks: + - app_network + + wp_db_phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: "${REVERSE_DOMAIN}_db_phpmyadmin" + depends_on: + - wp_db + environment: + PMA_HOST: "${REVERSE_DOMAIN}_wp_db" + PMA_PORT: 3306 + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + MYSQL_DATABASE: "${DB_NAME}" + MYSQL_USER: "${DB_USER}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + UPLOAD_LIMIT: 300M + expose: + - 80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.${REVERSE_DOMAIN}_pma.rule=Host(`pma.${APPLICATION_DOMAIN_NAME}`)" + - "traefik.http.routers.${REVERSE_DOMAIN}_pma.entrypoints=websecure" + - "traefik.http.routers.${REVERSE_DOMAIN}_pma.tls.certresolver=myresolver" + - "traefik.http.services.${REVERSE_DOMAIN}_pma.loadbalancer.server.port=80" + # VERY IMPORTANT WHEN TWO NETWORKS + - "traefik.docker.network=shared_network" + restart: always + networks: + - shared_network + - app_network + + wp_filebrowser: + image: filebrowser/filebrowser:latest + container_name: "${REVERSE_DOMAIN}_filebrowser" + volumes: + - wp_content:/srv + - filebrowser_db:/database + - ./docker/filebrowser/settings.json:/config/settings.json + environment: + - PUID=$(id -u) + - PGID=$(id -g) + expose: + - 80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.${REVERSE_DOMAIN}_filebrowser.rule=Host(`ftp.${APPLICATION_DOMAIN_NAME}`)" + - "traefik.http.routers.${REVERSE_DOMAIN}_filebrowser.entrypoints=websecure" + - "traefik.http.routers.${REVERSE_DOMAIN}_filebrowser.tls.certresolver=myresolver" + - "traefik.http.services.${REVERSE_DOMAIN}_filebrowser.loadbalancer.server.port=80" + - "traefik.docker.network=shared_network" + restart: always + networks: + - shared_network + - app_network + + wp: + image: "${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG}" + container_name: "${REVERSE_DOMAIN}_wp" + depends_on: + - wp_db + environment: + WORDPRESS_DB_HOST: "${REVERSE_DOMAIN}_wp_db" + WORDPRESS_DB_NAME: "${DB_NAME}" + WORDPRESS_DB_USER: "${DB_USER}" + WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}" + WORDPRESS_DB_CHARSET: "utf8" + WORDPRESS_DB_COLLATE: "" + WORDPRESS_AUTH_KEY: "${AUTH_KEY}" + WORDPRESS_SECURE_AUTH_KEY: "${SECURE_AUTH_KEY}" + WORDPRESS_LOGGED_IN_KEY: "${LOGGED_IN_KEY}" + WORDPRESS_NONCE_KEY: "${NONCE_KEY}" + WORDPRESS_AUTH_SALT: "${AUTH_SALT}" + WORDPRESS_SECURE_AUTH_SALT: "${SECURE_AUTH_SALT}" + WORDPRESS_LOGGED_IN_SALT: "${LOGGED_IN_SALT}" + WORDPRESS_NONCE_SALT: "${NONCE_SALT}" + WORDPRESS_TABLE_PREFIX: "${TABLE_PREFIX}" + WORDPRESS_CONFIG_EXTRA: | + $$to_bool = include __DIR__ . '/to-bool.php'; + define( 'WP_DEBUG', $$to_bool('${WP_DEBUG}') ); + define( 'WP_DEBUG_LOG', $$to_bool('${WP_DEBUG_LOG}') ); + define( 'WP_DEBUG_DISPLAY', $$to_bool('${WP_DEBUG_DISPLAY}') ); + define( 'DISABLE_WP_CRON', $$to_bool('${DISABLE_WP_CRON}') ); + expose: + - 80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.${REVERSE_DOMAIN}.rule=Host(`${APPLICATION_DOMAIN_NAME}`)" + - "traefik.http.routers.${REVERSE_DOMAIN}.entrypoints=websecure" + - "traefik.http.routers.${REVERSE_DOMAIN}.tls.certresolver=myresolver" + - "traefik.http.services.${REVERSE_DOMAIN}.loadbalancer.server.port=80" + - "traefik.docker.network=shared_network" + volumes: + # only user data persists + - wp_content:/var/www/html/wp-content + - ./docker/wordpress/php.ini:/usr/local/etc/php/conf.d/uploads.ini:ro + - ./docker/wordpress/public_dir/to-bool.php:/var/www/html/to-bool.php:ro + restart: always + networks: + - shared_network + - app_network + + wp_cron: + image: registry.sn48.zivili.ch/meow/wp-cron:amd64-1.0.0 + container_name: "${REVERSE_DOMAIN}_wp_cron" + depends_on: + - wp + networks: + - app_network + +networks: + shared_network: + name: shared_network + external: true + app_network: + name: ${REVERSE_DOMAIN}-app_network + +volumes: + wp_mysql: + name: "${REVERSE_DOMAIN}_wp_db-volume" + external: true + wp_content: + name: "${REVERSE_DOMAIN}_wp-data" + external: true + filebrowser_db: + name: "${REVERSE_DOMAIN}_filebrowser_db" + external: true diff --git a/docker/filebrowser/settings.json b/docker/filebrowser/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/docker/wordpress/Dockerfile b/docker/wordpress/Dockerfile new file mode 100644 index 0000000..95dd96b --- /dev/null +++ b/docker/wordpress/Dockerfile @@ -0,0 +1,23 @@ +FROM wordpress:latest + +# WP-CLI and other extras … +RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \ + && chmod +x wp-cli.phar && mv wp-cli.phar /usr/local/bin/wp + +# keep the upstream entrypoint +RUN mv /usr/local/bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint-upstream.sh + +# –– tracked application code –––––––––––––––––– +COPY docker/wordpress/public_dir/themes /usr/src/themes +COPY docker/wordpress/public_dir/to-bool.php /usr/src/to-bool.php +COPY docker/wordpress/duplicator-initial-deploy /usr/src/duplicator + +COPY docker/wordpress/php.ini /usr/local/etc/php/php.ini + +# custom wrapper +COPY docker/wordpress/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 80 +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["apache2-foreground"] diff --git a/docker/wordpress/docker-entrypoint.sh b/docker/wordpress/docker-entrypoint.sh new file mode 100644 index 0000000..a1e3131 --- /dev/null +++ b/docker/wordpress/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +################################################################################ +# ONE-TIME Duplicator restore +################################################################################ +if [ ! -f /var/www/html/wp-content/.duplicator_restored ]; then + echo "▶ First run – copy Duplicator package" + mkdir -p /var/www/html + cp -r /usr/src/duplicator/* /var/www/html/ + chown -R www-data:www-data /var/www/html + touch /var/www/html/wp-content/.duplicator_restored + echo "✓ Package placed – browse /installer.php to finish the restore" +fi + +################################################################################ +# ALWAYS sync the code we keep under version control +################################################################################ +echo "▶ Syncing version-controlled themes" +rsync -a --delete /usr/src/themes/ /var/www/html/wp-content/themes/ + +################################################################################ +# (optional) run safe automatic DB migrations, plugin installs, etc. +# wp core update-db --yes +# wp plugin update --all --quiet +################################################################################ + +# Hand back to the real WP entrypoint +exec /usr/local/bin/docker-entrypoint-upstream.sh "$@" diff --git a/docker/wordpress/duplicator-initial-deploy/20200611_danipagescopywriter_e1859eb5880be3f43509_20250516104423_archive.zip b/docker/wordpress/duplicator-initial-deploy/20200611_danipagescopywriter_e1859eb5880be3f43509_20250516104423_archive.zip new file mode 100644 index 0000000..f9f1d21 Binary files /dev/null and b/docker/wordpress/duplicator-initial-deploy/20200611_danipagescopywriter_e1859eb5880be3f43509_20250516104423_archive.zip differ diff --git a/docker/wordpress/duplicator-initial-deploy/installer.php b/docker/wordpress/duplicator-initial-deploy/installer.php new file mode 100644 index 0000000..fffd1a9 --- /dev/null +++ b/docker/wordpress/duplicator-initial-deploy/installer.php @@ -0,0 +1,1461 @@ +setHTTPHeaders(); + $this->targetRoot = self::setSafePath(__DIR__); + $this->log('', true); + $archive_filepath = $this->getArchiveFilePath(); + $this->origDupInstFolder = self::INSTALLER_DIR_NAME; + $this->targetDupInstFolder = filter_input(INPUT_GET, 'dup_folder', FILTER_SANITIZE_SPECIAL_CHARS, array( + "options" => array( + "default" => self::INSTALLER_DIR_NAME, + ), + 'flags' => FILTER_FLAG_STRIP_HIGH)); + $this->isCustomDupFolder = $this->origDupInstFolder !== $this->targetDupInstFolder; + $this->targetDupInst = $this->targetRoot . '/' . $this->targetDupInstFolder; + $this->manualExtractFileName = 'dup-manual-extract__' . self::PACKAGE_HASH; + if ($this->isCustomDupFolder) { + $this->extractionTmpFolder = $this->getTempDir($this->targetRoot); + } else { + $this->extractionTmpFolder = $this->targetRoot; + } + DUPX_CSRF::init($this->targetDupInst, self::PACKAGE_HASH); + $archiveActualSize = @file_exists($archive_filepath) ? @filesize($archive_filepath) : false; + $archiveActualSize = ($archiveActualSize !== false) ? $archiveActualSize : 0; + $this->hasZipArchive = class_exists('ZipArchive'); + $this->hasShellExecUnzip = $this->getUnzipFilePath() != null ? true : false; + $this->archiveExpectedSize = strlen(self::ARCHIVE_SIZE) ? self::ARCHIVE_SIZE : 0; + $this->archiveActualSize = $archiveActualSize; + if ($this->archiveExpectedSize > 0) { + $this->archiveRatio = (((1.0) * $this->archiveActualSize) / $this->archiveExpectedSize) * 100; + } else { + $this->archiveRatio = 100; + } + } + public static function getInstance() + { + if (is_null(self::$instance)) { + self::$instance = new self(); + } + return self::$instance; + } + private function setHTTPHeaders() + { + header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); + header("Cache-Control: post-check=0, pre-check=0", false); + header("Pragma: no-cache"); + } + private function getTempDir($path) + { + $tempfile = tempnam($path, 'dup-installer_tmp_'); + if (file_exists($tempfile)) { + unlink($tempfile); + mkdir($tempfile); + if (is_dir($tempfile)) { + return $tempfile; + } + } + return false; + } + public static function phpVersionCheck() + { + if (version_compare(PHP_VERSION, self::MINIMUM_PHP_VERSION, '>=')) { + return true; + } + $match = null; + if (preg_match("#^\d+(\.\d+)*#", PHP_VERSION, $match)) { + $phpVersion = $match[0]; + } else { + $phpVersion = PHP_VERSION; + } + ?> + + + + + Duplicator - issue + + +
+

DUPLICATOR ISSUE: PHP REQUIRED

+

+ This server is running PHP: . A minimum of PHP + is required.

+ Contact your hosting provider or server administrator and let them know you would like to upgrade your PHP version. +

+
+ + + log('==DUPLICATOR INSTALLER BOOTSTRAP v' . self::VERSION . '=='); + $this->log('----------------------------------------------------'); + $this->log('Installer bootstrap start'); + $archive_filepath = $this->getArchiveFilePath(); + $archive_filename = self::ARCHIVE_FILENAME; + $error = null; + $is_installer_file_valid = true; + if (preg_match('/_([a-z0-9]{7})[a-z0-9]+_[0-9]{6}([0-9]{8})_archive.(?:zip|daf)$/', $archive_filename, $matches)) { + $expected_package_hash = $matches[1] . '-' . $matches[2]; + if (self::PACKAGE_HASH != $expected_package_hash) { + $is_installer_file_valid = false; + $this->log("[ERROR] Installer and archive mismatch detected."); + } + } else { + $this->log("[ERROR] Invalid archive file name."); + $is_installer_file_valid = false; + } + if (false === $is_installer_file_valid) { + $error = "Installer and archive mismatch detected. + Ensure uncorrupted installer and matching archive are present."; + return $error; + } + $extract_installer = true; + $extract_success = false; + $archiveExpectedEasy = $this->readableByteSize($this->archiveExpectedSize); + $archiveActualEasy = $this->readableByteSize($this->archiveActualSize); + $archive_extension = strtolower(pathinfo($archive_filepath, PATHINFO_EXTENSION)); + $installer_dir_found = ( + file_exists($this->targetDupInst) && + file_exists($this->targetDupInst . "/main.installer.php") && + file_exists($this->targetDupInst . "/dup-archive__" . self::PACKAGE_HASH . ".txt") + ); + $manual_extract_found = ( + $installer_dir_found && + file_exists($this->targetDupInst . "/" . $this->manualExtractFileName) + ); + $isZip = ($archive_extension == 'zip'); + if (!$manual_extract_found) { + if (!file_exists($archive_filepath)) { + $this->log("[ERROR] Archive file not found!"); + $archive_candidates = ($isZip) ? $this->getFilesWithExtension('zip') : $this->getFilesWithExtension('daf'); + $candidate_count = count($archive_candidates); + $candidate_html = "- No {$archive_extension} files found -"; + if ($candidate_count >= 1) { + $candidate_html = "
    "; + foreach ($archive_candidates as $archive_candidate) { + $candidate_html .= '
  1. ' . $this->compareStrings($archive_filename, $archive_candidate) . '
  2. '; + } + $candidate_html .= "
"; + } + $error = "" + . "Archive not found! The required archive file must be present in the 'Extraction Path' below. " + . "When the archive file name was created it was named with a secure file name. This file name must be " + . "the exact same name as when it was created character for character. Each archive file has a unique installer associated " + . "with it and must be used together. See the list below for more options:
" + . ""; + return $error; + } + $archive_size = self::ARCHIVE_SIZE; + if (!empty($archive_size) && !self::checkInputValidInt(self::ARCHIVE_SIZE)) { + $no_of_bits = PHP_INT_SIZE * 8; + $error = 'Current is a ' . $no_of_bits . '-bit SO. This archive is too large for ' . $no_of_bits . '-bit PHP.' . '
'; + $this->log('[ERROR] ' . $error); + $error .= 'Possibibles solutions:
'; + $error .= '- Use the file filters to get your package lower to support this server or try the package on a Linux server.' . '
'; + $error .= '- Perform a ' . + 'Manual Extract Install' . '
'; + switch ($no_of_bits == 32) { + case 32: + $error .= '- Ask your host to upgrade the server to 64-bit PHP or install on another system has 64-bit PHP' . '
'; + break; + case 64: + $error .= '- Ask your host to upgrade the server to 128-bit PHP or install on another system has 128-bit PHP' . '
'; + break; + } + if (self::isWindows()) { + $error .= '- ' . + 'Windows DupArchive extractor to extract all files from the archive.' . '
'; + } + return $error; + } + if (($this->archiveRatio < 90) && ($this->archiveActualSize > 0) && ($this->archiveExpectedSize > 0)) { + $this->log( + "ERROR: The expected archive size should be around [{$archiveExpectedEasy}]. " . + "The actual size is currently [{$archiveActualEasy}]." + ); + $this->log("ERROR: The archive file may not have fully been downloaded to the server"); + $percent = round($this->archiveRatio); + $autochecked = isset($_POST['auto-fresh']) ? "checked='true'" : ''; + $error = "Archive file size warning.
The expected archive size is [{$archiveExpectedEasy}]. " + . "Currently the archive size is [{$archiveActualEasy}].
" + . "The archive file may have not fully been uploaded to the server." + . ""; + return $error; + } + } + if ($installer_dir_found) { + if (($extract_installer = filter_input(INPUT_GET, 'force-extract-installer', FILTER_VALIDATE_BOOLEAN))) { + $this->log("Manual extract found with force extract installer get parametr"); + } else { + $this->log("Manual extract found so not going to extract " . $this->targetDupInstFolder . " dir"); + } + } else { + $extract_installer = true; + } + if (file_exists($this->targetDupInst)) { + $this->log("EXTRACT " . $this->targetDupInstFolder . " dir"); + $hash_pattern = '[a-z0-9][a-z0-9][a-z0-9][a-z0-9][a-z0-9][a-z0-9][a-z0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'; + $file_patterns_with_hash_file = array( + 'dup-archive__' . $hash_pattern . '.txt' => 'dup-archive__' . self::PACKAGE_HASH . '.txt', + 'dup-database__' . $hash_pattern . '.sql' => 'dup-database__' . self::PACKAGE_HASH . '.sql', + 'dup-installer-data__' . $hash_pattern . '.sql' => 'dup-installer-data__' . self::PACKAGE_HASH . '.sql', + 'dup-installer-log__' . $hash_pattern . '.txt' => 'dup-installer-log__' . self::PACKAGE_HASH . '.txt', + 'dup-scan__' . $hash_pattern . '.json' => 'dup-scan__' . self::PACKAGE_HASH . '.json', + 'dup-scanned-dirs__' . $hash_pattern . '.txt' => 'dup-scanned-dirs__' . self::PACKAGE_HASH . '.txt', + 'dup-scanned-files__' . $hash_pattern . '.txt' => 'dup-scanned-files__' . self::PACKAGE_HASH . '.txt', + ); + foreach ($file_patterns_with_hash_file as $file_pattern => $hash_file) { + $globs = glob($this->targetDupInst . '/' . $file_pattern); + if (!empty($globs)) { + foreach ($globs as $glob) { + $file = basename($glob); + if ($file != $hash_file) { + if (unlink($glob)) { + $this->log('Successfully deleted the file ' . $glob); + } else { + $error .= '[ERROR] Error deleting the file ' . $glob . ' Please manually delete it and try again.'; + $this->log($error); + } + } + } + } + } + } + if ($extract_installer) { + $this->log("Ready to extract the installer"); + $this->log("Checking permission of destination folder"); + $destination = $this->targetRoot; + if (!is_writable($destination)) { + $this->log("destination folder for extraction is not writable"); + if (self::chmod($destination, 'u+rwx')) { + $this->log("Permission of destination folder changed to u+rwx"); + } else { + $this->log("[ERROR] Permission of destination folder failed to change to u+rwx"); + } + } + if (!is_writable($destination)) { + $this->log("WARNING: The {$destination} directory is not writable."); + $error = "NOTICE: The {$destination} directory is not writable on this server please talk to your host or server admin about making "; + $error .= "" . + "writable {$destination} directory on this server.
"; + return $error; + } + if ($isZip) { + $zip_mode = $this->getZipMode(); + if (($zip_mode == self::ZIP_MODE_AUTO) || ($zip_mode == self::ZIP_MODE_ARCHIVE) && class_exists('ZipArchive')) { + if ($this->hasZipArchive) { + $this->log("ZipArchive exists so using that"); + $extract_success = $this->extractInstallerZipArchive($archive_filepath, $this->origDupInstFolder, $this->extractionTmpFolder); + if ($extract_success) { + $this->log('Successfully extracted with ZipArchive'); + } else { + if (0 == $this->installer_files_found) { + $error = "[ERROR] This archive is not properly formatted and does not contain a " . $this->origDupInstFolder . + " directory. Please make sure you are attempting to install " . + "the original archive and not one that has been reconstructed."; + $this->log($error); + return $error; + } else { + $error = '[ERROR] Error extracting with ZipArchive. '; + $this->log($error); + } + } + } else { + $this->log("WARNING: ZipArchive is not enabled."); + $error = "NOTICE: ZipArchive is not enabled on this server please talk to your host or server admin about enabling "; + $error .= "" . + "ZipArchive on this server.
"; + } + } + if (!$extract_success) { + if (($zip_mode == self::ZIP_MODE_AUTO) || ($zip_mode == self::ZIP_MODE_SHELL)) { + $unzip_filepath = $this->getUnzipFilePath(); + if ($unzip_filepath != null) { + $extract_success = $this->extractInstallerShellexec($archive_filepath, $this->origDupInstFolder, $this->extractionTmpFolder); + $this->log("Resetting perms of items in folder {$this->targetDupInstFolder}"); + self::setPermsToDefaultR($this->targetDupInstFolder); + if ($extract_success) { + $this->log('Successfully extracted with Shell Exec'); + $error = null; + } else { + $error .= '[ERROR] Error extracting with Shell Exec. ' . + 'Please manually extract archive then choose Advanced > Manual Extract in installer.'; + $this->log($error); + } + } else { + $this->log('WARNING: Shell Exec Zip is not available'); + $error .= "NOTICE: Shell Exec is not enabled on this server please talk to your host or server admin about enabling "; + $error .= "Shell Exec " . + "on this server or manually extract archive then choose Advanced > Manual Extract in installer."; + } + } + } + if (!$extract_success && $zip_mode == self::ZIP_MODE_AUTO) { + $unzip_filepath = $this->getUnzipFilePath(); + if (!class_exists('ZipArchive') && empty($unzip_filepath)) { + $this->log("WARNING: ZipArchive and Shell Exec are not enabled on this server."); + $error = "NOTICE: ZipArchive and Shell Exec are not enabled on this server please " . + "talk to your host or server admin about enabling "; + $error .= "ZipArchive " . + "or Shell Exec " . + "on this server or manually extract archive then choose Advanced > Manual Extract in installer."; + } + } + } else { + try { + DupArchiveExpandBasicEngine::setCallbacks( + array($this, 'log'), + array($this, 'chmod'), + array($this, 'mkdir') + ); + $offset = DupArchiveExpandBasicEngine::getExtraOffset($archive_filepath); + $this->log('Expand directory from offset ' . $offset); + DupArchiveExpandBasicEngine::expandDirectory( + $archive_filepath, + $this->origDupInstFolder, + $this->extractionTmpFolder, + false, + $offset + ); + @unlink($this->extractionTmpFolder . "/" . $this->origDupInstFolder . "/" . $this->manualExtractFileName); + } catch (Exception $ex) { + $this->log("[ERROR] Error expanding installer subdirectory:" . $ex->getMessage()); + throw $ex; + } + } + if ($this->isCustomDupFolder) { + $this->log("Move dup-installer folder to custom folder:" . $this->targetDupInst); + if (file_exists($this->targetDupInst)) { + $this->log('Custom folder already exists so delete it'); + if (self::rrmdir($this->targetDupInst) == false) { + throw new Exception('Can\'t remove custom target folder'); + } + } + if (rename($this->extractionTmpFolder . '/' . $this->origDupInstFolder, $this->targetDupInst) === false) { + throw new Exception('Can\'t rename the tmp dup-installer folder'); + } + } + $htaccessToRemove = $this->targetDupInst.'/.htaccess'; + if (is_file($htaccessToRemove) && is_writable($htaccessToRemove)) { + $this->log("Remove Htaccess in dup-installer folder"); + @unlink($htaccessToRemove); + } + $is_apache = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false || strpos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') !== false); + $is_nginx = (strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false); + $sapi_type = php_sapi_name(); + $php_ini_data = array( + 'max_execution_time' => 3600, + 'max_input_time' => -1, + 'ignore_user_abort' => 'On', + 'post_max_size' => '4096M', + 'upload_max_filesize' => '4096M', + 'memory_limit' => DUPLICATOR_PHP_MAX_MEMORY, + 'default_socket_timeout' => 3600, + 'pcre.backtrack_limit' => 99999999999, + ); + $sapi_type_first_three_chars = substr($sapi_type, 0, 3); + if ('fpm' === $sapi_type_first_three_chars) { + $this->log("SAPI: FPM"); + if ($is_apache) { + $this->log('Server: Apache'); + } elseif ($is_nginx) { + $this->log('Server: Nginx'); + } + if (($is_apache && function_exists('apache_get_modules') && in_array('mod_rewrite', apache_get_modules())) || $is_nginx) { + $htaccess_data = array(); + foreach ($php_ini_data as $php_ini_key => $php_ini_val) { + if ($is_apache) { + $htaccess_data[] = 'SetEnv PHP_VALUE "' . $php_ini_key . ' = ' . $php_ini_val . '"'; + } elseif ($is_nginx) { + if ('On' == $php_ini_val || 'Off' == $php_ini_val) { + $htaccess_data[] = 'php_flag ' . $php_ini_key . ' ' . $php_ini_val; + } else { + $htaccess_data[] = 'php_value ' . $php_ini_key . ' ' . $php_ini_val; + } + } + } + $htaccess_text = implode("\n", $htaccess_data); + $htaccess_file_path = $this->targetDupInst . '/.htaccess'; + $this->log("creating {$htaccess_file_path} with the content:"); + $this->log($htaccess_text); + @file_put_contents($htaccess_file_path, $htaccess_text); + } + } elseif ('cgi' === $sapi_type_first_three_chars || 'litespeed' === $sapi_type) { + if ('cgi' === $sapi_type_first_three_chars) { + $this->log("SAPI: CGI"); + } else { + $this->log("SAPI: litespeed"); + } + if (version_compare(phpversion(), 5.5) >= 0 && (!$is_apache || 'litespeed' === $sapi_type)) { + $ini_data = array(); + foreach ($php_ini_data as $php_ini_key => $php_ini_val) { + $ini_data[] = $php_ini_key . ' = ' . $php_ini_val; + } + $ini_text = implode("\n", $ini_data); + $ini_file_path = $this->targetDupInst . '/.user.ini'; + $this->log("creating {$ini_file_path} with the content:"); + $this->log($ini_text); + @file_put_contents($ini_file_path, $ini_text); + } else { + $this->log("No need to create " . $this->targetDupInstFolder . "/.htaccess or " . $this->targetDupInstFolder . "/.user.ini"); + } + } elseif ("apache2handler" === $sapi_type) { + $this->log("No need to create " . $this->targetDupInstFolder . "/.htaccess or " . $this->targetDupInstFolder . "/.user.ini"); + $this->log("SAPI: apache2handler"); + } + else { + $this->log("No need to create " . $this->targetDupInstFolder . "/.htaccess or " . $this->targetDupInstFolder . "/.user.ini"); + $this->log("ERROR: SAPI: Unrecognized"); + } + } else { + $this->log("NOTICE: Didn't need to extract the installer."); + } + if (empty($error)) { + if ($this->isCustomDupFolder && file_exists($this->extractionTmpFolder)) { + rmdir($this->extractionTmpFolder); + } + $config_files = glob($this->targetDupInst . '/dup-archive__*.txt'); + $config_file_absolute_path = array_pop($config_files); + if (!file_exists($config_file_absolute_path)) { + $error = 'Archive config file not found in ' . $this->targetDupInstFolder . ' folder.

'; + return $error; + } + } + $uri_start = self::getCurrentUrl(false, false, 1); + if ($error === null) { + if (!file_exists($this->targetDupInst)) { + $error = 'Can\'t extract installer directory. ' . + 'See this FAQ item' . + ' for details on how to resolve.'; + } + if ($error == null) { + $bootloader_name = basename(__FILE__); + $this->mainInstallerURL = $uri_start . '/' . $this->targetDupInstFolder . '/main.installer.php'; + $this->archive = $archive_filepath; + $this->bootloader = $bootloader_name; + $this->fixInstallerPerms($this->mainInstallerURL); + $this->log("DONE: No detected errors so redirecting to the main installer. Main Installer URI = {$this->mainInstallerURL}"); + } + } + return $error; + } + public static function getCurrentUrl($queryString = true, $requestUri = false, $getParentDirLevel = 0) + { + if (isset($_SERVER['HTTP_X_ORIGINAL_HOST'])) { + $host = $_SERVER['HTTP_X_ORIGINAL_HOST']; + } else { + $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; //WAS SERVER_NAME and caused problems on some boxes + } + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + $_SERVER ['HTTPS'] = 'on'; + } + if (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'https') { + $_SERVER ['HTTPS'] = 'on'; + } + if (isset($_SERVER['HTTP_CF_VISITOR'])) { + $visitor = json_decode($_SERVER['HTTP_CF_VISITOR']); + if ($visitor->scheme == 'https') { + $_SERVER ['HTTPS'] = 'on'; + } + } + $protocol = 'http' . ((isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on') ? 's' : ''); + if ($requestUri) { + $serverUrlSelf = preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']); + } else { + $serverUrlSelf = $_SERVER['SCRIPT_NAME']; + for ($i = 0; $i < $getParentDirLevel; $i++) { + $serverUrlSelf = preg_match('/^[\\\\\/]?$/', dirname($serverUrlSelf)) ? '' : dirname($serverUrlSelf); + } + } + $query = ($queryString && isset($_SERVER['QUERY_STRING']) && strlen($_SERVER['QUERY_STRING']) > 0 ) ? '?' . $_SERVER['QUERY_STRING'] : ''; + return $protocol . '://' . $host . $serverUrlSelf . $query; + } + private function fixInstallerPerms() + { + $file_perms = 'u+rw'; + $dir_perms = 'u+rwx'; + $installer_dir_path = $this->targetDupInstFolder; + $this->setPerms($installer_dir_path, $dir_perms, false); + $this->setPerms($installer_dir_path, $file_perms, true); + } + private function setPerms($directory, $perms, $do_files) + { + if (!$do_files) { + $this->setPermsOnItem($directory, $perms); + } + $item_names = array_diff(scandir($directory), array('.', '..')); + foreach ($item_names as $item_name) { + $path = "$directory/$item_name"; + if (($do_files && is_file($path)) || (!$do_files && !is_file($path))) { + $this->setPermsOnItem($path, $perms); + } + } + } + private function setPermsOnItem($path, $perms) + { + if (($result = self::chmod($path, $perms)) === false) { + $this->log("ERROR: Couldn't set permissions of $path
"); + } else { + $this->log("Set permissions of $path
"); + } + return $result; + } + private function compareStrings($oldString, $newString) + { + $ret = ''; + for ($i = 0; isset($oldString[$i]) || isset($newString[$i]); $i++) { + if (!isset($oldString[$i])) { + $ret .= '' . $newString[$i] . ''; + continue; + } + for ($char = 0; isset($oldString[$i][$char]) || isset($newString[$i][$char]); $char++) { + if (!isset($oldString[$i][$char])) { + $ret .= '' . substr($newString[$i], $char) . ''; + break; + } elseif (!isset($newString[$i][$char])) { + break; + } + if (ord($oldString[$i][$char]) != ord($newString[$i][$char])) { + $ret .= '' . $newString[$i][$char] . ''; + } else { + $ret .= $newString[$i][$char]; + } + } + } + return $ret; + } + public function log($s, $deleteOld = false) + { + static $logfile = null; + if (is_null($logfile)) { + $logfile = $this->getBootLogFilePath(); + } + if ($deleteOld && file_exists($logfile)) { + @unlink($logfile); + } + $timestamp = date('M j H:i:s'); + return @file_put_contents($logfile, '[' . $timestamp . '] ' . self::postprocessLog($s) . "\n", FILE_APPEND); + } + public function getBootLogFilePath() + { + return $this->targetRoot . '/dup-installer-bootlog__' . self::SECONDARY_PACKAGE_HASH . '.txt'; + } + protected static function postprocessLog($str) + { + return str_replace(array( + self::getArchiveFileHash(), + self::PACKAGE_HASH, + self::SECONDARY_PACKAGE_HASH + ), '[HASH]', $str); + } + public static function getArchiveFileHash() + { + static $fileHash = null; + if (is_null($fileHash)) { + $fileHash = preg_replace('/^.+_([a-z0-9]+)_[0-9]{14}_archive\.(?:daf|zip)$/', '$1', self::ARCHIVE_FILENAME); + } + return $fileHash; + } + private function extractInstallerZipArchive($archive_filepath, $origDupInstFolder, $destination, $checkSubFolder = false) + { + $success = true; + $zipArchive = new ZipArchive(); + $subFolderArchiveList = array(); + if (($zipOpenRes = $zipArchive->open($archive_filepath)) === true) { + $this->log("Successfully opened archive file."); + $folder_prefix = $origDupInstFolder . '/'; + $this->log("Extracting all files from archive within " . $origDupInstFolder); + $installer_files_found = 0; + for ($i = 0; $i < $zipArchive->numFiles; $i++) { + $stat = $zipArchive->statIndex($i); + if ($checkSubFolder == false) { + $filenameCheck = $stat['name']; + $filename = $stat['name']; + $tmpSubFolder = null; + } else { + $safePath = rtrim(self::setSafePath($stat['name']), '/'); + $tmpArray = explode('/', $safePath); + if (count($tmpArray) < 2) { + continue; + } + $tmpSubFolder = $tmpArray[0]; + array_shift($tmpArray); + $filenameCheck = implode('/', $tmpArray); + $filename = $stat['name']; + } + if ($this->startsWith($filenameCheck, $folder_prefix)) { + $installer_files_found++; + if (!empty($tmpSubFolder) && !in_array($tmpSubFolder, $subFolderArchiveList)) { + $subFolderArchiveList[] = $tmpSubFolder; + } + if (basename($filename) === $this->manualExtractFileName) { + $this->log("Skipping manual extract file: {$filename}"); + continue; + } + if ($zipArchive->extractTo($destination, $filename) === true) { + $this->log("Success: {$filename} >>> {$destination}"); + } else { + $this->log("[ERROR] Error extracting {$filename} from archive archive file"); + $success = false; + break; + } + } + } + if ($checkSubFolder && count($subFolderArchiveList) !== 1) { + $this->log("Error: Multiple dup subfolder archive"); + $success = false; + } else { + if ($checkSubFolder) { + $this->moveUpfromSubFolder($destination . '/' . $subFolderArchiveList[0], true); + } + $lib_directory = $destination . '/' . $origDupInstFolder . '/lib'; + $snaplib_directory = $lib_directory . '/snaplib'; + if (!file_exists($snaplib_directory)) { + $folder_prefix = 'snaplib/'; + $destination = $lib_directory; + for ($i = 0; $i < $zipArchive->numFiles; $i++) { + $stat = $zipArchive->statIndex($i); + $filename = $stat['name']; + if ($this->startsWith($filename, $folder_prefix)) { + $installer_files_found++; + if ($zipArchive->extractTo($destination, $filename) === true) { + $this->log("Success: {$filename} >>> {$destination}"); + } else { + $this->log("[ERROR] Error extracting {$filename} from archive archive file"); + $success = false; + break; + } + } + } + } + } + if ($zipArchive->close() === true) { + $this->log("Successfully closed archive file"); + } else { + $this->log("[ERROR] Problem closing archive file"); + $success = false; + } + if ($success != false && $installer_files_found < 10) { + if ($checkSubFolder) { + $this->log("[ERROR] Couldn't find the installer directory in the archive!"); + $success = false; + } else { + $this->log("[ERROR] Couldn't find the installer directory in archive root! Check subfolder"); + $this->extractInstallerZipArchive($archive_filepath, $origDupInstFolder, $destination, true); + } + } + } else { + $this->log("[ERROR] Couldn't open archive archive file with ZipArchive CODE[" . $zipOpenRes . "]"); + $success = false; + } + return $success; + } + public static function isWindows() + { + static $isWindows = null; + if (is_null($isWindows)) { + $isWindows = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + } + return $isWindows; + } + public static function maxPathLen() + { + static $maxPath = null; + if (is_null($maxPath)) { + if (defined('PHP_MAXPATHLEN')) { + $maxPath = PHP_MAXPATHLEN; + } else { + $maxPath = self::isWindows() ? 260 : 4096; + } + } + return $maxPath; + } + public static function setPermsToDefaultR($directory) + { + $dir = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); + $iterator = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::SELF_FIRST); + $defaultFilePermission = 0666 & ~umask(); + $defaultDirPermission = 0777 & ~umask(); + foreach ($iterator as $item) { + if ($item->isFile()) { + self::chmod($item->getPathname(), $defaultFilePermission); + } + if ($item->isDir()) { + self::chmod($item->getPathname(), $defaultDirPermission); + } + } + } + public static function chmod($file, $mode) + { + if (!file_exists($file)) { + return false; + } + $octalMode = 0; + if (is_int($mode)) { + $octalMode = $mode; + } elseif (is_string($mode)) { + $mode = trim($mode); + if (preg_match('/([0-7]{1,3})/', $mode)) { + $octalMode = intval(('0' . $mode), 8); + } elseif (preg_match_all('/(a|[ugo]{1,3})([-=+])([rwx]{1,3})/', $mode, $gMatch, PREG_SET_ORDER)) { + if (!function_exists('fileperms')) { + return false; + } + $octalMode = (fileperms($file) & 0777); + foreach ($gMatch as $matches) { + $group = $matches[1]; + if ($group === 'a') { + $group = 'ugo'; + } + $action = $matches[2]; + $gPerms = $matches[3]; + $octalGroupMode = 0; + $subPerm = 0; + $subPerm += strpos($gPerms, 'x') !== false ? 1 : 0; // mask 001 + $subPerm += strpos($gPerms, 'w') !== false ? 2 : 0; // mask 010 + $subPerm += strpos($gPerms, 'r') !== false ? 4 : 0; // mask 100 + $ugoLen = strlen($group); + if ($action === '=') { + $ugoMaskInvert = 0777; + for ($i = 0; $i < $ugoLen; $i++) { + switch ($group[$i]) { + case 'u': + $octalGroupMode = $octalGroupMode | $subPerm << 6; // mask xxx000000 + $ugoMaskInvert = $ugoMaskInvert & 077; + break; + case 'g': + $octalGroupMode = $octalGroupMode | $subPerm << 3; // mask 000xxx000 + $ugoMaskInvert = $ugoMaskInvert & 0707; + break; + case 'o': + $octalGroupMode = $octalGroupMode | $subPerm; // mask 000000xxx + $ugoMaskInvert = $ugoMaskInvert & 0770; + break; + } + } + $octalMode = $octalMode & ($ugoMaskInvert | $octalGroupMode); + } else { + for ($i = 0; $i < $ugoLen; $i++) { + switch ($group[$i]) { + case 'u': + $octalGroupMode = $octalGroupMode | $subPerm << 6; // mask xxx000000 + break; + case 'g': + $octalGroupMode = $octalGroupMode | $subPerm << 3; // mask 000xxx000 + break; + case 'o': + $octalGroupMode = $octalGroupMode | $subPerm; // mask 000000xxx + break; + } + } + switch ($action) { + case '+': + $octalMode = $octalMode | $octalGroupMode; + break; + case '-': + $octalMode = $octalMode & ~$octalGroupMode; + break; + } + } + } + } + } + if (function_exists('fileperms') && $octalMode === (fileperms($file) & 0777)) { + return true; + } + if (!function_exists('chmod')) { + return false; + } + return @chmod($file, $octalMode); + } + public static function checkInputValidInt($input) + { + return (filter_var($input, FILTER_VALIDATE_INT) === 0 || filter_var($input, FILTER_VALIDATE_INT)); + } + public static function mkdir($path, $mode = 0777, $recursive = false, $context = null) + { + if (strlen($path) > self::maxPathLen()) { + throw new Exception('Skipping a file that exceeds allowed max path length [' . self::maxPathLen() . ']. File: ' . $filepath); + } + if (!file_exists($path)) { + if (!function_exists('mkdir')) { + return false; + } + if (!@mkdir($path, 0777, $recursive)) { + return false; + } + } + return self::chmod($path, $mode); + } + private function moveUpfromSubFolder($subFolderName, $deleteSubFolder = false) + { + if (!is_dir($subFolderName)) { + return false; + } + $parentFolder = dirname($subFolderName); + if (!is_writable($parentFolder)) { + return false; + } + $success = true; + if (($subList = glob(rtrim($subFolderName, '/') . '/*', GLOB_NOSORT)) === false) { + $this->log("[ERROR] Problem glob folder " . $subFolderName); + return false; + } else { + foreach ($subList as $cName) { + $destination = $parentFolder . '/' . basename($cName); + if (file_exists($destination)) { + $success = self::rrmdir($destination); + } + if ($success) { + $success = rename($cName, $destination); + } else { + break; + } + } + if ($success && $deleteSubFolder) { + $success = self::rrmdir($subFolderName, true); + } + } + if (!$success) { + $this->log("[ERROR] Problem om moveUpfromSubFolder subFolder:" . $subFolderName); + } + return $success; + } + private function extractInstallerShellexec($archive_filepath, $origDupInstFolder, $destination) + { + $success = false; + $this->log("Attempting to use Shell Exec"); + $unzip_filepath = $this->getUnzipFilePath(); + if ($unzip_filepath != null) { + $unzip_command = "$unzip_filepath -q $archive_filepath " . + $origDupInstFolder . '/* -d ' . $destination . ' -x ' . + $origDupInstFolder . '/' . $this->manualExtractFileName . ' 2>&1'; + $this->log("Executing $unzip_command"); + $stderr = shell_exec($unzip_command); + $lib_directory = $destination . '/' . $origDupInstFolder . '/lib'; + $snaplib_directory = $lib_directory . '/snaplib'; + if (!file_exists($snaplib_directory)) { + $local_lib_directory = $destination . '/snaplib'; + $unzip_command = "$unzip_filepath -q $archive_filepath snaplib/* -d '.$destination.' 2>&1"; + $this->log("Executing unzip command"); + $stderr .= shell_exec($unzip_command); + self::mkdir($lib_directory, 'u+rwx'); + rename($local_lib_directory, $snaplib_directory); + } + if ($stderr == '') { + $this->log("Shell exec unzip succeeded"); + $success = true; + } else { + $this->log("[ERROR] Shell exec unzip failed. Output={$stderr}"); + } + } + return $success; + } + private function getArchiveFilePath() + { + if (($archive_filepath = filter_input(INPUT_GET, 'archive', FILTER_SANITIZE_SPECIAL_CHARS)) != false) { + if (is_dir($archive_filepath) && file_exists($archive_filepath . '/' . self::ARCHIVE_FILENAME)) { + $archive_filepath = $archive_filepath . '/' . self::ARCHIVE_FILENAME; + } else { + $archive_filepath = $archive_filepath; + } + } else { + $archive_filepath = $this->targetRoot . '/' . self::ARCHIVE_FILENAME; + } + if (($realPath = realpath($archive_filepath)) !== false) { + return $realPath; + } else { + return $archive_filepath; + } + } + private function getZipMode() + { + $zip_mode = self::ZIP_MODE_AUTO; + if (isset($_GET['zipmode'])) { + $zipmode_string = $_GET['zipmode']; + $this->log("Unzip mode specified in querystring: $zipmode_string"); + switch ($zipmode_string) { + case 'autounzip': + $zip_mode = self::ZIP_MODE_AUTO; + break; + case 'ziparchive': + $zip_mode = self::ZIP_MODE_ARCHIVE; + break; + case 'shellexec': + $zip_mode = self::ZIP_MODE_SHELL; + break; + } + } + return $zip_mode; + } + private function startsWith($haystack, $needle) + { + return $needle === "" || strrpos($haystack, $needle, - strlen($haystack)) !== false; + } + public function hasShellExec() + { + $cmds = array('shell_exec', 'escapeshellarg', 'escapeshellcmd', 'extension_loaded'); + if (array_intersect($cmds, array_map('trim', explode(',', @ini_get('disable_functions'))))) { + return false; + } + if (extension_loaded('suhosin')) { + $suhosin_ini = @ini_get("suhosin.executor.func.blacklist"); + if (array_intersect($cmds, array_map('trim', explode(',', $suhosin_ini)))) { + return false; + } + } + if (! function_exists('shell_exec')) { + return false; + } + if (!@shell_exec('echo duplicator')) { + return false; + } + return true; + } + public function getUnzipFilePath() + { + $filepath = null; + if ($this->hasShellExec()) { + if (shell_exec('hash unzip 2>&1') == null) { + $filepath = 'unzip'; + } else { + $possible_paths = array( + '/usr/bin/unzip', + '/opt/local/bin/unzip', + '/bin/unzip', + '/usr/local/bin/unzip', + '/usr/sfw/bin/unzip', + '/usr/xdg4/bin/unzip', + '/opt/bin/unzip', + ); + foreach ($possible_paths as $path) { + if (file_exists($path)) { + $filepath = $path; + break; + } + } + } + } + return $filepath; + } + public function readableByteSize($size) + { + try { + $units = array('B', 'KB', 'MB', 'GB', 'TB'); + for ($i = 0; $size >= 1024 && $i < 4; $i++) { + $size /= 1024; + } + return round($size, 2) . $units[$i]; + } catch (Exception $e) { + return "n/a"; + } + } + public function getFilesWithExtension($extension) + { + $files = array(); + foreach (glob("*.{$extension}") as $name) { + if (file_exists($name)) { + $files[] = $name; + } + } + if (count($files) > 0) { + return $files; + } + if (($dh = opendir($this->targetRoot))) { + while (false !== ($name = readdir($dh))) { + $ext = substr($name, strrpos($name, '.') + 1); + if (in_array($ext, array($extension))) { + $files[] = $name; + } + } + closedir($dh); + } + return $files; + } + public static function rrmdir($path) + { + if (is_dir($path)) { + if (($dh = opendir($path)) === false) { + return false; + } + while (($object = readdir($dh)) !== false) { + if ($object == "." || $object == "..") { + continue; + } + if (!self::rrmdir($path . "/" . $object)) { + closedir($dh); + return false; + } + } + closedir($dh); + return @rmdir($path); + } else { + if (is_writable($path)) { + return @unlink($path); + } else { + return false; + } + } + } + public static function setSafePath($path) + { + return str_replace("\\", "/", $path); + } + } + class LogHandler + { + private static $initialized = false; + public static function init_error_handler() + { + if (!self::$initialized) { + @set_error_handler(array(__CLASS__, 'error')); + @register_shutdown_function(array(__CLASS__, 'shutdown')); + self::$initialized = true; + } + } + public static function error($errno, $errstr, $errfile, $errline) + { + switch ($errno) { + case E_ERROR: + $log_message = self::getMessage($errno, $errstr, $errfile, $errline); + if (DUPX_Bootstrap::getInstance()->log($log_message) === false) { + $log_message = "Can\'t wrinte logfile\n\n" . $log_message; + } + die('
' . htmlspecialchars($log_message) . '
'); + break; + case E_NOTICE: + case E_WARNING: + default: + $log_message = self::getMessage($errno, $errstr, $errfile, $errline); + DUPX_Bootstrap::getInstance()->log($log_message); + break; + } + } + private static function getMessage($errno, $errstr, $errfile, $errline) + { + $result = '[PHP ERR]'; + switch ($errno) { + case E_ERROR: + $result .= '[FATAL]'; + break; + case E_WARNING: + $result .= '[WARN]'; + break; + case E_NOTICE: + $result .= '[NOTICE]'; + break; + default: + $result .= '[ISSUE]'; + break; + } + $result .= ' MSG:'; + $result .= $errstr; + $result .= ' [CODE:' . $errno . '|FILE:' . $errfile . '|LINE:' . $errline . ']'; + return $result; + } + public static function shutdown() + { + if (($error = error_get_last())) { + LogHandler::error($error['type'], $error['message'], $error['file'], $error['line']); + } + } + } + class DUPX_CSRF + { + private static $packagHash = null; + private static $mainFolder = null; + public static $prefix = '_DUPX_CSRF'; + private static $CSRFVars = null; + public static function init($mainFolderm, $packageHash) + { + self::$mainFolder = $mainFolderm; + self::$packagHash = $packageHash; + self::$CSRFVars = null; + } + public static function setKeyVal($key, $val) + { + $CSRFVars = self::getCSRFVars(); + $CSRFVars[$key] = $val; + self::saveCSRFVars($CSRFVars); + self::$CSRFVars = null; + } + public static function getVal($key) + { + $CSRFVars = self::getCSRFVars(); + if (isset($CSRFVars[$key])) { + return $CSRFVars[$key]; + } else { + return false; + } + } + public static function generate($form = null) + { + $keyName = self::getKeyName($form); + $existingToken = self::getVal($keyName); + if (false !== $existingToken) { + $token = $existingToken; + } else { + $token = DUPX_CSRF::token() . DUPX_CSRF::fingerprint(); + } + self::setKeyVal($keyName, $token); + return $token; + } + public static function check($token, $form = null) + { + if (empty($form)) { + return false; + } + $keyName = self::getKeyName($form); + $CSRFVars = self::getCSRFVars(); + if (isset($CSRFVars[$keyName]) && $CSRFVars[$keyName] == $token) { // token OK + return true; + } + return false; + } + protected static function token() + { + $microtime = (int) (microtime(true) * 10000); + mt_srand($microtime); + $charid = strtoupper(md5(uniqid(rand(), true))); + return substr($charid, 0, 8) . substr($charid, 8, 4) . substr($charid, 12, 4) . substr($charid, 16, 4) . substr($charid, 20, 12); + } + protected static function fingerprint() + { + return strtoupper(md5(implode('|', array($_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'])))); + } + private static function getKeyName($form) + { + return DUPX_CSRF::$prefix . '_' . $form; + } + private static function getPackageHash() + { + if (is_null(self::$packagHash)) { + throw new Exception('Not init CSFR CLASS'); + } + return self::$packagHash; + } + private static function getFilePath() + { + if (is_null(self::$mainFolder)) { + throw new Exception('Not init CSFR CLASS'); + } + $dupInstallerfolderPath = self::$mainFolder; + $packageHash = self::getPackageHash(); + $fileName = 'dup-installer-csrf__' . $packageHash . '.txt'; + $filePath = $dupInstallerfolderPath . '/' . $fileName; + return $filePath; + } + private static function getCSRFVars() + { + if (is_null(self::$CSRFVars)) { + $filePath = self::getFilePath(); + if (file_exists($filePath)) { + $contents = file_get_contents($filePath); + if (empty($contents)) { + self::$CSRFVars = array(); + } else { + $CSRFobjs = json_decode($contents); + foreach ($CSRFobjs as $key => $value) { + self::$CSRFVars[$key] = $value; + } + } + } else { + self::$CSRFVars = array(); + } + } + return self::$CSRFVars; + } + private static function saveCSRFVars($CSRFVars) + { + $contents = json_encode($CSRFVars); + $filePath = self::getFilePath(); + file_put_contents($filePath, $contents); + } + } + $auto_refresh = isset($_POST['auto-fresh']) ? true : false; + DUPX_Bootstrap::phpVersionCheck(); + try { + $boot = DUPX_Bootstrap::getInstance(); + $boot_error = $boot->run(); + } catch (Exception $e) { + $boot_error = $e->getMessage(); + } + if ($boot_error == null) { + $secure_csrf_token = DUPX_CSRF::generate('secure'); + $ctrl_csrf_token = DUPX_CSRF::generate('ctrl-step1'); + DUPX_CSRF::setKeyVal('installerOrigCall', DUPX_Bootstrap::getCurrentUrl()); + DUPX_CSRF::setKeyVal('installerOrigPath', __FILE__); + DUPX_CSRF::setKeyVal('archive', $boot->archive); + DUPX_CSRF::setKeyVal('bootloader', $boot->bootloader); + DUPX_CSRF::setKeyVal('booturl', '//' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); + DUPX_CSRF::setKeyVal('bootLogFile', $boot->getBootLogFilePath()); + DUPX_CSRF::setKeyVal('package_hash', DUPX_Bootstrap::PACKAGE_HASH); + DUPX_CSRF::setKeyVal('secondaryHash', DUPX_Bootstrap::SECONDARY_PACKAGE_HASH); + } + ?> + + + + + Duplicator Installer + + + +
+ Initializing Installer. Please wait... +
+ mainInstallerURL}' />\n"; + $data = array( + 'ctrl_action' => 'ctrl-step1', + 'ctrl_csrf_token' => $ctrl_csrf_token, + 'step_action' => 'init' + ); + foreach ($data as $name => $value) { + if ('csrf_token' != $name) { + $_SESSION[$name] = $value; + } + $html .= "\n"; + } + $html .= "\n"; + $html .= ""; + echo $html; + ?> + + + + + + + +
+ + + + +
  Duplicator - Bootloader + + version:
+ » + + dup-installer-bootlog__[HASH].txt + +
+
+
+

Setup Notice

+
+ An error has occurred. In order to load the full installer please resolve the issue below. +
+
+ +
+

+

Server Settings

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ZipArchive:hasZipArchive ? 'Enabled' : 'Disabled'; ?>
Shell Unzip:hasShellExecUnzip ? 'Enabled' : 'Disabled'; ?>
Extraction Path:targetRoot; ?>
Installer Path:targetDupInstFolder; ?>
Archive Size: + Expected Size: readableByteSize($boot->archiveExpectedSize); ?>   + Actual Size: readableByteSize($boot->archiveActualSize); ?> +
Boot Log + + dup-installer-bootlog__[HASH].txt + +
+

+
+ Note: For archive.zip files either ZipArchive or Shell Exec will need to be enabled for the installer to run automatically + otherwise a manual extraction will need to be performed. In order to run the installer manually follow the instructions to + manually extract before + running the installer. +
+

+
+
+
+ + + + + /etc/crontabs/root + +# Run crond in the foreground +CMD ["crond", "-f"] \ No newline at end of file diff --git a/generate-env-secrets.sh b/generate-env-secrets.sh new file mode 100755 index 0000000..cf74048 --- /dev/null +++ b/generate-env-secrets.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Generate secure passwords and WordPress salts for .env.prod +# Usage: ./generate-env-secrets.sh [project-name] [domain] +# Example: ./generate-env-secrets.sh php-wp-ch_hair_select hair-sct.ch + +set -e + +PROJECT_NAME="${1:-php-wp-example}" +DOMAIN="${2:-example.com}" +TABLE_PREFIX="${3:-wp_}" + +# Generate random passwords +DB_ROOT_PASSWORD=$(openssl rand -base64 32) +DB_PASSWORD=$(openssl rand -base64 32) +FTP_PASSWORD=$(openssl rand -base64 24) + +# Fetch WordPress salts from official API +echo "Fetching WordPress salts from api.wordpress.org..." +SALTS=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/) + +# Extract individual salt values and escape for shell +extract_salt() { + echo "$SALTS" | grep "define('$1'" | sed "s/define('$1', *'//" | sed "s/');$//" | sed "s/\\\$/\\\\\$/g" | sed "s/\`/\\\\\`/g" +} + +AUTH_KEY=$(extract_salt "AUTH_KEY") +SECURE_AUTH_KEY=$(extract_salt "SECURE_AUTH_KEY") +LOGGED_IN_KEY=$(extract_salt "LOGGED_IN_KEY") +NONCE_KEY=$(extract_salt "NONCE_KEY") +AUTH_SALT=$(extract_salt "AUTH_SALT") +SECURE_AUTH_SALT=$(extract_salt "SECURE_AUTH_SALT") +LOGGED_IN_SALT=$(extract_salt "LOGGED_IN_SALT") +NONCE_SALT=$(extract_salt "NONCE_SALT") + +# Write .env.prod +cat > .env.prod << EOF +REVERSE_DOMAIN=${PROJECT_NAME} +DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} +DB_NAME=${PROJECT_NAME}-db_name +DB_USER=${PROJECT_NAME}-db_user +DB_PASSWORD=${DB_PASSWORD} +APPLICATION_DOMAIN_NAME=${DOMAIN} +DOCKER_IMAGE=wordpress +DOCKER_IMAGE_TAG=latest +AUTH_KEY="'${AUTH_KEY}'" +SECURE_AUTH_KEY="'${SECURE_AUTH_KEY}'" +LOGGED_IN_KEY="'${LOGGED_IN_KEY}'" +NONCE_KEY="'${NONCE_KEY}'" +AUTH_SALT="'${AUTH_SALT}'" +SECURE_AUTH_SALT="'${SECURE_AUTH_SALT}'" +LOGGED_IN_SALT="'${LOGGED_IN_SALT}'" +NONCE_SALT="'${NONCE_SALT}'" +TABLE_PREFIX=${TABLE_PREFIX} + +WP_DEBUG=1 +WP_DEBUG_LOG=1 +WP_DEBUG_DISPLAY=false +DISABLE_WP_CRON=true + +FTP_USERNAME=admin +FTP_PASSWORD=${FTP_PASSWORD} +EOF + +echo "Generated .env.prod for ${PROJECT_NAME} (${DOMAIN})" +echo "Remember to update TABLE_PREFIX if migrating existing database!"