Test Failed
Push — master ( 357bf5...71cdad )
by Russell
03:53
created

ExternalContentAdmin   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 920
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 113
eloc 413
c 1
b 0
f 1
dl 0
loc 920
rs 2

How to fix   Complexity   

Complex Class

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
2
3
use SilverStripe\CMS\Controllers\CMSMain;
4
use SilverStripe\Admin\LeftAndMain;
5
use SilverStripe\Assets\File;
6
use SilverStripe\Assets\Folder;
7
use SilverStripe\CMS\Model\CurrentPageIdentifier;
8
use SilverStripe\CMS\Model\SiteTree;
9
use SilverStripe\Security\PermissionProvider;
10
use SilverStripe\View\Requirements;
11
use SilverStripe\Core\ClassInfo;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\Security\Security;
14
use SilverStripe\Forms\TreeDropdownField;
15
use SilverStripe\Forms\CheckboxField;
16
use SilverStripe\Forms\OptionsetField;
17
use SilverStripe\Forms\LiteralField;
18
use SilverStripe\Forms\HiddenField;
19
use SilverStripe\Forms\FieldList;
20
use SilverStripe\Forms\FormAction;
21
use SilverStripe\Forms\Form;
22
use SilverStripe\Control\Director;
23
use SilverStripe\Control\HTTPRequest;
24
use SilverStripe\Control\HTTPResponse;
25
use SilverStripe\Core\Config\Config;
26
use SilverStripe\Forms\FormField;
27
use SilverStripe\Forms\DropdownField;
28
use SilverStripe\i18n\i18n;
29
use SilverStripe\ORM\ValidationException;
30
use SilverStripe\Core\Convert;
31
use SilverStripe\ORM\Hierarchy\MarkedSet;
32
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
33
use Symbiote\QueuedJobs\Services\QueuedJobService;
34
35
/**
36
 * Backend administration pages for the external content module
37
 *
38
 * @author Marcus Nyeholt <[email protected]>
39
 * @license BSD License http://silverstripe.org/bsd-license
40
 * @todo (Russell M) Subclass CMSMain instead and overload only the methods that _need_ it (viz SiteTree vs ExternalContentSource)
41
 */
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
    }
963
}
964