Completed
Pull Request — master (#622)
by Sean
05:37 queued 01:32
created

DeployPlanDispatcher::getAPIResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 1
eloc 8
nc 1
nop 2
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
				'State' => $deployment->State,
193
			];
194
		}
195
		return $this->getAPIResponse($data, 200);
196
	}
197
198
	/**
199
	 * @param SS_HTTPRequest $request
200
	 *
201
	 * @return SS_HTTPResponse
202
	 */
203
	public function deploysummary(SS_HTTPRequest $request) {
204
		if(!trim($request->getBody())) {
205
			return $this->getAPIResponse(['message' => 'no body was sent in the request'], 400);
206
		}
207
208
		$jsonBody = json_decode($request->getBody(), true);
209
		if(empty($jsonBody)) {
210
			return $this->getAPIResponse(['message' => 'request did not contain a parsable JSON payload'], 400);
211
		}
212
213
		$options = [
214
			'sha' => $jsonBody['sha']
215
		];
216
217
		$strategy = $this->environment->Backend()->planDeploy($this->environment, $options);
218
		$data = $strategy->toArray();
219
220
		$interface = $this->project->getRepositoryInterface();
221
		if($this->canCompareCodeVersions($interface, $data['changes'])) {
0 ignored issues
show
Bug introduced by
It seems like $interface defined by $this->project->getRepositoryInterface() on line 220 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...
222
			$compareurl = sprintf(
223
				'%s/compare/%s...%s',
224
				$interface->URL,
225
				$data['changes']['Code version']['from'],
226
				$data['changes']['Code version']['to']
227
			);
228
			$data['changes']['Code version']['compareUrl'] = $compareurl;
229
		}
230
231
		// Append json to response
232
		$token = SecurityToken::inst();
233
		$data['SecurityID'] = $token->getValue();
234
235
		$this->extend('updateDeploySummary', $data);
236
237
		return $this->getAPIResponse($data, 201);
238
	}
239
240
	/**
241
	 * @param $project
242
	 *
243
	 * @return array
244
	 */
245
	protected function getGitBranches($project) {
246
		$branches = [];
247
		foreach($project->DNBranchList() as $branch) {
248
			$branches[] = [
249
				'key' => $branch->SHA(),
250
				'value' => $branch->Name(),
251
			];
252
		}
253
		return $branches;
254
	}
255
256
	/**
257
	 * @param $project
258
	 *
259
	 * @return array
260
	 */
261
	protected function getGitTags($project) {
262
		$tags = [];
263
		foreach($project->DNTagList()->setLimit(null) as $tag) {
264
			$tags[] = [
265
				'key' => $tag->SHA(),
266
				'value' => $tag->Name(),
267
			];
268
		}
269
		return $tags;
270
	}
271
272
	/**
273
	 * @param $project
274
	 *
275
	 * @return array
276
	 */
277
	protected function getGitPrevDeploys($project) {
278
		$redeploy = [];
279 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...
280
			$envName = $dnEnvironment->Name;
281
			$perEnvDeploys = [];
282
			foreach($dnEnvironment->DeployHistory() as $deploy) {
283
				$sha = $deploy->SHA;
284
285
				// Check if exists to make sure the newest deployment date is used.
286
				if(!isset($perEnvDeploys[$sha])) {
287
					$pastValue = sprintf(
288
						"%s (deployed %s)",
289
						substr($sha, 0, 8),
290
						$deploy->obj('LastEdited')->Ago()
291
					);
292
					$perEnvDeploys[$sha] = [
293
						'key' => $sha,
294
						'value' => $pastValue
295
					];
296
				}
297
			}
298
			if(!empty($perEnvDeploys)) {
299
				$redeploy[$envName] = array_values($perEnvDeploys);
300
			}
301
		}
302
		return $redeploy;
303
	}
304
305
	/**
306
	 * Return a simple response with a message
307
	 *
308
	 * @param array $output
309
	 * @param int $statusCode
310
	 * @return SS_HTTPResponse
311
	 */
312
	protected function getAPIResponse($output, $statusCode) {
313
		$output['status_code'] = $statusCode;
314
		$body = json_encode($output, JSON_PRETTY_PRINT);
315
		$response = $this->getResponse();
316
		$response->addHeader('Content-Type', 'application/json');
317
		$response->setBody($body);
318
		$response->setStatusCode($statusCode);
319
		return $response;
320
	}
321
322
	/**
323
	 * @param ArrayData $interface
324
	 * @param $changes
325
	 *
326
	 * @return bool
327
	 *
328
	 */
329
	protected function canCompareCodeVersions(\ArrayData $interface, $changes) {
330
		if(empty($changes['Code version'])) {
331
			return false;
332
		}
333
		$codeVersion = ['Code version'];
334
		if(empty($interface)) {
335
			return false;
336
		}
337
		if(empty($interface->URL)) {
338
			return false;
339
		}
340
		if(empty($codeVersion['from']) || empty($codeVersion['to'])) {
341
			return false;
342
		}
343
		if(strlen($codeVersion['from']) !== 40 || strlen($codeVersion['to']) !== 40) {
344
			return false;
345
		}
346
		return true;
347
	}
348
}
349