Merged from branches/2.3

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@79438 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sean Harvey 2009-06-17 11:36:49 +00:00
parent 73e43c27ea
commit 6c754d29e9
12 changed files with 389 additions and 101 deletions

View File

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

View File

@ -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");
@ -1318,6 +1311,13 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$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"));
$tabMeta->setTitle(_t('SiteTree.TABMETA', "Metadata"));

View File

@ -23,60 +23,88 @@ 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 );
public function Summary($maxWords = 50, $flex = 15, $add = '...') {
$str = false;
// store any unmatched tags
$tagStack = array();
/* First we need the text of the first paragraph, without tags. Try using SimpleXML first */
if (class_exists('SimpleXMLElement')) {
$doc = new DOMDocument();
$pIndex = 0;
/* 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('<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>' . $this->value); }
catch (Exception $e) { $res = false; }
restore_error_handler();
// find the first paragraph tag
for( $i = 0; $i < count( $parts ); $i++ )
if( strpos( $parts[$i], '<p' ) === 0 ) {
$pIndex = $i;
break;
}
$summary = '';
$words = 0;
// create the summary, keeping track of opening and closing tags
while( $words <= $maxWords && $pIndex < count( $parts ) ) {
if( $parts[$pIndex] == '</p>' ) {
$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 .= "</{$tagName[1]}>";
if ($res) {
$xml = simplexml_import_dom($doc);
$res = $xml->xpath('//p');
if (!empty($res)) $str = strip_tags($res[0]->asXML());
}
}
/* 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('{<p(\s[^<>]*)?>(.*[A-Za-z]+.*)</p>}', $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('{</?[a-zA-Z]+[^<>]*>}', '', $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 &lt; or &gt; */
}
return $summary;
/* 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) {

View File

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

View File

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

View File

@ -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 '<div class="options"><ul>';
@ -129,6 +122,10 @@ class DevelopmentAdmin extends Controller {
}
function build() {
if(Director::is_cli()) {
$da = new DatabaseAdmin();
$da->build();
} else {
$renderer = new DebugView();
$renderer->writeHeader();
$renderer->writeInfo("Environment Builder (formerly db/build)", Director::absoluteBaseURL());
@ -140,6 +137,88 @@ class DevelopmentAdmin extends Controller {
echo "</div>";
$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 '<div style="margin: 0 2em">';
if(isset($_GET['done'])) {
echo "<p style=\"color: green\"><b>$databaseName</b> has been completely truncated and rebuilt.</p>";
echo "<p>Note: If you had <i>SS_DEFAULT_ADMIN_USERNAME</i> and <i>SS_DEFAULT_ADMIN_PASSWORD</i>
defined in your <b>_ss_environment.php</b> file, a default admin Member record has been created
with those credentials.</p>";
} else {
echo $this->ResetForm()->renderWith('Form');
}
echo '</div>';
$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', "<p style=\"color: red\">WARNING: This will completely
destroy ALL existing data in <b>$databaseName</b>! &nbsp; Press the button below to
confirm this action.</p>"),
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();
Session::clear('devResetRandNumber');
Director::redirect(Director::absoluteBaseURL() . 'dev/reset?done=1');
}
function errors() {
Director::redirect("Debug_");
@ -149,5 +228,4 @@ class DevelopmentAdmin extends Controller {
return new CodeViewer();
}
}
?>

View File

@ -47,7 +47,7 @@ class TextareaField extends FormField {
return $this->createTag(
'span',
$attributes,
(($this->value) ? htmlentities($this->value) : '<i>(' . _t('FormField.NONE', 'none') . ')</i>')
(($this->value) ? nl2br(htmlentities($this->value, ENT_COMPAT, 'UTF-8')) : '<i>(' . _t('FormField.NONE', 'none') . ')</i>')
);
} 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'));
}
}

View File

@ -85,6 +85,15 @@ 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()) {
@ -95,6 +104,6 @@ HTML;
$field = new ReadonlyField($this->name, $this->title);
$field->setValue($titleList);
$field->setForm($this->form);
return $field;
return $field->Field();
}
}

38
tests/MemoryLimitTest.php Normal file
View File

@ -0,0 +1,38 @@
<?php
class MemoryLimitTest extends SapphireTest {
function testIncreaseMemoryLimitTo() {
ini_set('memory_limit', '64M');
// It can go up
increase_memory_limit_to('128M');
$this->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);
}
}

View File

@ -21,5 +21,86 @@ class HTMLTextTest extends SapphireTest {
}
}
function testSummaryBasics() {
$cases = array(
'<h1>Should not take header</h1><p>Should take paragraph</p>' => 'Should take paragraph',
'<p>Should strip <b>tags, but leave</b> text</p>' => 'Should strip tags, but leave text',
'<p>Unclosed tags <br>should not phase it</p>' => 'Unclosed tags should not phase it',
'<p>Second paragraph</p><p>should not cause errors or appear in output</p>' => 'Second paragraph'
);
foreach($cases as $originalValue => $expectedValue) {
$textObj = new HTMLText('Test');
$textObj->setValue($originalValue);
$this->assertEquals($expectedValue, $textObj->Summary());
}
}
function testSummaryLimits() {
$cases = array(
'<p>A long paragraph should be cut off if limit is set</p>' => 'A long paragraph should be...',
'<p>No matter <i>how many <b>tags</b></i> are in it</p>' => 'No matter how many tags...',
'<p>A sentence is. nicer than hard limits</p>' => 'A sentence is.',
'<p>But not. If it\'s too short</p>' => '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 = '<p>Cut it off, cut it off</p>';
$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 = '<p>Cut it off, cut it off</p>';
$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 <p<> tag, but<p junk true>This doesn\'t <a id="boo">make</b class="wa"> < ><any< sense</p>' => 'This doesn\'t make any',
'This doesn\'t <a style="much horray= true>even</b> < ><have< a <i>p tag' => 'This doesn\'t even have'
);
foreach($cases as $orig => $match) {
$textObj = new HTMLText();
$textObj->setValue($orig);
$this->assertEquals($match, $textObj->Summary(4, 0, ''));
}
}
function testFirstSentence() {
$many = str_repeat('many ', 100);
$cases = array(
'<h1>should ignore</h1><p>First sentence. Second sentence.</p>' => 'First sentence.',
'<h1>should ignore</h1><p>First Mr. sentence. Second sentence.</p>' => 'First Mr. sentence.',
"<h1>should ignore</h1><p>Sentence with {$many}words. Second sentence.</p>" => "Sentence with {$many}words.",
);
foreach($cases as $orig => $match) {
$textObj = new HTMLText();
$textObj->setValue($orig);
$this->assertEquals($match, $textObj->FirstSentence());
}
}
}
?>

View File

@ -0,0 +1,46 @@
<?php
class TextareaFieldTest extends SapphireTest {
/**
* Quick smoke test to ensure that text is being encoded properly.
*/
function testTextEncoding() {
$inputText = "This is my <text>
What's on a new-line?
These are some unicodes: äöü&<>";
$field = new TextareaField("Test", "Test", 5, 20);
$field->setValue($inputText);
$this->assertEquals(<<<HTML
<textarea id="Test" name="Test" rows="5" cols="20">This is my &lt;text&gt;
What's on a new-line?
These are some unicodes: &auml;&ouml;&uuml;&amp;&lt;&gt;</textarea>
HTML
, $field->Field());
}
/**
* Quick smoke test to ensure that text is being encoded properly in readonly fields.
*/
function testReadonlyTextEncoding() {
$inputText = "This is my <text>
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(<<<HTML
<span id="Test" class="readonly" name="Test" readonly="readonly">This is my &lt;text&gt;<br />
What's on a new-line?<br />
These are some unicodes: &auml;&ouml;&uuml;&amp;&lt;&gt;</span>
HTML
, $field->Field());
}
}