Completed
Push — master ( bbb282...43d0b8 )
by Daniel
25s
created

CampaignAdmin::readCampaign()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 5
nop 1
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Admin;
4
5
use SilverStripe\ORM\SS_List;
6
use SilverStripe\ORM\Versioning\ChangeSet;
7
use SilverStripe\ORM\Versioning\ChangeSetItem;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\Security\SecurityToken;
10
use SilverStripe\Security\PermissionProvider;
11
use Convert;
12
use SS_HTTPResponse;
13
use SS_HTTPRequest;
14
use LogicException;
15
use HiddenField;
16
use Form;
17
use FieldList;
18
use FormAction;
19
use Controller;
20
21
/**
22
 * Campaign section of the CMS
23
 *
24
 * @package framework
25
 * @subpackage admin
26
 */
27
class CampaignAdmin extends LeftAndMain implements PermissionProvider {
28
29
	private static $allowed_actions = [
30
		'set',
31
		'sets',
32
		'schema',
33
		'DetailEditForm',
34
		'readCampaigns',
35
		'readCampaign',
36
		'deleteCampaign',
37
		'publishCampaign',
38
	];
39
40
	private static $menu_priority = 3;
41
42
	private static $menu_title = 'Campaigns';
43
44
	private static $tree_class = 'SilverStripe\\ORM\\Versioning\\ChangeSet';
45
46
	private static $url_handlers = [
47
		'GET sets' => 'readCampaigns',
48
		'POST set/$ID/publish' => 'publishCampaign',
49
		'GET set/$ID/$Name' => 'readCampaign',
50
		'DELETE set/$ID' => 'deleteCampaign',
51
	];
52
53
	private static $url_segment = 'campaigns';
54
55
	/**
56
	 * Size of thumbnail width
57
	 *
58
	 * @config
59
	 * @var int
60
	 */
61
	private static $thumbnail_width = 64;
62
63
	/**
64
	 * Size of thumbnail height
65
	 *
66
	 * @config
67
	 * @var int
68
	 */
69
	private static $thumbnail_height = 64;
70
71
	private static $required_permission_codes = 'CMS_ACCESS_CampaignAdmin';
72
73
	public function getClientConfig() {
74
		return array_merge(parent::getClientConfig(), [
75
			'reactRouter' => true,
76
			'form' => [
77
				// TODO Use schemaUrl instead
78
				'EditForm' => [
79
					'schemaUrl' => $this->Link('schema/EditForm')
80
				],
81
				'DetailEditForm' => [
82
					'schemaUrl' => $this->Link('schema/DetailEditForm')
83
				],
84
			],
85
			'itemListViewEndpoint' => [
86
				'url' => $this->Link() . 'set/:id/show',
87
				'method' => 'get'
88
			],
89
			'publishEndpoint' => [
90
				'url' => $this->Link() . 'set/:id/publish',
91
				'method' => 'post'
92
			],
93
			'treeClass' => $this->config()->tree_class
94
		]);
95
	}
96
97
	public function schema($request) {
98
		// TODO Hardcoding schema until we can get GridField to generate a schema dynamically
99
		$treeClassJS = Convert::raw2js($this->config()->tree_class);
100
        $adminURL = Convert::raw2js(AdminRootController::admin_url());
101
		$json = <<<JSON
102
{
103
	"id": "Form_EditForm",
104
	"schema": {
105
		"name": "EditForm",
106
		"id": "Form_EditForm",
107
		"action": "schema",
108
		"method": "GET",
109
		"schema_url": "{$adminURL}campaigns\/schema\/EditForm",
110
		"attributes": {
111
			"id": "Form_EditForm",
112
			"action": "{$adminURL}campaigns\/EditForm",
113
			"method": "POST",
114
			"enctype": "multipart\/form-data",
115
			"target": null
116
		},
117
		"data": [],
118
		"fields": [{
119
			"name": "ID",
120
			"id": "Form_EditForm_ID",
121
			"type": "Hidden",
122
			"component": null,
123
			"holder_id": null,
124
			"title": false,
125
			"source": null,
126
			"extraClass": "hidden form-group--no-label",
127
			"description": null,
128
			"rightTitle": null,
129
			"leftTitle": null,
130
			"readOnly": false,
131
			"disabled": false,
132
			"customValidationMessage": "",
133
			"attributes": [],
134
			"data": []
135
		}, {
136
			"name": "ChangeSets",
137
			"id": "Form_EditForm_ChangeSets",
138
			"type": "Custom",
139
			"component": "GridField",
140
			"holder_id": null,
141
			"title": "Campaigns",
142
			"source": null,
143
			"extraClass": null,
144
			"description": null,
145
			"rightTitle": null,
146
			"leftTitle": null,
147
			"readOnly": false,
148
			"disabled": false,
149
			"customValidationMessage": "",
150
			"attributes": [],
151
			"data": {
152
				"recordType": "{$treeClassJS}",
153
				"collectionReadEndpoint": {
154
					"url": "{$adminURL}campaigns\/sets",
155
					"method": "GET"
156
				},
157
				"itemReadEndpoint": {
158
					"url": "{$adminURL}campaigns\/set\/:id",
159
					"method": "GET"
160
				},
161
				"itemUpdateEndpoint": {
162
					"url": "{$adminURL}campaigns\/set\/:id",
163
					"method": "PUT"
164
				},
165
				"itemCreateEndpoint": {
166
					"url": "{$adminURL}campaigns\/set\/:id",
167
					"method": "POST"
168
				},
169
				"itemDeleteEndpoint": {
170
					"url": "{$adminURL}campaigns\/set\/:id",
171
					"method": "DELETE"
172
				},
173
				"editFormSchemaEndpoint": "{$adminURL}campaigns\/schema\/DetailEditForm",
174
				"columns": [
175
					{"name": "Title", "field": "Name"},
176
					{"name": "Changes", "field": "ChangesCount"},
177
					{"name": "Description", "field": "Description"}
178
				]
179
			}
180
		}, {
181
			"name": "SecurityID",
182
			"id": "Form_EditForm_SecurityID",
183
			"type": "Hidden",
184
			"component": null,
185
			"holder_id": null,
186
			"title": "Security ID",
187
			"source": null,
188
			"extraClass": "hidden",
189
			"description": null,
190
			"rightTitle": null,
191
			"leftTitle": null,
192
			"readOnly": false,
193
			"disabled": false,
194
			"customValidationMessage": "",
195
			"attributes": [],
196
			"data": []
197
		}],
198
		"actions": []
199
	}
200
}
201
JSON;
202
203
		$formName = $request->param('ID');
204
		if($formName == 'EditForm') {
205
			$response = $this->getResponse();
206
			$response->addHeader('Content-Type', 'application/json');
207
			$response->setBody($json);
208
			return $response;
209
		} else {
210
			return parent::schema($request);
211
		}
212
	}
213
214
	/**
215
	 * REST endpoint to get a list of campaigns.
216
	 *
217
	 * @return SS_HTTPResponse
218
	 */
219
	public function readCampaigns() {
220
		$response = new SS_HTTPResponse();
221
		$response->addHeader('Content-Type', 'application/json');
222
		$hal = $this->getListResource();
223
		$response->setBody(Convert::array2json($hal));
224
		return $response;
225
	}
226
227
	/**
228
	 * Get list contained as a hal wrapper
229
	 *
230
	 * @return array
231
	 */
232
	protected function getListResource() {
233
		$items = $this->getListItems();
234
		$count = $items->count();
235
		/** @var string $treeClass */
236
		$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...
237
		$hal = [
238
			'count' => $count,
239
			'total' => $count,
240
			'_links' => [
241
				'self' => [
242
					'href' => $this->Link('items')
243
				]
244
			],
245
			'_embedded' => [$treeClass => []]
246
		];
247
		foreach($items as $item) {
248
			/** @var ChangeSet $item */
249
			$resource = $this->getChangeSetResource($item);
250
			$hal['_embedded'][$treeClass][] = $resource;
251
		}
252
		return $hal;
253
	}
254
255
	/**
256
	 * Build item resource from a changeset
257
	 *
258
	 * @param ChangeSet $changeSet
259
	 * @return array
260
	 */
261
	protected function getChangeSetResource(ChangeSet $changeSet) {
262
		// Before presenting the changeset to the client,
263
		// synchronise it with new changes.
264
		$changeSet->sync();
265
		$hal = [
266
			'_links' => [
267
				'self' => [
268
					'href' => $this->SetLink($changeSet->ID)
269
				]
270
			],
271
			'ID' => $changeSet->ID,
272
			'Name' => $changeSet->Name,
273
			'Description' => $changeSet->getDescription(),
274
			'Created' => $changeSet->Created,
275
			'LastEdited' => $changeSet->LastEdited,
276
			'State' => $changeSet->State,
277
			'canEdit' => $changeSet->canEdit(),
278
			'canPublish' => $changeSet->canPublish(),
279
			'_embedded' => ['items' => []]
280
		];
281
		foreach($changeSet->Changes() as $changeSetItem) {
282
			if(!$changeSetItem) {
283
				continue;
284
			}
285
286
			/** @var ChangesetItem $changeSetItem */
287
			$resource = $this->getChangeSetItemResource($changeSetItem);
288
			$hal['_embedded']['items'][] = $resource;
289
		}
290
		$hal['ChangesCount'] = count($hal['_embedded']['items']);
291
		return $hal;
292
	}
293
294
	/**
295
	 * Build item resource from a changesetitem
296
	 *
297
	 * @param ChangeSetItem $changeSetItem
298
	 * @return array
299
	 */
300
	protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) {
301
		$baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass);
302
		$baseSingleton = DataObject::singleton($baseClass);
303
		$thumbnailWidth = (int)$this->config()->thumbnail_width;
304
		$thumbnailHeight = (int)$this->config()->thumbnail_height;
305
		$hal = [
306
			'_links' => [
307
				'self' => [
308
					'href' => $this->ItemLink($changeSetItem->ID)
309
				]
310
			],
311
			'ID' => $changeSetItem->ID,
312
			'Created' => $changeSetItem->Created,
313
			'LastEdited' => $changeSetItem->LastEdited,
314
			'Title' => $changeSetItem->getTitle(),
315
			'ChangeType' => $changeSetItem->getChangeType(),
316
			'Added' => $changeSetItem->Added,
317
			'ObjectClass' => $changeSetItem->ObjectClass,
318
			'ObjectID' => $changeSetItem->ObjectID,
319
			'BaseClass' => $baseClass,
320
			'Singular' => $baseSingleton->i18n_singular_name(),
321
			'Plural' => $baseSingleton->i18n_plural_name(),
322
			'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight),
323
		];
324
		// Get preview urls
325
		$previews = $changeSetItem->getPreviewLinks();
326
		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...
327
			$hal['_links']['preview'] = $previews;
328
		}
329
330
		// Get edit link
331
		$editLink = $changeSetItem->CMSEditLink();
332
		if($editLink) {
333
			$hal['_links']['edit'] = [
334
				'href' => $editLink,
335
			];
336
		}
337
338
		// Depending on whether the object was added implicitly or explicitly, set
339
		// other related objects.
340
		if($changeSetItem->Added === ChangeSetItem::IMPLICITLY) {
341
			$referencedItems = $changeSetItem->ReferencedBy();
342
			$referencedBy = [];
343
			foreach($referencedItems as $referencedItem) {
344
				$referencedBy[] = [
345
					'href' => $this->SetLink($referencedItem->ID)
346
				];
347
			}
348
			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...
349
				$hal['_links']['referenced_by'] = $referencedBy;
350
			}
351
		}
352
353
		return $hal;
354
	}
355
356
	/**
357
	 * Gets viewable list of campaigns
358
	 *
359
	 * @return SS_List
360
	 */
361
	protected function getListItems() {
362
		return ChangeSet::get()
363
			->filter('State', ChangeSet::STATE_OPEN)
364
			->filterByCallback(function($item) {
365
				/** @var ChangeSet $item */
366
				return ($item->canView());
367
			});
368
	}
369
370
371
	/**
372
	 * REST endpoint to get a campaign.
373
	 *
374
	 * @param SS_HTTPRequest $request
375
	 *
376
	 * @return SS_HTTPResponse
377
	 */
378
	public function readCampaign(SS_HTTPRequest $request) {
379
		$response = new SS_HTTPResponse();
380
381
		if ($request->getHeader('Accept') == 'text/json') {
382
			$response->addHeader('Content-Type', 'application/json');
383
			if (!$request->param('Name')) {
384
				return (new SS_HTTPResponse(null, 400));
385
			}
386
387
			/** @var ChangeSet $changeSet */
388
			$changeSet = ChangeSet::get()->byID($request->param('ID'));
389
			if(!$changeSet) {
390
				return (new SS_HTTPResponse(null, 404));
391
			}
392
393
			if(!$changeSet->canView()) {
394
				return (new SS_HTTPResponse(null, 403));
395
			}
396
397
			$body = Convert::raw2json($this->getChangeSetResource($changeSet));
398
			return (new SS_HTTPResponse($body, 200))
399
				->addHeader('Content-Type', 'application/json');
400
		} else {
401
			return $this->index($request);
402
		}
403
	}
404
405
	/**
406
	 * REST endpoint to delete a campaign.
407
	 *
408
	 * @param SS_HTTPRequest $request
409
	 *
410
	 * @return SS_HTTPResponse
411
	 */
412
	public function deleteCampaign(SS_HTTPRequest $request) {
413
		// Check security ID
414
		if (!SecurityToken::inst()->checkRequest($request)) {
415
			return new SS_HTTPResponse(null, 400);
416
		}
417
418
		$id = $request->param('ID');
419
		if (!$id || !is_numeric($id)) {
420
			return (new SS_HTTPResponse(null, 400));
421
		}
422
423
		$record = ChangeSet::get()->byID($id);
424
		if(!$record) {
425
			return (new SS_HTTPResponse(null, 404));
426
		}
427
428
		if(!$record->canDelete()) {
429
			return (new SS_HTTPResponse(null, 403));
430
		}
431
432
		$record->delete();
433
434
		return (new SS_HTTPResponse(null, 204));
435
	}
436
437
	/**
438
	 * REST endpoint to publish a {@link ChangeSet} and all of its items.
439
	 *
440
	 * @param SS_HTTPRequest $request
441
	 *
442
	 * @return SS_HTTPResponse
443
	 */
444
	public function publishCampaign(SS_HTTPRequest $request) {
445
		// Protect against CSRF on destructive action
446
		if(!SecurityToken::inst()->checkRequest($request)) {
447
			return (new SS_HTTPResponse(null, 400));
448
		}
449
450
		$id = $request->param('ID');
451
		if(!$id || !is_numeric($id)) {
452
			return (new SS_HTTPResponse(null, 400));
453
		}
454
455
		/** @var ChangeSet $record */
456
		$record = ChangeSet::get()->byID($id);
457
		if(!$record) {
458
			return (new SS_HTTPResponse(null, 404));
459
		}
460
461
		if(!$record->canPublish()) {
462
			return (new SS_HTTPResponse(null, 403));
463
		}
464
465
		try {
466
			$record->publish();
467
		} catch(LogicException $e) {
468
			return (new SS_HTTPResponse(json_encode(['status' => 'error', 'message' => $e->getMessage()]), 401))
469
				->addHeader('Content-Type', 'application/json');
470
		}
471
472
		return (new SS_HTTPResponse(
473
			Convert::raw2json($this->getChangeSetResource($record)),
474
			200
475
		))->addHeader('Content-Type', 'application/json');
476
	}
477
478
	/**
479
	 * Url handler for edit form
480
	 *
481
	 * @param SS_HTTPRequest $request
482
	 * @return Form
483
	 */
484
	public function DetailEditForm($request) {
485
		// Get ID either from posted back value, or url parameter
486
		$id = $request->param('ID') ?: $request->postVar('ID');
487
		return $this->getDetailEditForm($id);
488
	}
489
490
	/**
491
	 * @todo Use GridFieldDetailForm once it can handle structured data and form schemas
492
	 *
493
	 * @param int $id
494
	 * @return Form
495
	 */
496
	public function getDetailEditForm($id) {
497
		// Get record-specific fields
498
		$record = null;
499
		if($id) {
500
			$record = ChangeSet::get()->byID($id);
501
			if(!$record || !$record->canView()) {
502
				return null;
503
			}
504
		}
505
506
		if(!$record) {
507
			$record = ChangeSet::singleton();
508
		}
509
510
		$fields = $record->getCMSFields();
511
512
		// Add standard fields
513
		$fields->push(HiddenField::create('ID'));
514
		$form = Form::create(
515
			$this,
516
			'DetailEditForm',
517
			$fields,
518
			FieldList::create(
519
				FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
520
					->setIcon('save'),
521
				FormAction::create('cancel', _t('LeftAndMain.CANCEL', 'Cancel'))
522
					->setUseButtonTag(true)
523
			)
524
		);
525
526
		// Load into form
527
		if($id && $record) {
528
			$form->loadDataFrom($record);
529
		}
530
531
		// Configure form to respond to validation errors with form schema
532
		// if requested via react.
533
		$form->setValidationResponseCallback(function() use ($form) {
534
			return $this->getSchemaResponse($form);
535
		});
536
537
		return $form;
538
	}
539
540
	/**
541
	 * Gets user-visible url to edit a specific {@see ChangeSet}
542
	 *
543
	 * @param $itemID
544
	 * @return string
545
	 */
546
	public function SetLink($itemID) {
547
		return Controller::join_links(
548
			$this->Link('set'),
549
			$itemID
550
		);
551
	}
552
553
	/**
554
	 * Gets user-visible url to edit a specific {@see ChangeSetItem}
555
	 *
556
	 * @param int $itemID
557
	 * @return string
558
	 */
559
	public function ItemLink($itemID) {
560
		return Controller::join_links(
561
			$this->Link('item'),
562
			$itemID
563
		);
564
	}
565
566
	public function providePermissions() {
567
		return array(
568
			"CMS_ACCESS_CampaignAdmin" => array(
569
				'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => static::menu_title())),
570
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
571
				'help' => _t(
572
					'CampaignAdmin.ACCESS_HELP',
573
					'Allow viewing of the campaign publishing section.'
574
				)
575
			)
576
		);
577
	}
578
579
}
580