Completed
Pull Request — master (#622)
by Sean
03:17
created

DeployPlanDispatcher::getGitPrevDeploys()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 27
Code Lines 18

Duplication

Lines 23
Ratio 85.19 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 23
loc 27
rs 8.439
cc 5
eloc 18
nc 7
nop 1
1
<?php
2
3
4
class DeployPlanDispatcher extends Dispatcher {
5
6
	const ACTION_PLAN = 'plan';
7
8
	/**
9
	 * @var array
10
	 */
11
	private static $action_types = [
12
		self::ACTION_PLAN
13
	];
14
15
	/**
16
	 * @var array
17
	 */
18
	public static $allowed_actions = [
19
		'gitupdate',
20
		'gitrefs',
21
		'deploysummary',
22
		'deployhistorydata'
23
	];
24
25
	/**
26
	 * @var \DNProject
27
	 */
28
	protected $project = null;
29
30
	/**
31
	 * @var \DNEnvironment
32
	 */
33
	protected $environment = null;
34
35
	public function init() {
36
		parent::init();
37
38
		$this->project = $this->getCurrentProject();
39
40
		if(!$this->project) {
41
			return $this->project404Response();
42
		}
43
44
		// Performs canView permission check by limiting visible projects
45
		$this->environment = $this->getCurrentEnvironment($this->project);
46
		if(!$this->environment) {
47
			return $this->environment404Response();
48
		}
49
	}
50
51
	/**
52
	 * @return string
53
	 */
54
	public function Link() {
55
		return \Controller::join_links($this->environment->Link(), self::ACTION_PLAN);
56
	}
57
58
	/**
59
	 *
60
	 * @param \SS_HTTPRequest $request
61
	 *
62
	 * @return \HTMLText|\SS_HTTPResponse
63
	 */
64
	public function index(\SS_HTTPRequest $request) {
65
		$this->setCurrentActionType(self::ACTION_PLAN);
66
		return $this->customise([
67
			'Environment' => $this->environment
68
		])->renderWith(['Plan', 'DNRoot']);
69
	}
70
71
	/**
72
	 * @param SS_HTTPRequest $request
73
	 * @return SS_HTTPResponse
74
	 */
75
	public function gitupdate(SS_HTTPRequest $request) {
76
		switch($request->httpMethod()) {
77
			case 'POST':
78
				return $this->createFetch();
79
			case 'GET':
80
				return $this->getFetch($this->getRequest()->param('ID'));
81
			default:
82
				return $this->getAPIResponse(['message' => 'Method not allowed, requires POST or GET/{id}'], 405);
83
		}
84
	}
85
86
	/**
87
	 * @param SS_HTTPRequest $request
88
	 *
89
	 * @return string
90
	 */
91
	public function gitrefs(\SS_HTTPRequest $request) {
92
93
		$refs = [];
94
		$order = 0;
95
		$refs[] = [
96
			'id' => ++$order,
97
			'label' => "Branch version",
98
			"description" => "Deploy the latest version of a branch",
99
			"list" => $this->getGitBranches($this->project)
100
		];
101
102
		$refs[] = [
103
			'id' => ++$order,
104
			'label' => "Tag version",
105
			"description" => "Deploy a tagged release",
106
			"list" => $this->getGitTags($this->project)
107
		];
108
109
		// @todo: the original was a tree that was keyed by environment, the
110
		// front-end dropdown needs to be changed to support that. brrrr.
111
		$prevDeploys = [];
112
		foreach($this->getGitPrevDeploys($this->project) as $env) {
113
			foreach($env as $deploy) {
114
				$prevDeploys[] = $deploy;
115
			}
116
		}
117
		$refs[] = [
118
			'id' => ++$order,
119
			'label' => "Redeploy a release that was previously deployed (to any environment",
120
			"description" => "Deploy a previous release",
121
			"list" => $prevDeploys
122
		];
123
124
		return $this->getAPIResponse(['refs' => $refs], 200);
125
	}
126
127
	/**
128
	 * Generate the data structure used by the frontend component.
129
	 *
130
	 * @param string $name of the component
131
	 *
132
	 * @return array
133
	 */
134
	public function getModel($name) {
135
		return [
136
			'APIEndpoint' => Director::absoluteBaseURL().$this->Link()
137
		];
138
	}
139
140
	/**
141
	 * @param int $ID
142
	 * @return SS_HTTPResponse
143
	 */
144
	protected function getFetch($ID) {
145
		$ping = DNGitFetch::get()->byID($ID);
146
		if(!$ping) {
147
			return $this->getAPIResponse(['message' => 'Fetch not found'], 404);
148
		}
149
		$output = [
150
			'id' => $ID,
151
			'status' => $ping->ResqueStatus(),
152
			'message' => array_filter(explode(PHP_EOL, $ping->LogContent()))
153
		];
154
155
		return $this->getAPIResponse($output, 200);
156
	}
157
158
	/**
159
	 * @return SS_HTTPResponse
160
	 */
161 View Code Duplication
	protected function createFetch() {
0 ignored issues
show
Duplication introduced by
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...
162
		/** @var DNGitFetch $fetch */
163
		$fetch = DNGitFetch::create();
164
		$fetch->ProjectID = $this->project->ID;
165
		$fetch->write();
166
		$fetch->start();
167
168
		$location = Director::absoluteBaseURL() . $this->Link() . '/gitupdate/' . $fetch->ID;
169
		$output = array(
170
			'message' => 'Fetch queued as job ' . $fetch->ResqueToken,
171
			'href' => $location,
172
		);
173
174
		$response = $this->getAPIResponse($output, 201);
175
		$response->addHeader('Location', $location);
176
		return $response;
177
	}
178
179
	/**
180
	 * @return SS_HTTPResponse
181
	 */
182
	public function deployhistorydata(SS_HTTPRequest $request) {
183
		$data = [];
184
		foreach($this->DeployHistory() as $deployment) {
0 ignored issues
show
Bug introduced by
The expression $this->DeployHistory() of type object<PaginatedList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
185
			$data[] = [
186
				'CreatedDate' => $deployment->Created,
187
				'Branch' => $deployment->Branch,
188
				'Tags' => $deployment->getTags()->toArray(),
189
				'Changes' => $deployment->getDeploymentStrategy()->getChanges(),
190
				'CommitMessage' => $deployment->getCommitMessage(),
191
				'Deployer' => $deployment->Deployer()->getName(),
192
				'Approver' => $deployment->Approver()->getName(),
193
				'State' => $deployment->State,
194
			];
195
		}
196
		return $this->getAPIResponse(['history' => $data], 200);
197
	}
198
199
	/**
200
	 * @param SS_HTTPRequest $request
201
	 *
202
	 * @return SS_HTTPResponse
203
	 */
204
	public function deploysummary(SS_HTTPRequest $request) {
205
		if(!trim($request->getBody())) {
206
			return $this->getAPIResponse(['message' => 'no body was sent in the request'], 400);
207
		}
208
209
		$jsonBody = json_decode($request->getBody(), true);
210
		if(empty($jsonBody)) {
211
			return $this->getAPIResponse(['message' => 'request did not contain a parsable JSON payload'], 400);
212
		}
213
214
		$options = [
215
			'sha' => $jsonBody['sha']
216
		];
217
218
		$strategy = $this->environment->Backend()->planDeploy($this->environment, $options);
219
		$data = $strategy->toArray();
220
221
		$interface = $this->project->getRepositoryInterface();
222
		if($this->canCompareCodeVersions($interface, $data['changes'])) {
0 ignored issues
show
Bug introduced by
It seems like $interface defined by $this->project->getRepositoryInterface() on line 221 can be null; however, DeployPlanDispatcher::canCompareCodeVersions() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
223
			$compareurl = sprintf(
224
				'%s/compare/%s...%s',
225
				$interface->URL,
226
				$data['changes']['Code version']['from'],
227
				$data['changes']['Code version']['to']
228
			);
229
			$data['changes']['Code version']['compareUrl'] = $compareurl;
230
		}
231
232
		// Append json to response
233
		$token = SecurityToken::inst();
234
		$data['SecurityID'] = $token->getValue();
235
236
		$this->extend('updateDeploySummary', $data);
237
238
		return $this->getAPIResponse($data, 201);
239
	}
240
241
	/**
242
	 * @param $project
243
	 *
244
	 * @return array
245
	 */
246
	protected function getGitBranches($project) {
247
		$branches = [];
248
		foreach($project->DNBranchList() as $branch) {
249
			$branches[] = [
250
				'key' => $branch->SHA(),
251
				'value' => $branch->Name(),
252
			];
253
		}
254
		return $branches;
255
	}
256
257
	/**
258
	 * @param $project
259
	 *
260
	 * @return array
261
	 */
262
	protected function getGitTags($project) {
263
		$tags = [];
264
		foreach($project->DNTagList()->setLimit(null) as $tag) {
265
			$tags[] = [
266
				'key' => $tag->SHA(),
267
				'value' => $tag->Name(),
268
			];
269
		}
270
		return $tags;
271
	}
272
273
	/**
274
	 * @param $project
275
	 *
276
	 * @return array
277
	 */
278
	protected function getGitPrevDeploys($project) {
279
		$redeploy = [];
280 View Code Duplication
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
0 ignored issues
show
Duplication introduced by
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...
281
			$envName = $dnEnvironment->Name;
282
			$perEnvDeploys = [];
283
			foreach($dnEnvironment->DeployHistory() as $deploy) {
284
				$sha = $deploy->SHA;
285
286
				// Check if exists to make sure the newest deployment date is used.
287
				if(!isset($perEnvDeploys[$sha])) {
288
					$pastValue = sprintf(
289
						"%s (deployed %s)",
290
						substr($sha, 0, 8),
291
						$deploy->obj('LastEdited')->Ago()
292
					);
293
					$perEnvDeploys[$sha] = [
294
						'key' => $sha,
295
						'value' => $pastValue
296
					];
297
				}
298
			}
299
			if(!empty($perEnvDeploys)) {
300
				$redeploy[$envName] = array_values($perEnvDeploys);
301
			}
302
		}
303
		return $redeploy;
304
	}
305
306
	/**
307
	 * Return a simple response with a message
308
	 *
309
	 * @param array $output
310
	 * @param int $statusCode
311
	 * @return SS_HTTPResponse
312
	 */
313
	protected function getAPIResponse($output, $statusCode) {
314
		$output['status_code'] = $statusCode;
315
		$body = json_encode($output, JSON_PRETTY_PRINT);
316
		$response = $this->getResponse();
317
		$response->addHeader('Content-Type', 'application/json');
318
		$response->setBody($body);
319
		$response->setStatusCode($statusCode);
320
		return $response;
321
	}
322
323
	/**
324
	 * @param ArrayData $interface
325
	 * @param $changes
326
	 *
327
	 * @return bool
328
	 *
329
	 */
330
	protected function canCompareCodeVersions(\ArrayData $interface, $changes) {
331
		if(empty($changes['Code version'])) {
332
			return false;
333
		}
334
		$codeVersion = ['Code version'];
335
		if(empty($interface)) {
336
			return false;
337
		}
338
		if(empty($interface->URL)) {
339
			return false;
340
		}
341
		if(empty($codeVersion['from']) || empty($codeVersion['to'])) {
342
			return false;
343
		}
344
		if(strlen($codeVersion['from']) !== 40 || strlen($codeVersion['to']) !== 40) {
345
			return false;
346
		}
347
		return true;
348
	}
349
}
350