diff --git a/api/DataFormatter.php b/api/DataFormatter.php index fb5e554e8..9d974577c 100644 --- a/api/DataFormatter.php +++ b/api/DataFormatter.php @@ -44,6 +44,15 @@ abstract class DataFormatter extends Object { */ protected $customAddFields = null; + /** + * Fields which should be expicitly excluded from the export. + * Comes in handy for field-level permissions. + * Will overrule both {@link $customAddFields} and {@link $customFields} + * + * @var array + */ + protected $removeFields = null; + /** * Specifies the mimetype in which all strings * returned from the convert*() methods should be used, @@ -155,6 +164,20 @@ abstract class DataFormatter extends Object { return $this->customAddFields; } + /** + * @param array $fields + */ + public function setRemoveFields($fields) { + $this->removeFields = $fields; + } + + /** + * @return array + */ + public function getRemoveFields() { + return $this->removeFields; + } + public function getOutputContentType() { return $this->outputContentType; } @@ -193,6 +216,11 @@ abstract class DataFormatter extends Object { // add default required fields $dbFields = array_merge($dbFields, array('ID'=>'Int')); + // @todo Requires PHP 5.1+ + if($this->removeFields) { + $dbFields = array_diff_key($dbFields, array_combine($this->removeFields,$this->removeFields)); + } + return $dbFields; } diff --git a/api/RestfulServer.php b/api/RestfulServer.php index 90db8402a..60149df1b 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -38,6 +38,12 @@ * @todo Make SearchContext specification customizeable for each class * @todo Allow for range-searches (e.g. on Created column) * @todo Allow other authentication methods (currently only HTTP BasicAuth) + * @todo Filter relation listings by $api_access and canView() permissions + * @todo Exclude relations when "fields" are specified through URL (they should be explicitly requested in this case) + * @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in SiteTree/Versioned/Hierarchy + * @todo URL parameter namespacing for search-fields, limit, fields, add_fields (might all be valid dataobject properties) + * e.g. you wouldn't be able to search for a "limit" property on your subclass as its overlayed with the search logic + * @todo i18n integration (e.g. Page/1.xml?lang=de_DE) */ class RestfulServer extends Controller { static $url_handlers = array( @@ -151,7 +157,7 @@ class RestfulServer extends Controller { $obj = DataObject::get_by_id($className, $id); if(!$obj) return $this->notFound(); if(!$obj->canView()) return $this->permissionFailure(); - + if($relation) { if($relationClass = $obj->many_many($relation)) { $query = $obj->getManyManyComponentsQuery($relation); @@ -166,7 +172,7 @@ class RestfulServer extends Controller { } else { return $this->notFound(); } - + // get all results $obj = $this->search($relationClass, $this->request->getVars(), $sort, $limit, $query); if(!$obj) $obj = new DataObjectSet(); diff --git a/core/model/DataObject.php b/core/model/DataObject.php index 65e09491a..53b0eb312 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -1299,9 +1299,13 @@ class DataObject extends ViewableData implements DataObjectInterface { * @return SearchContext */ public function getDefaultSearchContext() { - return new SearchContext($this->class, new FieldSet($this->searchable_fields()), $this->defaultSearchFilters()); + return new SearchContext( + $this->class, + $this->scaffoldSearchFields(), + $this->defaultSearchFilters() + ); } - + /** * Determine which properties on the DataObject are * searchable, and map them to their default {@link FormField} @@ -1315,31 +1319,35 @@ class DataObject extends ViewableData implements DataObjectInterface { */ public function scaffoldSearchFields() { $fields = new FieldSet(); - foreach($this->searchable_fields() as $fieldName => $fieldType) { - $field = $this->relObject($fieldName)->scaffoldSearchField(); + foreach($this->searchableFields() as $fieldName => $spec) { + + // If we explicitly set a field, then construct that + if(isset($spec['field'])) { + $fieldClass = $spec['field']; + $field = new $fieldClass($fieldName); + + // Otherwise, use the database field's scaffolder + } else { + $field = $this->relObject($fieldName)->scaffoldSearchField(); + } + if (strstr($fieldName, '.')) { $field->setName(str_replace('.', '__', $fieldName)); } - $field->setTitle($this->searchable_fields_labels($fieldName)); + $field->setTitle($spec['title']); + $fields->push($field); } return $fields; } - /** - * CamelCase to sentence case label transform - * - * @todo move into utility class (String::toLabel) - */ - function toLabel($string) { - return preg_replace("/([a-z]+)([A-Z])/","$1 $2", $string); - } - /** * Scaffold a simple edit form for all properties on this dataobject, - * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()} + * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}. + * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}. * * @uses {@link DBField::scaffoldFormField()} + * @uses {@link DataObject::fieldLabels()} * @param array $fieldClasses Optional mapping of fieldnames to subclasses of {@link DBField} * @return FieldSet */ @@ -1356,6 +1364,7 @@ class DataObject extends ViewableData implements DataObjectInterface { } else { $fieldObject = $this->dbObject($fieldName)->scaffoldFormField(); } + $fieldObject->setTitle($this->fieldLabels($fieldName)); $fields->push($fieldObject); } foreach($this->has_one() as $relationship => $component) { @@ -1769,7 +1778,7 @@ class DataObject extends ViewableData implements DataObjectInterface { $object = $component->dbObject($fieldName); if (!($object instanceof DBField)) { - user_error("Unable to traverse to related object field [$fieldPath]", E_USER_ERROR); + user_error("Unable to traverse to related object field [$fieldPath] on [$this->class]", E_USER_ERROR); } return $object; } @@ -2291,29 +2300,49 @@ class DataObject extends ViewableData implements DataObjectInterface { * * @return array */ - public function searchable_fields() { + public function searchableFields() { + // can have mixed format, need to make consistent in most verbose form $fields = $this->stat('searchable_fields'); - - // if fields were passed in numeric array, - // convert to an associative array - if($fields && array_key_exists(0, $fields)) { - $fields = array_fill_keys(array_values($fields), 'TextField'); - } - - if (!$fields) { - $fields = array_fill_keys(array_keys($this->summaryFields()), 'TextField'); - } else { - // rewrite array, if it is using shorthand syntax - $rewrite = array(); - foreach($fields as $name => $type) { - if (is_int($name)) $rewrite[$type] = 'TextField'; - else $rewrite[$name] = $type; + $labels = $this->fieldLabels(); + + // fallback to summary fields + if(!$fields) $fields = array_keys($this->summaryFields()); + + // rewrite array, if it is using shorthand syntax + $rewrite = array(); + foreach($fields as $name => $specOrName) { + $identifer = (is_int($name)) ? $specOrName : $name; + if(is_int($name)) { + // Format: array('MyFieldName') + $rewrite[$identifer] = array(); + } elseif(is_array($specOrName)) { + // Format: array('MyFieldName' => array( + // 'filter => 'ExactMatchFilter', + // 'field' => 'NumericField', // optional + // 'title' => 'My Title', // optiona. + // ) + $rewrite[$identifer] = array_merge( + array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')), + (array)$specOrName + ); + } else { + // Format: array('MyFieldName' => 'ExactMatchFilter') + $rewrite[$identifer] = array( + 'filter' => $specOrName, + ); + } + if(!isset($rewrite[$identifer]['title'])) { + $rewrite[$identifer]['title'] = (isset($labels[$identifer])) ? $labels[$identifer] : FormField::name_to_label($identifer); + } + if(!isset($rewrite[$identifer]['filter'])) { + $rewrite[$identifer]['filter'] = 'PartialMatchFilter'; } - $fields = $rewrite; } + $fields = $rewrite; + return $fields; } - + /** * Get any user defined searchable fields labels that * exist. Allows overriding of default field names in the form @@ -2329,28 +2358,20 @@ class DataObject extends ViewableData implements DataObjectInterface { * Generates labels based on name of the field itself, if no static property * {@link self::searchable_fields_labels} exists. * - * @todo fix bad code - * * @param $fieldName name of the field to retrieve * @return array of all element labels if no argument given * @return string of label if field */ - public function searchable_fields_labels($fieldName=false) { - $custom_labels = $this->stat('searchable_fields_labels'); - - $fields = array_keys($this->searchable_fields()); - $labels = array_combine($fields, $fields); - if(is_array($custom_labels)) $labels = array_merge($labels, $custom_labels); - if ($fieldName) { - if(array_key_exists($fieldName, $labels)) { - return $labels[$fieldName]; - } elseif (strstr($fieldName, '.')) { - $parts = explode('.', $fieldName); - $label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1]; - return $this->toLabel($label); - } else { - return $this->toLabel($fieldName); - } + public function fieldLabels($fieldName = false) { + $customLabels = $this->stat('field_labels'); + $autoLabels = array(); + foreach($this->databaseFields() as $name => $type) { + $autoLabels[$name] = FormField::name_to_label($name); + } + $labels = array_merge((array)$autoLabels, (array)$customLabels); + + if($fieldName) { + return (isset($labels[$fieldName])) ? $labels[$fieldName] : FormField::name_to_label($fieldName); } else { return $labels; } @@ -2404,21 +2425,9 @@ class DataObject extends ViewableData implements DataObjectInterface { */ public function defaultSearchFilters() { $filters = array(); - foreach($this->searchable_fields() as $name => $type) { - if (is_int($name)) { - $filters[$type] = $this->relObject($type)->defaultSearchFilter(); - } else { - if(is_array($type)) { - $filter = current($type); - $filters[$name] = new $filter($name); - } else { - if(is_subclass_of($type, 'SearchFilter')) { - $filters[$name] = new $type($name); - } else { - $filters[$name] = $this->relObject($name)->defaultSearchFilter($name); - } - } - } + foreach($this->searchableFields() as $name => $spec) { + $filterClass = $spec['filter']; + $filters[$name] = new $filterClass($name); } return $filters; } @@ -2574,29 +2583,32 @@ class DataObject extends ViewableData implements DataObjectInterface { * Default list of fields that can be scaffolded by the ModelAdmin * search interface. * - * Defining a basic set of searchable fields: - * - * static $searchable_fields = array("Name", "Email"); - * - * - * Overriding the default form fields, with a custom defined field: - * - * static $searchable_fields = array( - * "Name" => "TextField" - * ); - * - * * Overriding the default filter, with a custom defined filter: * * static $searchable_fields = array( * "Name" => "PartialMatchFilter" * ); * - * - * Overriding the default form field and filter: + * + * Overriding the default form fields, with a custom defined field. + * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}. + * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}. * * static $searchable_fields = array( - * "Name" => array("TextField" => "PartialMatchFilter") + * "Name" => array( + * "field" => "TextField" + * ) + * ); + * + * + * Overriding the default form field, filter and title: + * + * static $searchable_fields = array( + * "Organisation.ZipCode" => array( + * "field" => "TextField", + * "filter" => "PartialMatchFilter", + * "title" => 'Organisation ZIP' + * ) * ); * */ @@ -2606,7 +2618,7 @@ class DataObject extends ViewableData implements DataObjectInterface { * User defined labels for searchable_fields, used to override * default display in the search form. */ - public static $searchable_fields_labels = null; + public static $field_labels = null; /** * Provides a default list of fields to be used by a 'summary' diff --git a/core/model/SQLQuery.php b/core/model/SQLQuery.php index 8f0e8b1a8..e849f3e01 100755 --- a/core/model/SQLQuery.php +++ b/core/model/SQLQuery.php @@ -141,7 +141,7 @@ class SQLQuery extends Object { * @return SQLQuery This instance */ public function leftJoin($table, $onPredicate) { - $this->from[] = "LEFT JOIN $table ON $onPredicate"; + $this->from[$table] = "LEFT JOIN $table ON $onPredicate"; return $this; } @@ -151,10 +151,17 @@ class SQLQuery extends Object { * @return SQLQuery This instance */ public function innerJoin($table, $onPredicate) { - $this->from[] = "INNER JOIN $table ON $onPredicate"; + $this->from[$table] = "INNER JOIN $table ON $onPredicate"; return $this; } + /** + * Returns true if we are already joining to the given table alias + */ + public function isJoinedTo($tableAlias) { + return isset($this->from[$tableAlias]); + } + /** * Pass LIMIT clause either as SQL snippet or in array format. * diff --git a/core/model/YamlFixture.php b/core/model/YamlFixture.php index fea20b7af..3898e2ba3 100644 --- a/core/model/YamlFixture.php +++ b/core/model/YamlFixture.php @@ -80,6 +80,10 @@ class YamlFixture extends Object { protected $fixtureDictionary; function __construct($fixtureFile) { + if(!file_exists(Director::baseFolder().'/'. $fixtureFile)) { + user_error('YamlFixture::__construct(): Fixture path "' . $fixtureFile . '" not found', E_USER_ERROR); + } + $this->fixtureFile = $fixtureFile; } @@ -118,9 +122,7 @@ class YamlFixture extends Object { function saveIntoDatabase() { $parser = new Spyc(); $fixtureContent = $parser->load(Director::baseFolder().'/'.$this->fixtureFile); - $this->fixtureDictionary = array(); - foreach($fixtureContent as $dataClass => $items) { foreach($items as $identifier => $fields) { $obj = new $dataClass(); diff --git a/core/model/fieldtypes/PrimaryKey.php b/core/model/fieldtypes/PrimaryKey.php index c626503bb..3fb1a35fb 100644 --- a/core/model/fieldtypes/PrimaryKey.php +++ b/core/model/fieldtypes/PrimaryKey.php @@ -13,6 +13,8 @@ class PrimaryKey extends Int { * @var DataObject */ protected $object; + + protected static $default_search_filter_class = 'ExactMatchMultiFilter'; function __construct($name, $object) { $this->object = $object; @@ -21,7 +23,11 @@ class PrimaryKey extends Int { public function scaffoldFormField($title = null) { $objs = DataObject::get($this->object->class); - $map = ($objs) ? $objs->toDropdownMap() : false; + + $first = $objs->First(); + $titleField = isset($first->Title) ? "Title" : "Name"; + + $map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false; return new DropdownField($this->name, $title, $map, null, null, ' '); } diff --git a/dev/CodeViewer.php b/dev/CodeViewer.php index 6a3137fa5..4bbfa4ce0 100644 --- a/dev/CodeViewer.php +++ b/dev/CodeViewer.php @@ -77,20 +77,22 @@ class CodeViewer extends Controller { protected $classComment, $methodComment; function saveClassComment($token) { - $this->classComment = $this->prettyComment($token); + $this->classComment = $this->parseComment($token); } function saveMethodComment($token) { - $this->methodComment = $this->prettyComment($token); + $this->methodComment = $this->parseComment($token); } function createClass($token) { $this->currentClass = array( - "description" => $this->classComment + "description" => $this->classComment['pretty'], + "heading" => isset($this->classComment['heading']) ? $this->classComment['heading'] : null, ); $ths->classComment = null; } function setClassName($token) { $this->currentClass['name'] = $token[1]; + if(!$this->currentClass['heading']) $this->currentClass['heading'] = $token[1]; } function completeClass($token) { $this->classes[] = $this->currentClass; @@ -99,12 +101,14 @@ class CodeViewer extends Controller { function createMethod($token) { $this->currentMethod = array(); $this->currentMethod['content'] = "
";
-		$this->currentMethod['description'] = $this->methodComment;
+		$this->currentMethod['description'] = $this->methodComment['pretty'];
+		$this->currentMethod['heading'] = isset($this->methodComment['heading']) ? $this->methodComment['heading'] : null;
 		$this->methodComment = null;
 
 	}
 	function setMethodName($token) {
 		$this->currentMethod['name'] = $token[1];
+		if(!$this->currentMethod['heading']) $this->currentMethod['heading'] = $token[1];
 	}
 	function appendMethodComment($token) {
 		if(substr($token[1],0,2) == '/*') {
@@ -123,6 +127,24 @@ class CodeViewer extends Controller {
 		$comment = str_replace("\n\n", "

", $comment); return "

$comment

"; } + + function parseComment($token) { + $parsed = array(); + + $comment = preg_replace('/^\/\*/','',$token[1]); + $comment = preg_replace('/\*\/$/','',$comment); + $comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment); + + foreach(array('heading','nav') as $var) { + if(preg_match('/@' . $var . '\s+([^\n]+)\n/', $comment, $matches)) { + $parsed[$var] = $matches[1]; + $comment = preg_replace('/@' . $var . '\s+([^\n]+)\n/','', $comment); + } + } + + $parsed['pretty'] = "

" . str_replace("\n\n", "

", htmlentities($comment)). "

"; + return $parsed; + } protected $isNewLine = true; @@ -271,12 +293,12 @@ class CodeViewer extends Controller { $subclasses = ClassInfo::subclassesFor('SapphireTest'); foreach($this->classes as $classDef) { if(true ||in_array($classDef['name'], $subclasses)) { - echo "

$classDef[name]

"; + echo "

$classDef[heading]

"; echo "
$classDef[description]
"; if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) { if(true || substr($method['name'],0,4) == 'test') { //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4)))); - $title = $method['name']; + $title = $method['heading']; echo "

$title

"; echo "
$method[description]
"; diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index b32ad4cae..6c98f3c90 100644 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -17,7 +17,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase { * * @var string */ - protected static $fixture_file = null; + static $fixture_file = null; protected $originalMailer; diff --git a/dev/TaskRunner.php b/dev/TaskRunner.php index 9f16603dd..4382f6b35 100644 --- a/dev/TaskRunner.php +++ b/dev/TaskRunner.php @@ -20,11 +20,15 @@ class TaskRunner extends Controller { } function runTask($request) { - echo "

Running task...

"; $TaskName = $request->param('TaskName'); - if (class_exists($TaskName)) { + if (class_exists($TaskName) && is_subclass_of($TaskName, 'BuildTask')) { + if(Director::is_cli()) echo "Running task '$TaskName'...\n\n"; + else echo "

Running task '$TaskName'...

\n"; + $task = new $TaskName(); if (!$task->isDisabled()) $task->run($request); + } else { + echo "Build task '$TaskName' not found."; } } diff --git a/forms/FieldSet.php b/forms/FieldSet.php index f5069060e..3453382e0 100755 --- a/forms/FieldSet.php +++ b/forms/FieldSet.php @@ -320,6 +320,64 @@ class FieldSet extends DataObjectSet { function makeReadonly() { return $this->transform(new ReadonlyTransformation()); } + + /** + * Transform the named field into a readonly feld. + */ + function makeFieldReadonly($fieldName) { + // Iterate on items, looking for the applicable field + foreach($this->items as $i => $field) { + // Once it's found, use FormField::transform to turn the field into a readonly version of itself. + if($field->Name() == $fieldName) { + $this->items[$i] = $field->transform(new ReadonlyTransformation()); + + // Clear an internal cache + $this->sequentialSet = null; + + // A true results indicates that the field was foudn + return true; + } + } + return false; + } + + /** + * Change the order of fields in this FieldSet by specifying an ordered list of field names. + * This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and + * shuffle the fields around to the order that you want. + * + * Please note that any tabs or other dataless fields will be clobbered by this operation. + * + * Field names can be given as an array, or just as a list of arguments. + */ + function changeFieldOrder($fieldNames) { + // Field names can be given as an array, or just as a list of arguments. + if(!is_array($fieldNames)) $fieldNames = func_get_args(); + + // Build a map of fields indexed by their name. This will make the 2nd step much easier. + $fieldMap = array(); + foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field; + + // Iterate through the ordered list of names, building a new array to be put into $this->items. + // While we're doing this, empty out $fieldMap so that we can keep track of leftovers. + // Unrecognised field names are okay; just ignore them + $fields = array(); + foreach($fieldNames as $fieldName) { + if(isset($fieldMap[$fieldName])) { + $fields[] = $fieldMap[$fieldName]; + unset($fieldMap[$fieldName]); + } + } + + // Add the leftover fields to the end of the list. + $fields = $fields + array_values($fieldMap); + + // Update our internal $this->items parameter. + $this->items = $fields; + + // Re-set an internal cache + $this->sequentialSet = null; + } } diff --git a/forms/TableListField.php b/forms/TableListField.php index 0806cbd43..d09e27ee5 100755 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -220,6 +220,7 @@ JS } function Headings() { + $headings = array(); foreach($this->fieldList as $fieldName => $fieldTitle) { $isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']); // we can't allow sorting with partial summaries (groupByField) @@ -313,10 +314,10 @@ JS // we don't limit when doing certain actions if(!isset($_REQUEST['methodName']) || !in_array($_REQUEST['methodName'],array('printall','export'))) { - $dataQuery->limit = $SQL_limit; - if(isset($SQL_start)) { - $dataQuery->limit .= " OFFSET {$SQL_start}"; - } + $dataQuery->limit(array( + 'limit' => $SQL_limit, + 'start' => (isset($SQL_start)) ? $SQL_start : null + )); } // get data diff --git a/javascript/TableListField.js b/javascript/TableListField.js index c4bf7846e..c9d9a62b4 100755 --- a/javascript/TableListField.js +++ b/javascript/TableListField.js @@ -113,7 +113,7 @@ TableListField.prototype = { new Ajax.Request( el.href, { - postBody: 'update=1&paginate=1', + postBody: 'update=1', onComplete: Ajax.Evaluator, onFailure: this.ajaxErrorHandler.bind(this) } diff --git a/main.php b/main.php index 1aa6fd51e..6269e7546 100644 --- a/main.php +++ b/main.php @@ -5,23 +5,28 @@ * * The main.php does a number of set-up activities for the request. * - * - Includes the first one of the following files that it finds: (root)/_ss_environment.php, (root)/../_ss_environment.php, or (root)/../../_ss_environment.php + * - Includes the first one of the following files that it finds: (root)/_ss_environment.php, + * (root)/../_ss_environment.php, or (root)/../../_ss_environment.php * - Gets an up-to-date manifest from {@link ManifestBuilder} * - Sets up error handlers with {@link Debug::loadErrorHandlers()} - * - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should be defined in an _config.php + * - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should + & be defined in an _config.php * - Sets up the default director rules using {@link Director::addRules()} * - * After that, it calls {@link Director::direct()}, which is responsible for doing most of the real work. + * After that, it calls {@link Director::direct()}, which is responsible for doing most of the + * real work. * - * Finally, main.php will use {@link Profiler} to show a profile if the querystring variable "debug_profile" is set. + * Finally, main.php will use {@link Profiler} to show a profile if the querystring variable + * "debug_profile" is set. * * CONFIGURING THE WEBSERVER * - * To use Sapphire, every request that doesn't point directly to a file should be rewritten to sapphire/main.php?url=(url). - * For example, http://www.example.com/about-us/rss would be rewritten to http://www.example.com/sapphire/main.php?url=about-us/rss + * To use Sapphire, every request that doesn't point directly to a file should be rewritten to + * sapphire/main.php?url=(url). For example, http://www.example.com/about-us/rss would be rewritten + * to http://www.example.com/sapphire/main.php?url=about-us/rss * - * It's important that requests that point directly to a file aren't rewritten; otherwise, visitors won't be able to download - * any CSS, JS, image files, or other downloads. + * It's important that requests that point directly to a file aren't rewritten; otherwise, visitors + * won't be able to download any CSS, JS, image files, or other downloads. * * On Apache, RewriteEngine can be used to do this. * @@ -34,8 +39,8 @@ * Include _ss_environment.php file */ $envFiles = array('../_ss_environment.php', '../../_ss_environment.php', '../../../_ss_environment.php'); -foreach($envFiles as $envFile) { - if(@file_exists($envFile)) { +foreach ($envFiles as $envFile) { + if (@file_exists($envFile)) { include($envFile); break; } @@ -47,37 +52,38 @@ foreach($envFiles as $envFile) { require_once("core/Core.php"); header("Content-type: text/html; charset=\"utf-8\""); -if(function_exists('mb_http_output')) { +if (function_exists('mb_http_output')) { mb_http_output('UTF-8'); mb_internal_encoding('UTF-8'); } -if(get_magic_quotes_gpc()) { +if (get_magic_quotes_gpc()) { if($_REQUEST) stripslashes_recursively($_REQUEST); if($_GET) stripslashes_recursively($_GET); if($_POST) stripslashes_recursively($_POST); } -if(isset($_REQUEST['trace'])) { +if (isset($_REQUEST['trace'])) { apd_set_pprof_trace(); } // Ensure we have enough memory $memString = ini_get("memory_limit"); -switch(strtolower(substr($memString,-1))) { - case "k": - $memory = round(substr($memString,0,-1)*1024); - break; - case "m": - $memory = round(substr($memString,0,-1)*1024*1024); - break; - case "g": - $memory = round(substr($memString,0,-1)*1024*1024*1024); - break; - default: - $memory = round($memString); +switch(strtolower(substr($memString, -1))) { +case "k": + $memory = round(substr($memString, 0, -1)*1024); + break; +case "m": + $memory = round(substr($memString, 0, -1)*1024*1024); + break; +case "g": + $memory = round(substr($memString, 0, -1)*1024*1024*1024); + break; +default: + $memory = round($memString); } + // Check we have at least 32M -if($memory < (32 * 1024 * 1024)) { +if ($memory < (32 * 1024 * 1024)) { // Increase memory limit ini_set('memory_limit', '32M'); } @@ -91,34 +97,34 @@ require_once('filesystem/Filesystem.php'); require_once("core/Session.php"); // If this is a dev site, enable php error reporting -if(Director::isDev()) { +if (Director::isDev()) { error_reporting(E_ALL); } Session::start(); -if(isset($_GET['url'])) { +if (isset($_GET['url'])) { $url = $_GET['url']; // Lighttpd uses this } else { list($url, $query) = explode('?', $_SERVER['REQUEST_URI'], 2); parse_str($query, $_GET); - if($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET); + if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET); } -if(ManifestBuilder::staleManifest()){ +if (ManifestBuilder::staleManifest()) { ManifestBuilder::compileManifest(); } require_once(MANIFEST_FILE); -if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE)); +if (isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE)); -if(isset($_GET['debug_profile'])) Profiler::init(); -if(isset($_GET['debug_profile'])) Profiler::mark('all_execution'); +if (isset($_GET['debug_profile'])) Profiler::init(); +if (isset($_GET['debug_profile'])) Profiler::mark('all_execution'); -if(isset($_GET['debug_profile'])) Profiler::mark('main.php init'); +if (isset($_GET['debug_profile'])) Profiler::mark('main.php init'); // Load error handlers Debug::loadErrorHandlers(); @@ -126,9 +132,9 @@ Debug::loadErrorHandlers(); // Connect to database require_once("core/model/DB.php"); -if(isset($_GET['debug_profile'])) Profiler::mark('DB::connect'); +if (isset($_GET['debug_profile'])) Profiler::mark('DB::connect'); DB::connect($databaseConfig); -if(isset($_GET['debug_profile'])) Profiler::unmark('DB::connect'); +if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect'); // Get the request URL @@ -136,15 +142,13 @@ $baseURL = dirname(dirname($_SERVER['SCRIPT_NAME'])); -if(substr($url,0,strlen($baseURL)) == $baseURL) $url = substr($url,strlen($baseURL)); +if (substr($url, 0, strlen($baseURL)) == $baseURL) $url = substr($url, strlen($baseURL)); // Direct away - this is the "main" function, that hands control to the appropriate controller -if(isset($_GET['debug_profile'])) Profiler::unmark('main.php init'); +if (isset($_GET['debug_profile'])) Profiler::unmark('main.php init'); Director::direct($url); -if(isset($_GET['debug_profile'])) { +if (isset($_GET['debug_profile'])) { Profiler::unmark('all_execution'); Profiler::show(isset($_GET['profile_trace'])); } - -?> diff --git a/sake b/sake index 11d40b8f5..1f012fb37 100755 --- a/sake +++ b/sake @@ -15,7 +15,7 @@ if [ "$1" = "installsake" ]; then fi # Find the PHP binary -for candidatephp in "php5 php"; do +for candidatephp in php5 php; do if [ -f `which $candidatephp` ]; then php=`which $candidatephp` break @@ -37,4 +37,4 @@ if [ -f ./cli-script.php ]; then exit 0 fi -echo "Can't find ./sapphire/cli-script.php or ./cli-script.php" \ No newline at end of file +echo "Can't find ./sapphire/cli-script.php or ./cli-script.php" diff --git a/search/SearchContext.php b/search/SearchContext.php index b612734df..f06745851 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -68,9 +68,9 @@ class SearchContext extends Object { * @return FieldSet */ public function getSearchFields() { + return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields(); // $this->fields is causing weirdness, so we ignore for now, using the default scaffolding - //return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields(); - return singleton($this->modelClass)->scaffoldSearchFields(); + //return singleton($this->modelClass)->scaffoldSearchFields(); } /** @@ -143,8 +143,6 @@ class SearchContext extends Object { $query = $this->getQuery($searchParams, $sort, $limit); - $sql = $query->sql(); - // use if a raw SQL query is needed $results = new DataObjectSet(); foreach($query->execute() as $row) { diff --git a/search/filters/CollectionFilter.php b/search/filters/CollectionFilter.php deleted file mode 100644 index fd80c560d..000000000 --- a/search/filters/CollectionFilter.php +++ /dev/null @@ -1,32 +0,0 @@ -applyRelation($query); - $values = explode(',',$this->getValue()); - if(!$values) return false; - - for($i=0; $iwhere("{$this->getDbName()} IN ({$SQL_valueStr})"); - } - -} -?> \ No newline at end of file diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index c4478fd30..92daa0ede 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -21,8 +21,10 @@ class ExactMatchFilter extends SearchFilter { * @return unknown */ public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where("{$this->getDbName()} = '{$this->getValue()}'"); + if($this->getValue()) { + $query = $this->applyRelation($query); + return $query->where("{$this->getDbName()} = '{$this->getValue()}'"); + } } } diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php new file mode 100644 index 000000000..5e8b87187 --- /dev/null +++ b/search/filters/ExactMatchMultiFilter.php @@ -0,0 +1,34 @@ +getValue()) { + $query = $this->applyRelation($query); + $values = explode(',',$this->getValue()); + if(!$values) return false; + + for($i=0; $iwhere("{$this->getDbName()} IN ({$SQL_valueStr})"); + } + } + +} +?> \ No newline at end of file diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index b85f408a4..dce6214df 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -103,18 +103,20 @@ abstract class SearchFilter extends Object { */ protected function applyRelation($query) { if (is_array($this->relation)) { - $model = singleton($this->model); foreach($this->relation as $rel) { + $model = singleton($this->model); if ($component = $model->has_one($rel)) { - $foreignKey = $model->getReverseAssociation($component); - $query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`"); + if(!$query->isJoinedTo($component)) { + $foreignKey = $model->getReverseAssociation($component); + $query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`"); + } $this->model = $component; } elseif ($component = $model->has_many($rel)) { - $ancestry = $model->getClassAncestry(); - $model = singleton($component); - $foreignKey = $model->getReverseAssociation($ancestry[0]); - $foreignKey = ($foreignKey) ? $foreignKey : $ancestry[0]; - $query->leftJoin($component, "`$component`.`{$foreignKey}ID` = `{$this->model}`.`ID`"); + if(!$query->isJoinedTo($component)) { + $ancestry = $model->getClassAncestry(); + $foreignKey = $model->getComponentJoinField($rel); + $query->leftJoin($component, "`$component`.`{$foreignKey}` = `{$ancestry[0]}`.`ID`"); + } $this->model = $component; } elseif ($component = $model->many_many($rel)) { list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; diff --git a/search/filters/StartsWithMultiFilter.php b/search/filters/StartsWithMultiFilter.php new file mode 100644 index 000000000..bd37aecbb --- /dev/null +++ b/search/filters/StartsWithMultiFilter.php @@ -0,0 +1,29 @@ +getValue()) { + $query = $this->applyRelation($query); + $values = explode(',',$this->getValue()); + + foreach($values as $value) { + $SQL_value = Convert::raw2sql(str_replace("'", '', $value)); + $matches[] = "{$this->getDbName()} LIKE '$SQL_value%'"; + } + return $query->where(implode(" OR ", $matches)); + } + } + +} +?> \ No newline at end of file diff --git a/tests/SearchContextTest.php b/tests/SearchContextTest.php index ee40de585..c2dba7d5d 100644 --- a/tests/SearchContextTest.php +++ b/tests/SearchContextTest.php @@ -1,12 +1,12 @@ getDefaultSearchContext(); - $results = $context->getResults(array('Name'=>'')); $this->assertEquals(5, $results->Count()); @@ -69,6 +69,20 @@ class SearchContextTest extends SapphireTest { $context->getFilters() ); } + + function testUserDefinedFieldsAppearInSearchContext() { + $company = singleton('SearchContextTest_Company'); + $context = $company->getDefaultSearchContext(); + $fields = $context->getFields(); + $this->assertEquals( + new FieldSet( + new TextField("Name", 'Name'), + new TextareaField("Industry", 'Industry'), + new NumericField("AnnualProfit", 'The Almighty Annual Profit') + ), + $context->getFields() + ); + } function testRelationshipObjectsLinkedInSearch() { $project = singleton('SearchContextTest_Project'); @@ -80,16 +94,12 @@ class SearchContextTest extends SapphireTest { $this->assertEquals(1, $results->Count()); - //Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next()); - $project = $results->First(); $this->assertType('SearchContextTest_Project', $project); $this->assertEquals("Blog Website", $project->Name); $this->assertEquals(2, $project->Actions()->Count()); $this->assertEquals("Get RSS feeds working", $project->Actions()->First()->Description); - //Debug::dump($project->Deadline()->CompletionDate); - //$this->assertEquals() } function testCanGenerateQueryUsingAllFilterTypes() { @@ -151,8 +161,14 @@ class SearchContextTest_Company extends DataObject implements TestOnly { static $searchable_fields = array( "Name" => "PartialMatchFilter", - "Industry" => "TextareaField", - "AnnualProfit" => array("NumericField" => "PartialMatchFilter") + "Industry" => array( + 'field' => "TextareaField" + ), + "AnnualProfit" => array( + 'field' => "NumericField", + 'filter' => "PartialMatchFilter", + 'title' => 'The Almighty Annual Profit' + ) ); } @@ -222,7 +238,7 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly { "PartialMatch" => "PartialMatchFilter", "Negation" => "NegationFilter", "SubstringMatch" => "SubstringFilter", - "CollectionMatch" => "CollectionFilter", + "CollectionMatch" => "ExactMatchMultiFilter", "StartsWith" => "StartsWithFilter", "EndsWith" => "EndsWithFilter" );