From 6c754d29e90b78454fcc44e438ce48878be04bdb Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 17 Jun 2009 11:36:49 +0000 Subject: [PATCH] Merged from branches/2.3 git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@79438 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/Core.php | 44 +++++----- core/model/SiteTree.php | 14 ++-- core/model/fieldtypes/HTMLText.php | 128 ++++++++++++++++++----------- dev/BulkLoader.php | 4 +- dev/Debug.php | 4 +- dev/DevelopmentAdmin.php | 110 +++++++++++++++++++++---- forms/DMYDateField.php | 2 +- forms/TextareaField.php | 4 +- forms/TreeMultiselectField.php | 15 +++- tests/MemoryLimitTest.php | 38 +++++++++ tests/fieldtypes/HTMLTextTest.php | 81 ++++++++++++++++++ tests/forms/TextareaFieldTest.php | 46 +++++++++++ 12 files changed, 389 insertions(+), 101 deletions(-) create mode 100644 tests/MemoryLimitTest.php create mode 100644 tests/forms/TextareaFieldTest.php diff --git a/core/Core.php b/core/Core.php index 31bbca9ec..a1f71f04a 100755 --- a/core/Core.php +++ b/core/Core.php @@ -125,26 +125,8 @@ define('PR_LOW',10); /** * Ensure we have enough memory */ -$memString = ini_get("memory_limit"); -switch(strtolower(substr($memString, -1))) { -case "k": - $memory = round(substr($memString, 0, -1)*1024); - break; -case "m": - $memory = round(substr($memString, 0, -1)*1024*1024); - break; -case "g": - $memory = round(substr($memString, 0, -1)*1024*1024*1024); - break; -default: - $memory = round($memString); -} -// Check we have at least 64M -if ($memory < (64 * 1024 * 1024)) { - // Increase memory limit - ini_set('memory_limit', '64M'); -} +increase_memory_limit_to('64M'); /////////////////////////////////////////////////////////////////////////////// // INCLUDES @@ -305,4 +287,28 @@ function _t($entity, $string = "", $priority = 40, $context = "") { return i18n::_t($entity, $string, $priority, $context); } +/** + * Increase the memory limit to the given level if it's currently too low. + * @param A memory limit string, such as "64M" + */ +function increase_memory_limit_to($memoryLimit) { + // Increase the memory limit if it's too low + if(translate_memstring($memoryLimit) > translate_memstring(ini_get('memory_limit'))) { + ini_set('memory_limit', $memoryLimit); + } +} + +/** + * Turn a memory string, such as 512M into an actual number of bytes. + * @param A memory limit string, such as "64M" + */ +function translate_memstring($memString) { + switch(strtolower(substr($memString, -1))) { + case "k": return round(substr($memString, 0, -1)*1024); + case "m": return round(substr($memString, 0, -1)*1024*1024); + case "g": return round(substr($memString, 0, -1)*1024*1024*1024); + default: return round($memString); + } +} + ?> diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php index 8e51f80c4..03c8c10ed 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -1298,13 +1298,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid //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"); $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone"); @@ -1317,6 +1310,13 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS"); $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)"); $editorsOptionsField->setSource($editorsOptionsSource); + + if(!Permission::check('SITETREE_GRANT_ACCESS')) { + $fields->makeFieldReadonly($viewersOptionsField); + $fields->makeFieldReadonly($viewerGroupsField); + $fields->makeFieldReadonly($editorsOptionsField); + $fields->makeFieldReadonly($editorGroupsField); + } $tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content")); $tabMain->setTitle(_t('SiteTree.TABMAIN', "Main")); diff --git a/core/model/fieldtypes/HTMLText.php b/core/model/fieldtypes/HTMLText.php index 16b7402b3..eb074d28c 100755 --- a/core/model/fieldtypes/HTMLText.php +++ b/core/model/fieldtypes/HTMLText.php @@ -23,62 +23,90 @@ class HTMLText extends Text { } /** - * Create a summary of the content. This will either be the first paragraph, or the first $maxWords - * words, whichever is shorter + * Create a summary of the content. This will be some section of the first paragraph, limited by + * $maxWords. All internal tags are stripped out - the return value is a string + * + * This is sort of the HTML aware equivilent to Text#Summary, although the logic for summarising is not exactly the same + * + * @param int $maxWords Maximum number of words to return - may return less, but never more. Pass -1 for no limit + * @param int $flex Number of words to search through when looking for a nice cut point + * @param string $add What to add to the end of the summary if we cut at a less-than-ideal cut point + * @return string A nice(ish) summary with no html tags (but possibly still some html entities) + * + * @see sapphire/core/model/fieldtypes/Text#Summary($maxWords) */ - public function Summary( $maxWords = 50 ) { - // split the string into tags and words - $parts = Convert::xml2array( $this->value ); - - // store any unmatched tags - $tagStack = array(); - - $pIndex = 0; - - // find the first paragraph tag - for( $i = 0; $i < count( $parts ); $i++ ) - if( strpos( $parts[$i], '' ) { - $summary .= $parts[$pIndex]; - break; - } - elseif( preg_match( '/<\/(\w+)>/', $parts[$pIndex], $endTag ) && $endTag[1] == substr( $tagStack[count($tagStack) - 1], 1, strlen( $endTag[1] ) ) ) { - array_pop( $tagStack ); - $words++; - $summary .= $parts[$pIndex++]; - } elseif( preg_match( '/^<\w+/', $parts[$pIndex] ) ) { - array_push( $tagStack, $parts[$pIndex] ); - $words++; - $summary .= $parts[$pIndex++]; - } else - $summary .= $parts[$pIndex++] . ' '; - } - - // Tags that shouldn't be closed - $noClose = array("br", "img"); - - // make sure that the summary is well formed XHTML by closing tags - while( $openTag = array_pop( $tagStack ) ) { - preg_match( '/^<(\w+)\s+/', $openTag, $tagName ); - if(sizeof($tagName) > 0) { - if(!in_array($tagName[1], $noClose)) { - $summary .= ""; - } + public function Summary($maxWords = 50, $flex = 15, $add = '...') { + $str = false; + + /* First we need the text of the first paragraph, without tags. Try using SimpleXML first */ + if (class_exists('SimpleXMLElement')) { + $doc = new DOMDocument(); + + /* Catch warnings thrown by loadHTML and turn them into a failure boolean rather than a SilverStripe error */ + set_error_handler(create_function('$no, $str', 'throw new Exception("HTML Parse Error: ".$str);'), E_ALL); + try { $res = $doc->loadHTML('' . $this->value); } + catch (Exception $e) { $res = false; } + restore_error_handler(); + + if ($res) { + $xml = simplexml_import_dom($doc); + $res = $xml->xpath('//p'); + if (!empty($res)) $str = strip_tags($res[0]->asXML()); } } - return $summary; + /* If that failed, most likely the passed HTML is broken. use a simple regex + a custom more brutal strip_tags. We don't use strip_tags because + * that does very badly on broken HTML*/ + if (!$str) { + /* See if we can pull a paragraph out*/ + if (preg_match('{]*)?>(.*[A-Za-z]+.*)

}', $this->value, $matches)) $str = $matches[2]; + /* If _that_ failed, just use the whole text */ + else $str = $this->value; + + /* Now pull out all the html-alike stuff */ + $str = preg_replace('{]*>}', '', $str); /* Take out anything that is obviously a tag */ + $str = preg_replace('{}', '', $str); /* Strip out any left over looking bits. Textual < or > should already be encoded to < or > */ + } + + /* Now split into words. If we are under the maxWords limit, just return the whole string (re-implode for whitespace normalization) */ + $words = preg_split('/\s+/', $str); + if ($maxWords == -1 || count($words) <= $maxWords) return implode(' ', $words); + + /* Otherwise work backwards for a looking for a sentence ending (we try to avoid abbreviations, but aren't very good at it) */ + for ($i = $maxWords; $i >= $maxWords - $flex && $i >= 0; $i--) { + if (preg_match('/\.$/', $words[$i]) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $words[$i])) { + return implode(' ', array_slice($words, 0, $i+1)); + } + } + + /* If we didn't find a sentence ending quickly enough, just cut at the maxWords point and add '...' to the end */ + return implode(' ', array_slice($words, 0, $maxWords)) . $add; } + /** + * Returns the first sentence from the first paragraph. If it can't figure out what the first paragraph is (or there isn't one) + * it returns the same as Summary() + * + * This is the HTML aware equivilent to Text#FirstSentence + * + * @see sapphire/core/model/fieldtypes/Text#FirstSentence() + */ + function FirstSentence() { + /* Use summary's html processing logic to get the first paragraph */ + $paragraph = $this->Summary(-1); + + /* Then look for the first sentence ending. We could probably use a nice regex, but for now this will do */ + $words = preg_split('/\s+/', $paragraph); + foreach ($words as $i => $word) { + if (preg_match('/\.$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) { + return implode(' ', array_slice($words, 0, $i+1)); + } + } + + /* If we didn't find a sentence ending, use the summary. We re-call rather than using paragraph so that Summary will limit the result this time */ + return $this->Summary(); + } + public function scaffoldFormField($title = null, $params = null) { return new HtmlEditorField($this->name, $title); } diff --git a/dev/BulkLoader.php b/dev/BulkLoader.php index 77613497d..6b581ac23 100644 --- a/dev/BulkLoader.php +++ b/dev/BulkLoader.php @@ -130,7 +130,9 @@ abstract class BulkLoader extends ViewableData { */ public function load($filepath, $memory_limit='512M') { ini_set('max_execution_time', 3600); - ini_set('memory_limit', $memory_limit); + + increase_memory_limit_to($memory_limit); + return $this->processAll($filepath); } diff --git a/dev/Debug.php b/dev/Debug.php index e0b40d2b4..d065076d3 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -646,14 +646,14 @@ function errorHandler($errno, $errstr, $errfile, $errline) { case E_ERROR: case E_CORE_ERROR: case E_USER_ERROR: - Debug::fatalHandler($errno, $errstr, $errfile, $errline, $bt); + Debug::fatalHandler($errno, $errstr, $errfile, $errline, null); break; case E_NOTICE: case E_WARNING: case E_CORE_WARNING: case E_USER_WARNING: - Debug::warningHandler($errno, $errstr, $errfile, $errline, $bt); + Debug::warningHandler($errno, $errstr, $errfile, $errline, null); break; } diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php index a103b18bc..506902b71 100644 --- a/dev/DevelopmentAdmin.php +++ b/dev/DevelopmentAdmin.php @@ -63,6 +63,7 @@ class DevelopmentAdmin extends Controller { function index() { $actions = array( "build" => "Build/rebuild this environment (formerly db/build). Call this whenever you have updated your project sources", + "reset" => "Reset this environment - truncate the database and rebuild. This is useful after testing to start with a fresh working copy", "tests" => "See a list of unit tests to run", "tests/all" => "Run all tests", "jstests" => "See a list of JavaScript tests to run", @@ -79,15 +80,7 @@ class DevelopmentAdmin extends Controller { $renderer = new DebugView(); $renderer->writeHeader(); - - // Todo: the database info could be encapsulated better - global $databaseConfig; - $summaryInfo = "Base URL: " . Director::absoluteBaseURL() . "\n" - . "Document root: " . Director::baseFolder() . "\n" - . "Database: " . DB::getConn()->class . ", database " . DB::getConn()->currentDatabase() - . " on " . $databaseConfig['server']; - - $renderer->writeInfo("Sapphire Development Tools", $summaryInfo); + $renderer->writeInfo("Sapphire Development Tools", Director::absoluteBaseURL()); $base = Director::baseURL(); echo '
    '; @@ -129,16 +122,102 @@ class DevelopmentAdmin extends Controller { } function build() { - $renderer = new DebugView(); - $renderer->writeHeader(); - $renderer->writeInfo("Environment Builder (formerly db/build)", Director::absoluteBaseURL()); - echo "
    "; + if(Director::is_cli()) { + $da = new DatabaseAdmin(); + $da->build(); + } else { + $renderer = new DebugView(); + $renderer->writeHeader(); + $renderer->writeInfo("Environment Builder (formerly db/build)", Director::absoluteBaseURL()); + echo "
    "; + $da = new DatabaseAdmin(); + $da->build(); + + echo "
    "; + $renderer->writeFooter(); + } + } + + function reset() { + global $databaseConfig; + $databaseName = $databaseConfig['database']; + + if(Director::is_cli()) { + echo "\nPlease run dev/reset from your web browser.\n"; + } else { + $renderer = new DebugView(); + $renderer->writeHeader(); + $renderer->writeInfo('Database reset', 'Completely truncate and rebuild the current database'); + echo '
    '; + + if(isset($_GET['done'])) { + echo "

    $databaseName has been completely truncated and rebuilt.

    "; + echo "

    Note: If you had SS_DEFAULT_ADMIN_USERNAME and SS_DEFAULT_ADMIN_PASSWORD + defined in your _ss_environment.php file, a default admin Member record has been created + with those credentials.

    "; + } else { + echo $this->ResetForm()->renderWith('Form'); + } + + echo '
    '; + $renderer->writeFooter(); + } + } + + function ResetForm() { + global $databaseConfig; + $databaseName = $databaseConfig['database']; + + if(!Session::get('devResetRandNumber')) { + $rand = rand(5,500); + Session::set('devResetRandNumber', $rand); + } else { + $rand = Session::get('devResetRandNumber'); + } + + $form = new Form( + $this, + 'ResetForm', + new FieldSet( + new LiteralField('ResetWarning', "

    WARNING: This will completely + destroy ALL existing data in $databaseName!   Press the button below to + confirm this action.

    "), + new HiddenField('devResetRandNumber', '', $rand) + ), + new FieldSet( + new FormAction('doReset', 'Reset and completely rebuild the database') + ) + ); + + $form->setFormAction(Director::absoluteBaseURL() . 'dev/ResetForm'); + + return $form; + } + + function doReset($data, $form, $request) { + if(!isset($data['devResetRandNumber'])) { + Director::redirectBack(); + return false; + } + + // Avoid accidental database resets by checking the posted number to the one in session + if(Session::get('devResetRandNumber') != $data['devResetRandNumber']) { + Director::redirectBack(); + return false; + } + $da = new DatabaseAdmin(); + $da->clearAllData(); + + // If _ss_environment.php has some constants set for default admin, set these up in the request + $_REQUEST['username'] = defined('SS_DEFAULT_ADMIN_USERNAME') ? SS_DEFAULT_ADMIN_USERNAME : null; + $_REQUEST['password'] = defined('SS_DEFAULT_ADMIN_PASSWORD') ? SS_DEFAULT_ADMIN_PASSWORD : null; + $da->build(); - echo "
    "; - $renderer->writeFooter(); + Session::clear('devResetRandNumber'); + Director::redirect(Director::absoluteBaseURL() . 'dev/reset?done=1'); } function errors() { @@ -149,5 +228,4 @@ class DevelopmentAdmin extends Controller { return new CodeViewer(); } } - ?> \ No newline at end of file diff --git a/forms/DMYDateField.php b/forms/DMYDateField.php index dc194e3aa..26c6a65b2 100644 --- a/forms/DMYDateField.php +++ b/forms/DMYDateField.php @@ -70,7 +70,7 @@ HTML; { $validator->validationError( $this->name, - _t('DMYDateField.VALIDDATEFORMAT', "Please enter a valid date format (DD-MM-YYYY)."), + _t('DMYDateField.VALIDDATEFORMAT', "Please enter a valid date format (DD-MM-YYYY)."), "validation", false ); diff --git a/forms/TextareaField.php b/forms/TextareaField.php index 3f68b8866..5f75a8612 100755 --- a/forms/TextareaField.php +++ b/forms/TextareaField.php @@ -47,7 +47,7 @@ class TextareaField extends FormField { return $this->createTag( 'span', $attributes, - (($this->value) ? htmlentities($this->value) : '(' . _t('FormField.NONE', 'none') . ')') + (($this->value) ? nl2br(htmlentities($this->value, ENT_COMPAT, 'UTF-8')) : '(' . _t('FormField.NONE', 'none') . ')') ); } else { $attributes = array( @@ -60,7 +60,7 @@ class TextareaField extends FormField { if($this->disabled) $attributes['disabled'] = 'disabled'; - return $this->createTag('textarea', $attributes, htmlentities($this->value)); + return $this->createTag('textarea', $attributes, htmlentities($this->value, ENT_COMPAT, 'UTF-8')); } } diff --git a/forms/TreeMultiselectField.php b/forms/TreeMultiselectField.php index 0fa7bb735..25f26457f 100755 --- a/forms/TreeMultiselectField.php +++ b/forms/TreeMultiselectField.php @@ -85,16 +85,25 @@ HTML; * Changes this field to the readonly field. */ function performReadonlyTransformation() { + return new TreeMultiselectField_Readonly($this->name, $this->title, $this->sourceObject, $this->keyField, $this->labelField); + } +} + +class TreeMultiselectField_Readonly extends TreeMultiselectField { + + protected $readonly = true; + + function Field() { $titleArray = array(); $titleList = array(); if($items = $this->getItems()) { foreach($items as $item) $titleArray[] = $item->Title; if($titleArray) $titleList = implode(", ", $titleArray); } - + $field = new ReadonlyField($this->name, $this->title); $field->setValue($titleList); $field->setForm($this->form); - return $field; + return $field->Field(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/tests/MemoryLimitTest.php b/tests/MemoryLimitTest.php new file mode 100644 index 000000000..a8907c29e --- /dev/null +++ b/tests/MemoryLimitTest.php @@ -0,0 +1,38 @@ +assertEquals('128M', ini_get('memory_limit')); + + // But not down + increase_memory_limit_to('64M'); + $this->assertEquals('128M', ini_get('memory_limit')); + + // Test the different kinds of syntaxes + increase_memory_limit_to(1024*1024*200); + $this->assertEquals(1024*1024*200, ini_get('memory_limit')); + + increase_memory_limit_to('409600K'); + $this->assertEquals('409600K', ini_get('memory_limit')); + + increase_memory_limit_to('1G'); + $this->assertEquals('1G', ini_get('memory_limit')); + } + + + /////////////////// + + private $origLimit; + + function setUp() { + $this->origLimit = ini_get('memory_limit'); + } + function tearDown() { + ini_set('memory_limit', $this->origLimit); + } +} \ No newline at end of file diff --git a/tests/fieldtypes/HTMLTextTest.php b/tests/fieldtypes/HTMLTextTest.php index a949478be..9303ba6bb 100644 --- a/tests/fieldtypes/HTMLTextTest.php +++ b/tests/fieldtypes/HTMLTextTest.php @@ -21,5 +21,86 @@ class HTMLTextTest extends SapphireTest { } } + function testSummaryBasics() { + $cases = array( + '

    Should not take header

    Should take paragraph

    ' => 'Should take paragraph', + '

    Should strip tags, but leave text

    ' => 'Should strip tags, but leave text', + '

    Unclosed tags
    should not phase it

    ' => 'Unclosed tags should not phase it', + '

    Second paragraph

    should not cause errors or appear in output

    ' => 'Second paragraph' + ); + + foreach($cases as $originalValue => $expectedValue) { + $textObj = new HTMLText('Test'); + $textObj->setValue($originalValue); + $this->assertEquals($expectedValue, $textObj->Summary()); + } + } + + function testSummaryLimits() { + $cases = array( + '

    A long paragraph should be cut off if limit is set

    ' => 'A long paragraph should be...', + '

    No matter how many tags are in it

    ' => 'No matter how many tags...', + '

    A sentence is. nicer than hard limits

    ' => 'A sentence is.', + '

    But not. If it\'s too short

    ' => 'But not. If it\'s too...' + ); + + foreach($cases as $originalValue => $expectedValue) { + $textObj = new HTMLText('Test'); + $textObj->setValue($originalValue); + $this->assertEquals($expectedValue, $textObj->Summary(5, 3, '...')); + } + } + + function testSummaryEndings() { + $cases = array( + '...', ' -> more', '' + ); + + $orig = '

    Cut it off, cut it off

    '; + $match = 'Cut it off, cut'; + + foreach($cases as $add) { + $textObj = new HTMLText(); + $textObj->setValue($orig); + $this->assertEquals($match.$add, $textObj->Summary(4, 0, $add)); + } + } + + function testSummaryFlexTooBigShouldNotCauseError() { + $orig = '

    Cut it off, cut it off

    '; + $match = 'Cut it off, cut'; + + $textObj = new HTMLText(); + $textObj->setValue($orig); + $this->assertEquals($match, $textObj->Summary(4, 10, '')); + } + + function testSummaryInvalidHTML() { + $cases = array( + 'It\'s got a tag, but

    This doesn\'t make < >' => 'This doesn\'t make any', + 'This doesn\'t should ignore

    Sentence with {$many}words. Second sentence.

    " => "Sentence with {$many}words.", + ); + + foreach($cases as $orig => $match) { + $textObj = new HTMLText(); + $textObj->setValue($orig); + $this->assertEquals($match, $textObj->FirstSentence()); + } + } } ?> \ No newline at end of file diff --git a/tests/forms/TextareaFieldTest.php b/tests/forms/TextareaFieldTest.php new file mode 100644 index 000000000..12018fbeb --- /dev/null +++ b/tests/forms/TextareaFieldTest.php @@ -0,0 +1,46 @@ + +What's on a new-line? +These are some unicodes: äöü&<>"; + + $field = new TextareaField("Test", "Test", 5, 20); + $field->setValue($inputText); + + $this->assertEquals(<<This is my <text> +What's on a new-line? +These are some unicodes: äöü&<> +HTML + , $field->Field()); + } + + /** + * Quick smoke test to ensure that text is being encoded properly in readonly fields. + */ + function testReadonlyTextEncoding() { + $inputText = "This is my +What's on a new-line? +These are some unicodes: äöü&<>"; + + $field = new TextareaField("Test", "Test", 5, 20); + $field = $field->performReadonlyTransformation(); + + // Make sure that the field is smart enough to have its value set after being made readonly + $field->setValue($inputText); + + $this->assertEquals(<<This is my <text>
    +What's on a new-line?
    +These are some unicodes: äöü&<> +HTML + , $field->Field()); + } + +} \ No newline at end of file