MINOR merged branches/2.3 into trunk

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@67465 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-12-04 22:38:32 +00:00
parent 5dab7fff55
commit d26f08b481
76 changed files with 1516 additions and 349 deletions

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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, "<html") !== false) {
$content = preg_replace('/(<html[^>]*>)/i', "\\1<!-- template $template -->", $content);

View File

@ -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();

View File

@ -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

View File

@ -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();";
}
/**

View File

@ -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;
}
/**

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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'
)
);
}
}
?>

View File

@ -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;

View File

@ -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.
*/

View File

@ -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);
}

View File

@ -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;
}

View File

@ -72,6 +72,10 @@ class DevelopmentAdmin extends Controller {
return new ModuleManager();
}
function viewmodel() {
return new ModelViewer();
}
function build() {
$renderer = new DebugView();
$renderer->writeHeader();

View File

@ -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')) {

175
dev/ModelViewer.php Normal file
View File

@ -0,0 +1,175 @@
<?php
/**
* Gives you a nice way of viewing your data model.
* Access at dev/viewmodel
*/
class ModelViewer extends Controller {
static $url_handlers = array(
'$Module!' => '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;
}
}
?>

View File

@ -91,6 +91,7 @@ class TestRunner extends Controller {
} else {
echo '<div class="trace">';
$tests = ClassInfo::subclassesFor('SapphireTest');
asort($tests);
echo "<h3><a href=\"" . $this->Link() . "all\">Run all " . count($tests) . " tests</a></h3>";
echo "<h3><a href=\"" . $this->Link() . "coverage\">Runs all tests and make test coverage report</a></h3>";
echo "<hr />";

View File

@ -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();
}
/**

View File

@ -0,0 +1,173 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>SilverStripe CMS Installation</title>
<script type="text/js">
function show(id) {
document.getElementById(id).style.display = '';
}
function hide(id) {
document.getElementById(id).style.display = 'none';
}
</script>
<link rel="stylesheet" type="text/css" href="themes/blackcandy/css/layout.css" />
<link rel="stylesheet" type="text/css" href="themes/blackcandy/css/typography.css" />
<link rel="stylesheet" type="text/css" href="themes/blackcandy/css/form.css" />
<link rel="stylesheet" type="text/css" href="sapphire/dev/install/install.css" />
<link rel="shortcut icon" href="favicon.ico" />
</head>
<body>
<div id="BgContainer">
<div id="Container">
<div id="Header">
<h1>SilverStripe CMS Installation</h1>
<p>Version <?php echo $silverstripe_version; ?></p>
</div>
<div id="Navigation">&nbsp;</div>
<div class="clear"><!-- --></div>
<div id="Layout">
<div class="typography">
<h1>Welcome to SilverStripe</h1>
<p>Thanks for choosing to use SilverStripe! Please follow the instructions below to get SilverStripe installed.</p>
<form action="install.php" method="post">
<?php if(isset($hasErrorOtherThanDatabase)) { ?>
<p class="error">
You aren't currently able to install the software. Please <a href="#requirements">see below</a> for details.<br />
If you are having problems meeting the requirements, see the <a href="http://doc.silverstripe.com/doku.php?id=server-requirements">server requirements wiki page</a>.
</p>
<?php } else { ?>
<?php if($req->hasWarnings()) { ?>
<p class="warning">
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.<br />
If you are having problems meeting the requirements, see the <a href="http://doc.silverstripe.com/doku.php?id=server-requirements">server requirements wiki page</a>.
</p>
<?php } else if(!$dbReq->hasErrors()) { ?>
<p class="good">
You're ready to install! &nbsp;&nbsp;
</p>
<?php } ?>
<p>
<b>Template to install:</b>
</p>
<ul id="Themes">
<li><input type="radio" name="template" value="blackcandy" id="BlackCandy" checked="checked" /><label for="BlackCandy">BlackCandy, default template ready to use.</label></li>
<li><input type="radio" name="template" value="tutorial" id="EmptyTemplate" /><label for="EmptyTemplate">Empty template, ready to begin the tutorial.</label></li>
</ul>
<p>You can change the template or download another from the SilverStripe website after installation.</p>
<input type="checkbox" id="stats" name="stats" checked="checked"><label for="stats">Send information on my webserver to SilverStripe (this is only version information, used for statistical purposes)</label><br />
<?php if($alreadyInstalled) { ?>
<p class="warning">
<strong>Note:</strong> It seems as though SilverStripe is already installed here. If you ask me to install, I will overwrite
the <strong>.htaccess</strong> and <strong>mysite/_config.php</strong> files.
<br />
<input type="checkbox" id="ReIn" name="force_reinstall" onclick="document.getElementById('install_button').disabled = !this.checked" /><label for="ReIn">That's okay, please re-install SilverStripe and overwrite these files.</label>
</p>
<?php } ?>
<p>
<?php if($alreadyInstalled) { ?>
<input id="install_button" type="submit" disabled="disabled" class="action" name="go" value="Install SilverStripe" onclick="document.getElementById('saving_top').style.display = ''; this.value = 'Installing SilverStripe...'" />
<?php } else { ?>
<input id="install_button" type="submit" class="action" name="go" value="Install SilverStripe" onclick="document.getElementById('saving_top').style.display = ''; this.value = 'Installing SilverStripe...'" />
<?php } ?>
<span id="saving_top" style="display: none">
&nbsp;
<img src="cms/images/network-save.gif" />
(this will take a minute or so)
</span>
</p>
<?php } ?>
<input type="hidden" name="database" value="MySQLDatabase" />
<h4>MySQL Database</h4>
<?php if($dbReq->hasErrors()) { ?>
<p class="error"><!-- class="error" -->
These database details don't appear to be correct. Please enter the correct details before installing.
</p>
<?php } else { ?>
<p class="good">
These database details look all good!
</p>
<?php } ?>
<p id="mysql_credentials">
<label for="mysql_server">MySQL server:</label>
<span class="middleColumn"><input id="mysql_server" class="text" type="text" name="mysql[server]" value="<?php echo $databaseConfig['server']; ?>" /></span>
<label for="mysql_username">MySQL username:</label>
<span class="middleColumn"><input id="mysql_username" class="text" type="text" name="mysql[username]" value="<?php echo $databaseConfig['username']; ?>" /></span>
<label for="mysql_password">MySQL password:</label>
<span class="middleColumn"><input id="mysql_password" class="text" type="password" name="mysql[password]" value="<?php echo $databaseConfig['password']; ?>" /></span>
<label for="mysql_database">MySQL database:</label>
<span class="middleColumn"><input id="mysql_database" class="text" type="text" name="mysql[database]" value="<?php echo $databaseConfig['database']; ?>" onchange="this.value = this.value.replace(/[^A-Za-z0-9_]+/g,'');" /></span>
<input type="submit" class="action" value="Re-check requirements" />
</p>
<p class="mysql">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.</p>
<div class="clear"><!-- --></div>
<h5>Details</h5>
<?php $dbReq->showTable("MySQL Configuration"); ?>
<br />
<h4>SilverStripe Administration Account</h4>
<p id="AdminAccount">
<label for="admin_username">Administrator email:</label>
<span class="middleColumn"><input type="text" class="text" name="admin[username]" id="admin_username" value="<?php echo $adminConfig['username']; ?>" /></span>
<label for="admin_password">Administrator password:</label>
<span class="middleColumn"><input type="text" class="text" name="admin[password]" id="admin_password" value="<?php echo $adminConfig['password']; ?>" /></span>
<label for="admin_firstname">Administrator first name:</label>
<span class="middleColumn"><input type="text" class="text" name="admin[firstname]" id="admin_firstname" value="<?php echo $adminConfig['firstname']; ?>" /></span>
<label for="admin_surname">Administrator surname:</label>
<span class="middleColumn"><input type="text" class="text" name="admin[surname]" id="admin_surname" value="<?php echo $adminConfig['surname']; ?>" /></span>
</p>
<p class="adminAcc">
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.
</p>
<br />
<h4>Development Servers</h4>
<p id="DevSites">
<label for="devsites">Development servers:</label>
<span class="middleColumn"><textarea name="devsites" id="devsites" rows="5" />localhost
127.0.0.1</textarea></span>
</p>
<p class="devHelp">
SilverStripe allows you to run a site in <a href="http://doc.silverstripe.com/doku.php?id=devmode">development mode</a>.
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.
</p>
<br />
<h4 id="requirements">Requirements</h4>
<?php
$req->showTable();
?>
</form>
</div>
</div>
<div class="clear"><!-- --></div>
</div>
<div id="Footer">
<div class="footerTop"><!-- --></div>
<p><a href="http://www.silverstripe.com">SilverStripe Open Source CMS</a> | Copyright &copy; 2008 SilverStripe Limited</p>
</div>
</div>
</body>
</html>

118
dev/install/install.css Normal file
View File

@ -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;
}

View File

@ -0,0 +1,40 @@
<html>
<head>
<title>PHP 5 is required</title>
<link rel="stylesheet" type="text/css" href="themes/blackcandy/css/layout.css" />
<link rel="stylesheet" type="text/css" href="themes/blackcandy/css/typography.css" />
<link rel="stylesheet" type="text/css" href="themes/blackcandy/css/form.css" />
<link rel="stylesheet" type="text/css" href="sapphire/dev/install/install.css" />
</head>
<body>
<div id="BgContainer">
<div id="Container">
<div id="Header">
<h1>SilverStripe CMS Installation</h1>
</div>
<div id="Navigation">&nbsp;</div>
<div class="clear"><!-- --></div>
<div id="Layout">
<div class="typography">
<h1>PHP 5 required</h1>
<h2>To run SilverStripe, please install PHP 5.0 or greater.</h2>
<p>We have detected that you are running PHP version <b>$PHPVersion</b>. 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.</p>
<p>If you are running on a shared host, you may need to ask your hosting provider how to do this.</p>
</div>
</div>
<div class="clear"><!-- --></div>
</div>
<div id="Footer">
<div class="footerTop"><!-- --></div>
<p><a href="http://www.silverstripe.com">SilverStripe Open Source CMS</a> | Copyright &copy; 2008 SilverStripe Limited</p>
</div>
</div>
</body>
</html>

View File

@ -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

View File

@ -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!

View File

@ -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) {

View File

@ -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 .= "<li class=\"$extraClass\"><input id=\"$itemID\" name=\"$this->name[$key]\" type=\"checkbox\" value=\"$key\"$checked $disabled class=\"checkbox\" /> <label for=\"$itemID\">$value</label></li>\n";
}
return "<ul id=\"{$this->id()}\" class=\"optionset\">\n$options</ul>\n";
return "<ul id=\"{$this->id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n$options</ul>\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;
}
/**

View File

@ -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) <ul>";

View File

@ -101,7 +101,7 @@ class CurrencyField_Readonly extends ReadonlyField{
* This already is a readonly field.
*/
function performReadonlyTransformation() {
return $this;
return clone $this;
}
}
@ -113,6 +113,8 @@ class CurrencyField_Readonly extends ReadonlyField{
*/
class CurrencyField_Disabled extends CurrencyField{
protected $disabled = true;
/**
* overloaded to display the correctly formated value for this datatype
*/

View File

@ -53,7 +53,9 @@ class DatalessField extends FormField {
* Returns a readonly version of this field
*/
function performReadonlyTransformation() {
return $this;
$clone = clone $this;
$clone->setReadonly(true);
return $clone;
}
/**

View File

@ -99,8 +99,11 @@ class DateField_Disabled extends DateField {
protected $disabled = true;
function setValue($val) {
if($val && $val != "0000-00-00") $this->value = date('d/m/Y', strtotime($val));
else $this->value = '('._t('DateField.NODATESET', 'No date set').')';
if(is_string($val) && preg_match('/^([\d]{2,4})-([\d]{1,2})-([\d]{1,2})/', $val)) {
$this->value = preg_replace('/^([\d]{2,4})-([\d]{1,2})-([\d]{1,2})/','\\3/\\2/\\1', $val);
} else {
$this->value = $val;
}
}
function Field() {
@ -136,4 +139,4 @@ class DateField_Disabled extends DateField {
return true;
}
}
?>
?>

View File

@ -451,19 +451,27 @@ class FieldSet extends DataObjectSet {
/**
* Transform the named field into a readonly feld.
*
* @param string|FormField
*/
function makeFieldReadonly($fieldName) {
function makeFieldReadonly($field) {
$fieldName = ($field instanceof FormField) ? $field->Name() : $field;
// Iterate on items, looking for the applicable field
foreach($this->items as $i => $field) {
// Once it's found, use FormField::transform to turn the field into a readonly version of itself.
if($field->Name() == $fieldName) {
$this->items[$i] = $field->transform(new ReadonlyTransformation());
// Clear an internal cache
$this->sequentialSet = null;
// A true results indicates that the field was foudn
return true;
foreach($this->items 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->items[$i] = $item->transform(new ReadonlyTransformation());
// Clear an internal cache
$this->sequentialSet = null;
// A true results indicates that the field was foudn
return true;
}
}
}
return false;

View File

@ -63,6 +63,15 @@ class Form extends RequestHandler {
*/
protected $target;
/**
* Legend value, to be inserted into the
* <legend> element before the <fieldset>
* in Form.ss template.
*
* @var string
*/
protected $legend;
protected $buttonClickedFunc;
protected $message;
@ -456,7 +465,15 @@ class Form extends RequestHandler {
function setTarget($target) {
$this->target = $target;
}
/**
* Set the legend value to be inserted into
* the <legend> element in the Form.ss template.
*/
function setLegend($legend) {
$this->legend = $legend;
}
/**
* Returns the encoding type of the form.
* This will be either "multipart/form-data"" if there are any {@link FileField} instances,
@ -662,6 +679,16 @@ class Form extends RequestHandler {
function getRecord() {
return $this->record;
}
/**
* Get the legend value to be inserted into the
* <legend> element in Form.ss
*
* @return string
*/
function getLegend() {
return $this->legend;
}
/**
* Processing that occurs before a form is executed.

View File

@ -91,8 +91,9 @@ class FormAction extends FormField {
* Globally disabled buttons would break the CMS.
*/
function performReadonlyTransformation() {
$this->setDisabled(true);
return $this;
$clone = clone $this;
$clone->setReadonly(true);
return $clone;
}
function readonlyField() {

View File

@ -432,12 +432,13 @@ HTML;
* Return a disabled version of this field
*/
function performDisabledTransformation() {
$disabledClassName = $this->class . '_Disabled';
$clone = clone $this;
$disabledClassName = $clone->class . '_Disabled';
if( ClassInfo::exists( $disabledClassName ) )
return new $disabledClassName( $this->name, $this->title, $this->value );
elseif($this->hasMethod('setDisabled')){
$this->setDisabled(true);
return $this;
elseif($clone->hasMethod('setDisabled')){
$clone->setDisabled(true);
return $clone;
}else{
return $this->performReadonlyTransformation();
}

View File

@ -24,10 +24,10 @@ class HeaderField extends DatalessField {
$form = (isset($args[3])) ? $args[3] : null;
}
$this->headingLevel = $headingLevel;
if($headingLevel) $this->headingLevel = $headingLevel;
$this->allowHTML = $allowHTML;
parent::__construct($name, $title, null, $form);
parent::__construct($name, $title, null, $allowHTML, $form);
}
function Field() {

View File

@ -16,8 +16,9 @@ class HiddenField extends FormField {
return $this->Field();
}
function performReadonlyTransformation() {
$this->setReadonly(true);
return $this;
$clone = clone $this;
$clone->setReadonly(true);
return $clone;
}
function IsHidden() {

View File

@ -53,8 +53,7 @@ class ImageField extends FileField {
* Returns a readonly version of this field
*/
function performReadonlyTransformation() {
$field = new SimpleImageField_Disabled($this->name, $this->title,
$this->value);
$field = new SimpleImageField_Disabled($this->name, $this->title, $this->value);
$field->setForm($this->form);
return $field;
}

View File

@ -42,8 +42,9 @@ class LiteralField extends DatalessField {
}
function performReadonlyTransformation() {
$this->setReadonly(true);
return $this;
$clone = clone $this;
$clone->setReadonly(true);
return $clone;
}
}

View File

@ -42,7 +42,8 @@ class LookupField extends DropdownField {
}
function performReadonlyTransformation() {
return $this;
$clone = clone $this;
return $clone;
}
function Type() {

View File

@ -1,4 +1,10 @@
<?php
/**
* @package forms
* @subpackage fields-formattedinput
*/
/**
* Field for displaying phone numbers. It separates the number, the area code and optionally the country code
* and extension.
@ -18,16 +24,24 @@ class PhoneNumberField extends FormField {
$this->ext = $extension;
$this->countryCode = $countryCode;
parent::__construct( $name, ($title===null) ? $name : $title, $value, $form );
parent::__construct( $name, $title, $value, $form );
}
public function Field() {
$field = new FieldGroup( $this->name );
$field->setID("{$this->name}_Holder");
list( $countryCode, $areaCode, $phoneNumber, $extension ) = $this->parseValue();
$hasTitle = false;
if ($this->value=="")
{
$countryCode=$this->countryCode;
$areaCode=$this->areaCode;
$extension=$this->ext;
}
if( $this->countryCode !== null )
$field->push( new NumericField( $this->name.'[Country]', '+', $countryCode, 4 ) );
@ -53,41 +67,42 @@ class PhoneNumberField extends FormField {
public static function joinPhoneNumber( $value ) {
if( is_array( $value ) ) {
$completeNumber = '';
if( ! empty($value['Country']) )
if( isset($value['Country']) && $value['Country']!=null) {
$completeNumber .= '+' . $value['Country'];
if( ! empty($value['Area']) )
}
if( isset($value['Area']) && $value['Area']!=null) {
$completeNumber .= '(' . $value['Area'] . ')';
}
$completeNumber .= $value['Number'];
if( ! empty($value['Extension']) )
if( isset($value['Extension']) && $value['Extension']!=null) {
$completeNumber .= '#' . $value['Extension'];
}
return $completeNumber;
} else
return $value;
}
protected function parseValue() {
if( !is_array( $this->value ) )
if( !is_array( $this->value ))
preg_match( '/^(?:(?:\+(\d+))?\s*\((\d+)\))?\s*([0-9A-Za-z]*)\s*(?:[#]\s*(\d+))?$/', $this->value, $parts );
else
return array( '', '', $this->value, '' );
if( is_array( $parts ) ) {
array_shift( $parts );
$parts = array_pad($parts, 4, false);
if(is_array($parts)) array_shift( $parts );
for ($x=0;$x<=3;$x++) {
if (!isset($parts[$x])) $parts[]='';
}
if(sizeof($parts) < 4) $parts[] = '';
return $parts;
}
public function saveInto( $record ) {
list( $countryCode, $areaCode, $phoneNumber, $extension ) = $this->parseValue();
$fieldName = $this->name;
@ -103,7 +118,7 @@ class PhoneNumberField extends FormField {
if( $extension )
$completeNumber .= '#' . $extension;
$record->$fieldName = $completeNumber;
}

View File

@ -9,7 +9,7 @@ class ReadonlyField extends FormField {
protected $readonly = true;
function performReadonlyTransformation() {
return $this;
return clone $this;
}
}
?>

View File

@ -11,7 +11,7 @@ class RestrictedTextField extends TextField {
function __construct($name, $title = null, $value = "", $restrictedChars = "", $maxLength = null){
$this->restrictedChars = $restrictedChars;
parent::__construct($name, $title, $value, $form);
parent::__construct($name, $title, $value);
}
function Field() {

View File

@ -31,16 +31,17 @@ class SelectionGroup extends CompositeField {
*/
public function performDisabledTransformation($trans) {
$newChildren = array();
if($this->children) foreach($this->children as $idx => $child) {
$clone = clone $this;
if($clone->children) foreach($clone->getChildren() as $idx => $child) {
if(is_object($child)) {
$child = $child->transform($trans);
}
$newChildren[$idx] = $child;
}
$this->children = new FieldSet($newChildren);
$this->readonly = true;
return $this;
$clone->setChildren(new FieldSet($newChildren));
$clone->setReadonly(true);
return $clone;
}
function FieldSet() {

View File

@ -80,7 +80,11 @@ class SimpleImageField extends FileField {
* @subpackage fields-files
*/
class SimpleImageField_Disabled extends FormField {
protected $disabled = true;
protected $readonly = true;
function Field() {
$record = $this->form->getRecord();
$fieldName = $this->name;

View File

@ -321,15 +321,17 @@ class TableField extends TableListField {
}
function performReadonlyTransformation() {
$this->permissions = array('show');
$this->setReadonly(true);
return $this;
$clone = clone $this;
$clone->permissions = array('show');
$clone->setReadonly(true);
return $clone;
}
function performDisabledTransformation() {
$this->permissions = array('show');
$this->setDisabled(true);
return $this;
$clone = clone $this;
$clone->setPermissions(array('show'));
$clone->setDisabled(true);
return $clone;
}
/**

View File

@ -512,11 +512,12 @@ JS
}
function performReadonlyTransformation() {
$this->setShowPagination(false);
$this->setPermissions(array('show'));
$this->addExtraClass( 'readonly' );
$this->setReadonly(true);
return $this;
$clone = clone $this;
$clone->setShowPagination(false);
$clone->setPermissions(array('show'));
$clone->addExtraClass( 'readonly' );
$clone->setReadonly(true);
return $clone;
}
/**
@ -1049,6 +1050,13 @@ JS
$this->csvFieldFormatting = $formatting;
}
/**
* Edit the field list
*/
function setFieldList($fieldList) {
$this->fieldList = $fieldList;
}
/**
* @return String
*/

View File

@ -71,9 +71,10 @@ class TextareaField extends FormField {
* The element shouldn't be both disabled and readonly at the same time.
*/
function performReadonlyTransformation() {
$this->readonly = true;
$this->disabled = false;
return $this;
$clone = clone $this;
$clone->setReadonly(true);
$clone->setDisabled(false);
return $clone;
}
/**
@ -83,9 +84,10 @@ class TextareaField extends FormField {
* The element shouldn't be both disabled and readonly at the same time.
*/
function performDisabledTransformation() {
$this->disabled = true;
$this->readonly = false;
return $this;
$clone = clone $this;
$clone->setDisabled(true);
$clone->setReadonly(false);
return $clone;
}
function Type() {

View File

@ -48,8 +48,11 @@ class TreeDropdownField extends FormField {
$id = $this->id();
$classes = "TreeDropdownField single";
if($this->extraClass()) $classes .= ' ' . $this->extraClass();
return <<<HTML
<div id="TreeDropdownField_$id" class="TreeDropdownField single"><input id="$id" type="hidden" name="$this->name" value="$this->value" /><span class="items">$title</span><a href="#" title="open" class="editLink">&nbsp;</a></div>
<div id="TreeDropdownField_$id" class="$classes"><input id="$id" type="hidden" name="$this->name" value="$this->value" /><span class="items">$title</span><a href="#" title="open" class="editLink">&nbsp;</a></div>
HTML;
}

View File

@ -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');

View File

@ -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'] = '<p>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\'</p>';
$lang['en_US']['SiteTree']['TOPLEVEL'] = 'Site Content (Top Level)';

View File

@ -1,5 +1,21 @@
<?php
/************************************************************************************
************************************************************************************
** **
** If you can read this text in your browser then you don't have PHP installed. **
** Please install PHP 5.0 or higher, preferably PHP 5.2. **
** **
************************************************************************************
************************************************************************************/
$majorVersion = strtok(phpversion(),'.');
if($majorVersion < 5) {
header("HTTP/1.1 500 Server Error");
echo str_replace('$PHPVersion', phpversion(), file_get_contents("dev/install/php5-required.html"));
die();
}
/**
* Main file that handles every page request.
*

2
sake
View File

@ -1,5 +1,5 @@
# Check for an argument
if [ $1 = "" ]; then
if [ ${1:-""} = "" ]; then
echo "Sapphire Sake
Usage: $0 (command-url) (params)

View File

@ -16,10 +16,15 @@ class SearchForm extends Form {
protected $showInSearchTurnOn;
/**
* @var int $numPerPage How many results are shown per page.
* @deprecated 2.3 Use {@link $pageLength}.
*/
protected $numPerPage;
/**
* @var int $pageLength How many results are shown per page.
* Relies on pagination being implemented in the search results template.
*/
protected $numPerPage = 10;
protected $pageLength = 10;
/**
*
@ -45,25 +50,14 @@ class SearchForm extends Form {
);
}
// We need this because it's a get form. It can't go in the action value
// Hayden: Sorry if I've got it mixed up, but on the results or not found pages, the
// RelativeLink seems to be empty and it packs a sad
$formController = isset($_GET['formController']) ? $_GET['formController'] : null;
if(!$formController) $formController = $controller->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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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(
'<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
_t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
);
$message = _t('Member.PROFILESAVESUCCESS', 'Successfully saved.') . ' ' . $closeLink;
$form->sessionMessage($message, 'good');
Director::redirectBack();
}
}

View File

@ -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;
}

View File

@ -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),

View File

@ -6,18 +6,22 @@
<% else %>
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
<% end_if %>
<fieldset>
<legend>$Legend</legend>
<% control Fields %>
$FieldHolder
<% end_control %>
<div class="clear"><!-- --></div>
</fieldset>
<% if Actions %>
<% if Actions %>
<div class="Actions">
<% control Actions %>$Field<% end_control %>
<% control Actions %>
$Field
<% end_control %>
</div>
<% end_if %>
<% end_if %>
<% if IncludeFormTag %>
</form>
<% end_if %>

34
templates/ModelViewer.ss Normal file
View File

@ -0,0 +1,34 @@
<html>
<head>
<% base_tag %>
<title>Data Model</title>
</head>
<body>
<h1>Data Model for your project</h1>
<% control Modules %>
<h1>Module $Name</h1>
<img src="$Link/graph" />
<% control Models %>
<h2>$Name <% if ParentModel %> (subclass of $ParentModel)<% end_if %></h2>
<h4>Fields</h4>
<ul>
<% control Fields %>
<li>$Name - $Type</li>
<% end_control %>
</ul>
<h4>Relations</h4>
<ul>
<% control Relations %>
<li>$Name $RelationType $RelatedClass</li>
<% end_control %>
</ul>
<% end_control %>
<% end_control %>
</body>
</html>

View File

@ -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 %>
}

View File

@ -1,10 +1,11 @@
<form $FormAttributes>
<fieldset>
<% control Fields %>
$FieldHolder
<% end_control %>
<% control Actions %>
$Field
<% end_control %>
<legend></legend>
<% control Fields %>
$FieldHolder
<% end_control %>
<% control Actions %>
$Field
<% end_control %>
</fieldset>
</form>

31
tests/ErrorPageTest.php Normal file
View File

@ -0,0 +1,31 @@
<?php
class ErrorPageTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/ErrorPageTest.yml';
function test404ErrorPage() {
$errorPage = DataObject::get_one('ErrorPage', "ErrorCode = '404'");
/* We have an ErrorPage object to use */
$this->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');
}
}
?>

5
tests/ErrorPageTest.yml Normal file
View File

@ -0,0 +1,5 @@
ErrorPage:
404:
Title: Page Not Found
URLSegment: page-not-found
ErrorCode: 404

View File

@ -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();
}
}

View File

@ -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'
);

View File

@ -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

View File

@ -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');

View File

@ -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);

View File

@ -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()'
);
}
}
?>

View File

@ -0,0 +1,89 @@
<?php
/**
* @package sapphire
* @subpackage tests
*/
class FormFieldTest extends SapphireTest {
function testFieldHasExtraClass() {
/* TextField has an extra class name and is in the HTML the field returns */
$textField = new TextField('Name');
$textField->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"
);
}
}
}
?>

View File

@ -62,6 +62,8 @@ class i18nTest extends SapphireTest {
unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']);
i18n::set_locale('en_US');
parent::tearDown();
}
function testGetExistingTranslations() {

View File

@ -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() {

View File

@ -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() {