<commit_before><?php
use \Symfony\Component\Process\Process;

/**
 * God controller for the deploynaut interface
 *
 * @package deploynaut
 * @subpackage control
 */
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {

	/**
	 * Access permission code
	 */
	const DEPLOYNAUT_ACCESS = 'DEPLOYNAUT_ACCESS';

	/**
	 * Bypass pipeline permission code
	 */
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';

	/**
	 * Allow dryrun of pipelines
	 */
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';

	/**
	 *
	 * @var array
	 */
	public static $allowed_actions = array(
		'projects',
		'update',
		'project',
		'branch',
		'environment',
		'abortpipeline',
		'pipeline',
		'pipelinelog',
		'metrics',
		'getDeployForm',
		'deploy',
		'deploylog',
		'getDataTransferForm',
		'transfer',
		'transferlog',
		'snapshots',
		'createsnapshot',
		'snapshotslog',
		'uploadsnapshot',
		'getUploadSnapshotForm',
		'getPostSnapshotForm',
		'getDataTransferRestoreForm',
		'getDeleteForm',
		'getMoveForm',
		'restoresnapshot',
		'deletesnapshot',
		'movesnapshot',
		'postsnapshotsuccess',
	);

	/**
	 * URL handlers pretending that we have a deep URL structure.
	 */
	public static $url_handlers = array(
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
		'project/$Project/DeleteForm' => 'getDeleteForm',
		'project/$Project/MoveForm' => 'getMoveForm',
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
		'project/$Project/environment/$Environment/metrics' => 'metrics',
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
		'project/$Project/transfer/$Identifier' => 'transfer',
		'project/$Project/environment/$Environment' => 'environment',
		'project/$Project/branch' => 'branch',
		'project/$Project/build/$Build' => 'build',
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
		'project/$Project/update' => 'update',
		'project/$Project/snapshots' => 'snapshots',
		'project/$Project/createsnapshot' => 'createsnapshot',
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
		'project/$Project/snapshotslog' => 'snapshotslog',
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
		'project/$Project' => 'project',
		'projects' => 'projects',
	);

	/**
	 *
	 * @var DNData
	 */
	protected $data;

	/**
	 * Check for feature flags:
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
	 *
	 * @return boolean
	 */
	public static function FlagSnapshotsEnabled() {
		if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) return true;
		if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
			$member = Member::currentUser();
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) return true;
		}
		return false;
	}

	/**
	 *
	 */
	public function init() {
		parent::init();

		if(!Permission::check(self::DEPLOYNAUT_ACCESS)) {
			return Security::permissionFailure();
		}

		parent::init();
		Requirements::combine_files(
			'deploynaut.js',
			array(
				THIRDPARTY_DIR . '/jquery/jquery.js',
				'deploynaut/javascript/bootstrap.js',
				'deploynaut/javascript/deploynaut.js',
				'deploynaut/javascript/bootstrap.file-input.js',

			)
		);

		Requirements::css(FRAMEWORK_ADMIN_DIR .'/thirdparty/chosen/chosen/chosen.css');
	}

	/**
	 *
	 * @return string
	 */
	public function Link() {
		return "naut/";
	}

	/**
	 * Actions
	 *
	 * @return \SS_HTTPResponse
	 */
	public function index(SS_HTTPRequest $request) {
		return $this->redirect($this->Link() . 'projects/');
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function projects(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
		return $this->customise(array(
			'Title' => 'Projects',
			'CurrentProject' => $this->getCurrentProject(),
		))->renderWith(array('DNRoot_projects', 'DNRoot'));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function snapshots(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		return $this->customise(array(
			'Title' => 'Data Snapshots',
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
		))->renderWith(array('DNRoot_snapshots', 'DNRoot'));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function createsnapshot(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canBackup()) {
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
		}

		return $this->customise(array(
			'Title' => 'Create Data Snapshot',
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
			'DataTransferForm' => $this->getDataTransferForm($request)
		))->renderWith(array('DNRoot_createsnapshot', 'DNRoot'));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function uploadsnapshot(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		return $this->customise(array(
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
			'UploadLimit' => $maxSize = File::format_size(min(
				File::ini2bytes(ini_get('upload_max_filesize')),
				File::ini2bytes(ini_get('post_max_size'))
			)),
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
	}

	/**
	 * Construct the upload form.
	 *
	 * @param SS_HTTPRequest $request
	 * @return Form
	 */
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		// Framing an environment as a "group of people with download access"
		// makes more sense to the user here, while still allowing us to enforce
		// environment specific restrictions on downloading the file later on.
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {return $item->canUploadArchive();});
		$envsMap = array();
		foreach($envs as $env) {
			$envsMap[$env->ID] = $env->Name;
		}

		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));

		$form = new Form(
			$this,
			'UploadSnapshotForm',
			new FieldList(
				$fileField,
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
			),
			new FieldList(
				$action = new FormAction('doUploadSnapshot', "Upload File")
			),
			new RequiredFields('ArchiveFile')
		);
		$action->addExtraClass('btn');
		$form->disableSecurityToken();
		$form->addExtraClass('fields-wide');
		// Tweak the action so it plays well with our fake URL structure.
		$form->setFormAction($project->Link().'/UploadSnapshotForm');

		return $form;
	}

	public function doUploadSnapshot($data, $form) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($this->getRequest()->latestParam('Project')) . "' not found.", 404);
		}

		$validEnvs = $project->DNEnvironmentList()
			->filterByCallback(function($item) {
				return $item->canUploadArchive();
			});

		// Validate $data['EnvironmentID'] by checking against $validEnvs.
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
		if(!$environment) {
			throw new LogicException('Invalid environment');
		}

		// Validate mode.
		if(!in_array($data['Mode'], array('all', 'assets', 'db'))) {
			throw new LogicException('Invalid mode');
		}

		$dataArchive = DNDataArchive::create(array(
			'AuthorID' => Member::currentUserID(),
			'EnvironmentID' => $data['EnvironmentID'],
			'IsManualUpload' => true,
		));
		// needs an ID and transfer to determine upload path
		$dataArchive->write();
		$dataTransfer = DNDataTransfer::create(array(
			'AuthorID' => Member::currentUserID(),
			'Mode' => $data['Mode'],
			'Origin' => 'ManualUpload',
			'EnvironmentID' => $data['EnvironmentID']
		));
		$dataTransfer->write();
		$dataArchive->DataTransfers()->add($dataTransfer);
		$form->saveInto($dataArchive);
		$dataArchive->write();
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;

		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
			$process->run();
			$dataTransfer->delete();
			$dataArchive->delete();
		};

		// extract the sspak contents so we can inspect them
		try {
			$dataArchive->extractArchive($workingDir);
		} catch(Exception $e) {
			$cleanupFn();
			$form->sessionMessage(
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
				'bad'
			);
			return $this->redirectBack();
		}

		// validate that the sspak contents match the declared contents
		$result = $dataArchive->validateArchiveContents();
		if(!$result->valid()) {
			$cleanupFn();
			$form->sessionMessage($result->message(), 'bad');
			return $this->redirectBack();
		}

		// fix file permissions of extracted sspak files then re-build the sspak
		try {
			$dataArchive->fixArchivePermissions($workingDir);
			$dataArchive->setArchiveFromFiles($workingDir);
		} catch(Exception $e) {
			$cleanupFn();
			$form->sessionMessage(
				'There was a problem processing your snapshot. Please try uploading again',
				'bad'
			);
			return $this->redirectBack();
		}

		// cleanup any extracted sspak contents lying around
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
		$process->run();

		return $this->customise(array(
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
			'DataArchive' => $dataArchive,
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
			'BackURL' => $project->Link('snapshots')
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
	}

	/**
	 * @param SS_HTTPRequest $request
	 * @return Form
	 */
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		// Framing an environment as a "group of people with download access"
		// makes more sense to the user here, while still allowing us to enforce
		// environment specific restrictions on downloading the file later on.
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {return $item->canUploadArchive();});
		$envsMap = array();
		foreach($envs as $env) {
			$envsMap[$env->ID] = $env->Name;
		}

		$form = new Form(
			$this,
			'PostSnapshotForm',
			new FieldList(
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
			),
			new FieldList(
				$action = new FormAction('doPostSnapshot', "Submit request")
			),
			new RequiredFields('File')
		);
		$action->addExtraClass('btn');
		$form->disableSecurityToken();
		$form->addExtraClass('fields-wide');
		// Tweak the action so it plays well with our fake URL structure.
		$form->setFormAction($project->Link().'/PostSnapshotForm');

		return $form;
	}

	public function doPostSnapshot($data, $form) {
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($this->getRequest()->latestParam('Project')) . "' not found.", 404);
		}

		$validEnvs = $project->DNEnvironmentList()
			->filterByCallback(function($item) {return $item->canUploadArchive();});

		// Validate $data['EnvironmentID'] by checking against $validEnvs.
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
		if(!$environment) {
			throw new LogicException('Invalid environment');
		}

		$dataArchive = DNDataArchive::create(array(
			'UploadToken' => DNDataArchive::generate_upload_token(),
		));
		$form->saveInto($dataArchive);
		$dataArchive->write();

		return $this->redirect(Controller::join_links(
			$project->Link(),
			'postsnapshotsuccess',
			$dataArchive->ID
		));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function snapshotslog(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		return $this->customise(array(
			'Title' => 'Data Snapshots Log',
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
		))->renderWith(array('DNRoot_snapshotslog', 'DNRoot'));
	}

	/**
	 * @param  SS_HTTPRequest $request [description]
	 */
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
		if(!$dataArchive) {
			return new SS_HTTPResponse("Archive not found.", 404);
		}

		if(!$dataArchive->canRestore()) {
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
		}

		return $this->customise(array(
			'Title' => 'How to send us your Data Snapshot by post',
			'Project' => $project,
			'CurrentProject' => $project,
			'DataArchive' => $dataArchive,
			'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
			'BackURL' => $project->Link(),
		))->renderWith(array('DNRoot_postsnapshotsuccess', 'DNRoot'));
	}

	/**
	 *
	 * @param SS_HTTPRequest $request
	 * @return \SS_HTTPResponse
	 */
	public function project(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		return $this->customise(array(
			'Project' => $project,
			'CurrentProject' => $project,
			'ProjectOverview' => 1,
			'DataTransferForm' => $this->getDataTransferForm($request)
		))->renderWith(array('DNRoot_project', 'DNRoot'));
	}

	/**
	 *
	 * @param SS_HTTPRequest $request
	 * @return \SS_HTTPResponse
	 */
	public function branch(SS_HTTPRequest $request) {
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . $request->latestParam('Project') . "' not found.", 404);
		}

		$branchName = $request->getVar('name');
		$branch = $project->DNBranchList()->byName($branchName);
		if(!$branch) {
			return new SS_HTTPResponse("Branch '" . $branchName . "' not found.", 404);
		}<commit_msg>Better 404 response for branch not found<commit_after><?php
use \Symfony\Component\Process\Process;

/**
 * God controller for the deploynaut interface
 *
 * @package deploynaut
 * @subpackage control
 */
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {

	/**
	 * Access permission code
	 */
	const DEPLOYNAUT_ACCESS = 'DEPLOYNAUT_ACCESS';

	/**
	 * Bypass pipeline permission code
	 */
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';

	/**
	 * Allow dryrun of pipelines
	 */
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';

	/**
	 *
	 * @var array
	 */
	public static $allowed_actions = array(
		'projects',
		'update',
		'project',
		'branch',
		'environment',
		'abortpipeline',
		'pipeline',
		'pipelinelog',
		'metrics',
		'getDeployForm',
		'deploy',
		'deploylog',
		'getDataTransferForm',
		'transfer',
		'transferlog',
		'snapshots',
		'createsnapshot',
		'snapshotslog',
		'uploadsnapshot',
		'getUploadSnapshotForm',
		'getPostSnapshotForm',
		'getDataTransferRestoreForm',
		'getDeleteForm',
		'getMoveForm',
		'restoresnapshot',
		'deletesnapshot',
		'movesnapshot',
		'postsnapshotsuccess',
	);

	/**
	 * URL handlers pretending that we have a deep URL structure.
	 */
	public static $url_handlers = array(
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
		'project/$Project/DeleteForm' => 'getDeleteForm',
		'project/$Project/MoveForm' => 'getMoveForm',
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
		'project/$Project/environment/$Environment/metrics' => 'metrics',
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
		'project/$Project/transfer/$Identifier' => 'transfer',
		'project/$Project/environment/$Environment' => 'environment',
		'project/$Project/branch' => 'branch',
		'project/$Project/build/$Build' => 'build',
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
		'project/$Project/update' => 'update',
		'project/$Project/snapshots' => 'snapshots',
		'project/$Project/createsnapshot' => 'createsnapshot',
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
		'project/$Project/snapshotslog' => 'snapshotslog',
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
		'project/$Project' => 'project',
		'projects' => 'projects',
	);

	/**
	 *
	 * @var DNData
	 */
	protected $data;

	/**
	 * Check for feature flags:
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
	 *
	 * @return boolean
	 */
	public static function FlagSnapshotsEnabled() {
		if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) return true;
		if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
			$member = Member::currentUser();
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) return true;
		}
		return false;
	}

	/**
	 *
	 */
	public function init() {
		parent::init();

		if(!Permission::check(self::DEPLOYNAUT_ACCESS)) {
			return Security::permissionFailure();
		}

		parent::init();
		Requirements::combine_files(
			'deploynaut.js',
			array(
				THIRDPARTY_DIR . '/jquery/jquery.js',
				'deploynaut/javascript/bootstrap.js',
				'deploynaut/javascript/deploynaut.js',
				'deploynaut/javascript/bootstrap.file-input.js',

			)
		);

		Requirements::css(FRAMEWORK_ADMIN_DIR .'/thirdparty/chosen/chosen/chosen.css');
	}

	/**
	 *
	 * @return string
	 */
	public function Link() {
		return "naut/";
	}

	/**
	 * Actions
	 *
	 * @return \SS_HTTPResponse
	 */
	public function index(SS_HTTPRequest $request) {
		return $this->redirect($this->Link() . 'projects/');
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function projects(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
		return $this->customise(array(
			'Title' => 'Projects',
			'CurrentProject' => $this->getCurrentProject(),
		))->renderWith(array('DNRoot_projects', 'DNRoot'));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function snapshots(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		return $this->customise(array(
			'Title' => 'Data Snapshots',
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
		))->renderWith(array('DNRoot_snapshots', 'DNRoot'));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function createsnapshot(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canBackup()) {
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
		}

		return $this->customise(array(
			'Title' => 'Create Data Snapshot',
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
			'DataTransferForm' => $this->getDataTransferForm($request)
		))->renderWith(array('DNRoot_createsnapshot', 'DNRoot'));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function uploadsnapshot(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		return $this->customise(array(
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
			'UploadLimit' => $maxSize = File::format_size(min(
				File::ini2bytes(ini_get('upload_max_filesize')),
				File::ini2bytes(ini_get('post_max_size'))
			)),
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
	}

	/**
	 * Construct the upload form.
	 *
	 * @param SS_HTTPRequest $request
	 * @return Form
	 */
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		// Framing an environment as a "group of people with download access"
		// makes more sense to the user here, while still allowing us to enforce
		// environment specific restrictions on downloading the file later on.
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {return $item->canUploadArchive();});
		$envsMap = array();
		foreach($envs as $env) {
			$envsMap[$env->ID] = $env->Name;
		}

		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));

		$form = new Form(
			$this,
			'UploadSnapshotForm',
			new FieldList(
				$fileField,
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
			),
			new FieldList(
				$action = new FormAction('doUploadSnapshot', "Upload File")
			),
			new RequiredFields('ArchiveFile')
		);
		$action->addExtraClass('btn');
		$form->disableSecurityToken();
		$form->addExtraClass('fields-wide');
		// Tweak the action so it plays well with our fake URL structure.
		$form->setFormAction($project->Link().'/UploadSnapshotForm');

		return $form;
	}

	public function doUploadSnapshot($data, $form) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($this->getRequest()->latestParam('Project')) . "' not found.", 404);
		}

		$validEnvs = $project->DNEnvironmentList()
			->filterByCallback(function($item) {
				return $item->canUploadArchive();
			});

		// Validate $data['EnvironmentID'] by checking against $validEnvs.
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
		if(!$environment) {
			throw new LogicException('Invalid environment');
		}

		// Validate mode.
		if(!in_array($data['Mode'], array('all', 'assets', 'db'))) {
			throw new LogicException('Invalid mode');
		}

		$dataArchive = DNDataArchive::create(array(
			'AuthorID' => Member::currentUserID(),
			'EnvironmentID' => $data['EnvironmentID'],
			'IsManualUpload' => true,
		));
		// needs an ID and transfer to determine upload path
		$dataArchive->write();
		$dataTransfer = DNDataTransfer::create(array(
			'AuthorID' => Member::currentUserID(),
			'Mode' => $data['Mode'],
			'Origin' => 'ManualUpload',
			'EnvironmentID' => $data['EnvironmentID']
		));
		$dataTransfer->write();
		$dataArchive->DataTransfers()->add($dataTransfer);
		$form->saveInto($dataArchive);
		$dataArchive->write();
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;

		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
			$process->run();
			$dataTransfer->delete();
			$dataArchive->delete();
		};

		// extract the sspak contents so we can inspect them
		try {
			$dataArchive->extractArchive($workingDir);
		} catch(Exception $e) {
			$cleanupFn();
			$form->sessionMessage(
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
				'bad'
			);
			return $this->redirectBack();
		}

		// validate that the sspak contents match the declared contents
		$result = $dataArchive->validateArchiveContents();
		if(!$result->valid()) {
			$cleanupFn();
			$form->sessionMessage($result->message(), 'bad');
			return $this->redirectBack();
		}

		// fix file permissions of extracted sspak files then re-build the sspak
		try {
			$dataArchive->fixArchivePermissions($workingDir);
			$dataArchive->setArchiveFromFiles($workingDir);
		} catch(Exception $e) {
			$cleanupFn();
			$form->sessionMessage(
				'There was a problem processing your snapshot. Please try uploading again',
				'bad'
			);
			return $this->redirectBack();
		}

		// cleanup any extracted sspak contents lying around
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
		$process->run();

		return $this->customise(array(
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
			'DataArchive' => $dataArchive,
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
			'BackURL' => $project->Link('snapshots')
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
	}

	/**
	 * @param SS_HTTPRequest $request
	 * @return Form
	 */
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		// Framing an environment as a "group of people with download access"
		// makes more sense to the user here, while still allowing us to enforce
		// environment specific restrictions on downloading the file later on.
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {return $item->canUploadArchive();});
		$envsMap = array();
		foreach($envs as $env) {
			$envsMap[$env->ID] = $env->Name;
		}

		$form = new Form(
			$this,
			'PostSnapshotForm',
			new FieldList(
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
			),
			new FieldList(
				$action = new FormAction('doPostSnapshot', "Submit request")
			),
			new RequiredFields('File')
		);
		$action->addExtraClass('btn');
		$form->disableSecurityToken();
		$form->addExtraClass('fields-wide');
		// Tweak the action so it plays well with our fake URL structure.
		$form->setFormAction($project->Link().'/PostSnapshotForm');

		return $form;
	}

	public function doPostSnapshot($data, $form) {
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($this->getRequest()->latestParam('Project')) . "' not found.", 404);
		}

		$validEnvs = $project->DNEnvironmentList()
			->filterByCallback(function($item) {return $item->canUploadArchive();});

		// Validate $data['EnvironmentID'] by checking against $validEnvs.
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
		if(!$environment) {
			throw new LogicException('Invalid environment');
		}

		$dataArchive = DNDataArchive::create(array(
			'UploadToken' => DNDataArchive::generate_upload_token(),
		));
		$form->saveInto($dataArchive);
		$dataArchive->write();

		return $this->redirect(Controller::join_links(
			$project->Link(),
			'postsnapshotsuccess',
			$dataArchive->ID
		));
	}

	/**
	 * Action
	 *
	 * @return string - HTML
	 */
	public function snapshotslog(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		return $this->customise(array(
			'Title' => 'Data Snapshots Log',
			'Project' => $project,
			'CurrentProject' => $project,
			'SnapshotsSection' => 1,
		))->renderWith(array('DNRoot_snapshotslog', 'DNRoot'));
	}

	/**
	 * @param  SS_HTTPRequest $request [description]
	 */
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		if(!$project->canUploadArchive()) {
			return new SS_HTTPResponse("Not allowed to upload", 401);
		}

		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
		if(!$dataArchive) {
			return new SS_HTTPResponse("Archive not found.", 404);
		}

		if(!$dataArchive->canRestore()) {
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
		}

		return $this->customise(array(
			'Title' => 'How to send us your Data Snapshot by post',
			'Project' => $project,
			'CurrentProject' => $project,
			'DataArchive' => $dataArchive,
			'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
			'BackURL' => $project->Link(),
		))->renderWith(array('DNRoot_postsnapshotsuccess', 'DNRoot'));
	}

	/**
	 *
	 * @param SS_HTTPRequest $request
	 * @return \SS_HTTPResponse
	 */
	public function project(SS_HTTPRequest $request) {
		// Performs canView permission check by limiting visible projects
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		return $this->customise(array(
			'Project' => $project,
			'CurrentProject' => $project,
			'ProjectOverview' => 1,
			'DataTransferForm' => $this->getDataTransferForm($request)
		))->renderWith(array('DNRoot_project', 'DNRoot'));
	}

	/**
	 *
	 * @param SS_HTTPRequest $request
	 * @return \SS_HTTPResponse
	 */
	public function branch(SS_HTTPRequest $request) {
		$project = $this->getCurrentProject();
		if(!$project) {
			return new SS_HTTPResponse("Project '" . Convert::raw2xml($request->latestParam('Project')) . "' not found.", 404);
		}

		$branchName = $request->getVar('name');
		$branch = $project->DNBranchList()->byName($branchName);
		if(!$branch) {
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
		}
{'fraction_non_alphanumeric': 0.16204508535438455, 'fraction_numerical': 0.00453133460628687, 'mean_word_length': 4.701056051990252, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 399, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 22}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1644787', 'n_tokens_mistral': 12272, 'n_tokens_neox': 10822, 'n_words': 2246}