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 = 5; switch ($task) { case '': html_body("
This script cannot run standalone. Please log into Mautic to check for updates.
"); // 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 */ $paths = []; include MAUTIC_APP_ROOT.'/config/paths.php'; // Include local config to get cache_path $localConfig = str_replace('%kernel.project_dir%', MAUTIC_ROOT, $paths['local_config']); /** @var array $parameters */ $parameters = []; include $localConfig; $localParameters = $parameters; // check for parameter overrides if (file_exists(MAUTIC_APP_ROOT.'/../config/parameters_local.php')) { /** @var array $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 bool */ 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; } /** * 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((int) 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 array $status * @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. * * @param array $status * * @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 array $status * @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 */ function copy_directories($dir, &$errorLog, $createDest = true): bool { // 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)); } } } return true; } /** * Processes the error log for each step. * * @param array $errorLog */ function process_error_log(array $errorLog): void { // 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 = " $status * * @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; } /** * @param array $state * * @return string */ function get_state_param(array $state) { return base64_encode(json_encode($state)); } /** * Send the response back to the main application. * * @param array $status */ function send_response(array $status): void { header('Content-Type: application/json; charset=utf-8'); echo json_encode($status); } /** * Wrap content in some HTML. */ function html_body(string $content): void { $html = << Upgrade Mautic
$content
HTML; echo $html; exit; }