1<?php
2
3/**
4 * @author Victor Dubiniuk <dubiniuk@owncloud.com>
5 *
6 * @copyright Copyright (c) 2015, ownCloud, Inc.
7 * @license AGPL-3.0
8 *
9 * This code is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License, version 3,
11 * as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License, version 3,
19 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20 *
21 */
22
23namespace Owncloud\Updater\Command;
24
25use Owncloud\Updater\Utils\Checkpoint;
26use Owncloud\Updater\Utils\FilesystemHelper;
27use Symfony\Component\Console\Input\InputInterface;
28use Symfony\Component\Console\Output\OutputInterface;
29use Symfony\Component\Process\Exception\ProcessFailedException;
30use Owncloud\Updater\Utils\OccRunner;
31use Owncloud\Updater\Utils\ZipExtractor;
32
33/**
34 * Class ExecuteCoreUpgradeScriptsCommand
35 *
36 * @package Owncloud\Updater\Command
37 */
38class ExecuteCoreUpgradeScriptsCommand extends Command {
39
40	/**
41	 * @var OccRunner $occRunner
42	 */
43	protected $occRunner;
44
45	/**
46	 * ExecuteCoreUpgradeScriptsCommand constructor.
47	 *
48	 * @param null|string $occRunner
49	 */
50	public function __construct($occRunner){
51		parent::__construct();
52		$this->occRunner = $occRunner;
53	}
54
55	protected function configure(){
56		$this
57				->setName('upgrade:executeCoreUpgradeScripts')
58				->setDescription('execute core upgrade scripts [danger, might take long]');
59	}
60
61	/**
62	 * @param InputInterface $input
63	 * @param OutputInterface $output
64	 * @throws \Exception
65	 */
66	protected function execute(InputInterface $input, OutputInterface $output){
67		$locator = $this->container['utils.locator'];
68		/** @var FilesystemHelper $fsHelper */
69		$fsHelper = $this->container['utils.filesystemhelper'];
70		$registry = $this->container['utils.registry'];
71		$fetcher = $this->container['utils.fetcher'];
72		/** @var Checkpoint $checkpoint */
73		$checkpoint = $this->container['utils.checkpoint'];
74
75		$installedVersion = implode('.', $locator->getInstalledVersion());
76		$registry->set('installedVersion', $installedVersion);
77
78		/** @var  \Owncloud\Updater\Utils\Feed $feed */
79		$feed = $registry->get('feed');
80
81		if ($feed){
82			$path = $fetcher->getBaseDownloadPath($feed);
83			$fullExtractionPath = $locator->getExtractionBaseDir() . '/' . $feed->getVersion();
84
85			if (file_exists($fullExtractionPath)){
86				$fsHelper->removeIfExists($fullExtractionPath);
87			}
88			try{
89				$fsHelper->mkdir($fullExtractionPath, true);
90			} catch (\Exception $e){
91					$output->writeln('Unable create directory ' . $fullExtractionPath);
92					throw $e;
93			}
94
95			$output->writeln('Extracting source into ' . $fullExtractionPath);
96			$extractor = new ZipExtractor($path, $fullExtractionPath, $output);
97
98			try{
99				$extractor->extract();
100			} catch (\Exception $e){
101				$output->writeln('Extraction has been failed');
102				$fsHelper->removeIfExists($locator->getExtractionBaseDir());
103				throw $e;
104			}
105
106			$tmpDir = $locator->getExtractionBaseDir() . '/' . $installedVersion;
107			$fsHelper->removeIfExists($tmpDir);
108			$fsHelper->mkdir($tmpDir);
109			$oldSourcesDir = $locator->getOwncloudRootPath();
110			$newSourcesDir = $fullExtractionPath . '/owncloud';
111
112			$packageVersion = $this->loadVersion($newSourcesDir);
113			$allowedPreviousVersions = $this->loadAllowedPreviousVersions($newSourcesDir);
114
115			if (!$this->isUpgradeAllowed($installedVersion, $packageVersion, $allowedPreviousVersions)){
116				$message = sprintf(
117					'Update from %s to %s is not possible. Updates between multiple major versions and downgrades are unsupported.',
118					$installedVersion,
119					$packageVersion
120				);
121				$this->getApplication()->getLogger()->error($message);
122				$output->writeln('<error>'. $message .'</error>');
123				return 1;
124			}
125
126			$rootDirContent = array_unique(
127				array_merge(
128					$locator->getRootDirContent(),
129					$locator->getRootDirItemsFromSignature($oldSourcesDir),
130					$locator->getRootDirItemsFromSignature($newSourcesDir)
131				)
132			);
133			foreach ($rootDirContent as $dir){
134				if ($dir === 'updater') {
135					continue;
136				}
137				$this->getApplication()->getLogger()->debug('Replacing ' . $dir);
138				$fsHelper->tripleMove($oldSourcesDir, $newSourcesDir, $tmpDir, $dir);
139			}
140
141			$fsHelper->copyr($tmpDir . '/config/config.php', $oldSourcesDir . '/config/config.php');
142
143			//Get a new shipped apps list
144			$newAppsDir = $fullExtractionPath . '/owncloud/apps';
145			$newAppsList = $fsHelper->scandirFiltered($newAppsDir);
146
147			//Remove old apps
148			$appDirectories = $fsHelper->scandirFiltered($oldSourcesDir . '/apps');
149			$oldAppList = array_intersect($appDirectories, $newAppsList);
150			foreach ($oldAppList as $appDirectory){
151				$fsHelper->rmdirr($oldSourcesDir . '/apps/' . $appDirectory);
152			}
153
154			foreach ($newAppsList as $appId){
155				$output->writeln('Copying the application ' . $appId);
156				$fsHelper->copyr($newAppsDir . '/' . $appId, $locator->getOwnCloudRootPath() . '/apps/' . $appId, false);
157			}
158
159			try {
160				$plain = $this->occRunner->run('upgrade');
161				$output->writeln($plain);
162			} catch (ProcessFailedException $e){
163				$lastCheckpointId = $checkpoint->getLastCheckpointId();
164				if ($lastCheckpointId){
165					$lastCheckpointPath = $checkpoint->getCheckpointPath($lastCheckpointId);
166					$fsHelper->copyr($lastCheckpointPath . '/apps', $oldSourcesDir . '/apps', false);
167				}
168				if ($e->getProcess()->getExitCode() != 3){
169					throw ($e);
170				}
171			}
172		}
173	}
174
175	public function isUpgradeAllowed($installedVersion, $packageVersion, $canBeUpgradedFrom){
176		if (version_compare($installedVersion, $packageVersion, '<=')){
177			foreach ($canBeUpgradedFrom as $allowedPreviousVersion){
178				if (version_compare($allowedPreviousVersion, $installedVersion, '<=')){
179					return true;
180				}
181			}
182		}
183		return false;
184	}
185
186	/**
187	 * @param string $pathToPackage
188	 * @return string
189	 */
190	protected function loadVersion($pathToPackage){
191		require  $pathToPackage . '/version.php';
192		/** @var $OC_Version array */
193		return implode('.', $OC_Version);
194	}
195
196	/**
197	 * @param string $pathToPackage
198	 * @return string[]
199	 */
200	protected function loadAllowedPreviousVersions($pathToPackage) {
201		$canBeUpgradedFrom = $this->loadCanBeUpgradedFrom($pathToPackage);
202
203		$firstItem = reset($canBeUpgradedFrom);
204		if (!is_array($firstItem)){
205			$canBeUpgradedFrom = [$canBeUpgradedFrom];
206		}
207
208		$allowedVersions = [];
209		foreach ($canBeUpgradedFrom as $version){
210			$allowedVersions[] = implode('.', $version);
211		}
212
213		return $allowedVersions;
214	}
215
216	/**
217	 * @param string $pathToPackage
218	 * @return array
219	 */
220	protected function loadCanBeUpgradedFrom($pathToPackage){
221		require $pathToPackage . '/version.php';
222		/** @var array $OC_VersionCanBeUpgradedFrom */
223		return $OC_VersionCanBeUpgradedFrom;
224	}
225}
226