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; $i where("{$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" );