<?php

/*
 * @copyright   2014 Mautic Contributors. All rights reserved
 * @author      Mautic
 *
 * @link        http://mautic.org
 *
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
 */

ini_set('display_errors', 'Off');
date_default_timezone_set('UTC');

// Running this script standalone is no longer supported
$standalone = 0;
$task       = getVar('task');

define('IN_CLI', 'cli' === php_sapi_name());
define('MAUTIC_ROOT', (IN_CLI || $standalone || empty($task)) ? __DIR__ : dirname(__DIR__));
define('MAUTIC_APP_ROOT', MAUTIC_ROOT.'/app');

if ($standalone || IN_CLI) {
    if (!file_exists(__DIR__.'/upgrade')) {
        mkdir(__DIR__.'/upgrade');
    }
    define('MAUTIC_UPGRADE_ROOT', __DIR__.'/upgrade');
} else {
    define('MAUTIC_UPGRADE_ROOT', __DIR__);
}

// Fail-safe PHP version check
if (file_exists(MAUTIC_UPGRADE_ROOT.'/app/release_metadata.json')) {
    $metadata = json_decode(file_get_contents(MAUTIC_UPGRADE_ROOT.'/app/release_metadata.json'), true);

    // Are we running the minimum version?
    if (version_compare(PHP_VERSION, $metadata['minimum_php_version'], 'lt')) {
        echo 'Your server does not meet the minimum PHP requirements. Mautic requires PHP version '.$metadata['minimum_php_version'].' while your server has '
            .PHP_VERSION.'. Please contact your host to update your PHP installation.'."\n";
        exit;
    }

    // Are we running a version newer than what Mautic supports?
    if (version_compare(PHP_VERSION, $metadata['maximum_php_version'], 'gt')) {
        echo 'Mautic does not support PHP version '.PHP_VERSION.' at this time. To use Mautic, you will need to downgrade to an earlier version.'
            ."\n";
        exit;
    }
}

// Get local parameters
$localParameters = get_local_config();
$cacheDir        = (isset($localParameters['cache_path'])) ? str_replace('%kernel.root_dir%', MAUTIC_APP_ROOT, $localParameters['cache_path'].'/prod') : MAUTIC_APP_ROOT.'/../var/cache/prod';
$logDir          = (isset($localParameters['log_path'])) ? str_replace('%kernel.root_dir%', MAUTIC_APP_ROOT, $localParameters['log_path'].'/prod') : MAUTIC_APP_ROOT.'/../var/logs';

define('MAUTIC_CACHE_DIR', $cacheDir);
define('MAUTIC_UPGRADE_ERROR_LOG', $logDir.'/upgrade_errors.php');

/*
 * Updating to 2.8.1: Check to see if we have a mautic_session_name
 * and use that to populate the actual session name that will be
 * generated after a successful update.
 */
if (isset($_COOKIE['mautic_session_name'])) {
    $sessionValue = $_COOKIE[$_COOKIE['mautic_session_name']];

    include MAUTIC_APP_ROOT.'/config/paths.php';
    $localConfigPath = str_replace('%kernel.root_dir%', MAUTIC_APP_ROOT, $paths['local_config']);

    $newSessionName = md5(md5($localConfigPath).$localParameters['secret_key']);

    setcookie($newSessionName, $sessionValue, 0, '/', '', false, true);

    unset($_COOKIE['mautic_session_name']);
    setcookie('mautic_session_name', null, -1);
}

// Fetch the update state out of the request if applicable
$state = json_decode(base64_decode(getVar('updateState', 'W10=')), true);

// Prime the state if it's empty
if (empty($state)) {
    $state['pluginComplete'] = false;
    $state['bundleComplete'] = false;
    $state['cacheComplete']  = false;
    $state['coreComplete']   = false;
    $state['vendorComplete'] = false;
}
$status = ['complete' => false, 'error' => false, 'updateState' => $state, 'stepStatus' => 'In Progress'];

if (IN_CLI) {
    echo "Upgrading through upgrade.php using the CLI is no longer supported. Please use 'php bin/console mautic:update:find' instead. \n";
    exit(1);
}

// Web request upgrade
$request         = explode('?', $_SERVER['REQUEST_URI'])[0];
$url             = "//{$_SERVER['HTTP_HOST']}{$request}";
$isSSL           = (!empty($_SERVER['HTTPS']) && 'off' != $_SERVER['HTTPS']);
$cookie_path     = (isset($localParameters['cookie_path'])) ? $localParameters['cookie_path'] : '/';
$cookie_domain   = (isset($localParameters['cookie_domain'])) ? $localParameters['cookie_domain'] : '';
$cookie_secure   = (isset($localParameters['cookie_secure'])) ? $localParameters['cookie_secure'] : $isSSL;
$cookie_httponly = (isset($localParameters['cookie_httponly'])) ? $localParameters['cookie_httponly'] : false;

setcookie('mautic_update', $task, time() + 300, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly);
$query    = '';
$maxCount = (!empty($standalone)) ? 25 : 5;

switch ($task) {
    case '':
        html_body("<div class='well text-center'>This script cannot run standalone. Please log into Mautic to check for updates.</div>");

        // no break
    case 'moveBundles':
        $status = move_mautic_bundles($status, $maxCount);
        if (empty($status['complete'])) {
            if (!isset($state['refresh_count'])) {
                $state['refresh_count'] = 1;
            }
            $nextTask = 'moveBundles';
            $query    = 'count='.$state['refresh_count'].'&';
            ++$state['refresh_count'];
        } else {
            $nextTask = 'moveCore';
            unset($state['refresh_count']);
        }
        break;

    case 'moveCore':
        $status   = move_mautic_core($status);
        $nextTask = 'moveVendors';
        break;

    case 'moveVendors':
        $status   = move_mautic_vendors($status, $maxCount);
        $nextTask = (!empty($status['complete'])) ? 'clearCache' : 'moveVendors';

        if (empty($status['complete'])) {
            if (!isset($state['refresh_count'])) {
                $state['refresh_count'] = 1;
            }
            $nextTask = 'moveVendors';
            $query    = 'count='.$state['refresh_count'].'&';
            ++$state['refresh_count'];
        } else {
            $nextTask = 'clearCache';
            unset($state['refresh_count']);
        }
        break;

    case 'clearCache':
        clear_mautic_cache();
        $nextTask = 'finish';
        break;

    case 'finish':
        $status['complete']                     = true;
        $status['stepStatus']                   = 'Success';
        $status['nextStep']                     = 'Processing Database Updates';
        $status['nextStepStatus']               = 'In Progress';
        $status['updateState']['cacheComplete'] = true;

        break;

    default:
        $status['error']      = true;
        $status['message']    = 'Invalid task';
        $status['stepStatus'] = 'Failed';
        break;
}

// Request through Mautic's UI
$status['updateState'] = get_state_param($status['updateState']);

send_response($status);

/**
 * Get local parameters.
 *
 * @return mixed
 */
function get_local_config()
{
    static $parameters;

    if (null === $parameters) {
        // Used in paths.php
        $root = MAUTIC_APP_ROOT;

        /** @var array $paths */
        include MAUTIC_APP_ROOT.'/config/paths.php';

        // Include local config to get cache_path
        $localConfig = str_replace('%kernel.root_dir%', MAUTIC_APP_ROOT, $paths['local_config']);

        /** @var array $parameters */
        include $localConfig;

        $localParameters = $parameters;

        //check for parameter overrides
        if (file_exists(MAUTIC_APP_ROOT.'/config/parameters_local.php')) {
            /** @var $parameters */
            include MAUTIC_APP_ROOT.'/config/parameters_local.php';
            $localParameters = array_merge($localParameters, $parameters);
        }

        foreach ($localParameters as $k => &$v) {
            if (!empty($v) && is_string($v) && preg_match('/getenv\((.*?)\)/', $v, $match)) {
                $v = (string) getenv($match[1]);
            }
        }

        $parameters = $localParameters;
    }

    return $parameters;
}

/**
 * Clears the application cache.
 *
 * Since this script is being executed via web requests and standalone from the Mautic application, we don't have access to Symfony's
 * CLI suite.  So we'll go with Option B in this instance and just nuke the entire production cache and let Symfony rebuild it on the next
 * application cycle.
 *
 * @return array
 */
function clear_mautic_cache()
{
    if (!recursive_remove_directory(MAUTIC_CACHE_DIR)) {
        process_error_log(['Could not remove the application cache.  You will need to manually delete '.MAUTIC_CACHE_DIR.'.']);

        return false;
    }

    // Follow the same pattern as the console command and flush opcache/apc as appropriate.
    if (function_exists('opcache_reset')) {
        opcache_reset();
    }

    if (function_exists('apcu_clear_cache')) {
        apcu_clear_cache();
    }

    return true;
}

/**
 * @param $command
 *
 * @return array
 *
 * @throws Exception
 */
function run_symfony_command($command, array $args)
{
    static $application;

    require_once MAUTIC_APP_ROOT.'/autoload.php';
    require_once MAUTIC_APP_ROOT.'/AppKernel.php';

    $args = array_merge(
        ['console', $command],
        $args
    );

    if (null == $application) {
        $kernel      = new \AppKernel('prod', true);
        $application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel);
        $application->setAutoExit(false);
    }

    $input    = new \Symfony\Component\Console\Input\ArgvInput($args);
    $output   = new \Symfony\Component\Console\Output\NullOutput();
    $exitCode = $application->run($input, $output);

    unset($input, $output);

    return 0 === $exitCode;
}

/**
 * Copy a folder.
 *
 * This function is based on \Joomla\Filesystem\Folder:copy()
 *
 * @param string $src  The path to the source folder
 * @param string $dest The path to the destination folder
 *
 * @return array|string|bool True on success, a single error message on a "boot" fail, or an array of errors from the recursive operation
 */
function copy_directory($src, $dest)
{
    @set_time_limit(ini_get('max_execution_time'));
    $errorLog = [];

    // Eliminate trailing directory separators, if any
    $src  = rtrim($src, DIRECTORY_SEPARATOR);
    $dest = rtrim($dest, DIRECTORY_SEPARATOR);

    // Make sure the destination exists
    if (!is_dir($dest)) {
        if (!@mkdir($dest, 0777, true)) {
            return sprintf(
                'Could not move files from %s to production since the folder could not be created.',
                str_replace(MAUTIC_UPGRADE_ROOT, '', $src)
            );
        }
    }

    if (!($dh = @opendir($src))) {
        return sprintf('Could not read directory %s to move files.', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
    }

    // Walk through the directory copying files and recursing into folders.
    while (false !== ($file = readdir($dh))) {
        $sfid = $src.'/'.$file;
        $dfid = $dest.'/'.$file;

        switch (filetype($sfid)) {
            case 'dir':
                if ('.' != $file && '..' != $file) {
                    $ret = copy_directory($sfid, $dfid);

                    if (true !== $ret) {
                        if (is_array($ret)) {
                            $errorLog += $ret;
                        } else {
                            $errorLog[] = $ret;
                        }
                    }
                }
                break;

            case 'file':
                if (!@rename($sfid, $dfid)) {
                    $errorLog[] = sprintf('Could not move file %s to production.', str_replace(MAUTIC_UPGRADE_ROOT, '', $sfid));
                }
                break;
        }
    }

    if (!empty($errorLog)) {
        return $errorLog;
    }

    return true;
}

/**
 * Fetches a request variable and returns the sanitized version of it.
 *
 * @param string $name
 * @param string $default
 * @param int    $filter
 *
 * @return mixed|string
 */
function getVar($name, $default = '', $filter = FILTER_SANITIZE_STRING)
{
    if (isset($_REQUEST[$name])) {
        return filter_var($_REQUEST[$name], $filter);
    }

    return $default;
}

/**
 * Moves the Mautic bundles from the upgrade directory to production.
 *
 * A typical update package will only include changed files in the bundles.  However, in this script we will assume that all of
 * the bundle resources are included here and recursively iterate over the bundles in batches to update the filesystem.
 *
 * @param int $maxCount
 *
 * @return array
 */
function move_mautic_bundles(array $status, $maxCount = 5)
{
    $errorLog = [];

    // First, we will move any addon bundles into position
    if (is_dir(MAUTIC_UPGRADE_ROOT.'/plugins') && !$status['updateState']['pluginComplete']) {
        $iterator = new DirectoryIterator(MAUTIC_UPGRADE_ROOT.'/plugins');

        // Sanity check, make sure there are actually directories here to process
        $dirs = glob(MAUTIC_UPGRADE_ROOT.'/plugins/*', GLOB_ONLYDIR);

        if (count($dirs)) {
            /** @var DirectoryIterator $directory */
            foreach ($iterator as $directory) {
                // Sanity checks
                if (!$directory->isDot() && $directory->isDir()) {
                    $src  = $directory->getPath().'/'.$directory->getFilename();
                    $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

                    $result = copy_directory($src, $dest);

                    if (true !== $result) {
                        if (is_array($result)) {
                            $errorLog += $result;
                        } else {
                            $errorLog[] = $result;
                        }
                    }

                    $deleteDir = recursive_remove_directory($src);

                    if (!$deleteDir) {
                        $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
                    }
                }
            }
        }

        // At this point, there shouldn't be any plugins remaining; nuke the folder
        $deleteDir = recursive_remove_directory(MAUTIC_UPGRADE_ROOT.'/plugins');

        if (!$deleteDir) {
            $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', '/plugins');
        }

        process_error_log($errorLog);

        $status['updateState']['pluginComplete'] = true;

        if (-1 != $maxCount) {
            // Finished with plugins, get a response back to the app so we can iterate to the next part
            return $status;
        }
    }

    // Now we move the main app bundles into production
    if (is_dir(MAUTIC_UPGRADE_ROOT.'/app/bundles') && !$status['updateState']['bundleComplete']) {
        // Initialize the bundle state if it isn't
        if (!isset($status['updateState']['completedBundles'])) {
            $status['updateState']['completedBundles'] = [];
        }

        $completed = true;
        $iterator  = new DirectoryIterator(MAUTIC_UPGRADE_ROOT.'/app/bundles');

        // Sanity check, make sure there are actually directories here to process
        $dirs = glob(MAUTIC_UPGRADE_ROOT.'/app/bundles/*', GLOB_ONLYDIR);

        if (count($dirs)) {
            $count = 0;

            /** @var DirectoryIterator $directory */
            foreach ($iterator as $directory) {
                // Exit the loop if the count has reached 5
                if (-1 != $maxCount && $count === $maxCount) {
                    $completed = false;
                    break;
                }

                // Sanity checks
                if (!$directory->isDot() && $directory->isDir()) {
                    // Don't process this bundle if we've already tried it
                    if (isset($status['updateState']['completedBundles'][$directory->getFilename()])) {
                        continue;
                    }

                    $src  = $directory->getPath().'/'.$directory->getFilename();
                    $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

                    $result = copy_directory($src, $dest);

                    if (true !== $result) {
                        if (is_array($result)) {
                            $errorLog += $result;
                        } else {
                            $errorLog[] = $result;
                        }
                    }

                    $deleteDir = recursive_remove_directory($src);

                    if (!$deleteDir) {
                        $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
                    }

                    $status['updateState']['completedBundles'][$directory->getFilename()] = true;
                    ++$count;
                }
            }
        }

        if ($completed) {
            $status['updateState']['bundleComplete'] = true;

            // At this point, there shouldn't be any bundles remaining; nuke the folder
            $deleteDir = recursive_remove_directory(MAUTIC_UPGRADE_ROOT.'/app/bundles');

            if (!$deleteDir) {
                $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', '/app/bundles');
            }
        }

        process_error_log($errorLog);

        // If we haven't finished the bundles yet, throw a response back to repeat the step
        if (!$status['updateState']['bundleComplete']) {
            return $status;
        }
    }

    // To get here, all of the bundle updates must have been processed (or there are literally none).  Step complete.
    $status['complete'] = true;

    return $status;
}

/**
 * Moves the Mautic core files that are not part of bundles or vendors into production.
 *
 * The "core" files are broken into groups for purposes of the update script: bundles, vendor, and everything else.  This step
 * will take care of the everything else.
 *
 * @return array
 */
function move_mautic_core(array $status)
{
    $errorLog = [];

    // Multilevel directories
    $nestedDirectories = [
        '/media',
        '/themes',
        '/translations',
        '/app/middlewares',
    ];

    foreach ($nestedDirectories as $dir) {
        if (is_dir(MAUTIC_UPGRADE_ROOT.$dir)) {
            copy_directories($dir, $errorLog);

            // At this point, we can remove the media directory
            $deleteDir = recursive_remove_directory(MAUTIC_UPGRADE_ROOT.$dir);

            if (!$deleteDir) {
                $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', $dir);
            }
        }
    }

    // Single level directories with files only
    $fileOnlyDirectories = [
        '/app/config',
        '/app/migrations',
        '/app',
        '/bin',
    ];

    foreach ($fileOnlyDirectories as $dir) {
        if (copy_files($dir, $errorLog)) {
            // At this point, we can remove the config directory
            $deleteDir = recursive_remove_directory(MAUTIC_UPGRADE_ROOT.$dir);

            if (!$deleteDir) {
                $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', $dir);
            }
        }
    }

    // Now move any root level files
    $iterator = new FilesystemIterator(MAUTIC_UPGRADE_ROOT);

    /** @var FilesystemIterator $file */
    foreach ($iterator as $file) {
        // Sanity checks
        if ($file->isFile() && !in_array($file->getFilename(), ['deleted_files.txt', 'critical_migrations.txt', 'upgrade.php'])) {
            $src  = $file->getPath().'/'.$file->getFilename();
            $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

            if (!@rename($src, $dest)) {
                $errorLog[] = sprintf('Could not move file %s to production.', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
            }
        }
    }

    process_error_log($errorLog);

    // In this step, we'll also go ahead and remove deleted files, return the results from that
    return remove_mautic_deleted_files($status);
}

/**
 * Moves the Mautic dependencies from the upgrade directory to production.
 *
 * Since the /vendor folder is not stored under version control, we cannot accurately track changes in third party dependencies
 * between releases.  Therefore, this step will recursively iterate over the vendors in batches to remove each package completely
 * and replace it with the new version.
 *
 * @param int $maxCount
 *
 * @return array
 */
function move_mautic_vendors(array $status, $maxCount = 5)
{
    $errorLog = [];

    // If there isn't even a vendor directory, just skip this step
    if (!is_dir(MAUTIC_UPGRADE_ROOT.'/vendor')) {
        $status['complete']                      = true;
        $status['stepStatus']                    = 'Success';
        $status['nextStep']                      = 'Clearing Application Cache';
        $status['nextStepStatus']                = 'In Progress';
        $status['updateState']['vendorComplete'] = true;

        return $status;
    }

    // Initialize the vendor state if it isn't
    if (!isset($status['updateState']['completedVendors'])) {
        $status['updateState']['completedVendors'] = [];
    }

    // Symfony is the largest of our vendors, we will process it first
    if (is_dir(MAUTIC_UPGRADE_ROOT.'/vendor/symfony') && !isset($status['updateState']['completedVendors']['symfony'])) {
        // Initialize the Symfony state if it isn't, this step will recurse
        if (!isset($status['updateState']['completedSymfony'])) {
            $status['updateState']['completedSymfony'] = [];
        }

        $completed = true;
        $iterator  = new DirectoryIterator(MAUTIC_UPGRADE_ROOT.'/vendor/symfony');

        // Sanity check, make sure there are actually directories here to process
        $dirs = glob(MAUTIC_UPGRADE_ROOT.'/vendor/symfony/*', GLOB_ONLYDIR);

        if (count($dirs)) {
            $count = 0;

            /** @var DirectoryIterator $directory */
            foreach ($iterator as $directory) {
                // Exit the loop if the count has reached 5
                if (-1 != $maxCount && $count === $maxCount) {
                    $completed = false;
                    break;
                }

                // Sanity checks
                if (!$directory->isDot() && $directory->isDir()) {
                    // Don't process this directory if we've already tried it
                    if (isset($status['updateState']['completedSymfony'][$directory->getFilename()])) {
                        continue;
                    }

                    $src  = $directory->getPath().'/'.$directory->getFilename();
                    $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

                    // We'll need to completely remove the existing vendor first
                    recursive_remove_directory($dest);

                    $result = copy_directory($src, $dest);

                    if (true !== $result) {
                        if (is_array($result)) {
                            $errorLog += $result;
                        } else {
                            $errorLog[] = $result;
                        }
                    }

                    $deleteDir = recursive_remove_directory($src);

                    if (!$deleteDir) {
                        $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
                    }

                    $status['updateState']['completedSymfony'][$directory->getFilename()] = true;
                    ++$count;
                }
            }
        }

        if ($completed) {
            $status['updateState']['completedVendors']['symfony'] = true;

            // At this point, there shouldn't be any Symfony code remaining; nuke the folder
            $deleteDir = recursive_remove_directory(MAUTIC_UPGRADE_ROOT.'/vendor/symfony');

            if (!$deleteDir) {
                $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', '/vendor/symfony');
            }
        }

        process_error_log($errorLog);

        // If we haven't finished Symfony yet, throw a response back to repeat the step
        if (!isset($status['updateState']['completedVendors']['symfony'])) {
            return $status;
        }
    }

    // Once we've gotten here, we can safely iterate through the rest of the vendor directory; the rest of the contents are rather small in size
    $completed = true;
    $iterator  = new DirectoryIterator(MAUTIC_UPGRADE_ROOT.'/vendor');

    // Sanity check, make sure there are actually directories here to process
    $dirs = glob(MAUTIC_UPGRADE_ROOT.'/vendor/*', GLOB_ONLYDIR);

    if (count($dirs)) {
        $count = 0;

        /** @var DirectoryIterator $directory */
        foreach ($iterator as $directory) {
            // Exit the loop if the count has reached 5
            if (-1 != $maxCount && $count === $maxCount) {
                $completed = false;
                break;
            }

            // Sanity checks
            if (!$directory->isDot() && $directory->isDir()) {
                // Don't process this directory if we've already tried it
                if (isset($status['updateState']['completedVendors'][$directory->getFilename()])) {
                    continue;
                }

                $src  = $directory->getPath().'/'.$directory->getFilename();
                $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

                // We'll need to completely remove the existing vendor first
                recursive_remove_directory($dest);

                $result = copy_directory($src, $dest);

                if (true !== $result) {
                    if (is_array($result)) {
                        $errorLog += $result;
                    } else {
                        $errorLog[] = $result;
                    }
                }

                $deleteDir = recursive_remove_directory($src);

                if (!$deleteDir) {
                    $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
                }

                $status['updateState']['completedVendors'][$directory->getFilename()] = true;
                ++$count;
            }
        }
    }

    if ($completed) {
        $status['updateState']['vendorComplete'] = true;

        // Move the autoload.php file over now
        if (!@rename(MAUTIC_UPGRADE_ROOT.'/vendor/autoload.php', MAUTIC_ROOT.'/vendor/autoload.php')) {
            $errorLog[] = 'Could not move file /vendor/autoload.php to production.';
        }

        // At this point, there shouldn't be any vendors remaining; nuke the folder
        $deleteDir = recursive_remove_directory(MAUTIC_UPGRADE_ROOT.'/vendor');

        if (!$deleteDir) {
            $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', '/vendor');
        }
    }

    process_error_log($errorLog);

    // If we haven't finished the vendors yet, throw a response back to repeat the step
    if (!$status['updateState']['vendorComplete']) {
        return $status;
    }

    // Once we get here, we have finished the moving files step; notifiy Mautic of this
    $status['complete']                      = true;
    $status['stepStatus']                    = 'Success';
    $status['nextStep']                      = 'Clearing Application Cache';
    $status['nextStepStatus']                = 'In Progress';
    $status['updateState']['vendorComplete'] = true;

    return $status;
}

/**
 * Copy files from the directory.
 *
 * @param string $dir
 * @param array  &$errorLog
 *
 * @return bool
 */
function copy_files($dir, &$errorLog)
{
    if (is_dir(MAUTIC_UPGRADE_ROOT.$dir)) {
        $iterator = new FilesystemIterator(MAUTIC_UPGRADE_ROOT.$dir);

        /** @var FilesystemIterator $file */
        foreach ($iterator as $file) {
            // Sanity checks
            if ($file->isFile()) {
                $src  = $file->getPath().'/'.$file->getFilename();
                $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

                if (!@rename($src, $dest)) {
                    $errorLog[] = sprintf('Could not move file %s to production.', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
                }
            }
        }

        return true;
    }

    return false;
}

/**
 * Copy directories.
 *
 * @param string $dir
 * @param array  &$errorLog
 * @param bool   $createDest
 *
 * @return bool|void
 */
function copy_directories($dir, &$errorLog, $createDest = true)
{
    // Ensure the destination directory exists
    $exists = file_exists(MAUTIC_ROOT.$dir);
    if ($createDest && !$exists) {
        mkdir(MAUTIC_ROOT.$dir, 0755, true);
    } elseif (!$exists) {
        $errorLog[] = sprintf('%s does not exist.', MAUTIC_ROOT.$dir);

        return false;
    }

    // Copy root level files first
    copy_files($dir, $errorLog);

    $iterator = new DirectoryIterator(MAUTIC_UPGRADE_ROOT.$dir);

    /** @var DirectoryIterator $directory */
    foreach ($iterator as $directory) {
        // Sanity checks
        if (!$directory->isDot() && $directory->isDir()) {
            $src  = $directory->getPath().'/'.$directory->getFilename();
            $dest = str_replace(MAUTIC_UPGRADE_ROOT, MAUTIC_ROOT, $src);

            $result = copy_directory($src, $dest);

            if (true !== $result) {
                if (is_array($result)) {
                    $errorLog += $result;
                } else {
                    $errorLog[] = $result;
                }
            }

            $deleteDir = recursive_remove_directory($src);

            if (!$deleteDir) {
                $errorLog[] = sprintf('Failed to remove the upgrade directory %s folder', str_replace(MAUTIC_UPGRADE_ROOT, '', $src));
            }
        }
    }
}

/**
 * Processes the error log for each step.
 */
function process_error_log(array $errorLog)
{
    // If there were any errors, add them to the error log
    if (count($errorLog)) {
        // Check if the error log exists first
        if (file_exists(MAUTIC_UPGRADE_ERROR_LOG)) {
            $errors = file_get_contents(MAUTIC_UPGRADE_ERROR_LOG);
        } else {
            $errors = "<?php die('no access'); \n\n";
        }

        $errors .= implode(PHP_EOL, $errorLog)."\n";

        @file_put_contents(MAUTIC_UPGRADE_ERROR_LOG, $errors);
    }
}

/**
 * Tries to recursively delete a directory.
 *
 * This code is based on the recursive_remove_directory function used by Akeeba Restore
 *
 * @param string $directory
 *
 * @return bool
 */
function recursive_remove_directory($directory)
{
    // if the path has a slash at the end we remove it here
    if ('/' == substr($directory, -1)) {
        $directory = substr($directory, 0, -1);
    }

    // if the path is not valid or is not a directory ...
    if (!file_exists($directory)) {
        return true;
    } elseif (!is_dir($directory)) {
        return false;
    // ... if the path is not readable
    } elseif (!is_readable($directory)) {
        // ... we return false and exit the function
        return false;
    // ... else if the path is readable
    } else {
        // we open the directory
        $handle = opendir($directory);

        // and scan through the items inside
        while (false !== ($item = readdir($handle))) {
            // if the filepointer is not the current directory
            // or the parent directory
            if ('.' != $item && '..' != $item) {
                // we build the new path to delete
                $path = $directory.'/'.$item;
                // if the new path is a directory
                if (is_dir($path)) {
                    // we call this function with the new path
                    recursive_remove_directory($path);
                // if the new path is a file
                } else {
                    // we remove the file
                    @unlink($path);
                }
            }
        }

        // close the directory
        closedir($handle);

        // try to delete the now empty directory
        if (!@rmdir($directory)) {
            // return false if not possible
            return false;
        }

        // return success
        return true;
    }
}

/**
 * Removes deleted files from the system.
 *
 * While packaging updates, the script will generate a list of deleted files in comparison to the previous version.  In this step,
 * we will process that list to remove files which are no longer included in the application.
 *
 * @return array
 */
function remove_mautic_deleted_files(array $status)
{
    $errorLog = [];

    // Make sure we have a deleted_files list otherwise we can't process this step
    if (file_exists(MAUTIC_UPGRADE_ROOT.'/deleted_files.txt')) {
        $deletedFiles = json_decode(file_get_contents(MAUTIC_UPGRADE_ROOT.'/deleted_files.txt'), true);

        foreach ($deletedFiles as $file) {
            $path = MAUTIC_ROOT.'/'.$file;

            // If it doesn't exist, don't even bother
            if (file_exists($path)) {
                // Try setting the permissions to 777 just to make sure we can get rid of the file
                @chmod($path, 0777);

                if (!@unlink($path)) {
                    // Failed to delete, reset the permissions to 644 for safety
                    @chmod($path, 0644);

                    $errorLog[] = sprintf(
                        'Failed removing the file at %s from the production path.  As this is a deleted file, you can manually remove this file.',
                        $file
                    );
                } else {
                    // Check to see if directory is now empty and if so, delete it
                    $dirpath = dirname($path);
                    if (file_exists($dirpath) && !glob($dirpath.'/*')) {
                        @chmod($dirpath, 0777);
                        if (!@unlink($dirpath)) {
                            // Failed to delete, reset the permissions to 0755 for safety
                            @chmod($dirpath, 0755);
                        }
                    }
                }
            }
        }
    } else {
        $errorLog[] = 'The file containing the list of deleted files was not found, could not process the deleted file list.';
    }

    process_error_log($errorLog);

    $status['complete']                    = true;
    $status['updateState']['coreComplete'] = true;

    return $status;
}

/**
 * @return string
 */
function get_state_param(array $state)
{
    return base64_encode(json_encode($state));
}

/**
 * Send the response back to the main application.
 */
function send_response(array $status)
{
    header('Content-Type: application/json; charset=utf-8');

    echo json_encode($status);
}

/**
 * Crap means of not having issues with.
 */
function make_request($url, $method = 'GET', $data = null)
{
    $method  = strtoupper($method);
    $ch      = curl_init();
    $timeout = 15;
    if ($data && 'POST' == $method) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    }
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
    curl_setopt($ch, CURLOPT_CAINFO, MAUTIC_ROOT.'/vendor/joomla/http/src/Transport/cacert.pem');
    $data = curl_exec($ch);
    curl_close($ch);

    return $data;
}

/**
 * Wrap content in some HTML.
 *
 * @param $content
 */
function html_body($content)
{
    $html = <<<HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Upgrade Mautic</title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
  </head>
  <body>
    <div class="container" style="padding: 25px;">
        $content
    </div>
  </body>
</html>
HTML;

    echo $html;

    exit;
}
