/**
  * Return the columns to export
  *
  * @param GridField $gridField
  *
  * @return array
  */
 protected function getExportColumnsForGridField(GridField $gridField)
 {
     if ($this->exportColumns) {
         return $this->exportColumns;
     }
     /** @var GridFieldDataColumns $dataCols */
     $dataCols = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
     if ($dataCols) {
         return $dataCols->getDisplayFields($gridField);
     }
     return DataObject::singleton($gridField->getModelClass())->summaryFields();
 }
 /**
  * Given a relation declared on a remote class, generate a substitute component for the opposite
  * side of the relation.
  *
  * Notes on behaviour:
  *  - This can still be used on components that are defined on both sides, but do not need to be.
  *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
  *  - Cannot be used on polymorphic relationships
  *  - Cannot be used on unsaved objects.
  *
  * @param string $remoteClass
  * @param string $remoteRelation
  * @return DataList|DataObject The component, either as a list or single object
  * @throws BadMethodCallException
  * @throws InvalidArgumentException
  */
 public function inferReciprocalComponent($remoteClass, $remoteRelation)
 {
     $remote = DataObject::singleton($remoteClass);
     $class = $remote->getRelationClass($remoteRelation);
     $schema = static::getSchema();
     // Validate arguments
     if (!$this->isInDB()) {
         throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
     }
     if (empty($class)) {
         throw new InvalidArgumentException(sprintf("%s invoked with invalid relation %s.%s", __METHOD__, $remoteClass, $remoteRelation));
     }
     if ($class === self::class) {
         throw new InvalidArgumentException(sprintf("%s cannot generate opposite component of relation %s.%s as it is polymorphic. " . "This method does not support polymorphic relationships", __METHOD__, $remoteClass, $remoteRelation));
     }
     if (!is_a($this, $class, true)) {
         throw new InvalidArgumentException(sprintf("Relation %s on %s does not refer to objects of type %s", $remoteRelation, $remoteClass, static::class));
     }
     // Check the relation type to mock
     $relationType = $remote->getRelationType($remoteRelation);
     switch ($relationType) {
         case 'has_one':
             // Mock has_many
             $joinField = "{$remoteRelation}ID";
             $componentClass = $schema->classForField($remoteClass, $joinField);
             $result = HasManyList::create($componentClass, $joinField);
             if ($this->model) {
                 $result->setDataModel($this->model);
             }
             return $result->setDataQueryParam($this->getInheritableQueryParams())->forForeignID($this->ID);
         case 'belongs_to':
         case 'has_many':
             // These relations must have a has_one on the other end, so find it
             $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
             if ($polymorphic) {
                 throw new InvalidArgumentException(sprintf("%s cannot generate opposite component of relation %s.%s, as the other end appears" . "to be a has_one polymorphic. This method does not support polymorphic relationships", __METHOD__, $remoteClass, $remoteRelation));
             }
             $joinID = $this->getField($joinField);
             if (empty($joinID)) {
                 return null;
             }
             // Get object by joined ID
             return DataObject::get($remoteClass)->filter('ID', $joinID)->setDataQueryParam($this->getInheritableQueryParams())->first();
         case 'many_many':
         case 'belongs_many_many':
             // Get components and extra fields from parent
             list($relationClass, $componentClass, $parentClass, $componentField, $parentField, $table) = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
             $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
             // Reverse parent and component fields and create an inverse ManyManyList
             /** @var RelationList $result */
             $result = Injector::inst()->create($relationClass, $componentClass, $table, $componentField, $parentField, $extraFields);
             if ($this->model) {
                 $result->setDataModel($this->model);
             }
             $this->extend('updateManyManyComponents', $result);
             // If this is called on a singleton, then we return an 'orphaned relation' that can have the
             // foreignID set elsewhere.
             return $result->setDataQueryParam($this->getInheritableQueryParams())->forForeignID($this->ID);
         default:
             return null;
     }
 }
 public function setUp()
 {
     //nest config and injector for each test so they are effectively sandboxed per test
     Config::nest();
     Injector::nest();
     $this->originalReadingMode = Versioned::get_reading_mode();
     // We cannot run the tests on this abstract class.
     if (get_class($this) == __CLASS__) {
         $this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
         return;
     }
     // Mark test as being run
     $this->originalIsRunningTest = self::$is_running_test;
     self::$is_running_test = true;
     // i18n needs to be set to the defaults or tests fail
     i18n::set_locale(i18n::config()->get('default_locale'));
     i18n::config()->date_format = null;
     i18n::config()->time_format = null;
     // Set default timezone consistently to avoid NZ-specific dependencies
     date_default_timezone_set('UTC');
     // Remove password validation
     $this->originalMemberPasswordValidator = Member::password_validator();
     $this->originalRequirements = Requirements::backend();
     Member::set_password_validator(null);
     Cookie::config()->update('report_errors', false);
     if (class_exists('SilverStripe\\CMS\\Controllers\\RootURLController')) {
         RootURLController::reset();
     }
     if (class_exists('Translatable')) {
         Translatable::reset();
     }
     Versioned::reset();
     DataObject::reset();
     if (class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
         SiteTree::reset();
     }
     Hierarchy::reset();
     if (Controller::has_curr()) {
         Controller::curr()->setSession(Session::create(array()));
     }
     Security::$database_is_ready = null;
     // Add controller-name auto-routing
     // @todo Fix to work with namespaced controllers
     Director::config()->update('rules', array('$Controller//$Action/$ID/$OtherID' => '*'));
     $fixtureFiles = $this->getFixturePaths();
     // Todo: this could be a special test model
     $this->model = DataModel::inst();
     // Set up fixture
     if ($fixtureFiles || $this->usesDatabase) {
         if (!self::using_temp_db()) {
             self::create_temp_db();
         }
         DataObject::singleton()->flushCache();
         self::empty_temp_db();
         foreach ($this->requireDefaultRecordsFrom as $className) {
             $instance = singleton($className);
             if (method_exists($instance, 'requireDefaultRecords')) {
                 $instance->requireDefaultRecords();
             }
             if (method_exists($instance, 'augmentDefaultRecords')) {
                 $instance->augmentDefaultRecords();
             }
         }
         foreach ($fixtureFiles as $fixtureFilePath) {
             $fixture = YamlFixture::create($fixtureFilePath);
             $fixture->writeInto($this->getFixtureFactory());
         }
         $this->logInWithPermission("ADMIN");
     }
     // Preserve memory settings
     $this->originalMemoryLimit = ini_get('memory_limit');
     // turn off template debugging
     SSViewer::config()->update('source_file_comments', false);
     // Clear requirements
     Requirements::clear();
     // Set up email
     $this->mailer = new TestMailer();
     Injector::inst()->registerService($this->mailer, 'SilverStripe\\Control\\Email\\Mailer');
     Email::config()->remove('send_all_emails_to');
 }
 /**
  * Gets summary of items in changeset
  *
  * @return string
  */
 public function getDescription()
 {
     // Initialise list of items to count
     $counted = [];
     $countedOther = 0;
     foreach ($this->config()->important_classes as $type) {
         if (class_exists($type)) {
             $counted[$type] = 0;
         }
     }
     // Check each change item
     /** @var ChangeSetItem $change */
     foreach ($this->Changes() as $change) {
         $found = false;
         foreach ($counted as $class => $num) {
             if (is_a($change->ObjectClass, $class, true)) {
                 $counted[$class]++;
                 $found = true;
                 break;
             }
         }
         if (!$found) {
             $countedOther++;
         }
     }
     // Describe set based on this output
     $counted = array_filter($counted);
     // Empty state
     if (empty($counted) && empty($countedOther)) {
         return '';
     }
     // Put all parts together
     $parts = [];
     foreach ($counted as $class => $count) {
         $parts[] = DataObject::singleton($class)->i18n_pluralise($count);
     }
     // Describe non-important items
     if ($countedOther) {
         if ($counted) {
             $parts[] = i18n::pluralise(_t('ChangeSet.DESCRIPTION_OTHER_ITEM', 'other item'), _t('ChangeSet.DESCRIPTION_OTHER_ITEMS', 'other items'), $countedOther);
         } else {
             $parts[] = i18n::pluralise(_t('ChangeSet.DESCRIPTION_ITEM', 'item'), _t('ChangeSet.DESCRIPTION_ITEMS', 'items'), $countedOther);
         }
     }
     // Figure out how to join everything together
     if (empty($parts)) {
         return '';
     }
     if (count($parts) === 1) {
         return $parts[0];
     }
     // Non-comma list
     if (count($parts) === 2) {
         return _t('ChangeSet.DESCRIPTION_AND', '{first} and {second}', ['first' => $parts[0], 'second' => $parts[1]]);
     }
     // First item
     $string = _t('ChangeSet.DESCRIPTION_LIST_FIRST', '{item}', ['item' => $parts[0]]);
     // Middle items
     for ($i = 1; $i < count($parts) - 1; $i++) {
         $string = _t('ChangeSet.DESCRIPTION_LIST_MID', '{list}, {item}', ['list' => $string, 'item' => $parts[$i]]);
     }
     // Oxford comma
     $string = _t('ChangeSet.DESCRIPTION_LIST_LAST', '{list}, and {item}', ['list' => $string, 'item' => end($parts)]);
     return $string;
 }
 /**
  * @return SearchContext
  */
 public function getSearchContext()
 {
     $context = DataObject::singleton($this->modelClass)->getDefaultSearchContext();
     // Namespace fields, for easier detection if a search is present
     foreach ($context->getFields() as $field) {
         $field->setName(sprintf('q[%s]', $field->getName()));
     }
     foreach ($context->getFilters() as $filter) {
         $filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
     }
     $this->extend('updateSearchContext', $context);
     return $context;
 }
 /**
  * Build item resource from a changesetitem
  *
  * @param ChangeSetItem $changeSetItem
  * @return array
  */
 protected function getChangeSetItemResource(ChangeSetItem $changeSetItem)
 {
     $baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass);
     $baseSingleton = DataObject::singleton($baseClass);
     $thumbnailWidth = (int) $this->config()->thumbnail_width;
     $thumbnailHeight = (int) $this->config()->thumbnail_height;
     $hal = ['_links' => ['self' => ['href' => $this->ItemLink($changeSetItem->ID)]], 'ID' => $changeSetItem->ID, 'Created' => $changeSetItem->Created, 'LastEdited' => $changeSetItem->LastEdited, 'Title' => $changeSetItem->getTitle(), 'ChangeType' => $changeSetItem->getChangeType(), 'Added' => $changeSetItem->Added, 'ObjectClass' => $changeSetItem->ObjectClass, 'ObjectID' => $changeSetItem->ObjectID, 'BaseClass' => $baseClass, 'Singular' => $baseSingleton->i18n_singular_name(), 'Plural' => $baseSingleton->i18n_plural_name(), 'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight)];
     // Get preview urls
     $previews = $changeSetItem->getPreviewLinks();
     if ($previews) {
         $hal['_links']['preview'] = $previews;
     }
     // Get edit link
     $editLink = $changeSetItem->CMSEditLink();
     if ($editLink) {
         $hal['_links']['edit'] = ['href' => $editLink];
     }
     // Depending on whether the object was added implicitly or explicitly, set
     // other related objects.
     if ($changeSetItem->Added === ChangeSetItem::IMPLICITLY) {
         $referencedItems = $changeSetItem->ReferencedBy();
         $referencedBy = [];
         foreach ($referencedItems as $referencedItem) {
             $referencedBy[] = ['href' => $this->SetLink($referencedItem->ID)];
         }
         if ($referencedBy) {
             $hal['_links']['referenced_by'] = $referencedBy;
         }
     }
     return $hal;
 }
 /**
  * Get the whole tree of a part of the tree via an AJAX request.
  *
  * @param HTTPRequest $request
  * @return string
  * @throws Exception
  */
 public function tree(HTTPRequest $request)
 {
     // Array sourceObject is an explicit list of values - construct a "flat tree"
     if (is_array($this->sourceObject)) {
         $output = "<ul class=\"tree\">\n";
         foreach ($this->sourceObject as $k => $v) {
             $output .= '<li id="selector-' . $this->name . '-' . $k . '"><a>' . $v . '</a>';
         }
         $output .= "</ul>";
         return $output;
     }
     // Regular source specification
     $isSubTree = false;
     $this->search = $request->requestVar('search');
     $id = is_numeric($request->latestParam('ID')) ? (int) $request->latestParam('ID') : (int) $request->requestVar('ID');
     /** @var DataObject|Hierarchy $obj */
     $obj = null;
     if ($id && !$request->requestVar('forceFullTree')) {
         $obj = DataObject::get_by_id($this->sourceObject, $id);
         $isSubTree = true;
         if (!$obj) {
             throw new Exception("TreeDropdownField->tree(): the object #{$id} of type {$this->sourceObject} could not be found");
         }
     } else {
         if ($this->baseID) {
             $obj = DataObject::get_by_id($this->sourceObject, $this->baseID);
         }
         if (!$this->baseID || !$obj) {
             $obj = DataObject::singleton($this->sourceObject);
         }
     }
     // pre-process the tree - search needs to operate globally, not locally as marking filter does
     if ($this->search) {
         $this->populateIDs();
     }
     if ($this->filterCallback || $this->search) {
         $obj->setMarkingFilterFunction(array($this, "filterMarking"));
     }
     $obj->markPartialTree($nodeCountThreshold = 30, $context = null, $this->childrenMethod, $this->numChildrenMethod);
     // allow to pass values to be selected within the ajax request
     if (isset($_REQUEST['forceValue']) || $this->value) {
         $forceValue = isset($_REQUEST['forceValue']) ? $_REQUEST['forceValue'] : $this->value;
         $values = preg_split('/,\\s*/', $forceValue);
         if ($values) {
             foreach ($values as $value) {
                 if (!$value || $value == 'unchanged') {
                     continue;
                 }
                 $obj->markToExpose($this->objectForKey($value));
             }
         }
     }
     $self = $this;
     $titleFn = function (&$child) use(&$self) {
         /** @var DataObject|Hierarchy $child */
         $keyField = $self->keyField;
         $labelField = $self->labelField;
         return sprintf('<li id="selector-%s-%s" data-id="%s" class="class-%s %s %s"><a rel="%d">%s</a>', Convert::raw2xml($self->getName()), Convert::raw2xml($child->{$keyField}), Convert::raw2xml($child->{$keyField}), Convert::raw2xml($child->class), Convert::raw2xml($child->markingClasses($self->numChildrenMethod)), $self->nodeIsDisabled($child) ? 'disabled' : '', (int) $child->ID, $child->obj($labelField)->forTemplate());
     };
     // Limit the amount of nodes shown for performance reasons.
     // Skip the check if we're filtering the tree, since its not clear how many children will
     // match the filter criteria until they're queried (and matched up with previously marked nodes).
     $nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf');
     if ($nodeThresholdLeaf && !$this->filterCallback && !$this->search) {
         $className = $this->sourceObject;
         $nodeCountCallback = function ($parent, $numChildren) use($className, $nodeThresholdLeaf) {
             if ($className === 'SilverStripe\\CMS\\Model\\SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
                 return sprintf('<ul><li><span class="item">%s</span></li></ul>', _t('LeftAndMain.TooManyPages', 'Too many pages'));
             }
             return null;
         };
     } else {
         $nodeCountCallback = null;
     }
     if ($isSubTree) {
         $html = $obj->getChildrenAsUL("", $titleFn, null, true, $this->childrenMethod, $this->numChildrenMethod, true, null, $nodeCountCallback);
         return substr(trim($html), 4, -5);
     } else {
         $html = $obj->getChildrenAsUL('class="tree"', $titleFn, null, true, $this->childrenMethod, $this->numChildrenMethod, true, null, $nodeCountCallback);
         return $html;
     }
 }
 /**
  * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
  * a list of record IDs, for more efficient database querying.  If $idList
  * is null, then every record will be pre-cached.
  *
  * @param string $class
  * @param string $stage
  * @param array $idList
  */
 public static function prepopulate_versionnumber_cache($class, $stage, $idList = null)
 {
     if (!Config::inst()->get('SilverStripe\\ORM\\Versioning\\Versioned', 'prepopulate_versionnumber_cache')) {
         return;
     }
     $filter = "";
     $parameters = array();
     if ($idList) {
         // Validate the ID list
         foreach ($idList as $id) {
             if (!is_numeric($id)) {
                 user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id, E_USER_ERROR);
             }
         }
         $filter = 'WHERE "ID" IN (' . DB::placeholders($idList) . ')';
         $parameters = $idList;
     }
     /** @var Versioned|DataObject $singleton */
     $singleton = DataObject::singleton($class);
     $baseClass = $singleton->baseClass();
     $baseTable = $singleton->baseTable();
     $stageTable = $singleton->stageTable($baseTable, $stage);
     $versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"{$stageTable}\" {$filter}", $parameters)->map();
     foreach ($versions as $id => $version) {
         self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
     }
 }
 /**
  * @param array $properties
  * @return string
  */
 public function Field($properties = array())
 {
     Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang');
     Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
     Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');
     Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jstree/jquery.jstree.js');
     Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/TreeDropdownField.js');
     Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css');
     Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/TreeDropdownField.css');
     $item = DataObject::singleton($this->sourceObject);
     $emptyTitle = _t('DropdownField.CHOOSE_MODEL', '(Choose {name})', ['name' => $item->i18n_singular_name()]);
     $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
     if ($record instanceof ViewableData) {
         $title = $record->obj($this->labelField)->forTemplate();
     } elseif ($record) {
         $title = Convert::raw2xml($record->{$this->labelField});
     } else {
         $title = $emptyTitle;
     }
     // TODO Implement for TreeMultiSelectField
     $metadata = array('id' => $record ? $record->ID : null, 'ClassName' => $record ? $record->ClassName : $this->sourceObject);
     $properties = array_merge($properties, array('Title' => $title, 'EmptyTitle' => $emptyTitle, 'Metadata' => $metadata ? Convert::raw2json($metadata) : null));
     return parent::Field($properties);
 }