Passed
Pull Request — master (#77)
by Daniel
33:27
created

Version010000::checkComposer()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 15
ccs 0
cts 14
cp 0
rs 9.9
cc 3
nc 3
nop 0
crap 12
1
<?php
2
/**
3
 * CMS Pico - Create websites using Pico CMS for Nextcloud.
4
 *
5
 * @copyright Copyright (c) 2019, Daniel Rudolf (<[email protected]>)
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 */
22
23
declare(strict_types=1);
24
25
namespace OCA\CMSPico\Migration;
26
27
use Doctrine\DBAL\Schema\SchemaException;
28
use OC\Encryption\Manager as EncryptionManager;
29
use OCA\CMSPico\AppInfo\Application;
30
use OCA\CMSPico\Db\CoreRequestBuilder;
31
use OCA\CMSPico\Exceptions\ComposerException;
32
use OCA\CMSPico\Exceptions\FilesystemNotWritableException;
33
use OCA\CMSPico\Model\Template;
34
use OCA\CMSPico\Model\Theme;
35
use OCA\CMSPico\Model\WebsiteCore;
36
use OCA\CMSPico\Service\ConfigService;
37
use OCA\CMSPico\Service\FileService;
38
use OCA\CMSPico\Service\MiscService;
39
use OCA\CMSPico\Service\PicoService;
40
use OCA\CMSPico\Service\TemplatesService;
41
use OCA\CMSPico\Service\ThemesService;
42
use OCP\DB\ISchemaWrapper;
43
use OCP\Files\AlreadyExistsException;
44
use OCP\Files\InvalidPathException;
45
use OCP\Files\NotPermittedException;
46
use OCP\IDBConnection;
47
use OCP\IL10N;
48
use OCP\Migration\IOutput;
49
use OCP\Migration\SimpleMigrationStep;
50
51
class Version010000 extends SimpleMigrationStep
52
{
53
	/** @var IDBConnection */
54
	private $databaseConnection;
55
56
	/** @var IL10N */
57
	private $l10n;
58
59
	/** @var EncryptionManager */
60
	private $encryptionManager;
61
62
	/** @var ConfigService */
63
	private $configService;
64
65
	/** @var TemplatesService */
66
	private $templatesService;
67
68
	/** @var ThemesService */
69
	private $themesService;
70
71
	/** @var FileService */
72
	private $fileService;
73
74
	/** @var MiscService */
75
	private $miscService;
76
77
	/**
78
	 * Version010000 constructor.
79
	 */
80
	public function __construct()
81
	{
82
		$this->databaseConnection = \OC::$server->getDatabaseConnection();
83
		$this->l10n = \OC::$server->getL10N(Application::APP_NAME);
84
		$this->encryptionManager = \OC::$server->getEncryptionManager();
85
		$this->configService = \OC::$server->query(ConfigService::class);
86
		$this->templatesService = \OC::$server->query(TemplatesService::class);
87
		$this->themesService = \OC::$server->query(ThemesService::class);
88
		$this->fileService = \OC::$server->query(FileService::class);
89
		$this->miscService = \OC::$server->query(MiscService::class);
90
	}
91
92
	/**
93
	 * @param IOutput  $output
94
	 * @param \Closure $schemaClosure
95
	 * @param array    $options
96
	 *
97
	 * @return ISchemaWrapper
98
	 */
99
	public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ISchemaWrapper
100
	{
101
		/** @var ISchemaWrapper $schema */
102
		$schema = $schemaClosure();
103
104
		try {
105
			$table = $schema->getTable(CoreRequestBuilder::TABLE_WEBSITES);
106
		} catch (SchemaException $e) {
107
			$table = $schema->createTable(CoreRequestBuilder::TABLE_WEBSITES);
108
109
			$table->addColumn('id', 'integer', [
110
				'autoincrement' => true,
111
				'notnull' => true,
112
				'length' => 4,
113
				'unsigned' => true,
114
			]);
115
			$table->addColumn('user_id', 'string', [
116
				'notnull' => true,
117
				'length' => 64,
118
			]);
119
			$table->addColumn('name', 'string', [
120
				'notnull' => true,
121
				'length' => 255,
122
			]);
123
			$table->addColumn('site', 'string', [
124
				'notnull' => true,
125
				'length' => 255,
126
			]);
127
			$table->addColumn('theme', 'string', [
128
				'notnull' => true,
129
				'length' => 64,
130
				'default' => 'default',
131
			]);
132
			$table->addColumn('type', 'smallint', [
133
				'notnull' => true,
134
				'length' => 1,
135
			]);
136
			$table->addColumn('options', 'string', [
137
				'notnull' => false,
138
				'length' => 255,
139
			]);
140
			$table->addColumn('path', 'string', [
141
				'notnull' => false,
142
				'length' => 255,
143
			]);
144
			$table->addColumn('creation', 'datetime', [
145
				'notnull' => false,
146
			]);
147
148
			$table->setPrimaryKey(['id']);
149
		}
150
151
		$themeColumn = $table->getColumn('theme');
152
		if ($themeColumn->getLength() < 64) {
153
			$themeColumn->setLength(64);
154
		}
155
156
		if (!$table->hasIndex('user_id')) {
157
			$table->addIndex([ 'user_id' ], 'user_id');
158
		}
159
160
		if (!$table->hasIndex('site')) {
161
			$table->addIndex([ 'site' ], 'site');
162
		}
163
164
		return $schema;
165
	}
166
167
	/**
168
	 * @param IOutput  $output
169
	 * @param \Closure $schemaClosure
170
	 * @param array    $options
171
	 */
172
	public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options)
173
	{
174
		// check app dependencies
175
		$this->checkComposer();
176
		$this->createPublicFolder();
177
178
		// update from cms_pico v0.9
179
		// migrate the app config of custom templates and themes
180
		$this->migrateCustomTemplates();
181
		$this->migrateCustomThemes();
182
183
		// migrate old copies of system templates and themes in Nextcloud's data dir
184
		$this->migrateSystemTemplates();
185
		$themesMigrationMap = $this->migrateSystemThemes();
186
187
		// migrate cms_pico_websites database table
188
		$this->migratePrivateWebsites($themesMigrationMap);
189
	}
190
191
	/**
192
	 * @throws ComposerException
193
	 */
194
	private function checkComposer()
195
	{
196
		$appPath = Application::getAppPath();
197
		if (!is_file($appPath . '/vendor/autoload.php')) {
198
			try {
199
				$relativeAppPath = $this->miscService->getRelativePath($appPath) . '/';
200
			} catch (InvalidPathException $e) {
201
				$relativeAppPath = 'apps/' . Application::APP_NAME . '/';
202
			}
203
204
			throw new ComposerException($this->l10n->t(
205
				'Failed to enable Pico CMS for Nextcloud: Couldn\'t find "%s". Make sure to install the app\'s '
206
						. 'dependencies by executing `composer install` in the app\'s install directory below "%s". '
207
						. 'Then try again enabling Pico CMS for Nextcloud.',
208
				[ $relativeAppPath . 'vendor/autoload.php', $relativeAppPath ]
209
			));
210
		}
211
	}
212
213
	/**
214
	 * @throws FilesystemNotWritableException
215
	 */
216
	private function createPublicFolder()
217
	{
218
		$publicFolder = $this->fileService->getPublicFolder();
219
220
		try {
221
			try {
222
				$publicThemesFolder = $publicFolder->newFolder(PicoService::DIR_THEMES);
223
			} catch (AlreadyExistsException $e) {
224
				$publicThemesFolder = $publicFolder->getFolder(PicoService::DIR_THEMES);
225
			}
226
227
			$publicThemesTestFileName = $this->miscService->getRandom(10, 'tmp', Application::APP_NAME . '-test');
228
			$publicThemesTestFile = $publicThemesFolder->newFile($publicThemesTestFileName);
229
			$publicThemesTestFile->delete();
230
231
			try {
232
				$publicPluginsFolder = $publicFolder->newFolder(PicoService::DIR_PLUGINS);
233
			} catch (AlreadyExistsException $e) {
234
				$publicPluginsFolder = $publicFolder->getFolder(PicoService::DIR_PLUGINS);
235
			}
236
237
			$publicPluginsTestFileName = $this->miscService->getRandom(10, 'tmp', Application::APP_NAME . '-test');
238
			$publicPluginsTestFile = $publicPluginsFolder->newFile($publicPluginsTestFileName);
239
			$publicPluginsTestFile->delete();
240
		} catch (NotPermittedException $e) {
241
			try {
242
				$appDataPublicPath = Application::getAppPath() . '/appdata_public';
243
				$appDataPublicPath = $this->miscService->getRelativePath($appDataPublicPath) . '/';
244
			} catch (InvalidPathException $e) {
245
				$appDataPublicPath = 'apps/' . Application::APP_NAME . '/appdata_public/';
246
			}
247
248
			try {
249
				$dataPath = $this->configService->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
250
				$dataPath = $this->miscService->getRelativePath($dataPath) . '/';
251
			} catch (InvalidPathException $e) {
252
				$dataPath = 'data/';
253
			}
254
255
			throw new FilesystemNotWritableException($this->l10n->t(
256
				'Failed to enable Pico CMS for Nextcloud: The webserver has no permission to create files and '
257
						. 'folders below "%s". Make sure to give the webserver write access to this directory by '
258
						. 'changing its permissions and ownership to the same as of your "%s" directory. Then try '
259
						. 'again enabling Pico CMS for Nextcloud.',
260
				[ $appDataPublicPath, $dataPath ]
261
			));
262
		}
263
	}
264
265
	/**
266
	 * @return void
267
	 */
268
	private function migrateCustomTemplates()
269
	{
270
		$customTemplatesJson = $this->configService->getAppValue(ConfigService::CUSTOM_TEMPLATES);
271
		$customTemplates = $customTemplatesJson ? json_decode($customTemplatesJson, true) : [];
272
273
		$newCustomTemplates = [];
274
		foreach ($customTemplates as $templateName) {
275
			$newCustomTemplates[$templateName] = [
276
				'name' => $templateName,
277
				'type' => Template::TYPE_CUSTOM,
278
				'compat' => true
279
			];
280
		}
281
282
		$this->configService->setAppValue(ConfigService::CUSTOM_TEMPLATES, json_encode($newCustomTemplates));
283
	}
284
285
	/**
286
	 * @return void
287
	 */
288
	private function migrateCustomThemes()
289
	{
290
		$customThemesJson = $this->configService->getAppValue(ConfigService::CUSTOM_THEMES);
291
		$customThemes = $customThemesJson ? json_decode($customThemesJson, true) : [];
292
293
		$newCustomThemes = [];
294
		foreach ($customThemes as $themeName) {
295
			$newCustomThemes[$themeName] = [
296
				'name' => $themeName,
297
				'type' => Theme::TYPE_CUSTOM,
298
				'compat' => true
299
			];
300
		}
301
302
		$this->configService->setAppValue(ConfigService::CUSTOM_THEMES, json_encode($newCustomThemes));
303
	}
304
305
	/**
306
	 * @return array<string,string>
307
	 */
308
	private function migrateSystemTemplates()
309
	{
310
		$templatesFolder = $this->fileService->getAppDataFolder(PicoService::DIR_TEMPLATES);
311
312
		$templates = $this->templatesService->getTemplates();
313
		$systemTemplates = $this->templatesService->getSystemTemplates();
314
315
		$templatesMigrationMap = [];
316
		foreach ($templatesFolder as $templateFolder) {
317
			$templateName = $templateFolder->getName();
318
			if ($templateFolder->isFolder() && isset($systemTemplates[$templateName])) {
319
				$newTemplateName = $templateName . '-v0.9';
320
				for ($i = 1; isset($templates[$newTemplateName]) || $templatesFolder->exists($newTemplateName); $i++) {
321
					$newTemplateName = $templateName . '-v0.9-dup' . $i;
322
				}
323
324
				$templateFolder->rename($newTemplateName);
325
				$this->templatesService->registerCustomTemplate($newTemplateName);
326
				$templatesMigrationMap[$templateName] = $newTemplateName;
327
			}
328
		}
329
330
		return $templatesMigrationMap;
331
	}
332
333
	/**
334
	 * @return array<string,string>
335
	 */
336
	private function migrateSystemThemes()
337
	{
338
		$themesFolder = $this->fileService->getAppDataFolder(PicoService::DIR_THEMES);
339
340
		$themes = $this->themesService->getThemes();
341
		$systemThemes = $this->themesService->getSystemThemes();
342
343
		$themesMigrationMap = [];
344
		foreach ($themesFolder as $themeFolder) {
345
			$themeName = $themeFolder->getName();
346
			if ($themeFolder->isFolder() && isset($systemThemes[$themeName])) {
347
				$newThemeName = $themeName . '-v0.9';
348
				for ($i = 1; isset($themes[$newThemeName]) || $themesFolder->exists($newThemeName); $i++) {
349
					$newThemeName = $themeName . '-v0.9-dup' . $i;
350
				}
351
352
				$themeFolder->rename($newThemeName);
353
				$this->themesService->publishCustomTheme($newThemeName);
354
				$themesMigrationMap[$themeName] = $newThemeName;
355
			}
356
		}
357
358
		return $themesMigrationMap;
359
	}
360
361
	/**
362
	 * @param array $themesMigrationMap
363
	 */
364
	private function migratePrivateWebsites(array $themesMigrationMap)
365
	{
366
		$qbUpdate = $this->databaseConnection->getQueryBuilder();
367
		$qbUpdate
368
			->update(CoreRequestBuilder::TABLE_WEBSITES, 'w')
369
			->set('w.theme', $qbUpdate->createParameter('theme'))
370
			->set('w.type', $qbUpdate->createParameter('type'))
371
			->set('w.options', $qbUpdate->createParameter('options'))
372
			->where($qbUpdate->expr()->eq('w.id', $qbUpdate->createParameter('id')));
373
374
		$selectCursor = $this->databaseConnection->getQueryBuilder()
375
			->select('w.id', 'w.theme', 'w.type', 'w.options')
376
			->from(CoreRequestBuilder::TABLE_WEBSITES, 'w')
377
			->execute();
378
379
		while ($data = $selectCursor->fetch()) {
380
			$websiteTheme = $themesMigrationMap[$data['theme']] ?? $data['theme'];
381
382
			$websiteType = $data['type'] ?: WebsiteCore::TYPE_PUBLIC;
383
			$websiteOptions = $data['options'] ? json_decode($data['options'], true) : [];
384
			if (isset($websiteOptions['private'])) {
385
				$websiteType = $websiteOptions['private'] ? WebsiteCore::TYPE_PRIVATE : WebsiteCore::TYPE_PUBLIC;
386
				unset($websiteOptions['private']);
387
			}
388
389
			$qbUpdate->setParameters([
390
				'id' => $data['id'],
391
				'theme' => $websiteTheme,
392
				'type' => $websiteType,
393
				'options' => json_encode($websiteOptions),
394
			]);
395
396
			$qbUpdate->execute();
397
		}
398
399
		$selectCursor->closeCursor();
400
	}
401
}
402