Issues (524)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/model/DNDataArchive.php (14 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Represents a file archive of database and/or assets extracted from
5
 * a specific Deploynaut environment.
6
 *
7
 * The model can also represent a request to upload a file later,
8
 * through offline processes like mailing a DVD. In order to associate
9
 * and authenticate those requests easily, an upload token is generated for every archive.
10
 *
11
 * The "OriginalEnvironment" points to original source of this snapshot
12
 * (the one it was backed up from). It will be empty if the snapshot has been created with offline process.
13
 *
14
 * The "Environment" denotes the ownership of the snapshot. It will be initially set to match the
15
 * "OriginalEnvironment", but can be changed later. During the offline process the ownership can be set up
16
 * arbitrarily.
17
 *
18
 * When moving snapshots, the file always remains in its initial location.
19
 *
20
 * The archive can have associations to {@link DNDataTransfer}:
21
 * - Zero transfers if a manual upload was requested, but not fulfilled yet
22
 * - One transfer with Direction=get for a backup from an environment
23
 * - One or more transfers with Direction=push for a restore to an environment
24
 *
25
 * The "Author" is either the person creating the archive through a "backup" operation,
26
 * the person uploading through a web form, or the person requesting a manual upload.
27
 *
28
 * The "Mode" is what the "Author" said the file includes (either 'only assets', 'only
29
 * database', or both). This is used in the ArchiveList.ss template.
30
 *
31
 * @property Varchar $UploadToken
32
 * @property Varchar $ArchiveFileHash
33
 * @property Enum $Mode
34
 * @property Boolean $IsBackup
35
 * @property Boolean $IsManualUpload
36
 *
37
 * @method Member Author()
38
 * @property int $AuthorID
39
 * @method DNEnvironment OriginalEnvironment()
40
 * @property int $OriginalEnvironmentID
41
 * @method DNEnvironment Environment()
42
 * @property int $EnvironmentID
43
 * @method File ArchiveFile()
44
 * @property int $ArchiveFileID
45
 *
46
 * @method ManyManyList DataTransfers()
47
 *
48
 */
49
class DNDataArchive extends DataObject {
0 ignored issues
show
The property $has_one is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $has_many is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $singular_name is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $plural_name is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $summary_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $searchable_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $_cache_can_restore is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $_cache_can_download is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
50
51
	private static $db = array(
52
		'UploadToken' => 'Varchar(8)',
53
		"Mode" => "Enum('all, assets, db', '')",
54
		"IsBackup" => "Boolean",
55
		"IsManualUpload" => "Boolean",
56
	);
57
58
	private static $has_one = array(
59
		'Author' => 'Member',
60
		'OriginalEnvironment' => 'DNEnvironment',
61
		'Environment' => 'DNEnvironment',
62
		'ArchiveFile' => 'File'
63
	);
64
65
	private static $has_many = array(
66
		'DataTransfers' => 'DNDataTransfer',
67
	);
68
69
	private static $singular_name = 'Data Archive';
0 ignored issues
show
The property $singular_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
70
71
	private static $plural_name = 'Data Archives';
72
73
	private static $summary_fields = array(
74
		'Created' => 'Created',
75
		'Author.Title' => 'Author',
76
		'Environment.Project.Name' => 'Project',
77
		'OriginalEnvironment.Name' => 'Origin',
78
		'Environment.Name' => 'Environment',
79
		'ArchiveFile.Name' => 'File',
80
	);
81
82
	private static $searchable_fields = array(
83
		'Environment.Project.Name' => array(
84
			'title' => 'Project',
85
		),
86
		'OriginalEnvironment.Name' => array(
87
			'title' => 'Origin',
88
		),
89
		'Environment.Name' => array(
90
			'title' => 'Environment',
91
		),
92
		'UploadToken' => array(
93
			'title' => 'Upload Token',
94
		),
95
		'Mode' => array(
96
			'title' => 'Mode',
97
		),
98
	);
99
100
	private static $_cache_can_restore = array();
101
102
	private static $_cache_can_download = array();
103
104
	public static function get_mode_map() {
105
		return array(
106
			'all' => 'Database and Assets',
107
			'db' => 'Database only',
108
			'assets' => 'Assets only',
109
		);
110
	}
111
112
	/**
113
	 * Returns a unique token to correlate an offline item (posted DVD)
114
	 * with a specific archive placeholder.
115
	 *
116
	 * @return string
117
	 */
118
	public static function generate_upload_token($chars = 8) {
119
		$generator = new RandomGenerator();
120
		return strtoupper(substr($generator->randomToken(), 0, $chars));
121
	}
122
123
	public function onBeforeWrite() {
124
		if(!$this->AuthorID) {
125
			$this->AuthorID = Member::currentUserID();
126
		}
127
128
		parent::onBeforeWrite();
129
	}
130
131
	public function onAfterDelete() {
132
		$file = $this->ArchiveFile();
133
		if ($file && $file->exists()) {
134
			$file->delete();
135
		}
136
	}
137
138
	public function getCMSFields() {
139
		$fields = parent::getCMSFields();
140
		$fields->removeByName('OriginalEnvironmentID');
141
		$fields->removeByName('EnvironmentID');
142
		$fields->removeByName('ArchiveFile');
143
		$fields->addFieldsToTab(
144
			'Root.Main',
145
			array(
146
				new ReadonlyField('ProjectName', 'Project', $this->Environment()->Project()->Name),
147
				new ReadonlyField('OriginalEnvironmentName', 'OriginalEnvironment', $this->OriginalEnvironment()->Name),
148
				new ReadonlyField('EnvironmentName', 'Environment', $this->Environment()->Name),
149
				$linkField = new ReadonlyField(
150
					'DataArchive',
151
					'Archive File',
152
					sprintf(
153
						'<a href="%s">%s</a>',
154
						$this->ArchiveFile()->AbsoluteURL,
155
						$this->ArchiveFile()->Filename
156
					)
157
				),
158
				new GridField(
159
					'DataTransfers',
160
					'Transfers',
161
					$this->DataTransfers()
162
				),
163
			)
164
		);
165
		$linkField->dontEscape = true;
166
		$fields = $fields->makeReadonly();
167
168
		return $fields;
169
	}
170
171
	public function getDefaultSearchContext() {
172
		$context = parent::getDefaultSearchContext();
173
		$context->getFields()->dataFieldByName('Mode')->setHasEmptyDefault(true);
174
175
		return $context;
176
	}
177
178
	/**
179
	 * Calculates and returns a human-readable size of this archive file. If the file exists, it will determine
180
	 * whether to display the output in bytes, kilobytes, megabytes, or gigabytes.
181
	 *
182
	 * @return string The human-readable size of this archive file
183
	 */
184
	public function FileSize() {
185
		if($this->ArchiveFile()->exists()) {
186
			return $this->ArchiveFile()->getSize();
187
		} else {
188
			return "N/A";
189
		}
190
	}
191
192
	public function getModeNice() {
193
		if($this->Mode == 'all') {
194
			return 'database and assets';
195
		} else {
196
			return $this->Mode;
197
		}
198
	}
199
200
	/**
201
	 * Some archives don't have files attached to them yet,
202
	 * because a file has been posted offline and is waiting to be uploaded
203
	 * against this "archive placeholder".
204
	 *
205
	 * @return boolean
206
	 */
207
	public function isPending() {
208
		return !($this->ArchiveFileID);
209
	}
210
211
	/**
212
	 * Inferred from both restore and backup permissions.
213
	 *
214
	 * @param Member|null $member The {@link Member} object to test against.
215
	 */
216
	public function canView($member = null) {
217
		return ($this->canRestore($member) || $this->canDownload($member));
218
	}
219
220
	/**
221
	 * Whether a {@link Member} can restore this archive to an environment.
222
	 * This only needs to be checked *once* per member and environment.
223
	 *
224
	 * @param Member|null $member The {@link Member} object to test against.
225
	 * @return true if $member (or the currently logged in member if null) can upload this archive
226
	 */
227 View Code Duplication
	public function canRestore($member = null) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
228
		$memberID = $member ? $member->ID : Member::currentUserID();
229
		if(!$memberID) {
230
			return false;
231
		}
232
233
		$key = $memberID . '-' . $this->EnvironmentID;
234
		if(!isset(self::$_cache_can_restore[$key])) {
235
			self::$_cache_can_restore[$key] = $this->Environment()->canUploadArchive($member);
236
		}
237
238
		return self::$_cache_can_restore[$key];
239
	}
240
241
	/**
242
	 * Whether a {@link Member} can download this archive to their PC.
243
	 * This only needs to be checked *once* per member and environment.
244
	 *
245
	 * @param Member|null $member The {@link Member} object to test against.
246
	 * @return true if $member (or the currently logged in member if null) can download this archive
247
	 */
248 View Code Duplication
	public function canDownload($member = null) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
249
		$memberID = $member ? $member->ID : Member::currentUserID();
250
		if(!$memberID) {
251
			return false;
252
		}
253
254
		$key = $memberID . '-' . $this->EnvironmentID;
255
		if(!isset(self::$_cache_can_download[$key])) {
256
			self::$_cache_can_download[$key] = $this->Environment()->canDownloadArchive($member);
257
		}
258
		return self::$_cache_can_download[$key];
259
	}
260
261
	/**
262
	 * Whether a {@link Member} can delete this archive from staging area.
263
	 *
264
	 * @param Member|null $member The {@link Member} object to test against.
265
	 * @return boolean if $member (or the currently logged in member if null) can delete this archive
266
	 */
267
	public function canDelete($member = null) {
268
		return $this->Environment()->canDeleteArchive($member);
269
	}
270
271
	/**
272
	 * Check if this member can move archive into the environment.
273
	 *
274
	 * @param DNEnvironment $targetEnv Environment to check.
275
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
276
	 *
277
	 * @return boolean true if $member can upload archives linked to this environment, false if they can't.
278
	 */
279
	public function canMoveTo($targetEnv, $member = null) {
280
		if($this->Environment()->Project()->ID != $targetEnv->Project()->ID) {
281
			// We don't permit moving snapshots between projects at this stage.
282
			return false;
283
		}
284
285
		if(!$member) {
286
			$member = Member::currentUser();
287
		}
288
289
		// Must be logged in to check permissions
290
		if(!$member) {
291
			return false;
292
		}
293
294
		// Admin can always move.
295
		if(Permission::checkMember($member, 'ADMIN')) {
296
			return true;
297
		}
298
299
		// Checks if the user can actually access the archive.
300
		if(!$this->canDownload($member)) {
301
			return false;
302
		}
303
304
		// Hooks into ArchiveUploaders permission to prevent proliferation of permission checkboxes.
305
		// Bypasses the quota check - we don't need to check for it as long as we move the snapshot within the project.
306
		return $targetEnv->ArchiveUploaders()->byID($member->ID)
307
			|| $member->inGroups($targetEnv->ArchiveUploaderGroups());
308
	}
309
310
	/**
311
	 * Finds all environments within this project where the archive can be moved to.
312
	 * Excludes current environment automatically.
313
	 *
314
	 * @return ArrayList List of valid environments.
315
	 */
316
	public function validTargetEnvironments() {
317
		$archive = $this;
318
		$envs = $this->Environment()->Project()->DNEnvironmentList()
319
			->filterByCallback(function($item) use ($archive) {
320
				return $archive->EnvironmentID != $item->ID && $archive->canMoveTo($item);
321
			});
322
323
		return $envs;
324
	}
325
326
	/**
327
	 * Returns a unique filename, including project/environment/timestamp details.
328
	 * @return string
329
	 */
330
	public function generateFilename(\DNDataTransfer $dataTransfer) {
331
		$generator = new RandomGenerator();
332
		$filter = FileNameFilter::create();
333
334
		return sprintf(
335
			'%s-%s-%s-%s-%s',
336
			$filter->filter(strtolower($this->OriginalEnvironment()->Project()->Name)),
337
			$filter->filter(strtolower($this->OriginalEnvironment()->Name)),
338
			$dataTransfer->Mode,
339
			date('Ymd'),
340
			sha1($generator->generateEntropy())
341
		);
342
	}
343
344
	/**
345
	 * Returns a path unique to a specific transfer, including project/environment details.
346
	 * Does not create the path on the filesystem. Can be used to store files related to this transfer.
347
	 *
348
	 * @param DNDataTransfer
349
	 * @return string Absolute file path
350
	 */
351
	public function generateFilepath(\DNDataTransfer $dataTransfer) {
352
		$data = DNData::inst();
353
		$transferDir = $data->getDataTransferDir();
354
		$filter = FileNameFilter::create();
355
356
		return sprintf('%s/%s/%s/transfer-%s/',
357
			$transferDir,
358
			$filter->filter(strtolower($this->OriginalEnvironment()->Project()->Name)),
359
			$filter->filter(strtolower($this->OriginalEnvironment()->Name)),
360
			$dataTransfer->ID
361
		);
362
	}
363
364
	/**
365
	 * Attach an sspak file path to this archive and associate the transfer.
366
	 * Does the job of creating a {@link File} record, and setting correct paths into the assets directory.
367
	 *
368
	 * @param string $sspakFilepath
369
	 * @param DNDataTransfer $dataTransfer
370
	 * @return bool
371
	 */
372
	public function attachFile($sspakFilepath, DNDataTransfer $dataTransfer) {
373
		$sspakFilepath = ltrim(
374
			str_replace(
375
				array(ASSETS_PATH, realpath(ASSETS_PATH)),
376
				'',
377
				$sspakFilepath
378
			),
379
			DIRECTORY_SEPARATOR
380
		);
381
382
		$folder = Folder::find_or_make(dirname($sspakFilepath));
383
		$file = new File();
384
		$file->Name = basename($sspakFilepath);
385
		$file->Filename = $sspakFilepath;
386
		$file->ParentID = $folder->ID;
387
		$file->write();
388
389
		// "Status" will be updated by the job execution
390
		$dataTransfer->write();
391
392
		$this->ArchiveFileID = $file->ID;
393
		$this->DataTransfers()->add($dataTransfer);
394
		$this->write();
395
396
		return true;
397
	}
398
399
	/**
400
	 * Extract the current sspak contents into the given working directory.
401
	 * This also extracts the assets and database and puts them into
402
	 * <workingdir>/database.sql and <workingdir>/assets, respectively.
403
	 *
404
	 * @param string|null $workingDir The path to extract to
405
	 * @throws RuntimeException
406
	 * @return bool
407
	 */
408
	public function extractArchive($workingDir = null) {
409
		if(!is_dir($workingDir)) {
410
			mkdir($workingDir, 0700, true);
411
		}
412
413
		$cleanupFn = function() use($workingDir) {
414
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
415
			$process->setTimeout(120);
416
			$process->run();
417
		};
418
419
		// Extract *.sspak to a temporary location
420
		$sspakFilename = $this->ArchiveFile()->FullPath;
421
		$process = new AbortableProcess(sprintf(
422
			'tar -xf %s --directory %s',
423
			escapeshellarg($sspakFilename),
424
			escapeshellarg($workingDir)
425
		));
426
		$process->setTimeout(3600);
427
		$process->run();
428
		if(!$process->isSuccessful()) {
429
			$cleanupFn();
430
			throw new RuntimeException(sprintf('Could not extract the sspak file: %s', $process->getErrorOutput()));
431
		}
432
433
		// Extract database.sql.gz to <workingdir>/database.sql
434 View Code Duplication
		if(file_exists($workingDir . DIRECTORY_SEPARATOR . 'database.sql.gz')) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
435
			$process = new AbortableProcess('gunzip database.sql.gz', $workingDir);
436
			$process->setTimeout(3600);
437
			$process->run();
438
			if(!$process->isSuccessful()) {
439
				$cleanupFn();
440
				throw new RuntimeException(sprintf('Could not extract the db archive: %s', $process->getErrorOutput()));
441
			}
442
		}
443
444
		// Extract assets.tar.gz to <workingdir>/assets/
445 View Code Duplication
		if(file_exists($workingDir . DIRECTORY_SEPARATOR . 'assets.tar.gz')) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
446
			$process = new AbortableProcess('tar xzf assets.tar.gz', $workingDir);
447
			$process->setTimeout(3600);
448
			$process->run();
449
			if(!$process->isSuccessful()) {
450
				$cleanupFn();
451
				throw new RuntimeException(sprintf('Could not extract the assets archive: %s', $process->getErrorOutput()));
452
			}
453
		}
454
455
		return true;
456
	}
457
458
	/**
459
	 * Validate that an sspak contains the correct content.
460
	 *
461
	 * For example, if the user uploaded an sspak containing just the db, but declared in the form
462
	 * that it contained db+assets, then the archive is not valid.
463
	 *
464
	 * @param string|null $mode "db", "assets", or "all". This is the content we're checking for. Default to the archive setting
0 ignored issues
show
This line exceeds maximum limit of 120 characters; contains 125 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
465
	 * @return ValidationResult
466
	 */
467
	public function validateArchiveContents($mode = null) {
468
		$mode = $mode ?: $this->Mode;
469
		$result = new ValidationResult();
470
471
		$file = $this->ArchiveFile()->FullPath;
472
473
		if(!is_readable($file)) {
474
			$result->error(sprintf('SSPak file "%s" cannot be read.', $file));
475
			return $result;
476
		}
477
478
		$process = new AbortableProcess(sprintf('tar -tf %s', escapeshellarg($file)));
479
		$process->setTimeout(120);
480
		$process->run();
481
		if(!$process->isSuccessful()) {
482
			throw new RuntimeException(sprintf('Could not list files in archive: %s', $process->getErrorOutput()));
483
		}
484
485
		$output = explode(PHP_EOL, $process->getOutput());
486
		$files = array_filter($output);
487
488
		if(in_array($mode, array('all', 'db')) && !in_array('database.sql.gz', $files)) {
489
			$result->error('The snapshot is missing the database.');
490
			return $result;
491
		}
492
493
		if(in_array($mode, array('all', 'assets')) && !in_array('assets.tar.gz', $files)) {
494
			$result->error('The snapshot is missing assets.');
495
			return $result;
496
		}
497
498
		return $result;
499
	}
500
501
	/**
502
	 * Given a path that already exists and contains an extracted sspak, including
503
	 * the assets, fix all of the file permissions so they're in a state ready to
504
	 * be pushed to remote servers.
505
	 *
506
	 * Normally, command line tar will use permissions found in the archive, but will
507
	 * substract the user's umask from them. This has a potential to create unreadable
508
	 * files, e.g. cygwin on Windows will pack files with mode 000, hence why this fix
509
	 * is necessary.
510
	 *
511
	 * @param string|null $workingDir The path of where the sspak has been extracted to
512
	 * @throws RuntimeException
513
	 * @return bool
514
	 */
515
	public function fixArchivePermissions($workingDir) {
516
		$fixCmds = array(
517
			// The directories need to have permissions changed one by one (hence the ; instead of +),
518
			// otherwise we might end up having no +x access to a directory deeper down.
519
			sprintf('find %s -type d -exec chmod 755 {} \;', escapeshellarg($workingDir)),
520
			sprintf('find %s -type f -exec chmod 644 {} +', escapeshellarg($workingDir))
521
		);
522
523
		foreach($fixCmds as $cmd) {
524
			$process = new AbortableProcess($cmd);
525
			$process->setTimeout(3600);
526
			$process->run();
527
			if(!$process->isSuccessful()) {
528
				throw new RuntimeException($process->getErrorOutput());
529
			}
530
		}
531
532
		return true;
533
	}
534
535
	/**
536
	 * Given extracted sspak contents, create an sspak from it
537
	 * and overwrite the current ArchiveFile with it's contents.
538
	 * Use GZIP=-1 for less compression on assets, which are already
539
	 * heavily compressed to begin with.
540
	 *
541
	 * @param string|null $workingDir The path of where the sspak has been extracted to
542
	 * @return bool
543
	 */
544
	public function setArchiveFromFiles($workingDir) {
545
		$commands = array();
546
		if($this->Mode == 'db') {
547
			if (file_exists($workingDir . '/database.sql')) {
548
				$commands[] = 'gzip database.sql';
549
			}
550
			$commands[] = sprintf('tar -cf %s database.sql.gz', $this->ArchiveFile()->FullPath);
551
			$commands[] = 'rm -f database.sql.gz';
552
		} elseif($this->Mode == 'assets') {
553
			$commands[] = 'GZIP=-1 tar --dereference -czf assets.tar.gz assets';
554
			$commands[] = sprintf('tar -cf %s assets.tar.gz', $this->ArchiveFile()->FullPath);
555
			$commands[] = 'rm -f assets.tar.gz';
556
		} else {
557
			if (file_exists($workingDir . '/database.sql')) {
558
				$commands[] = 'gzip database.sql';
559
			}
560
			$commands[] = 'GZIP=-1 tar --dereference -czf assets.tar.gz assets';
561
			$commands[] = sprintf('tar -cf %s database.sql.gz assets.tar.gz', $this->ArchiveFile()->FullPath);
562
			$commands[] = 'rm -f database.sql.gz assets.tar.gz';
563
		}
564
565
		$process = new AbortableProcess(implode(' && ', $commands), $workingDir);
566
		$process->setTimeout(3600);
567
		$process->run();
568
		if(!$process->isSuccessful()) {
569
			throw new RuntimeException($process->getErrorOutput());
570
		}
571
572
		$this->write();
573
574
		return true;
575
	}
576
577
}
578