Completed
Push — master ( e01846...7c0007 )
by Damian
35s
created

CampaignAdmin::DetailEditForm()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
1
<?php
2
3
/**
4
 * Campaign section of the CMS
5
 *
6
 * @package framework
7
 * @subpackage admin
8
 */
9
class CampaignAdmin extends LeftAndMain implements PermissionProvider {
10
11
	private static $allowed_actions = [
12
		'set',
13
		'sets',
14
		'schema',
15
		'DetailEditForm',
16
		'readCampaigns',
17
		'readCampaign',
18
		'deleteCampaign',
19
		'publishCampaign',
20
	];
21
22
	private static $menu_priority = 11;
23
24
	private static $menu_title = 'Campaigns';
25
26
	private static $tree_class = 'ChangeSet';
27
28
	private static $url_handlers = [
29
		'GET sets' => 'readCampaigns',
30
		'POST set/$ID/publish' => 'publishCampaign',
31
		'GET set/$ID/$Name' => 'readCampaign',
32
		'DELETE set/$ID' => 'deleteCampaign',
33
	];
34
35
	private static $url_segment = 'campaigns';
36
37
	/**
38
	 * Size of thumbnail width
39
	 *
40
	 * @config
41
	 * @var int
42
	 */
43
	private static $thumbnail_width = 64;
44
45
	/**
46
	 * Size of thumbnail height
47
	 *
48
	 * @config
49
	 * @var int
50
	 */
51
	private static $thumbnail_height = 64;
52
53
	public function getClientConfig() {
54
		return array_merge(parent::getClientConfig(), [
55
			'form' => [
56
				// TODO Use schemaUrl instead
57
				'EditForm' => [
58
					'schemaUrl' => $this->Link('schema/EditForm')
59
				],
60
				'DetailEditForm' => [
61
					'schemaUrl' => $this->Link('schema/DetailEditForm')
62
				],
63
			],
64
			'campaignViewRoute' => $this->Link() . ':type?/:id?/:view?',
65
			'itemListViewEndpoint' => $this->Link() . 'set/:id/show',
66
			'publishEndpoint' => [
67
				'url' => $this->Link() . 'set/:id/publish',
68
				'method' => 'post'
69
			]
70
		]);
71
	}
72
73
	public function schema($request) {
74
		// TODO Hardcoding schema until we can get GridField to generate a schema dynamically
75
		$json = <<<JSON
76
{
77
	"id": "Form_EditForm",
78
	"schema": {
79
		"name": "EditForm",
80
		"id": "Form_EditForm",
81
		"action": "schema",
82
		"method": "GET",
83
		"schema_url": "admin\/campaigns\/schema\/EditForm",
84
		"attributes": {
85
			"id": "Form_EditForm",
86
			"action": "admin\/campaigns\/EditForm",
87
			"method": "POST",
88
			"enctype": "multipart\/form-data",
89
			"target": null
90
		},
91
		"data": [],
92
		"fields": [{
93
			"name": "ID",
94
			"id": "Form_EditForm_ID",
95
			"type": "Hidden",
96
			"component": null,
97
			"holder_id": null,
98
			"title": false,
99
			"source": null,
100
			"extraClass": "hidden nolabel",
101
			"description": null,
102
			"rightTitle": null,
103
			"leftTitle": null,
104
			"readOnly": false,
105
			"disabled": false,
106
			"customValidationMessage": "",
107
			"attributes": [],
108
			"data": []
109
		}, {
110
			"name": "ChangeSets",
111
			"id": "Form_EditForm_ChangeSets",
112
			"type": "Custom",
113
			"component": "GridField",
114
			"holder_id": null,
115
			"title": "Campaigns",
116
			"source": null,
117
			"extraClass": null,
118
			"description": null,
119
			"rightTitle": null,
120
			"leftTitle": null,
121
			"readOnly": false,
122
			"disabled": false,
123
			"customValidationMessage": "",
124
			"attributes": [],
125
			"data": {
126
				"recordType": "ChangeSet",
127
				"collectionReadEndpoint": {
128
					"url": "admin\/campaigns\/sets",
129
					"method": "GET"
130
				},
131
				"itemReadEndpoint": {
132
					"url": "admin\/campaigns\/set\/:id",
133
					"method": "GET"
134
				},
135
				"itemUpdateEndpoint": {
136
					"url": "admin\/campaigns\/set\/:id",
137
					"method": "PUT"
138
				},
139
				"itemCreateEndpoint": {
140
					"url": "admin\/campaigns\/set\/:id",
141
					"method": "POST"
142
				},
143
				"itemDeleteEndpoint": {
144
					"url": "admin\/campaigns\/set\/:id",
145
					"method": "DELETE"
146
				},
147
				"editFormSchemaEndpoint": "admin\/campaigns\/schema\/DetailEditForm",
148
				"columns": [
149
					{"name": "Title", "field": "Name"},
150
					{"name": "Changes", "field": "_embedded.ChangeSetItems.length"},
151
					{"name": "Description", "field": "Description"}
152
				]
153
			}
154
		}, {
155
			"name": "SecurityID",
156
			"id": "Form_EditForm_SecurityID",
157
			"type": "Hidden",
158
			"component": null,
159
			"holder_id": null,
160
			"title": "Security ID",
161
			"source": null,
162
			"extraClass": "hidden",
163
			"description": null,
164
			"rightTitle": null,
165
			"leftTitle": null,
166
			"readOnly": false,
167
			"disabled": false,
168
			"customValidationMessage": "",
169
			"attributes": [],
170
			"data": []
171
		}],
172
		"actions": []
173
	}
174
}
175
JSON;
176
177
		$formName = $request->param('ID');
178
		if($formName == 'EditForm') {
179
			$response = $this->getResponse();
180
			$response->addHeader('Content-Type', 'application/json');
181
			$response->setBody($json);
182
			return $response;
183
		} else {
184
			return parent::schema($request);
185
		}
186
	}
187
188
	/**
189
	 * REST endpoint to get a list of campaigns.
190
	 *
191
	 * @param SS_HTTPRequest $request
192
	 *
193
	 * @return SS_HTTPResponse
194
	 */
195
	public function readCampaigns(SS_HTTPRequest $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
196
		$response = new SS_HTTPResponse();
197
		$response->addHeader('Content-Type', 'application/json');
198
		$hal = $this->getListResource();
199
		$response->setBody(Convert::array2json($hal));
200
		return $response;
201
	}
202
203
	/**
204
	 * Get list contained as a hal wrapper
205
	 *
206
	 * @return array
207
	 */
208
	protected function getListResource() {
209
		$items = $this->getListItems();
210
		$count = $items->count();
211
		$hal = [
212
			'count' => $count,
213
			'total' => $count,
214
			'_links' => [
215
				'self' => [
216
					'href' => $this->Link('items')
217
				]
218
			],
219
			'_embedded' => ['ChangeSets' => []]
220
		];
221
		foreach($items as $item) {
222
			/** @var ChangeSet $item */
223
			$resource = $this->getChangeSetResource($item);
224
			$hal['_embedded']['ChangeSets'][] = $resource;
225
		}
226
		return $hal;
227
	}
228
229
	/**
230
	 * Build item resource from a changeset
231
	 *
232
	 * @param ChangeSet $changeSet
233
	 * @return array
234
	 */
235
	protected function getChangeSetResource(ChangeSet $changeSet) {
236
		$hal = [
237
			'_links' => [
238
				'self' => [
239
					'href' => $this->SetLink($changeSet->ID)
240
				]
241
			],
242
			'ID' => $changeSet->ID,
243
			'Name' => $changeSet->Name,
244
			'Description' => $changeSet->getDescription(),
245
			'Created' => $changeSet->Created,
246
			'LastEdited' => $changeSet->LastEdited,
247
			'State' => $changeSet->State,
248
			'canEdit' => $changeSet->canEdit(),
249
			'canPublish' => $changeSet->canPublish(),
250
			'_embedded' => ['ChangeSetItems' => []]
251
		];
252
		foreach($changeSet->Changes() as $changeSetItem) {
253
			if(!$changeSetItem) {
254
				continue;
255
			}
256
257
			/** @var ChangesetItem $changeSetItem */
258
			$resource = $this->getChangeSetItemResource($changeSetItem);
259
			$hal['_embedded']['ChangeSetItems'][] = $resource;
260
		}
261
		return $hal;
262
	}
263
264
	/**
265
	 * Build item resource from a changesetitem
266
	 *
267
	 * @param ChangeSetItem $changeSetItem
268
	 * @return array
269
	 */
270
	protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) {
271
		$baseClass = ClassInfo::baseDataClass($changeSetItem->ObjectClass);
272
		$baseSingleton = DataObject::singleton($baseClass);
273
		$thumbnailWidth = (int)$this->config()->thumbnail_width;
274
		$thumbnailHeight = (int)$this->config()->thumbnail_height;
275
		$hal = [
276
			'_links' => [
277
				'self' => [
278
					'href' => $this->ItemLink($changeSetItem->ID)
279
				]
280
			],
281
			'ID' => $changeSetItem->ID,
282
			'Created' => $changeSetItem->Created,
283
			'LastEdited' => $changeSetItem->LastEdited,
284
			'Title' => $changeSetItem->getTitle(),
285
			'ChangeType' => $changeSetItem->getChangeType(),
286
			'Added' => $changeSetItem->Added,
287
			'ObjectClass' => $changeSetItem->ObjectClass,
288
			'ObjectID' => $changeSetItem->ObjectID,
289
			'BaseClass' => $baseClass,
290
			'Singular' => $baseSingleton->i18n_singular_name(),
291
			'Plural' => $baseSingleton->i18n_plural_name(),
292
			'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight),
293
		];
294
		// Depending on whether the object was added implicitly or explicitly, set
295
		// other related objects.
296
		if($changeSetItem->Added === ChangeSetItem::IMPLICITLY) {
297
			$referencedItems = $changeSetItem->ReferencedBy();
298
			$referencedBy = [];
299
			foreach($referencedItems as $referencedItem) {
300
				$referencedBy[] = [
301
					'href' => $this->SetLink($referencedItem->ID)
302
				];
303
			}
304
			if($referencedBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $referencedBy of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
305
				$hal['_links']['referenced_by'] = $referencedBy;
306
			}
307
		}
308
309
		return $hal;
310
	}
311
312
	/**
313
	 * Gets viewable list of campaigns
314
	 *
315
	 * @return SS_List
316
	 */
317
	protected function getListItems() {
318
		return ChangeSet::get()
319
			->filter('State', ChangeSet::STATE_OPEN)
320
			->filterByCallback(function($item) {
321
				return ($item->canView());
322
			});
323
	}
324
325
326
	/**
327
	 * REST endpoint to get a campaign.
328
	 *
329
	 * @param SS_HTTPRequest $request
330
	 *
331
	 * @return SS_HTTPResponse
332
	 */
333
	public function readCampaign(SS_HTTPRequest $request) {
334
		$response = new SS_HTTPResponse();
335
336
		if ($request->getHeader('Accept') == 'text/json') {
337
			$response->addHeader('Content-Type', 'application/json');
338
			if (!$request->param('Name')) {
339
				return (new SS_HTTPResponse(null, 400));
340
			}
341
342
			$changeSet = ChangeSet::get()->byId($request->param('ID'));
343
			if(!$changeSet) {
344
				return (new SS_HTTPResponse(null, 404));
345
			}
346
347
			if(!$changeSet->canView()) {
348
				return (new SS_HTTPResponse(null, 403));
349
			}
350
351
			$body = Convert::raw2json($this->getChangeSetResource($changeSet));
0 ignored issues
show
Compatibility introduced by
$changeSet of type object<DataObject> is not a sub-type of object<ChangeSet>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
352
			return (new SS_HTTPResponse($body, 200))
353
				->addHeader('Content-Type', 'application/json');
354
		} else {
355
			return $this->index($request);
356
		}
357
	}
358
359
	/**
360
	 * REST endpoint to delete a campaign.
361
	 *
362
	 * @param SS_HTTPRequest $request
363
	 *
364
	 * @return SS_HTTPResponse
365
	 */
366
	public function deleteCampaign(SS_HTTPRequest $request) {
367
		$id = $request->param('ID');
368
		if (!$id || !is_numeric($id)) {
369
			return (new SS_HTTPResponse(null, 400));
370
		}
371
372
		$record = ChangeSet::get()->byID($id);
373
		if(!$record) {
374
			return (new SS_HTTPResponse(null, 404));
375
		}
376
377
		if(!$record->canDelete()) {
378
			return (new SS_HTTPResponse(null, 403));
379
		}
380
381
		$record->delete();
382
383
		return (new SS_HTTPResponse(null, 204));
384
	}
385
386
	/**
387
	 * REST endpoint to publish a {@link ChangeSet} and all of its items.
388
	 *
389
	 * @param SS_HTTPRequest $request
390
	 *
391
	 * @return SS_HTTPResponse
392
	 */
393
	public function publishCampaign(SS_HTTPRequest $request) {
394
		// Protect against CSRF on destructive action
395
		if(!SecurityToken::inst()->checkRequest($request)) {
396
			return (new SS_HTTPResponse(null, 400));
397
		}
398
399
		$id = $request->param('ID');
400
		if(!$id || !is_numeric($id)) {
401
			return (new SS_HTTPResponse(null, 400));
402
		}
403
404
		$record = ChangeSet::get()->byID($id);
405
		if(!$record) {
406
			return (new SS_HTTPResponse(null, 404));
407
		}
408
409
		if(!$record->canPublish()) {
410
			return (new SS_HTTPResponse(null, 403));
411
		}
412
413
		try {
414
			$record->publish();
415
		} catch(LogicException $e) {
416
			return (new SS_HTTPResponse(json_encode(['status' => 'error', 'message' => $e->getMessage()]), 401))
417
				->addHeader('Content-Type', 'application/json');
418
		}
419
420
		return (new SS_HTTPResponse(
421
			Convert::raw2json($this->getChangeSetResource($record)),
0 ignored issues
show
Compatibility introduced by
$record of type object<DataObject> is not a sub-type of object<ChangeSet>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
422
			200
423
		))->addHeader('Content-Type', 'application/json');
424
	}
425
426
	/**
427
	 * Url handler for edit form
428
	 *
429
	 * @param SS_HTTPRequest $request
430
	 * @return Form
431
	 */
432
	public function DetailEditForm($request) {
433
		// Get ID either from posted back value, or url parameter
434
		$id = $request->param('ID') ?: $request->postVar('ID');
435
		return $this->getDetailEditForm($id);
436
	}
437
438
	/**
439
	 * @todo Use GridFieldDetailForm once it can handle structured data and form schemas
440
	 *
441
	 * @param int $id
442
	 * @return Form
443
	 */
444
	public function getDetailEditForm($id) {
445
		// Get record-specific fields
446
		$record = null;
447
		if($id) {
448
			$record = ChangeSet::get()->byId($id);
449
			if(!$record || !$record->canView()) {
450
				return null;
451
			}
452
		}
453
454
		if(!$record) {
455
			$record = ChangeSet::singleton();
456
		}
457
458
		$fields = $record->getCMSFields();
459
460
		// Add standard fields
461
		$fields->push(HiddenField::create('ID'));
462
		$form = Form::create(
463
			$this,
464
			'DetailEditForm',
465
			$fields,
466
			FieldList::create(
467
				FormAction::create('save', _t('CMSMain.SAVE', 'Save')),
468
				FormAction::create('cancel', _t('LeftAndMain.CANCEL', 'Cancel'))
469
			)
470
		);
471
		// Configure form to respond to validation errors with form schema
472
		// if requested via react.
473
		$form->setValidationResponseCallback(function() use ($form) {
474
			return $this->getSchemaResponse($form);
475
		});
476
477
		return $form;
478
	}
479
480
	/**
481
	 * Gets user-visible url to edit a specific {@see ChangeSet}
482
	 *
483
	 * @param $itemID
484
	 * @return string
485
	 */
486
	public function SetLink($itemID) {
487
		return Controller::join_links(
488
			$this->Link('set'),
489
			$itemID
490
		);
491
	}
492
493
	/**
494
	 * Gets user-visible url to edit a specific {@see ChangeSetItem}
495
	 *
496
	 * @param int $itemID
497
	 * @return string
498
	 */
499
	public function ItemLink($itemID) {
500
		return Controller::join_links(
501
			$this->Link('item'),
502
			$itemID
503
		);
504
	}
505
506
}
507