diff --git a/core/HTTP.php b/core/HTTP.php index 04f7d818a..f48f5b96e 100644 --- a/core/HTTP.php +++ b/core/HTTP.php @@ -139,7 +139,7 @@ class HTTP { * @deprecated 2.3 Return a HTTPResponse::send_file() object instead */ static function sendFileToBrowser($fileData, $fileName, $mimeType = false) { - user_error("HTTP::sendFileToBrowser() deprecated; return a HTTPResponse::send_file() object instead", E_USER_NOTICE); + user_error("HTTP::sendFileToBrowser() deprecated; return a HTTPRequest::send_file() object instead", E_USER_NOTICE); HTTPRequest::send_file($fileData, $fileName, $mimeType)->output(); exit(0); } diff --git a/core/ManifestBuilder.php b/core/ManifestBuilder.php index 9dc1bc40d..cf4e1aca2 100644 --- a/core/ManifestBuilder.php +++ b/core/ManifestBuilder.php @@ -62,7 +62,10 @@ class ManifestBuilder { if(isset($_REQUEST['usetestmanifest'])) { self::load_test_manifest(); } else { - if(!file_exists(MANIFEST_FILE) || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH)) || isset($_GET['flush'])) { + // The dev/build reference is some coupling but it solves an annoying bug + if(!file_exists(MANIFEST_FILE) || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH)) + || isset($_GET['flush']) || (isset($_REQUEST['url']) && ($_REQUEST['url'] == 'dev/build' + || $_REQUEST['url'] == BASE_URL . '/dev/build'))) { self::create_manifest_file(); } require_once(MANIFEST_FILE); @@ -114,7 +117,7 @@ class ManifestBuilder { $output .= "global \$$globalName;\n\$$globalName = " . var_export($globalVal, true) . ";\n\n"; } foreach($manifestInfo['require_once'] as $requireItem) { - $output .= "require_once(\"$requireItem\");\n"; + $output .= 'require_once("' . addslashes($requireItem) . "\");\n"; } return $output; diff --git a/core/Requirements.php b/core/Requirements.php index d4e206c38..476560598 100644 --- a/core/Requirements.php +++ b/core/Requirements.php @@ -131,8 +131,6 @@ class Requirements { static function block($fileOrID) { self::backend()->block($fileOrID); } - - /** * Removes an item from the blocking-list. @@ -243,6 +241,17 @@ class Requirements { return self::backend()->get_custom_scripts(); } + /** + * Set whether you want to write the JS to the body of the page or + * in the head section + * + * @see {@link Requirements_Backend::set_write_js_to_body()} + * @param boolean + */ + static function set_write_js_to_body($var) { + self::backend()->set_write_js_to_body($var); + } + static function debug() { return self::backend()->debug(); } @@ -338,7 +347,16 @@ class Requirements_Backend { * @var boolean */ public $write_js_to_body = true; - + + /** + * Set whether you want the files written to the head or the body. It + * writes to the body by default which can break some scripts + * + * @param boolean + */ + public function set_write_js_to_body($var) { + $this->write_js_to_body = $var; + } /** * Register the given javascript file as required. * Filenames should be relative to the base, eg, 'sapphire/javascript/loader.js' @@ -783,7 +801,7 @@ class Requirements_Backend { $newJSRequirements[$file] = true; } } - + foreach($this->css as $file => $params) { if(isset($combinerCheck[$file])) { $newCSSRequirements[$combinerCheck[$file]] = true; diff --git a/core/SSViewer.php b/core/SSViewer.php index 6ac2c5d14..f812f00c0 100644 --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -48,6 +48,15 @@ * @subpackage view */ class SSViewer extends Object { + protected static $source_file_comments = true; + + /** + * Set whether HTML comments indicating the source .SS file used to render this page should be + * included in the output. This is enabled by default + */ + function set_source_file_comments($val) { + self::$source_file_comments = $val; + } /** * @var array $chosenTemplates Associative array for the different @@ -346,7 +355,7 @@ class SSViewer extends Object { static function parseTemplateContent($content, $template="") { // Add template filename comments on dev sites - if(Director::isDev() && $template) { + if(Director::isDev() && self::$source_file_comments && $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, "]*>)/i', "\\1", $content); diff --git a/core/control/ContentController.php b/core/control/ContentController.php index 547419d5a..319f59847 100644 --- a/core/control/ContentController.php +++ b/core/control/ContentController.php @@ -25,7 +25,14 @@ class ContentController extends Controller { * The ContentController will take the URLSegment parameter from the URL and use that to look * up a SiteTree record. */ - public function __construct($dataRecord) { + public function __construct($dataRecord = null) { + if(!$dataRecord) { + $dataRecord = new Page(); + if($this->hasMethod("Title")) $dataRecord->Title = $this->Title(); + $dataRecord->URLSegment = get_class($this); + $dataRecord->ID = -1; + } + $this->dataRecord = $dataRecord; $this->failover = $this->dataRecord; parent::__construct(); diff --git a/core/control/Director.php b/core/control/Director.php index 209158be9..d84a7d50c 100644 --- a/core/control/Director.php +++ b/core/control/Director.php @@ -87,7 +87,7 @@ class Director { * @uses handleRequest() rule-lookup logic is handled by this. * @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call. */ - function direct($url) { + static function direct($url) { $req = new HTTPRequest( (isset($_SERVER['X-HTTP-Method-Override'])) ? $_SERVER['X-HTTP-Method-Override'] : $_SERVER['REQUEST_METHOD'], $url, @@ -151,18 +151,19 @@ class Director { * @uses getControllerForURL() The rule-lookup logic is handled by this. * @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call. */ - function test($url, $postVars = null, $session = null, $httpMethod = null, $body = null, $headers = null) { + static function test($url, $postVars = null, $session = null, $httpMethod = null, $body = null, $headers = null) { // 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 $oldStage = Versioned::current_stage(); + $getVars = array(); if(!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET"; - $getVars = array(); - if(strpos($url,'?') !== false) { + if(strpos($url, '?') !== false) { list($url, $getVarsEncoded) = explode('?', $url, 2); - parse_str($getVarsEncoded, $getVars); + parse_str($getVarsEncoded, $getVars); } + if(!$session) $session = new Session(null); // Back up the current values of the superglobals diff --git a/core/control/FormResponse.php b/core/control/FormResponse.php index aeb22561b..a9f68097f 100755 --- a/core/control/FormResponse.php +++ b/core/control/FormResponse.php @@ -101,7 +101,7 @@ class FormResponse { $JS_content = Convert::raw2js($content); self::$rules[] = "\$('{$id}').loadNewPage('{$JS_content}');"; self::$rules[] = "\$('{$id}').initialize();"; - self::$rules[] = "onload_init_tabstrip();"; + self::$rules[] = "if(typeof onload_init_tabstrip != 'undefined') onload_init_tabstrip();"; } /** diff --git a/core/control/HTTPRequest.php b/core/control/HTTPRequest.php index 7dffc05a5..796704308 100644 --- a/core/control/HTTPRequest.php +++ b/core/control/HTTPRequest.php @@ -385,10 +385,8 @@ class HTTPRequest extends Object implements ArrayAccess { * @return string Value of the URL parameter (if found) */ function param($name) { - if(isset($this->allParams[$name])) - return $this->allParams[$name]; - else - return null; + if(isset($this->allParams[$name])) return $this->allParams[$name]; + else return null; } /** diff --git a/core/model/DataObject.php b/core/model/DataObject.php index 77a3133c2..b3945b032 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -1565,6 +1565,19 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP return $tabbedFields; } + /** + * need to be overload by solid dataobject, so that the customised actions of that dataobject, + * including that dataobject's decorator customised actions could be added to the EditForm. + * + * @return an Empty FieldSet(); need to be overload by solid subclass + */ + public function getCMSActions() { + $actions = new FieldSet(); + $this->extend('updateCMSActions', $actions); + return $actions; + } + + /** * Used for simple frontend forms without relation editing * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()} @@ -2015,13 +2028,16 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP $component = singleton($rel); } elseif ($rel = $component->many_many($relation)) { $component = singleton($rel[1]); + } elseif($info = $this->castingHelperPair($relation)) { + $component = singleton($info['className']); } } $object = $component->dbObject($fieldName); if (!($object instanceof DBField) && !($object instanceof ComponentSet)) { - user_error("Unable to traverse to related object field [$fieldPath] on [$this->class]", E_USER_ERROR); + // Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically + throw new Exception("Unable to traverse to related object field [$fieldPath] on [$this->class]"); } return $object; } diff --git a/core/model/DataObjectSet.php b/core/model/DataObjectSet.php index efb704a10..d9aa75ee9 100644 --- a/core/model/DataObjectSet.php +++ b/core/model/DataObjectSet.php @@ -584,7 +584,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate { public function column($value = "ID") { $list = array(); foreach($this->items as $item ){ - $list[] = $item->$value; + $list[] = ($item->hasMethod($value)) ? $item->$value() : $item->$value; } return $list; } diff --git a/core/model/ErrorPage.php b/core/model/ErrorPage.php index 1263ddcec..5a336e521 100755 --- a/core/model/ErrorPage.php +++ b/core/model/ErrorPage.php @@ -97,7 +97,8 @@ class ErrorPage extends Page { $oldStage = Versioned::current_stage(); // Run the page - $response = Director::test($this->Link()); + $response = Director::test(Director::makeRelative($this->Link())); + $errorContent = $response->getBody(); if(!file_exists(ASSETS_PATH)) { @@ -130,6 +131,7 @@ class ErrorPage extends Page { class ErrorPage_Controller extends Page_Controller { public function init() { parent::init(); + Director::set_status_code($this->failover->ErrorCode ? $this->failover->ErrorCode : 404); } } diff --git a/core/model/Hierarchy.php b/core/model/Hierarchy.php index a9900a6f3..37029aad2 100644 --- a/core/model/Hierarchy.php +++ b/core/model/Hierarchy.php @@ -510,9 +510,10 @@ class Hierarchy extends DataObjectDecorator { */ public function getParent($filter = '') { if($p = $this->owner->__get("ParentID")) { - $className = $this->owner->class; - $filter .= $filter?" AND ":""."\"$className\".\"ID\" = $p"; - return DataObject::get_one($className, $filter); + $tableClasses = ClassInfo::dataClassesFor($this->owner->class); + $baseClass = array_shift($tableClasses); + $filter .= ($filter) ? " AND " : ""."\"$baseClass\".\"ID\" = $p"; + return DataObject::get_one($this->owner->class, $filter); } } diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php index 327bb17e0..0df8bae67 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -7,7 +7,7 @@ * In addition, it contains a number of static methods for querying the site tree. * @package cms */ -class SiteTree extends DataObject { +class SiteTree extends DataObject implements PermissionProvider { /** * Indicates what kind of children this page type can have. @@ -525,7 +525,7 @@ class SiteTree extends DataObject { function can($perm, $member = null) { if(!$member && $member !== FALSE) $member = Member::currentUser(); - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; if(method_exists($this, 'can' . ucfirst($perm))) { $method = 'can' . ucfirst($perm); @@ -561,7 +561,7 @@ class SiteTree extends DataObject { */ public function canAddChildren($member = null) { if(!$member && $member !== FALSE) $member = Member::currentUser(); - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; // DEPRECATED 2.3: use canAddChildren() instead $results = $this->extend('alternateCanAddChildren', $member); @@ -570,7 +570,7 @@ class SiteTree extends DataObject { $results = $this->extend('canAddChildren', $member); if($results && is_array($results)) if(!min($results)) return false; - return $this->canEdit() && $this->stat('allowed_children') != 'none'; + return $this->canEdit($member) && $this->stat('allowed_children') != 'none'; } @@ -594,7 +594,7 @@ class SiteTree extends DataObject { if(!$member && $member !== FALSE) $member = Member::currentUser(); // admin override - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; // DEPRECATED 2.3: use canView() instead $results = $this->extend('alternateCanView', $member); @@ -648,7 +648,7 @@ class SiteTree extends DataObject { public function canDelete($member = null) { if(!$member && $member !== FALSE) $member = Member::currentUser(); - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; // DEPRECATED 2.3: use canDelete() instead $results = $this->extend('alternateCanDelete', $member); @@ -659,11 +659,11 @@ class SiteTree extends DataObject { if($results && is_array($results)) if(!min($results)) return false; // if page can't be edited, don't grant delete permissions - if(!$this->canEdit()) return false; + if(!$this->canEdit($member)) return false; $children = $this->AllChildren(); if($children) foreach($children as $child) { - if(!$child->canDelete()) return false; + if(!$child->canDelete($member)) return false; } return $this->stat('can_create') != false; @@ -690,7 +690,7 @@ class SiteTree extends DataObject { public function canCreate($member = null) { if(!$member && $member !== FALSE) $member = Member::currentUser(); - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; // DEPRECATED 2.3: use canCreate() instead $results = $this->extend('alternateCanCreate', $member); @@ -726,7 +726,7 @@ class SiteTree extends DataObject { public function canEdit($member = null) { if(!$member && $member !== FALSE) $member = Member::currentUser(); - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; // DEPRECATED 2.3: use canEdit() instead $results = $this->extend('alternateCanEdit', $member); @@ -737,7 +737,7 @@ class SiteTree extends DataObject { if($results && is_array($results)) if(!min($results)) return false; // if page can't be viewed, don't grant edit permissions - if(!$this->canView()) return false; + if(!$this->canView($member)) return false; // check for empty spec if(!$this->CanEditType || $this->CanEditType == 'Anyone') return true; @@ -745,11 +745,11 @@ class SiteTree extends DataObject { // check for inherit if($this->CanEditType == 'Inherit') { if($this->ParentID) return $this->Parent()->canEdit($member); - else return Permission::checkMember($member, 'CMS_ACCESS_CMSMain'); + else return ($member && Permission::checkMember($member, 'CMS_ACCESS_CMSMain')); } // check for any logged-in users - if($this->CanEditType == 'LoggedInUsers' && Permission::checkMember($member, 'CMS_ACCESS_CMSMain')) return true; + if($this->CanEditType == 'LoggedInUsers' && $member && Permission::checkMember($member, 'CMS_ACCESS_CMSMain')) return true; // check for specific groups if($this->CanEditType == 'OnlyTheseUsers' && $member && $member->inGroups($this->EditorGroups())) return true; @@ -774,7 +774,7 @@ class SiteTree extends DataObject { public function canPublish($member = null) { if(!$member && $member !== FALSE) $member = Member::currentUser(); - if(Permission::checkMember($member, "ADMIN")) return true; + if($member && Permission::checkMember($member, "ADMIN")) return true; // DEPRECATED 2.3: use canPublish() instead $results = $this->extend('alternateCanPublish', $member); @@ -786,7 +786,7 @@ class SiteTree extends DataObject { if($results && is_array($results)) if(!min($results)) return false; // Normal case - return $this->canEdit(); + return $this->canEdit($member); } /** @@ -1129,7 +1129,7 @@ class SiteTree extends DataObject { new TextField("MenuTitle", $this->fieldLabel('MenuTitle')), new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", PR_MEDIUM, 'HTML editor title')) ), - $tabMeta = new Tab('Meta-data', + $tabMeta = new Tab('Metadata', new FieldGroup(_t('SiteTree.URL', "URL"), new LabelField('BaseUrlLabel',Director::absoluteBaseURL()), new UniqueRestrictedTextField("URLSegment", @@ -1191,17 +1191,24 @@ class SiteTree extends DataObject { "CanViewType", "" ), - new TreeMultiselectField("ViewerGroups", $this->fieldLabel('ViewerGroups')), + $viewerGroupsField = new TreeMultiselectField("ViewerGroups", $this->fieldLabel('ViewerGroups')), new HeaderField('WhoCanEditHeader',_t('SiteTree.EDITHEADER', "Who can edit this page?"), 2), $editorsOptionsField = new OptionsetField( "CanEditType", "" ), - new TreeMultiselectField("EditorGroups", $this->fieldLabel('EditorGroups')) + $editorGroupsField = new TreeMultiselectField("EditorGroups", $this->fieldLabel('EditorGroups')) ) ) //new NamedLabelField("Status", $message, "pageStatusMessage", true) ); + + if(!Permission::check('SITETREE_GRANT_ACCESS')) { + $fields->makeFieldReadonly($viewersOptionsField); + $fields->makeFieldReadonly($viewerGroupsField); + $fields->makeFieldReadonly($editorsOptionsField); + $fields->makeFieldReadonly($editorGroupsField); + } $viewersOptionsSource = array(); if($this->Parent()->ID || $this->CanViewType == 'Inherit') $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page"); @@ -1218,7 +1225,7 @@ class SiteTree extends DataObject { $tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content")); $tabMain->setTitle(_t('SiteTree.TABMAIN', "Main")); - $tabMeta->setTitle(_t('SiteTree.TABMETA', "Meta-data")); + $tabMeta->setTitle(_t('SiteTree.TABMETA', "Metadata")); $tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behaviour")); $tabReports->setTitle(_t('SiteTree.TABREPORTS', "Reports")); $tabAccess->setTitle(_t('SiteTree.TABACCESS', "Access")); @@ -1266,36 +1273,57 @@ class SiteTree extends DataObject { /** * Get the actions available in the CMS for this page - eg Save, Publish. - * - * @return DataObjectSet The available actions for this page. + * @return FieldSet The available actions for this page. */ function getCMSActions() { - $actions = array(); + $actions = new FieldSet(); if($this->isPublished() && $this->canPublish()) { + // "unpublish" $unpublish = FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete'); $unpublish->describe(_t('SiteTree.BUTTONUNPUBLISHDESC', "Remove this page from the published site")); $unpublish->addExtraClass('delete'); - $actions[] = $unpublish; + $actions->push($unpublish); } if($this->stagesDiffer('Stage', 'Live')) { if($this->isPublished() && $this->canEdit()) { + // "rollback" $rollback = FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete'); $rollback->describe(_t('SiteTree.BUTTONCANCELDRAFTDESC', "Delete your draft and revert to the currently published page")); $rollback->addExtraClass('delete'); - $actions[] = $rollback; + $actions->push($rollback); } } - if($this->canPublish()) { - $actions[] = new FormAction('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save and Publish')); + if($this->DeletedFromStage) { + if($this->can('CMSEdit')) { + // "restore" + $actions->push(new FormAction('revert',_t('CMSMain.RESTORE','Restore'))); + + // "delete from live" + $actions->push(new FormAction('deletefromlive',_t('CMSMain.DELETEFP','Delete from the published site'))); + } + } else { + if($this->canEdit()) { + // "delete" + $actions->push($deleteAction = new FormAction('delete',_t('CMSMain.DELETE','Delete from the draft site'))); + $deleteAction->addExtraClass('delete'); + + // "save" + $actions->push(new FormAction('save',_t('CMSMain.SAVE','Save'))); + } } - // getCMSActions() can be extended with updateCmsActions() on a decorator + if($this->canPublish()) { + // "publish" + $actions->push(new FormAction('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save and Publish'))); + } + + // getCMSActions() can be extended with updateCMSActions() on a decorator $this->extend('updateCMSActions', $actions); - return new DataObjectSet($actions); + return $actions; } /** @@ -1454,12 +1482,6 @@ class SiteTree extends DataObject { $instance = singleton($class); if((($instance instanceof HiddenClass) || !$instance->canCreate()) && ($class != $this->class)) continue; - /* - $addAction = $instance->uninherited('add_action', true); - if(!$addAction) { - $addAction = $instance->singular_name(); - } - */ $addAction = $instance->i18n_singular_name(); if($class == $this->class) { @@ -1670,6 +1692,15 @@ class SiteTree extends DataObject { public static function enableCMSFieldsExtensions() { self::$runCMSFieldsExtensions = true; } + + function providePermissions() { + return array( + 'SITETREE_GRANT_ACCESS' => _t( + 'SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', + 'Control which groups can access or edit certain pages' + ) + ); + } } ?> diff --git a/core/model/VirtualPage.php b/core/model/VirtualPage.php index cfe6bc523..35eb033da 100755 --- a/core/model/VirtualPage.php +++ b/core/model/VirtualPage.php @@ -99,9 +99,12 @@ class VirtualPage extends Page { function onBeforeWrite() { // Don't do this stuff when we're publishing if(!$this->extension_instances['Versioned']->migratingVersion) { - if(isset($this->changed['CopyContentFromID']) && $this->changed['CopyContentFromID'] - && $this->CopyContentFromID != 0 && $this->class == 'VirtualPage' ) { - $CopyContentFromID = $this->CopyContentFromID; + if( + isset($this->changed['CopyContentFromID']) + && $this->changed['CopyContentFromID'] + && $this->CopyContentFromID != 0 + && $this instanceof VirtualPage + ) { $source = DataObject::get_one("SiteTree","\"SiteTree\".\"ID\"='$CopyContentFromID'"); $this->copyFrom($source); $this->URLSegment = $source->URLSegment . '-' . $this->ID; @@ -147,7 +150,7 @@ class VirtualPage_Controller extends Page_Controller { * We can't load the content without an ID or record to copy it from. */ function init(){ - if($this->record->ID){ + if(isset($this->record) && $this->record->ID){ if($this->record->VersionID != $this->failover->CopyContentFrom()->Version){ $this->reloadContent(); $this->VersionID = $this->failover->CopyContentFrom()->VersionID; diff --git a/core/model/fieldtypes/Enum.php b/core/model/fieldtypes/Enum.php index b3c321878..16adedc0c 100755 --- a/core/model/fieldtypes/Enum.php +++ b/core/model/fieldtypes/Enum.php @@ -64,6 +64,12 @@ class Enum extends DBField { return $field; } + function scaffoldSearchField($title = null) { + $field = $this->formField($title); + $field->Source = array_merge(array("" => "(Any)"), $this->enumValues()); + return $field; + } + /** * Return the values of this enum, suitable for insertion into a dropdown field. */ diff --git a/dev/CsvBulkLoader.php b/dev/CsvBulkLoader.php index acae53fd6..663ec88e3 100644 --- a/dev/CsvBulkLoader.php +++ b/dev/CsvBulkLoader.php @@ -74,7 +74,7 @@ class CsvBulkLoader extends BulkLoader { // and write it back to the relation (or create a new object) $relationName = $this->relationCallbacks[$fieldName]['relationname']; if($this->hasMethod($this->relationCallbacks[$fieldName]['callback'])) { - $relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}(&$obj, $val, $record); + $relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}($obj, $val, $record); } elseif($obj->hasMethod($this->relationCallbacks[$fieldName]['callback'])) { $relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record); } diff --git a/dev/Debug.php b/dev/Debug.php index 33f14cfb5..5a919a256 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -433,9 +433,9 @@ class Debug { * if(Director::isLive()) Debug::send_errors_to("sam@silverstripe.com"); * * @param string $emailAddress The email address to send errors to - * @param string $sendWarnings Set to true to send warnings as well as errors (Default: true) + * @param string $sendWarnings Set to true to send warnings as well as errors (Default: false) */ - static function send_errors_to($emailAddress, $sendWarnings = true) { + static function send_errors_to($emailAddress, $sendWarnings = false) { self::$send_errors_to = $emailAddress; self::$send_warnings_to = $sendWarnings ? $emailAddress : null; } diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php index c3cb2819c..ab274f4a6 100644 --- a/dev/DevelopmentAdmin.php +++ b/dev/DevelopmentAdmin.php @@ -72,6 +72,10 @@ class DevelopmentAdmin extends Controller { return new ModuleManager(); } + function viewmodel() { + return new ModelViewer(); + } + function build() { $renderer = new DebugView(); $renderer->writeHeader(); diff --git a/dev/FunctionalTest.php b/dev/FunctionalTest.php index 8c02077eb..e89e06b63 100644 --- a/dev/FunctionalTest.php +++ b/dev/FunctionalTest.php @@ -76,7 +76,7 @@ class FunctionalTest extends SapphireTest { function tearDown() { parent::tearDown(); - $this->mainSession = null; + unset($this->mainSession); // Re-enable theme, if previously disabled if($this->stat('disable_themes')) { diff --git a/dev/ModelViewer.php b/dev/ModelViewer.php new file mode 100644 index 000000000..d85dcabd2 --- /dev/null +++ b/dev/ModelViewer.php @@ -0,0 +1,175 @@ + 'handleModule', + ); + + protected $module = null; + + function handleModule($request) { + return new ModelViewer_Module($request->param('Module')); + } + + function init() { + parent::init(); + if(!Permission::check("ADMIN")) Security::permissionFailure(); + } + + /** + * Model classes + */ + function Models() { + $classes = ClassInfo::subclassesFor('DataObject'); + array_shift($classes); + $output = new DataObjectSet(); + foreach($classes as $class) { + $output->push(new ModelViewer_Model($class)); + } + return $output; + } + + /** + * Model classes, grouped by Module + */ + function Modules() { + $classes = ClassInfo::subclassesFor('DataObject'); + array_shift($classes); + + $modules = array(); + foreach($classes as $class) { + $model = new ModelViewer_Model($class); + if(!isset($modules[$model->Module])) $modules[$model->Module] = new DataObjectSet(); + $modules[$model->Module]->push($model); + } + ksort($modules); + unset($modules['userforms']); + + if($this->module) { + $modules = array($this->module => $modules[$this->module]); + } + + $output = new DataObjectSet(); + foreach($modules as $moduleName => $models) { + $output->push(new ArrayData(array( + 'Link' => 'dev/viewmodel/' . $moduleName, + 'Name' => $moduleName, + 'Models' => $models, + ))); + } + + return $output; + } +} + +class ModelViewer_Module extends ModelViewer { + static $url_handlers = array( + 'graph' => 'graph', + ); + + /** + * ModelViewer can be optionally constructed to restrict its output to a specific module + */ + function __construct($module = null) { + $this->module = $module; + } + + function graph() { + SSViewer::set_source_file_comments(false); + $dotContent = $this->renderWith("ModelViewer_dotsrc"); + $CLI_dotContent = escapeshellarg($dotContent); + + $output= `echo $CLI_dotContent | neato -Tpng:gd &> /dev/stdout`; + if(substr($output,1,3) == 'PNG') header("Content-type: image/png"); + else header("Content-type: text/plain"); + echo $output; + } +} + +/** + * Represents a single model in the model viewer + */ +class ModelViewer_Model extends ViewableData { + protected $className; + + function __construct($className) { + $this->className = $className; + } + + function getModule() { + global $_CLASS_MANIFEST; + $className = $this->className; + if(($pos = strpos($className,'_')) !== false) $className = substr($className,0,$pos); + if(isset($_CLASS_MANIFEST[$className])) { + if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $_CLASS_MANIFEST[$className], $matches)) { + return $matches[1]; + } + } + } + + function getName() { + return $this->className; + } + + function getParentModel() { + $parentClass = get_parent_class($this->className); + if($parentClass != "DataObject") return $parentClass; + } + + function Fields() { + $output = new DataObjectSet(); + + $output->push(new ModelViewer_Field($this,'ID', 'PrimaryKey')); + if(!$this->ParentModel) { + $output->push(new ModelViewer_Field($this,'Created', 'Datetime')); + $output->push(new ModelViewer_Field($this,'LastEdited', 'Datetime')); + } + + $db = singleton($this->className)->uninherited('db',true); + if($db) foreach($db as $k => $v) { + $output->push(new ModelViewer_Field($this, $k, $v)); + } + return $output; + } + + function Relations() { + $output = new DataObjectSet(); + + foreach(array('has_one','has_many','many_many') as $relType) { + $items = singleton($this->className)->uninherited($relType,true); + if($items) foreach($items as $k => $v) { + $output->push(new ModelViewer_Relation($this, $k, $v, $relType)); + } + } + return $output; + } +} + +class ModelViewer_Field extends ViewableData { + public $Model, $Name, $Type; + + function __construct($model, $name, $type) { + $this->Model = $model; + $this->Name = $name; + $this->Type = $type; + } +} + +class ModelViewer_Relation extends ViewableData { + public $Model, $Name, $RelationType, $RelatedClass; + + function __construct($model, $name, $relatedClass, $relationType) { + $this->Model = $model; + $this->Name = $name; + $this->RelatedClass = $relatedClass; + $this->RelationType = $relationType; + } + +} + + +?> diff --git a/dev/TestRunner.php b/dev/TestRunner.php index fa9742d24..c85dda21e 100644 --- a/dev/TestRunner.php +++ b/dev/TestRunner.php @@ -91,6 +91,7 @@ class TestRunner extends Controller { } else { echo '
'; $tests = ClassInfo::subclassesFor('SapphireTest'); + asort($tests); echo "

Link() . "all\">Run all " . count($tests) . " tests

"; echo "

Link() . "coverage\">Runs all tests and make test coverage report

"; echo "
"; diff --git a/dev/TestSession.php b/dev/TestSession.php index 2ecd33e9f..158554b06 100644 --- a/dev/TestSession.php +++ b/dev/TestSession.php @@ -9,6 +9,13 @@ class TestSession { private $session; private $lastResponse; + /** + * @param Controller $controller Necessary to use the mock session + * created in {@link session} in the normal controller stack, + * e.g. to overwrite Member::currentUser() with custom login data. + */ + protected $controller; + /** * @var string $lastUrl Fake HTTP Referer Tracking, set in {@link get()} and {@link post()}. */ @@ -16,6 +23,13 @@ class TestSession { function __construct() { $this->session = new Session(array()); + $this->controller = new Controller(); + $this->controller->setSession($this->session); + $this->controller->pushCurrent(); + } + + function __destruct() { + $this->controller->popCurrent(); } /** diff --git a/dev/install/config-form.html b/dev/install/config-form.html new file mode 100644 index 000000000..147ba92fa --- /dev/null +++ b/dev/install/config-form.html @@ -0,0 +1,173 @@ + + + + SilverStripe CMS Installation + + + + + + + + + +
+
+ + + +
+ +
+
+

Welcome to SilverStripe

+

Thanks for choosing to use SilverStripe! Please follow the instructions below to get SilverStripe installed.

+ +
+ +

+ You aren't currently able to install the software. Please see below for details.
+ If you are having problems meeting the requirements, see the server requirements wiki page. +

+ + hasWarnings()) { ?> +

+ There are some issues that we recommend you look at before installing, however, you are still able to install the software. + Please see below for details.
+ If you are having problems meeting the requirements, see the server requirements wiki page. +

+ hasErrors()) { ?> +

+ You're ready to install!    +

+ +

+ Template to install: +

+
    +
  • +
  • +
+

You can change the template or download another from the SilverStripe website after installation.

+ +
+ + + +

+ Note: It seems as though SilverStripe is already installed here. If you ask me to install, I will overwrite + the .htaccess and mysite/_config.php files. +
+ +

+ + +

+ + + + + + + +

+ + + +

MySQL Database

+ hasErrors()) { ?> +

+ These database details don't appear to be correct. Please enter the correct details before installing. +

+ +

+ These database details look all good! +

+ + +

+ + + + + + + + + +

+

SilverStripe stores its content in a MySQL database. Please provide the username and password to connect to the server here. If this account has permission to create databases, then we will create the database for you; otherwise, you must give the name of a database that already exists.

+
+
Details
+ showTable("MySQL Configuration"); ?> + +
+ +

SilverStripe Administration Account

+ +

+ + + + + + + + +

+

+ We will set up 1 administrator account for you automatically. Enter the email address and password. If you'd + rather log-in with a username instead of an email address, enter that instead. +

+ +
+ +

Development Servers

+

+ + +

+

+ SilverStripe allows you to run a site in development mode. + This shows all error messages in the web browser instead of emailing them to the administrator, and allows + the database to be built without logging in as administrator. Please enter the host/domain names for servers + you will be using for development. +

+ +
+ + +

Requirements

+ showTable(); + ?> + +
+
+
+ +
+
+ + +
+ + diff --git a/dev/install/install.css b/dev/install/install.css new file mode 100644 index 000000000..182f156ed --- /dev/null +++ b/dev/install/install.css @@ -0,0 +1,118 @@ +body { + text-align: center; +} + +#Container * { + text-align: left; +} +ul#Themes{ + list-style: none; + margin: 5px; +} + ul#Themes li { + clear: both; + padding: 3px 0; + } + ul#Themes input { + float: left; + width: 10px; + height: 10px; + } + ul#Themes label { + margin: -2px 5px 0 15px; + } +.good td { + color: green; +} + +.warning td { + color: #ef7f24; +} +.testResults .error td { + border: 1px #CCC solid; + color: red; +} + +p.error { + padding: 0.5em; + background-color: #ffe9e9; + border: 1px #ff8e8e solid; + color: #f03838; +} +p.warning { + padding: 0.5em; + background-color: #fef1e1; + border: 1px #ffc28b solid; + color: #cb6a1c; +} + p.warning label { + display: inline; + margin-left: 5px; + color: #cb6a1c + } +p.good { + padding: 0.5em; + background-color: #e2fee1; + border: 1px #43cb3e solid; + color: #359318; +} +p.error a, +p.warning a, +p.good a { + color: inherit; + text-decoration: underline; +} +p.error a:hover { + text-decoration: none; +} +span.middleColumn { + width: 312px; + margin-right: 0; + padding: 4px; +} +input.text, textarea, select { + padding: 2px; + border: 1px solid #A7A7A7; + color: #000; + font-size: 1.2em; + font-weight: bold; + width: 305px; +} +#stats { + float: left; + margin: 5px; +} +table.testResults { + border-collapse: collapse; + width: 100%; + margin: 10px 0; +} +#Layout h4 { + font-size: 2em; + clear: left; +} +.testResults td { + border: 1px #CCC solid; + width: 400px; + padding: 4px; +} +.clear { + clear: both; +} +p.mysql, +p.adminAcc, +p.devHelp { + padding-top: 20px; +} +p#mysql_credentials, +p#AdminAccount, +p#DevSites { + width: 330px; + margin-top: 0; + float: left; +} +#Layout input.action { + text-align: center; + width: 160px; + font-size: 1em; +} \ No newline at end of file diff --git a/dev/install/php5-required.html b/dev/install/php5-required.html new file mode 100644 index 000000000..7169b0899 --- /dev/null +++ b/dev/install/php5-required.html @@ -0,0 +1,40 @@ + + + PHP 5 is required + + + + + + +
+
+ + + +
+ +
+
+

PHP 5 required

+

To run SilverStripe, please install PHP 5.0 or greater.

+ +

We have detected that you are running PHP version $PHPVersion. In order to run SilverStripe, + you must have PHP version 5.0 or greater, and for best results we recommend PHP 5.2 or greater.

+ +

If you are running on a shared host, you may need to ask your hosting provider how to do this.

+
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/email/Email.php b/email/Email.php index 8033f8eff..b287cc11f 100755 --- a/email/Email.php +++ b/email/Email.php @@ -24,13 +24,53 @@ if(isset($_SERVER['SERVER_NAME'])) { * @subpackage email */ class Email extends ViewableData { - protected $from, $to, $subject, $body, $plaintext_body, $cc, $bcc; + /** + * @param string $from Email-Address + */ + protected $from; + + /** + * @param string $to Email-Address. Use comma-separation to pass multiple email-addresses. + */ + protected $to; + + /** + * @param string $subject Subject of the email + */ + protected $subject; + + /** + * @param string $body HTML content of the email. + * Passed straight into {@link $ss_template} as $Body variable. + */ + protected $body; + + /** + * @param string $plaintext_body Optional string for plaintext emails. + * If not set, defaults to converting the HTML-body with {@link Convert::xml2raw()}. + */ + protected $plaintext_body; + + /** + * @param string $cc + */ + protected $cc; + + /** + * @param string $bcc + */ + protected $bcc; + + /** + * @param Mailer $mailer Instance of a {@link Mailer} class. + */ protected static $mailer; /** - * Set the mailer. - * This can be used to provide a mailer other than the default, for testing, for example. + * This can be used to provide a mailer class other than the default, e.g. for testing. + * + * @param Mailer $mailer */ static function set_mailer(Mailer $mailer) { self::$mailer = $mailer; @@ -38,6 +78,8 @@ class Email extends ViewableData { /** * Get the mailer. + * + * @return Mailer */ static function mailer() { if(!self::$mailer) self::$mailer = new Mailer(); @@ -45,23 +87,55 @@ class Email extends ViewableData { } /** - * A map of header-name -> header-value + * @param array $customHeaders A map of header-name -> header-value */ protected $customHeaders; + /** + * @param array $attachements Internal, use {@link attachFileFromString()} or {@link attachFile()} + */ protected $attachments = array(); + + /** + * @param boolean $ + */ protected $parseVariables_done = false; + /** + * @param string $ss_template The name of the used template (without *.ss extension) + */ protected $ss_template = "GenericEmail"; + + /** + * @param array $template_data Additional data available in a template. + * Used in the same way than {@link ViewableData->customize()}. + */ protected $template_data = null; + + /** + * @param string $bounceHandlerURL + */ protected $bounceHandlerURL = null; /** - * The default administrator email address. This will be set in the config on a site-by-site basis - */ + * @param sring $admin_email_address The default administrator email address. + * This will be set in the config on a site-by-site basis + */ static $admin_email_address = ''; + + /** + * @param string $send_all_emails_to Email-Address + */ protected static $send_all_emails_to = null; + + /** + * @param string $bcc_all_emails_to Email-Address + */ protected static $bcc_all_emails_to = null; + + /** + * @param string $cc_all_emails_to Email-Address + */ protected static $cc_all_emails_to = null; /** @@ -101,35 +175,61 @@ class Email extends ViewableData { } } + /** + * @deprecated 2.3 Not used anywhere else + */ public function setFormat($format) { - $this->format = $format; + user_error('Email->setFormat() is deprecated', E_USER_NOTICE); } public function Subject() { return $this->subject; } + public function Body() { return $this->body; } + public function To() { return $this->to; } + public function From() { return $this->from; } + public function Cc() { return $this->cc; } + public function Bcc() { return $this->bcc; } - public function setSubject($val) { $this->subject = $val; } - public function setBody($val) { $this->body = $val; } - public function setTo($val) { $this->to = $val; } - public function setFrom($val) { $this->from = $val; } - public function setCc($val) {$this->cc = $val;} - public function setBcc($val) {$this->bcc = $val;} + public function setSubject($val) { + $this->subject = $val; + } + + public function setBody($val) { + $this->body = $val; + } + + public function setTo($val) { + $this->to = $val; + } + + public function setFrom($val) { + $this->from = $val; + } + + public function setCc($val) { + $this->cc = $val; + } + + public function setBcc($val) { + $this->bcc = $val; + } + /** * Add a custom header to this value. * Useful for implementing all those cool features that we didn't think of. @@ -180,7 +280,7 @@ class Email extends ViewableData { } /** - * Used by SSViewer templates to detect if we're rendering an email template rather than a page template + * Used by {@link SSViewer} templates to detect if we're rendering an email template rather than a page template */ public function IsEmail() { return true; @@ -340,12 +440,10 @@ class Email extends ViewableData { $headers['Bcc'] = self::$bcc_all_emails_to; } } - - return self::mailer()->sendHTML($to, $this->from, $subject, $this->body, $this->attachments, $headers, $this->plaintext_body); - + Requirements::restore(); - return $result; + return self::mailer()->sendHTML($to, $this->from, $subject, $this->body, $this->attachments, $headers, $this->plaintext_body); } /** @@ -465,6 +563,23 @@ class Email extends ViewableData { } } +/** + * Implements an email template that can be populated. + * + * @deprecated - Please use Email instead!. + * @todo Remove this in 2.4 + * @package sapphire + * @subpackage email + */ +class Email_Template extends Email { + + public function __construct($from = null, $to = null, $subject = null, $body = null, $bounceHandlerURL = null, $cc = null, $bcc = null) { + parent::__construct($from, $to, $subject, $body, $bounceHandlerURL, $cc, $bcc); + user_error('Email_Template is deprecated. Please use Email instead.', E_USER_NOTICE); + } + +} + /** * Base class that email bounce handlers extend * @package sapphire diff --git a/filesystem/File.php b/filesystem/File.php index 5c7bcb9e5..ea34cb618 100755 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -49,6 +49,28 @@ class File extends DataObject { */ protected static $cache_file_fields = null; + /** + * Find a File object by the given filename. + * @return mixed null if not found, File object of found file + */ + static function find($filename) { + // Get the base file if $filename points to a resampled file + $filename = ereg_replace('_resampled/[^-]+-','',$filename); + + $parts = explode("/", $filename); + $parentID = 0; + $item = null; + + foreach($parts as $part) { + if($part == "assets" && !$parentID) continue; + $item = DataObject::get_one("File", "\"Name\" = '$part' AND \"ParentID\" = $parentID"); + if(!$item) break; + $parentID = $item->ID; + } + + return $item; + } + function Link($action = null) { return Director::baseURL() . $this->RelativeLink($action); } @@ -138,25 +160,6 @@ class File extends DataObject { return $this->canEdit($member); } - /* - * Find the given file - */ - static function find($filename) { - // Get the base file if $filename points to a resampled file - $filename = ereg_replace('_resampled/[^-]+-','',$filename); - - $parts = explode("/",$filename); - $parentID = 0; - - foreach($parts as $part) { - if($part == "assets" && !$parentID) continue; - $item = DataObject::get_one("File", "\"Name\" = '$part' AND \"ParentID\" = $parentID"); - if(!$item) break; - $parentID = $item->ID; - } - return $item; - } - public function appCategory() { $ext = $this->Extension; switch($ext) { @@ -523,24 +526,18 @@ class File extends DataObject { } /** - * returns the size in bytes with no extensions for calculations. + * Return file size in bytes. + * @return int */ function getAbsoluteSize(){ - if(file_exists($this->getFullPath() )) { + if(file_exists($this->getFullPath())) { $size = filesize($this->getFullPath()); return $size; - }else{ + } else { return 0; } } - - /** - * Select clause for DataObject::get('File') operations/ - * Stores an array, suitable for a {@link SQLQuery} object. - */ - private static $dataobject_select; - /** * We've overridden the DataObject::get function for File so that the very large content field * is excluded! diff --git a/forms/CheckboxField.php b/forms/CheckboxField.php index af35f25a6..a31d56603 100755 --- a/forms/CheckboxField.php +++ b/forms/CheckboxField.php @@ -82,8 +82,9 @@ HTML; } function performDisabledTransformation() { - $this->disabled = true; - return $this; + $clone = clone $this; + $clone->setDisabled(true); + return $clone; } } @@ -94,7 +95,7 @@ HTML; */ class CheckboxField_Readonly extends ReadonlyField { function performReadonlyTransformation() { - return $this; + return clone $this; } function setValue($val) { diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php index 728c9c1f1..cd5f9c944 100755 --- a/forms/CheckboxSetField.php +++ b/forms/CheckboxSetField.php @@ -9,7 +9,6 @@ */ class CheckboxSetField extends OptionsetField { - protected $disabled = false; /** @@ -20,6 +19,7 @@ class CheckboxSetField extends OptionsetField { function Field() { Requirements::css(SAPPHIRE_DIR . '/css/CheckboxSetField.css'); + $source = $this->source; $values = $this->value; // Get values from the join, if available @@ -28,50 +28,50 @@ class CheckboxSetField extends OptionsetField { if(!$values && $record && $record->hasMethod($this->name)) { $funcName = $this->name; $join = $record->$funcName(); - if($join) foreach($join as $joinItem) $values[] = $joinItem->ID; + if($join) { + foreach($join as $joinItem) { + $values[] = $joinItem->ID; + } + } } } - $source = $this->source; - if(!is_array($source) && !is_a($source, 'SQLMap')){ - // Source and values are DataObject sets. + + // Source is not an array + if(!is_array($source) && !is_a($source, 'SQLMap')) { if(is_array($values)) { $items = $values; } else { - if($values&&is_a($values, "DataObjectSet")){ - foreach($values as $object){ - if( is_a( $object, 'DataObject' ) ) + // Source and values are DataObject sets. + if($values && is_a($values, 'DataObjectSet')) { + foreach($values as $object) { + if(is_a($object, 'DataObject')) { $items[] = $object->ID; + } } - }elseif($values&&is_string($values)){ + } elseif($values && is_string($values)) { $items = explode(',', $values); $items = str_replace('{comma}', ',', $items); } } - } else { - - // Sometimes we pass a singluar default value - // thats ! an array && !DataObjectSet - if(is_a($values,'DataObjectSet') || is_array($values)) + // Sometimes we pass a singluar default value thats ! an array && !DataObjectSet + if(is_a($values, 'DataObjectSet') || is_array($values)) { $items = $values; - else{ - $items = explode(',',$values); + } else { + $items = explode(',', $values); $items = str_replace('{comma}', ',', $items); } } - - if(is_array($source)){ - // Commented out to fix "'Specific newsletters' option in 'newsletter subscription form' page type does not work" bug - // See: http://www.silverstripe.com/bugs/flat/1675 - // unset($source[0]); + if(is_array($source)) { unset($source['']); } $odd = 0; $options = ''; + foreach($source as $index => $item) { - if(is_a($item,'DataObject')) { + if(is_a($item, 'DataObject')) { $key = $item->ID; $value = $item->Title; } else { @@ -80,23 +80,20 @@ class CheckboxSetField extends OptionsetField { } $odd = ($odd + 1) % 2; - $extraClass = $odd ? "odd" : "even"; - $extraClass .= " val" . str_replace(' ','',$key); - - $itemID = $this->id() . "_" . ereg_replace('[^a-zA-Z0-9]+','',$key); + $extraClass = $odd ? 'odd' : 'even'; + $extraClass .= ' val' . str_replace(' ', '', $key); + $itemID = $this->id() . '_' . ereg_replace('[^a-zA-Z0-9]+', '', $key); + $checked = ''; - $checked =""; - if(isset($items)){ + if(isset($items)) { in_array($key,$items) ? $checked = " checked=\"checked\"" : $checked = ""; } $this->disabled ? $disabled = " disabled=\"disabled\"" : $disabled = ""; - $options .= "
  • name[$key]\" type=\"checkbox\" value=\"$key\"$checked $disabled class=\"checkbox\" />
  • \n"; } - - return "\n"; + return "\n"; } function setDisabled($val) { @@ -159,8 +156,9 @@ class CheckboxSetField extends OptionsetField { } function performDisabledTransformation() { - $this->setDisabled(true); - return $this; + $clone = clone $this; + $clone->setDisabled(true); + return $clone; } /** diff --git a/forms/CompositeField.php b/forms/CompositeField.php index c96ce7712..ec49ed148 100755 --- a/forms/CompositeField.php +++ b/forms/CompositeField.php @@ -64,6 +64,13 @@ class CompositeField extends FormField { public function getChildren() { return $this->children; } + + /** + * @param FieldSet $children + */ + public function setChildren($children) { + $this->children = $children; + } /** * Returns the fields nested inside another DIV @@ -207,14 +214,15 @@ class CompositeField extends FormField { */ public function performReadonlyTransformation() { $newChildren = new FieldSet(); - foreach($this->children as $idx => $child) { + $clone = clone $this; + foreach($clone->getChildren() as $idx => $child) { if(is_object($child)) $child = $child->transform(new ReadonlyTransformation()); $newChildren->push($child, $idx); } - $this->children = $newChildren; - $this->readonly = true; - return $this; + $clone->children = $newChildren; + $clone->readonly = true; + return $clone; } /** @@ -223,17 +231,18 @@ class CompositeField extends FormField { */ public function performDisabledTransformation($trans) { $newChildren = new FieldSet(); - if($this->children) foreach($this->children as $idx => $child) { + $clone = clone $this; + if($clone->getChildren()) foreach($clone->getChildren() as $idx => $child) { if(is_object($child)) { $child = $child->transform($trans); } $newChildren->push($child, $idx); } - $this->children = $newChildren; - $this->readonly = true; + $clone->children = $newChildren; + $clone->readonly = true; - return $this; + return $clone; } function IsReadonly() { @@ -259,6 +268,34 @@ class CompositeField extends FormField { return false; } + + /** + * Transform the named field into a readonly feld. + * + * @param string|FormField + */ + function makeFieldReadonly($field) { + $fieldName = ($field instanceof FormField) ? $field->Name() : $field; + + // Iterate on items, looking for the applicable field + foreach($this->children as $i => $item) { + if($item->isComposite()) { + $item->makeFieldReadonly($fieldName); + } else { + // Once it's found, use FormField::transform to turn the field into a readonly version of itself. + if($item->Name() == $fieldName) { + $this->children->replaceField($fieldName, $item->transform(new ReadonlyTransformation())); + + // Clear an internal cache + $this->sequentialSet = null; + + // A true results indicates that the field was foudn + return true; + } + } + } + return false; + } function debug() { $result = "$this->class ($this->name)
    +
    $title 
    HTML; } diff --git a/javascript/TreeSelectorField.js b/javascript/TreeSelectorField.js index 3dd28aa50..0c92f9467 100755 --- a/javascript/TreeSelectorField.js +++ b/javascript/TreeSelectorField.js @@ -35,6 +35,15 @@ TreeDropdownField.prototype = { } }, + refresh: function() { + this.createTreeNode(); + + this.ajaxGetTree( (function(response) { + this.newTreeReady(response, false); + this.updateTreeLabel(); + }).bind(this)); + }, + helperURLBase: function() { return this.ownerForm().action + '/field/' + this.inputTag.name + '/'; }, @@ -290,4 +299,4 @@ TreeMultiselectField.prototype = { } TreeMultiselectField.applyTo('div.TreeDropdownField.multiple'); -TreeDropdownField.applyTo('div.TreeDropdownField.single'); +TreeDropdownField.applyTo('div.TreeDropdownField.single'); \ No newline at end of file diff --git a/lang/en_US.php b/lang/en_US.php index 56dc13e33..6ebf98a24 100644 --- a/lang/en_US.php +++ b/lang/en_US.php @@ -738,7 +738,7 @@ $lang['en_US']['SiteTree']['TABBACKLINKS'] = 'BackLinks'; $lang['en_US']['SiteTree']['TABBEHAVIOUR'] = 'Behaviour'; $lang['en_US']['SiteTree']['TABCONTENT'] = 'Content'; $lang['en_US']['SiteTree']['TABMAIN'] = 'Main'; -$lang['en_US']['SiteTree']['TABMETA'] = 'Meta-data'; +$lang['en_US']['SiteTree']['TABMETA'] = 'Metadata'; $lang['en_US']['SiteTree']['TABREPORTS'] = 'Reports'; $lang['en_US']['SiteTree']['TODOHELP'] = '

    You can use this to keep track of work that needs to be done to the content of your site. To see all your pages with to do information, open the \'Site Reports\' window on the left and select \'To Do\'

    '; $lang['en_US']['SiteTree']['TOPLEVEL'] = 'Site Content (Top Level)'; diff --git a/main.php b/main.php index 2234cc22a..88545c462 100644 --- a/main.php +++ b/main.php @@ -1,5 +1,21 @@ RelativeLink(); - - $fields->push(new HiddenField('formController', null, $formController)); - $fields->push(new HiddenField('executeForm', null, $name)); - parent::__construct($controller, $name, $fields, $actions); + $this->setFormMethod('get'); + $this->disableSecurityToken(); } - function FormMethod() { - return "get"; - } - - public function forTemplate(){ + public function forTemplate() { return $this->renderWith(array( 'SearchForm', 'Form' @@ -74,11 +68,11 @@ class SearchForm extends Form { * Return dataObjectSet of the results using $_REQUEST to get info from form. * Wraps around {@link searchEngine()}. * - * @param int $numPerPage DEPRECATED 2.3 Use SearchForm->numPerPage + * @param int $pageLength DEPRECATED 2.3 Use SearchForm->pageLength * @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 = null, $data = null){ + public function getResults($pageLength = null, $data = null){ // legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials if(!isset($data)) $data = $_REQUEST; @@ -99,9 +93,9 @@ class SearchForm extends Form { $keywords = $this->addStarsToKeywords($keywords); if(strpos($keywords, '"') !== false || strpos($keywords, '+') !== false || strpos($keywords, '-') !== false || strpos($keywords, '*') !== false) { - $results = $this->searchEngine($keywords, $numPerPage, "Relevance DESC", "", true); + $results = $this->searchEngine($keywords, $pageLength, "Relevance DESC", "", true); } else { - $results = $this->searchEngine($keywords, $numPerPage); + $results = $this->searchEngine($keywords, $pageLength); } // filter by permission @@ -139,8 +133,8 @@ class SearchForm extends Form { * * @param string $keywords Keywords as a string. */ - public function searchEngine($keywords, $numPerPage = null, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) { - if(!$numPerPage) $numPerPage = $this->numPerPage; + public function searchEngine($keywords, $pageLength = null, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) { + if(!$pageLength) $pageLength = $this->pageLength; $fileFilter = ''; $keywords = addslashes($keywords); @@ -155,7 +149,7 @@ class SearchForm extends Form { if($this->showInSearchTurnOn) $extraFilter .= " AND showInSearch <> 0"; $start = isset($_GET['start']) ? (int)$_GET['start'] : 0; - $limit = $start . ", " . (int) $numPerPage; + $limit = $start . ", " . (int) $pageLength; $notMatch = $invertedMatch ? "NOT " : ""; if($keywords) { @@ -201,7 +195,7 @@ class SearchForm extends Form { if(isset($objects)) $doSet = new DataObjectSet($objects); else $doSet = new DataObjectSet(); - $doSet->setPageLimits($start, $numPerPage, $totalCount); + $doSet->setPageLimits($start, $pageLength, $totalCount); return $doSet; } @@ -217,6 +211,23 @@ class SearchForm extends Form { return Convert::raw2xml($data['Search']); } + + /** + * Set the maximum number of records shown on each page. + * + * @param int $length + */ + public function setPageLength($length) { + $this->pageLength = $length; + } + + /** + * @return int + */ + public function getPageLength() { + // legacy handling for deprecated $numPerPage + return (isset($this->numPerPage)) ? $this->numPerPage : $this->pageLength; + } } diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index 2e92dabe3..7853383c1 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -82,6 +82,9 @@ abstract class SearchFilter extends Object { * @return string */ function getDbName() { + // Special handler for "NULL" relations + if($this->name == "NULL") return $this->name; + // SRM: This code finds the table where the field named $this->name lives // Todo: move to somewhere more appropriate, such as DataMapper, the magical class-to-be? $candidateClass = $this->model; @@ -140,6 +143,24 @@ abstract class SearchFilter extends Object { $query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); $query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); $this->model = $componentClass; + + // Experimental support for user-defined relationships via a "(relName)Query" method + // This will likely be dropped in 2.4 for a system that makes use of Lazy Data Lists. + } elseif($model->hasMethod($rel.'Query')) { + // Get the query representing the join - it should have "$ID" in the filter + $newQuery = $model->{"{$rel}Query"}(); + if($newQuery) { + // Get the table to join to + $newModel = str_replace('`','',array_shift($newQuery->from)); + // Get the filter to use on the join + $ancestry = $model->getClassAncestry(); + $newFilter = "(" . str_replace('$ID', "`{$ancestry[0]}`.`ID`" , implode(") AND (", $newQuery->where) ) . ")"; + $query->leftJoin($newModel, $newFilter); + $this->model = $newModel; + } else { + $this->name = "NULL"; + return; + } } } } diff --git a/security/Member.php b/security/Member.php index 7ba49a230..4b943e0a4 100644 --- a/security/Member.php +++ b/security/Member.php @@ -506,6 +506,11 @@ class Member extends DataObject { } } } + + // save locale + if(!$this->Locale) { + $this->Locale = i18n::get_locale(); + } parent::onBeforeWrite(); } @@ -860,8 +865,6 @@ class Member extends DataObject { // Groups relation will get us into logical conflicts because // Members are displayed within group edit form in SecurityAdmin $fields->removeByName('Groups'); - - $this->extend('updateCMSFields', $fields); return $fields; } @@ -1226,6 +1229,13 @@ class Member_ProfileForm extends Form { $form->saveInto($member); $member->write(); + $closeLink = sprintf( + '(%s)', + _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') + ); + $message = _t('Member.PROFILESAVESUCCESS', 'Successfully saved.') . ' ' . $closeLink; + $form->sessionMessage($message, 'good'); + Director::redirectBack(); } } diff --git a/security/Permission.php b/security/Permission.php index 97cb3bd5d..7bcfc7b8a 100755 --- a/security/Permission.php +++ b/security/Permission.php @@ -124,76 +124,78 @@ class Permission extends DataObject { $perms_list = self::get_declared_permissions_list(); $memberID = (is_object($member)) ? $member->ID : $member; - if(self::$declared_permissions && is_array($perms_list) && - !in_array($code, $perms_list)) { - //user_error("Permission '$code' has not been declared. Use " . - // "Permission::declare_permissions() to add this permission", - // E_USER_WARNING); + /* + if(self::$declared_permissions && is_array($perms_list) && !in_array($code, $perms_list)) { + user_error( + "Permission '$code' has not been declared. Use " . + "Permission::declare_permissions() to add this permission", + E_USER_WARNING + ); } - + */ + $groupList = self::groupList($memberID); - if($groupList) { - $groupCSV = implode(", ", $groupList); + if(!$groupList) return false; + + $groupCSV = implode(", ", $groupList); - // Arg component - switch($arg) { - case "any": - $argClause = ""; - break; - case "all": - $argClause = " AND \"Arg\" = -1"; - break; - default: - if(is_numeric($arg)) { - $argClause = "AND \"Arg\" IN (-1, $arg) "; - } else { - user_error("Permission::checkMember: bad arg '$arg'", - E_USER_ERROR); - } - } - - if(is_array($code)) $SQL_codeList = "'" . implode("', '", Convert::raw2sql($code)) . "'"; - else $SQL_codeList = "'" . Convert::raw2sql($code) . "'"; - - $SQL_code = Convert::raw2sql($code); - - $adminFilter = (self::$admin_implies_all) - ? ",'ADMIN'" - : ''; - - // Raw SQL for efficiency - $permission = DB::query(" - SELECT \"ID\" - FROM \"Permission\" - WHERE ( - \"Code\" IN ($SQL_codeList $adminFilter) - AND \"Type\" = " . self::GRANT_PERMISSION . " - AND \"GroupID\" IN ($groupCSV) - $argClause - ) - ")->value(); - - if($permission) - return $permission; - - - // Strict checking disabled? - if(!self::$strict_checking || !$strict) { - $hasPermission = DB::query(" - SELECT COUNT(*) - FROM \"Permission\" - WHERE ( - (\"Code\" IN '$SQL_code')' - AND (\"Type\" = " . self::GRANT_PERMISSION . ") - ) - ")->value(); - if(!$hasPermission) { - return true; + // Arg component + switch($arg) { + case "any": + $argClause = ""; + break; + case "all": + $argClause = " AND \"Arg\" = -1"; + break; + default: + if(is_numeric($arg)) { + $argClause = "AND \"Arg\" IN (-1, $arg) "; + } else { + user_error("Permission::checkMember: bad arg '$arg'", E_USER_ERROR); } - } - - return false; } + + if(is_array($code)) { + $SQL_codeList = "'" . implode("', '", Convert::raw2sql($code)) . "'"; + } else { + $SQL_codeList = "'" . Convert::raw2sql($code) . "'"; + } + + $SQL_code = Convert::raw2sql($code); + + $adminFilter = (self::$admin_implies_all) ? ",'ADMIN'" : ''; + + // Raw SQL for efficiency + $permission = DB::query(" + SELECT \"ID\" + FROM \"Permission\" + WHERE ( + \"Code\" IN ($SQL_codeList $adminFilter) + AND \"Type\" = " . self::GRANT_PERMISSION . " + AND \"GroupID\" IN ($groupCSV) + $argClause + ) + ")->value(); + + if($permission) + return $permission; + + // Strict checking disabled? + if(!self::$strict_checking || !$strict) { + $hasPermission = DB::query(" + SELECT COUNT(*) + FROM \"Permission\" + WHERE ( + (\"Code\" IN '$SQL_code')' + AND (\"Type\" = " . self::GRANT_PERMISSION . ") + ) + ")->value(); + if(!$hasPermission) { + return true; + } + } + + return false; } diff --git a/security/Security.php b/security/Security.php index fe92c73d6..34d530eab 100644 --- a/security/Security.php +++ b/security/Security.php @@ -445,7 +445,7 @@ class Security extends Controller { $controller = new Page_Controller($tmpPage); $controller->init(); - $email = Convert::raw2xml($request->param('ID')); + $email = Convert::raw2xml($request->param('ID') . '.' . $request->getExtension()); $customisedController = $controller->customise(array( 'Title' => sprintf(_t('Security.PASSWORDSENTHEADER', "Password reset link sent to '%s'"), $email), diff --git a/templates/Includes/Form.ss b/templates/Includes/Form.ss index a42ab4456..d41db386f 100644 --- a/templates/Includes/Form.ss +++ b/templates/Includes/Form.ss @@ -6,18 +6,22 @@ <% else %> <% end_if %> +
    + $Legend <% control Fields %> $FieldHolder <% end_control %>
    -<% if Actions %> + <% if Actions %>
    - <% control Actions %>$Field<% end_control %> + <% control Actions %> + $Field + <% end_control %>
    -<% end_if %> + <% end_if %> <% if IncludeFormTag %> <% end_if %> diff --git a/templates/ModelViewer.ss b/templates/ModelViewer.ss new file mode 100644 index 000000000..a155caadb --- /dev/null +++ b/templates/ModelViewer.ss @@ -0,0 +1,34 @@ + + + <% base_tag %> + Data Model + + + +

    Data Model for your project

    + + <% control Modules %> +

    Module $Name

    + + + + <% control Models %> +

    $Name <% if ParentModel %> (subclass of $ParentModel)<% end_if %>

    +

    Fields

    + + +

    Relations

    + + <% end_control %> + <% end_control %> + + + diff --git a/templates/ModelViewer_dotsrc.ss b/templates/ModelViewer_dotsrc.ss new file mode 100644 index 000000000..a7070a6f2 --- /dev/null +++ b/templates/ModelViewer_dotsrc.ss @@ -0,0 +1,20 @@ +digraph g { + orientation=portrait; + overlap=false; + splines=true; + + edge[fontsize=8,len=1.5]; + node[fontsize=10,shape=box]; + + <% control Modules %> + <% control Models %> + $Name [shape=record,label="{$Name|<% control Fields %>$Name\\n<% end_control %>}"]; + <% if ParentModel %> + $Name -> $ParentModel [style=dotted]; + <% end_if %> + <% control Relations %> + $Model.Name -> $RelatedClass [label="$Name\\n$RelationType"]; + <% end_control %> + <% end_control %> + <% end_control %> +} diff --git a/templates/SearchForm.ss b/templates/SearchForm.ss index ffc8ecead..9cf27f63e 100755 --- a/templates/SearchForm.ss +++ b/templates/SearchForm.ss @@ -1,10 +1,11 @@
    - <% control Fields %> - $FieldHolder - <% end_control %> - <% control Actions %> - $Field - <% end_control %> + + <% control Fields %> + $FieldHolder + <% end_control %> + <% control Actions %> + $Field + <% end_control %>
    diff --git a/tests/ErrorPageTest.php b/tests/ErrorPageTest.php new file mode 100644 index 000000000..a59190104 --- /dev/null +++ b/tests/ErrorPageTest.php @@ -0,0 +1,31 @@ +assertTrue($errorPage instanceof ErrorPage); + + /* Test the URL of the error page out to get a response */ + $response = Director::test(Director::makeRelative($errorPage->Link())); + + /* We have an HTTPResponse object for the error page */ + $this->assertTrue($response instanceof HTTPResponse); + + /* We have body text from the error page */ + $this->assertTrue($response->getBody() != null); + + /* Status code of the HTTPResponse for error page is "404" */ + $this->assertTrue($response->getStatusCode() == '404'); + + /* Status message of the HTTPResponse for error page is "Not Found" */ + $this->assertTrue($response->getStatusDescription() == 'Not Found'); + } + +} + +?> \ No newline at end of file diff --git a/tests/ErrorPageTest.yml b/tests/ErrorPageTest.yml new file mode 100644 index 000000000..e85ea39d3 --- /dev/null +++ b/tests/ErrorPageTest.yml @@ -0,0 +1,5 @@ +ErrorPage: + 404: + Title: Page Not Found + URLSegment: page-not-found + ErrorCode: 404 \ No newline at end of file diff --git a/tests/ManifestBuilderTest.php b/tests/ManifestBuilderTest.php index a0f168486..0660f1185 100644 --- a/tests/ManifestBuilderTest.php +++ b/tests/ManifestBuilderTest.php @@ -74,6 +74,8 @@ class ManifestBuilderTest extends SapphireTest { protected static $test_fixture_project; function setUp() { + parent::setUp(); + // Trick the auto-loder into loading this class before we muck with the manifest new TokenisedRegularExpression(null); @@ -131,6 +133,8 @@ class ManifestBuilderTest extends SapphireTest { // Kill the folder after we're done $baseFolder = TEMP_FOLDER . '/manifest-test/'; Filesystem::removeFolder($baseFolder); + + parent::tearDown(); } } diff --git a/tests/SiteTreePermissionsTest.php b/tests/SiteTreePermissionsTest.php index 9414eb03a..fb49246cd 100644 --- a/tests/SiteTreePermissionsTest.php +++ b/tests/SiteTreePermissionsTest.php @@ -18,6 +18,36 @@ class SiteTreePermissionsTest extends FunctionalTest { $this->autoFollowRedirection = false; } + function testAccessTabOnlyDisplaysWithGrantAccessPermissions() { + $page = $this->objFromFixture('Page', 'standardpage'); + + $subadminuser = $this->objFromFixture('Member', 'subadmin'); + $this->session()->inst_set('loggedInAs', $subadminuser->ID); + $fields = $page->getCMSFields(); + $this->assertFalse( + $fields->dataFieldByName('CanViewType')->isReadonly(), + 'Users with SITETREE_GRANT_ACCESS permission can change "view" permissions in cms fields' + ); + $this->assertFalse( + $fields->dataFieldByName('CanEditType')->isReadonly(), + 'Users with SITETREE_GRANT_ACCESS permission can change "edit" permissions in cms fields' + ); + + $editoruser = $this->objFromFixture('Member', 'editor'); + $this->session()->inst_set('loggedInAs', $editoruser->ID); + $fields = $page->getCMSFields(); + $this->assertTrue( + $fields->dataFieldByName('CanViewType')->isReadonly(), + 'Users without SITETREE_GRANT_ACCESS permission cannot change "view" permissions in cms fields' + ); + $this->assertTrue( + $fields->dataFieldByName('CanEditType')->isReadonly(), + 'Users without SITETREE_GRANT_ACCESS permission cannot change "edit" permissions in cms fields' + ); + + $this->session()->inst_set('loggedInAs', null); + } + function testRestrictedViewLoggedInUsers() { $page = $this->objFromFixture('Page', 'restrictedViewLoggedInUsers'); @@ -30,7 +60,7 @@ class SiteTreePermissionsTest extends FunctionalTest { $response = $this->get($page->URLSegment); $this->assertEquals( $response->getStatusCode(), - 403, + 302, 'Unauthenticated members cant view a page marked as "Viewable for any logged in users"' ); @@ -62,7 +92,7 @@ class SiteTreePermissionsTest extends FunctionalTest { $response = $this->get($page->URLSegment); $this->assertEquals( $response->getStatusCode(), - 403, + 302, 'Unauthenticated members cant view a page marked as "Viewable by these groups"' ); @@ -76,7 +106,7 @@ class SiteTreePermissionsTest extends FunctionalTest { $response = $this->get($page->URLSegment); $this->assertEquals( $response->getStatusCode(), - 403, + 302, 'Authenticated members cant view a page marked as "Viewable by these groups" if theyre not in the listed groups' ); $this->session()->inst_set('loggedInAs', null); @@ -159,7 +189,7 @@ class SiteTreePermissionsTest extends FunctionalTest { $response = $this->get($childPage->URLSegment); $this->assertEquals( $response->getStatusCode(), - 403, + 302, 'Unauthenticated members cant view a page marked as "Viewable by these groups" by inherited permission' ); diff --git a/tests/SiteTreePermissionsTest.yml b/tests/SiteTreePermissionsTest.yml index a9652b8c6..c0c6fdff0 100644 --- a/tests/SiteTreePermissionsTest.yml +++ b/tests/SiteTreePermissionsTest.yml @@ -3,11 +3,13 @@ Permission: Code: CMS_ACCESS_CMSMain cmsmain2: Code: CMS_ACCESS_CMSMain + grantaccess: + Code: SITETREE_GRANT_ACCESS Group: subadmingroup: Title: Create, edit and delete pages Code: subadmingroup - Permissions: =>Permission.cmsmain1 + Permissions: =>Permission.cmsmain1,=>Permission.grantaccess editorgroup: Title: Edit existing pages Code: editorgroup @@ -28,6 +30,8 @@ Member: Password: test Groups: =>Group.websiteusers Page: + standardpage: + URLSegment: standardpage restrictedViewLoggedInUsers: CanViewType: LoggedInUsers URLSegment: restrictedViewLoggedInUsers diff --git a/tests/forms/CheckboxSetFieldTest.php b/tests/forms/CheckboxSetFieldTest.php index a3b237ebc..0dd700ced 100644 --- a/tests/forms/CheckboxSetFieldTest.php +++ b/tests/forms/CheckboxSetFieldTest.php @@ -4,6 +4,14 @@ class CheckboxSetFieldTest extends SapphireTest { static $fixture_file = 'sapphire/tests/forms/CheckboxSetFieldTest.yml'; + function testAddExtraClass() { + /* CheckboxSetField has an extra class name and is in the HTML the field returns */ + $cboxSetField = new CheckboxSetField('FeelingOk', 'Are you feeling ok?', array(0 => 'No', 1 => 'Yes'), '', null, '(Select one)'); + $cboxSetField->addExtraClass('thisIsMyExtraClassForCheckboxSetField'); + preg_match('/thisIsMyExtraClassForCheckboxSetField/', $cboxSetField->Field(), $matches); + $this->assertTrue($matches[0] == 'thisIsMyExtraClassForCheckboxSetField'); + } + function testSaveWithNothingSelected() { $article = $this->fixture->objFromFixture('CheckboxSetFieldTest_Article', 'articlewithouttags'); diff --git a/tests/forms/DropdownFieldTest.php b/tests/forms/DropdownFieldTest.php index 08bf6e8c3..b2e8baf10 100644 --- a/tests/forms/DropdownFieldTest.php +++ b/tests/forms/DropdownFieldTest.php @@ -5,6 +5,14 @@ */ class DropdownFieldTest extends SapphireTest { + function testAddExtraClass() { + /* DropdownField has an extra class name and is in the HTML the field returns */ + $dropdownField = new DropdownField('FeelingOk', 'Are you feeling ok?', array(0 => 'No', 1 => 'Yes'), '', null, '(Select one)'); + $dropdownField->addExtraClass('thisIsMyExtraClassForDropdownField'); + preg_match('/thisIsMyExtraClassForDropdownField/', $dropdownField->Field(), $matches); + $this->assertTrue($matches[0] == 'thisIsMyExtraClassForDropdownField'); + } + function testGetSource() { $source = array(1=>'one'); $field = new DropdownField('Field', null, $source); diff --git a/tests/forms/FieldSetTest.php b/tests/forms/FieldSetTest.php index 4d76559c2..a5b854d5c 100644 --- a/tests/forms/FieldSetTest.php +++ b/tests/forms/FieldSetTest.php @@ -670,5 +670,20 @@ class FieldSetTest extends SapphireTest { unset($set); } + + function testMakeFieldReadonly() { + $fieldSet = new FieldSet( + new TabSet('Root', new Tab('Main', + new TextField('A'), + new TextField('B') + ) + )); + + $fieldSet->makeFieldReadonly('A'); + $this->assertTrue( + $fieldSet->dataFieldByName('A')->isReadonly(), + 'Field nested inside a TabSet and FieldSet can be marked readonly by FieldSet->makeFieldReadonly()' + ); + } } ?> \ No newline at end of file diff --git a/tests/forms/FormFieldTest.php b/tests/forms/FormFieldTest.php new file mode 100644 index 000000000..02b58a558 --- /dev/null +++ b/tests/forms/FormFieldTest.php @@ -0,0 +1,89 @@ +addExtraClass('thisIsMyClassNameForTheFormField'); + preg_match('/thisIsMyClassNameForTheFormField/', $textField->Field(), $matches); + $this->assertTrue($matches[0] == 'thisIsMyClassNameForTheFormField'); + + /* EmailField has an extra class name and is in the HTML the field returns */ + $emailField = new EmailField('Email'); + $emailField->addExtraClass('thisIsMyExtraClassForEmailField'); + preg_match('/thisIsMyExtraClassForEmailField/', $emailField->Field(), $matches); + $this->assertTrue($matches[0] == 'thisIsMyExtraClassForEmailField'); + + /* OptionsetField has an extra class name and is in the HTML the field returns */ + $optionsetField = new OptionsetField('FeelingOk', 'Are you feeling ok?', array(0 => 'No', 1 => 'Yes'), '', null, '(Select one)'); + $optionsetField->addExtraClass('thisIsMyExtraClassForOptionsetField'); + preg_match('/thisIsMyExtraClassForOptionsetField/', $optionsetField->Field(), $matches); + $this->assertTrue($matches[0] == 'thisIsMyExtraClassForOptionsetField'); + } + + function testEveryFieldTransformsReadonlyAsClone() { + $fieldClasses = ClassInfo::subclassesFor('FormField'); + foreach($fieldClasses as $fieldClass) { + $reflectionClass = new ReflectionClass($fieldClass); + if(!$reflectionClass->isInstantiable()) continue; + $constructor = $reflectionClass->getMethod('__construct'); + if($constructor->getNumberOfRequiredParameters() > 1) continue; + if($fieldClass == 'CompositeField' || is_subclass_of($fieldClass, 'CompositeField')) continue; + + $instance = new $fieldClass("{$fieldClass}_instance"); + + $isReadonlyBefore = $instance->isReadonly(); + $readonlyInstance = $instance->performReadonlyTransformation(); + $this->assertEquals( + $isReadonlyBefore, + $instance->isReadonly(), + "FormField class '{$fieldClass} retains its readonly state after calling performReadonlyTransformation()" + ); + $this->assertTrue( + $readonlyInstance->isReadonly(), + "FormField class '{$fieldClass} returns a valid readonly representation as of isReadonly()" + ); + $this->assertNotSame( + $readonlyInstance, + $instance, + "FormField class '{$fieldClass} returns a valid cloned readonly representation" + ); + } + } + + function testEveryFieldTransformsDisabledAsClone() { + $fieldClasses = ClassInfo::subclassesFor('FormField'); + foreach($fieldClasses as $fieldClass) { + $reflectionClass = new ReflectionClass($fieldClass); + if(!$reflectionClass->isInstantiable()) continue; + $constructor = $reflectionClass->getMethod('__construct'); + if($constructor->getNumberOfRequiredParameters() > 1) continue; + if($fieldClass == 'CompositeField' || is_subclass_of($fieldClass, 'CompositeField')) continue; + + $instance = new $fieldClass("{$fieldClass}_instance"); + + $isDisabledBefore = $instance->isDisabled(); + $disabledInstance = $instance->performDisabledTransformation(); + $this->assertEquals( + $isDisabledBefore, + $instance->isDisabled(), + "FormField class '{$fieldClass} retains its disabled state after calling performDisabledTransformation()" + ); + $this->assertTrue( + $disabledInstance->isDisabled(), + "FormField class '{$fieldClass} returns a valid disabled representation as of isDisabled()" + ); + $this->assertNotSame( + $disabledInstance, + $instance, + "FormField class '{$fieldClass} returns a valid cloned disabled representation" + ); + } + } + +} +?> \ No newline at end of file diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index 52ee53aa9..2817a7a65 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -62,6 +62,8 @@ class i18nTest extends SapphireTest { unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); i18n::set_locale('en_US'); + + parent::tearDown(); } function testGetExistingTranslations() { diff --git a/tests/i18n/i18nTextCollectorTest.php b/tests/i18n/i18nTextCollectorTest.php index 8655c2ca0..e5f3fcb8b 100644 --- a/tests/i18n/i18nTextCollectorTest.php +++ b/tests/i18n/i18nTextCollectorTest.php @@ -60,6 +60,8 @@ class i18nTextCollectorTest extends SapphireTest { global $_TEMPLATE_MANIFEST; unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']); unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); + + parent::tearDown(); } function testCollectFromTemplateSimple() { diff --git a/tests/search/SearchFormTest.php b/tests/search/SearchFormTest.php index f1afec84f..65638e2ca 100644 --- a/tests/search/SearchFormTest.php +++ b/tests/search/SearchFormTest.php @@ -17,14 +17,6 @@ class SearchFormTest extends FunctionalTest { $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() {