| 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 |