/** * @see DataExtension::augmentSQL() */ public function augmentSQL(SQLQuery &$query) { $select = $query->getSelect(); if (empty($select) || $query->getDelete() || in_array("COUNT(*)", $select) || in_array("count(*)", $select)) { return; } if (!isset(self::$sortTables[$this->owner->class])) { $classes = array_reverse(ClassInfo::dataClassesFor($this->owner->class)); $class = null; foreach ($classes as $cls) { if (DataObject::has_own_table($cls) && ($fields = DataObject::database_fields($cls)) && isset($fields['SortOrder'])) { $class = $cls; break; } } self::$sortTables[$this->owner->class] = $class; } else { $class = self::$sortTables[$this->owner->class]; } if ($class) { $query->addOrderBy("\"{$class}\".\"SortOrder\" " . self::$sort_dir); } else { $query->addOrderBy("\"SortOrder\" " . self::$sort_dir); } }
/** * fieldsInExtraTables function. * * @access public * @param mixed $suffix * @return array */ public function fieldsInExtraTables($suffix) { $fields = array(); //$fields['db'] = DataObject::database_fields($this->owner->class); $fields['indexes'] = $this->owner->databaseIndexes(); $fields['db'] = array_merge(DataObject::database_fields($this->owner->class)); return $fields; }
public static function allFieldsForClass($class) { $dataClasses = ClassInfo::dataClassesFor($class); $fields = array(); foreach ($dataClasses as $dataClass) { $fields = array_merge($fields, array_keys(DataObject::database_fields($dataClass))); } return array_combine($fields, $fields); }
/** * Constructor method for MemberTableField. * * @param Controller $controller Controller class which created this field * @param string $name Name of the field (e.g. "Members") * @param mixed $group Can be the ID of a Group instance, or a Group instance itself * @param DataObjectSet $members Optional set of Members to set as the source items for this field * @param boolean $hidePassword Hide the password field or not in the summary? */ function __construct($controller, $name, $group = null, $members = null, $hidePassword = true) { $sourceClass = self::$data_class; $SNG_member = singleton($sourceClass); $fieldList = $SNG_member->summaryFields(); $memberDbFields = DataObject::database_fields('Member'); $csvFieldList = array(); foreach ($memberDbFields as $field => $dbFieldType) { $csvFieldList[$field] = $field; } if ($group) { if (is_object($group)) { $this->group = $group; } elseif (is_numeric($group)) { $this->group = DataObject::get_by_id('Group', $group); } } else { if (isset($_REQUEST['ctf'][$this->Name()]["ID"]) && is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) { $this->group = DataObject::get_by_id('Group', $_REQUEST['ctf'][$this->Name()]["ID"]); } } if (!$hidePassword) { $fieldList["SetPassword"] = "******"; } $this->hidePassword = $hidePassword; // @todo shouldn't this use $this->group? It's unclear exactly // what group it should be customising the custom Member set with. if ($members && $group) { $this->setCustomSourceItems($this->memberListWithGroupID($members, $group)); } parent::__construct($controller, $name, $sourceClass, $fieldList); $SQL_search = isset($_REQUEST['MemberSearch']) ? Convert::raw2sql($_REQUEST['MemberSearch']) : null; if (!empty($_REQUEST['MemberSearch'])) { $searchFilters = array(); foreach ($SNG_member->searchableFields() as $fieldName => $fieldSpec) { if (strpos($fieldName, '.') === false) { $searchFilters[] = "\"{$fieldName}\" LIKE '%{$SQL_search}%'"; } } $this->sourceFilter[] = '(' . implode(' OR ', $searchFilters) . ')'; } if ($this->group) { $groupIDs = array($this->group->ID); if ($this->group->AllChildren()) { $groupIDs = array_merge($groupIDs, $this->group->AllChildren()->column('ID')); } $this->sourceFilter[] = sprintf('"Group_Members"."GroupID" IN (%s)', implode(',', $groupIDs)); } $this->sourceJoin = " INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\""; $this->setFieldListCsv($csvFieldList); $this->setPageSize($this->stat('page_size')); }
function onBeforeWrite() { parent::onBeforeWrite(); if (!$this->isInDb()) { if ($this->Type) { $this->ClassName = $this->Type; } foreach (DataObject::database_fields($this->ClassName) as $fieldName => $fieldType) { $this->{$fieldName} = $this->record[$fieldName . "-for-" . $this->ClassName]; } $this->Label = $this->record["Label-for-" . $this->ClassName]; } }
/** * Gets an array of elastic field definitions. * * @return array */ public function getElasticaFields() { $db = \DataObject::database_fields(get_class($this->owner)); $fields = $this->owner->searchableFields(); $result = array(); foreach ($fields as $name => $params) { $type = null; $spec = array(); if (array_key_exists($name, $db)) { $class = $db[$name]; if ($pos = strpos($class, '(')) { $class = substr($class, 0, $pos); } if (array_key_exists($class, self::$mappings)) { $spec['type'] = self::$mappings[$class]; } } $result[$name] = $spec; } $result['LastEdited'] = array('type' => 'date'); $result['Created'] = array('type' => 'date'); $result['ID'] = array('type' => 'integer'); $result['ParentID'] = array('type' => 'integer'); $result['Sort'] = array('type' => 'integer'); $result['Name'] = array('type' => 'string'); $result['MenuTitle'] = array('type' => 'string'); $result['ShowInSearch'] = array('type' => 'integer'); $result['ClassName'] = array('type' => 'string'); $result['ClassNameHierarchy'] = array('type' => 'string'); // fix up dates foreach ($result as $field => $spec) { if (isset($spec['type']) && $spec['type'] == 'date') { $spec['format'] = 'yyyy-MM-dd HH:mm:ss'; $result[$field] = $spec; } } if (isset($result['Content']) && count($result['Content'])) { $spec = $result['Content']; $spec['store'] = false; $result['Content'] = $spec; } if (method_exists($this->owner, 'updateElasticMappings')) { $this->owner->updateElasticMappings($result); } $this->owner->extend('updateElasticMappings', $result); return $result; }
public function sendUpdateNotification($data) { $name = $data['FirstName'] . " " . $data['Surname']; $body = "{$name} has updated their details via the website. Here is the new information:<br/>"; $notifyOnFields = Member::config()->frontend_update_notification_fields ?: DataObject::database_fields('Member'); $changedFields = $this->member->getChangedFields(true, 2); $send = false; foreach ($changedFields as $key => $field) { if (in_array($key, $notifyOnFields)) { $body .= "<br/><strong>{$key}:</strong><br/>" . "<strike style='color:red;'>" . $field['before'] . "</strike><br/>" . "<span style='color:green;'>" . $field['after'] . "</span><br/>"; $send = true; } } if ($send) { $email = new Email(Email::config()->admin_email, Email::config()->admin_email, "Member details update: {$name}", $body); $email->send(); } }
/** * Gets an array of elastic field definitions. * * @return array */ public function getElasticaFields() { $db = \DataObject::database_fields(get_class($this->owner)); $fields = $this->owner->searchableFields(); $result = array(); foreach ($fields as $name => $params) { $type = null; $spec = array(); if (array_key_exists($name, $db)) { $class = $db[$name]; if ($pos = strpos($class, '(')) { $class = substr($class, 0, $pos); } if (array_key_exists($class, self::$mappings)) { $spec['type'] = self::$mappings[$class]; } } $result[$name] = $spec; } return $result; }
/** * Tests the generation of the ClassName spec and ensure it's not unnecessarily influenced * by the order of classnames of existing records */ public function testClassNameSpecGeneration() { // Test with blank entries DataObject::clear_classname_spec_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')", $fields['ClassName']); // Test with instance of subclass $item1 = new DataObjectSchemaGenerationTest_IndexDO(); $item1->write(); DataObject::clear_classname_spec_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')", $fields['ClassName']); $item1->delete(); // Test with instance of main class $item2 = new DataObjectSchemaGenerationTest_DO(); $item2->write(); DataObject::clear_classname_spec_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')", $fields['ClassName']); $item2->delete(); // Test with instances of both classes $item1 = new DataObjectSchemaGenerationTest_IndexDO(); $item1->write(); $item2 = new DataObjectSchemaGenerationTest_DO(); $item2->write(); DataObject::clear_classname_spec_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')", $fields['ClassName']); $item1->delete(); $item2->delete(); }
/** * Checks the database is in a state to perform security checks. * See {@link DatabaseAdmin->init()} for more information. * * @return bool */ public static function database_is_ready() { // Used for unit tests if (self::$force_database_is_ready !== NULL) { return self::$force_database_is_ready; } if (self::$database_is_ready) { return self::$database_is_ready; } $requiredTables = ClassInfo::dataClassesFor('Member'); $requiredTables[] = 'Group'; $requiredTables[] = 'Permission'; foreach ($requiredTables as $table) { // Skip test classes, as not all test classes are scaffolded at once if (is_subclass_of($table, 'TestOnly')) { continue; } // if any of the tables aren't created in the database if (!ClassInfo::hasTable($table)) { return false; } // HACK: DataExtensions aren't applied until a class is instantiated for // the first time, so create an instance here. singleton($table); // if any of the tables don't have all fields mapped as table columns $dbFields = DB::field_list($table); if (!$dbFields) { return false; } $objFields = DataObject::database_fields($table, false); $missingFields = array_diff_key($objFields, $dbFields); if ($missingFields) { return false; } } self::$database_is_ready = true; return true; }
/** * Entry point for being called from a template. * * This gets the aggregate function * */ public function XML_val($name, $args = null, $cache = false) { $func = strtoupper(strpos($name, 'get') === 0 ? substr($name, 3) : $name); $attribute = $args ? $args[0] : 'ID'; $table = null; foreach (ClassInfo::ancestry($this->type, true) as $class) { $fields = DataObject::database_fields($class); if (array_key_exists($attribute, $fields)) { $table = $class; break; } } if (!$table) { user_error("Couldn't find table for field {$attribute} in type {$this->type}", E_USER_ERROR); } $query = $this->query("{$func}(\"{$table}\".\"{$attribute}\")"); // Cache results of this specific SQL query until flushCache() is triggered. $cachekey = sha1($query->sql()); $cache = self::cache(); if (!($result = $cache->load($cachekey))) { $result = (string) $query->execute()->value(); if (!$result) { $result = '0'; } $cache->save($result, null, array('aggregate', preg_replace('/[^a-zA-Z0-9_]/', '_', $this->type))); } return $result; }
/** * Remove invalid records from tables - that is, records that don't have * corresponding records in their parent class tables. */ public function cleanup() { $allClasses = get_declared_classes(); foreach ($allClasses as $class) { if (get_parent_class($class) == 'DataObject') { $baseClasses[] = $class; } } foreach ($baseClasses as $baseClass) { // Get data classes $subclasses = ClassInfo::subclassesFor($baseClass); unset($subclasses[0]); foreach ($subclasses as $k => $subclass) { if (DataObject::database_fields($subclass)) { unset($subclasses[$k]); } } if ($subclasses) { $records = DB::query("SELECT * FROM \"{$baseClass}\""); foreach ($subclasses as $subclass) { $recordExists[$subclass] = DB::query("SELECT \"ID\" FROM \"{$subclass}\"")->keyedColumn(); } foreach ($records as $record) { foreach ($subclasses as $subclass) { $id = $record['ID']; if ($record['ClassName'] != $subclass && !is_subclass_of($record['ClassName'], $subclass) && isset($recordExists[$subclass][$id])) { $sql = "DELETE FROM \"{$subclass}\" WHERE \"ID\" = {$record['ID']}"; echo "<li>{$sql}"; DB::query($sql); } } } } } }
/** * Recursively return the relationships for a given data object map. */ private function recursiveRelationships(&$object, $attributeVisibility = false, $cache = array()) { $output = array(); // Cache relationship data objects to prevent infinite recursion. if (!in_array("{$object['ClassName']} {$object['ID']}", $cache)) { $cache[] = "{$object['ClassName']} {$object['ID']}"; foreach ($object as $attribute => $value) { if ($attribute !== 'ClassName' && $attribute !== 'RecordClassName') { // Grab the name of a relationship. $relationship = substr($attribute, strlen($attribute) - 2) === 'ID' && strlen($attribute) > 2 ? substr($attribute, 0, -2) : null; if ($relationship && ($relationObject = DataObject::get_by_id($object['ClassName'], $object['ID'])) && $relationObject->hasMethod($relationship) && $value != 0) { // Grab the relationship. $relationObject = $relationObject->{$relationship}(); // Make sure recursive relationships are enabled. if (!$this->recursiveRelationships) { $output[$relationship] = array($relationObject->ClassName => array('ID' => (string) $relationObject->ID)); continue; } $temporaryMap = $relationObject->toMap(); if ($attributeVisibility) { // Grab the attribute visibility. $class = is_subclass_of($relationObject->ClassName, 'SiteTree') ? 'SiteTree' : (is_subclass_of($relationObject->ClassName, 'File') ? 'File' : $relationObject->ClassName); $relationConfiguration = DataObjectOutputConfiguration::get_one('DataObjectOutputConfiguration', "IsFor = '" . Convert::raw2sql($class) . "'"); $relationVisibility = $relationConfiguration && $relationConfiguration->APIwesomeVisibility ? explode(',', $relationConfiguration->APIwesomeVisibility) : null; $columns = array(); foreach (ClassInfo::subclassesFor($class) as $subclass) { // Prepend the table names. $subclassColumns = array(); foreach (DataObject::database_fields($subclass) as $column => $type) { $subclassColumns["{$subclass}.{$column}"] = $type; } $columns = array_merge($columns, $subclassColumns); } array_shift($columns); // Make sure this relationship has visibility customisation. if (is_null($relationVisibility) || count($relationVisibility) !== count($columns) || !in_array('1', $relationVisibility)) { $output[$relationship] = array($relationObject->ClassName => array('ID' => (string) $relationObject->ID)); continue; } // Grab all data object visible attributes. $select = array('ClassName' => $relationObject->ClassName, 'ID' => $relationObject->ID); $iteration = 0; foreach ($columns as $relationshipAttribute => $relationshipType) { if (isset($relationVisibility[$iteration]) && $relationVisibility[$iteration]) { $split = explode('.', $relationshipAttribute); $relationshipAttribute = count($split) === 2 ? $split[1] : $relationshipAttribute; if (isset($temporaryMap[$relationshipAttribute]) && $temporaryMap[$relationshipAttribute]) { // Retrieve the relationship value, and compose any asset file paths. $relationshipValue = $temporaryMap[$relationshipAttribute]; $select[$relationshipAttribute] = (strpos(strtolower($relationshipAttribute), 'file') !== false || strpos(strtolower($relationshipAttribute), 'image') !== false) && strpos($relationshipValue, 'assets/') !== false ? Director::absoluteURL($relationshipValue) : (is_integer($relationshipValue) ? (string) $relationshipValue : $relationshipValue); } } $iteration++; } } else { $select = $temporaryMap; } // Check the corresponding relationship. $output[$relationship] = array($relationObject->ClassName => $this->recursiveRelationships($select, $attributeVisibility, $cache)); } else { // Compose any asset file paths. $output[$attribute] = (strpos(strtolower($attribute), 'file') !== false || strpos(strtolower($attribute), 'image') !== false) && strpos($value, 'assets/') !== false ? Director::absoluteURL($value) : (is_integer($value) ? (string) $value : $value); } } } } else { // This relationship has previously been cached. $output['ID'] = $object['ID']; } // Return the visible relationship attributes. return $output; }
function augmentDatabase() { $classTable = $this->owner->class; $isRootClass = $this->owner->class == ClassInfo::baseDataClass($this->owner->class); // Build a list of suffixes whose tables need versioning $allSuffixes = array(); foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) { if ($this->owner->hasExtension($versionableExtension)) { $allSuffixes = array_merge($allSuffixes, (array) $suffixes); foreach ((array) $suffixes as $suffix) { $allSuffixes[$suffix] = $versionableExtension; } } } // Add the default table with an empty suffix to the list (table name = class name) array_push($allSuffixes, ''); foreach ($allSuffixes as $key => $suffix) { // check that this is a valid suffix if (!is_int($key)) { continue; } if ($suffix) { $table = "{$classTable}_{$suffix}"; } else { $table = $classTable; } if ($fields = DataObject::database_fields($this->owner->class)) { $indexes = $this->owner->databaseIndexes(); if ($suffix && ($ext = $this->owner->getExtensionInstance($allSuffixes[$suffix]))) { if (!$ext->isVersionedTable($table)) { continue; } $ext->setOwner($this->owner); $fields = $ext->fieldsInExtraTables($suffix); $ext->clearOwner(); $indexes = $fields['indexes']; $fields = $fields['db']; } // Create tables for other stages foreach ($this->stages as $stage) { // Extra tables for _Live, etc. if ($stage != $this->defaultStage) { DB::requireTable("{$table}_{$stage}", $fields, $indexes, false); } // Version fields on each root table (including Stage) /* if($isRootClass) { $stageTable = ($stage == $this->defaultStage) ? $table : "{$table}_$stage"; $parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)0); $values=Array('type'=>'int', 'parts'=>$parts); DB::requireField($stageTable, 'Version', $values); } */ } if ($isRootClass) { // Create table for all versions $versionFields = array_merge(self::$db_for_versions_table, (array) $fields); $versionIndexes = array_merge(self::$indexes_for_versions_table, (array) $indexes); } else { // Create fields for any tables of subclasses $versionFields = array_merge(array("RecordID" => "Int", "Version" => "Int"), (array) $fields); $versionIndexes = array_merge(array('RecordID_Version' => array('type' => 'unique', 'value' => 'RecordID,Version'), 'RecordID' => true, 'Version' => true), (array) $indexes); } if (DB::getConn()->hasTable("{$table}_versions")) { // Fix data that lacks the uniqueness constraint (since this was added later and // bugs meant that the constraint was validated) $duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\" \n\t\t\t\t\t\tFROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\" \n\t\t\t\t\t\tHAVING COUNT(*) > 1"); foreach ($duplications as $dup) { DB::alteration_message("Removing {$table}_versions duplicate data for " . "{$dup['RecordID']}/{$dup['Version']}", "deleted"); DB::query("DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = {$dup['RecordID']}\n\t\t\t\t\t\t\tAND \"Version\" = {$dup['Version']} AND \"ID\" != {$dup['ID']}"); } // Remove junk which has no data in parent classes. Only needs to run the following // when versioned data is spread over multiple tables if (!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) { foreach ($versionedTables as $child) { if ($table == $child) { break; } // only need subclasses $count = DB::query("\n\t\t\t\t\t\t\t\tSELECT COUNT(*) FROM \"{$table}_versions\"\n\t\t\t\t\t\t\t\tLEFT JOIN \"{$child}_versions\" \n\t\t\t\t\t\t\t\t\tON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"\n\t\t\t\t\t\t\t\t\tAND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\"\n\t\t\t\t\t\t\t\tWHERE \"{$child}_versions\".\"ID\" IS NULL\n\t\t\t\t\t\t\t")->value(); if ($count > 0) { DB::alteration_message("Removing orphaned versioned records", "deleted"); $effectedIDs = DB::query("\n\t\t\t\t\t\t\t\t\tSELECT \"{$table}_versions\".\"ID\" FROM \"{$table}_versions\"\n\t\t\t\t\t\t\t\t\tLEFT JOIN \"{$child}_versions\" \n\t\t\t\t\t\t\t\t\t\tON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"\n\t\t\t\t\t\t\t\t\t\tAND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\"\n\t\t\t\t\t\t\t\t\tWHERE \"{$child}_versions\".\"ID\" IS NULL\n\t\t\t\t\t\t\t\t")->column(); if (is_array($effectedIDs)) { foreach ($effectedIDs as $key => $value) { DB::query("DELETE FROM \"{$table}_versions\" WHERE \"{$table}_versions\".\"ID\" = '{$value}'"); } } } } } } DB::requireTable("{$table}_versions", $versionFields, $versionIndexes); } else { DB::dontRequireTable("{$table}_versions"); foreach ($this->stages as $stage) { if ($stage != $this->defaultStage) { DB::dontrequireTable("{$table}_{$stage}"); } } } } }
/** * @param $objects * @param $class * @param $table * @param bool $setID * @param bool $update * @throws Exception */ private function writeClassTable($objects, $class, $table, $setID = false, $update = false) { $fields = DataObject::database_fields($class); $singleton = singleton($class); $fields = array_filter(array_keys($fields), function ($field) use($singleton) { return $singleton->hasOwnTableDatabaseField($field); }); if ($setID || $update) { array_unshift($fields, 'ID'); } $typeLookup = array('ID' => 'i'); foreach ($fields as $field) { $dbObject = $singleton->dbObject($field); if ($dbObject instanceof Boolean || $dbObject instanceof Int) { $typeLookup[$field] = 'i'; } else { if ($dbObject instanceof Float || $dbObject instanceof Decimal || $dbObject instanceof Money) { $typeLookup[$field] = 'd'; } else { $typeLookup[$field] = 's'; } } } $inserts = array(); $insert = '(' . implode(',', array_fill(0, count($fields), '?')) . ')'; $types = ''; $params = array(); foreach ($objects as $obj) { $record = $this->dataObjectRecordProperty->getValue($obj); foreach ($fields as $field) { $type = $typeLookup[$field]; $types .= $type; $value = isset($record[$field]) ? $record[$field] : $obj->getField($field); if (is_bool($value)) { $value = (int) $value; } if ($type != 's' && !$value) { $value = 0; } $params[] = $value; } $inserts[] = $insert; } array_unshift($params, $types); $columns = implode(', ', array_map(function ($name) { return "`{$name}`"; }, $fields)); $inserts = implode(',', $inserts); $sql = "INSERT INTO `{$table}` ({$columns}) VALUES {$inserts}"; if ($update) { $mappings = array(); foreach ($fields as $field) { if ($field !== 'ID') { $mappings[] = "`{$field}` = VALUES(`{$field}`)"; } } $mappings = implode(',', $mappings); $sql .= " ON DUPLICATE KEY UPDATE {$mappings}"; } $this->executeQuery($sql, $params); }
/** * Display CMS JSON/XML output visibility configuration. */ public function getCMSFields() { $fields = parent::getCMSFields(); // Hide the data object name and output visibility associated with this configuration. $fields->removeByName('IsFor'); $fields->removeByName('APIwesomeVisibility'); // Grab a single data object. Requirements::css(APIWESOME_PATH . '/css/apiwesome.css'); if (DataObject::get_one($this->IsFor)) { // Grab the appropriate attributes for this data object. $class = is_subclass_of($this->IsFor, 'SiteTree') ? 'SiteTree' : (is_subclass_of($this->IsFor, 'File') ? 'File' : $this->IsFor); $columns = array(); foreach (ClassInfo::subclassesFor($class) as $subclass) { // Prepend the table names. $subclassColumns = array(); foreach (DataObject::database_fields($subclass) as $column => $type) { $subclassColumns["{$subclass}.{$column}"] = $type; } $columns = array_merge($columns, $subclassColumns); } array_shift($columns); $visibility = $this->APIwesomeVisibility ? explode(',', $this->APIwesomeVisibility) : null; // Display the check box fields for JSON/XML output visibility. $configuration = FieldGroup::create('Visibility')->addExtraClass('visibility'); $iteration = 0; foreach ($columns as $name => $type) { // Print the attribute name, including any relationships. $split = explode('.', $name); $printName = substr($name, strlen($name) - 2) === 'ID' && count($split) === 2 && ClassInfo::exists($split[0]) && Singleton($split[0])->hasMethod(substr($split[1], 0, -2)) ? substr($name, 0, -2) : $name; $printName = ltrim(preg_replace(array('/([A-Z][a-z]+)/', '/([A-Z]{2,})/', '/([_.0-9]+)/'), ' $0', $printName)); // Set an already existing attribute visibility. $configuration->push(CheckboxField::create(str_replace('.', '-', "{$name}.APIwesomeVisibility"), "Display <strong>{$printName}</strong>?", count($visibility) === count($columns) && isset($visibility[$iteration]) ? $visibility[$iteration] : 0)); $iteration++; } $fields->addFieldToTab('Root.Main', $configuration); } else { // Display a notice that data objects should first be created. $fields->removeByName('CallbackFunction'); $name = $this->getTitle(); $fields->addFieldToTab('Root.Main', LiteralField::create('ConfigurationNotice', "<p class='apiwesome notice'><strong>No {$name}s Found</strong></p>")); } $this->extend('updateDataObjectOutputConfigurationCMSFields', $fields); return $fields; }
/** * Checks the database is in a state to perform security checks. * See {@link DatabaseAdmin->init()} for more information. * * @return bool */ public static function database_is_ready() { // Used for unit tests if (self::$force_database_is_ready !== NULL) { return self::$force_database_is_ready; } $requiredTables = ClassInfo::dataClassesFor('Member'); $requiredTables[] = 'Group'; $requiredTables[] = 'Permission'; foreach ($requiredTables as $table) { // if any of the tables aren't created in the database if (!ClassInfo::hasTable($table)) { return false; } // if any of the tables don't have all fields mapped as table columns $dbFields = DB::fieldList($table); if (!$dbFields) { return false; } $objFields = DataObject::database_fields($table); $missingFields = array_diff_key($objFields, $dbFields); if ($missingFields) { return false; } } return true; }
/** * Generates a ($table)_version DB manipulation and injects it into the current $manipulation * * @param array $manipulation Source manipulation data * @param string $table Name of table * @param int $recordID ID of record to version */ protected function augmentWriteVersioned(&$manipulation, $table, $recordID) { $baseDataClass = ClassInfo::baseDataClass($table); // Set up a new entry in (table)_versions $newManipulation = array("command" => "insert", "fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null); // Add any extra, unchanged fields to the version record. $data = DB::prepared_query("SELECT * FROM \"{$table}\" WHERE \"ID\" = ?", array($recordID))->record(); if ($data) { $fields = DataObject::database_fields($table); if (is_array($fields)) { $data = array_intersect_key($data, $fields); foreach ($data as $k => $v) { if (!isset($newManipulation['fields'][$k])) { $newManipulation['fields'][$k] = $v; } } } } // Ensure that the ID is instead written to the RecordID field $newManipulation['fields']['RecordID'] = $recordID; unset($newManipulation['fields']['ID']); // Generate next version ID to use $nextVersion = 0; if ($recordID) { $nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1\n\t\t\t\tFROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?", array($recordID))->value(); } $nextVersion = $nextVersion ?: 1; // Add the version number to this data $manipulation[$table]['fields']['Version'] = $nextVersion; $newManipulation['fields']['Version'] = $nextVersion; // Write AuthorID for baseclass if ($table === $baseDataClass) { $userID = Member::currentUser() ? Member::currentUser()->ID : 0; $newManipulation['fields']['AuthorID'] = $userID; } $manipulation["{$table}_versions"] = $newManipulation; }
/** * Update the SELECT clause of the query with the columns from the given table */ protected function selectAllFromTable(SQLQuery &$query, $tableClass) { // Add SQL for multi-value fields $databaseFields = DataObject::database_fields($tableClass); $compositeFields = DataObject::composite_fields($tableClass, false); if($databaseFields) foreach($databaseFields as $k => $v) { if(!isset($compositeFields[$k])) { // Update $collidingFields if necessary if(isset($query->select[$k])) { if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]); $this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; } else { $query->select[$k] = "\"$tableClass\".\"$k\""; } } } if($compositeFields) foreach($compositeFields as $k => $v) { if($v) { $dbO = Object::create_from_string($v, $k); $dbO->addToQuery($query); } } }
/** * Update the SELECT clause of the query with the columns from the given table */ protected function selectColumnsFromTable(SQLQuery &$query, $tableClass, $columns = null) { // Add SQL for multi-value fields $databaseFields = DataObject::database_fields($tableClass); $compositeFields = DataObject::composite_fields($tableClass, false); if ($databaseFields) { foreach ($databaseFields as $k => $v) { if ((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { // Update $collidingFields if necessary if ($expressionForField = $query->expressionForField($k)) { if (!isset($this->collidingFields[$k])) { $this->collidingFields[$k] = array($expressionForField); } $this->collidingFields[$k][] = "\"{$tableClass}\".\"{$k}\""; } else { $query->selectField("\"{$tableClass}\".\"{$k}\"", $k); } } } } if ($compositeFields) { foreach ($compositeFields as $k => $v) { if ((is_null($columns) || in_array($k, $columns)) && $v) { $dbO = Object::create_from_string($v, $k); $dbO->addToQuery($query); } } } }
/** * Generate export fields for CSV. * * @param GridField $gridField * @return array */ public function generateExportFileData($gridField) { $separator = $this->csvSeparator; $singl = singleton($gridField->getModelClass()); if ($singl->hasMethod('exportedFields')) { $fallbackColumns = $singl->exportedFields(); } else { $fields = array_keys(DataObject::database_fields($gridField->getModelClass())); $fallbackColumns = array_combine($fields, $fields); } $csvColumns = $this->exportColumns ? $this->exportColumns : $fallbackColumns; $fileData = ''; $columnData = array(); $fieldItems = new ArrayList(); if ($this->csvHasHeader) { $headers = array(); // determine the CSV headers. If a field is callable (e.g. anonymous function) then use the // source name as the header instead foreach ($csvColumns as $columnSource => $columnHeader) { $headers[] = !is_string($columnHeader) && is_callable($columnHeader) ? utf8_decode($columnSource) : utf8_decode($columnHeader); } $fileData .= "\"" . implode("\"{$separator}\"", array_values($headers)) . "\""; $fileData .= "\n"; } $items = $gridField->getList(); $count = $items->count(); $fastMode = false; $veryFastMode = true; // If you export too much, you need some boost! if ($count > 1500) { $fastMode = true; } if ($count > 7500) { $veryFastMode = true; } foreach ($items as $item) { if ($fastMode || !$item->hasMethod('canView') || $item->canView()) { $columnData = array(); foreach ($csvColumns as $columnSource => $columnHeader) { if (!$veryFastMode && !is_string($columnHeader) && is_callable($columnHeader)) { if ($item->hasMethod($columnSource)) { $relObj = $item->{$columnSource}(); } else { $relObj = $item->relObject($columnSource); } $value = $columnHeader($relObj); } else { if ($veryFastMode) { $value = $item->{$columnSource}; } else { $value = $gridField->getDataFieldValue($item, $columnSource); if (!$value) { $value = $gridField->getDataFieldValue($item, $columnHeader); } } } $value = str_replace(array("\r", "\n"), "\n", $value); $columnData[] = '"' . str_replace('"', '""', utf8_decode($value)) . '"'; } $fileData .= implode($separator, $columnData); $fileData .= "\n"; } if ($item->hasMethod('destroy')) { $item->destroy(); } } return $fileData; }
/** * Add all database-backed text fields as fulltext searchable fields. * * For every class included in the index, examines those classes and all subclasses looking for "Text" database * fields (Varchar, Text, HTMLText, etc) and adds them all as fulltext searchable fields. */ public function addAllFulltextFields($includeSubclasses = true) { foreach ($this->getClasses() as $class => $options) { foreach (SearchIntrospection::hierarchy($class, $includeSubclasses, true) as $dataclass) { $fields = DataObject::database_fields($dataclass); foreach ($fields as $field => $type) { if (preg_match('/^(\\w+)\\(/', $type, $match)) { $type = $match[1]; } if (is_subclass_of($type, 'StringField')) { $this->addFulltextField($field); } } } } }
/** * Determine the search engine specific selectable fields, primarily for sorting. * * @return array(string, string) */ public function getSelectableFields() { // Instantiate some default selectable fields, just in case the search engine does not provide any. $selectable = array('LastEdited' => 'Last Edited', 'ID' => 'Created', 'ClassName' => 'Type'); // Determine the search engine that has been selected. if ($this->SearchEngine !== 'Full-Text' && ClassInfo::exists($this->SearchEngine)) { // Determine the search engine specific selectable fields. foreach ($this->extension_instances as $instance) { if (get_class($instance) === $this->SearchEngine) { $instance->setOwner($this); $fields = method_exists($instance, 'getSelectableFields') ? $instance->getSelectableFields() : array(); return $fields + $selectable; } } } else { if ($this->SearchEngine === 'Full-Text' && is_array($classes = Config::inst()->get('FulltextSearchable', 'searchable_classes')) && count($classes) > 0) { // Determine the full-text specific selectable fields. $selectable = array('Relevance' => 'Relevance') + $selectable; foreach ($classes as $class) { $fields = DataObject::database_fields($class); // Determine the most appropriate fields, primarily for sorting. if (isset($fields['Title'])) { $selectable['Title'] = 'Title'; } if (isset($fields['MenuTitle'])) { $selectable['MenuTitle'] = 'Navigation Title'; } if (isset($fields['Sort'])) { $selectable['Sort'] = 'Display Order'; } // This is specific to file searching. if (isset($fields['Name'])) { $selectable['Name'] = 'File Name'; } } } } // Allow extension customisation, so custom fields may be selectable. $this->extend('updateExtensibleSearchPageSelectableFields', $selectable); return $selectable; }
/** * Tests the generation of the ClassName spec and ensure it's not unnecessarily influenced * by the order of classnames of existing records */ public function testClassNameSpecGeneration() { // Test with blank entries DBClassName::clear_classname_cache(); $do1 = new DataObjectSchemaGenerationTest_DO(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("DBClassName", $fields['ClassName']); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $do1->dbObject('ClassName')->getEnum()); // Test with instance of subclass $item1 = new DataObjectSchemaGenerationTest_IndexDO(); $item1->write(); DBClassName::clear_classname_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("DBClassName", $fields['ClassName']); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $item1->dbObject('ClassName')->getEnum()); $item1->delete(); // Test with instance of main class $item2 = new DataObjectSchemaGenerationTest_DO(); $item2->write(); DBClassName::clear_classname_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("DBClassName", $fields['ClassName']); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $item2->dbObject('ClassName')->getEnum()); $item2->delete(); // Test with instances of both classes $item1 = new DataObjectSchemaGenerationTest_IndexDO(); $item1->write(); $item2 = new DataObjectSchemaGenerationTest_DO(); $item2->write(); DBClassName::clear_classname_cache(); $fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO'); $this->assertEquals("DBClassName", $fields['ClassName']); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $item1->dbObject('ClassName')->getEnum()); $item1->delete(); $item2->delete(); }
/** * @todo Re-enable all test cases for field inheritance aggregation after behaviour has been fixed */ function testFieldInheritance() { $teamInstance = $this->objFromFixture('DataObjectTest_Team', 'team1'); $subteamInstance = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1'); $this->assertEquals(array_keys($teamInstance->inheritedDatabaseFields()), array('Title', 'DatabaseField', 'DecoratedDatabaseField', 'CaptainID', 'HasOneRelationshipID', 'DecoratedHasOneRelationshipID'), 'inheritedDatabaseFields() contains all fields defined on instance, including base fields, decorated fields and foreign keys'); $this->assertEquals(array_keys(DataObject::database_fields('DataObjectTest_Team')), array('ClassName', 'Created', 'LastEdited', 'Title', 'DatabaseField', 'DecoratedDatabaseField', 'CaptainID', 'HasOneRelationshipID', 'DecoratedHasOneRelationshipID'), 'databaseFields() contains only fields defined on instance, including base fields, decorated fields and foreign keys'); $this->assertEquals(array_keys($subteamInstance->inheritedDatabaseFields()), array('SubclassDatabaseField', 'Title', 'DatabaseField', 'DecoratedDatabaseField', 'CaptainID', 'HasOneRelationshipID', 'DecoratedHasOneRelationshipID'), 'inheritedDatabaseFields() on subclass contains all fields defined on instance, including base fields, decorated fields and foreign keys'); $this->assertEquals(array_keys(DataObject::database_fields('DataObjectTest_SubTeam')), array('SubclassDatabaseField'), 'databaseFields() on subclass contains only fields defined on instance'); }
protected function obsoletefields($deleteSafeOnes = false, $fixBrokenDataObject = false, $deleteAll = false) { increase_time_limit_to(600); $dataClasses = ClassInfo::subclassesFor('DataObject'); $notCheckedArray = array(); $canBeSafelyDeleted = array(); //remove dataobject array_shift($dataClasses); $rows = DB::query("SHOW TABLES;"); $actualTables = array(); if ($rows) { foreach ($rows as $key => $item) { foreach ($item as $table) { $actualTables[$table] = $table; } } } echo "<h1>Report of fields that may not be required.</h1>"; echo "<p>NOTE: it may contain fields that are actually required (e.g. versioning or many-many relationships) and it may also leave out some obsolete fields. Use as a guide only.</p>"; foreach ($dataClasses as $dataClass) { // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness if (class_exists($dataClass)) { $dataObject = $dataClass::create(); if (!$dataObject instanceof TestOnly) { $requiredFields = $this->swapArray(DataObject::database_fields($dataObject->ClassName)); if (count($requiredFields)) { foreach ($requiredFields as $field) { if (!$dataObject->hasOwnTableDatabaseField($field)) { DB::alteration_message(" **** {$dataClass}.{$field} DOES NOT EXIST BUT IT SHOULD BE THERE!", "deleted"); } } $actualFields = $this->swapArray(DB::fieldList($dataClass)); if ($actualFields) { foreach ($actualFields as $actualField) { if ($deleteAll) { $link = " !!!!!!!!!!! DELETED !!!!!!!!!"; } else { $warning = Config::inst()->get("DataIntegrityTest", "warning"); $link = "<a href=\"" . Director::absoluteBaseURL() . "dev/tasks/DataIntegrityTest/?do=deleteonefield/" . $dataClass . "/" . $actualField . "/\" onclick=\"return confirm('" . $warning . "');\">delete field</a>"; } if (!in_array($actualField, array("ID", "Version"))) { if (!in_array($actualField, $requiredFields)) { $distinctCount = DB::query("SELECT COUNT(DISTINCT \"{$actualField}\") FROM \"{$dataClass}\" WHERE \"{$actualField}\" IS NOT NULL AND \"{$actualField}\" <> '' AND \"{$actualField}\" <> '0';")->value(); DB::alteration_message("<br /><br />\n\n{$dataClass}.{$actualField} {$link} - unique entries: {$distinctCount}", "deleted"); if ($distinctCount) { $rows = DB::query("\r\n\t\t\t\t\t\t\t\t\t\t\t\tSELECT \"{$actualField}\" as N, COUNT(\"{$actualField}\") as C\r\n\t\t\t\t\t\t\t\t\t\t\t\tFROM \"{$dataClass}\"\r\n\t\t\t\t\t\t\t\t\t\t\t\tGROUP BY \"{$actualField}\"\r\n\t\t\t\t\t\t\t\t\t\t\t\tORDER BY C DESC\r\n\t\t\t\t\t\t\t\t\t\t\t\tLIMIT 7"); if ($rows) { foreach ($rows as $row) { DB::alteration_message(" " . $row["C"] . ": " . $row["N"]); } } } else { if (!isset($canBeSafelyDeleted[$dataClass])) { $canBeSafelyDeleted[$dataClass] = array(); } $canBeSafelyDeleted[$dataClass][$actualField] = "{$dataClass}.{$actualField}"; } if ($deleteAll || $deleteSafeOnes && $distinctCount == 0) { $this->deleteField($dataClass, $actualField); } } } if ($actualField == "Version" && !in_array($actualField, $requiredFields)) { $versioningPresent = $dataObject->hasVersioning(); if (!$versioningPresent) { DB::alteration_message("{$dataClass}.{$actualField} {$link}", "deleted"); if ($deleteAll) { $this->deleteField($dataClass, $actualField); } } } } } $rawCount = DB::query("SELECT COUNT(\"ID\") FROM \"{$dataClass}\"")->value(); Versioned::set_reading_mode("Stage.Stage"); $realCount = 0; $allSubClasses = array_unique(array($dataClass) + ClassInfo::subclassesFor($dataClass)); $objects = $dataClass::get()->filter(array("ClassName" => $allSubClasses)); if ($objects->count()) { $realCount = $objects->count(); } if ($rawCount != $realCount) { echo "<hr />"; $sign = " > "; if ($rawCount < $realCount) { $sign = " < "; } DB::alteration_message("The DB Table Row Count does not seem to match the DataObject Count for <strong>{$dataClass} ({$rawCount} {$sign} {$realCount})</strong>. This could indicate an error as generally these numbers should match.", "deleted"); if ($fixBrokenDataObject) { $objects = $dataClass::get()->where("LinkedTable.ID IS NULL")->leftJoin($dataClass, "{$dataClass}.ID = LinkedTable.ID", "LinkedTable"); if ($objects->count() > 500) { DB::alteration_message("It is recommended that you manually fix the difference in real vs object count in {$dataClass}. There are more than 500 records so it would take too long to do it now.", "deleted"); } else { DB::alteration_message("Now trying to recreate missing items... COUNT = " . $objects->count(), "created"); foreach ($objects as $object) { if (DB::query("SELECT COUNT(\"ID\") FROM \"{$dataClass}\" WHERE \"ID\" = " . $object->ID . ";")->value() != 1) { Config::inst()->update('DataObject', 'validation_enabled', false); $object->write(true, false, true, false); Config::inst()->update('DataObject', 'validation_enabled', true); } } $objectCount = $dataClass::get()->count(); DB::alteration_message("Consider deleting superfluous records from table {$dataClass} .... COUNT =" . ($rawCount - $objectCount)); $ancestors = ClassInfo::ancestry($dataClass, true); if ($ancestors && is_array($ancestors) && count($ancestors)) { foreach ($ancestors as $ancestor) { if ($ancestor != $dataClass) { echo "DELETE `{$dataClass}`.* FROM `{$dataClass}` LEFT JOIN `{$ancestor}` ON `{$dataClass}`.`ID` = `{$ancestor}`.`ID` WHERE `{$ancestor}`.`ID` IS NULL;"; DB::query("DELETE `{$dataClass}`.* FROM `{$dataClass}` LEFT JOIN `{$ancestor}` ON `{$dataClass}`.`ID` = `{$ancestor}`.`ID` WHERE `{$ancestor}`.`ID` IS NULL;"); } } } } } echo "<hr />"; } unset($actualTables[$dataClass]); } else { $db = DB::getConn(); if ($db->hasTable($dataClass)) { DB::alteration_message(" **** The {$dataClass} table exists, but according to the data-scheme it should not be there ", "deleted"); } else { $notCheckedArray[] = $dataClass; } } } } } if (count($canBeSafelyDeleted)) { DB::alteration_message("<h2>Can be safely deleted: </h2>"); foreach ($canBeSafelyDeleted as $table => $fields) { DB::alteration_message($table . ": " . implode(", ", $fields)); } } if (count($notCheckedArray)) { echo "<h3>Did not check the following classes as no fields appear to be required and hence there is no database table.</h3>"; foreach ($notCheckedArray as $table) { if (DB::query("SHOW TABLES LIKE '" . $table . "'")->value()) { DB::alteration_message($table . " - NOTE: a table exists for this Class, this is an unexpected result", "deleted"); } else { DB::alteration_message($table, "created"); } } } if (count($actualTables)) { echo "<h3>Other Tables in Database not directly linked to a Silverstripe DataObject:</h3>"; foreach ($actualTables as $table) { $remove = true; if (class_exists($table)) { $classExistsMessage = " a PHP class with this name exists."; $obj = singleton($table); //not sure why we have this. if ($obj instanceof DataExtension) { $remove = false; } elseif (class_exists("Versioned") && $obj->hasExtension("Versioned")) { $remove = false; } } else { $classExistsMessage = " NO PHP class with this name exists."; if (substr($table, -5) == "_Live") { $remove = false; } if (substr($table, -9) == "_versions") { $remove = false; } //many 2 many tables... if (strpos($table, "_")) { $class = explode("_", $table); $manyManyClass = substr($table, 0, strrpos($table, '_')); $manyManyExtension = substr($table, strrpos($table, '_') + 1 - strlen($table)); if (class_exists($manyManyClass)) { $manyManys = Config::inst()->get($manyManyClass, "many_many"); if (isset($manyManys[$manyManyExtension])) { $remove = false; } } } } if ($remove) { if (substr($table, 0, strlen("_obsolete_")) != "_obsolete_") { $rowCount = DB::query("SELECT COUNT(*) FROM {$table}")->value(); DB::alteration_message($table . ", rows " . $rowCount); $obsoleteTableName = "_obsolete_" . $table; if (!$this->tableExists($obsoleteTableName)) { DB::alteration_message("We recommend deleting {$table} or making it obsolete by renaming it to " . $obsoleteTableName, "deleted"); if ($deleteAll) { DB::getConn()->renameTable($table, $obsoleteTableName); } else { DB::alteration_message($table . " - " . $classExistsMessage . " It can be moved to _obsolete_" . $table . ".", "created"); } } else { DB::alteration_message("I'd recommend to move <strong>{$table}</strong> to <strong>" . $obsoleteTableName . "</strong>, but that table already exists", "deleted"); } } } } } echo "<a href=\"" . Director::absoluteURL("/dev/tasks/DataIntegrityTest/") . "\">back to main menu.</a>"; }