silverstripe /
silverstripe-campaign-admin
| 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] |
||
| 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
introduced
by
Loading history...
|
|||
| 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) { |
||
| 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 |