Completed
Push — master ( 2846af...322d3e )
by Ingo
12:00
created

CampaignAdmin::getDetailEditForm()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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