diff --git a/README.md b/README.md index a3e891f37..a1f9833df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -## Overview +## SilverStripe Framework + +[![Build Status](https://secure.travis-ci.org/silverstripe/sapphire.png)](http://travis-ci.org/silverstripe/sapphire) PHP5 framework forming the base for the SilverStripe CMS ([http://silverstripe.org](http://silverstripe.org)). Requires a [`silverstripe-installer`](http://github.com/silverstripe/silverstripe-installer) base project. Typically used alongside the [`cms`](http://github.com/silverstripe/silverstripe-cms) module. diff --git a/admin/css/screen.css b/admin/css/screen.css index 8c89a3628..8a0a34f2f 100644 --- a/admin/css/screen.css +++ b/admin/css/screen.css @@ -149,8 +149,9 @@ form.nostyle input.text, form.nostyle textarea, form.nostyle select, form.nostyl .field:after { content: "\0020"; display: block; height: 0; clear: both; overflow: hidden; visibility: hidden; } .field.nolabel .middleColumn { margin-left: 0; } .field.nolabel .help { margin-left: 0; } +.field.checkbox label.right { margin: 4px 0 0 0; display: inline; font-style: normal; color: #444444; clear: none; } .field label.left { float: left; display: block; width: 176px; padding: 8px 8px 8px 0; line-height: 16px; font-weight: bold; text-shadow: 1px 1px 0 white; } -.field label.right { cursor: pointer; } +.field label.right { cursor: pointer; clear: both; color: #777777; display: block; font-style: italic; margin: 4px 0 0 184px; } .field .middleColumn { margin-left: 184px; } .field span.readonly { padding-top: 8px; line-height: 16px; display: block; } .field .help { clear: both; color: #777777; display: block; font-style: italic; margin: 4px 0 0 184px; } @@ -381,7 +382,7 @@ body.cms { overflow: hidden; } .cms-add-form #PageType ul li .description { font-style: italic; } /** -------------------------------------------- Content toolbar -------------------------------------------- */ -.cms-content-toolbar { display: block; margin: 0 0 15px 0; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -o-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); *zoom: 1; /* smaller treedropdown */ } +.cms-content-toolbar { min-height: 35px; display: block; margin: 0 0 15px 0; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -o-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); *zoom: 1; /* smaller treedropdown */ } .cms-content-toolbar:after { content: "\0020"; display: block; height: 0; clear: both; overflow: hidden; visibility: hidden; } .cms-content-toolbar .cms-tree-view-modes { float: right; padding-top: 5px; } .cms-content-toolbar .cms-tree-view-modes * { display: inline-block; } diff --git a/admin/scss/_forms.scss b/admin/scss/_forms.scss index 0443f534e..395a4a168 100644 --- a/admin/scss/_forms.scss +++ b/admin/scss/_forms.scss @@ -22,7 +22,6 @@ form.nostyle { //TODO: use single border line with shadow instead:: http://daverupert.com/2011/06/two-tone-borders-with-css3/ //overflow: hidden; - // bottom padding accounts for the border and we have a negative // margin with a postive padding to ensure the bottom border extends // over the edges @@ -47,9 +46,15 @@ form.nostyle { margin-left: 0; } } - - label { - + + &.checkbox label.right{ + margin: $grid-y/2 0 0 0; + display:inline; + font-style: normal; + color: $color-text; + clear:none; + } + label { &.left { float: left; display: block; @@ -58,11 +63,15 @@ form.nostyle { line-height: $grid-y * 2; font-weight: bold; @include text-shadow(1px 1px 0 $color-text-shadow); - } - + } &.right { cursor: pointer; - } + clear: both; + color: lighten($color-text, 20%); + display: block; + font-style: italic; + margin: $grid-y/2 0 0 $grid-x*23; + } } .middleColumn { diff --git a/admin/scss/_style.scss b/admin/scss/_style.scss index 2a2aa744f..08f35c5e8 100644 --- a/admin/scss/_style.scss +++ b/admin/scss/_style.scss @@ -596,7 +596,7 @@ body.cms { * -------------------------------------------- */ .cms-content-toolbar { - + min-height:35px; display: block; margin: 0 0 15px 0; diff --git a/core/Convert.php b/core/Convert.php index 205a95040..3df0b37b7 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -317,4 +317,15 @@ class Convert { $f = URLSegmentFilter::create(); return $f->filter($title); } + + /** + * Normalises newline sequences to conform to (an) OS specific format. + * @param string $data Text containing potentially mixed formats of newline + * sequences including \r, \r\n, \n, or unicode newline characters + * @param string $nl The newline sequence to normalise to. Defaults to that + * specified by the current OS + */ + public static function nl2os($data, $nl = PHP_EOL) { + return preg_replace('~\R~u', $nl, $data); + } } diff --git a/core/HTMLCleaner.php b/core/HTMLCleaner.php index a37275442..53cf02210 100644 --- a/core/HTMLCleaner.php +++ b/core/HTMLCleaner.php @@ -93,6 +93,8 @@ class TidyHTMLCleaner extends HTMLCleaner { public function cleanHTML($content) { $tidy = new tidy(); $output = $tidy->repairString($content, $this->config); - return $output; + + // Clean leading/trailing whitespace + return preg_replace('/(^\s+)|(\s+$)/', '', $output); } } diff --git a/dev/Deprecation.php b/dev/Deprecation.php index 98f7403a0..d673cb28f 100644 --- a/dev/Deprecation.php +++ b/dev/Deprecation.php @@ -86,11 +86,11 @@ class Deprecation { protected static function get_calling_module_from_trace($backtrace) { if (!isset($backtrace[1]['file'])) return; - $callingfile = $backtrace[1]['file']; + $callingfile = realpath($backtrace[1]['file']); global $manifest; foreach ($manifest->getModules() as $name => $path) { - if (strpos($callingfile, $path) === 0) { + if (strpos($callingfile, realpath($path)) === 0) { return $name; } } diff --git a/docs/en/reference/uploadfield.md b/docs/en/reference/uploadfield.md new file mode 100644 index 000000000..37dbbfac9 --- /dev/null +++ b/docs/en/reference/uploadfield.md @@ -0,0 +1,189 @@ +# UploadField + +## Introduction + +The UploadField will let you upload one or multiple files of all types, +including images. But that's not all it does - it will also link the +uploaded file(s) to an existing relation and let you edit the linked files +as well. That makes it flexible enough to sometimes even replace the Gridfield, +like for instance in creating and managing a simple gallery. + +## Usage +The UploadField can be used in two ways: + +### Single fileupload + +The following example adds an UploadField to a page for single fileupload, +based on a has_one relation: + + :::php + class GalleryPage extends Page { + + static $has_one = array( + 'SingleImage' => 'Image' + ); + + function getCMSFields() { + + $fields = parent::getCMSFields(); + + $fields->addFieldToTab( + 'Root.Upload', + $uploadField = new UploadField( + $name = 'SingleImage', + $title = 'Upload a single image' + ) + ); + return $fields; + } + } + +The UploadField will autodetect the relation based on it's `name` property, and +save it into the GalleyPages' `SingleImageID` field. Setting the +`allowedMaxFileNumber` to 1 will make sure that only one image can ever be +uploaded and linked to the relation. + +### Multiple fileupload + +Enable multiple fileuploads by using a many_many relation. Again, the +UploadField will detect the relation based on its $name property value: + + :::php + class GalleryPage extends Page { + + static $many_many = array( + 'GalleryImages' => 'Image' + ); + + function getCMSFields() { + + $fields = parent::getCMSFields(); + + $fields->addFieldToTab( + 'Root.Upload', + $uploadField = new UploadField( + $name = 'GalleryImages', + $title = 'Upload one or more images (max 10 in total)' + ) + ); + $uploadField->setConfig('allowedMaxFileNumber', 10); + + return $fields; + } + } + class GalleryPage_Controller extends Page_Controller { + } + +WARNING: Currently the UploadField doesn't fully support has_many relations, so use a many_many relation instead! + +## Set a custom folder + +This example will save all uploads in the `/assets/customfolder/` folder. If +the folder doesn't exist, it will be created. + + :::php + $fields->addFieldToTab( + 'Root.Upload', + $uploadField = new UploadField( + $name = 'GalleryImages', + $title = 'Please upload one or more images' ) + ); + $uploadField->setFolderName('customfolder'); + +## Limit the allowed filetypes + +`AllowedExtensions` is by default `File::$allowed_extensions` but can be overwritten for each UploadField: + + :::php + $uploadField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif')); + + +## Limit the maximum file size + +`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations: `upload_max_filesize` and `post_max_size` +The value is set as bytes. + +NOTE: this only sets the configuration for your UploadField, this does NOT change your server upload settings, so if your server is set to only allow 1 MB and you set the UploadFIeld to 2 MB, uploads will not work. + + :::php + $sizeMB = 2; // 2 MB + $size = $sizeMB * 1024 * 1024; // 2 MB in bytes + $this->getValidator()->setAllowedMaxFileSize($size); + +## Other configuration settings + +### Preview dimensions + +Set the dimensions of the image preview. By default the max width is set to 80 +and the max height is set to 60. + + :::php + $uploadField->setConfig('previewMaxWidth', 100); + $uploadField->setConfig('previewMaxHeight', 100); + +### Automatic or manual upload + +By default, the UploadField will try to automatically upload all selected files. +Setting the `autoUpload` property to false, will present you with a list of +selected files that you can then upload manually one by one: + + :::php + $uploadField->setConfig('autoUpload', false); + +### Build a simple gallery + +A gallery most times needs more then simple images. You might want to add a +description, or maybe some settings to define a transition effect for each slide. +First create a +[DataExtension](http://doc.silverstripe.org/framework/en/reference/dataextension) +like this: + + :::php + class GalleryImage extends DataExtension { + + static $db = array( + 'Description' => 'Text' + ); + + public static $belongs_many_many = array( + 'GalleryPage' => 'GalleryPage' + ); + } + +Now register the DataExtension for the Image class in your _config.php: + + :::php + Object::add_extension('Image', 'GalleryImage'); + +NOTE: although you can subclass the Image class instead of using a DataExtension, this is not advisable. For instance: when using a subclass, the 'From files' button will only return files that were uploaded for that subclass, it won't recognize any other images! +### Edit uploaded images + +By default the UploadField will let you edit the following fields: *Title, +Filename, Owner and Folder*. The `fileEditFields` configuration setting allows +you you alter these settings. One way to go about this is create a +`getCustomFields` function in your GalleryImage object like this: + + :::php + class GalleryImage extends DataExtension { + ... + + function getCustomFields() { + $fields = new FieldList(); + $fields->push(new TextField('Title', 'Title')); + $fields->push(new TextareaField('Description', 'Description')); + return $fields; + } + } + +Then, in your GalleryPage, tell the UploadField to use this function: + + :::php + $uploadField->setConfig('fileEditFields', 'getCustomFields'); + +In a similar fashion you can use 'fileEditActions' to set the actions for the +editform, or 'fileEditValidator' to determine the validator (eg RequiredFields). + +## TODO: Using the UploadField in a frontend form + +*At this moment the UploadField not yet fully supports being used on a frontend +form.* diff --git a/docs/en/topics/datamodel.md b/docs/en/topics/datamodel.md index 25f1c454a..c442678e9 100755 --- a/docs/en/topics/datamodel.md +++ b/docs/en/topics/datamodel.md @@ -226,15 +226,6 @@ since 1/1/2011. 'LastVisited:GreaterThan' => '2011-01-01' )); -If you wish to match against any of a number of columns, you can list several field names, separated by commas. -This will return all members whose first name or surname contain the string 'sam'. - - :::php - $members = Member::get()->filter(array( - 'FirstName,Surname:PartialMatch' => 'sam' - )); - - ### Subtract You can subtract entries from a DataList by passing in another DataList to `subtract()` diff --git a/docs/en/tutorials/1-building-a-basic-site.md b/docs/en/tutorials/1-building-a-basic-site.md index 301821cfd..47956fc48 100644 --- a/docs/en/tutorials/1-building-a-basic-site.md +++ b/docs/en/tutorials/1-building-a-basic-site.md @@ -145,8 +145,8 @@ or placed between SilverStripe template tags: **Flushing the cache** -Whenever we edit a template file, we need to append *?flush=all* onto the end of the URL, e.g. -http://localhost/your_site_name/?flush=all. SilverStripe stores template files in a cache for quicker load times. Whenever there are +Whenever we edit a template file, we need to append *?flush=1* onto the end of the URL, e.g. +http://localhost/your_site_name/?flush=1. SilverStripe stores template files in a cache for quicker load times. Whenever there are changes to the template, we must flush the cache in order for the changes to take effect. ## The Navigation System @@ -343,7 +343,7 @@ Create a new file *HomePage.php* in *mysite/code*. Copy the following code into Every page type also has a database table corresponding to it. Every time we modify the database, we need to rebuild it. -We can do this by going to [http://localhost/your_site_name/dev/build?flush=all](http://localhost/your_site_name/dev/build?flush=1) (replace *localhost/your_site_name* with your own domain name if applicable). +We can do this by going to [http://localhost/your_site_name/dev/build](http://localhost/your_site_name/dev/build) (replace *localhost/your_site_name* with your own domain name if applicable). It may take a moment, so be patient. This add tables and fields needed by your site, and modifies any structures that have changed. It does this non-destructively - it will never delete your data. @@ -366,7 +366,7 @@ It always tries to use the most specific template in an inheritance chain. ### Creating a new template -To create a new template layout, create a copy of *Page.ss* (found in *themes/simple/templates/Layout*) and call it *HomePage.ss*. If we flush the cache (*?flush=all*), SilverStripe should now be using *HomePage.ss* for the homepage, and *Page.ss* for the rest of the site. Now let's customize the *HomePage* template. +To create a new template layout, create a copy of *Page.ss* (found in *themes/simple/templates/Layout*) and call it *HomePage.ss*. If we flush the cache (*?flush=1*), SilverStripe should now be using *HomePage.ss* for the homepage, and *Page.ss* for the rest of the site. Now let's customize the *HomePage* template. First, we don't need the breadcrumbs and the secondary menu for the homepage. Let's remove them: :::ss diff --git a/docs/en/tutorials/2-extending-a-basic-site.md b/docs/en/tutorials/2-extending-a-basic-site.md index 04103e108..f0da5bcd7 100644 --- a/docs/en/tutorials/2-extending-a-basic-site.md +++ b/docs/en/tutorials/2-extending-a-basic-site.md @@ -90,7 +90,7 @@ to be children of the page in the site tree. As we only want **news articles** i We will be introduced to other fields like this as we progress; there is a full list in the documentation for `[api:SiteTree]`. -Now that we have created our page types, we need to let SilverStripe rebuild the database: [http://localhost/your_site_name/dev/build?flush=all](http://localhost/your_site_name/dev/build?flush=all). SilverStripe should detect that there are two new page types, and add them to the list of page types in the database. +Now that we have created our page types, we need to let SilverStripe rebuild the database: [http://localhost/your_site_name/dev/build](http://localhost/your_site_name/dev/build). SilverStripe should detect that there are two new page types, and add them to the list of page types in the database.
wrong nesting
' . "\n"), - 'wrong nesting
' . "\n", + $cleaner->cleanHTML('wrong nesting
'), + 'wrong nesting
', "HTML cleaned properly" ); $this->assertEquals( - $cleaner->cleanHTML('unclosed paragraph' . "\n"), - '
unclosed paragraph
' . "\n", + $cleaner->cleanHTML('unclosed paragraph'), + '
unclosed paragraph
', "HTML cleaned properly" ); } else { diff --git a/tests/i18n/i18nTextCollectorTest.php b/tests/i18n/i18nTextCollectorTest.php index f9b59ab59..7218ac7ee 100644 --- a/tests/i18n/i18nTextCollectorTest.php +++ b/tests/i18n/i18nTextCollectorTest.php @@ -413,7 +413,7 @@ de: OtherEntityName: 'Other Text' YAML; - $this->assertEquals($yaml, $writer->getYaml($entities, 'de')); + $this->assertEquals($yaml, Convert::nl2os($writer->getYaml($entities, 'de'))); } public function testCollectFromIncludedTemplates() { diff --git a/tests/model/DatabaseTest.php b/tests/model/DatabaseTest.php index b62859904..ff8832956 100644 --- a/tests/model/DatabaseTest.php +++ b/tests/model/DatabaseTest.php @@ -62,6 +62,22 @@ class DatabaseTest extends SapphireTest { } } + function testIsSchemaUpdating() { + $db = DB::getConn(); + + $this->assertFalse($db->isSchemaUpdating(), 'Before the transaction the flag is false.'); + + $db->beginSchemaUpdate(); + $this->assertTrue($db->isSchemaUpdating(), 'During the transaction the flag is true.'); + + $db->endSchemaUpdate(); + $this->assertFalse($db->isSchemaUpdating(), 'After the transaction the flag is false.'); + + $db->beginSchemaUpdate(); + $db->cancelSchemaUpdate(); + $this->assertFalse($db->doesSchemaNeedUpdating(), 'After cancelling the transaction the flag is false'); + } + public function testSchemaUpdateChecking() { $db = DB::getConn();