Total Complexity | 113 |
Total Lines | 920 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 1 |
Complex classes like ExternalContentAdmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ExternalContentAdmin, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
42 | class ExternalContentAdmin extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider |
||
43 | { |
||
44 | /** |
||
45 | * The URL format to get directly to this controller |
||
46 | * @var unknown_type |
||
47 | */ |
||
48 | public const URL_STUB = 'extadmin'; |
||
49 | |||
50 | /** |
||
51 | * The directory that the module is assuming it's installed in to. |
||
52 | */ |
||
53 | public static $directory = 'external-content'; |
||
54 | |||
55 | /** |
||
56 | * URL segment used by the backend |
||
57 | * |
||
58 | * @var string |
||
59 | */ |
||
60 | private static $url_segment = 'external-content'; |
||
61 | private static $url_rule = '$Action//$ID'; |
||
62 | private static $menu_title = 'External Content'; |
||
63 | private static $tree_class = ExternalContentSource::class; |
||
64 | private static $allowed_actions = [ |
||
65 | 'addprovider', |
||
66 | 'deleteprovider', |
||
67 | 'deletemarked', |
||
68 | 'CreateProviderForm', |
||
69 | 'DeleteItemsForm', |
||
70 | 'getsubtree', |
||
71 | 'save', |
||
72 | 'migrate', |
||
73 | 'download', |
||
74 | 'view', |
||
75 | 'treeview', |
||
76 | 'EditForm', |
||
77 | 'AddForm', |
||
78 | 'updateSources', |
||
79 | 'updatetreenodes' |
||
80 | ]; |
||
81 | |||
82 | public function init() |
||
83 | { |
||
84 | parent::init(); |
||
85 | |||
86 | Requirements::customCSS($this->generatePageIconsCss()); |
||
87 | Requirements::javascript('phptek/silverstripe-exodus:external-content/javascript/external-content-admin.js'); |
||
88 | Requirements::javascript('phptek/silverstripe-exodus:external-content/javascript/external-content-reload.js'); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * Overridden to properly output a value and end, instead of |
||
93 | * letting further headers (X-Javascript-Include) be output |
||
94 | */ |
||
95 | public function pageStatus() |
||
96 | { |
||
97 | // If no ID is set, we're merely keeping the session alive |
||
98 | if (!isset($_REQUEST['ID'])) { |
||
99 | echo '{}'; |
||
100 | return; |
||
101 | } |
||
102 | |||
103 | parent::pageStatus(); |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * @return string |
||
108 | */ |
||
109 | public function LinkTreeViewDeferred(): string |
||
110 | { |
||
111 | return $this->Link('treeview'); |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Copied directly from {@link CMSMain}. It appears this method used to be on {@link LeftAndMain} |
||
116 | * when the "external-content" module was first written. Since we need to to subclass L&M and not CMSMain |
||
117 | * we need a copy of it here. |
||
118 | * |
||
119 | * @inheritDoc |
||
120 | */ |
||
121 | public function getSiteTreeFor( |
||
122 | $className, |
||
123 | $rootID = null, |
||
124 | $childrenMethod = null, |
||
125 | $numChildrenMethod = null, |
||
126 | $filterFunction = null, |
||
127 | $nodeCountThreshold = null |
||
128 | ) { |
||
129 | $nodeCountThreshold = is_null($nodeCountThreshold) ? Config::inst()->get($className, 'node_threshold_total') : $nodeCountThreshold; |
||
130 | // Provide better defaults from filter |
||
131 | $filter = $this->getSearchFilter(); |
||
132 | |||
133 | if ($filter) { |
||
134 | if (!$childrenMethod) { |
||
135 | $childrenMethod = $filter->getChildrenMethod(); |
||
136 | } |
||
137 | |||
138 | if (!$numChildrenMethod) { |
||
139 | $numChildrenMethod = $filter->getNumChildrenMethod(); |
||
140 | } |
||
141 | |||
142 | if (!$filterFunction) { |
||
143 | $filterFunction = function ($node) use ($filter) { |
||
144 | return $filter->isPageIncluded($node); |
||
145 | }; |
||
146 | } |
||
147 | } |
||
148 | |||
149 | // Build set from node and begin marking |
||
150 | $record = $rootID ? $this->getRecord($rootID) : null; |
||
151 | $rootNode = $record ? $record : DataObject::singleton($className); |
||
152 | $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold); |
||
153 | |||
154 | // Set filter function |
||
155 | if ($filterFunction) { |
||
156 | $markingSet->setMarkingFilterFunction($filterFunction); |
||
157 | } |
||
158 | |||
159 | // Mark tree from this node |
||
160 | $markingSet->markPartialTree(); |
||
161 | |||
162 | // Ensure current page is exposed |
||
163 | $currentPage = $this->currentPage(); |
||
164 | if ($currentPage) { |
||
165 | $markingSet->markToExpose($currentPage); |
||
166 | } |
||
167 | |||
168 | // Pre-cache permissions |
||
169 | // $checker = SiteTree::getPermissionChecker(); |
||
170 | // if ($checker instanceof InheritedPermissions) { |
||
171 | // $checker->prePopulatePermissionCache( |
||
172 | // InheritedPermissions::EDIT, |
||
173 | // $markingSet->markedNodeIDs() |
||
174 | // ); |
||
175 | // } |
||
176 | |||
177 | // Render using full-subtree template |
||
178 | return $markingSet->renderChildren( |
||
179 | [ self::class . '_SubTree', 'type' => 'Includes' ], |
||
180 | $this->getTreeNodeCustomisations() |
||
181 | ); |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * Get callback to determine template customisations for nodes |
||
186 | * |
||
187 | * @return callable |
||
188 | */ |
||
189 | protected function getTreeNodeCustomisations() |
||
190 | { |
||
191 | $rootTitle = $this->getCMSTreeTitle(); |
||
192 | return function ($node) use ($rootTitle) { |
||
193 | return [ |
||
194 | 'listViewLink' => $this->LinkListViewChildren($node->ID), |
||
195 | 'rootTitle' => $rootTitle, |
||
196 | 'extraClass' => $this->getTreeNodeClasses($node), |
||
197 | 'Title' => _t( |
||
198 | self::class . '.PAGETYPE_TITLE', |
||
199 | '(Page type: {type}) {title}', |
||
200 | [ |
||
201 | 'type' => $node->i18n_singular_name(), |
||
202 | 'title' => $node->Title, |
||
203 | ] |
||
204 | ) |
||
205 | ]; |
||
206 | }; |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * Link to list view for children of a parent page |
||
211 | * |
||
212 | * @param int|string $parentID Literal parentID, or placeholder (e.g. '%d') for |
||
213 | * client side substitution |
||
214 | * @return string |
||
215 | */ |
||
216 | public function LinkListViewChildren($parentID) |
||
217 | { |
||
218 | return sprintf( |
||
219 | '%s?ParentID=%s', |
||
220 | CMSMain::singleton()->Link(), |
||
221 | $parentID |
||
222 | ); |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * Get extra CSS classes for a page's tree node |
||
227 | * |
||
228 | * @param $node |
||
229 | * @return string |
||
230 | */ |
||
231 | public function getTreeNodeClasses($node): string |
||
232 | { |
||
233 | // Get classes from object |
||
234 | $classes = $node->CMSTreeClasses() ?? ''; |
||
235 | |||
236 | // Get additional filter classes |
||
237 | $filter = $this->getSearchFilter(); |
||
238 | |||
239 | if ($filter && ($filterClasses = $filter->getPageClasses($node))) { |
||
240 | if (is_array($filterClasses)) { |
||
241 | $filterClasses = implode(' ', $filterClasses); |
||
242 | } |
||
243 | |||
244 | $classes .= ' ' . $filterClasses; |
||
245 | } |
||
246 | |||
247 | return trim($classes); |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * |
||
252 | * If there's no ExternalContentSource ID available from Session or Request data then instead of |
||
253 | * LeftAndMain::currentPageID() returning just `null`, "extend" its range to use the first sub-class |
||
254 | * of {@link ExternalContentSource} the system can find, either via config or introspection. |
||
255 | * |
||
256 | * @return number | null |
||
257 | */ |
||
258 | public function getCurrentPageID() |
||
259 | { |
||
260 | if (!$id = $this->currentPageID()) { |
||
261 | // Try an id from an ExternalContentSource Subclass |
||
262 | $defaultSources = ClassInfo::getValidSubClasses(ExternalContentSource::class); |
||
263 | array_shift($defaultSources); |
||
264 | // Use one if defined in config, otherwise use first one found through reflection |
||
265 | $defaultSourceConfig = ExternalContentSource::config()->get('default_source'); |
||
266 | |||
267 | if ($defaultSourceConfig) { |
||
268 | $class = $defaultSourceConfig; |
||
269 | } elseif (isset($defaultSources[0])) { |
||
270 | $class = $defaultSources[0]; |
||
271 | } else { |
||
272 | $class = null; |
||
273 | } |
||
274 | |||
275 | if ($class && $source = DataObject::get($class)->first()) { |
||
276 | return $source->ID; |
||
277 | } |
||
278 | |||
279 | return null; |
||
280 | } |
||
281 | |||
282 | return $id; |
||
283 | } |
||
284 | |||
285 | public function currentPageID() |
||
286 | { |
||
287 | $session = $this->getRequest()->getSession(); |
||
288 | |||
289 | if ($this->getRequest()->requestVar('ID') && preg_match(ExternalContent::ID_FORMAT, $this->getRequest()->requestVar('ID'))) { |
||
290 | return $this->getRequest()->requestVar('ID'); |
||
291 | } elseif (isset($this->urlParams['ID']) && preg_match(ExternalContent::ID_FORMAT, $this->urlParams['ID'])) { |
||
292 | return $this->urlParams['ID']; |
||
293 | } elseif ($session && $session->get($this->sessionNamespace() . ".currentPage")) { |
||
294 | return $session->get($this->sessionNamespace() . ".currentPage"); |
||
295 | } |
||
296 | |||
297 | return null; |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * Custom currentPage() method to handle opening the 'root' node (which is always going to be an instance of ExternalContentSource???) |
||
302 | * |
||
303 | * @return DataObject |
||
304 | */ |
||
305 | public function currentPage() |
||
306 | { |
||
307 | $id = (string) $this->getCurrentPageID(); |
||
308 | |||
309 | if (preg_match(ExternalContent::ID_FORMAT, $id)) { |
||
310 | return ExternalContent::getDataObjectFor($id); |
||
311 | } |
||
312 | |||
313 | if ($id == 'root') { |
||
314 | return singleton($this->config()->get('tree_class')); |
||
315 | } |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * Is the passed in ID a valid |
||
320 | * format? |
||
321 | * |
||
322 | * @return boolean |
||
323 | */ |
||
324 | public static function isValidId($id) |
||
325 | { |
||
326 | return preg_match(ExternalContent::ID_FORMAT, $id); |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * Action to migrate a selected object through to SS. |
||
331 | * |
||
332 | * Note: Even if all content imports and url crawls are deleted, imports still stop prematurely and without error. |
||
333 | * Could be to do with the select parent page to import into having been "marked"...? Nope.. |
||
334 | * |
||
335 | * @param array $request |
||
336 | */ |
||
337 | public function migrate(array $request) |
||
338 | { |
||
339 | $form = $this->getEditForm(); |
||
340 | $migrationTarget = isset($request['MigrationTarget']) ? $request['MigrationTarget'] : ''; |
||
341 | $fileMigrationTarget = isset($request['FileMigrationTarget']) ? $request['FileMigrationTarget'] : ''; |
||
342 | $includeSelected = isset($request['IncludeSelected']) ? $request['IncludeSelected'] : 0; |
||
343 | $includeChildren = isset($request['IncludeChildren']) ? $request['IncludeChildren'] : 0; |
||
344 | $duplicates = isset($request['DuplicateMethod']) ? $request['DuplicateMethod'] : ExternalContentTransformer::DS_OVERWRITE; |
||
345 | $selected = isset($request['ID']) ? $request['ID'] : 0; |
||
346 | |||
347 | if (!$selected) { |
||
348 | $messageType = 'bad'; |
||
349 | $message = _t('ExternalContent.NOITEMSELECTED', 'No item selected to import into.'); |
||
350 | |||
351 | $form->sessionError($message, $messageType); |
||
352 | |||
353 | return $this->getResponseNegotiator()->respond($this->request); |
||
354 | } elseif (!($migrationTarget && $fileMigrationTarget)) { |
||
355 | $messageType = 'bad'; |
||
356 | $message = _t('ExternalContent.NOTARGETSELECTED', 'No target(s) selected to import crawled content into.'); |
||
357 | |||
358 | $form->sessionError($message, $messageType); |
||
359 | return $this->getResponseNegotiator()->respond($this->request); |
||
360 | } else { |
||
361 | if ($migrationTarget) { |
||
362 | $targetType = SiteTree::class; |
||
363 | $target = DataObject::get_by_id($targetType, $migrationTarget); |
||
364 | } elseif ($fileMigrationTarget) { |
||
365 | $targetType = File::class; |
||
366 | $target = DataObject::get_by_id($targetType, $fileMigrationTarget); |
||
367 | } |
||
368 | |||
369 | $from = ExternalContent::getDataObjectFor($selected); |
||
370 | // Don't use an instance of ExternalContentSource which has nothing to do with site-tree's but is yet |
||
371 | // strangely selectable from Silverstripe's UI |
||
372 | if ($from instanceof ExternalContentSource) { |
||
373 | $selected = false; |
||
374 | } |
||
375 | |||
376 | if (isset($request['Repeat']) && $request['Repeat'] > 0) { |
||
377 | $job = ScheduledExternalImportJob::create($request['Repeat'], $from, $target, $includeSelected, $includeChildren, $targetType, $duplicates, $request); |
||
378 | singleton(QueuedJobService::class)->queueJob($job); |
||
379 | |||
380 | $messageType = 'good'; |
||
381 | $message = _t('ExternalContent.CONTENTMIGRATEQUEUED', 'Import job queued.'); |
||
382 | } else { |
||
383 | if ($importer = $from->getContentImporter($targetType)) { |
||
384 | $result = $importer->import($from, $target, $includeSelected, $includeChildren, $duplicates, $request); |
||
385 | |||
386 | if ($result instanceof QueuedExternalContentImporter) { |
||
387 | $messageType = 'good'; |
||
388 | $message = _t('ExternalContent.CONTENTMIGRATEQUEUED', 'Import job queued.'); |
||
389 | } else { |
||
390 | $messageType = 'good'; |
||
391 | $message = _t('ExternalContent.CONTENTMIGRATED', 'Import Successful. Please review the CMS site-tree.'); |
||
392 | } |
||
393 | } |
||
394 | } |
||
395 | } |
||
396 | |||
397 | $form->sessionError($message, $messageType); |
||
398 | return $this->getResponseNegotiator()->respond($this->request); |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * Return the record corresponding to the given ID. |
||
403 | * |
||
404 | * Both the numeric IDs of ExternalContentSource records and the composite IDs of ExternalContentItem entries |
||
405 | * are supported. |
||
406 | * |
||
407 | * @param string $id The ID |
||
408 | * @param string $versionID |
||
409 | * @return Dataobject The relevant object |
||
410 | */ |
||
411 | public function getRecord($id, $versionID = null) |
||
412 | { |
||
413 | if (is_numeric($id)) { |
||
414 | return parent::getRecord($id); |
||
415 | } |
||
416 | |||
417 | return ExternalContent::getDataObjectFor($id); |
||
418 | } |
||
419 | |||
420 | |||
421 | /** |
||
422 | * Return the edit form or null on error. |
||
423 | * |
||
424 | * @param array $request |
||
425 | * @return mixed Form|boolean |
||
426 | * @see LeftAndMain#EditForm() |
||
427 | */ |
||
428 | public function EditForm($request = null) |
||
429 | { |
||
430 | if ($curr = $this->getCurrentPageID()) { |
||
431 | if (!$record = $this->currentPage()) { |
||
432 | return false; |
||
433 | } |
||
434 | |||
435 | if (!$record->canView()) { |
||
436 | return Security::permissionFailure($this); |
||
437 | } |
||
438 | } |
||
439 | |||
440 | if ($this->hasMethod('getEditForm')) { |
||
441 | return $this->getEditForm($curr); |
||
442 | } |
||
443 | |||
444 | return false; |
||
445 | } |
||
446 | |||
447 | /** |
||
448 | * Return the form for editing |
||
449 | */ |
||
450 | public function getEditForm($id = null, $fields = null) |
||
451 | { |
||
452 | if (!$id) { |
||
453 | $id = $this->getCurrentPageID(); |
||
454 | } |
||
455 | |||
456 | if ($id && $id != "root") { |
||
457 | $record = $this->getRecord($id); |
||
458 | } |
||
459 | |||
460 | if (isset($record)) { |
||
461 | $fields = $record->getCMSFields(); |
||
462 | |||
463 | // If we're editing an external source or item, and it can be imported |
||
464 | // then add the "Import" tab. |
||
465 | $isSource = $record instanceof ExternalContentSource; |
||
466 | $isItem = $record instanceof ExternalContentItem; |
||
467 | |||
468 | // TODO Get the crawl status and show the "Import" tab if crawling has completed |
||
469 | if (($isSource || $isItem) && $record->canImport()) { |
||
470 | $allowedTypes = $record->allowedImportTargets(); |
||
471 | |||
472 | if (isset($allowedTypes['sitetree'])) { |
||
473 | $fields->addFieldToTab( |
||
474 | 'Root.Import', |
||
475 | TreeDropdownField::create( |
||
476 | "MigrationTarget", |
||
477 | _t('ExternalContent.MIGRATE_TARGET', 'Parent Page To Import Into'), |
||
478 | SiteTree::class, |
||
479 | ) |
||
480 | ->setValue($this->getRequest()->postVars()['MigrationTarget'] ?? null) |
||
481 | ->setDescription('All imported page-like content will be organised hierarchically under here.') |
||
482 | ); |
||
483 | } |
||
484 | |||
485 | if (isset($allowedTypes['file'])) { |
||
486 | $fields->addFieldToTab( |
||
487 | 'Root.Import', |
||
488 | TreeDropdownField::create( |
||
489 | "FileMigrationTarget", |
||
490 | _t('ExternalContent.FILE_MIGRATE_TARGET', 'Parent Folder To Import Into'), |
||
491 | Folder::class, |
||
492 | ) |
||
493 | ->setValue($this->getRequest()->postVars()['FileMigrationTarget'] ?? null) |
||
494 | ->setDescription('All imported file-like content will be organised hierarchically under here.') |
||
495 | ); |
||
496 | } |
||
497 | |||
498 | // This tells the module to include the selected "SiteTee" and "Folder" subclasses |
||
499 | // to be _included_ in the import. But it throws exceptions. |
||
500 | $fields->addFieldToTab( |
||
501 | 'Root.Import', |
||
502 | CheckboxField::create( |
||
503 | "IncludeSelected", |
||
504 | _t('ExternalContent.INCLUDE_SELECTED', 'Include Selected Item in Import') |
||
505 | )->setAttribute('disabled', true) // <-- temporary |
||
506 | ); |
||
507 | |||
508 | $fields->addFieldToTab( |
||
509 | 'Root.Import', |
||
510 | CheckboxField::create( |
||
511 | "IncludeChildren", |
||
512 | _t('ExternalContent.INCLUDE_CHILDREN', 'Include Child Items in Import'), |
||
513 | true |
||
514 | ) |
||
515 | ); |
||
516 | |||
517 | $duplicateOptions = [ |
||
518 | ExternalContentTransformer::DS_OVERWRITE => ExternalContentTransformer::DS_OVERWRITE, |
||
519 | ExternalContentTransformer::DS_DUPLICATE => ExternalContentTransformer::DS_DUPLICATE, |
||
520 | ExternalContentTransformer::DS_SKIP => ExternalContentTransformer::DS_SKIP, |
||
521 | ]; |
||
522 | |||
523 | $fields->addFieldToTab( |
||
524 | 'Root.Import', |
||
525 | OptionsetField::create( |
||
526 | "DuplicateMethod", |
||
527 | _t('ExternalContent.DUPLICATES', 'Duplicate Item Handling'), |
||
528 | $duplicateOptions, |
||
529 | $duplicateOptions[ExternalContentTransformer::DS_SKIP] |
||
530 | )->setValue($this->getRequest()->postVars()['DuplicateMethod'] ?? ExternalContentTransformer::DS_SKIP) |
||
531 | ); |
||
532 | |||
533 | if (class_exists(QueuedJobDescriptor::class)) { |
||
534 | $repeats = [ |
||
535 | 0 => 'None', |
||
536 | 300 => '5 minutes', |
||
537 | 900 => '15 minutes', |
||
538 | 1800 => '30 minutes', |
||
539 | 3600 => '1 hour', |
||
540 | 33200 => '12 hours', |
||
541 | 86400 => '1 day', |
||
542 | 604800 => '1 week', |
||
543 | ]; |
||
544 | |||
545 | $fields->addFieldToTab( |
||
546 | 'Root.Import', |
||
547 | DropdownField::create('Repeat', 'Import Repeat Interval', $repeats) |
||
548 | ->setValue($this->getRequest()->postVars()['Repeat'] ?? null) |
||
549 | ); |
||
550 | } |
||
551 | |||
552 | $migrateButton = FormAction::create('migrate', _t('ExternalContent.IMPORT', 'Start Importing')) |
||
553 | ->setAttribute('data-icon', 'arrow-circle-double') |
||
554 | ->addExtraClass('btn action btn btn-primary tool-button font-icon-plus') |
||
555 | ->setUseButtonTag(true); |
||
556 | |||
557 | $fields->addFieldToTab( |
||
558 | 'Root.Import', |
||
559 | LiteralField::create( |
||
560 | 'MigrateActions', |
||
561 | '<div class="btn-toolbar">' . $migrateButton->forTemplate() . '</div>' |
||
562 | ) |
||
563 | ); |
||
564 | } |
||
565 | |||
566 | $fields->push($hf = HiddenField::create("ID")); |
||
567 | $hf->setValue($id); |
||
568 | |||
569 | $fields->push($hf = HiddenField::create("Version")); |
||
570 | $hf->setValue(1); |
||
571 | |||
572 | $actions = FieldList::create(); |
||
573 | |||
574 | // Only show save button if not 'assets' folder |
||
575 | if ($record->canEdit()) { |
||
576 | $actions->push( |
||
577 | FormAction::create('save', _t('ExternalContent.SAVE', 'Save')) |
||
578 | ->addExtraClass('save btn btn-primary tool-button font-icon-plus') |
||
579 | ->setAttribute('data-icon', 'accept') |
||
580 | ->setUseButtonTag(true) |
||
581 | ); |
||
582 | } |
||
583 | |||
584 | if ($isSource && $record->canDelete()) { |
||
585 | $actions->push( |
||
586 | FormAction::create('delete', _t('ExternalContent.DELETE', 'Delete')) |
||
587 | ->addExtraClass('delete btn btn-primary tool-button font-icon-plus') |
||
588 | ->setAttribute('data-icon', 'decline') |
||
589 | ->setUseButtonTag(true) |
||
590 | ); |
||
591 | } |
||
592 | |||
593 | $form = Form::create($this, "EditForm", $fields, $actions); |
||
594 | |||
595 | if ($record->exists()) { |
||
596 | $form->loadDataFrom($record); |
||
597 | } else { |
||
598 | $form->loadDataFrom([ |
||
599 | "ID" => "root", |
||
600 | "URL" => Director::absoluteBaseURL() . self::$url_segment, |
||
601 | ]); |
||
602 | } |
||
603 | |||
604 | if (!$record->canEdit()) { |
||
605 | $form->makeReadonly(); |
||
606 | } |
||
607 | } else { |
||
608 | // Create a dummy form |
||
609 | $form = Form::create($this, "EditForm", FieldList::create(), FieldList::create()); |
||
610 | } |
||
611 | |||
612 | $form |
||
613 | ->addExtraClass('cms-edit-form center ss-tabset ' . $this->BaseCSSClasses()) |
||
614 | ->setTemplate($this->getTemplatesWithSuffix('_EditForm')) |
||
615 | ->setAttribute('data-pjax-fragment', 'CurrentForm'); |
||
616 | |||
617 | $this->extend('updateEditForm', $form); |
||
618 | |||
619 | return $form; |
||
620 | } |
||
621 | |||
622 | /** |
||
623 | * Get the form used to create a new provider |
||
624 | * |
||
625 | * @return Form |
||
626 | */ |
||
627 | public function AddForm() |
||
628 | { |
||
629 | $classes = ClassInfo::subclassesFor(self::$tree_class); |
||
630 | array_shift($classes); |
||
631 | |||
632 | foreach ($classes as $key => $class) { |
||
633 | if (!$class::create()->canCreate()) { |
||
634 | unset($classes[$key]); |
||
635 | } |
||
636 | |||
637 | $visible = explode('\\', $class); |
||
638 | $classes[$key] = FormField::name_to_label($visible[count($visible)-1]); |
||
639 | } |
||
640 | |||
641 | $fields = FieldList::create( |
||
642 | HiddenField::create("ParentID"), |
||
643 | HiddenField::create("Locale", 'Locale', i18n::get_locale()), |
||
644 | DropdownField::create("ProviderType", "", $classes) |
||
645 | ); |
||
646 | |||
647 | $actions = FieldList::create( |
||
648 | FormAction::create("addprovider", _t('ExternalContent.CREATE', "Create")) |
||
649 | ->addExtraClass('btn btn-primary tool-button font-icon-plus') |
||
650 | ->setAttribute('data-icon', 'accept') |
||
651 | ->setUseButtonTag(true) |
||
652 | ); |
||
653 | |||
654 | $form = Form::create($this, "AddForm", $fields, $actions); |
||
655 | |||
656 | $this->extend('updateEditForm', $form); |
||
657 | |||
658 | return $form; |
||
659 | } |
||
660 | |||
661 | /** |
||
662 | * Add a new provider (triggered by the ExternalContentAdmin_left template) |
||
663 | * |
||
664 | * @return unknown_type |
||
665 | */ |
||
666 | public function addprovider() |
||
667 | { |
||
668 | // Providers are ALWAYS at the root |
||
669 | $parent = 0; |
||
670 | $name = (isset($_REQUEST['Name'])) ? |
||
671 | basename($_REQUEST['Name']) : |
||
672 | _t('ExternalContent.NEWCONNECTOR', "New Connector"); |
||
673 | $type = $_REQUEST['ProviderType']; |
||
674 | $providerClasses = array_map( |
||
675 | function ($item) { |
||
676 | return strtolower($item); |
||
677 | }, |
||
678 | ClassInfo::subclassesFor(self::$tree_class) |
||
679 | ); |
||
680 | |||
681 | if (!in_array($type, $providerClasses)) { |
||
682 | throw new \Exception("Invalid connector type"); |
||
683 | } |
||
684 | |||
685 | $parentObj = null; |
||
686 | |||
687 | // Create object |
||
688 | $record = $type::create(); |
||
689 | $record->ParentID = $parent; |
||
690 | $record->Name = $record->Title = $name; |
||
691 | |||
692 | // if (isset($_REQUEST['returnID'])) { |
||
693 | // return $p->ID; |
||
694 | // } else { |
||
695 | // return $this->returnItemToUser($p); |
||
696 | // } |
||
697 | |||
698 | $message = sprintf(_t('ExternalContent.SourceAdded', 'Successfully created %s'), $type); |
||
699 | $messageType = 'good'; |
||
700 | |||
701 | try { |
||
702 | $record->write(); |
||
703 | } catch (ValidationException $ex) { |
||
704 | $message = sprintf(_t('ExternalContent.SourceAddedFailed', 'Not successfully created %s'), $type); |
||
705 | $messageType = 'bad'; |
||
706 | } |
||
707 | |||
708 | $this->setCurrentPageID($record->ID); |
||
709 | $this->getEditForm()->sessionError($message, $messageType); |
||
710 | |||
711 | return $this->getResponseNegotiator()->respond($this->request); |
||
712 | } |
||
713 | |||
714 | /** |
||
715 | * Copied from AssetAdmin... |
||
716 | * |
||
717 | * @return Form |
||
718 | */ |
||
719 | public function DeleteItemsForm() |
||
720 | { |
||
721 | $form = Form::create( |
||
722 | $this, |
||
723 | 'DeleteItemsForm', |
||
724 | FieldList::create( |
||
725 | LiteralField::create( |
||
726 | 'SelectedPagesNote', |
||
727 | sprintf('<p>%s</p>', _t('ExternalContentAdmin.SELECT_CONNECTORS', 'Select the connectors that you want to delete and then click the button below')) |
||
728 | ), |
||
729 | HiddenField::create('csvIDs') |
||
730 | ), |
||
731 | FieldList::create( |
||
732 | FormAction::create('deleteprovider', _t('ExternalContentAdmin.DELCONNECTORS', 'Delete the selected connectors')) |
||
733 | ) |
||
734 | ); |
||
735 | |||
736 | $form->addExtraClass('actionparams'); |
||
737 | |||
738 | return $form; |
||
739 | } |
||
740 | |||
741 | /** |
||
742 | * Delete a folder |
||
743 | */ |
||
744 | public function deleteprovider() |
||
745 | { |
||
746 | $script = ''; |
||
747 | $ids = explode(' *, *', $_REQUEST['csvIDs']); |
||
748 | $script = ''; |
||
749 | |||
750 | if (!$ids) { |
||
751 | return false; |
||
752 | } |
||
753 | |||
754 | foreach ($ids as $id) { |
||
755 | if (is_numeric($id)) { |
||
756 | $record = ExternalContent::getDataObjectFor($id); |
||
757 | |||
758 | if ($record) { |
||
759 | $script .= $this->deleteTreeNodeJS($record); |
||
760 | $record->delete(); |
||
761 | $record->destroy(); |
||
762 | } |
||
763 | } |
||
764 | } |
||
765 | |||
766 | $size = sizeof($ids); |
||
767 | |||
768 | if ($size > 1) { |
||
769 | $message = $size . ' ' . _t('AssetAdmin.FOLDERSDELETED', 'folders deleted.'); |
||
770 | } else { |
||
771 | $message = $size . ' ' . _t('AssetAdmin.FOLDERDELETED', 'folder deleted.'); |
||
772 | } |
||
773 | |||
774 | $script .= "statusMessage('$message');"; |
||
775 | echo $script; |
||
776 | } |
||
777 | |||
778 | public function getCMSTreeTitle() |
||
779 | { |
||
780 | return 'Connectors'; |
||
781 | } |
||
782 | |||
783 | /** |
||
784 | * @return string |
||
785 | */ |
||
786 | public function treeview() |
||
787 | { |
||
788 | return $this->renderWith($this->getTemplatesWithSuffix('_TreeView')); |
||
789 | } |
||
790 | |||
791 | /** |
||
792 | * @return string |
||
793 | */ |
||
794 | public function SiteTreeAsUL() |
||
795 | { |
||
796 | $html = $this->getSiteTreeFor($this->config()->get('tree_class'), null, null, 'NumChildren'); |
||
797 | //$this->extend('updateSiteTreeAsUL', $html); |
||
798 | |||
799 | return $html; |
||
800 | } |
||
801 | |||
802 | /** |
||
803 | * Get a subtree underneath the request param 'ID'. |
||
804 | * If ID = 0, then get the whole tree. |
||
805 | */ |
||
806 | public function getsubtree($request) |
||
807 | { |
||
808 | $html = $this->getSiteTreeFor( |
||
809 | ExternalContentItem::class, |
||
810 | $request->getVar('ID'), |
||
811 | null, |
||
812 | 'NumChildren', |
||
813 | null, |
||
814 | $request->getVar('minNodeCount') |
||
815 | ); |
||
816 | |||
817 | // Trim off the outer tag |
||
818 | $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html); |
||
819 | $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html); |
||
820 | |||
821 | return $html; |
||
822 | } |
||
823 | |||
824 | |||
825 | /** |
||
826 | * Include CSS for page icons. We're not using the JSTree 'types' option |
||
827 | * because it causes too much performance overhead just to add some icons. |
||
828 | * |
||
829 | * @return String CSS |
||
830 | */ |
||
831 | public function generatePageIconsCss() |
||
832 | { |
||
833 | $css = ''; |
||
834 | |||
835 | $sourceClasses = ClassInfo::subclassesFor(ExternalContentSource::class); |
||
836 | $itemClasses = ClassInfo::subclassesFor(ExternalContentItem::class); |
||
837 | $classes = array_merge($sourceClasses, $itemClasses); |
||
838 | |||
839 | foreach ($classes as $class) { |
||
840 | $obj = singleton($class); |
||
841 | $iconSpec = $obj->config()->get('icon'); |
||
842 | |||
843 | if (!$iconSpec) { |
||
844 | continue; |
||
845 | } |
||
846 | |||
847 | // Legacy support: We no longer need separate icon definitions for folders etc. |
||
848 | $iconFile = (is_array($iconSpec)) ? $iconSpec[0] : $iconSpec; |
||
849 | |||
850 | // Legacy support: Add file extension if none exists |
||
851 | if (!pathinfo($iconFile, PATHINFO_EXTENSION)) { |
||
852 | $iconFile .= '-file.gif'; |
||
853 | } |
||
854 | |||
855 | $iconPathInfo = pathinfo($iconFile); |
||
856 | |||
857 | // Base filename |
||
858 | $baseFilename = $iconPathInfo['dirname'] . '/' . $iconPathInfo['filename']; |
||
859 | $fileExtension = $iconPathInfo['extension']; |
||
860 | |||
861 | $selector = ".page-icon.class-$class, li.class-$class > a .jstree-pageicon"; |
||
862 | |||
863 | if (Director::fileExists($iconFile)) { |
||
864 | $css .= "$selector { background: transparent url('$iconFile') 0 0 no-repeat; }\n"; |
||
865 | } else { |
||
866 | // Support for more sophisticated rules, e.g. sprited icons |
||
867 | $css .= "$selector { $iconFile }\n"; |
||
868 | } |
||
869 | } |
||
870 | |||
871 | $css .= "li.type-file > a .jstree-pageicon { background: transparent url('framework/admin/images/sitetree_ss_pageclass_icons_default.png') 0 0 no-repeat; }\n}"; |
||
872 | |||
873 | return $css; |
||
874 | } |
||
875 | |||
876 | /** |
||
877 | * Save the content source/item. |
||
878 | */ |
||
879 | public function save($urlParams, $form) |
||
880 | { |
||
881 | // Retrieve the record. |
||
882 | $record = null; |
||
883 | |||
884 | if (isset($urlParams['ID'])) { |
||
885 | $record = ExternalContent::getDataObjectFor($urlParams['ID']); |
||
886 | } |
||
887 | |||
888 | if (!$record) { |
||
889 | return parent::save($urlParams, $form); |
||
890 | } |
||
891 | |||
892 | if ($record->canEdit()) { |
||
893 | // lets load the params that have been sent and set those that have an editable mapping |
||
894 | if ($record->hasMethod('editableFieldMapping')) { |
||
895 | $editable = $record->editableFieldMapping(); |
||
896 | $form->saveInto($record, array_keys($editable)); |
||
897 | $record->remoteWrite(); |
||
898 | } else { |
||
899 | $form->saveInto($record); |
||
900 | $record->write(); |
||
901 | } |
||
902 | |||
903 | // Set the form response. |
||
904 | $this->response->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.'))); |
||
905 | } else { |
||
906 | $this->response->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'You don\'t have write access.'))); |
||
907 | } |
||
908 | |||
909 | return $this->getResponseNegotiator()->respond($this->request); |
||
910 | } |
||
911 | |||
912 | /** |
||
913 | * Delete the content source/item. |
||
914 | */ |
||
915 | public function delete($data, $form) |
||
916 | { |
||
917 | $className = $this->config()->get('tree_class'); |
||
918 | $record = DataObject::get_by_id($className, Convert::raw2sql($data['ID'])); |
||
919 | |||
920 | if ($record && !$record->canDelete()) { |
||
921 | return Security::permissionFailure(); |
||
922 | } |
||
923 | |||
924 | if (!$record || !$record->ID) { |
||
925 | $this->httpError(404, "Bad record ID #" . (int)$data['ID']); |
||
926 | } |
||
927 | |||
928 | $type = $record->ClassName; |
||
929 | $record->delete(); |
||
930 | $message = "Deleted $type successfully."; |
||
931 | $messageType = 'bad'; |
||
932 | $this->getEditForm()->sessionError($message, $messageType); |
||
933 | |||
934 | $this->response->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.'))); |
||
935 | |||
936 | return $this->getResponseNegotiator()->respond( |
||
937 | $this->request, |
||
938 | array('currentform' => array($this, 'EmptyForm')) |
||
939 | ); |
||
940 | } |
||
941 | |||
942 | /** |
||
943 | * Retrieve the updated source list, used in an AJAX request to update the current view. |
||
944 | * |
||
945 | * @return string |
||
946 | */ |
||
947 | public function updateSources(): string |
||
948 | { |
||
949 | $HTML = $this->treeview()->value; |
||
950 | |||
951 | return preg_replace('/^\s+|\n|\r|\s+$/m', '', $HTML); |
||
952 | } |
||
953 | |||
954 | /** |
||
955 | * @param HTTPRequest $request |
||
956 | * @return HTTPResponse |
||
957 | */ |
||
958 | public function updatetreenodes(HTTPRequest $request): HTTPResponse |
||
959 | { |
||
960 | // noop |
||
961 | return singleton(CMSMain::class)->updatetreenodes($request); |
||
962 | } |
||
964 |