public function transform(FormField $field) { if (!$field instanceof GridField) { throw new Exception(__CLASS__ . ' requires GridField FormField type.'); } $title = $field->Title(); $list = $field->getList(); $config = $field->getConfig(); $result = MultiRecordField::create($field->getName(), $title, $list); // Support: GridFieldExtensions (https://github.com/silverstripe-australia/silverstripe-gridfieldextensions) $gridFieldAddNewMultiClass = $config->getComponentsByType('GridFieldAddNewMultiClass')->first(); if ($gridFieldAddNewMultiClass) { $classes = $gridFieldAddNewMultiClass->getClasses($field); $result->setModelClasses($classes); } return $result; }
/** * @return string */ public function getFieldID() { return $this->parent->getFieldID($this->record); }
public function saveInto(\DataObjectInterface $record) { if ($this->depth == 1) { // Reset records to write for top-level MultiRecordField. self::$_new_records_to_write = array(); self::$_existing_records_to_write = array(); self::$_records_to_delete = array(); } $class_id_field = $this->Value(); if (!$class_id_field) { return $this; } $list = $this->list; // Workaround for #5775 - Fix bug where ListboxField writes to $record, making // UnsavedRelationList redundant. // https://github.com/silverstripe/silverstripe-framework/pull/5775 $relationName = $this->getName(); $relation = $record->hasMethod($relationName) ? $record->{$relationName}() : null; if ($relation) { // When ListboxField (or other) has saved a new record in its 'saveInto' function if ($record->ID && $list instanceof UnsavedRelationList) { if ($this->config()->enable_patch_5775 === false) { throw new Exception("ListboxField or another FormField called DataObject::write() when it wasn't meant to on your unsaved record. https://github.com/silverstripe/silverstripe-framework/pull/5775 ---- Enable 'enable_patch_5775' in your config YML against " . __CLASS__ . " to enable a workaround."); } if ($relation instanceof ElementalArea) { // Hack to support Elemental $relation = $relation->Elements(); } else { if ($relation instanceof DataObject) { throw new Exception("Unable to use enable_patch_5775 workaround as \"" . $record->class . "\"::\"" . $relationName . "\"() does not return a DataList."); } } $list = $relation; } } $flatList = array(); if ($list instanceof DataList) { $flatList = array(); foreach ($list as $r) { $flatList[$r->ID] = $r; } } else { if (!$list instanceof UnsavedRelationList) { throw new Exception('Expected SS_List, but got "' . $list->class . '" in ' . __CLASS__); } } $sortFieldName = $this->getSortFieldName(); foreach ($class_id_field as $class => $id_field) { // Create and add records to list foreach ($id_field as $idString => $subRecordData) { if (strpos($idString, 'o-multirecordediting') !== FALSE) { throw new Exception('Invalid template ID passed in ("' . $idString . '"). This should have been replaced by MultiRecordField.js. Is your JavaScript broken?'); } $idParts = explode('_', $idString); $id = 0; $subRecord = null; if ($idParts[0] === 'new') { if (!isset($idParts[1])) { throw new Exception('Missing ID part of "new_" identifier.'); } $id = (int) $idParts[1]; if (!$id && $id > 0) { throw new Exception('Invalid ID part of "new_" identifier. Positive Non-Zero Integers only are accepted.'); } // New record $subRecord = $class::create(); } else { $id = $idParts[0]; // Find existing $id = (int) $id; if (!isset($flatList[$id])) { throw new Exception('Record #' . $id . ' on "' . $class . '" does not exist in this DataList context. (From ID string: ' . $idString . ')'); } $subRecord = $flatList[$id]; } // Detect if record was deleted if (isset($subRecordData['multirecordfield_delete']) && $subRecordData['multirecordfield_delete']) { if ($subRecord && $subRecord->exists()) { self::$_records_to_delete[] = $subRecord; } continue; } // maybetodo(Jake): To improve performance, maybe add 'dumb fields' config where it just gets the fields available // on an unsaved record and just re-uses them for each instance. Of course // this means conditional fields based on parent values/db values wont work. $fields = $this->getRecordDataFields($subRecord); $fields = $fields->dataFields(); if (!$fields) { throw new Exception($class . ' is returning 0 fields.'); } // foreach ($subRecordData as $fieldName => $fieldData) { if ($sortFieldName !== $fieldName && !isset($fields[$fieldName]) && strpos($fieldName, '_ClassName') == false) { // todo(Jake): Say whether its missing the field from getCMSFields or getMultiRecordFields or etc. throw new Exception('Missing field "' . $fieldName . '" from "' . $subRecord->class . '" fields based on data sent from client. (Could be a hack attempt)'); } if (isset($fields[$fieldName])) { $field = $fields[$fieldName]; if (!$field instanceof MultiRecordField) { $value = $fieldData->value; } else { $value = $fieldData; } if ($field) { // NOTE(Jake): Added for FileAttachmentField as it uses the name used in the request for // file deletion. $field->MultiRecordEditing_Name = $this->getUniqueFieldName($field->getName(), $subRecord); $field->setValue($value); // todo(Jake): Some field types (ie. UploadField/FileAttachmentField) directly modify the record // on 'saveInto', meaning people -could- circumvent certain permission checks // potentially. Must test this or defer extensions of 'FileField' to 'saveInto' later. $field->saveInto($subRecord); $field->MultiRecordField_SavedInto = true; } } } // Handle sort if its not manually handled on the form if ($sortFieldName && !isset($fields[$sortFieldName])) { $newSortValue = $id; // Default to order added if (isset($subRecordData[$sortFieldName])) { $newSortValue = $subRecordData[$sortFieldName]; } if ($newSortValue) { $subRecord->{$sortFieldName} = $newSortValue; } } // Check if sort value is invalid $sortValue = $subRecord->{$sortFieldName}; if ($sortValue <= 0) { throw new Exception('Invalid sort value (' . $sortValue . ') on #' . $subRecord->ID . ' for class ' . $subRecord->class . '. Sort value must be greater than 0.'); } if (!$subRecord->doValidate()) { throw new ValidationException('Failed validation on ' . $subRecord->class . '::doValidate() on record #' . $subRecord->ID); } if ($subRecord->exists()) { self::$_existing_records_to_write[] = $subRecord; } else { // NOTE(Jake): I used to directly add the record to the list here, but // if it's a HasManyList/ManyManyList, it will create the record // before doing permission checks. self::$_new_records_to_write[] = array(self::NEW_RECORD => $subRecord, self::NEW_LIST => $list); } } } // The top-most MutliRecordField handles all the permission checking/saving at once if ($this->depth == 1) { // Remove records from list that haven't been changed to avoid unnecessary // permission check and ->write overhead foreach (self::$_existing_records_to_write as $i => $subRecord) { $hasRecordChanged = false; $changedFields = $subRecord->getChangedFields(true); foreach ($changedFields as $field => $data) { $hasRecordChanged = $hasRecordChanged || $data['before'] != $data['after']; } if (!$hasRecordChanged) { // Remove from list, stops the record from calling ->write() unset(self::$_existing_records_to_write[$i]); } } // // Check permissions on everything at once // (includes records added in nested-nested-nested-etc MultiRecordField's) // $currentMember = Member::currentUser(); $recordsPermissionUnable = array(); foreach (self::$_new_records_to_write as $subRecordAndList) { $subRecord = $subRecordAndList[self::NEW_RECORD]; // Check each new record to see if you can create them if (!$subRecord->canCreate($currentMember)) { $recordsPermissionUnable['canCreate'][$subRecord->class][$subRecord->ID] = true; } } foreach (self::$_existing_records_to_write as $subRecord) { // Check each existing record to see if you can edit them if (!$subRecord->canEdit($currentMember)) { $recordsPermissionUnable['canEdit'][$subRecord->class][$subRecord->ID] = true; } } foreach (self::$_records_to_delete as $subRecord) { // Check each record deleting to see if you can delete them if (!$subRecord->canDelete($currentMember)) { $recordsPermissionUnable['canDelete'][$subRecord->class][$subRecord->ID] = true; } } if ($recordsPermissionUnable) { /** * Output a nice exception/error message telling you exactly what records/classes * the permissions failed on. * * eg. * Current member #7 does not have permission. * * Unable to "canCreate" records: * - ElementGallery (26) * * Unable to "canEdit" records: * - ElementGallery (24,23,22) * - ElementGallery_Item (16,23,17,18,19,20,22,21) */ $message = ''; foreach ($recordsPermissionUnable as $permissionFunction => $classAndID) { $message .= "\n" . 'Unable to "' . $permissionFunction . '" records: ' . "\n"; foreach ($classAndID as $class => $idAsKeys) { $message .= '- ' . $class . ' (' . implode(',', array_keys($idAsKeys)) . ')' . "\n"; } } throw new Exception('Current member #' . Member::currentUserID() . ' does not have permission.' . "\n" . $message); } // Add new records into the appropriate list foreach (self::$_new_records_to_write as $subRecordAndList) { $list = $subRecordAndList[self::NEW_LIST]; if ($list instanceof UnsavedRelationList || $list instanceof RelationList) { $subRecord = $subRecordAndList[self::NEW_RECORD]; // NOTE(Jake): Adding an empty record into an existing ManyManyList/HasManyList -seems- to create that record. $list->add($subRecord); } else { throw new Exception('Unsupported SS_List type "' . $list->class . '"'); } } // Debugging (for looking at UnsavedRelationList's to ensure $_new_records_to_write is working) // NOTE(Jake): Added to debug Frontend Objects module support //Debug::dump($record); Debug::dump($relation_class_id_field); exit('Exited at: '.__CLASS__.'::'.__FUNCTION__);// Debug raw request information tree // Save existing items foreach (self::$_existing_records_to_write as $subRecord) { // NOTE(Jake): Records are checked above to see if they've been changed. // If they haven't been changed, they're removed from the 'self::$_existing_records_to_write' list. $subRecord->write(); } // Remove deleted items foreach (self::$_records_to_delete as $subRecord) { $subRecord->delete(); } } }