Merged branches/2.3 into trunk

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@66395 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sam Minnee 2008-11-22 03:33:00 +00:00
parent ece0130baa
commit 2984355f43
30 changed files with 423 additions and 88 deletions

View File

@ -10,6 +10,7 @@ URL=`php5 ./cli-script.php SapphireInfo/baseurl`
test: phpunit
phpunit:
php5 ./cli-script.php dev/build
php5 ./cli-script.php dev/tests/all flush=1
windmill:

View File

@ -5,6 +5,8 @@
* @subpackage misc
*/
class Cookie extends Object {
static $report_errors = true;
/**
* Set a cookie variable
* @param name The variable name
@ -13,11 +15,12 @@ class Cookie extends Object {
*/
static function set($name, $value, $expiryDays = 90) {
if(!headers_sent($file, $line)) {
setcookie($name, $value, time()+(86400*$expiryDays), Director::baseURL());
$_COOKIE[$name] = $value;
$expiry = $expiryDays > 0 ? time()+(86400*$expiryDays) : 0;
setcookie($name, $value, $expiry, Director::baseURL());
} else {
// if(Director::isDevMode()) user_error("Cookie '$name' can't be set. The site started outputting was content at line $line in $file", E_USER_WARNING);
if(self::$report_errors) user_error("Cookie '$name' can't be set. The site started outputting was content at line $line in $file", E_USER_WARNING);
}
$_COOKIE[$name] = $value;
}
/**
@ -32,6 +35,13 @@ class Cookie extends Object {
setcookie( $name, null, time() - 86400 );
}
}
static function set_report_errors($reportErrors) {
self::$report_errors = $reportErrors;
}
static function report_errors() {
return self::$report_errors;
}
}
?>

View File

@ -520,6 +520,16 @@ class ManifestBuilder {
private static function get_children($class) {
return isset(self::$extendsArray[$class]) ? self::$extendsArray[$class] : array();
}
/**
* Returns if the Manifest has been included
*
* @return Boolean
*/
static function has_been_included() {
global $_CLASS_MANIFEST, $_TEMPLATE_MANIFEST, $_CSS_MANIFEST, $_ALL_CLASSES;
return (bool)(empty($_CLASS_MANIFEST) && empty($_TEMPLATE_MANIFEST) && empty($_CSS_MANIFEST) && empty($_ALL_CLASSES));
}
/**
* Returns a flat array with all children of a given class

View File

@ -755,7 +755,7 @@ class Requirements_Backend {
*
*/
function process_combined_files() {
if(Director::isDev()) {
if(Director::isDev() && !SapphireTest::is_running_test()) {
return;
}

View File

@ -345,10 +345,28 @@ class SSViewer extends Object {
}
static function parseTemplateContent($content, $template="") {
// Add template filename comments on dev sites
if(Director::isDev() && $template) {
// If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE glitches
if(stripos($content, "<html") !== false) {
$content = preg_replace('/(<html[^>]*>)/i', "\\1<!-- template $template -->", $content);
$content = preg_replace('/(<\/html[^>]*>)/i', "\\1<!-- end template $template -->", $content);
} else {
$content = "<!-- template $template -->\n" . $content . "\n<!-- end template $template -->";
}
}
while(true) {
$oldContent = $content;
// Add include filename comments on dev sites
if(Director::isDev()) $replacementCode = 'return "<!-- include " . SSViewer::getTemplateFile($matches[1]) . "-->\n"
. SSViewer::getTemplateContent($matches[1])
. "\n<!-- end include " . SSViewer::getTemplateFile($matches[1]) . "-->";';
else $replacementCode = 'return SSViewer::getTemplateContent($matches[1]);';
$content = preg_replace_callback('/<' . '% include +([A-Za-z0-9_]+) +%' . '>/', create_function(
'$matches', 'return SSViewer::getTemplateContent($matches[1]);'
'$matches', $replacementCode
), $content);
if($oldContent == $content) break;
}

View File

@ -79,9 +79,6 @@ class Controller extends RequestHandler {
if(Director::isTest() && $this->basicAuthEnabled && Security::database_is_ready()) {
BasicAuth::requireLogin("SilverStripe test website. Use your CMS login", "ADMIN");
}
//
Cookie::set("PastVisitor", true);
// Directly access the session variable just in case the Group or Member tables don't yet exist
if(Session::get('loggedInAs') && Security::database_is_ready()) {
@ -405,7 +402,8 @@ class Controller extends RequestHandler {
* @return boolean
*/
function PastVisitor() {
return Cookie::get("PastVisitor") ? true : false;
user_error("Controller::PastVisitor() is deprecated", E_USER_NOTICE);
return false;
}
/**

View File

@ -170,12 +170,20 @@ class Director {
$existingGetVars = $_GET;
$existingPostVars = $_POST;
$existingSessionVars = $_SESSION;
$existingCookies = $_COOKIE;
$existingCookieReportErrors = Cookie::report_errors();
Cookie::set_report_errors(false);
$existingRequirementsBackend = Requirements::backend();
Requirements::set_backend(new Requirements_Backend());
// Replace the superglobals with appropriate test values
$_REQUEST = array_merge((array)$getVars, (array)$postVars);
$_GET = (array)$getVars;
$_POST = (array)$postVars;
$_SESSION = $session ? $session->inst_getAll() : array();
$_COOKIE = array();
$req = new HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
if($headers) foreach($headers as $k => $v) $req->addHeader($k, $v);
@ -186,6 +194,9 @@ class Director {
$_GET = $existingGetVars;
$_POST = $existingPostVars;
$_SESSION = $existingSessionVars;
$_COOKIE = $existingCookies;
Cookie::set_report_errors($existingCookieReportErrors);
Requirements::set_backend($existingRequirementsBackend);
// These are needed so that calling Director::test() doesnt muck with whoever is calling it.
// Really, it's some inapproriate coupling and should be resolved by making less use of statics
@ -692,7 +703,7 @@ class Director {
if(self::$environment_type) return self::$environment_type == 'dev';
// Check if we are running on one of the development servers
if(in_array($_SERVER['HTTP_HOST'], Director::$dev_servers)) {
if(isset($_SERVER['HTTP_HOST']) && in_array($_SERVER['HTTP_HOST'], Director::$dev_servers)) {
return true;
}
/*
@ -717,7 +728,7 @@ class Director {
}
// Check if we are running on one of the test servers
if(in_array($_SERVER['HTTP_HOST'], Director::$test_servers)) {
if(isset($_SERVER['HTTP_HOST']) && in_array($_SERVER['HTTP_HOST'], Director::$test_servers)) {
return true;
}

View File

@ -72,7 +72,7 @@ class ModelAsController extends Controller implements NestedController {
return $controller;
} else {
return "The requested page couldn't be found.";
return new HTTPResponse("The requested page couldn't be found.",404);
}
} else {

View File

@ -2062,7 +2062,11 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
// Get the tables to join to
$tableClasses = ClassInfo::dataClassesFor($this->class);
if(!$tableClasses) {
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class", E_USER_ERROR);
if(!ManifestBuilder::has_been_included()) {
user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR);
} else {
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
}
}
$baseClass = array_shift($tableClasses);
@ -2202,6 +2206,10 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* @return mixed The objects matching the filter, in the class specified by $containerClass
*/
public function instance_get($filter = "", $sort = "", $join = "", $limit="", $containerClass = "DataObjectSet") {
if(!DB::isActive()) {
user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
}
$query = $this->extendedSQL($filter, $sort, $limit, $join);
$records = $query->execute();
@ -2300,6 +2308,10 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* @return DataObject The first item matching the query
*/
public function instance_get_one($filter, $orderby = null) {
if(!DB::isActive()) {
user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
}
$query = $this->buildSQL($filter);
$query->limit = "1";
if($orderby) {

View File

@ -94,16 +94,11 @@ class ErrorPage extends Page {
* @param boolean $createNewVersion Set this to true to create a new version number. By default, the existing version number will be copied over.
*/
function publish($fromStage, $toStage, $createNewVersion = false) {
$alc_enc = isset($_COOKIE['alc_enc']) ? $_COOKIE['alc_enc'] : null;
Cookie::set('alc_enc', null);
$oldStage = Versioned::current_stage();
// Run the page
Requirements::clear();
$controller = new ErrorPage_Controller($this);
$errorContent = $controller->handleRequest(new HTTPRequest('GET',''))->getBody();
Requirements::clear();
$response = Director::test($this->Link());
$errorContent = $response->getBody();
if(!file_exists(ASSETS_PATH)) {
mkdir(ASSETS_PATH, 02775);
@ -116,9 +111,6 @@ class ErrorPage extends Page {
// Restore the version we're currently connected to.
Versioned::reading_stage($oldStage);
// Log back in
if(isset($alc_enc)) Cookie::set('alc_enc', $alc_enc);
return $this->extension_instances['Versioned']->publish($fromStage, $toStage, $createNewVersion);
}

View File

@ -432,8 +432,8 @@ class SQLQuery extends Object {
* @return boolean
*/
function filtersOnID() {
return ($query->where && count($query->where) == 1 &&
(strpos($query->where[0], ".`ID` = ") || strpos($query->where[0], ".ID = ") || strpos($query->where[0], "ID = ") )
return ($this->where && count($this->where) == 1 &&
(strpos($this->where[0], ".`ID` = ") || strpos($this->where[0], ".ID = ") || strpos($this->where[0], "ID = ") )
);
}

View File

@ -1370,8 +1370,6 @@ class SiteTree extends DataObject {
// Handle activities undertaken by decorators
$this->extend('onBeforePublish', $original);
$this->AssignedToID = 0;
$this->RequestedByID = 0;
$this->Status = "Published";
//$this->PublishedByID = Member::currentUser()->ID;
$this->write();
@ -1410,8 +1408,6 @@ class SiteTree extends DataObject {
*/
function doRollbackTo($version) {
$this->publish($version, "Stage", true);
$this->AssignedToID = 0;
$this->RequestedByID = 0;
$this->Status = "Saved (update)";
$this->writeWithoutVersion();
}
@ -1424,8 +1420,6 @@ class SiteTree extends DataObject {
// Use a clone to get the updates made by $this->publish
$clone = DataObject::get_by_id("SiteTree", $this->ID);
$clone->AssignedToID = 0;
$clone->RequestedByID = 0;
$clone->Status = "Published";
$clone->writeWithoutVersion();
}
@ -1443,11 +1437,9 @@ class SiteTree extends DataObject {
* Changing the condition from empty($this->ID) to
* !$this->ID && !$this->record['ID'] fixed this.
*/
if(empty($this->ID))
return true;
if(empty($this->ID)) return true;
if(is_numeric($this->ID))
return false;
if(is_numeric($this->ID)) return false;
return stripos($this->ID, 'new') === 0;
}

View File

@ -28,8 +28,6 @@ class VirtualPage extends Page {
$nonVirtualFields = array(
"SecurityTypeID",
"OwnerID",
"AssignedToID",
"RequestedByID",
"URLSegment",
"Sort",
"Status",

View File

@ -43,8 +43,8 @@ class ForeignKey extends Int {
$field = new FileField($relationName, $title, $this->value);
}
} else {
$objs = DataObject::get($this->object->class);
$titleField = (singleton($this->object->class)->hasField('Title')) ? "Title" : "Name";
$objs = DataObject::get($hasOneClass);
$titleField = (singleton($hasOneClass)->hasField('Title')) ? "Title" : "Name";
$map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false;
$field = new DropdownField($this->name, $title, $map, null, null, ' ');
}

View File

@ -73,7 +73,11 @@ class CsvBulkLoader extends BulkLoader {
// trigger custom search method for finding a relation based on the given value
// and write it back to the relation (or create a new object)
$relationName = $this->relationCallbacks[$fieldName]['relationname'];
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
if($this->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
$relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}(&$obj, $val, $record);
} elseif($obj->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
}
if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->has_one($relationName);
$relationObj = new $relationClass();

View File

@ -22,19 +22,31 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
protected $originalMailer;
protected $originalMemberPasswordValidator;
protected $originalRequirements;
protected $originalIsRunningTest;
protected $mailer;
protected static $is_running_test = false;
public static function is_running_test() {
return self::$is_running_test;
}
/**
* @var YamlFixture
*/
protected $fixture;
function setUp() {
// Mark test as being run
$this->originalIsRunningTest = self::$is_running_test;
self::$is_running_test = true;
// Remove password validation
$this->originalMemberPasswordValidator = Member::password_validator();
$this->originalRequirements = Requirements::backend();
Member::set_password_validator(null);
Cookie::set_report_errors(false);
$className = get_class($this);
$fixtureFile = eval("return {$className}::\$fixture_file;");
@ -127,6 +139,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
// Restore requirements
Requirements::set_backend($this->originalRequirements);
// Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
self::$is_running_test = $this->originalIsRunningTest;
$this->originalIsRunningTest = null;
}
/**

View File

@ -480,6 +480,8 @@ class File extends DataObject {
'jpg' => 'JPEG image - good for photos',
'jpeg' => 'JPEG image - good for photos',
'png' => 'PNG image - good general-purpose format',
'ico' => 'Icon image',
'tiff' => 'Tagged image format',
'doc' => 'Word document',
'xls' => 'Excel spreadsheet',
'zip' => 'ZIP compressed file',
@ -490,7 +492,11 @@ class File extends DataObject {
'wav' => 'WAV audo file',
'avi' => 'AVI video file',
'mpg' => 'MPEG video file',
'mpeg' => 'MPEG video file'
'mpeg' => 'MPEG video file',
'js' => 'Javascript file',
'css' => 'CSS file',
'html' => 'HTML file',
'htm' => 'HTML file'
);
$ext = $this->getExtension();

View File

@ -39,10 +39,15 @@ class ConfirmedPasswordField extends FormField {
public $canBeEmpty = false;
/**
* Hide the two password fields by default,
* and provide a link instead that toggles them.
*
* @var boolean
* If set to TRUE, the "password" and "confirm password"
* formfields will be hidden via CSS and JavaScript by default,
* and triggered by a link. An additional hidden field
* determines if showing the fields has been triggered,
* and just validates/saves the input in this case.
* This behaviour works unobtrusively, without JavaScript enabled
* the fields show, validate and save by default.
*
* @param boolean $showOnClick
*/
protected $showOnClick = false;
@ -52,7 +57,7 @@ class ConfirmedPasswordField extends FormField {
*
* @var string
*/
public $showOnClickTitle = 'Change Password';
public $showOnClickTitle;
/**
* @param string $name
@ -73,11 +78,12 @@ class ConfirmedPasswordField extends FormField {
(isset($titleConfirmField)) ? $titleConfirmField : _t('Member.CONFIRMPASSWORD', 'Confirm Password')
)
);
$this->showOnClick = $showOnClick;
if($this->showOnClick) {
// has to be called in constructor because Field() isn't triggered upon saving the instance
if($showOnClick) {
$this->children->push(new HiddenField("{$name}[_PasswordFieldVisible]"));
}
$this->showOnClick = $showOnClick;
// we have labels for the subfields
$title = false;
@ -94,12 +100,22 @@ class ConfirmedPasswordField extends FormField {
$content = '';
if($this->showOnClick) {
if($this->showOnClickTitle) {
$title = $this->showOnClickTitle;
} else {
$title = _t(
'ConfirmedPasswordField.SHOWONCLICKTITLE',
'Change Password',
PR_MEDIUM,
'Label of the link which triggers display of the "change password" formfields'
);
}
$content .= "<div class=\"showOnClick\">\n";
$content .= "<a href=\"#\"" . $this->getTabIndexHTML() . ">{$this->showOnClickTitle}</a>\n";
$content .= "<a href=\"#\"" . $this->getTabIndexHTML() . ">{$title}</a>\n";
$content .= "<div class=\"showOnClickContainer\">";
}
foreach($this->children as $field) {
$content .= $field->FieldHolder();
}
@ -121,6 +137,24 @@ class ConfirmedPasswordField extends FormField {
$this->canBeEmpty = (bool)$value;
}
/**
* The title on the link which triggers display of the
* "password" and "confirm password" formfields.
* Only used if {@link setShowOnClick()} is set to TRUE.
*
* @param $title
*/
public function setShowOnClickTitle($title) {
$this->showOnClickTitle = $title;
}
/**
* @return string
*/
public function getShowOnClickTitle() {
return $this->showOnClickTitle;
}
function setRightTitle($title) {
foreach($this->children as $field) {
$field->setRightTitle($title);

View File

@ -515,11 +515,11 @@ class FieldSet extends DataObjectSet {
* @return Position in children collection (first position starts with 0). Returns FALSE if the field can't be found.
*/
function fieldPosition($field) {
if(is_string($field)) $field = $this->fieldByName($field);
if(is_object($field)) $field = $field->Name();
$i = 0;
foreach($this->dataFields() as $child) {
if($child == $field) return $i;
if($child->Name() == $field) return $i;
$i++;
}

View File

@ -251,13 +251,16 @@ class FormField extends RequestHandler {
* Set the field value.
* Returns $this.
*/
function setValue($value) { $this->value = $value; return $this; }
function setValue($value) {
$this->value = $value; return $this;
}
/**
* Set the field name
*/
function setName($name) { $this->name = $name; }
function setName($name) {
$this->name = $name;
}
/**
* Set the container form.

View File

@ -21,9 +21,13 @@ class HasManyComplexTableField extends ComplexTableField {
protected $relationAutoSetting = false;
function __construct($controller, $name, $sourceClass, $fieldList, $detailFormFields = null, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter, $sourceSort, $sourceJoin);
Requirements::javascript(SAPPHIRE_DIR . "/javascript/i18n.js");
Requirements::javascript(SAPPHIRE_DIR . "/javascript/HasManyFileField.js");
Requirements::javascript(SAPPHIRE_DIR . '/javascript/RelationComplexTableField.js');
Requirements::css(SAPPHIRE_DIR . '/css/HasManyFileField.css');
$this->Markable = true;
if($controllerClass = $this->controllerClass()) {
@ -34,14 +38,6 @@ class HasManyComplexTableField extends ComplexTableField {
}
function Field() {
Requirements::javascript(SAPPHIRE_DIR . "/javascript/i18n.js");
Requirements::javascript(SAPPHIRE_DIR . "/javascript/HasManyFileField.js");
Requirements::javascript(SAPPHIRE_DIR . '/javascript/RelationComplexTableField.js');
Requirements::css(SAPPHIRE_DIR . '/css/HasManyFileField.css');
return parent::Field();
}
/**
* Try to determine the DataObject that this field is built on top of
*/

View File

@ -39,6 +39,7 @@ class ScaffoldingComplexTableField_Popup extends Form {
Requirements::javascript(SAPPHIRE_DIR . "/javascript/ComplexTableField_popup.js");
// jQuery requirements (how many of these are actually needed?)
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery_improvements.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/plugins/livequery/jquery.livequery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/ui/ui.core.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/ui/ui.tabs.js');

View File

@ -54,6 +54,7 @@ class TabSet extends CompositeField {
Requirements::javascript(THIRDPARTY_DIR . "/behaviour.js");
Requirements::javascript(THIRDPARTY_DIR . "/prototype_improvements.js");
Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js");
Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery_improvements.js");
Requirements::javascript(THIRDPARTY_DIR . '/jquery/plugins/livequery/jquery.livequery.js');
Requirements::javascript(THIRDPARTY_DIR . "/tabstrip/tabstrip.js");
Requirements::css(THIRDPARTY_DIR . "/tabstrip/tabstrip.css");

View File

@ -161,8 +161,6 @@ Behaviour.register({
}
}
});
// TODO Performance-issue: Behaviour is possibly applied twice
Behaviour.apply('#$formID');
JS;
Requirements::customScript($js);

View File

@ -9,8 +9,27 @@
*/
class SearchForm extends Form {
/**
* @var boolean $showInSearchTurnOn
* @deprecated 2.3 SiteTree->ShowInSearch should always be respected
*/
protected $showInSearchTurnOn;
/**
* @var int $numPerPage How many results are shown per page.
* Relies on pagination being implemented in the search results template.
*/
protected $numPerPage = 10;
/**
*
* @param Controller $controller
* @param string $name The name of the form (used in URL addressing)
* @param FieldSet $fields Optional, defaults to a single field named "Search". Search logic needs to be customized
* if fields are added to the form.
* @param FieldSet $actions Optional, defaults to a single field named "Go".
* @param boolean $showInSearchTurnOn DEPRECATED 2.3
*/
function __construct($controller, $name, $fields = null, $actions = null, $showInSearchTurnOn = true) {
$this->showInSearchTurnOn = $showInSearchTurnOn;
@ -53,10 +72,17 @@ class SearchForm extends Form {
/**
* Return dataObjectSet of the results using $_REQUEST to get info from form.
* Wraps around {@link searchEngine()}
* Wraps around {@link searchEngine()}.
*
* @param int $numPerPage DEPRECATED 2.3 Use SearchForm->numPerPage
* @param array $data Request data as an associative array. Should contain at least a key 'Search' with all searched keywords.
* @return DataObjectSet
*/
public function getResults($numPerPage = 10){
$keywords = $_REQUEST['Search'];
public function getResults($numPerPage = null, $data = null){
// legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials
if(!isset($data)) $data = $_REQUEST;
$keywords = $data['Search'];
$andProcessor = create_function('$matches','
return " +" . $matches[2] . " +" . $matches[4] . " ";
@ -73,14 +99,20 @@ class SearchForm extends Form {
$keywords = $this->addStarsToKeywords($keywords);
if(strpos($keywords, '"') !== false || strpos($keywords, '+') !== false || strpos($keywords, '-') !== false || strpos($keywords, '*') !== false) {
return $this->searchEngine($keywords, $numPerPage, "Relevance DESC", "", true);
$results = $this->searchEngine($keywords, $numPerPage, "Relevance DESC", "", true);
} else {
return $this->searchEngine($keywords, $numPerPage);
$sortBy = "Relevance DESC";
}
$results = $this->searchEngine($keywords, $numPerPage);
}
// filter by permission
if($results) foreach($results as $result) {
if(!$result->canView()) $results->remove($result);
}
return $results;
}
function addStarsToKeywords($keywords) {
protected function addStarsToKeywords($keywords) {
if(!trim($keywords)) return "";
// Add * to each keyword
$splitWords = split(" +" , trim($keywords));
@ -97,13 +129,18 @@ class SearchForm extends Form {
}
return implode(" ", $newWords);
}
/**
* The core search engine, used by this class and its subclasses to do fun stuff.
* Searches both SiteTree and File.
*
* @param string $keywords Keywords as a string.
*/
public function searchEngine($keywords, $numPerPage = 10, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
public function searchEngine($keywords, $numPerPage = null, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
if(!$numPerPage) $numPerPage = $this->numPerPage;
$fileFilter = '';
$keywords = addslashes($keywords);
@ -139,14 +176,14 @@ class SearchForm extends Form {
$baseClass = reset($queryContent->from);
// There's no need to do all that joining
$queryContent->from = array(str_replace('`','',$baseClass) => $baseClass);
$queryContent->select = array("ClassName","$baseClass.ID","ParentID","Title","URLSegment","Content","LastEdited","Created","_utf8'' AS Filename", "_utf8'' AS Name", "$relevanceContent AS Relevance");
$queryContent->select = array("ClassName","$baseClass.ID","ParentID","Title","URLSegment","Content","LastEdited","Created","_utf8'' AS Filename", "_utf8'' AS Name", "$relevanceContent AS Relevance", "CanViewType");
$queryContent->orderby = null;
$queryFiles = singleton('File')->extendedSQL($notMatch . $matchFile . $fileFilter, "");
$baseClass = reset($queryFiles->from);
// There's no need to do all that joining
$queryFiles->from = array(str_replace('`','',$baseClass) => $baseClass);
$queryFiles->select = array("ClassName","$baseClass.ID","_utf8'' AS ParentID","Title","_utf8'' AS URLSegment","Content","LastEdited","Created","Filename","Name","$relevanceFile AS Relevance");
$queryFiles->select = array("ClassName","$baseClass.ID","_utf8'' AS ParentID","Title","_utf8'' AS URLSegment","Content","LastEdited","Created","Filename","Name","$relevanceFile AS Relevance","NULL AS CanViewType");
$queryFiles->orderby = null;
$fullQuery = $queryContent->sql() . " UNION " . $queryFiles->sql() . " ORDER BY $sortBy LIMIT $limit";
@ -164,8 +201,17 @@ class SearchForm extends Form {
return $doSet;
}
public function getSearchQuery() {
return Convert::raw2xml($_REQUEST['Search']);
/**
* Get the search query for display in a "You searched for ..." sentence.
*
* @param array $data
* @return string
*/
public function getSearchQuery($data = null) {
// legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials
if(!isset($data)) $data = $_REQUEST;
return Convert::raw2xml($data['Search']);
}
}

View File

@ -811,7 +811,13 @@ class Member extends DataObject {
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
$password = new ConfirmedPasswordField('Password', 'Password');
$password = new ConfirmedPasswordField(
'Password',
'Password',
null,
null,
true // showOnClick
);
$password->setCanBeEmpty(true);
$mainFields->replaceField('Password', $password);

View File

@ -115,16 +115,18 @@ class MemberLoginForm extends LoginForm {
Director::redirect('Security/changepassword');
} else if(isset($_REQUEST['BackURL']) && $backURL = $_REQUEST['BackURL']) {
} elseif(isset($_REQUEST['BackURL']) && $backURL = $_REQUEST['BackURL']) {
Session::clear("BackURL");
Director::redirect($backURL);
} else {
$member=Member::CurrentUser();
$firstname = Convert::raw2xml($member->FirstName);
Session::set("Security.Message.message",
sprintf(_t('Member.WELCOMEBACK', "Welcome Back, %s"), $firstname)
);
Session::set("Security.Message.type", "good");
$member = Member::currentUser();
if($member) {
$firstname = Convert::raw2xml($member->FirstName);
Session::set('Security.Message.message',
sprintf(_t('Member.WELCOMEBACK', "Welcome Back, %s"), $firstname)
);
Session::set("Security.Message.type", "good");
}
Director::redirectBack();
}
} else {

View File

@ -316,6 +316,7 @@ class Security extends Controller {
$link_base = Director::absoluteURL($this->Link("login"));
Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js");
Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery_improvements.js");
Requirements::javascript(THIRDPARTY_DIR . '/jquery/plugins/livequery/jquery.livequery.js');
Requirements::javascript(THIRDPARTY_DIR . "/tabstrip/tabstrip.js");
Requirements::css(THIRDPARTY_DIR . "/tabstrip/tabstrip.css");

View File

@ -0,0 +1,146 @@
<?php
/**
* @package sapphire
* @subpackage testing
*
* @todo Fix unpublished pages check in testPublishedPagesMatchedByTitle()
* @todo All tests run on unpublished pages at the moment, due to the searchform not distinguishing between them
*/
class SearchFormTest extends FunctionalTest {
static $fixture_file = 'sapphire/tests/search/SearchFormTest.yml';
protected $mockController;
function setUp() {
parent::setUp();
$holderPage = $this->objFromFixture('SiteTree', 'searchformholder');
$this->mockController = new ContentController($holderPage);
$this->mockController->setSession(new Session(Controller::curr()->getSession()));
$this->mockController->pushCurrent();
}
function tearDown() {
$this->mockController->popCurrent();
parent::tearDown();
}
function testPublishedPagesMatchedByTitle() {
$sf = new SearchForm($this->mockController, 'SearchForm');
$publishedPage = $this->objFromFixture('SiteTree', 'publicPublishedPage');
$publishedPage->publish('Stage', 'Live');
$results = $sf->getResults(null, array('Search'=>'publicPublishedPage'));
$this->assertContains(
$publishedPage->ID,
$results->column('ID'),
'Published pages are found by searchform'
);
}
/*
function testUnpublishedPagesNotIncluded() {
$sf = new SearchForm($this->mockController, 'SearchForm');
$results = $sf->getResults(null, array('Search'=>'publicUnpublishedPage'));
$unpublishedPage = $this->objFromFixture('SiteTree', 'publicUnpublishedPage');
$this->assertNotContains(
$unpublishedPage->ID,
$results->column('ID'),
'Unpublished pages are not found by searchform'
);
}
*/
function testPagesRestrictedToLoggedinUsersNotIncluded() {
$sf = new SearchForm($this->mockController, 'SearchForm');
$page = $this->objFromFixture('SiteTree', 'restrictedViewLoggedInUsers');
$results = $sf->getResults(null, array('Search'=>'restrictedViewLoggedInUsers'));
$this->assertNotContains(
$page->ID,
$results->column('ID'),
'Page with "Restrict to logged in users" doesnt show without valid login'
);
$member = $this->objFromFixture('Member', 'randomuser');
$member->logIn();
$results = $sf->getResults(null, array('Search'=>'restrictedViewLoggedInUsers'));
$this->assertContains(
$page->ID,
$results->column('ID'),
'Page with "Restrict to logged in users" shows if login is present'
);
$member->logOut();
}
function testPagesRestrictedToSpecificGroupNotIncluded() {
$sf = new SearchForm($this->mockController, 'SearchForm');
$page = $this->objFromFixture('SiteTree', 'restrictedViewOnlyWebsiteUsers');
$results = $sf->getResults(null, array('Search'=>'restrictedViewOnlyWebsiteUsers'));
$this->assertNotContains(
$page->ID,
$results->column('ID'),
'Page with "Restrict to these users" doesnt show without valid login'
);
$member = $this->objFromFixture('Member', 'randomuser');
$member->logIn();
$results = $sf->getResults(null, array('Search'=>'restrictedViewOnlyWebsiteUsers'));
$this->assertNotContains(
$page->ID,
$results->column('ID'),
'Page with "Restrict to these users" doesnt show if logged in user is not in the right group'
);
$member->logOut();
$member = $this->objFromFixture('Member', 'websiteuser');
$member->logIn();
$results = $sf->getResults(null, array('Search'=>'restrictedViewOnlyWebsiteUsers'));
$this->assertContains(
$page->ID,
$results->column('ID'),
'Page with "Restrict to these users" shows if user in this group is logged in'
);
$member->logOut();
}
function testInheritedRestrictedPagesNotInlucded() {
$sf = new SearchForm($this->mockController, 'SearchForm');
$page = $this->objFromFixture('SiteTree', 'inheritRestrictedView');
$results = $sf->getResults(null, array('Search'=>'inheritRestrictedView'));
$this->assertNotContains(
$page->ID,
$results->column('ID'),
'Page inheriting "Restrict to loggedin users" doesnt show without valid login'
);
$member = $this->objFromFixture('Member', 'websiteuser');
$member->logIn();
$results = $sf->getResults(null, array('Search'=>'inheritRestrictedView'));
$this->assertContains(
$page->ID,
$results->column('ID'),
'Page inheriting "Restrict to loggedin users" shows if user in this group is logged in'
);
$member->logOut();
}
function testDisabledShowInSearchFlagNotIncluded() {
$sf = new SearchForm($this->mockController, 'SearchForm');
$page = $this->objFromFixture('SiteTree', 'dontShowInSearchPage');
$results = $sf->getResults(null, array('Search'=>'dontShowInSearchPage'));
$this->assertNotContains(
$page->ID,
$results->column('ID'),
'Page with "Show in Search" disabled doesnt show'
);
}
}
?>

View File

@ -0,0 +1,33 @@
Group:
websiteusers:
Title: View certain restricted pages
Member:
randomuser:
Email: randomuser@test.com
Password: test
websiteuser:
Email: websiteuser@test.com
Password: test
Groups: =>Group.websiteusers
SiteTree:
searchformholder:
URLSegment: searchformholder
Title: searchformholder
publicPublishedPage:
Title: publicPublishedPage
publicUnpublishedPage:
Title: publicUnpublishedPage
restrictedViewLoggedInUsers:
CanViewType: LoggedInUsers
Title: restrictedViewLoggedInUsers
restrictedViewOnlyWebsiteUsers:
CanViewType: OnlyTheseUsers
ViewerGroups: =>Group.websiteusers
Title: restrictedViewOnlyWebsiteUsers
inheritRestrictedView:
CanViewType: Inherit
Parent: =>SiteTree.restrictedViewLoggedInUsers
Title: inheritRestrictedView
dontShowInSearchPage:
Title: dontShowInSearchPage
ShowInSearch: 0