<?php
// Note: much of the code in this file is intentionally independent and possibly repeated. This is to reduce
// dependencies which may cause problems when upgrading.
/**
* Class XFUpgrader
*
* Provides the primary logic for handling the upgrade.
*/
class XFUpgrader
{
// set this to true to make debugging the upgrader simpler
const DEBUGGING = false;
protected $upgradeKey;
/**
* @var ZipArchive|null
*/
protected $zip;
protected $zipVersionId;
public function canAttempt(&$error = null)
{
if (!class_exists('ZipArchive'))
{
$error = 'ZipArchive class does not exist.';
return false;
}
$config = \XF::app()->config();
if (!$config['enableOneClickUpgrade'])
{
$error = 'One-click upgrades have not been enabled.';
return false;
}
if (!self::DEBUGGING)
{
if ($config['development']['enabled'])
{
$error = 'This is a development install (via dev mode).';
return false;
}
$xfHashes = \XF::getAddOnDirectory() . '/XF/hashes.json';
if (!file_exists($xfHashes))
{
$error = 'This is a development install (via missing hashes).';
return false;
}
}
if (!is_writable(__FILE__))
{
$error = 'The files are not writable.';
return false;
}
$isWindows = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN');
if (!$isWindows)
{
// Only allow the upgrade to run if we're not likely to cause mixed file ownership.
// This is possibly over restrictive. (If relaxed in the future, we should special case
// to prevent using root unless the files are owned by root.)
$uid = function_exists('posix_getuid') ? posix_getuid() : fileowner(\XF\Util\File::getTempFile());
if ($uid !== fileowner(__FILE__))
{
$error = 'The files are owned by a different user than the upgrade is running as.';
return false;
}
}
return true;
}
public function setUpgradeKey($upgradeKey, &$error = null, $fullVerification = true)
{
$error = null;
if (!class_exists('ZipArchive'))
{
$error = 'ZipArchive class does not exist. Please upgrade manually.';
return false;
}
$upgradeKey = preg_replace('#[^a-z0-9_\-]#i', '', $upgradeKey);
if (!$upgradeKey)
{
return false;
}
$upgradeFile = $this->getUpgradeFile($upgradeKey);
if (!file_exists($upgradeFile))
{
return false;
}
$zip = new ZipArchive();
if ($zip->open($upgradeFile) !== true)
{
return false;
}
if (!$this->validateZipFile($zip, $error, $zipVersionId, $fullVerification))
{
return false;
}
$this->upgradeKey = $upgradeKey;
$this->zip = $zip;
$this->zipVersionId = $zipVersionId;
return true;
}
public function validateFile($file, &$error = null, $fullVerification = true)
{
$zip = new ZipArchive();
if ($zip->open($file) !== true)
{
$error = 'Error opening file.';
return false;
}
return $this->validateZipFile($zip, $error, $zipVersionid, $fullVerification);
}
protected function validateZipFile(ZipArchive $zip, &$error = null, &$zipVersionId = null, $fullVerification = true)
{
$xfClass = $zip->getFromName('upload/src/XF.php');
if (!$xfClass)
{
$error = 'The zip file does not appear to be a valid XenForo release.';
return false;
}
if (!self::DEBUGGING)
{
if ($zip->locateName('upload/src/addons/XF/hashes.json') === false)
{
$error = 'The zip file does not appear to be a valid XenForo release.';
return false;
}
}
if (!preg_match('#public\s+static\s+\$versionId\s*=\s*(\d+)\s*;#', $xfClass, $match))
{
$error = 'The zip file does not appear to contain the expected contents.';
return false;
}
$zipVersionId = intval($match[1]);
if ($zipVersionId < \XF::$versionId)
{
$error = 'The zip file contains a version older than the version currently in use. Cannot continue.';
return false;
}
if ($zipVersionId >= 3000000)
{
// assume that 3.0 won't be supported with this unless we decide otherwise
$error = 'The zip file contains a version that cannot be upgraded to automatically.';
return false;
}
if ($fullVerification)
{
$requirements = $zip->getFromName('upload/src/XF/Install/_upgrader/requirements.json');
if ($requirements)
{
$reqJson = json_decode($requirements, true);
if (!$this->checkRequirements($reqJson, $errors))
{
$error = 'The following requirements were not met for this upgrade: ' . implode(' ', $errors);
return false;
}
}
}
return true;
}
public function checkRequirements(array $requirements, &$errors = [])
{
$errors = [];
foreach ($requirements AS $productKey => $requirement)
{
if (is_array($requirement))
{
list($version, $printable) = $requirement;
}
else
{
$version = $requirement;
$printable = null;
}
$enabled = false;
$versionValid = false;
if (strpos($productKey, 'php-ext/') === 0)
{
$parts = explode('/', $productKey, 2);
$enabled = extension_loaded($parts[1]);
if ($version === '*')
{
$versionValid = true;
}
else
{
$versionValid = version_compare(phpversion($parts[1]), $version, '>=');
}
if ($printable === null)
{
$printable = "PHP extension $parts[1]";
if ($version !== '*')
{
$printable .= " $version+";
}
}
}
else if ($productKey === 'php')
{
$enabled = true;
$versionValid = version_compare(phpversion(), $version, '>=');
if ($printable === null)
{
$printable = "PHP $version+";
}
}
else if ($productKey === 'mysql')
{
$mySqlVersion = \XF::db()->getServerVersion();
if ($mySqlVersion)
{
$enabled = true;
$versionValid = version_compare(strtolower($mySqlVersion), $version, '>=');
}
if ($printable === null)
{
$printable = "MySQL $version+";
}
}
else
{
throw new \LogicException("Unknown requirement check $productKey");
}
// TODO: expand to PHP function checks?
if (!$enabled || !$versionValid)
{
$errors[] = "$printable is required.";
}
}
return $errors ? false : true;
}
public function getHashes()
{
$file = \XF::getAddOnDirectory() . '/XF/hashes.json';
if (!file_exists($file))
{
return null;
}
return json_decode(file_get_contents($file), true);
}
public function compareHashes()
{
$existingHashes = $this->getHashes();
if ($existingHashes)
{
return $this->getExtractor()->compareHashes($existingHashes);
}
else
{
return null;
}
}
public function checkWritable(array $changeset = null, &$failures = [])
{
return $this->getExtractor()->checkWritable($changeset, $failures);
}
public function copyFiles(XFUpgraderExtractAction $action, &$error)
{
return $this->getExtractor()->copyFiles($action, $error);
}
/**
* @return XFUpgraderExtractor
*/
protected function getExtractor()
{
if (!$this->zip)
{
throw new \LogicException("Zip not opened yet");
}
return new XFUpgraderExtractor($this->zip);
}
/**
* @param array|null $hashChanges
* @param int $start
* @param int|null $maxTime
*
* @return XFUpgraderExtractAction
*/
public function getExtractAction(array $hashChanges = null, $start = 0, $maxTime = null)
{
return new XFUpgraderExtractAction($hashChanges, $start, $maxTime);
}
public function cleanUp()
{
if (!$this->upgradeKey)
{
return;
}
if ($this->zip)
{
$this->zip->close();
$this->zip = null;
}
if (!self::DEBUGGING)
{
@unlink($this->getUpgradeFile());
}
}
protected function getUpgradeFile($upgradeKey = null)
{
$upgradeKey = $upgradeKey ?: $this->upgradeKey;
$dir = \XF\Util\File::getTempDir();
return "$dir/upgrade-{$upgradeKey}.zip";
}
public function getZipVersionId()
{
return $this->zipVersionId;
}
}
class XFUpgraderExtractAction
{
protected $hashChanges = null;
protected $matchFiles = [];
protected $skipFiles = [];
protected $start = 0;
protected $maxTime = null;
protected $forceUpdateHashFile = false;
/**
* @var \Closure|null
*/
protected $tickHandler = null;
protected $matchRegex = null;
protected $skipRegex = null;
public function __construct(array $hashChanges = null, $start = 0, $maxTime = null)
{
$this->hashChanges = $hashChanges;
$this->start = max(0, intval($start));
$this->maxTime = $maxTime;
}
public function setMatchFiles(array $files)
{
$this->matchFiles = $files;
$this->matchRegex = $this->getFileRegex($files);
return $this;
}
public function setSkipFiles(array $files)
{
$this->skipFiles = $files;
$this->skipRegex = $this->getFileRegex($files);
return $this;
}
protected function getFileRegex(array $files)
{
if (!$files)
{
return null;
}
$regexParts = [];
foreach ($files AS $file)
{
$part = preg_quote($file, '#');
$part = str_replace('\\*', '.*', $part);
$regexParts[] = $part;
}
return '#^(' . implode('|', $regexParts) . ')$#';
}
public function setForceUpdateHashFile()
{
$this->forceUpdateHashFile = true;
return $this;
}
public function getForceUpdateHashFile()
{
return $this->forceUpdateHashFile;
}
public function setTickHandler(\Closure $tick)
{
$this->tickHandler = $tick;
}
public function getHashChanges()
{
return $this->hashChanges;
}
public function getStart()
{
return $this->start;
}
public function getMaxTime()
{
return $this->maxTime;
}
public function isFileMatched($fsFileName)
{
if (is_array($this->hashChanges) && !isset($this->hashChanges[$fsFileName]))
{
// file hasn't been updated
return false;
}
if ($this->skipRegex && preg_match($this->skipRegex, $fsFileName))
{
// file matches the skip regex
return false;
}
if ($this->matchRegex && !preg_match($this->matchRegex, $fsFileName))
{
// file does not match match regex
return false;
}
return true;
}
public function onTick($i, $totalFiles, $zipFileName, XFUpgraderExtractor $extractor)
{
if ($this->tickHandler)
{
$tick = $this->tickHandler;
$tick($zipFileName, $i, $totalFiles, $extractor, $this);
}
}
}
/**
* Class XFUpgraderExtractor
*
* Manages extracting files from the upgrade zip.
*
* Striking similarity to src/XF/Service/AddOnArchive/Extractor.php. Changes should be mirrored as necessary.
*/
class XFUpgraderExtractor
{
/**
* @var ZipArchive
*/
protected $zip;
public function __construct(ZipArchive $zip)
{
$this->zip = $zip;
}
public function compareHashes(array $existingHashes)
{
$newHashes = $this->getNewHashes();
if (!$newHashes)
{
return null;
}
$changes = [];
foreach ($newHashes AS $file => $newHash)
{
if (!isset($existingHashes[$file]))
{
$changes[$file] = 'create';
}
else if ($newHash !== $existingHashes[$file])
{
$changes[$file] = 'update';
}
}
$changes[preg_replace('#^upload/#', '', $this->getHashFileName())] = 'update';
foreach ($existingHashes AS $oldFile => $null)
{
if (!isset($newHashes[$oldFile]))
{
$changes[$oldFile] = 'delete';
}
}
return $changes;
}
public function checkWritable(array $changeset = null, &$failures = [])
{
$zip = $this->zip;
$failures = [];
for ($i = 0; $i < $zip->numFiles; $i++)
{
$zipFileName = $zip->getNameIndex($i);
$fsFileName = $this->getFsFileNameFromZipName($zipFileName);
if ($fsFileName === null)
{
continue;
}
if (is_array($changeset) && !isset($changeset[$fsFileName]))
{
// we're not changing this file
continue;
}
if (!\XF\Util\File::isWritable($this->getFinalFsFileName($fsFileName)))
{
$failures[] = $fsFileName;
}
}
return $failures ? false : true;
}
public function copyFiles(XFUpgraderExtractAction $action, &$error)
{
$zip = $this->zip;
$start = $action->getStart();
$maxTime = $action->getMaxTime();
$lastComplete = $start;
$totalFiles = $zip->numFiles;
$s = microtime(true);
for ($i = $start; $i < $totalFiles; $i++)
{
$lastComplete = $i;
$zipFileName = $zip->getNameIndex($i);
$targetWritten = $this->writeFileFromZip(
$zipFileName,
function($fsFileName) use ($action)
{
return $action->isFileMatched($fsFileName);
}
);
if (!$targetWritten)
{
$error = "Failed write to {$zipFileName}";
return false;
}
$action->onTick($zipFileName, $i, $totalFiles, $this);
if ($maxTime !== null && (microtime(true) - $s) > $maxTime)
{
break;
}
}
$complete = ($i >= $zip->numFiles);
if ($complete && $action->getForceUpdateHashFile())
{
// if we don't have a new hashes file, we need to remove the old one if it exists as it will be wrong
$hashZipFileName = $this->getHashFileName();
if ($zip->locateName($hashZipFileName) === false)
{
$fsFileName = $this->getFsFileNameFromZipName($hashZipFileName);
$finalFileName = $this->getFinalFsFileName($fsFileName);
if (file_exists($finalFileName) && !@unlink($finalFileName))
{
$error = "Failed write to {$fsFileName}";
return false;
}
}
}
return [
'status' => ($complete ? 'complete' : 'incomplete'),
'last' => $lastComplete,
'percent' => ($complete || !$zip->numFiles) ? 100 : 100 * ($lastComplete / $zip->numFiles)
];
}
protected function writeFileFromZip($zipFileName, \Closure $checkWriteNeeded = null)
{
$fsFileName = $this->getFsFileNameFromZipName($zipFileName);
if ($fsFileName === null)
{
// not a writable file - consider fine
return true;
}
$finalFileName = $this->getFinalFsFileName($fsFileName);
if ($checkWriteNeeded)
{
$isWriteNeeded = $checkWriteNeeded($fsFileName, $finalFileName, $zipFileName);
if (!$isWriteNeeded)
{
// no action required, so consider fine
return true;
}
}
$dataStream = $this->zip->getStream($zipFileName);
return @\XF\Util\File::writeFile($finalFileName, $dataStream, false);
}
protected function getNewHashes()
{
$newHashesJson = $this->zip->getFromName($this->getHashFileName());
if (!$newHashesJson)
{
return null;
}
return json_decode($newHashesJson, true);
}
protected function getFsFileNameFromZipName($fileName)
{
if (substr($fileName, -1) === '/')
{
// this is a directory we can just skip this
return null;
}
if (!preg_match("#^upload/.#", $fileName))
{
// file outside of "upload" so we can just skip this
return null;
}
return substr($fileName, 7); // remove "upload/"
}
protected function getFinalFsFileName($fileName)
{
return \XF::getRootDirectory() . \XF::$DS . $fileName;
}
protected function getHashFileName()
{
return "upload/src/addons/XF/hashes.json";
}
}
/**
* Class XFUpgraderWeb
*
* Provides the logic for triggering the upgrade via the web (with page refreshes, etc).
*/
class XFUpgraderWeb
{
/**
* @var \XF\Http\Request
*/
protected $request;
/**
* @var \XF\Template\Templater
*/
protected $templater;
/**
* @var XFUpgrader
*/
protected $upgrader;
protected $key;
protected $state = [];
public function __construct(\XF\App $app)
{
$this->request = $app->request();
$this->templater = $app->templater();
$this->upgrader = new XFUpgrader();
}
public function run()
{
$request = $this->request;
if (!$request->isPost())
{
header('Location: index.php?upgrade/');
return;
}
$key = $request->filter('key', 'string');
if (!$this->upgrader->setUpgradeKey($key, $error))
{
if ($error)
{
$this->outputError($error . ' Please upgrade manually.');
}
else
{
$this->outputError('Invalid key. Please upgrade manually.');
}
return;
}
$this->key = $key;
\XF::app()->error()->setForceShowTrace(true);
if (!$this->upgrader->canAttempt($error))
{
$this->outputError("Cannot attempt: $error Please upgrade manually.");
$this->cleanUp();
return;
}
$step = $request->filter('step', 'string');
if (!$step)
{
$step = 'init';
}
else if ($step === 'copy' || $step === 'reinit')
{
// these are old upgrader steps that selfupdate use to point to -- need to repoint to the new
// method to ensure full updates from older versions
$step = 'postselfupdate';
}
$stepMethod = 'step' . $step;
if (!method_exists($this, $stepMethod))
{
$this->outputError("Failed to find step $step. Please upgrade manually.");
return;
}
$this->state = $request->filter('state', 'json-array');
$params = $request->filter('params', 'json-array');
$result = $this->$stepMethod($params);
if (is_array($result))
{
$newStep = $step;
$newParams = $result;
}
else if (is_string($result))
{
$newStep = $result;
$newParams = [];
}
else if ($result === false)
{
// indicates we're already generated the page so stop
return;
}
else
{
throw new \LogicException("{$stepMethod} didn't return the expected data");
}
$ticks = $this->request->filter('ticks', 'uint');
$this->outputResult($newStep, $newParams, $ticks);
}
// Setup and ensure that we can continue
protected function stepInit(array $params)
{
// note that this is redone after the self-update
$hashChanges = $this->upgrader->compareHashes();
if (!$this->upgrader->checkWritable($hashChanges, $failures))
{
$this->outputError('Not all files are writable. Please upgrade manually.');
$this->cleanUp();
return false;
}
$this->state['changes'] = $hashChanges;
return 'selfupdate';
}
// Update the updater first to benefit from bug fixes
protected function stepSelfUpdate(array $params)
{
$files = [
'src/XF/Install/_upgrader/*',
'src/XF/Install/_templates/*',
'install/oc-upgrader.php'
];
if (!$this->copyNamedFilesOrError($files))
{
return false;
}
// This step should always go here and, if necessary, the target of the post self-update step should be changed.
// Otherwise, the self-update that happens here won't pick up changes that re-point the next step.
return 'postselfupdate';
}
// Placeholder step to allow changes to what we do after the self update
protected function stepPostSelfUpdate(array $params)
{
// This step should always be empty and simply redirect to the "real" step to take after the self update.
return 'composerdepscreate';
}
// update newly created files of our composer dependencies (skipping the composer core files)
protected function stepComposerDepsCreate(array $params)
{
$nextStep = 'composerdepsupdate';
$hashChanges = $this->getFilteredHashChanges('=', 'create');
if (!is_array($hashChanges))
{
// the next step will handle these
return $nextStep;
}
$action = $this->setupComposerExtractAction($params, $hashChanges);
return $this->copyActionPaginated($action, $nextStep, 'Dependencies (created)');
}
// update the remaining files of our composer dependencies (skipping the composer core files)
protected function stepComposerDepsUpdate(array $params)
{
$hashChanges = $this->getFilteredHashChanges('!=', 'create');
$action = $this->setupComposerExtractAction($params, $hashChanges);
return $this->copyActionPaginated($action, 'composercore', 'Dependencies (updated)');
}
/**
* @param array $params
* @param array|null $hashChanges
*
* @return XFUpgraderExtractAction
*/
protected function setupComposerExtractAction(array $params, array $hashChanges = null)
{
$params = array_replace([
'start' => 0
], $params);
$action = $this->upgrader->getExtractAction(
$hashChanges, $params['start'], \XF::app()->config('jobMaxRunTime')
);
$action->setMatchFiles(['src/vendor/*']);
$action->setSkipFiles(['src/vendor/composer/*', 'src/vendor/autoload.php']);
return $action;
}
// now update composer itself
protected function stepComposerCore(array $params)
{
$files = ['src/vendor/composer/*', 'src/vendor/autoload.php'];
if (!$this->copyNamedFilesOrError($files))
{
return false;
}
return 'reinitxf';
}
// don't name a step "reinit" -- it's a legacy step name that old versions of the upgrader may
// redirect to, so there's code to handle it.
// reinit our hashes to be safe
protected function stepReInitXf(array $params)
{
// redo this after the partial updates to avoid bugs
$this->state['changes'] = $this->upgrader->compareHashes();
return 'copyxfcreate';
}
// don't name a step "copy" -- it's a legacy step name that old versions of the upgrader may
// redirect to, so there's code to handle it.
// copy over new, non-composer files
protected function stepCopyXfCreate(array $params)
{
$nextStep = 'copyxfupdate';
$hashChanges = $this->getFilteredHashChanges('=', 'create');
if (!is_array($hashChanges))
{
// the next step will handle these
return $nextStep;
}
$action = $this->setupCoreExtractAction($params, $hashChanges);
return $this->copyActionPaginated($action, 'copyxfupdate', 'Core files (created)');
}
// copy over remaining non-composer files
protected function stepCopyXfUpdate(array $params)
{
$hashChanges = $this->getFilteredHashChanges('!=', 'create');
$action = $this->setupCoreExtractAction($params, $hashChanges);
return $this->copyActionPaginated($action, 'complete', 'Core files (updated)');
}
/**
* @param array $params
* @param array|null $hashChanges
*
* @return XFUpgraderExtractAction
*/
protected function setupCoreExtractAction(array $params, array $hashChanges = null)
{
$params = array_replace([
'start' => 0
], $params);
$action = $this->upgrader->getExtractAction(
$hashChanges, $params['start'], \XF::app()->config('jobMaxRunTime')
);
$action->setSkipFiles(['src/vendor/*']);
return $action;
}
// clean up and redirect to the upgrader
protected function stepComplete(array $params)
{
$this->cleanUp();
$app = \XF::app();
$basicUpgradeRedirect = true;
if ($app instanceof \XF\Install\App)
{
$app->setupUpgradeSession();
if (\XF::visitor()->is_admin)
{
// we have an install session
$basicUpgradeRedirect = false;
}
}
$upgrader = new \XF\Install\Upgrader($app);
if ($upgrader->isCliRecommended())
{
// if we recommend CLI upgrades, force this
$basicUpgradeRedirect = true;
}
if ($basicUpgradeRedirect)
{
header('Location: index.php?upgrade/');
}
else
{
// output a page to post into the upgrade system
$content = $this->templater->renderTemplate('upgrade_oc_complete');
$this->outputContainer($content);
}
return false;
}
protected function copyActionPaginated(XFUpgraderExtractAction $action, $nextStepName, $stepTitle = 'Files')
{
$result = $this->copyActionOrError($action);
if (!$result)
{
return false;
}
switch ($result['status'])
{
case 'incomplete':
$params['start'] = $result['last'] + 1;
$params['percent'] = $result['percent'];
$params['title'] = $stepTitle;
return $params;
case 'complete':
\XF\Util\Php::resetOpcache();
return $nextStepName;
default:
throw new \LogicException("Unknown result from copy '$result[status]'");
}
}
protected function copyActionOrError(XFUpgraderExtractAction $action)
{
$result = $this->upgrader->copyFiles($action, $error);
if (!$result)
{
$this->outputError('One or more files failed to copy. Please upgrade manually.');
$this->cleanUp();
return false;
}
return $result;
}
protected function copyNamedFilesOrError(array $files, $resetOpcache = true)
{
if (!$files)
{
return true;
}
$action = $this->upgrader->getExtractAction($this->getHashChanges());
$action->setMatchFiles($files);
if (!$this->copyActionOrError($action))
{
return false;
}
if ($resetOpcache)
{
\XF\Util\Php::resetOpcache();
}
return true;
}
/**
* @return array|null
*/
protected function getHashChanges()
{
if (isset($this->state['changes']) && is_array($this->state['changes']))
{
return $this->state['changes'];
}
else
{
return null;
}
}
protected function getFilteredHashChanges(string $operator, string $compareType)
{
$hashChanges = $this->getHashChanges();
if (!is_array($hashChanges))
{
return $hashChanges;
}
return $this->filterHashChanges($hashChanges, $operator, $compareType);
}
protected function filterHashChanges(array $hashChanges, string $operator, string $compareType)
{
switch ($operator)
{
case '=':
case '!=':
break;
default:
throw new \InvalidArgumentException("Operator must be = or !=");
}
$output = [];
foreach ($hashChanges AS $changeFile => $type)
{
switch ($operator)
{
case '=':
$matched = ($type === $compareType);
break;
case '!=':
$matched = ($type !== $compareType);
break;
default:
$matched = false;
}
if ($matched)
{
$output[$changeFile] = $type;
}
}
return $output;
}
protected function outputResult($step, array $params, $lastTicks)
{
$state = $this->state;
$ticks = max(0, intval($lastTicks)) + 1;
$content = $this->templater->renderTemplate('upgrade_oc_step', [
'key' => $this->key,
'step' => $step,
'ticks' => $ticks,
'state' => $state,
'params' => $params
]);
$this->outputContainer($content);
}
protected function outputError($message, $code = 400)
{
$content = $this->templater->renderTemplate('error', [
'error' => $message
]);
$this->outputContainer($content, $code);
}
protected function outputContainer($content, $code = 200)
{
$pageParams = $this->templater->pageParams;
$pageParams['content'] = $content;
header('Content-type: text/html; charset=utf-8', true, $code);
echo $this->templater->renderTemplate('PAGE_CONTAINER', $pageParams);
}
protected function cleanUp()
{
$this->upgrader->cleanUp();
}
/**
* Factory creation method. This is to move as much code into this file as possible to allow flexibility
* with future changes.
*
* @param string $rootDir Root of XF install
*
* @return XFUpgraderWeb
*/
public static function create($rootDir)
{
require($rootDir . '/src/XF.php');
XF::start($rootDir);
$app = XF::setupApp('XF\Install\App');
$app->start();
return new XFUpgraderWeb($app);
}
}