1 | <?php |
||
2 | |||
3 | namespace SilverStripe\CampaignAdmin; |
||
4 | |||
5 | use SilverStripe\Control\Controller; |
||
6 | use SilverStripe\Control\Director; |
||
7 | use SilverStripe\Control\HTTPResponse_Exception; |
||
8 | use SilverStripe\Control\HTTPResponse; |
||
9 | use SilverStripe\Core\ClassInfo; |
||
10 | use SilverStripe\Core\Injector\Injectable; |
||
11 | use SilverStripe\Dev\Deprecation; |
||
12 | use SilverStripe\Forms\CheckboxField; |
||
13 | use SilverStripe\Forms\DropdownField; |
||
14 | use SilverStripe\Forms\HiddenField; |
||
15 | use SilverStripe\Forms\LiteralField; |
||
16 | use SilverStripe\Forms\FieldList; |
||
17 | use SilverStripe\Forms\Form; |
||
18 | use SilverStripe\Forms\TextField; |
||
19 | use SilverStripe\ORM\ArrayList; |
||
20 | use SilverStripe\ORM\DataObject; |
||
21 | use SilverStripe\ORM\FieldType\DBHTMLText; |
||
22 | use SilverStripe\ORM\ValidationException; |
||
23 | use SilverStripe\ORM\ValidationResult; |
||
24 | use SilverStripe\Versioned\ChangeSet; |
||
25 | use SilverStripe\Versioned\ChangeSetItem; |
||
26 | use SilverStripe\Versioned\Versioned; |
||
27 | use SilverStripe\Core\Convert; |
||
28 | |||
29 | /** |
||
30 | * Class AddToCampaignHandler - handle the AddToCampaign action. |
||
31 | * |
||
32 | * This is a class designed to be delegated to by a Form action handler method in the EditForm of a LeftAndMain |
||
33 | * child class. |
||
34 | * |
||
35 | * Add To Campaign can be seen as an item action like "publish" or "rollback", but unlike those actions |
||
36 | * it needs one additional piece of information to execute, the ChangeSet ID. |
||
37 | * |
||
38 | * So this handler does one of two things to respond to the action request, depending on whether the ChangeSet ID |
||
39 | * was included in the submitted data |
||
40 | * - If it was, perform the Add To Campaign action (as per any other action) |
||
41 | * - If it wasn't, return a form to get the ChangeSet ID and then repeat this action submission |
||
42 | * |
||
43 | * To use, you'd add an action to your LeftAndMain subclass, like this: |
||
44 | * |
||
45 | * function addtocampaign($data, $form) { |
||
46 | * $handler = AddToCampaignHandler::create($form, $data); |
||
47 | * return $handler->handle(); |
||
48 | * } |
||
49 | * |
||
50 | * and add an AddToCampaignHandler_FormAction to the EditForm, possibly through getCMSActions |
||
51 | */ |
||
52 | class AddToCampaignHandler |
||
53 | { |
||
54 | use Injectable; |
||
55 | |||
56 | /** |
||
57 | * Parent controller for this form |
||
58 | * |
||
59 | * @var Controller |
||
60 | */ |
||
61 | protected $controller; |
||
62 | |||
63 | /** |
||
64 | * The submitted form data |
||
65 | * |
||
66 | * @var array |
||
67 | */ |
||
68 | protected $data; |
||
69 | |||
70 | /** |
||
71 | * Form name to use |
||
72 | * |
||
73 | * @var string |
||
74 | */ |
||
75 | protected $name; |
||
76 | |||
77 | /** |
||
78 | * AddToCampaignHandler constructor. |
||
79 | * |
||
80 | * @param Controller $controller Controller for this form |
||
81 | * @param array|DataObject $data The data submitted as part of that form |
||
82 | * @param string $name Form name |
||
83 | */ |
||
84 | public function __construct($controller = null, $data = [], $name = 'AddToCampaignForm') |
||
85 | { |
||
86 | $this->controller = $controller; |
||
87 | if ($data instanceof DataObject) { |
||
88 | $data = $data->toMap(); |
||
89 | } |
||
90 | $this->data = $data; |
||
91 | $this->name = $name; |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * Perform the action. Either returns a Form or performs the action, as per the class doc |
||
96 | * |
||
97 | * @return DBHTMLText|HTTPResponse |
||
98 | */ |
||
99 | public function handle() |
||
100 | { |
||
101 | Deprecation::notice('5.0', 'handle() will be removed. Use addToCampaign or Form directly'); |
||
102 | $object = $this->getObject($this->data['ID'], $this->data['ClassName']); |
||
103 | |||
104 | if (empty($this->data['Campaign'])) { |
||
105 | return $this->Form($object)->forTemplate(); |
||
106 | } else { |
||
107 | return $this->addToCampaign($object, $this->data['Campaign']); |
||
108 | } |
||
109 | } |
||
110 | |||
111 | /** |
||
112 | * Get what ChangeSets are available for an item to be added to by this user |
||
113 | * |
||
114 | * @return ArrayList|ChangeSet[] |
||
115 | */ |
||
116 | protected function getAvailableChangeSets() |
||
117 | { |
||
118 | return ChangeSet::get() |
||
119 | ->filter([ |
||
120 | 'State' => ChangeSet::STATE_OPEN, |
||
121 | 'IsInferred' => 0 |
||
122 | ]) |
||
123 | ->filterByCallback(function ($item) { |
||
124 | /** @var ChangeSet $item */ |
||
125 | return $item->canView(); |
||
126 | }); |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * Get changesets that a given object is already in |
||
131 | * |
||
132 | * @param DataObject |
||
133 | * @return ArrayList[ChangeSet] |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
134 | */ |
||
135 | protected function getInChangeSets($object) |
||
136 | { |
||
137 | $inChangeSetIDs = array_unique(ChangeSetItem::get_for_object($object)->column('ChangeSetID') ?? []); |
||
138 | if ($inChangeSetIDs > 0) { |
||
139 | $changeSets = $this->getAvailableChangeSets()->filter([ |
||
140 | 'ID' => $inChangeSetIDs, |
||
141 | 'State' => ChangeSet::STATE_OPEN |
||
142 | ]); |
||
143 | } else { |
||
144 | $changeSets = new ArrayList(); |
||
145 | } |
||
146 | |||
147 | return $changeSets; |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * Safely get a DataObject from a client-supplied ID and ClassName, checking: argument |
||
152 | * validity; existence; and canView permissions. |
||
153 | * |
||
154 | * @param int $id The ID of the DataObject |
||
155 | * @param string $class The Class of the DataObject |
||
156 | * @return DataObject The referenced DataObject |
||
157 | * @throws HTTPResponse_Exception |
||
158 | */ |
||
159 | protected function getObject($id, $class) |
||
160 | { |
||
161 | $id = (int)$id; |
||
162 | $class = ClassInfo::class_name($class); |
||
163 | |||
164 | if (!$class |
||
165 | || !is_subclass_of($class, DataObject::class) |
||
166 | || !DataObject::has_extension($class, Versioned::class) |
||
167 | ) { |
||
168 | $this->controller->httpError(400, _t( |
||
169 | __CLASS__ . '.ErrorGeneral', |
||
170 | 'We apologise, but there was an error' |
||
171 | )); |
||
172 | return null; |
||
173 | } |
||
174 | |||
175 | $object = DataObject::get($class)->byID($id); |
||
176 | |||
177 | if (!$object) { |
||
178 | $this->controller->httpError(404, _t( |
||
179 | __CLASS__ . '.ErrorNotFound', |
||
180 | 'That {Type} couldn\'t be found', |
||
181 | '', |
||
182 | ['Type' => $class] |
||
183 | )); |
||
184 | return null; |
||
185 | } |
||
186 | |||
187 | if (!$object->canView()) { |
||
188 | $this->controller->httpError(403, _t( |
||
189 | __CLASS__ . '.ErrorItemPermissionDenied', |
||
190 | 'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign', |
||
191 | '', |
||
192 | ['ObjectTitle' => $object->Title] |
||
193 | )); |
||
194 | return null; |
||
195 | } |
||
196 | |||
197 | return $object; |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Builds a Form that mirrors the parent editForm, but with an extra field to collect the ChangeSet ID |
||
202 | * |
||
203 | * @param DataObject $object The object we're going to be adding to whichever ChangeSet is chosen |
||
204 | * @return Form |
||
205 | */ |
||
206 | public function Form($object) |
||
207 | { |
||
208 | $inChangeSets = $this->getInChangeSets($object); |
||
209 | $inChangeSetIDs = $inChangeSets->column('ID'); |
||
210 | |||
211 | // Get changesets that can be added to |
||
212 | $candidateChangeSets = $this->getAvailableChangeSets(); |
||
213 | if ($inChangeSetIDs) { |
||
214 | $candidateChangeSets = $candidateChangeSets->exclude('ID', $inChangeSetIDs); |
||
215 | } |
||
216 | |||
217 | $canCreate = ChangeSet::singleton()->canCreate(); |
||
218 | $message = $this->getFormAlert($inChangeSets, $candidateChangeSets, $canCreate); |
||
219 | $fields = new FieldList(array_filter([ |
||
220 | $message ? LiteralField::create("AlertMessages", $message) : null, |
||
221 | HiddenField::create('ID', null, $object->ID), |
||
222 | HiddenField::create('ClassName', null, $object->baseClass()) |
||
223 | ])); |
||
224 | |||
225 | // Add fields based on available options |
||
226 | $showSelect = $candidateChangeSets->count() > 0; |
||
227 | if ($showSelect) { |
||
228 | $campaignDropdown = DropdownField::create( |
||
229 | 'Campaign', |
||
230 | _t(__CLASS__ . '.AddToCampaignAvailableLabel', 'Available campaigns'), |
||
231 | $candidateChangeSets |
||
232 | ) |
||
233 | ->setEmptyString(_t(__CLASS__ . '.AddToCampaignFormFieldLabel', 'Select a Campaign')) |
||
234 | ->addExtraClass('noborder') |
||
235 | ->addExtraClass('no-chosen'); |
||
236 | $fields->push($campaignDropdown); |
||
237 | |||
238 | // Show visibilty toggle of other create field |
||
239 | if ($canCreate) { |
||
240 | $addCampaignSelect = CheckboxField::create('AddNewSelect', _t( |
||
241 | __CLASS__ . '.ADD_TO_A_NEW_CAMPAIGN', |
||
242 | 'Add to a new campaign' |
||
243 | )) |
||
244 | ->setAttribute('data-shows', 'NewTitle') |
||
245 | ->setSchemaData(['data' => ['shows' => 'NewTitle']]); |
||
246 | $fields->push($addCampaignSelect); |
||
247 | } |
||
248 | } |
||
249 | if ($canCreate) { |
||
250 | $placeholder = _t(__CLASS__ . '.CREATE_NEW_PLACEHOLDER', 'Enter campaign name'); |
||
251 | $createBox = TextField::create( |
||
252 | 'NewTitle', |
||
253 | _t(__CLASS__ . '.CREATE_NEW', 'Create a new campaign') |
||
254 | ) |
||
255 | ->setAttribute('placeholder', $placeholder) |
||
256 | ->setSchemaData(['attributes' => ['placeholder' => $placeholder]]); |
||
257 | $fields->push($createBox); |
||
258 | } |
||
259 | |||
260 | $actions = FieldList::create(); |
||
261 | if ($canCreate || $showSelect) { |
||
262 | $actions->push( |
||
263 | AddToCampaignHandler_FormAction::create() |
||
264 | ->setTitle(_t(__CLASS__ . '.AddToCampaignAddAction', 'Add')) |
||
265 | ->addExtraClass('add-to-campaign__action') |
||
266 | ); |
||
267 | } |
||
268 | |||
269 | $form = Form::create( |
||
270 | $this->controller, |
||
271 | $this->name, |
||
272 | $fields, |
||
273 | $actions, |
||
274 | AddToCampaignValidator::create() |
||
275 | ); |
||
276 | |||
277 | $form->setHTMLID('Form_EditForm_AddToCampaign'); |
||
278 | $form->addExtraClass('form--no-dividers add-to-campaign__form'); |
||
279 | |||
280 | return $form; |
||
281 | } |
||
282 | |||
283 | /** |
||
284 | * Performs the actual action of adding the object to the ChangeSet, once the ChangeSet ID is known |
||
285 | * |
||
286 | * @param DataObject $object The object to add to the ChangeSet |
||
287 | * @param array|int $data Post data for this campaign form, or the ID of the campaign to add to |
||
288 | * @return HTTPResponse |
||
289 | * @throws ValidationException |
||
290 | */ |
||
291 | public function addToCampaign($object, $data) |
||
292 | { |
||
293 | // Extract $campaignID from $data |
||
294 | $campaignID = $this->getOrCreateCampaign($data); |
||
295 | |||
296 | /** @var ChangeSet $changeSet */ |
||
297 | $changeSet = ChangeSet::get()->byID($campaignID); |
||
298 | |||
299 | if (!$changeSet) { |
||
0 ignored issues
–
show
|
|||
300 | throw new ValidationException(_t( |
||
301 | __CLASS__ . '.ErrorNotFound', |
||
302 | 'That {Type} couldn\'t be found', |
||
303 | ['Type' => 'Campaign'] |
||
304 | )); |
||
305 | } |
||
306 | |||
307 | if (!$changeSet->canEdit()) { |
||
308 | throw new ValidationException(_t( |
||
309 | __CLASS__ . '.ErrorCampaignPermissionDenied', |
||
310 | 'It seems you don\'t have the necessary permissions to add {ObjectTitle} to {CampaignTitle}', |
||
311 | [ |
||
312 | 'ObjectTitle' => $object->Title, |
||
313 | 'CampaignTitle' => $changeSet->Title |
||
314 | ] |
||
315 | )); |
||
316 | } |
||
317 | |||
318 | $changeSet->addObject($object); |
||
319 | |||
320 | $childObjects = $object->findRelatedObjects('cascade_add_to_campaign'); |
||
321 | if ($childObjects) { |
||
0 ignored issues
–
show
|
|||
322 | foreach ($childObjects as $childObject) { |
||
323 | $changeSet->addObject($childObject); |
||
324 | } |
||
325 | } |
||
326 | |||
327 | $request = $this->controller->getRequest(); |
||
328 | $message = _t( |
||
329 | __CLASS__ . '.Success', |
||
330 | 'Successfully added <strong>{ObjectTitle}</strong> to <strong>{CampaignTitle}</strong>', |
||
331 | [ |
||
332 | 'ObjectTitle' => Convert::raw2xml($object->Title), |
||
333 | 'CampaignTitle' => Convert::raw2xml($changeSet->Title) |
||
334 | ] |
||
335 | ); |
||
336 | if ($request->getHeader('X-Formschema-Request')) { |
||
337 | return $message; |
||
0 ignored issues
–
show
|
|||
338 | } elseif (Director::is_ajax()) { |
||
339 | $response = new HTTPResponse($message, 200); |
||
340 | |||
341 | $response->addHeader('Content-Type', 'text/html; charset=utf-8'); |
||
342 | return $response; |
||
343 | } else { |
||
344 | return $this->controller->redirectBack(); |
||
345 | } |
||
346 | } |
||
347 | |||
348 | /** |
||
349 | * Get descriptive alert to display at the top of the form |
||
350 | * |
||
351 | * @param ArrayList $inChangeSets List of changesets this item exists in |
||
352 | * @param ArrayList $candidateChangeSets List of changesets this item could be added to |
||
353 | * @param bool $canCreate |
||
354 | * @return string |
||
355 | */ |
||
356 | protected function getFormAlert($inChangeSets, $candidateChangeSets, $canCreate) |
||
357 | { |
||
358 | // In a subset of changesets |
||
359 | if ($inChangeSets->count() > 0 && $candidateChangeSets->count() > 0) { |
||
360 | return sprintf( |
||
361 | '<div class="alert alert-info"><strong>%s</strong><br/>%s</div>', |
||
362 | _t( |
||
363 | __CLASS__ . '.AddToCampaignInChangsetLabel', |
||
364 | 'Heads up, this item is already in campaign(s):' |
||
365 | ), |
||
366 | Convert::raw2xml(implode(', ', $inChangeSets->column('Name'))) |
||
367 | ); |
||
368 | } |
||
369 | |||
370 | // In all changesets |
||
371 | if ($inChangeSets->count() > 0) { |
||
372 | return sprintf( |
||
373 | '<div class="alert alert-info"><strong>%s</strong><br/>%s</div>', |
||
374 | _t( |
||
375 | __CLASS__ . '.AddToCampaignInChangsetLabelAll', |
||
376 | 'Heads up, this item is already in ALL campaign(s):' |
||
377 | ), |
||
378 | Convert::raw2xml(implode(', ', $inChangeSets->column('Name'))) |
||
379 | ); |
||
380 | } |
||
381 | |||
382 | // Create only |
||
383 | if ($candidateChangeSets->count() === 0 && $canCreate) { |
||
384 | return sprintf( |
||
385 | '<div class="alert alert-info">%s</div>', |
||
386 | _t( |
||
387 | __CLASS__ . '.NO_CAMPAIGNS', |
||
388 | "You currently don't have any campaigns. " |
||
389 | . "You can edit campaign details later in the Campaigns section." |
||
390 | ) |
||
391 | ); |
||
392 | } |
||
393 | |||
394 | // Can't select or create |
||
395 | if ($candidateChangeSets->count() === 0 && !$canCreate) { |
||
396 | return sprintf( |
||
397 | '<div class="alert alert-warning">%s</div>', |
||
398 | _t( |
||
399 | __CLASS__ . '.NO_CREATE', |
||
400 | "Oh no! You currently don't have any campaigns created. " |
||
401 | . "Your current login does not have privileges to create campaigns. " |
||
402 | . "Campaigns can only be created by users with Campaigns section rights." |
||
403 | ) |
||
404 | ); |
||
405 | } |
||
406 | return null; |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * Find or build campaign from posted data |
||
411 | * |
||
412 | * @param array|int $data |
||
413 | * @return int |
||
414 | * @throws ValidationException |
||
415 | */ |
||
416 | protected function getOrCreateCampaign($data) |
||
417 | { |
||
418 | // Create new campaign if selected |
||
419 | if (is_array($data) && !empty($data['AddNewSelect']) // Explicitly click "Add to a new campaign" |
||
0 ignored issues
–
show
|
|||
420 | || (is_array($data) && !isset($data['Campaign']) && isset($data['NewTitle'])) // This is the only option |
||
421 | ) { |
||
422 | // Permission |
||
423 | if (!ChangeSet::singleton()->canCreate()) { |
||
424 | throw $this->validationResult( |
||
425 | _t(__CLASS__ . '.CREATE_DENIED', 'You do not have permission to create campaigns') |
||
426 | ); |
||
427 | } |
||
428 | |||
429 | // Check title is valid |
||
430 | $title = $data['NewTitle']; |
||
431 | if (empty($title)) { |
||
432 | throw $this->validationResult( |
||
433 | _t(__CLASS__ . '.MISSING_TITLE', 'Campaign name is required'), |
||
434 | 'NewTitle' |
||
435 | ); |
||
436 | } |
||
437 | |||
438 | // Prevent duplicates |
||
439 | $hasExistingName = Changeset::get() |
||
440 | ->filter('Name:nocase', $title) |
||
441 | ->count() > 0; |
||
442 | |||
443 | if ($hasExistingName) { |
||
444 | throw $this->validationResult( |
||
445 | _t( |
||
446 | 'SilverStripe\\CampaignAdmin\\CampaignAdmin.ERROR_DUPLICATE_NAME', |
||
447 | 'Name "{Name}" already exists', |
||
448 | ['Name' => $title] |
||
449 | ), |
||
450 | 'NewTitle' |
||
451 | ); |
||
452 | } |
||
453 | |||
454 | // Create and return |
||
455 | $campaign = ChangeSet::create(); |
||
456 | $campaign->Name = $title; |
||
457 | $campaign->write(); |
||
458 | return $campaign->ID; |
||
459 | } |
||
460 | |||
461 | // Get selected campaign ID |
||
462 | $campaignID = null; |
||
463 | if (is_array($data) && !empty($data['Campaign'])) { |
||
464 | $campaignID = $data['Campaign']; |
||
465 | } elseif (is_numeric($data)) { |
||
466 | $campaignID = (int)$data; |
||
467 | } |
||
468 | if (empty($campaignID)) { |
||
469 | throw $this->validationResult(_t(__CLASS__ . '.NONE_SELECTED', 'No campaign selected')); |
||
470 | } |
||
471 | return $campaignID; |
||
472 | } |
||
473 | |||
474 | /** |
||
475 | * Raise validation error |
||
476 | * |
||
477 | * @param string $message |
||
478 | * @param string $field |
||
479 | * @return ValidationException |
||
480 | */ |
||
481 | protected function validationResult($message, $field = null) |
||
482 | { |
||
483 | $error = ValidationResult::create() |
||
484 | ->addFieldError($field, $message); |
||
485 | return new ValidationException($error); |
||
486 | } |
||
487 | } |
||
488 |