Completed
Push — master ( 27ce71...012526 )
by Hamish
23s
created

CampaignAdmin::getListItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 5
c 1
b 1
f 0
nc 1
nop 0
dl 0
loc 8
rs 9.4285
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 = 3;
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
			'reactRoute' => true,
56
			'form' => [
57
				// TODO Use schemaUrl instead
58
				'EditForm' => [
59
					'schemaUrl' => $this->Link('schema/EditForm')
60
				],
61
				'DetailEditForm' => [
62
					'schemaUrl' => $this->Link('schema/DetailEditForm')
63
				],
64
			],
65
			'campaignViewRoute' => $this->Link() . ':type?/:id?/:view?',
66
			'itemListViewEndpoint' => $this->Link() . 'set/:id/show',
67
			'publishEndpoint' => [
68
				'url' => $this->Link() . 'set/:id/publish',
69
				'method' => 'post'
70
			],
71
			'treeClass' => $this->config()->tree_class
72
		]);
73
	}
74
75
	public function schema($request) {
76
		// TODO Hardcoding schema until we can get GridField to generate a schema dynamically
77
		$treeClassJS = Convert::raw2js($this->config()->tree_class);
78
		$json = <<<JSON
79
{
80
	"id": "Form_EditForm",
81
	"schema": {
82
		"name": "EditForm",
83
		"id": "Form_EditForm",
84
		"action": "schema",
85
		"method": "GET",
86
		"schema_url": "admin\/campaigns\/schema\/EditForm",
87
		"attributes": {
88
			"id": "Form_EditForm",
89
			"action": "admin\/campaigns\/EditForm",
90
			"method": "POST",
91
			"enctype": "multipart\/form-data",
92
			"target": null
93
		},
94
		"data": [],
95
		"fields": [{
96
			"name": "ID",
97
			"id": "Form_EditForm_ID",
98
			"type": "Hidden",
99
			"component": null,
100
			"holder_id": null,
101
			"title": false,
102
			"source": null,
103
			"extraClass": "hidden nolabel",
104
			"description": null,
105
			"rightTitle": null,
106
			"leftTitle": null,
107
			"readOnly": false,
108
			"disabled": false,
109
			"customValidationMessage": "",
110
			"attributes": [],
111
			"data": []
112
		}, {
113
			"name": "ChangeSets",
114
			"id": "Form_EditForm_ChangeSets",
115
			"type": "Custom",
116
			"component": "GridField",
117
			"holder_id": null,
118
			"title": "Campaigns",
119
			"source": null,
120
			"extraClass": null,
121
			"description": null,
122
			"rightTitle": null,
123
			"leftTitle": null,
124
			"readOnly": false,
125
			"disabled": false,
126
			"customValidationMessage": "",
127
			"attributes": [],
128
			"data": {
129
				"recordType": "{$treeClassJS}",
130
				"collectionReadEndpoint": {
131
					"url": "admin\/campaigns\/sets",
132
					"method": "GET"
133
				},
134
				"itemReadEndpoint": {
135
					"url": "admin\/campaigns\/set\/:id",
136
					"method": "GET"
137
				},
138
				"itemUpdateEndpoint": {
139
					"url": "admin\/campaigns\/set\/:id",
140
					"method": "PUT"
141
				},
142
				"itemCreateEndpoint": {
143
					"url": "admin\/campaigns\/set\/:id",
144
					"method": "POST"
145
				},
146
				"itemDeleteEndpoint": {
147
					"url": "admin\/campaigns\/set\/:id",
148
					"method": "DELETE"
149
				},
150
				"editFormSchemaEndpoint": "admin\/campaigns\/schema\/DetailEditForm",
151
				"columns": [
152
					{"name": "Title", "field": "Name"},
153
					{"name": "Changes", "field": "ChangesCount"},
154
					{"name": "Description", "field": "Description"}
155
				]
156
			}
157
		}, {
158
			"name": "SecurityID",
159
			"id": "Form_EditForm_SecurityID",
160
			"type": "Hidden",
161
			"component": null,
162
			"holder_id": null,
163
			"title": "Security ID",
164
			"source": null,
165
			"extraClass": "hidden",
166
			"description": null,
167
			"rightTitle": null,
168
			"leftTitle": null,
169
			"readOnly": false,
170
			"disabled": false,
171
			"customValidationMessage": "",
172
			"attributes": [],
173
			"data": []
174
		}],
175
		"actions": []
176
	}
177
}
178
JSON;
179
180
		$formName = $request->param('ID');
181
		if($formName == 'EditForm') {
182
			$response = $this->getResponse();
183
			$response->addHeader('Content-Type', 'application/json');
184
			$response->setBody($json);
185
			return $response;
186
		} else {
187
			return parent::schema($request);
188
		}
189
	}
190
191
	/**
192
	 * REST endpoint to get a list of campaigns.
193
	 *
194
	 * @return SS_HTTPResponse
195
	 */
196
	public function readCampaigns() {
197
		$response = new SS_HTTPResponse();
198
		$response->addHeader('Content-Type', 'application/json');
199
		$hal = $this->getListResource();
200
		$response->setBody(Convert::array2json($hal));
201
		return $response;
202
	}
203
204
	/**
205
	 * Get list contained as a hal wrapper
206
	 *
207
	 * @return array
208
	 */
209
	protected function getListResource() {
210
		$items = $this->getListItems();
211
		$count = $items->count();
212
		/** @var string $treeClass */
213
		$treeClass = $this->config()->tree_class;
0 ignored issues
show
Documentation introduced by
The property tree_class does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
214
		$hal = [
215
			'count' => $count,
216
			'total' => $count,
217
			'_links' => [
218
				'self' => [
219
					'href' => $this->Link('items')
220
				]
221
			],
222
			'_embedded' => [$treeClass => []]
223
		];
224
		foreach($items as $item) {
225
			/** @var ChangeSet $item */
226
			$resource = $this->getChangeSetResource($item);
227
			$hal['_embedded'][$treeClass][] = $resource;
228
		}
229
		return $hal;
230
	}
231
232
	/**
233
	 * Build item resource from a changeset
234
	 *
235
	 * @param ChangeSet $changeSet
236
	 * @return array
237
	 */
238
	protected function getChangeSetResource(ChangeSet $changeSet) {
239
		// Before presenting the changeset to the client,
240
		// synchronise it with new changes.
241
		$changeSet->sync();
242
		$hal = [
243
			'_links' => [
244
				'self' => [
245
					'href' => $this->SetLink($changeSet->ID)
246
				]
247
			],
248
			'ID' => $changeSet->ID,
249
			'Name' => $changeSet->Name,
250
			'Description' => $changeSet->getDescription(),
251
			'Created' => $changeSet->Created,
252
			'LastEdited' => $changeSet->LastEdited,
253
			'State' => $changeSet->State,
254
			'canEdit' => $changeSet->canEdit(),
255
			'canPublish' => $changeSet->canPublish(),
256
			'_embedded' => ['items' => []]
257
		];
258
		foreach($changeSet->Changes() as $changeSetItem) {
259
			if(!$changeSetItem) {
260
				continue;
261
			}
262
263
			/** @var ChangesetItem $changeSetItem */
264
			$resource = $this->getChangeSetItemResource($changeSetItem);
265
			$hal['_embedded']['items'][] = $resource;
266
		}
267
		$hal['ChangesCount'] = count($hal['_embedded']['items']);
268
		return $hal;
269
	}
270
271
	/**
272
	 * Build item resource from a changesetitem
273
	 *
274
	 * @param ChangeSetItem $changeSetItem
275
	 * @return array
276
	 */
277
	protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) {
278
		$baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass);
279
		$baseSingleton = DataObject::singleton($baseClass);
280
		$thumbnailWidth = (int)$this->config()->thumbnail_width;
281
		$thumbnailHeight = (int)$this->config()->thumbnail_height;
282
		$hal = [
283
			'_links' => [
284
				'self' => [
285
					'href' => $this->ItemLink($changeSetItem->ID)
286
				]
287
			],
288
			'ID' => $changeSetItem->ID,
289
			'Created' => $changeSetItem->Created,
290
			'LastEdited' => $changeSetItem->LastEdited,
291
			'Title' => $changeSetItem->getTitle(),
292
			'ChangeType' => $changeSetItem->getChangeType(),
293
			'Added' => $changeSetItem->Added,
294
			'ObjectClass' => $changeSetItem->ObjectClass,
295
			'ObjectID' => $changeSetItem->ObjectID,
296
			'BaseClass' => $baseClass,
297
			'Singular' => $baseSingleton->i18n_singular_name(),
298
			'Plural' => $baseSingleton->i18n_plural_name(),
299
			'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight),
300
		];
301
		// Get preview urls
302
		$previews = $changeSetItem->getPreviewLinks();
303
		if($previews) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $previews 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...
304
			$hal['_links']['preview'] = $previews;
305
		}
306
307
		// Get edit link
308
		$editLink = $changeSetItem->CMSEditLink();
309
		if($editLink) {
310
			$hal['_links']['edit'] = [
311
				'href' => $editLink,
312
			];
313
		}
314
315
		// Depending on whether the object was added implicitly or explicitly, set
316
		// other related objects.
317
		if($changeSetItem->Added === ChangeSetItem::IMPLICITLY) {
318
			$referencedItems = $changeSetItem->ReferencedBy();
319
			$referencedBy = [];
320
			foreach($referencedItems as $referencedItem) {
321
				$referencedBy[] = [
322
					'href' => $this->SetLink($referencedItem->ID)
323
				];
324
			}
325
			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...
326
				$hal['_links']['referenced_by'] = $referencedBy;
327
			}
328
		}
329
330
		return $hal;
331
	}
332
333
	/**
334
	 * Gets viewable list of campaigns
335
	 *
336
	 * @return SS_List
337
	 */
338
	protected function getListItems() {
339
		return ChangeSet::get()
340
			->filter('State', ChangeSet::STATE_OPEN)
341
			->filterByCallback(function($item) {
342
				/** @var ChangeSet $item */
343
				return ($item->canView());
344
			});
345
	}
346
347
348
	/**
349
	 * REST endpoint to get a campaign.
350
	 *
351
	 * @param SS_HTTPRequest $request
352
	 *
353
	 * @return SS_HTTPResponse
354
	 */
355
	public function readCampaign(SS_HTTPRequest $request) {
356
		$response = new SS_HTTPResponse();
357
358
		if ($request->getHeader('Accept') == 'text/json') {
359
			$response->addHeader('Content-Type', 'application/json');
360
			if (!$request->param('Name')) {
361
				return (new SS_HTTPResponse(null, 400));
362
			}
363
364
			/** @var ChangeSet $changeSet */
365
			$changeSet = ChangeSet::get()->byID($request->param('ID'));
366
			if(!$changeSet) {
367
				return (new SS_HTTPResponse(null, 404));
368
			}
369
370
			if(!$changeSet->canView()) {
371
				return (new SS_HTTPResponse(null, 403));
372
			}
373
374
			$body = Convert::raw2json($this->getChangeSetResource($changeSet));
375
			return (new SS_HTTPResponse($body, 200))
376
				->addHeader('Content-Type', 'application/json');
377
		} else {
378
			return $this->index($request);
379
		}
380
	}
381
382
	/**
383
	 * REST endpoint to delete a campaign.
384
	 *
385
	 * @param SS_HTTPRequest $request
386
	 *
387
	 * @return SS_HTTPResponse
388
	 */
389
	public function deleteCampaign(SS_HTTPRequest $request) {
390
		// Check security ID
391
		if (!SecurityToken::inst()->checkRequest($request)) {
392
			return new SS_HTTPResponse(null, 400);
393
		}
394
395
		$id = $request->param('ID');
396
		if (!$id || !is_numeric($id)) {
397
			return (new SS_HTTPResponse(null, 400));
398
		}
399
400
		$record = ChangeSet::get()->byID($id);
401
		if(!$record) {
402
			return (new SS_HTTPResponse(null, 404));
403
		}
404
405
		if(!$record->canDelete()) {
406
			return (new SS_HTTPResponse(null, 403));
407
		}
408
409
		$record->delete();
410
411
		return (new SS_HTTPResponse(null, 204));
412
	}
413
414
	/**
415
	 * REST endpoint to publish a {@link ChangeSet} and all of its items.
416
	 *
417
	 * @param SS_HTTPRequest $request
418
	 *
419
	 * @return SS_HTTPResponse
420
	 */
421
	public function publishCampaign(SS_HTTPRequest $request) {
422
		// Protect against CSRF on destructive action
423
		if(!SecurityToken::inst()->checkRequest($request)) {
424
			return (new SS_HTTPResponse(null, 400));
425
		}
426
427
		$id = $request->param('ID');
428
		if(!$id || !is_numeric($id)) {
429
			return (new SS_HTTPResponse(null, 400));
430
		}
431
432
		/** @var ChangeSet $record */
433
		$record = ChangeSet::get()->byID($id);
434
		if(!$record) {
435
			return (new SS_HTTPResponse(null, 404));
436
		}
437
438
		if(!$record->canPublish()) {
439
			return (new SS_HTTPResponse(null, 403));
440
		}
441
442
		try {
443
			$record->publish();
444
		} catch(LogicException $e) {
445
			return (new SS_HTTPResponse(json_encode(['status' => 'error', 'message' => $e->getMessage()]), 401))
446
				->addHeader('Content-Type', 'application/json');
447
		}
448
449
		return (new SS_HTTPResponse(
450
			Convert::raw2json($this->getChangeSetResource($record)),
451
			200
452
		))->addHeader('Content-Type', 'application/json');
453
	}
454
455
	/**
456
	 * Url handler for edit form
457
	 *
458
	 * @param SS_HTTPRequest $request
459
	 * @return Form
460
	 */
461
	public function DetailEditForm($request) {
462
		// Get ID either from posted back value, or url parameter
463
		$id = $request->param('ID') ?: $request->postVar('ID');
464
		return $this->getDetailEditForm($id);
465
	}
466
467
	/**
468
	 * @todo Use GridFieldDetailForm once it can handle structured data and form schemas
469
	 *
470
	 * @param int $id
471
	 * @return Form
472
	 */
473
	public function getDetailEditForm($id) {
474
		// Get record-specific fields
475
		$record = null;
476
		if($id) {
477
			$record = ChangeSet::get()->byID($id);
478
			if(!$record || !$record->canView()) {
479
				return null;
480
			}
481
		}
482
483
		if(!$record) {
484
			$record = ChangeSet::singleton();
485
		}
486
487
		$fields = $record->getCMSFields();
488
489
		// Add standard fields
490
		$fields->push(HiddenField::create('ID'));
491
		$form = Form::create(
492
			$this,
493
			'DetailEditForm',
494
			$fields,
495
			FieldList::create(
496
				FormAction::create('save', _t('CMSMain.SAVE', 'Save')),
497
				FormAction::create('cancel', _t('LeftAndMain.CANCEL', 'Cancel'))
498
			)
499
		);
500
501
		// Load into form
502
		if($id && $record) {
503
			$form->loadDataFrom($record);
504
		}
505
506
		// Configure form to respond to validation errors with form schema
507
		// if requested via react.
508
		$form->setValidationResponseCallback(function() use ($form) {
509
			return $this->getSchemaResponse($form);
510
		});
511
512
		return $form;
513
	}
514
515
	/**
516
	 * Gets user-visible url to edit a specific {@see ChangeSet}
517
	 *
518
	 * @param $itemID
519
	 * @return string
520
	 */
521
	public function SetLink($itemID) {
522
		return Controller::join_links(
523
			$this->Link('set'),
524
			$itemID
525
		);
526
	}
527
528
	/**
529
	 * Gets user-visible url to edit a specific {@see ChangeSetItem}
530
	 *
531
	 * @param int $itemID
532
	 * @return string
533
	 */
534
	public function ItemLink($itemID) {
535
		return Controller::join_links(
536
			$this->Link('item'),
537
			$itemID
538
		);
539
	}
540
541
}
542