Merging in refactored Translatable architecture from trunk, including related/required changesets like enhancements to Object static handling (see details below)

------------------------------------------------------------------------
r68900 | sminnee | 2008-12-15 14:30:41 +1300 (Mon, 15 Dec 2008) | 1 line

Static caching merges from dnc branch
------------------------------------------------------------------------
r68917 | sminnee | 2008-12-15 14:49:06 +1300 (Mon, 15 Dec 2008) | 1 line

Merged Requirements fix from nestedurls branch
------------------------------------------------------------------------
r70033 | aoneil | 2009-01-13 14:03:41 +1300 (Tue, 13 Jan 2009) | 2 lines

Add translation migration task

------------------------------------------------------------------------
r70072 | ischommer | 2009-01-13 17:34:27 +1300 (Tue, 13 Jan 2009) | 5 lines

API CHANGE Removed obsolete internal Translatable methods: hasOwnTranslatableFields(), allFieldsInTable()
ENHANCEMENT Removed $create flag in Translatable::getTranslation() and replaced with explit action createTranslation()
ENHANCEMENT Sorting return array of Translatable::getTranslatedLangs()
ENHANCEMENT Added a note about saving a page before creating a translation
MINOR Added phpdoc to Translatable
------------------------------------------------------------------------
r70073 | ischommer | 2009-01-13 17:34:45 +1300 (Tue, 13 Jan 2009) | 1 line

ENHANCEMENT Added basic unit tests to new Translatable API
------------------------------------------------------------------------
r70080 | aoneil | 2009-01-13 18:04:21 +1300 (Tue, 13 Jan 2009) | 3 lines

BUGFIX: Fix translatable migration regenerating URLSegments when it shouldn't
BUGFIX: Fix translatable migration not writing records to Live properly

------------------------------------------------------------------------
r70118 | ischommer | 2009-01-14 11:28:24 +1300 (Wed, 14 Jan 2009) | 3 lines

API CHANGE Removed obsolete Translatable::table_exists()
ENHANCEMENT Made Translatable constructor arguments optional, as by default all database fields are marked translatable
MINOR More unit tests for Translatable
------------------------------------------------------------------------
r70138 | ischommer | 2009-01-14 17:00:30 +1300 (Wed, 14 Jan 2009) | 1 line

BUGFIX Disabled assumption that SQLQuery->filtersOnID() should only kick in when exactly one WHERE clause is given - this is very fragile and hard to test. It would return TRUE on $where = "SiteTree.ID = 5", but not on $where = array("Lang = 'de'", "SiteTree.ID = 5")
------------------------------------------------------------------------
r70214 | ischommer | 2009-01-15 18:56:25 +1300 (Thu, 15 Jan 2009) | 3 lines

BUGFIX Falling back to Translatable::current_lang() if no $context object is given, in augmentAllChildrenIncludingDeleted() and AllChildrenIncludingDeleted()
MINOR phpdoc for Translatable
MINOR Added more Translatable unit tests
------------------------------------------------------------------------
r70306 | ischommer | 2009-01-16 17:14:34 +1300 (Fri, 16 Jan 2009) | 9 lines

ENHANCEMENT Recursively creating translations for parent pages to ensure that a translated page is still accessible by traversing the tree, e.g. in "cms translation mode" (in Translatable->onBeforeWrite())
ENHANCEMENT Simplified AllChildrenIncludingDeleted() to not require a special augmentAllChildrenIncludingDeleted() implementation: We don't combine untranslated/translated children any longer (which was used in CMS tree view), but rather just show translated records
ENHANCEMENT Ensuring uniqueness of URL segments by appending "-<langcode>" to new translations (in Translatable->onBeforeWrite())
ENHANCEMENT Added Translatable->alternateGetByUrl() as a hook into SiteTree::get_by_url()
ENHANCEMENT Adding link back to original page in CMS editform for translations
BUGFIX Excluding HiddenField instances from Translatable->updateCMSFields()
BUGFIX Don't require a record to be written (through exists()) when checking Translatable->isTranslation() or Translatable->hasTranslation()
MINOR Don't use createMethod() shortcut for Translatable->AllChildrenIncludingDeleted()
MINOR Added Translatable unit tests
------------------------------------------------------------------------
r70318 | ischommer | 2009-01-19 11:46:16 +1300 (Mon, 19 Jan 2009) | 1 line

BUGFIX Reverted special cases for Translatable in Versioned->canBeVersioned() (originally committed in r42119) - was checking for existence of underscores in table names as an indication of the "_lang" suffix, which is no longer needed. It was also a flawed assumption which tripped over classes like TranslatableTest_TestPage
------------------------------------------------------------------------
r70319 | ischommer | 2009-01-19 11:47:02 +1300 (Mon, 19 Jan 2009) | 1 line

ENHANCEMENT Disabled Translatab-e>augmentWrite() - was only needed for the blacklist fields implementation which is inactive for the moment
------------------------------------------------------------------------
r70326 | ischommer | 2009-01-19 14:25:23 +1300 (Mon, 19 Jan 2009) | 2 lines

ENHANCEMENT Making ErrorPage static HTML files translatable (#2233)
ENHANCEMENT Added ErrorPage::$static_filepath to flexibly set location of static error pages (defaults to /assets)
------------------------------------------------------------------------
r70327 | ischommer | 2009-01-19 15:18:41 +1300 (Mon, 19 Jan 2009) | 1 line

FEATURE Enabled specifying a language through a hidden field in SearchForm which limits the search to pages in this language (incl. unit tests)
------------------------------------------------------------------------
r71258 | sharvey | 2009-02-03 15:49:34 +1300 (Tue, 03 Feb 2009) | 2 lines

BUGFIX: Fix translatable being enabled when it shouldn't be

------------------------------------------------------------------------
r71340 | ischommer | 2009-02-04 14:36:12 +1300 (Wed, 04 Feb 2009) | 1 line

BUGFIX Including Hierarchy->children in flushCache() and renamed to _cache_children. This caused problems in TranslatableTest when re-using the same SiteTree->Children() method with different languages on the same object (even with calling flushCache() inbetween the calls)
------------------------------------------------------------------------
r71567 | gmunn | 2009-02-10 13:49:16 +1300 (Tue, 10 Feb 2009) | 1 line

'URLSegment' on line 484 and 494 now escaped
------------------------------------------------------------------------
r72054 | ischommer | 2009-02-23 10:30:41 +1300 (Mon, 23 Feb 2009) | 3 lines

BUGFIX Fixed finding a translated homepage without an explicit URLSegment (e.g. http://mysite.com/?lang=de) - see #3540
ENHANCEMENT Added Translatable::get_homepage_urlsegment_by_language()
ENHANCEMENT Added RootURLController::get_default_homepage_urlsegment()
------------------------------------------------------------------------
r72367 | ischommer | 2009-03-03 11:13:30 +1300 (Tue, 03 Mar 2009) | 2 lines

ENHANCEMENT Added i18n::get_lang_from_locale() and i18n::convert_rfc1766()
ENHANCEMENT Using IETF/HTTP compatible "long" language code in SiteTree->MetaTags(). This means the default <meta type="content-language..."> value will be "en-US" instead of "en". The locale can be either set through the Translatable content language, or through i18n::set_locale()
------------------------------------------------------------------------
r73036 | sminnee | 2009-03-14 13:16:32 +1300 (Sat, 14 Mar 2009) | 1 line

ENHANCEMENT #3032 ajshort: Use static methods for accessing static data
------------------------------------------------------------------------
r73059 | sminnee | 2009-03-15 14:09:59 +1300 (Sun, 15 Mar 2009) | 2 lines

ENHANCEMENT: Added Object::clearCache() to clear a cache
BUGFIX: Make object cache testing more robust
------------------------------------------------------------------------
r73338 | ischommer | 2009-03-19 05:13:40 +1300 (Thu, 19 Mar 2009) | 9 lines

API CHANGE Added concept of "translation groups" to Translatable- every page can belong to a group of related translations, rather than having an explicit "original", meaning you can have pages in "non-default" languages which have no representation in other language trees. This group is recorded in a new table "<classname>_translationgroups". Translatable->createTranslation() and Translatable->onBeforeWrite() will automatically associate records in this groups. Added Translatable->addTranslationGroup(), Translatable->removeTranslationGroup(), Translatable->getTranslationGroup()
API CHANGE Removed Translatable->isTranslation() - after the new "translation group" model, every page is potentially a translation
API CHANGE Translatable->findOriginalIDs(), Translatable->setOriginalPage(), Translatable->getOriginalPage()
ENHANCEMENT Translatable->getCMSFields() will now always show the "create translation" option, not only on default languages - meaning you can create translations based on other translations
ENHANCEMENT Translatable language dropdown in CMS will always show all available languages, rather than filtering by already existing translations
ENHANCEMENT Added check for an existing record in Translatable->createTranslation()
BUGFIX Removed Translatable->getLang() which overloaded the $db property - it was causing side effects during creation of SiteTree default records.
BUGFIX Added check in Translatable->augmentSQL() to avoid reapplying "Lang = ..." filter twice
BUGFIX Removed bypass in Translatable->AllChildrenIncludingDeleted()
------------------------------------------------------------------------
r73339 | ischommer | 2009-03-19 05:15:46 +1300 (Thu, 19 Mar 2009) | 1 line

BUGFIX Disabled "untranslated" CSS class for SiteTree elements - doesn't apply any longer with the new "translation groups" concept
------------------------------------------------------------------------
r73341 | ischommer | 2009-03-19 06:01:51 +1300 (Thu, 19 Mar 2009) | 1 line

BUGFIX Disabled auto-excluding of default language from the "available languages" array in LanguageDropdownField - due to the new "translation groups" its possible to have a translation from another language into the default language
------------------------------------------------------------------------
r73342 | ischommer | 2009-03-19 06:13:23 +1300 (Thu, 19 Mar 2009) | 4 lines

BUGFIX Setting ParentID of translated record if recursively creating parents in Translatable::onBeforeWrite()
BUGFIX Fixing inline form action for "create translation"
BUGFIX Removed link to "original page" for a translation - no longer valid
MINOR documentation for Translatable
------------------------------------------------------------------------
r73464 | ischommer | 2009-03-20 20:51:00 +1300 (Fri, 20 Mar 2009) | 1 line

MINOR documentation
------------------------------------------------------------------------
r73465 | ischommer | 2009-03-20 20:58:52 +1300 (Fri, 20 Mar 2009) | 1 line

BUGFIX Fixed Hierarchy->Children() testing in TranslatableTest - with the new datamodel you can't call Children() in a different language regardless of Translatable::set_reading_lang(), the Children() call has to be made from a parent in the same language
------------------------------------------------------------------------
r73466 | ischommer | 2009-03-20 21:36:40 +1300 (Fri, 20 Mar 2009) | 2 lines

ENHANCEMENT Added Translatable::get_locale_from_lang(), Translatable::get_common_locales(), $common_locales and $likely_subtags in preparation to switch Translatable from using short "lang" codes to proper long locales
API CHANGE Deprecated Translatable::set_default_lang(), Translatable::default_lang()
------------------------------------------------------------------------
r73467 | ischommer | 2009-03-20 21:38:57 +1300 (Fri, 20 Mar 2009) | 1 line

ENHANCEMENT Supporting "Locale-English" and "Locale-Native" as listing arguments in LanguageDropdownField
------------------------------------------------------------------------
r73468 | ischommer | 2009-03-20 21:47:06 +1300 (Fri, 20 Mar 2009) | 7 lines

ENHANCEMENT Adjusted SearchForm, Debug, ErrorPage, SiteTree to using locales instead of lang codes
API CHANGE Changed Translatable datamodel to use locales ("en_US") instead of lang values ("en).
API CHANGE Changed Translatable::$default_lang to $default_locale, Translatable::$reading_lang to $reading_locale
API CHANGE Using "locale" instead of "lang" in Translatable::choose_site_lang() to auto-detect language from cookies or GET parameters
API CHANGE Deprecated Translatable::is_default_lang(), set_default_lang(), get_default_lang(), current_lang(), set_reading_lang(), get_reading_lang(), get_by_lang(), get_one_by_lang()
API CHANGE Removed Translatable::get_original() - with the new "translation groups" concept there no longer is an original for a translation
BUGFIX Updated MigrateTranslatableTask to new Locale based datamodel
------------------------------------------------------------------------
r73470 | ischommer | 2009-03-20 21:56:57 +1300 (Fri, 20 Mar 2009) | 1 line

MINOR fixed typo
------------------------------------------------------------------------
r73472 | sminnee | 2009-03-21 17:30:04 +1300 (Sat, 21 Mar 2009) | 1 line

BUGFIX: Fixed translatable test execution by making protected methods public
------------------------------------------------------------------------
r73473 | sminnee | 2009-03-21 18:10:05 +1300 (Sat, 21 Mar 2009) | 1 line

ENHANCEMENT: Added Object::combined_static(), which gets all values of a static property from each class in the hierarchy
------------------------------------------------------------------------
r73883 | ischommer | 2009-04-01 08:32:19 +1300 (Wed, 01 Apr 2009) | 1 line

BUGFIX Making $_SINGLETONS a global instead of a static in Core.php so it can be re-used in other places
------------------------------------------------------------------------
r73951 | ischommer | 2009-04-02 05:35:32 +1300 (Thu, 02 Apr 2009) | 3 lines

API CHANGE Deprecated Translatable::enable() and i18n::enable()- use Object::add_extension('SiteTree','Translatable'), Deprecated Translatable::disable() and i18n::disable() - use Object::remove_extension('SiteTree','Translatable'), Deprecated Translatable::enabled() - use $myPage->hasExtension('Translatable')
API CHANGE Removed Translatable::creating_from() - doesn't apply any longer
ENHANCEMENT Translatable extension is no longer hooked up to SiteTree by default, which should improve performance and memory usage for sites not using Translatable. Please use Object::add_extension('SiteTree','Translatable') in your _config.php instead. Adjusted several classes (Image, ErrorPage, RootURLController) to the new behaviour.
------------------------------------------------------------------------
r73882 | ischommer | 2009-04-01 08:31:21 +1300 (Wed, 01 Apr 2009) | 1 line

ENHANCEMENT Added DataObjectDecorator->setOwner()
------------------------------------------------------------------------
r73884 | ischommer | 2009-04-01 08:32:51 +1300 (Wed, 01 Apr 2009) | 1 line

ENHANCEMENT Added Extension::get_classname_without_arguments()
------------------------------------------------------------------------
r73900 | ischommer | 2009-04-01 11:27:53 +1300 (Wed, 01 Apr 2009) | 7 lines

API CHANGE Deprecated Object->extInstance(), use getExtensionInstance() instead
ENHANCEMENT Added Object->getExtensionInstances()
ENHANCEMENT Added Object::get_extensions()
ENHANCEMENT Unsetting class caches when using Object::add_extension() to avoid problems with defineMethods etc.
BUGFIX Fixed extension comparison with case sensitivity and stripping arguments in Object::has_extension()
BUGFIX Unsetting all cached singletons in Object::remove_extension() to avoid outdated extension_instances
MINOR Documentation in Object
------------------------------------------------------------------------
r74017 | ischommer | 2009-04-03 10:49:40 +1300 (Fri, 03 Apr 2009) | 1 line

ENHANCEMENT Improved deprecated fallbacks in Translatable by auto-converting short language codes to long locales and vice versa through i18n::get_lang_from_locale()/i18n::get_locale_from_lang()
------------------------------------------------------------------------
r74030 | ischommer | 2009-04-03 11:41:26 +1300 (Fri, 03 Apr 2009) | 1 line

MINOR Re-added Translatable::default_lang() for more graceful fallback to Translatable::default_locale()
------------------------------------------------------------------------
r74065 | ischommer | 2009-04-04 05:38:51 +1300 (Sat, 04 Apr 2009) | 1 line

BUGFIX Re-added Translatable->isTranslation() for more friendly deprecation (originally removed in r73338)
------------------------------------------------------------------------
r74069 | ischommer | 2009-04-04 09:43:01 +1300 (Sat, 04 Apr 2009) | 1 line

BUGFIX Fixed legacy handling of Translatable::enable(),Translatable::disable() and Translatable::is_enabled() - applying extension to SiteTree instead of Page to avoid datamodel clashes
------------------------------------------------------------------------
r74070 | ischommer | 2009-04-04 10:23:51 +1300 (Sat, 04 Apr 2009) | 1 line

API CHANGE Deprecated Translatable::choose_site_lang(), use choose_site_locale()
------------------------------------------------------------------------
r74941 | ischommer | 2009-04-22 15:22:09 +1200 (Wed, 22 Apr 2009) | 2 lines

ENHANCEMENT Adding SapphireTest::set_up_once() and SapphireTest::tear_down_once() for better test performance with state that just needs to be initialized once per test case (not per test method). Added new SapphireTestSuite to support this through PHPUnit.
ENHANCEMENT Using set_up_once() in TranslatableTest and TranslatableSearchFormTest for better test run performance
------------------------------------------------------------------------
r74942 | ischommer | 2009-04-22 15:24:50 +1200 (Wed, 22 Apr 2009) | 1 line

BUGFIX Fixed TranslatableSearchFormTest->setUp() method
------------------------------------------------------------------------
r73509 | ischommer | 2009-03-23 11:59:14 +1300 (Mon, 23 Mar 2009) | 1 line

MINOR phpdoc documentation
------------------------------------------------------------------------


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.3@74986 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2009-04-23 01:45:10 +00:00 committed by Sam Minnee
parent ced1f25a4e
commit 3469e4d22a
63 changed files with 3825 additions and 1400 deletions

View File

@ -1,10 +1,11 @@
<?php <?php
/** /**
* A DataFormatter object handles transformation of data from Sapphire model objects to a particular output format, and vice versa. * A DataFormatter object handles transformation of data from Sapphire model objects to a particular output format, and vice versa.
* This is most commonly used in developing RESTful APIs. * This is most commonly used in developing RESTful APIs.
*
* @package sapphire
* @subpackage formatters
*/ */
abstract class DataFormatter extends Object { abstract class DataFormatter extends Object {
/** /**

View File

@ -1,5 +1,8 @@
<?php <?php
/**
* @package sapphire
* @subpackage formatters
*/
class JSONDataFormatter extends DataFormatter { class JSONDataFormatter extends DataFormatter {
/** /**
* @todo pass this from the API to the data formatter somehow * @todo pass this from the API to the data formatter somehow

View File

@ -1,7 +1,9 @@
<?php <?php
/** /**
* Simple wrapper to allow access to the live site via REST * Simple wrapper to allow access to the live site via REST
*
* @package sapphire
* @subpackage integration
*/ */
class VersionedRestfulServer extends Controller { class VersionedRestfulServer extends Controller {
function handleRequest($request) { function handleRequest($request) {

View File

@ -1,5 +1,8 @@
<?php <?php
/**
* @package sapphire
* @subpackage formatters
*/
class XMLDataFormatter extends DataFormatter { class XMLDataFormatter extends DataFormatter {
/** /**
* @todo pass this from the API to the data formatter somehow * @todo pass this from the API to the data formatter somehow

View File

@ -116,14 +116,14 @@ class ClassInfo {
* through the $class parameter as the first array value. * through the $class parameter as the first array value.
* *
* Example usage: * Example usage:
* <example> * <code>
* ClassInfo::subclassesFor('BaseClass'); * ClassInfo::subclassesFor('BaseClass');
* array( * array(
* 0 => 'BaseClass', * 0 => 'BaseClass',
* 'ChildClass' => 'ChildClass', * 'ChildClass' => 'ChildClass',
* 'GrandChildClass' => 'GrandChildClass' * 'GrandChildClass' => 'GrandChildClass'
* ) * )
* </example> * </code>
* *
* @param mixed $class string of the classname or instance of the class * @param mixed $class string of the classname or instance of the class
* @return array Names of all subclasses as an associative array. * @return array Names of all subclasses as an associative array.

View File

@ -17,7 +17,6 @@
* Objects of type {@link ViewableData} can have an "escaping type", * Objects of type {@link ViewableData} can have an "escaping type",
* which determines if they are automatically escaped before output by {@link SSViewer}. * which determines if they are automatically escaped before output by {@link SSViewer}.
* *
* @usedby ViewableData::XML_val()
* @package sapphire * @package sapphire
* @subpackage misc * @subpackage misc
*/ */

View File

@ -93,8 +93,8 @@ if(!isset($_SERVER['HTTP_HOST'])) {
/** /**
* Define system paths * Define system paths
*/ */
define('BASE_PATH', dirname(dirname($_SERVER['SCRIPT_FILENAME']))); define('BASE_PATH', rtrim(dirname(dirname($_SERVER['SCRIPT_FILENAME'])), DIRECTORY_SEPARATOR));
define('BASE_URL', dirname(dirname($_SERVER['SCRIPT_NAME']))); define('BASE_URL', rtrim(dirname(dirname($_SERVER['SCRIPT_NAME'])), DIRECTORY_SEPARATOR));
define('MODULES_DIR', 'modules'); define('MODULES_DIR', 'modules');
define('MODULES_PATH', BASE_PATH . '/' . MODULES_DIR); define('MODULES_PATH', BASE_PATH . '/' . MODULES_DIR);
define('THIRDPARTY_DIR', 'jsparty'); define('THIRDPARTY_DIR', 'jsparty');
@ -255,8 +255,20 @@ function getClassFile($className) {
if($_CLASS_MANIFEST[$className]) return $_CLASS_MANIFEST[$className]; if($_CLASS_MANIFEST[$className]) return $_CLASS_MANIFEST[$className];
} }
/**
* Creates a class instance by the "singleton" design pattern.
* It will always return the same instance for this class,
* which can be used for performance reasons and as a simple
* way to access instance methods which don't rely on instance
* data (e.g. the custom SilverStripe static handling).
*
* @uses Object::strong_create()
*
* @param string $className
* @return Object
*/
function singleton($className) { function singleton($className) {
static $_SINGLETONS; global $_SINGLETONS;
if(!isset($className)) user_error("singleton() Called without a class", E_USER_ERROR); if(!isset($className)) user_error("singleton() Called without a class", E_USER_ERROR);
if(!is_string($className)) user_error("singleton() passed bad class_name: " . var_export($className,true), E_USER_ERROR); if(!is_string($className)) user_error("singleton() passed bad class_name: " . var_export($className,true), E_USER_ERROR);
if(!isset($_SINGLETONS[$className])) { if(!isset($_SINGLETONS[$className])) {

View File

@ -1,7 +1,12 @@
<?php <?php
/** /**
* Add extension that can be added to an object with Object::add_extension(). * Add extension that can be added to an object with {@link Object::add_extension()}.
* For DataObject extensions, use DataObjectDecorator * For {@link DataObject} extensions, use {@link DataObjectDecorator}.
* Each extension instance has an "owner" instance, accessible through
* {@link getOwner()}.
* Every object instance gets its own set of extension instances,
* meaning you can set parameters specific to the "owner instance"
* in new Extension instances.
* *
* @package sapphire * @package sapphire
* @subpackage core * @subpackage core
@ -35,6 +40,19 @@ abstract class Extension extends Object {
public function getOwner() { public function getOwner() {
return $this->owner; return $this->owner;
} }
/**
* Helper method to strip eval'ed arguments from a string
* thats passed to {@link DataObject::$extensions} or
* {@link Object::add_extension()}.
*
* @param string $extensionStr E.g. "Versioned('Stage','Live')"
* @return string Extension classname, e.g. "Versioned"
*/
public static function get_classname_without_arguments($extensionStr) {
return (($p = strpos($extensionStr, '(')) !== false) ? substr($extensionStr, 0, $p) : $extensionStr;
}
} }
?> ?>

View File

@ -338,7 +338,7 @@ class HTTP {
// Now that we've generated them, either output them or attach them to the HTTPResponse as appropriate // Now that we've generated them, either output them or attach them to the HTTPResponse as appropriate
foreach($responseHeaders as $k => $v) { foreach($responseHeaders as $k => $v) {
if($body) $body->addHeader($k, $v); if($body) $body->addHeader($k, $v);
else header("$k: $v"); else if(!headers_sent()) header("$k: $v");
} }
} }
@ -351,6 +351,13 @@ class HTTP {
static function gmt_date($timestamp) { static function gmt_date($timestamp) {
return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT'; return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
} }
/*
* Return static variable cache_age in second
*/
static function get_cache_age() {
return self::$cache_age;
}
} }

View File

@ -1,629 +1,886 @@
<?php <?php
/** /**
* Base object that all others should inherit from. * A base class for all sapphire objects to inherit from.
* This object provides a number of helper methods that patch over PHP's deficiencies. *
* This class provides a number of pattern implementations, as well as methods and fixes to add extra psuedo-static
* and method functionality to PHP.
* *
* See {@link Extension} on how to implement a custom multiple
* inheritance for object instances based on PHP5 method call overloading.
*
* @todo Create instance-specific removeExtension() which removes an extension from $extension_instances,
* but not from static $extensions, and clears everything added through defineMethods(), mainly $extra_methods.
*
* @package sapphire * @package sapphire
* @subpackage core * @subpackage core
*/ */
class Object { abstract class Object {
/** /**
* @var string $class * An array of extension names and parameters to be applied to this object upon construction.
*
* Example:
* <code>
* public static $extensions = array (
* 'Hierachy',
* "Version('Stage', 'Live')"
* );
* </code>
*
* Use {@link Object::add_extension()} to add extensions without access to the class code,
* e.g. to extend core classes.
*
* Extensions are instanciated together with the object and stored in {@link $extension_instances}.
*
* @var array $extensions
*/
public static $extensions = null;
/**#@+
* @var array
*/
private static
$statics = array(),
$cached_statics = array(),
$extra_statics = array(),
$replaced_statics = array();
private static
$classes_constructed = array(),
$extra_methods = array(),
$built_in_methods = array();
private static
$custom_classes = array(),
$strong_classes = array();
/**#@-*/
/**
* @var string the class name
*/ */
public $class; public $class;
/** /**
* @var array $statics * @var array all current extension instances.
*/
protected static $statics = array();
/**
* @var array $static_cached
*/
protected static $static_cached = array();
/**
* This DataObjects extensions, eg Versioned.
* @var array
*/ */
protected $extension_instances = array(); protected $extension_instances = array();
/** /**
* Extensions to be used on this object. An array of extension names * An implementation of the factory method, allows you to create an instance of a class
* and parameters eg: *
* This method first for strong class overloads (singletons & DB interaction), then custom class overloads. If an
* overload is found, an instance of this is returned rather than the original class. To overload a class, use
* {@link Object::useCustomClass()}
*
* @param string $class the class name
* @param mixed $arguments,... arguments to pass to the constructor
* @return Object
*/
public static function create() {
$args = func_get_args();
$class = self::getCustomClass(array_shift($args));
if(version_compare(PHP_VERSION, '5.1.3', '>=')) {
$reflector = new ReflectionClass($class);
return $reflector->newInstanceArgs($args);
} else {
// we're using a PHP install that doesn't support ReflectionClass->newInstanceArgs()
$args = $args + array_fill(0, 9, null);
return new $class($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6], $args[7], $args[8]);
}
}
/**
* Similar to {@link Object::create()}, except that classes are only overloaded if you set the $strong parameter to
* TRUE when using {@link Object::useCustomClass()}
*
* @param string $class the class name
* @param mixed $arguments,... arguments to pass to the constructor
* @return Object
*/
public static function strong_create() {
$args = func_get_args();
$class = array_shift($args);
if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
$class = self::$strong_classes[$class];
}
if(version_compare(PHP_VERSION, '5.1.3', '>=')) {
$reflector = new ReflectionClass($class);
return $reflector->newInstanceArgs($args);
} else {
$args = $args + array_fill(0, 9, null);
return new $class($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6], $args[7], $args[8]);
}
}
/**
* This class allows you to overload classes with other classes when they are constructed using the factory method
* {@link Object::create()}
*
* @param string $oldClass the class to replace
* @param string $newClass the class to replace it with
* @param bool $strong allows you to enforce a certain class replacement under all circumstances. This is used in
* singletons and DB interaction classes
*/
public static function useCustomClass($oldClass, $newClass, $strong = false) {
if($strong) {
self::$strong_classes[$oldClass] = $newClass;
} else {
self::$custom_classes[$oldClass] = $newClass;
}
}
/**
* If a class has been overloaded, get the class name it has been overloaded with - otherwise return the class name
*
* @param string $class the class to check
* @return string the class that would be created if you called {@link Object::create()} with the class
*/
public static function getCustomClass($class) {
if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
return self::$strong_classes[$class];
} elseif(isset(self::$custom_classes[$class]) && ClassInfo::exists(self::$custom_classes[$class])) {
return self::$custom_classes[$class];
}
return $class;
}
/**
* Get a static variable, taking into account SS's inbuild static caches and pseudo-statics
*
* This method first checks for any extra values added by {@link Object::add_static_var()}, and attemps to traverse
* up the extra static var chain until it reaches the top, or it reaches a replacement static.
*
* If any extra values are discovered, they are then merged with the default PHP static values, or in some cases
* completely replace the default PHP static when you set $replace = true, and do not define extra data on any child
* classes
*
* @param string $class
* @param string $name the property name
* @param bool $uncached if set to TRUE, force a regeneration of the static cache
* @return mixed
*/
public static function get_static($class, $name, $uncached = false) {
if(!isset(self::$cached_statics[$class][$name]) || $uncached) {
$extra = $builtIn = $break = $replacedAt = false;
$ancestry = array_reverse(ClassInfo::ancestry($class));
// traverse up the class tree and build extra static and stop information
foreach($ancestry as $ancestor) {
if(isset(self::$extra_statics[$ancestor][$name])) {
$toMerge = self::$extra_statics[$ancestor][$name];
if(is_array($toMerge) && is_array($extra)) {
$extra = array_merge($toMerge, $extra);
} elseif(!$extra) {
$extra = $toMerge;
} else {
$break = true;
}
if(isset(self::$replaced_statics[$ancestor][$name])) $replacedAt = $break = $ancestor;
if($break) break;
}
}
// check whether to merge in the default value
if($replacedAt && ($replacedAt == $class || !is_array($extra))) {
$value = $extra;
} elseif($replacedAt) {
// determine whether to merge in lower-class variables
$ancestorRef = new ReflectionClass(reset($ancestry));
$ancestorProps = $ancestorRef->getStaticProperties();
$ancestorInbuilt = array_key_exists($name, $ancestorProps) ? $ancestorProps[$name] : null;
$replacedRef = new ReflectionClass($replacedAt);
$replacedProps = $replacedRef->getStaticProperties();
$replacedInbuilt = array_key_exists($name, $replacedProps) ? $replacedProps[$name] : null;
if($ancestorInbuilt != $replacedInbuilt) {
$value = is_array($ancestorInbuilt) ? array_merge($ancestorInbuilt, (array) $extra) : $extra;
} else {
$value = $extra;
}
} else {
// get a built-in value
$reflector = new ReflectionClass($class);
$props = $reflector->getStaticProperties();
$inbuilt = array_key_exists($name, $props) ? $props[$name] : null;
$value = isset($extra) && is_array($extra) ? array_merge($extra, (array) $inbuilt) : $inbuilt;
}
self::$cached_statics[$class][$name] = true;
self::$statics[$class][$name] = $value;
}
return self::$statics[$class][$name];
}
/**
* Set a static variable
*
* @param string $class
* @param string $name the property name to set
* @param mixed $value
*/
public static function set_static($class, $name, $value) {
self::$statics[$class][$name] = $value;
self::$cached_statics[$class][$name] = true;
}
/**
* Get an uninherited static variable - a variable that is explicity set in this class, and not in the parent class.
* *
* static $extensions = array( * @todo Recursively filter out parent statics, currently only inspects the parent class
* "Hierarchy", *
* "Versioned('Stage', 'Live')", * @param string $class
* ); * @param string $name
* @return mixed
*/
public static function uninherited_static($class, $name) {
$inherited = self::get_static($class, $name);
$parent = null;
if($parentClass = get_parent_class($class)) {
$parent = self::get_static($parentClass, $name);
}
if(is_array($inherited) && is_array($parent)) {
return array_diff_assoc($inherited, $parent);
}
return ($inherited != $parent) ? $inherited : null;
}
/**
* Traverse down a class ancestry and attempt to merge all the uninherited static values for a particular static
* into a single variable
*
* @param string $class
* @param string $name the static name
* @param string $ceiling an optional parent class name to begin merging statics down from, rather than traversing
* the entire hierarchy
* @return mixed
*/
public static function combined_static($class, $name, $ceiling = false) {
$ancestry = ClassInfo::ancestry($class);
$values = null;
if($ceiling) while(current($ancestry) != $ceiling && $ancestry) {
array_shift($ancestry);
}
if($ancestry) foreach($ancestry as $ancestor) {
$merge = self::uninherited_static($ancestor, $name);
if(is_array($values) && is_array($merge)) {
$values = array_merge($values, $merge);
} elseif($merge) {
$values = $merge;
}
}
return $values;
}
/**
* Merge in a set of additional static variables
*
* @param string $class
* @param array $properties in a [property name] => [value] format
* @param bool $replace replace existing static vars
*/
public static function addStaticVars($class, $properties, $replace = false) {
foreach($properties as $prop => $value) self::add_static_var($class, $prop, $value, $replace);
}
/**
* Add a static variable without replacing it completely if possible, but merging in with both existing PHP statics
* and existing psuedo-statics. Uses PHP's array_merge_recursive() with if the $replace argument is FALSE.
* *
* @var array * Documentation from http://php.net/array_merge_recursive:
* If the input arrays have the same string keys, then the values for these keys are merged together
* into an array, and this is done recursively, so that if one of the values is an array itself,
* the function will merge it with a corresponding entry in another array too.
* If, however, the arrays have the same numeric key, the later value will not overwrite the original value,
* but will be appended.
*
* @param string $class
* @param string $name the static name
* @param mixed $value
* @param bool $replace completely replace existing static values
*/ */
public static $extensions = null; public static function add_static_var($class, $name, $value, $replace = false) {
if(is_array($value) && isset(self::$extra_statics[$class][$name]) && !$replace) {
/** self::$extra_statics[$class][$name] = array_merge_recursive(self::$extra_statics[$class][$name], $value);
* @var array $extraStatics } else {
*/ self::$extra_statics[$class][$name] = $value;
protected static $extraStatics = array(); }
if ($replace) {
self::set_static($class, $name, $value);
self::$replaced_statics[$class][$name] = true;
} else {
self::$cached_statics[$class][$name] = null;
}
}
/** /**
* @var array $classConstructed * Return TRUE if a class has a specified extension
*
* @param string $class
* @param string $requiredExtension the class name of the extension to check for.
*/ */
protected static $classConstructed = array(); public static function has_extension($class, $requiredExtension) {
$requiredExtension = strtolower($requiredExtension);
/** if($extensions = self::get_static($class, 'extensions')) foreach($extensions as $extension) {
* @var array $extraMethods $left = strtolower(Extension::get_classname_without_arguments($extension));
*/ $right = strtolower(Extension::get_classname_without_arguments($requiredExtension));
protected static $extraMethods = array(); if($left == $right) return true;
}
return false;
}
/** /**
* @var array $builtInMethods * Add an extension to a specific class.
* As an alternative, extensions can be added to a specific class
* directly in the {@link Object::$extensions} array.
* See {@link SiteTree::$extensions} for examples.
* Keep in mind that the extension will only be applied to new
* instances, not existing ones (including all instances created through {@link singleton()}).
*
* @param string $class Class that should be decorated - has to be a subclass of {@link Object}
* @param string $extension Subclass of {@link Extension} with optional parameters
* as a string, e.g. "Versioned" or "Translatable('Param')"
*/ */
protected static $builtInMethods = array(); public static function add_extension($class, $extension) {
if(!preg_match('/([^(]*)/', $extension, $matches)) {
/** return false;
* @var array $custom_classes Use the class in the value instead of the class in the key }
*/ $extensionClass = $matches[1];
private static $custom_classes = array(); if(!class_exists($extensionClass)) {
user_error(sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass), E_USER_ERROR);
/** }
* @var array $strong_classes
*/ if(!is_subclass_of($extensionClass, 'Extension')) {
private static $strong_classes = array(); user_error(sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass), E_USER_ERROR);
}
// unset some caches
self::$cached_statics[$class]['extensions'] = null;
$subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class;
if($subclasses) foreach($subclasses as $subclass) {
unset(self::$classes_constructed[$subclass]);
}
// merge with existing static vars
self::add_static_var($class, 'extensions', array($extension));
}
/** /**
* @var array $uninherited_statics * Remove an extension from a class.
* Keep in mind that this won't revert any datamodel additions
* of the extension at runtime, unless its used before the
* schema building kicks in (in your _config.php).
* Doesn't remove the extension from any {@link Object}
* instances which are already created, but will have an
* effect on new extensions.
* Clears any previously created singletons through {@link singleton()}
* to avoid side-effects from stale extension information.
*
* @todo Add support for removing extensions with parameters
*
* @param string $class
* @param string $extension Classname of an {@link Extension} subclass, without parameters
*/ */
private static $uninherited_statics = array(); public static function remove_extension($class, $extension) {
if(self::has_extension($class, $extension)) {
function __construct() { self::set_static(
$this->class = get_class($this); $class,
'extensions',
// Set up the extensions array_diff(self::get_static($class, 'extensions'), array($extension))
if($extensions = $this->stat('extensions')) { );
foreach($extensions as $extension) { }
// unset singletons to avoid side-effects
global $_SINGLETONS;
$_SINGLETONS = array();
}
/**
* @param string $class
* @param bool $includeArgumentString Include the argument string in the return array,
* FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')").
* @return array Numeric array of either {@link DataObjectDecorator} classnames,
* or eval'ed classname strings with constructor arguments.
*/
function get_extensions($class, $includeArgumentString = false) {
$extensions = self::get_static($class, 'extensions');
if($includeArgumentString) {
return $extensions;
} else {
$extensionClassnames = array();
if($extensions) foreach($extensions as $extension) {
$extensionClassnames[] = Extension::get_classname_without_arguments($extension);
}
return $extensionClassnames;
}
}
// -----------------------------------------------------------------------------------------------------------------
public function __construct() {
$this->class = get_class($this);
if($extensionClasses = ClassInfo::ancestry($this->class)) foreach($extensionClasses as $class) {
if($extensions = self::uninherited_static($class, 'extensions')) foreach($extensions as $extension) {
// an $extension value can contain parameters as a string,
// e.g. "Versioned('Stage','Live')"
$instance = eval("return new $extension;"); $instance = eval("return new $extension;");
$instance->setOwner($this); $instance->setOwner($this);
$this->extension_instances[$instance->class] = $instance; $this->extension_instances[$instance->class] = $instance;
} }
} }
if(!isset(Object::$classConstructed[$this->class])) { if(!isset(self::$classes_constructed[$this->class])) {
$this->defineMethods(); $this->defineMethods();
Object::$classConstructed[$this->class] = true; self::$classes_constructed[$this->class] = true;
} }
} }
/** /**
* Calls a method. * Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
* Extra methods can be hooked to a class using
*/
public function __call($methodName, $args) {
$lowerMethodName = strtolower($methodName);
if(isset(Object::$extraMethods[$this->class][$lowerMethodName])) {
$config = Object::$extraMethods[$this->class][$lowerMethodName];
if(isset($config['parameterName'])) {
if(isset($config['arrayIndex'])) $obj = $this->{$config['parameterName']}[$config['arrayIndex']];
else $obj = $this->{$config['parameterName']};
if($obj) {
return call_user_func_array(array(&$obj, $methodName), $args);
} else {
if($this->destroyed) user_error("Attempted to call $methodName on destroyed '$this->class' object", E_USER_ERROR);
else user_error("'$this->class' object doesn't have a parameter $config[parameterName]($config[arrayIndex]) to pass control to. Perhaps this object has been mistakenly destroyed?", E_USER_WARNING);
}
} else if(isset($config['wrap'])) {
array_unshift($args, $config['methodName']);
return call_user_func_array(array(&$this, $config['wrap']), $args);
} else if(isset($config['function'])) {
$function = $config['function'];
return $function($this, $args);
} else if($config['function_str']) {
$function = Object::$extraMethods[$this->class][strtolower($methodName)]['function'] = create_function('$obj, $args', $config['function_str']);
return $function($this, $args);
} else {
user_error("Object::__call() Method '$methodName' in class '$this->class' an invalid format: " . var_export(Object::$extraMethods[$this->class][$methodName],true), E_USER_ERROR);
}
} else {
user_error("Object::__call() Method '$methodName' not found in class '$this->class'", E_USER_ERROR);
}
}
/**
* This function allows you to overload class creation methods, so certain classes are
* always created correctly over your system.
* *
* @param oldClass = the old classname you want to replace with. * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
* @param customClass = the new Classname you wish to replace the old class with. * {@link Object::addWrapperMethod()}
* @param strong - If you want to force a replacement of a class then we use a different array *
* e.g for use in singleton classes. * @param string $method
*/ * @param array $arguments
public static function useCustomClass( $oldClass, $customClass,$strong = false ) {
if($strong){
self::$strong_classes[$oldClass] = $customClass;
}else{
self::$custom_classes[$oldClass] = $customClass;
}
}
public static function getCustomClass( $oldClass ) {
if( array_key_exists($oldClass, self::$custom_classes) )
return self::$custom_classes[$oldClass];
else{
return $oldClass;
}
}
/**
* Create allows us to override the standard classes of sapphire with our own custom classes.
* create will load strong classes firstly for singleton level and database interaction, otherwise will
* use the fallback custom classes.
* To set a strong custom class to overide an object at for say singleton use, use the syntax
* Object::useCustomClass('Datetime','SSDatetime',true);
* @param className - The classname you want to create
* @param args - Up to 9 arguments you wish to pass on to the new class
*/
public static function create( $className, $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null, $arg6 = null, $arg7 = null, $arg8 = null ) {
$useStrongClassName = isset(self::$strong_classes[$className]);
$useClassName = isset(self::$custom_classes[$className]);
if($useStrongClassName){
$classToCreate = self::$strong_classes[$className];
}elseif($useClassName){
$classToCreate = self::$custom_classes[$className];
}
$hasStrong = isset(self::$strong_classes[$className]) && class_exists(self::$strong_classes[$className]);
$hasNormal = isset(self::$custom_classes[$className]) && class_exists(self::$custom_classes[$className]);
if( !isset($classToCreate) || (!$hasStrong && !$hasNormal)){
$classToCreate = $className;
}
return new $classToCreate( $arg0, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7, $arg8 );
}
/**
* Strong_create is a function to enforce a certain class replacement
* e.g Php5.2's latest introduction of a namespace conflict means we have to replace
* all instances of Datetime with SSdatetime.
* this allows us to seperate those, and sapphires classes
* @param className - The class you wish to create.
* @param args - pass up to 8 arguments to the created class.
*/
public static function strong_create( $className, $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null, $arg6 = null, $arg7 = null, $arg8 = null ) {
$useStrongClassName = isset(self::$strong_classes[$className]);
if($useStrongClassName){
$classToCreate = self::$strong_classes[$className];
}
if( !isset($classToCreate) || !class_exists( self::$strong_classes[$className])){
$classToCreate = $className;
}
return new $classToCreate( $arg0, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7, $arg8 );
}
/**
* Returns true if the given method exists.
*/
public function hasMethod($methodName) {
if(method_exists($this, $methodName)) return true;
$methodName = strtolower($methodName);
if(!isset($this->class)) $this->class = get_class($this);
/*
if(!isset(Object::$builtInMethods['_set'][$this->class])) $this->buildMethodList();
if(isset(Object::$builtInMethods[$this->class][$methodName])) return true;
*/
if(isset(Object::$extraMethods[$this->class][$methodName])) return true;
return false;
}
/**
* Add the all methods from a given parameter to this object.
* This is used for extensions.
* @param parameterName The name of the parameter. This parameter must be instanciated with an item of the correct class.
* @param arrayIndex If parameterName is an array, this can be an index. If null, we'll assume the value is all that is needed.
*/
protected function addMethodsFrom($parameterName, $arrayIndex = null) {
$obj = isset($arrayIndex) ? $this->{$parameterName}[$arrayIndex] : $this->$parameterName;
if(!$obj) user_error("Object::addMethodsFrom: $parameterName/$arrayIndex", E_USER_ERROR);
// Hack to fix Fatal error: Call to undefined method stdClass::allMethodNames()
if(method_exists($obj, 'allMethodNames')) {
$methodNames = $obj->allMethodNames(true);
foreach($methodNames as $methodName) {
Object::$extraMethods[$this->class][strtolower($methodName)] = array("parameterName" => $parameterName, "arrayIndex" => $arrayIndex);
}
}
}
/**
* Add a 'wrapper method'.
* For example, Thumbnail($arg, $arg) can be defined to call generateImage("Thumbnail", $arg, $arg)
*/
protected function addWrapperMethod($methodName, $wrapperMethod) {
Object::$extraMethods[$this->class][strtolower($methodName)] = array("wrap" => $wrapperMethod, "methodName" => $methodName);
}
/**
* Create a new method
* @param methodName The name of the method
* @param methodCode The PHP code of the method, in a string. Arguments will be contained
* in an array called $args. The object will be $obj, not $this. You won't be able to access
* any protected methods; the method is actually contained in an external function.
*/
protected function createMethod($methodName, $methodCode) {
Object::$extraMethods[$this->class][strtolower($methodName)] = array("function_str" => $methodCode);
}
/**
* Return the names of all the methods on this object.
* param includeCustom If set to true, then return custom methods too.
*/
function allMethodNames($includeCustom = false) {
if(!$this->class) $this->class = get_class($this);
if(!isset(Object::$builtInMethods['_set'][$this->class])) $this->buildMethodList();
if($includeCustom && isset(Object::$extraMethods[$this->class])) {
return array_merge(Object::$builtInMethods[$this->class], array_keys(Object::$extraMethods[$this->class]));
} else {
return Object::$builtInMethods[$this->class];
}
}
function buildMethodList() {
if(!$this->class) $this->class = get_class($this);
$reflection = new ReflectionClass($this->class);
$methods = $reflection->getMethods();
foreach($methods as $method) {
$name = $method->getName();
$methodNames[strtolower($name)] = $name;
}
Object::$builtInMethods[$this->class] = $methodNames;
Object::$builtInMethods['_set'][$this->class] = true;
}
/**
* This constructor will be called the first time an object of this class is created.
* You can overload it with methods for setting up the class - for example, extra methods.
*/
protected function defineMethods() {
if($this->extension_instances) foreach($this->extension_instances as $i => $instance) {
$this->addMethodsFrom('extension_instances', $i);
}
if(isset($_REQUEST['debugmethods']) && isset(Object::$builtInMethods[$this->class])) {
Debug::require_developer_login();
echo "<h2>Methods defined for $this->class</h2>";
foreach(Object::$builtInMethods[$this->class] as $name => $info) {
echo "<li>$name";
}
}
}
/**
* This method lets us extend a built-in class by adding pseudo-static variables to it.
*
* @param string $class Classname
* @param array $statics Statics to add, with keys being static property names on the class
* @param boolean $replace Replace the whole variable instead of merging arrays
*/
static function addStaticVars($class, $statics, $replace = false) {
if(empty(Object::$extraStatics[$class])) {
Object::$extraStatics[$class] = (array)$statics;
} elseif($replace) {
Object::$extraStatics[$class] = $statics;
} else {
$ar1 = (array)Object::$extraStatics[$class];
$ar2 = (array)$statics;
Object::$extraStatics[$class] = array_merge_recursive($ar1, $ar2);
}
}
/**
* Set an uninherited static variable
*/
function set_uninherited($name, $val) {
return Object::$uninherited_statics[$this->class][$name] = $val;
}
/**
* Get an uninherited static variable
*/
function uninherited($name, $builtIn = false) {
// Copy a built-in value into our own array cache. PHP's static variable support is shit.
if($builtIn) {
$val = $this->stat($name);
$val2 = null;
// isset() can handle the case where a variable isn't defined; more reliable than reflection
$propertyName = get_parent_class($this) . "::\$$name";
$val2 = eval("return isset($propertyName) ? $propertyName : null;");
return ($val != $val2) ? $val : null;
}
return isset(Object::$uninherited_statics[$this->class][$name]) ? Object::$uninherited_statics[$this->class][$name] : null;
}
/**
* Get a static variable.
*
* @param string $name
* @param boolean $uncached
* @return mixed * @return mixed
*/ */
function stat($name, $uncached = false) { public function __call($method, $arguments) {
if(!$this->class) $this->class = get_class($this); $method = strtolower($method);
if(!isset(Object::$static_cached[$this->class][$name]) || $uncached) { if(isset(self::$extra_methods[$this->class][$method])) {
$classes = ClassInfo::ancestry($this->class); $config = self::$extra_methods[$this->class][$method];
foreach($classes as $class) {
if(isset(Object::$extraStatics[$class][$name])) { switch(true) {
$extra = Object::$extraStatics[$class][$name]; case isset($config['property']) :
if(!is_array($extra)) return $extra; $obj = $config['index'] !== null ?
break; $this->{$config['property']}[$config['index']] :
} $this->{$config['property']};
if($obj) return call_user_func_array(array($obj, $method), $arguments);
if($this->destroyed) {
throw new Exception (
"Object->__call(): attempt to call $method on a destroyed $this->class object"
);
} else {
throw new Exception (
"Object->__call(): $this->class cannot pass control to $config[property]($config[index])." .
' Perhaps this object was mistakenly destroyed?'
);
}
case isset($config['wrap']) :
array_unshift($arguments, $config['method']);
return call_user_func_array(array($this, $config['wrap']), $arguments);
case isset($config['function']) :
return $config['function']($this, $arguments);
default :
throw new Exception (
"Object->__call(): extra method $method is invalid on $this->class:" . var_export($config, true)
);
} }
$stat = eval("return {$this->class}::\$$name;"); } else {
Object::$statics[$this->class][$name] = isset($extra) ? array_merge($extra, (array)$stat) : $stat; throw new Exception("Object->__call(): the method '$method' does not exist on '$this->class'");
Object::$static_cached[$this->class][$name] = true;
} }
return Object::$statics[$this->class][$name];
} }
// -----------------------------------------------------------------------------------------------------------------
/** /**
* Set a static variable * Return TRUE if a method exists on this object
*
* This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
* extensions
*
* @param string $method
* @return bool
*/ */
function set_stat($name, $val) { public function hasMethod($method) {
Object::$statics[$this->class][$name] = $val; return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
Object::$static_cached[$this->class][$name] = true;
} }
/** /**
* Returns true if this object "exists", i.e., has a sensible value. * Return the names of all the methods available on this object
* Overload this in subclasses.
* For example, an empty DataObject record could return false.
* *
* @return boolean * @param bool $custom include methods added dynamically at runtime
* @return array
*/
public function allMethodNames($custom = false) {
if(!isset(self::$built_in_methods['_set'][$this->class])) $this->buildMethodList();
if($custom && isset(self::$extra_methods[$this->class])) {
return array_merge(self::$built_in_methods[$this->class], array_keys(self::$extra_methods[$this->class]));
} else {
return self::$built_in_methods[$this->class];
}
}
protected function buildMethodList() {
foreach(get_class_methods($this) as $method) {
self::$built_in_methods[$this->class][strtolower($method)] = strtolower($method);
}
self::$built_in_methods['_set'][$this->class] = true;
}
/**
* Adds any methods from {@link Extension} instances attached to this object.
* All these methods can then be called directly on the instance (transparently
* mapped through {@link __call()}), or called explicitly through {@link extend()}.
*
* @uses addMethodsFrom()
*/
protected function defineMethods() {
if($this->extension_instances) foreach(array_keys($this->extension_instances) as $key) {
$this->addMethodsFrom('extension_instances', $key);
}
if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$this->class])) {
Debug::require_developer_login();
echo '<h2>Methods defined on ' . $this->class . '</h2><ul>';
foreach(self::$built_in_methods[$this->class] as $method) {
echo "<li>$method</li>";
}
echo '</ul>';
}
}
/**
* Add all the methods from an object property (which is an {@link Extension}) to this object.
*
* @param string $property the property name
* @param string|int $index an index to use if the property is an array
*/
protected function addMethodsFrom($property, $index = null) {
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
if(!$extension) {
throw new InvalidArgumentException (
"Object->addMethodsFrom(): could not add methods from {$this->class}->{$property}[$index]"
);
}
if(method_exists($extension, 'allMethodNames')) {
foreach($extension->allMethodNames(true) as $method) {
self::$extra_methods[$this->class][$method] = array (
'property' => $property,
'index' => $index
);
}
}
}
/**
* Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
* can be wrapped to generateThumbnail(x)
*
* @param string $method the method name to wrap
* @param string $wrap the method name to wrap to
*/
protected function addWrapperMethod($method, $wrap) {
self::$extra_methods[$this->class][strtolower($method)] = array (
'wrap' => $wrap,
'method' => $method
);
}
/**
* Add an extra method using raw PHP code passed as a string
*
* @param string $method the method name
* @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
* by using $obj. Note that you cannot call protected methods, as the method is actually an external function
*/
protected function createMethod($method, $code) {
self::$extra_methods[$this->class][strtolower($method)] = array (
'function' => create_function('$obj, $args', $code)
);
}
// -----------------------------------------------------------------------------------------------------------------
/**
* @see Object::get_static()
*/
public function stat($name, $uncached = false) {
return self::get_static(($this->class ? $this->class : get_class($this)), $name, $uncached);
}
/**
* @see Object::set_static()
*/
public function set_stat($name, $value) {
self::set_static(($this->class ? $this->class : get_class($this)), $name, $value);
}
/**
* @see Object::uninherited_static()
*/
public function uninherited($name) {
return self::uninherited_static(($this->class ? $this->class : get_class($this)), $name);
}
/**
* @deprecated
*/
public function set_uninherited() {
user_error (
'Object->set_uninherited() is deprecated, please use a custom static on your object', E_USER_WARNING
);
}
// -----------------------------------------------------------------------------------------------------------------
/**
* Return true if this object "exists" i.e. has a sensible value
*
* This method should be overriden in subclasses to provide more context about the classes state. For example, a
* {@link DataObject} class could return false when it is deleted from the database
*
* @return bool
*/ */
public function exists() { public function exists() {
return true; return true;
} }
function parentClass() { /**
* @return string this classes parent class
*/
public function parentClass() {
return get_parent_class($this); return get_parent_class($this);
} }
/** /**
* Wrapper for PHP's is_a() * Check if this class is an instance of a specific class, or has that class as one of its parents
* *
* @param string $class * @param string $class
* @return boolean * @return bool
*/ */
function is_a($class) { public function is_a($class) {
return is_a($this, $class); return $this instanceof $class;
} }
/**
* @return string the class name
*/
public function __toString() { public function __toString() {
return $this->class; return $this->class;
} }
// -----------------------------------------------------------------------------------------------------------------
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// EXTENSION METHODS
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** /**
* Invokes a method on the object itself, or proxied through a decorator. * Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge
* * all results into an array
* This method breaks the normal rules of inheritance, and aggregates everything
* in the returned result. If the invoked methods return void, they are still recorded as
* empty array keys.
*
* @todo find a better way of integrating inheritance rules
* *
* @param unknown_type $funcName * @param string $method the method name to call
* @param unknown_type $arg * @param mixed $argument a single argument to pass
* @return mixed
* @todo integrate inheritance rules
*/ */
public function invokeWithExtensions($funcName, $arg=null) { public function invokeWithExtensions($method, $argument = null) {
$results = array(); $result = method_exists($this, $method) ? array($this->$method($argument)) : array();
if (method_exists($this, $funcName)) { $extras = $this->extend($method, $argument);
$results[] = $this->$funcName($arg);
}
$extras = $this->extend($funcName, $arg);
if ($extras) {
return array_merge($results, $extras);
} else {
return $results;
}
}
/**
* Run the given function on all of this object's extensions. Note that this method
* originally returned void, so if you wanted to return results, you're hosed.
*
* Currently returns an array, with an index resulting every time the function is called.
* Only adds returns if they're not NULL, to avoid bogus results from methods just
* defined on the parent decorator. This is important for permission-checks through
* extend, as they use min() to determine if any of the returns is FALSE.
* As min() doesn't do type checking, an included NULL return would fail the permission checks.
*
* @param string $funcName The name of the function.
* @param mixed $arg An Argument to be passed to each of the extension functions.
*/
public function extend($funcName, &$arg1=null, &$arg2=null, &$arg3=null, &$arg4=null, &$arg5=null, &$arg6=null, &$arg7=null) {
$arguments = func_get_args();
array_shift($arguments);
if($this->extension_instances) { return $extras ? array_merge($result, $extras) : $result;
$returnArr = array();
foreach($this->extension_instances as $extension) {
if($extension->hasMethod($funcName)) {
$return = $extension->$funcName($arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7);
if($return !== NULL) $returnArr[] = $return;
}
}
return $returnArr;
}
} }
/** /**
* Get an extension on this DataObject * Run the given function on all of this object's extensions. Note that this method originally returned void, so if
* * you wanted to return results, you're hosed
* @param string $name Classname of the Extension (e.g. 'Versioned')
*
* @return DataObjectDecorator The instance of the extension
*/
public function extInstance($name) {
if(isset($this->extension_instances[$name])) {
return $this->extension_instances[$name];
}
}
/**
* Returns true if the given extension class is attached to this object
*
* @param string $requiredExtension Classname of the extension
*
* @return boolean True if the given extension class is attached to this object
*/
public function hasExtension($requiredExtension) {
return isset($this->extension_instances[$requiredExtension]) ? true : false;
}
/**
* Add an extension to the given object.
* This can be used to add extensions to built-in objects, such as role decorators on Member
*/
public static function add_extension($className, $extensionName) {
Object::addStaticVars($className, array(
'extensions' => array(
$extensionName,
),
));
}
public static function remove_extension($className, $extensionName) {
Object::$extraStatics[$className]['extensions'] = array_diff(Object::$extraStatics[$className]['extensions'], array($extensionName));
}
/**
* Loads a current cache from the filesystem, if it can.
* *
* @param string $cachename The name of the cache to load * Currently returns an array, with an index resulting every time the function is called. Only adds returns if
* @param int $expire The lifetime of the cache in seconds * they're not NULL, to avoid bogus results from methods just defined on the parent decorator. This is important for
* @return mixed The data from the cache, or false if the cache wasn't loaded * permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't
* do type checking, an included NULL return would fail the permission checks.
*
* The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
*
* @param string $method the name of the method to call on each extension
* @param mixed $a1,... up to 7 arguments to be passed to the method
* @return array
*/ */
protected function loadCache($cachename, $expire = 3600) { public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) {
$cache_dir = TEMP_FOLDER; $values = array();
$cache_path = $cache_dir . "/" . $this->sanitiseCachename($cachename);
if((!isset($_GET['flush']) || $_GET['flush']!=1) && (@file_exists($cache_path) && ((@filemtime($cache_path) + $expire) > (time())))) { if($this->extension_instances) foreach($this->extension_instances as $instance) {
return @unserialize(file_get_contents($cache_path)); if($instance->hasMethod($method)) {
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
if($value !== null) $values[] = $value;
}
} }
return $values;
}
/**
* Get an extension instance attached to this object by name.
*
* @uses hasExtension()
*
* @param string $extension
* @return Extension
*/
public function getExtensionInstance($extension) {
if($this->hasExtension($extension)) return $this->extension_instances[$extension];
}
/**
* Returns TRUE if this object instance has a specific extension applied
* in {@link $extension_instances}. Extension instances are initialized
* at constructor time, meaning if you use {@link add_extension()}
* afterwards, the added extension will just be added to new instances
* of the decorated class. Use the static method {@link has_extension()}
* to check if a class (not an instance) has a specific extension.
* Caution: Don't use singleton(<class>)->hasExtension() as it will
* give you inconsistent results based on when the singleton was first
* accessed.
*
* @param string $extension Classname of an {@link Extension} subclass without parameters
* @return bool
*/
public function hasExtension($extension) {
return isset($this->extension_instances[$extension]);
}
/**
* Get all extension instances for this specific object instance.
* See {@link get_extensions()} to get all applied extension classes
* for this class (not the instance).
*
* @return array Map of {@link DataObjectDecorator} instances, keyed by classname.
*/
public function getExtensionInstances() {
return $this->extension_instances;
}
// -----------------------------------------------------------------------------------------------------------------
/**
* Cache the results of an instance method in this object to a file, or if it is already cache return the cached
* results
*
* @param string $method the method name to cache
* @param int $lifetime the cache lifetime in seconds
* @param string $ID custom cache ID to use
* @param array $arguments an optional array of arguments
* @return mixed the cached data
*/
public function cacheToFile($method, $lifetime = 3600, $ID = false, $arguments = array()) {
if(!$this->hasMethod($method)) {
throw new InvalidArgumentException("Object->cacheToFile(): the method $method does not exist to cache");
}
$cacheName = $this->class . '_' . $method;
if(!is_array($arguments)) $arguments = array($arguments);
if($ID) $cacheName .= '_' . $ID;
if(count($arguments)) $cacheName .= '_' . implode('_', $arguments);
if($data = $this->loadCache($cacheName, $lifetime)) {
return $data;
}
$data = call_user_func_array(array($this, $method), $arguments);
$this->saveCache($cacheName, $data);
return $data;
}
/**
* Clears the cache for the given cacheToFile call
*/
public function clearCache($method, $ID = false, $arguments = array()) {
$cacheName = $this->class . '_' . $method;
if(!is_array($arguments)) $arguments = array($arguments);
if($ID) $cacheName .= '_' . $ID;
if(count($arguments)) $cacheName .= '_' . implode('_', $arguments);
unlink(TEMP_FOLDER . '/' . $this->sanitiseCachename($cacheName));
}
/**
* @deprecated
*/
public function cacheToFileWithArgs($callback, $arguments = array(), $lifetime = 3600, $ID = false) {
user_error (
'Object->cacheToFileWithArgs() is deprecated, please use Object->cacheToFile() with the $arguments param',
E_USER_NOTICE
);
return $this->cacheToFile($callback, $lifetime, $ID, $arguments);
}
/**
* Loads a cache from the filesystem if a valid on is present and within the specified lifetime
*
* @param string $cache the cache name
* @param int $lifetime the lifetime (in seconds) of the cache before it is invalid
* @return mixed
*/
protected function loadCache($cache, $lifetime = 3600) {
$path = TEMP_FOLDER . '/' . $this->sanitiseCachename($cache);
if(!isset($_REQUEST['flush']) && file_exists($path) && (filemtime($path) + $lifetime) > time()) {
return unserialize(file_get_contents($path));
}
return false; return false;
} }
/** /**
* Saves a cache to the file system * Save a piece of cached data to the file system
* *
* @param string $cachename The name of the cache to save * @param string $cache the cache name
* @param mixed $data The data to cache * @param mixed $data data to save (must be serializable)
*/ */
protected function saveCache($cachename, $data) { protected function saveCache($cache, $data) {
$cache_dir = TEMP_FOLDER; file_put_contents(TEMP_FOLDER . '/' . $this->sanitiseCachename($cache), serialize($data));
$cache_path = $cache_dir . "/" . $this->sanitiseCachename($cachename);
$fp = @fopen($cache_path, "w+");
if(!$fp) {
return; // Throw an error?
}
@fwrite($fp, @serialize($data));
@fclose($fp);
} }
/** /**
* Makes a cache name safe to use in a file system * Strip a file name of special characters so it is suitable for use as a cache file name
* *
* @param string $cachename The cache name to sanitise * @param string $name
* @return string the sanitised cache name * @return string the name with all special cahracters replaced with underscores
*/ */
protected function sanitiseCachename($cachename) { protected function sanitiseCachename($name) {
// Replace illegal characters with underscores return str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $name);
$cachename = str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $cachename);
return $cachename;
} }
/** /**
* Caches the return value of a method. * @deprecated 2.4 Use getExtensionInstance
*
* @param callback $callback The method to cache
* @param int $expire The lifetime of the cache
* @param string|int $id An id for the cache
* @return mixed The cached return of the method
*/ */
public function cacheToFile($callback, $expire = 3600, $id = false) { public function extInstance($extension) {
if(!$this->class) { return $this->getExtensionInstance($extension);
$this->class = get_class($this);
}
if(!method_exists($this->class, $callback)) {
user_error("Class {$this->class} doesn't have the method $callback.", E_USER_ERROR);
}
$cachename = $this->class . "_" . $callback;
if($id) {
$cachename .= "_" . (string)$id;
}
if(($data = $this->loadCache($cachename, $expire)) !== false) {
return $data;
}
// No cache to use
$data = $this->$callback();
if($data === false) {
// Some problem with function. Didn't give anything to cache. So don't cache it.
return false;
}
$this->saveCache($cachename, $data);
return $data;
} }
/**
* Caches the return value of a method. Passes args to the method as well.
*
* @param callback $callback The method to cache
* @param array $args The arguments to pass to the method
* @param int $expire The lifetime of the cache
* @param string|int $id An id for the cache
* @return mixed The cached return of the method
*/
public function cacheToFileWithArgs($callback, $args = array(), $expire = 3600, $id = false) {
if(!$this->class) {
$this->class = get_class($this);
}
if(!method_exists($this->class, $callback)) {
user_error("Class {$this->class} doesn't have the method $callback.", E_USER_ERROR);
}
$cachename = $this->class . "_" . $callback;
if($id) {
$cachename .= "_" . (string)$id;
}
if(($data = $this->loadCache($cachename, $expire)) !== false) {
return $data;
}
// No cache to use
$data = call_user_func_array(array($this, $callback), $args);
if($data === false) {
// Some problem with function. Didn't give anything to cache. So don't cache it.
return false;
}
$this->saveCache($cachename, $data);
return $data;
}
} }
?>

View File

@ -345,8 +345,6 @@ class Requirements_Backend {
/** /**
* Remembers the filepaths of all cleared Requirements * Remembers the filepaths of all cleared Requirements
* through {@link clear()}. * through {@link clear()}.
*
* @usedby {@link restore()}
* *
* @var array $disabled * @var array $disabled
*/ */
@ -529,6 +527,7 @@ class Requirements_Backend {
$this->disabled['css'] = $this->css; $this->disabled['css'] = $this->css;
$this->disabled['customScript'] = $this->customScript; $this->disabled['customScript'] = $this->customScript;
$this->disabled['customCSS'] = $this->customCSS; $this->disabled['customCSS'] = $this->customCSS;
$this->disabled['customHeadTags'] = $this->customHeadTags;
$this->javascript = array(); $this->javascript = array();
$this->css = array(); $this->css = array();
@ -561,6 +560,7 @@ class Requirements_Backend {
$this->css = $this->disabled['css']; $this->css = $this->disabled['css'];
$this->customScript = $this->disabled['customScript']; $this->customScript = $this->disabled['customScript'];
$this->customCSS = $this->disabled['customCSS']; $this->customCSS = $this->disabled['customCSS'];
$this->customHeadTags = $this->disabled['customHeadTags'];
} }
/** /**
@ -611,7 +611,6 @@ class Requirements_Backend {
foreach(array_diff_key($this->customCSS, $this->blocked) as $css) { foreach(array_diff_key($this->customCSS, $this->blocked) as $css) {
$requirements .= "<style type=\"text/css\">\n$css\n</style>\n"; $requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
} }
foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) { foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
$requirements .= "$customHeadTag\n"; $requirements .= "$customHeadTag\n";
} }

View File

@ -3,6 +3,9 @@
* Exception thrown by {@link DataObject}::write if validation fails. By throwing an * Exception thrown by {@link DataObject}::write if validation fails. By throwing an
* exception rather than a user error, the exception can be caught in unit tests and as such * exception rather than a user error, the exception can be caught in unit tests and as such
* can be used as a successful test. * can be used as a successful test.
*
* @package sapphire
* @subpackage validation
*/ */
class ValidationException extends Exception { class ValidationException extends Exception {

View File

@ -278,7 +278,6 @@ class ViewableData extends Object implements IteratorAggregate {
/** /**
* Return the string-format type for the given field. * Return the string-format type for the given field.
* *
* @usedby ViewableData::XML_val()
* @param string $fieldName * @param string $fieldName
* @return string 'xml'|'raw' * @return string 'xml'|'raw'
*/ */
@ -867,9 +866,9 @@ class ViewableData extends Object implements IteratorAggregate {
* Avoids having to subclass just to built templates with new css-classes, * Avoids having to subclass just to built templates with new css-classes,
* and allows for versatile css inheritance and overrides. * and allows for versatile css inheritance and overrides.
* *
* <example> * <code>
* <body class="$CSSClasses"> * <body class="$CSSClasses">
* </example> * </code>
* *
* @uses ClassInfo * @uses ClassInfo
* *

View File

@ -318,7 +318,7 @@ HTML;
* Returns the xml:lang and lang attributes * Returns the xml:lang and lang attributes
*/ */
function LangAttributes() { function LangAttributes() {
$lang = Translatable::current_lang(); $lang = Translatable::current_locale();
return "xml:lang=\"$lang\" lang=\"$lang\""; return "xml:lang=\"$lang\" lang=\"$lang\"";
} }

View File

@ -51,9 +51,6 @@ class ContentNegotiator {
return self::$encoding; return self::$encoding;
} }
/**
* @usedby Controller->handleRequest()
*/
static function process(HTTPResponse $response) { static function process(HTTPResponse $response) {
if(!self::enabled_for($response)) return; if(!self::enabled_for($response)) return;

View File

@ -5,6 +5,7 @@
* that controller will be used instead. It should be a subclass of ContentController. * that controller will be used instead. It should be a subclass of ContentController.
* *
* @package sapphire * @package sapphire
* @subpackage control
*/ */
class ModelAsController extends Controller implements NestedController { class ModelAsController extends Controller implements NestedController {
@ -48,9 +49,9 @@ class ModelAsController extends Controller implements NestedController {
$url = Controller::join_links( $url = Controller::join_links(
Director::baseURL(), Director::baseURL(),
$child->URLSegment, $child->URLSegment,
isset($this->urlParams['Action']) ? $this->urlParams['Action'] : null, (isset($this->urlParams['Action'])) ? $this->urlParams['Action'] : null,
isset($this->urlParams['ID']) ? $this->urlParams['ID'] : null, (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null,
isset($this->urlParams['OtherID']) ? $this->urlParams['OtherID'] : null (isset($this->urlParams['OtherID'])) ? $this->urlParams['OtherID'] : null
); );
$response = new HTTPResponse(); $response = new HTTPResponse();
@ -64,6 +65,9 @@ class ModelAsController extends Controller implements NestedController {
if($child) { if($child) {
if(isset($_REQUEST['debug'])) Debug::message("Using record #$child->ID of type $child->class with URL {$this->urlParams['URLSegment']}"); if(isset($_REQUEST['debug'])) Debug::message("Using record #$child->ID of type $child->class with URL {$this->urlParams['URLSegment']}");
// set language
if($child->Locale) Translatable::set_reading_locale($child->Locale);
$controllerClass = "{$child->class}_Controller"; $controllerClass = "{$child->class}_Controller";
if($this->urlParams['Action'] && ClassInfo::exists($controllerClass.'_'.$this->urlParams['Action'])) { if($this->urlParams['Action'] && ClassInfo::exists($controllerClass.'_'.$this->urlParams['Action'])) {

View File

@ -24,6 +24,9 @@
* Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class) * Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
* *
* {@link RequestHandler::handleRequest()} is where this behaviour is implemented. * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
*
* @package sapphire
* @subpackage control
*/ */
class RequestHandler extends ViewableData { class RequestHandler extends ViewableData {
protected $request = null; protected $request = null;
@ -81,8 +84,7 @@ class RequestHandler extends ViewableData {
$handlerClass = ($this->class) ? $this->class : get_class($this); $handlerClass = ($this->class) ? $this->class : get_class($this);
// We stop after RequestHandler; in other words, at ViewableData // We stop after RequestHandler; in other words, at ViewableData
while($handlerClass && $handlerClass != 'ViewableData') { while($handlerClass && $handlerClass != 'ViewableData') {
// Todo: ajshort's stat rewriting could be useful here. $urlHandlers = Object::get_static($handlerClass, 'url_handlers');
$urlHandlers = eval("return $handlerClass::\$url_handlers;");
if($urlHandlers) foreach($urlHandlers as $rule => $action) { if($urlHandlers) foreach($urlHandlers as $rule => $action) {
if(isset($_REQUEST['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class"); if(isset($_REQUEST['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
@ -144,65 +146,57 @@ class RequestHandler extends ViewableData {
// If nothing matches, return this object // If nothing matches, return this object
return $this; return $this;
} }
/** /**
* Check that the given action is allowed to be called from a URL. * Check that the given action is allowed to be called from a URL.
* It will interrogate {@link self::$allowed_actions} to determine this. * It will interrogate {@link self::$allowed_actions} to determine this.
*/ */
function checkAccessAction($action) { function checkAccessAction($action) {
// Collate self::$allowed_actions from this class and all parent classes $action = strtolower($action);
$access = null; $allowedActions = Object::combined_static($this->class, 'allowed_actions');
$className = ($this->class) ? $this->class : get_class($this); $newAllowedActions = array();
while($className && $className != 'RequestHandler') {
// Merge any non-null parts onto $access.
$accessPart = eval("return $className::\$allowed_actions;");
if($accessPart != null) $access = array_merge((array)$access, $accessPart);
// Build an array of parts for checking if part[0] == part[1], which means that this class doesn't directly define it.
$accessParts[] = $accessPart;
$className = get_parent_class($className);
}
// Add $allowed_actions from extensions // merge in any $allowed_actions from extensions
if($this->extension_instances) { if($this->extension_instances) foreach($this->extension_instances as $extension) {
foreach($this->extension_instances as $inst) { if($extAccess = $extension->stat('allowed_actions')) {
$accessPart = $inst->stat('allowed_actions'); $allowedActions = array_merge($allowedActions, $extAccess);
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
} }
} }
if($action == 'index') return true; if($action == 'index') return true;
// Make checkAccessAction case-insensitive if($allowedActions) {
$action = strtolower($action); foreach($allowedActions as $key => $value) {
if($access) { $newAllowedActions[strtolower($key)] = strtolower($value);
foreach($access as $k => $v) $newAccess[strtolower($k)] = strtolower($v); }
$access = $newAccess;
$allowedActions = $newAllowedActions;
if(isset($allowedActions[$action])) {
$test = $allowedActions[$action];
if(isset($access[$action])) { if($test === true) {
$test = $access[$action]; return true;
if($test === true) return true; } elseif(substr($test, 0, 2) == '->') {
if(substr($test,0,2) == '->') { return $this->{substr($test, 2)}();
$funcName = substr($test,2); } elseif(Permission::check($test)) {
return $this->$funcName(); return true;
} }
if(Permission::check($test)) return true; } elseif((($key = array_search($action, $allowedActions)) !== false) && is_numeric($key)) {
} else if((($key = array_search($action, $access)) !== false) && is_numeric($key)) {
return true; return true;
} }
} }
if($access === null || (isset($accessParts[1]) && $accessParts[0] === $accessParts[1])) { if($allowedActions === null || !$this->uninherited('allowed_actions')) {
// If no allowed_actions are provided, then we should only let through actions that aren't handled by magic methods // If no allowed_actions are provided, then we should only let through actions that aren't handled by magic methods
// we test this by calling the unmagic method_exists and comparing it to the magic $this->hasMethod(). This will // we test this by calling the unmagic method_exists and comparing it to the magic $this->hasMethod(). This will
// still let through actions that are handled by templates. // still let through actions that are handled by templates.
return method_exists($this, $action) || !$this->hasMethod($action); return method_exists($this, $action) || !$this->hasMethod($action);
} }
return false; return false;
} }
/** /**
* Throw an HTTP error instead of performing the normal processing * Throw an HTTP error instead of performing the normal processing
* @todo This doesn't work properly right now. :-( * @todo This doesn't work properly right now. :-(

View File

@ -6,8 +6,19 @@
* @subpackage control * @subpackage control
*/ */
class RootURLController extends Controller { class RootURLController extends Controller {
/**
* @var boolean $is_at_root
*/
protected static $is_at_root = false; protected static $is_at_root = false;
/**
* @var string $default_homepage_urlsegment Defines which URLSegment value on a {@link SiteTree} object
* is regarded as the correct "homepage" if the requested URI doesn't contain
* an explicit segment. E.g. http://mysite.com should show http://mysite.com/home.
*/
protected static $default_homepage_urlsegment = 'home';
public function init() { public function init() {
Director::set_site_mode('site'); Director::set_site_mode('site');
parent::init(); parent::init();
@ -27,7 +38,6 @@ class RootURLController extends Controller {
} }
$controller = new ModelAsController(); $controller = new ModelAsController();
$request = new HTTPRequest("GET", self::get_homepage_urlsegment().'/', $request->getVars(), $request->postVars()); $request = new HTTPRequest("GET", self::get_homepage_urlsegment().'/', $request->getVars(), $request->postVars());
$request->match('$URLSegment//$Action', true); $request->match('$URLSegment//$Action', true);
@ -39,18 +49,29 @@ class RootURLController extends Controller {
/** /**
* Return the URL segment for the current HTTP_HOST value * Return the URL segment for the current HTTP_HOST value
*
* @return string
*/ */
static function get_homepage_urlsegment() { static function get_homepage_urlsegment() {
$host = $_SERVER['HTTP_HOST']; $urlSegment = '';
$host = str_replace('www.','',$host);
$SQL_host = str_replace('.','\\.',Convert::raw2sql($host)); // @todo Temporarily restricted to MySQL database while testing db abstraction
$homePageOBJ = DataObject::get_one("SiteTree", "HomepageForDomain REGEXP '(,|^) *$SQL_host *(,|\$)'"); if(DB::getConn() instanceof MySQLDatabase) {
$host = $_SERVER['HTTP_HOST'];
if($homePageOBJ) { $host = str_replace('www.','',$host);
return $homePageOBJ->URLSegment; $SQL_host = str_replace('.','\\.',Convert::raw2sql($host));
$homePageOBJ = DataObject::get_one("SiteTree", "HomepageForDomain REGEXP '(,|^) *$SQL_host *(,|\$)'");
} else { } else {
return 'home'; $homePageOBJ = null;
} }
if(singleton('SiteTree')->hasExtension('Translatable')) {
$urlSegment = Translatable::get_homepage_urlsegment_by_language(Translatable::current_locale());
} elseif($homePageOBJ) {
$urlSegment = $homePageOBJ->URLSegment;
}
return ($urlSegment) ? $urlSegment : self::get_default_homepage_urlsegment();
} }
/** /**
@ -61,6 +82,13 @@ class RootURLController extends Controller {
if(!self::$is_at_root) return self::get_homepage_urlsegment() == $currentPage->URLSegment; if(!self::$is_at_root) return self::get_homepage_urlsegment() == $currentPage->URLSegment;
else return false; else return false;
} }
/**
* @return string
*/
static function get_default_homepage_urlsegment() {
return self::$default_homepage_urlsegment;
}
} }
?> ?>

View File

@ -575,8 +575,8 @@ class i18n extends Object {
); );
/** /**
* A list of commonly used languages, in the form * @var array $common_languages A list of commonly used languages, in the form
* langcode => array( EnglishName, NativeName) * langcode => array( EnglishName, NativeName)
*/ */
public static $common_languages = array( public static $common_languages = array(
'af' => array('Afrikaans', 'Afrikaans'), 'af' => array('Afrikaans', 'Afrikaans'),
@ -665,6 +665,99 @@ class i18n extends Object {
'zu' => array('Zulu', 'isiZulu') 'zu' => array('Zulu', 'isiZulu')
); );
/**
* @var array $common_locales
* Sorted alphabtically by the common language name,
* not the locale key.
*/
public static $common_locales = array(
'af_ZA' => array('Afrikaans', 'Afrikaans'),
'sq_AL' => array('Albanian', 'shqip'),
'ar_EG' => array('Arabic', '&#1575;&#1604;&#1593;&#1585;&#1576;&#1610;&#1577;'),
'eu_ES' => array('Basque', 'euskera'),
'be_BY' => array('Belarusian', '&#1041;&#1077;&#1083;&#1072;&#1088;&#1091;&#1089;&#1082;&#1072;&#1103; &#1084;&#1086;&#1074;&#1072;'),
'bn_BD' => array('Bengali', '&#2476;&#2494;&#2434;&#2482;&#2494;'),
'bg_BG' => array('Bulgarian', '&#1073;&#1098;&#1083;&#1075;&#1072;&#1088;&#1089;&#1082;&#1080;'),
'ca_ES' => array('Catalan', 'catal&agrave;'),
'zh-yue_ZH-YUE' => array('Chinese (Cantonese)', '&#24291;&#26481;&#35441; [&#24191;&#19996;&#35805;]'),
'zh-cmn_ZH-CMN' => array('Chinese (Mandarin)', '&#26222;&#36890;&#35441; [&#26222;&#36890;&#35805;]'),
'zh-min-nan_ZH-MIN-NAN' => array('Chinese (Min Nan)', '&#21488;&#35486;'),
'hr_HR' => array('Croatian', 'Hrvatski'),
'cs_CZ' => array('Czech', '&#x010D;e&#353;tina'),
'cy_GB' => array('Welsh', 'Welsh/Cymraeg'),
'da_DK' => array('Danish', 'dansk'),
'nl_NL' => array('Dutch', 'Nederlands'),
'en_US' => array('English', 'English'),
'eo_EO' => array('Esperanto', 'Esperanto'),
'et_EE' => array('Estonian', 'eesti keel'),
'fo_FO' => array('Faroese', 'F&oslash;royska'),
'fi_FI' => array('Finnish', 'suomi'),
'fr_FR' => array('French', 'fran&ccedil;ais'),
'gd_GB' => array('Gaelic', 'Gaeilge'),
'gl_ES' => array('Galician', 'Galego'),
'de_DE' => array('German', 'Deutsch'),
'el_GR' => array('Greek', '&#949;&#955;&#955;&#951;&#957;&#953;&#954;&#940;'),
'gu_IN' => array('Gujarati', '&#2711;&#2753;&#2716;&#2736;&#2750;&#2724;&#2752;'),
'ha_NG' => array('Hausa', '&#1581;&#1614;&#1608;&#1618;&#1587;&#1614;'),
'he_IL' => array('Hebrew', '&#1506;&#1489;&#1512;&#1497;&#1514;'),
'hi_IN' => array('Hindi', '&#2361;&#2367;&#2344;&#2381;&#2342;&#2368;'),
'hu_HU' => array('Hungarian', 'magyar'),
'is_IS' => array('Icelandic', '&Iacute;slenska'),
'io_IO' => array('Ido', 'Ido'),
'id_ID' => array('Indonesian', 'Bahasa Indonesia'),
'ga_IE' => array('Irish', 'Irish'),
'it_IT' => array('Italian', 'italiano'),
'ja_JP' => array('Japanese', '&#26085;&#26412;&#35486;'),
'jv_ID' => array('Javanese', 'basa Jawa'),
'ko_KR' => array('Korean', '&#54620;&#44397;&#50612; [&#38867;&#22283;&#35486;]'),
'ku_IQ' => array('Kurdish', 'Kurd&iacute;'),
'lv_LV' => array('Latvian', 'latvie&#353;u'),
'lt_LT' => array('Lithuanian', 'lietuvi&#353;kai'),
'lmo_LMO' => array('Lombard', 'Lombardo'),
'mk_MK' => array('Macedonian', '&#1084;&#1072;&#1082;&#1077;&#1076;&#1086;&#1085;&#1089;&#1082;&#1080;'),
'mi_NZ' => array('Maori', 'Maori'),
'ms_MY' => array('Malay', 'Bahasa melayu'),
'mt_MT' => array('Maltese', 'Malti'),
'mr_IN' => array('Marathi', '&#2350;&#2352;&#2366;&#2336;&#2368;'),
'ne_NP' => array('Nepali', '&#2344;&#2375;&#2346;&#2366;&#2354;&#2368;'),
'no_NO' => array('Norwegian', 'Norsk'),
'om_ET' => array('Oromo', 'Afaan Oromo'),
'fa_IR' => array('Persian', '&#1601;&#1575;&#1585;&#1587;&#1609;'),
'pl_PL' => array('Polish', 'polski'),
'pt-PT_PT-PT' => array('Portuguese (Portugal)', 'portugu&ecirc;s (Portugal)'),
'pt-BR_PT-BR' => array('Portuguese (Brazil)', 'portugu&ecirc;s (Brazil)'),
'pa_IN' => array('Punjabi', '&#2602;&#2672;&#2588;&#2622;&#2604;&#2624;'),
'qu_PE' => array('Quechua', 'Quechua'),
'rm_CH' => array('Romansh', 'rumantsch'),
'ro_RO' => array('Romanian', 'rom&acirc;n'),
'ru_RU' => array('Russian', '&#1056;&#1091;&#1089;&#1089;&#1082;&#1080;&#1081;'),
'sco_SCO' => array('Scots', 'Scoats leid, Lallans'),
'sr_RS' => array('Serbian', '&#1089;&#1088;&#1087;&#1089;&#1082;&#1080;'),
'sk_SK' => array('Slovak', 'sloven&#269;ina'),
'sl_SI' => array('Slovenian', 'sloven&#353;&#269;ina'),
'es_ES' => array('Spanish', 'espa&ntilde;ol'),
'sv_SE' => array('Swedish', 'Svenska'),
'tl_PH' => array('Tagalog', 'Tagalog'),
'ta_IN' => array('Tamil', '&#2980;&#2990;&#3007;&#2996;&#3021;'),
'te_IN' => array('Telugu', '&#3108;&#3142;&#3122;&#3137;&#3095;&#3137;'),
'to_TO' => array('Tonga', 'chiTonga'),
'ts_ZA' => array('Tsonga', 'xiTshonga'),
'tn_ZA' => array('Tswana', 'seTswana'),
'tr_TR' => array('Turkish', 'T&uuml;rk&ccedil;e'),
'tk_TM' => array('Turkmen', '&#1090;&#1199;&#1088;&#1082;m&#1077;&#1085;&#1095;&#1077;'),
'tw_GH' => array('Twi', 'twi'),
'uk_UA' => array('Ukrainian', '&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1089;&#1100;&#1082;&#1072;'),
'ur_PK' => array('Urdu', '&#1575;&#1585;&#1583;&#1608;'),
'uz_UZ' => array('Uzbek', '&#1118;&#1079;&#1073;&#1077;&#1082;'),
've_ZA' => array('Venda', 'tshiVen&#x1E13;a'),
'vi_VN' => array('Vietnamese', 'ti&#7871;ng vi&#7879;t'),
'wa_WA' => array('Walloon', 'walon'),
'wo_SN' => array('Wolof', 'Wollof'),
'xh_ZA' => array('Xhosa', 'isiXhosa'),
'yi_YI' => array('Yiddish', '&#1522;&#1460;&#1491;&#1497;&#1513;'),
'zu_ZA' => array('Zulu', 'isiZulu'),
);
static $tinymce_lang = array( static $tinymce_lang = array(
'ca_AD' => 'ca', 'ca_AD' => 'ca',
'ca_ES' => 'ca', 'ca_ES' => 'ca',
@ -827,6 +920,473 @@ class i18n extends Object {
); );
/**
* @var array $likely_subtags Provides you "likely locales"
* for a given "short" language code. This is a guess,
* as we can't disambiguate from e.g. "en" to "en_US" - it
* could also mean "en_UK".
* @see http://www.unicode.org/cldr/data/charts/supplemental/likely_subtags.html
*/
static $likely_subtags = array(
'aa' => 'aa_ET',
'ab' => 'ab_GE',
'ady' => 'ady_RU',
'af' => 'af_ZA',
'ak' => 'ak_GH',
'am' => 'am_ET',
'ar' => 'ar_EG',
'as' => 'as_IN',
'ast' => 'ast_ES',
'av' => 'av_RU',
'ay' => 'ay_BO',
'az' => 'az_AZ',
'az_Cyrl' => 'az_AZ',
'az_Arab' => 'az_IR',
'az_IR' => 'az_IR',
'ba' => 'ba_RU',
'be' => 'be_BY',
'bg' => 'bg_BG',
'bi' => 'bi_VU',
'bn' => 'bn_BD',
'bo' => 'bo_CN',
'bs' => 'bs_BA',
'ca' => 'ca_ES',
'ce' => 'ce_RU',
'ceb' => 'ceb_PH',
'ch' => 'ch_GU',
'chk' => 'chk_FM',
'crk' => 'crk_CA',
'cs' => 'cs_CZ',
'cwd' => 'cwd_CA',
'cy' => 'cy_GB',
'da' => 'da_DK',
'de' => 'de_DE',
'dv' => 'dv_MV',
'dz' => 'dz_BT',
'ee' => 'ee_GH',
'efi' => 'efi_NG',
'el' => 'el_GR',
'en' => 'en_US',
'es' => 'es_ES',
'et' => 'et_EE',
'eu' => 'eu_ES',
'fa' => 'fa_IR',
'fi' => 'fi_FI',
'fil' => 'fil_PH',
'fj' => 'fj_FJ',
'fo' => 'fo_FO',
'fr' => 'fr_FR',
'fur' => 'fur_IT',
'fy' => 'fy_NL',
'ga' => 'ga_IE',
'gaa' => 'gaa_GH',
'gd' => 'gd_GB',
'gil' => 'gil_KI',
'gl' => 'gl_ES',
'gn' => 'gn_PY',
'gu' => 'gu_IN',
'ha' => 'ha_NG',
'ha_Arab' => 'ha_SD',
'ha_SD' => 'ha_SD',
'haw' => 'haw_US',
'he' => 'he_IL',
'hi' => 'hi_IN',
'hil' => 'hil_PH',
'ho' => 'ho_PG',
'hr' => 'hr_HR',
'ht' => 'ht_HT',
'hu' => 'hu_HU',
'hy' => 'hy_AM',
'id' => 'id_ID',
'ig' => 'ig_NG',
'ii' => 'ii_CN',
'ilo' => 'ilo_PH',
'inh' => 'inh_RU',
'is' => 'is_IS',
'it' => 'it_IT',
'iu' => 'iu_CA',
'ja' => 'ja_JP',
'jv' => 'jv_ID',
'ka' => 'ka_GE',
'kaj' => 'kaj_NG',
'kam' => 'kam_KE',
'kbd' => 'kbd_RU',
'kha' => 'kha_IN',
'kk' => 'kk_KZ',
'kl' => 'kl_GL',
'km' => 'km_KH',
'kn' => 'kn_IN',
'ko' => 'ko_KR',
'koi' => 'koi_RU',
'kok' => 'kok_IN',
'kos' => 'kos_FM',
'kpe' => 'kpe_LR',
'kpv' => 'kpv_RU',
'krc' => 'krc_RU',
'ks' => 'ks_IN',
'ku' => 'ku_IQ',
'ku_Latn' => 'ku_TR',
'ku_TR' => 'ku_TR',
'kum' => 'kum_RU',
'ky' => 'ky_KG',
'la' => 'la_VA',
'lah' => 'lah_PK',
'lb' => 'lb_LU',
'lbe' => 'lbe_RU',
'lez' => 'lez_RU',
'ln' => 'ln_CD',
'lo' => 'lo_LA',
'lt' => 'lt_LT',
'lv' => 'lv_LV',
'mai' => 'mai_IN',
'mdf' => 'mdf_RU',
'mdh' => 'mdh_PH',
'mg' => 'mg_MG',
'mh' => 'mh_MH',
'mi' => 'mi_NZ',
'mk' => 'mk_MK',
'ml' => 'ml_IN',
'mn' => 'mn_MN',
'mn_CN' => 'mn_CN',
'mn_Mong' => 'mn_CN',
'mr' => 'mr_IN',
'ms' => 'ms_MY',
'mt' => 'mt_MT',
'my' => 'my_MM',
'myv' => 'myv_RU',
'na' => 'na_NR',
'nb' => 'nb_NO',
'ne' => 'ne_NP',
'niu' => 'niu_NU',
'nl' => 'nl_NL',
'nn' => 'nn_NO',
'nr' => 'nr_ZA',
'nso' => 'nso_ZA',
'ny' => 'ny_MW',
'om' => 'om_ET',
'or' => 'or_IN',
'os' => 'os_GE',
'pa' => 'pa_IN',
'pa_Arab' => 'pa_PK',
'pa_PK' => 'pa_PK',
'pag' => 'pag_PH',
'pap' => 'pap_AN',
'pau' => 'pau_PW',
'pl' => 'pl_PL',
'pon' => 'pon_FM',
'ps' => 'ps_AF',
'pt' => 'pt_BR',
'qu' => 'qu_PE',
'rm' => 'rm_CH',
'rn' => 'rn_BI',
'ro' => 'ro_RO',
'ru' => 'ru_RU',
'rw' => 'rw_RW',
'sa' => 'sa_IN',
'sah' => 'sah_RU',
'sat' => 'sat_IN',
'sd' => 'sd_IN',
'se' => 'se_NO',
'sg' => 'sg_CF',
'si' => 'si_LK',
'sid' => 'sid_ET',
'sk' => 'sk_SK',
'sl' => 'sl_SI',
'sm' => 'sm_WS',
'sn' => 'sn_ZW',
'so' => 'so_SO',
'sq' => 'sq_AL',
'sr' => 'sr_RS',
'ss' => 'ss_ZA',
'st' => 'st_ZA',
'su' => 'su_ID',
'sv' => 'sv_SE',
'sw' => 'sw_TZ',
'swb' => 'swb_KM',
'ta' => 'ta_IN',
'te' => 'te_IN',
'tet' => 'tet_TL',
'tg' => 'tg_TJ',
'th' => 'th_TH',
'ti' => 'ti_ET',
'tig' => 'tig_ER',
'tk' => 'tk_TM',
'tkl' => 'tkl_TK',
'tl' => 'tl_PH',
'tn' => 'tn_ZA',
'to' => 'to_TO',
'tpi' => 'tpi_PG',
'tr' => 'tr_TR',
'trv' => 'trv_TW',
'ts' => 'ts_ZA',
'tsg' => 'tsg_PH',
'tt' => 'tt_RU',
'tts' => 'tts_TH',
'tvl' => 'tvl_TV',
'tw' => 'tw_GH',
'ty' => 'ty_PF',
'tyv' => 'tyv_RU',
'udm' => 'udm_RU',
'ug' => 'ug_CN',
'uk' => 'uk_UA',
'uli' => 'uli_FM',
'und' => 'en_US',
'und_AD' => 'ca_AD',
'und_AE' => 'ar_AE',
'und_AF' => 'fa_AF',
'und_AL' => 'sq_AL',
'und_AM' => 'hy_AM',
'und_AN' => 'pap_AN',
'und_AO' => 'pt_AO',
'und_AR' => 'es_AR',
'und_AS' => 'sm_AS',
'und_AT' => 'de_AT',
'und_AW' => 'nl_AW',
'und_AX' => 'sv_AX',
'und_AZ' => 'az_AZ',
'und_Arab' => 'ar_EG',
'und_Arab_CN' => 'ug_CN',
'und_Arab_DJ' => 'ar_DJ',
'und_Arab_ER' => 'ar_ER',
'und_Arab_IL' => 'ar_IL',
'und_Arab_IN' => 'ur_IN',
'und_Arab_PK' => 'ur_PK',
'und_Armn' => 'hy_AM',
'und_BA' => 'bs_BA',
'und_BD' => 'bn_BD',
'und_BE' => 'nl_BE',
'und_BF' => 'fr_BF',
'und_BG' => 'bg_BG',
'und_BH' => 'ar_BH',
'und_BI' => 'rn_BI',
'und_BJ' => 'fr_BJ',
'und_BL' => 'fr_BL',
'und_BN' => 'ms_BN',
'und_BO' => 'es_BO',
'und_BR' => 'pt_BR',
'und_BT' => 'dz_BT',
'und_BY' => 'be_BY',
'und_Beng' => 'bn_BD',
'und_CD' => 'fr_CD',
'und_CF' => 'sg_CF',
'und_CG' => 'ln_CG',
'und_CH' => 'de_CH',
'und_CI' => 'fr_CI',
'und_CL' => 'es_CL',
'und_CM' => 'fr_CM',
'und_CN' => 'zh_CN',
'und_CO' => 'es_CO',
'und_CR' => 'es_CR',
'und_CU' => 'es_CU',
'und_CV' => 'pt_CV',
'und_CY' => 'el_CY',
'und_CZ' => 'cs_CZ',
'und_Cans' => 'cwd_CA',
'und_Cyrl' => 'ru_RU',
'und_Cyrl_BA' => 'sr_BA',
'und_Cyrl_GE' => 'ab_GE',
'und_DE' => 'de_DE',
'und_DJ' => 'aa_DJ',
'und_DK' => 'da_DK',
'und_DO' => 'es_DO',
'und_DZ' => 'ar_DZ',
'und_Deva' => 'hi_IN',
'und_EC' => 'es_EC',
'und_EE' => 'et_EE',
'und_EG' => 'ar_EG',
'und_EH' => 'ar_EH',
'und_ER' => 'ti_ER',
'und_ES' => 'es_ES',
'und_ET' => 'am_ET',
'und_Ethi' => 'am_ET',
'und_FI' => 'fi_FI',
'und_FJ' => 'fj_FJ',
'und_FM' => 'chk_FM',
'und_FO' => 'fo_FO',
'und_FR' => 'fr_FR',
'und_GA' => 'fr_GA',
'und_GE' => 'ka_GE',
'und_GF' => 'fr_GF',
'und_GH' => 'ak_GH',
'und_GL' => 'kl_GL',
'und_GN' => 'fr_GN',
'und_GP' => 'fr_GP',
'und_GQ' => 'fr_GQ',
'und_GR' => 'el_GR',
'und_GT' => 'es_GT',
'und_GU' => 'ch_GU',
'und_GW' => 'pt_GW',
'und_Geor' => 'ka_GE',
'und_Grek' => 'el_GR',
'und_Gujr' => 'gu_IN',
'und_Guru' => 'pa_IN',
'und_HK' => 'zh_HK',
'und_HN' => 'es_HN',
'und_HR' => 'hr_HR',
'und_HT' => 'ht_HT',
'und_HU' => 'hu_HU',
'und_Hani' => 'zh_CN',
'und_Hans' => 'zh_CN',
'und_Hant' => 'zh_TW',
'und_Hebr' => 'he_IL',
'und_ID' => 'id_ID',
'und_IL' => 'he_IL',
'und_IN' => 'hi_IN',
'und_IQ' => 'ar_IQ',
'und_IR' => 'fa_IR',
'und_IS' => 'is_IS',
'und_IT' => 'it_IT',
'und_JO' => 'ar_JO',
'und_JP' => 'ja_JP',
'und_Jpan' => 'ja_JP',
'und_KG' => 'ky_KG',
'und_KH' => 'km_KH',
'und_KM' => 'ar_KM',
'und_KP' => 'ko_KP',
'und_KR' => 'ko_KR',
'und_KW' => 'ar_KW',
'und_KZ' => 'ru_KZ',
'und_Khmr' => 'km_KH',
'und_Knda' => 'kn_IN',
'und_Kore' => 'ko_KR',
'und_LA' => 'lo_LA',
'und_LB' => 'ar_LB',
'und_LI' => 'de_LI',
'und_LK' => 'si_LK',
'und_LS' => 'st_LS',
'und_LT' => 'lt_LT',
'und_LU' => 'fr_LU',
'und_LV' => 'lv_LV',
'und_LY' => 'ar_LY',
'und_Laoo' => 'lo_LA',
'und_Latn_CN' => 'ii_CN',
'und_Latn_CY' => 'tr_CY',
'und_Latn_DZ' => 'fr_DZ',
'und_Latn_ET' => 'om_ET',
'und_Latn_KM' => 'fr_KM',
'und_Latn_MA' => 'fr_MA',
'und_Latn_MK' => 'sq_MK',
'und_Latn_SY' => 'fr_SY',
'und_Latn_TD' => 'fr_TD',
'und_Latn_TN' => 'fr_TN',
'und_MA' => 'ar_MA',
'und_MC' => 'fr_MC',
'und_MD' => 'ro_MD',
'und_ME' => 'sr_ME',
'und_MF' => 'fr_MF',
'und_MG' => 'mg_MG',
'und_MH' => 'mh_MH',
'und_MK' => 'mk_MK',
'und_ML' => 'fr_ML',
'und_MM' => 'my_MM',
'und_MN' => 'mn_MN',
'und_MO' => 'zh_MO',
'und_MQ' => 'fr_MQ',
'und_MR' => 'ar_MR',
'und_MT' => 'mt_MT',
'und_MV' => 'dv_MV',
'und_MW' => 'ny_MW',
'und_MX' => 'es_MX',
'und_MY' => 'ms_MY',
'und_MZ' => 'pt_MZ',
'und_Mlym' => 'ml_IN',
'und_Mong' => 'mn_CN',
'und_Mymr' => 'my_MM',
'und_NC' => 'fr_NC',
'und_NE' => 'ha_NE',
'und_NG' => 'ha_NG',
'und_NI' => 'es_NI',
'und_NL' => 'nl_NL',
'und_NO' => 'nb_NO',
'und_NP' => 'ne_NP',
'und_NR' => 'na_NR',
'und_NU' => 'niu_NU',
'und_OM' => 'ar_OM',
'und_Orya' => 'or_IN',
'und_PA' => 'es_PA',
'und_PE' => 'es_PE',
'und_PF' => 'ty_PF',
'und_PG' => 'tpi_PG',
'und_PH' => 'fil_PH',
'und_PK' => 'ur_PK',
'und_PL' => 'pl_PL',
'und_PM' => 'fr_PM',
'und_PR' => 'es_PR',
'und_PS' => 'ar_PS',
'und_PT' => 'pt_PT',
'und_PW' => 'pau_PW',
'und_PY' => 'gn_PY',
'und_QA' => 'ar_QA',
'und_RE' => 'fr_RE',
'und_RO' => 'ro_RO',
'und_RS' => 'sr_RS',
'und_RU' => 'ru_RU',
'und_RW' => 'rw_RW',
'und_SA' => 'ar_SA',
'und_SD' => 'ar_SD',
'und_SE' => 'sv_SE',
'und_SI' => 'sl_SI',
'und_SJ' => 'nb_SJ',
'und_SK' => 'sk_SK',
'und_SM' => 'it_SM',
'und_SN' => 'fr_SN',
'und_SO' => 'so_SO',
'und_SR' => 'nl_SR',
'und_ST' => 'pt_ST',
'und_SV' => 'es_SV',
'und_SY' => 'ar_SY',
'und_Sinh' => 'si_LK',
'und_TD' => 'ar_TD',
'und_TG' => 'ee_TG',
'und_TH' => 'th_TH',
'und_TJ' => 'tg_TJ',
'und_TK' => 'tkl_TK',
'und_TL' => 'tet_TL',
'und_TM' => 'tk_TM',
'und_TN' => 'ar_TN',
'und_TO' => 'to_TO',
'und_TR' => 'tr_TR',
'und_TV' => 'tvl_TV',
'und_TW' => 'zh_TW',
'und_Taml' => 'ta_IN',
'und_Telu' => 'te_IN',
'und_Thaa' => 'dv_MV',
'und_Thai' => 'th_TH',
'und_Tibt' => 'bo_CN',
'und_UA' => 'uk_UA',
'und_UY' => 'es_UY',
'und_UZ' => 'uz_UZ',
'und_VA' => 'la_VA',
'und_VE' => 'es_VE',
'und_VN' => 'vi_VN',
'und_VU' => 'fr_VU',
'und_WF' => 'fr_WF',
'und_WS' => 'sm_WS',
'und_YE' => 'ar_YE',
'und_YT' => 'fr_YT',
'und_ZW' => 'sn_ZW',
'ur' => 'ur_PK',
'uz' => 'uz_UZ',
'uz_AF' => 'uz_AF',
'uz_Arab' => 'uz_AF',
've' => 've_ZA',
'vi' => 'vi_VN',
'wal' => 'wal_ET',
'war' => 'war_PH',
'wo' => 'wo_SN',
'xh' => 'xh_ZA',
'yap' => 'yap_FM',
'yo' => 'yo_NG',
'za' => 'za_CN',
'zh' => 'zh_CN',
'zh_HK' => 'zh_HK',
'zh_Hani' => 'zh_CN',
'zh_Hant' => 'zh_TW',
'zh_MO' => 'zh_MO',
'zh_TW' => 'zh_TW',
'zu' => 'zu_ZA',
);
/** /**
* This is the main translator function. Returns the string defined by $class and $entity according to the currently set locale. * This is the main translator function. Returns the string defined by $class and $entity according to the currently set locale.
* *
@ -876,6 +1436,20 @@ class i18n extends Object {
return $languages; return $languages;
} }
/**
* Get a list of commonly used locales
*
* @param boolean $native Use native names for locale instead of English ones
* @return list of languages in the form 'code' => 'name'
*/
static function get_common_locales($native = false) {
$languages = array();
foreach (self::$common_locales as $code => $name) {
$languages[$code] = ($native ? $name[1] : $name[0]);
}
return $languages;
}
/** /**
* Get a list of locales (code => language and country) * Get a list of locales (code => language and country)
* *
@ -921,7 +1495,9 @@ class i18n extends Object {
} }
/** /**
* Get a name from a language code * Get a name from a language code (two characters, e.g. "en").
*
* @see get_locale_name()
* *
* @param mixed $code Language code * @param mixed $code Language code
* @param boolean $native If true, the native name will be returned * @param boolean $native If true, the native name will be returned
@ -933,7 +1509,9 @@ class i18n extends Object {
} }
/** /**
* Get a name from a locale code (xx_YY) * Get a name from a locale code (xx_YY).
*
* @see get_language_name()
* *
* @param mixed $code locale code * @param mixed $code locale code
* @return Name of the locale * @return Name of the locale
@ -992,6 +1570,59 @@ class i18n extends Object {
return $translatableModules; return $translatableModules;
} }
/**
* Returns the "short" language name from a locale,
* e.g. "en_US" would return "en". This conversion
* is determined internally by the {@link $tinymce_lang}
* lookup table. If no match can be found in this lookup,
* the characters before the underscore ("_") are returned.
*
* @todo More generic lookup table, don't rely on tinymce specific conversion
*
* @param string $locale E.g. "en_US"
* @return string Short language code, e.g. "en"
*/
static function get_lang_from_locale($locale) {
if(isset(self::$tinymce_lang[$locale])) {
return self::$tinymce_lang[$locale];
} else {
return preg_replace('/(_|-).*/', '', $locale);
}
}
/**
* Provides you "likely locales"
* for a given "short" language code. This is a guess,
* as we can't disambiguate from e.g. "en" to "en_US" - it
* could also mean "en_UK". Based on the Unicode CLDR
* project.
* @see http://www.unicode.org/cldr/data/charts/supplemental/likely_subtags.html
*
* @param string $lang Short language code, e.g. "en"
* @return string Long locale, e.g. "en_US"
*/
static function get_locale_from_lang($lang) {
if(isset(self::$likely_subtags[$lang])) {
return self::$likely_subtags[$lang];
} else {
return $lang . '_' . strtoupper($lang);
}
}
/**
* Gets a RFC 1766 compatible language code,
* e.g. "en-US".
*
* @see http://www.ietf.org/rfc/rfc1766.txt
* @see http://tools.ietf.org/html/rfc2616#section-3.10
*
* @param string $locale
* @return string
*/
static function convert_rfc1766($locale) {
return str_replace('_','-', $locale);
}
/** /**
* Given a file name (a php class name, without the .php ext, or a template name, including the .ss extension) * Given a file name (a php class name, without the .php ext, or a template name, including the .ss extension)
* this helper function determines the module where this file is located * this helper function determines the module where this file is located
@ -1047,21 +1678,19 @@ class i18n extends Object {
} }
/** /**
* Set default language (proxy for Translatable::set_default_lang()) * @deprecated 2.4 Use Translatable::set_default_locale()
*
* @param $lang String * @param $lang String
*/ */
static function set_default_lang($lang) { static function set_default_lang($lang) {
Translatable::set_default_lang($lang); Translatable::set_default_locale($lang);
} }
/** /**
* Get default language (proxy for Translatable::default_lang()) * @deprecated 2.4 Use Translatable::default_locale()
*
* @return String * @return String
*/ */
static function default_lang() { static function default_lang() {
return Translatable::default_lang(); return Translatable::default_locale();
} }
static function default_locale() { static function default_locale() {
@ -1069,7 +1698,9 @@ class i18n extends Object {
} }
/** /**
* Enables the multilingual content feature (proxy for Translatable::enable()) * Enables the multilingual content feature (proxy for Translatable::enable()).
*
* @deprecated 2.4 Use Object::add_extension('Page', 'Translatable');
*/ */
static function enable() { static function enable() {
Translatable::enable(); Translatable::enable();
@ -1077,6 +1708,8 @@ class i18n extends Object {
/** /**
* Disable the multilingual content feature (proxy for Translatable::disable()) * Disable the multilingual content feature (proxy for Translatable::disable())
*
* @deprecated 2.4 Use Object::add_extension('Page', 'Translatable');
*/ */
static function disable() { static function disable() {
Translatable::disable(); Translatable::disable();
@ -1149,7 +1782,7 @@ class i18n extends Object {
*/ */
public function removelang() { public function removelang() {
if (!Permission::check("ADMIN")) user_error("You must be an admin to remove a language", E_USER_ERROR); if (!Permission::check("ADMIN")) user_error("You must be an admin to remove a language", E_USER_ERROR);
$translatedToDelete = Translatable::get_by_lang('SiteTree',$this->urlParams['ID']); $translatedToDelete = Translatable::get_by_locale('SiteTree',$this->urlParams['ID']);
foreach ($translatedToDelete as $object) { foreach ($translatedToDelete as $object) {
$object->delete(); $object->delete();
} }

View File

@ -53,7 +53,36 @@
* @package sapphire * @package sapphire
* @subpackage model * @subpackage model
*/ */
class DataObject extends ViewableData implements DataObjectInterface,i18nEntityProvider { class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
/**
* Human-readable singular name.
* @var string
*/
public static $singular_name = null;
/**
* Human-readable pluaral name
* @var string
*/
public static $plural_name = null;
/**
* Allow API access to this object?
* @todo Define the options that can be set here
*/
public static $api_access = false;
public static
$cache_has_own_table = array(),
$cache_has_own_table_field = array();
/**
* True if this DataObject has been destroyed.
* @var boolean
*/
public $destroyed = false;
/** /**
* Data stored in this objects database record. An array indexed * Data stored in this objects database record. An array indexed
* by fieldname. * by fieldname.
@ -80,31 +109,6 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* @var array * @var array
*/ */
protected $components; protected $components;
/**
* True if this DataObject has been destroyed.
* @var boolean
*/
public $destroyed = false;
/**
* Human-readable singular name.
* @var string
*/
static $singular_name = null;
/**
* Human-readable pluaral name
* @var string
*/
static $plural_name = null;
/**
* Allow API access to this object?
* @todo Define the options that can be set here
*/
static $api_access = false;
/** /**
* Should dataobjects be validated before they are written? * Should dataobjects be validated before they are written?
@ -127,6 +131,44 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
static function set_validation_enabled($enable) { static function set_validation_enabled($enable) {
self::$validation_enabled = (bool) $enable; self::$validation_enabled = (bool) $enable;
} }
/**
* Return the complete map of fields on this object, including Created, LastEdited and ClassName
*
* @param string $class
* @return array
*/
public static function database_fields($class) {
if(get_parent_class($class) == 'DataObject') {
return array_merge (
array (
'ClassName' => "Enum('" . implode(', ', ClassInfo::subclassesFor($class)) . "')",
'Created' => 'SSDatetime',
'LastEdited' => 'SSDatetime'
),
self::custom_database_fields($class)
);
}
return self::custom_database_fields($class);
}
/**
* Get all database fields explicitly defined on a class in {@link DataObject::$db} and {@link DataObject::$has_one}
*
* @param string $class
* @return array
*/
public static function custom_database_fields($class) {
$fields = Object::uninherited_static($class, 'db');
$hasOne = Object::uninherited_static($class, 'has_one');
if($hasOne) foreach(array_keys($hasOne) as $field) {
$fields[$field . 'ID'] = 'ForeignKey';
}
return (array) $fields;
}
/** /**
@ -372,8 +414,6 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* } * }
* </code> * </code>
* *
* @usedby {@link DataObjectSet->toDropDownMap()}
*
* @return string * @return string
*/ */
public function getTitle() { public function getTitle() {
@ -657,11 +697,10 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
*/ */
public function populateDefaults() { public function populateDefaults() {
$classes = array_reverse(ClassInfo::ancestry($this)); $classes = array_reverse(ClassInfo::ancestry($this));
foreach($classes as $class) { foreach($classes as $class) {
$singleton = ($class == $this->class) ? $this : singleton($class); $defaults = Object::get_static($class, 'defaults');
$defaults = $singleton->stat('defaults');
if($defaults) foreach($defaults as $fieldName => $fieldValue) { if($defaults) foreach($defaults as $fieldName => $fieldValue) {
// SRM 2007-03-06: Stricter check // SRM 2007-03-06: Stricter check
if(!isset($this->$fieldName) || $this->$fieldName === null) { if(!isset($this->$fieldName) || $this->$fieldName === null) {
@ -782,6 +821,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if(isset($ancestry) && is_array($ancestry)) { if(isset($ancestry) && is_array($ancestry)) {
foreach($ancestry as $idx => $class) { foreach($ancestry as $idx => $class) {
$classSingleton = singleton($class); $classSingleton = singleton($class);
foreach($this->record as $fieldName => $fieldValue) { foreach($this->record as $fieldName => $fieldValue) {
if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) { if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) {
$fieldObj = $this->dbObject($fieldName); $fieldObj = $this->dbObject($fieldName);
@ -829,7 +869,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if(isset($isNewRecord) && $isNewRecord && isset($manipulation[$baseTable])) { if(isset($isNewRecord) && $isNewRecord && isset($manipulation[$baseTable])) {
$manipulation[$baseTable]['command'] = 'update'; $manipulation[$baseTable]['command'] = 'update';
} }
DB::manipulate($manipulation); DB::manipulate($manipulation);
if(isset($isNewRecord) && $isNewRecord) { if(isset($isNewRecord) && $isNewRecord) {
@ -1272,12 +1312,13 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue; if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue;
if($component) { if($component) {
$candidate = eval("return isset({$class}::\$has_one[\$component]) ? {$class}::\$has_one[\$component] : null;"); $hasOne = Object::get_static($class, 'has_one');
if($candidate) {
return $candidate; if(isset($hasOne[$component])) {
return $hasOne[$component];
} }
} else { } else {
$newItems = eval("return (array){$class}::\$has_one;"); $newItems = (array) Object::get_static($class, 'has_one');
// Validate the data // Validate the data
foreach($newItems as $k => $v) { foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_one has a bad entry: " if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_one has a bad entry: "
@ -1310,12 +1351,13 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
} }
if($component) { if($component) {
$candidate = eval("return isset({$class}::\$db[\$component]) ? {$class}::\$db[\$component] : null;"); $db = Object::get_static($class, 'db');
if($candidate) {
return $candidate; if(isset($db[$component])) {
return $db[$component];
} }
} else { } else {
$newItems = eval("return (array){$class}::\$db;"); $newItems = (array) Object::get_static($class, 'db');
// Validate the data // Validate the data
foreach($newItems as $k => $v) { foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$db has a bad entry: " if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$db has a bad entry: "
@ -1343,13 +1385,13 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue; if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
if($component) { if($component) {
$candidate = eval("return isset({$class}::\$has_many[\$component]) ? {$class}::\$has_many[\$component] : null;"); $hasMany = Object::get_static($class, 'has_many');
$candidate = eval("if ( isset({$class}::\$has_many[\$component]) ) { return {$class}::\$has_many[\$component]; } else { return false; }");
if($candidate) { if(isset($hasMany[$component])) {
return $candidate; return $hasMany[$component];
} }
} else { } else {
$newItems = eval("return (array){$class}::\$has_many;"); $newItems = (array) Object::get_static($class, 'has_many');
// Validate the data // Validate the data
foreach($newItems as $k => $v) { foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_many has a bad entry: " if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_many has a bad entry: "
@ -1379,7 +1421,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue; if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
if($component) { if($component) {
$manyMany = singleton($class)->stat('many_many'); $manyMany = Object::get_static($class, 'many_many');
// Try many_many // Try many_many
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null; $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) { if($candidate) {
@ -1389,13 +1431,13 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
} }
// Try belongs_many_many // Try belongs_many_many
$belongsManyMany = singleton($class)->stat('belongs_many_many'); $belongsManyMany = Object::get_static($class, 'belongs_many_many');
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null; $candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
if($candidate) { if($candidate) {
$childField = $candidate . "ID"; $childField = $candidate . "ID";
// We need to find the inverse component name // We need to find the inverse component name
$otherManyMany = singleton($candidate)->stat('many_many'); $otherManyMany = Object::get_static($candidate, 'many_many');
if(!$otherManyMany) { if(!$otherManyMany) {
user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR); user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR);
} }
@ -1414,15 +1456,15 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
user_error("Orphaned \$belongs_many_many value for $this->class.$component", E_USER_ERROR); user_error("Orphaned \$belongs_many_many value for $this->class.$component", E_USER_ERROR);
} }
} else { } else {
$newItems = eval("return (array){$class}::\$many_many;"); $newItems = (array) Object::get_static($class, 'many_many');
// Validate the data // Validate the data
foreach($newItems as $k => $v) { foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$many_many has a bad entry: " if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$many_many has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR); . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
} }
$items = isset($items) ? array_merge($newItems, $items) : $newItems; $items = isset($items) ? array_merge($newItems, $items) : $newItems;
$newItems = eval("return (array){$class}::\$belongs_many_many;"); $newItems = (array) Object::get_static($class, 'belongs_many_many');
// Validate the data // Validate the data
foreach($newItems as $k => $v) { foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$belongs_many_many has a bad entry: " if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$belongs_many_many has a bad entry: "
@ -1440,7 +1482,6 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* Generates a SearchContext to be used for building and processing * Generates a SearchContext to be used for building and processing
* a generic search form for properties on this object. * a generic search form for properties on this object.
* *
* @usedby {@link ModelAdmin}
* @return SearchContext * @return SearchContext
*/ */
public function getDefaultSearchContext() { public function getDefaultSearchContext() {
@ -1459,7 +1500,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* Some additional logic is included for switching field labels, based on * Some additional logic is included for switching field labels, based on
* how generic or specific the field type is. * how generic or specific the field type is.
* *
* @usedby {@link SearchContext} * Used by {@link SearchContext}.
* *
* @param array $_params * @param array $_params
* 'fieldClasses': Associative array of field names as keys and FormField classes as values * 'fieldClasses': Associative array of field names as keys and FormField classes as values
@ -1552,7 +1593,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* generate this set. To customize, overload this method in a subclass * generate this set. To customize, overload this method in a subclass
* or decorate onto it by using {@link DataObjectDecorator->updateCMSFields()}. * or decorate onto it by using {@link DataObjectDecorator->updateCMSFields()}.
* *
* <example> * <code>
* klass MyCustomClass extends DataObject { * klass MyCustomClass extends DataObject {
* static $db = array('CustomProperty'=>'Boolean'); * static $db = array('CustomProperty'=>'Boolean');
* *
@ -1562,7 +1603,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* return $fields; * return $fields;
* } * }
* } * }
* </example> * </code>
* *
* @see Good example of complex FormField building: SiteTree::getCMSFields() * @see Good example of complex FormField building: SiteTree::getCMSFields()
* *
@ -1829,7 +1870,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if($field == "Version") return $this->hasExtension('Versioned') ? "Int" : false; if($field == "Version") return $this->hasExtension('Versioned') ? "Int" : false;
// get cached fieldmap // get cached fieldmap
$fieldMap = $this->uninherited('_cache_hasOwnTableDatabaseField'); $fieldMap = isset(self::$cache_has_own_table_field[$this->class]) ? self::$cache_has_own_table_field[$this->class] : null;
// if no fieldmap is cached, get all fields // if no fieldmap is cached, get all fields
if(!$fieldMap) { if(!$fieldMap) {
@ -1844,18 +1885,22 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
} }
// set cached fieldmap // set cached fieldmap
$this->set_uninherited('_cache_hasOwnTableDatabaseField', $fieldMap); self::$cache_has_own_table_field[$this->class] = $fieldMap;
} }
// Remove string-based "constructor-arguments" from the DBField definition // Remove string-based "constructor-arguments" from the DBField definition
return isset($fieldMap[$field]) ? strtok($fieldMap[$field],'(') : null; return isset($fieldMap[$field]) ? strtok($fieldMap[$field],'(') : null;
} }
/** /**
* Returns true if given class has its own table. * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
* Uses the rules for whether the table should exist rather than actually looking in the database. * actually looking in the database.
*
* @param string $dataClass
* @return bool
*/ */
public function has_own_table($dataClass) { public function has_own_table($dataClass) {
// The condition below has the same effect as !is_subclass_of($dataClass,'DataObject'), // The condition below has the same effect as !is_subclass_of($dataClass,'DataObject'),
// which causes PHP < 5.3 to segfault in rare circumstances, see PHP bug #46753 // which causes PHP < 5.3 to segfault in rare circumstances, see PHP bug #46753
if($dataClass == 'DataObject' || !in_array('DataObject', ClassInfo::ancestry($dataClass))) return false; if($dataClass == 'DataObject' || !in_array('DataObject', ClassInfo::ancestry($dataClass))) return false;
@ -1864,16 +1909,12 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if(get_parent_class($dataClass) == 'DataObject') { if(get_parent_class($dataClass) == 'DataObject') {
self::$cache_has_own_table[$dataClass] = true; self::$cache_has_own_table[$dataClass] = true;
} else { } else {
$sng = singleton($dataClass); self::$cache_has_own_table[$dataClass] = Object::uninherited_static($dataClass, 'db') || Object::uninherited_static($dataClass, 'has_one');
self::$cache_has_own_table[$dataClass] = $sng->uninherited('db',true) || $sng->uninherited('has_one',true);
} }
} }
return self::$cache_has_own_table[$dataClass]; return self::$cache_has_own_table[$dataClass];
} }
private static $cache_has_own_table = array();
/** /**
* Returns true if the member is allowed to do the given action. * Returns true if the member is allowed to do the given action.
* *
@ -2141,12 +2182,11 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
$query->select[] = "`$tableClass`.*"; $query->select[] = "`$tableClass`.*";
// Add SQL for multi-value fields // Add SQL for multi-value fields
$SNG = singleton($tableClass); $databaseFields = self::database_fields($tableClass);
$databaseFields = $SNG->databaseFields();
if($databaseFields) foreach($databaseFields as $k => $v) { if($databaseFields) foreach($databaseFields as $k => $v) {
if(!in_array($k, array('ClassName', 'LastEdited', 'Created'))) { if(!in_array($k, array('ClassName', 'LastEdited', 'Created'))) {
if(ClassInfo::classImplements($v, 'CompositeDBField')) { if(ClassInfo::classImplements($v, 'CompositeDBField')) {
$SNG->dbObject($k)->addToQuery($query); singleton($tableClass)->dbObject($k)->addToQuery($query);
} }
} }
} }
@ -2349,6 +2389,15 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
$this->componentCache = array(); $this->componentCache = array();
} }
static function flush_and_destroy_cache() {
if(self::$cache_get_one) foreach(self::$cache_get_one as $class => $items) {
if(is_array($items)) foreach($items as $item) {
if($item) $item->destroy();
}
}
self::$cache_get_one = array();
}
/** /**
* Does the hard work for get_one() * Does the hard work for get_one()
* *
@ -2400,7 +2449,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
*/ */
public static function get_by_id($callerClass, $id) { public static function get_by_id($callerClass, $id) {
if(is_numeric($id)) { if(is_numeric($id)) {
if(singleton($callerClass) instanceof DataObject) { if(is_subclass_of($callerClass, 'DataObject')) {
$tableClasses = ClassInfo::dataClassesFor($callerClass); $tableClasses = ClassInfo::dataClassesFor($callerClass);
$baseClass = array_shift($tableClasses); $baseClass = array_shift($tableClasses);
return DataObject::get_one($callerClass,"`$baseClass`.`ID` = $id"); return DataObject::get_one($callerClass,"`$baseClass`.`ID` = $id");
@ -2527,52 +2576,21 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
// Let any extentions make their own database default data // Let any extentions make their own database default data
$this->extend('requireDefaultRecords', $dummy); $this->extend('requireDefaultRecords', $dummy);
} }
/** /**
* Return the complete set of database fields, including Created, LastEdited and ClassName. * @see DataObject::database_fields()
*
* @return array A map of field name to class of all databases fields on this object
*
*/ */
public function databaseFields() { public function databaseFields() {
// For base tables, add a classname field return self::database_fields($this->class);
if($this->parentClass() == 'DataObject') {
$childClasses = ClassInfo::subclassesFor($this->class);
return array_merge(
array(
"ClassName" => "Enum('" . implode(", ", $childClasses) . "')",
"Created" => "SSDatetime",
"LastEdited" => "SSDatetime",
),
(array)$this->customDatabaseFields()
);
// Child table
} else {
return (array)$this->customDatabaseFields();
}
} }
/** /**
* Get the custom database fields for this object, from self::$db and self::$has_one, * @see DataObject::custom_database_fields()
* but not built-in fields like ID, ClassName, Created, LastEdited.
*
* @return array
*/ */
public function customDatabaseFields() { public function customDatabaseFields() {
$db = $this->uninherited('db',true); return self::custom_database_fields($this->class);
$has_one = $this->uninherited('has_one',true);
$def = $db;
if($has_one) {
foreach($has_one as $field => $joinTo) {
$def[$field . 'ID'] = "ForeignKey";
}
}
return (array)$def;
} }
/** /**
* Returns fields bu traversing the class heirachy in a bottom-up direction. * Returns fields bu traversing the class heirachy in a bottom-up direction.
* *
@ -2584,13 +2602,15 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
* @todo review whether this is still needed after recent API changes * @todo review whether this is still needed after recent API changes
*/ */
public function inheritedDatabaseFields() { public function inheritedDatabaseFields() {
$fields = array(); $fields = array();
$currentObj = $this; $currentObj = $this->class;
while(get_class($currentObj) != 'DataObject') {
$fields = array_merge($fields, (array)$currentObj->customDatabaseFields()); while($currentObj != 'DataObject') {
$currentObj = singleton($currentObj->parentClass()); $fields = array_merge($fields, self::custom_database_fields($currentObj));
$currentObj = get_parent_class($currentObj);
} }
return (array)$fields;
return (array) $fields;
} }
/** /**
@ -2688,7 +2708,10 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
if($ancestry) foreach($ancestry as $ancestorClass) { if($ancestry) foreach($ancestry as $ancestorClass) {
if($ancestorClass == 'ViewableData') break; if($ancestorClass == 'ViewableData') break;
$types = array( $types = array(
'db' => (array)singleton($ancestorClass)->uninherited('db', true) 'db' => (array) Object::uninherited_static($ancestorClass, 'db'),
'has_one' => (array) Object::uninherited_static($ancestorClass, 'has_one'),
'has_many' => (array) Object::uninherited_static($ancestorClass, 'has_many'),
'many_many' => (array) Object::uninherited_static($ancestorClass, 'many_many')
); );
if($includerelations){ if($includerelations){
$types['has_one'] = (array)singleton($ancestorClass)->uninherited('has_one', true); $types['has_one'] = (array)singleton($ancestorClass)->uninherited('has_one', true);

View File

@ -27,26 +27,45 @@ abstract class DataObjectDecorator extends Extension {
'searchable_fields', 'searchable_fields',
); );
/**
* Set the owner of this decorator.
* @param DataObject $owner
*/
function setOwner(Object $owner) {
if(!($owner instanceof DataObject)) {
user_error(sprintf(
"DataObjectDecorator->setOwner(): Trying to decorate an object of class '%s' with '%s',
only Dataobject subclasses are supported.",
get_class($owner), $this->class),
E_USER_ERROR
);
return false;
}
parent::setOwner($owner);
}
/** /**
* Load the extra database fields defined in extraStatics. * Load the extra database fields defined in extraStatics.
*/ */
function loadExtraStatics() { function loadExtraStatics() {
// Don't apply DB fields if the parent object has this extension too // Don't apply DB fields if the parent object has this extension too
if(singleton(get_parent_class($this->owner))->extInstance($this->class)) return; if(Object::has_extension($this->owner->parentClass(), $this->class)) return;
$fields = $this->extraStatics();
$className = $this->owner->class;
if($fields) { if($fields = $this->extraStatics()) {
foreach($fields as $relationType => $fields) { foreach($fields as $relation => $fields) {
if(in_array($relationType, self::$decoratable_statics)) { if(in_array($relation, self::$decoratable_statics)) {
eval("$className::\$$relationType = array_merge((array){$className}::\$$relationType, (array)\$fields);"); // Can't use add_static_var() here as it would merge the array rather than replacing
$this->owner->set_stat($relationType, eval("return $className::\$$relationType;")); Object::set_static (
$this->owner->class,
$relation,
array_merge((array) Object::get_static($this->owner->class, $relation), $fields)
);
} }
// clear previously set caches from DataObject->hasOwnTableDatabaseField()
$this->owner->set_uninherited('_cache_hasOwnTableDatabaseField', null);
} }
DataObject::$cache_has_own_table[$this->owner->class] = null;
DataObject::$cache_has_own_table_field[$this->owner->class] = null;
} }
} }
@ -212,4 +231,4 @@ abstract class DataObjectDecorator extends Extension {
} }
} }
?> ?>

View File

@ -22,6 +22,8 @@ class ErrorPage extends Page {
"ShowInSearch" => 0 "ShowInSearch" => 0
); );
protected static $static_filepath = ASSETS_PATH;
/** /**
* Ensures that there is always a 404 page * Ensures that there is always a 404 page
* by checking if there's an instance of * by checking if there's an instance of
@ -105,12 +107,10 @@ class ErrorPage extends Page {
mkdir(ASSETS_PATH, 02775); mkdir(ASSETS_PATH, 02775);
} }
// Path to the error file in the file store // if the page is published in a language other than default language,
$errorFile = ASSETS_PATH . "/error-$this->ErrorCode.html"; // write a specific language version of the HTML page
$filePath = self::get_filepath_for_errorcode($this->ErrorCode, $this->Lang);
// Attempt to open the file, writing it if it doesn't exist if($fh = fopen($filePath, "w")) {
$fh = @fopen($errorFile, "w");
if($fh) {
fwrite($fh, $errorContent); fwrite($fh, $errorContent);
fclose($fh); fclose($fh);
} else { } else {
@ -143,6 +143,39 @@ class ErrorPage extends Page {
return $labels; return $labels;
} }
/**
* Returns an absolute filesystem path to a static error file
* which is generated through {@link publish()}.
*
* @param int $statusCode A HTTP Statuscode, mostly 404 or 500
* @param String $lang A language code in short locale format, e.g. 'de' (Optional)
* @return String
*/
static function get_filepath_for_errorcode($statusCode, $lang = null) {
if(Translatable::is_enabled() && $lang && $lang != Translatable::default_lang()) {
return self::$static_filepath . "/error-{$statusCode}-{$lang}.html";
} else {
return self::$static_filepath . "/error-{$statusCode}.html";
}
}
/**
* Set the path where static error files are saved through {@link publish()}.
* Defaults to /assets.
*
* @param string $path
*/
static function set_static_filepath($path) {
self::$static_filepath = $path;
}
/**
* @return string
*/
static function get_static_filepath($path) {
return self::$static_filepath;
}
} }
/** /**

View File

@ -32,7 +32,7 @@ class Hierarchy extends DataObjectDecorator {
if($limitToMarked && $rootCall) { if($limitToMarked && $rootCall) {
$this->markingFinished(); $this->markingFinished();
} }
$children = $this->owner->AllChildrenIncludingDeleted($extraArg); $children = $this->owner->AllChildrenIncludingDeleted($extraArg);
if($children) { if($children) {
@ -43,6 +43,7 @@ class Hierarchy extends DataObjectDecorator {
$output = "<ul$attributes>\n"; $output = "<ul$attributes>\n";
foreach($children as $child) { foreach($children as $child) {
if(!$limitToMarked || $child->isMarked()) { if(!$limitToMarked || $child->isMarked()) {
$foundAChild = true; $foundAChild = true;
$output .= eval("return $titleEval;") . "\n" . $output .= eval("return $titleEval;") . "\n" .
@ -356,18 +357,18 @@ class Hierarchy extends DataObjectDecorator {
* @return DataObjectSet * @return DataObjectSet
*/ */
public function Children() { public function Children() {
if(!$this->children) { if(!(isset($this->_cache_children) && $this->_cache_children)) {
$result = $this->owner->stageChildren(false); $result = $this->owner->stageChildren(false);
if(isset($result)) { if(isset($result)) {
$this->children = new DataObjectSet(); $this->_cache_children = new DataObjectSet();
foreach($result as $child) { foreach($result as $child) {
if($child->canView()) { if($child->canView()) {
$this->children->push($child); $this->_cache_children->push($child);
} }
} }
} }
} }
return $this->children; return $this->_cache_children;
} }
/** /**
@ -594,6 +595,12 @@ class Hierarchy extends DataObjectDecorator {
return null; return null;
} }
}
function flushCache() {
$this->_cache_children = null;
$this->_cache_allChildrenIncludingDeleted = null;
$this->_cache_allChildren = null;
}
}
?> ?>

View File

@ -462,8 +462,8 @@ class Image_Cached extends Image {
* Is connected to the URL routing "/image" through sapphire/_config.php, * Is connected to the URL routing "/image" through sapphire/_config.php,
* and used by all iframe-based upload-fields in the CMS. * and used by all iframe-based upload-fields in the CMS.
* *
* @usedby FileIFrameField * Used by {@link FileIFrameField}, {@link ImageField}.
* @usedby ImageField *
* @todo Refactor to using FileIFrameField and ImageField as a controller for the upload, * @todo Refactor to using FileIFrameField and ImageField as a controller for the upload,
* rather than something totally disconnected from the original Form and FormField * rather than something totally disconnected from the original Form and FormField
* context. Without the original context its impossible to control permissions etc. * context. Without the original context its impossible to control permissions etc.
@ -494,8 +494,8 @@ class Image_Uploader extends Controller {
} }
// set reading lang // set reading lang
if(Translatable::is_enabled() && !Director::is_ajax()) { if(singleton('SiteTree')->hasExtension('Translatable') && !Director::is_ajax()) {
Translatable::choose_site_lang(array_keys(Translatable::get_existing_content_languages('SiteTree'))); Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SiteTree')));
} }
parent::init(); parent::init();

View File

@ -143,6 +143,12 @@ class RedirectorPage extends Page {
return $fields; return $fields;
} }
function subPagesToCache() {
$urls = parent::subPagesToCache();
$urls[] = $this->URLSegment . '/';
return $urls;
}
} }
/** /**
@ -170,4 +176,4 @@ class RedirectorPage_Controller extends Page_Controller {
); );
} }
} }
?> ?>

View File

@ -436,7 +436,7 @@ class SQLQuery extends Object {
function filtersOnID() { function filtersOnID() {
return ( return (
$this->where $this->where
&& count($this->where) == 1 //&& count($this->where) == 1
&& preg_match('/^(.*\.)?("|`)?ID("|`)?\s?=/', $this->where[0]) && preg_match('/^(.*\.)?("|`)?ID("|`)?\s?=/', $this->where[0])
); );
} }

View File

@ -169,7 +169,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
static $extensions = array( static $extensions = array(
"Hierarchy", "Hierarchy",
"Translatable('Title', 'MenuTitle', 'Content', 'URLSegment', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Status')",
"Versioned('Stage', 'Live')" "Versioned('Stage', 'Live')"
); );
@ -883,7 +882,10 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if($this->ExtraMeta) { if($this->ExtraMeta) {
$tags .= $this->ExtraMeta . "\n"; $tags .= $this->ExtraMeta . "\n";
} }
$tags .= "<meta http-equiv=\"Content-Language\" content=\"". Translatable::current_lang() ."\"/>\n";
// get the "long" lang name suitable for the HTTP content-language flag (with hyphens instead of underscores)
$currentLang = ($this->hasExtension('Translatable')) ? Translatable::current_locale() : i18n::get_locale();
$tags .= "<meta http-equiv=\"Content-Language\" content=\"". i18n::convert_rfc1766($currentLang) ."\"/>\n";
// DEPRECATED 2.3: Use MetaTags // DEPRECATED 2.3: Use MetaTags
$this->extend('updateMetaTags', $tags); $this->extend('updateMetaTags', $tags);
@ -1725,7 +1727,12 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if(!$this->ShowInMenus) if(!$this->ShowInMenus)
$classes .= " notinmenu"; $classes .= " notinmenu";
//TODO: Add integration
/*
if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
$classes .= " untranslated ";
*/
$classes .= $this->markingClasses(); $classes .= $this->markingClasses();
return $classes; return $classes;

View File

@ -1,44 +1,129 @@
<?php <?php
/** /**
* The {Translatable} decorator allows your DataObjects to have versions in different languages, * The Translatable decorator allows your DataObjects to have versions in different languages,
* defining which fields are can be translated. * defining which fields are can be translated. Translatable can be applied
* to any {@link DataObject} subclass, but is mostly used with {@link SiteTree}.
* Translatable is compatible with the {@link Versioned} extension.
* *
* Common language names (e.g. 'en') are used in {Translatable} for * Locales (e.g. 'en_US') are used in Translatable for identifying a record by language.
* database-entities. On the other hand, the file-based i18n-translations
* always have a "locale" (e.g. 'en_US').
* *
* You can enable {Translatabe} for any DataObject-subclass: * <h2>Configuration</h2>
*
* Enabling Translatable in the $extension array of a DataObject
* <code>
* class MyClass extends DataObject {
* static $extensions = array(
* "Translatable"
* );
* }
* </code>
*
* Enabling Translatable through {@link Object::add_extension()} in your _config.php:
* <example> * <example>
* static $extensions = array( * Object::add_extension('MyClass', 'Translatable');
* "Translatable('MyTranslatableVarchar', 'OtherTranslatableText')"
* );
* </example> * </example>
* *
* Make sure to rebuild the database through /dev/build after enabling translatable.
*
* <h2>Usage</h2>
*
* Getting a translation for an existing instance:
* <code>
* $translatedObj = DataObject::get_one_by_locale('MyObject', 'de_DE');
* </code>
*
* Getting a translation for an existing instance:
* <code>
* $obj = DataObject::get_by_id('MyObject', 99); // original language
* $translatedObj = $obj->getTranslation('de_DE');
* </code>
*
* Getting translations through {@link Translatable::set_reading_locale()}.
* This is *not* a recommended approach, but sometimes inavoidable (e.g. for {@link Versioned} methods).
* <code>
* $obj = DataObject::get_by_id('MyObject', 99); // original language
* $translatedObj = $obj->getTranslation('de_DE');
* </code>
*
* Creating a translation:
* <code>
* $obj = new MyObject();
* $translatedObj = $obj->createTranslation('de_DE');
* </code>
*
* <h2>Usage for SiteTree</h2>
*
* Translatable can be used for subclasses of {@link SiteTree} as well.
* If a child page translation is requested without the parent
* page already having a translation in this language, the extension
* will recursively create translations up the tree.
* Caution: The "URLSegment" property is enforced to be unique across
* languages by auto-appending the language code at the end.
* You'll need to ensure that the appropriate "reading language" is set
* before showing links to other pages on a website: Either
* through setting $_COOKIE['locale'], $_SESSION['locale'] or $_GET['locale'].
* Pages in different languages can have different publication states
* through the {@link Versioned} extension.
*
* Note: You can't get Children() for a parent page in a different language
* through set_reading_lang(). Get the translated parent first.
*
* <code>
* // wrong
* Translatable::set_reading_lang('de');
* $englishParent->Children();
* // right
* Translatable::set_reading_lang('de');
* $germanParent = $englishParent->getTranslation('de');
* $germanParent->Children();
* </code>
*
* <h2>Translation groups</h2>
*
* Each translation can have an associated "master" object in another language which it is based on,
* as defined by the "MasterTranslationID" property. This relation is optional, meaning you can
* create translations which have no representation in the "default language".
* This "original" doesn't have to be in a default language, meaning
* a french translation can have a german original, without either of them having a representation
* in the default english language tree.
* Caution: There is no versioning for translation groups,
* meaning associating an object with a group will affect both stage and live records.
*
* <h2>Character Sets</h2>
*
* Caution: Does not apply any character-set conversion, it is assumed that all content * Caution: Does not apply any character-set conversion, it is assumed that all content
* is stored and represented in UTF-8 (Unicode). Please make sure your database and * is stored and represented in UTF-8 (Unicode). Please make sure your database and
* HTML-templates adjust to this. * HTML-templates adjust to this.
* *
* Caution: Further decorations of DataObject might conflict with this implementation, * <h2>"Default" languages</h2>
* e.g. when overriding the get_one()-calls (which are already extended by {Translatable}).
* *
* Important: If the "default language" of your site is not english (en_US),
* please ensure to set the appropriate default language for
* your content before building the database with Translatable enabled:
* Translatable::set_default_locale(<locale>);
*
* <h2>Uninstalling/Disabling</h2>
*
* Disabling Translatable after creating translations will lead to all
* pages being shown in the default sitetree regardless of their language.
* It is advised to start with a new database after uninstalling Translatable,
* or manually filter out translated objects through their "Locale" property
* in the database.
*
* @author Michael Gall <michael (at) wakeless (dot) net>
* @author Ingo Schommer <ingo (at) silverstripe (dot) com>
* @author Bernat Foj Capell <bernat@silverstripe.com> * @author Bernat Foj Capell <bernat@silverstripe.com>
*
* @package sapphire * @package sapphire
* @subpackage misc * @subpackage misc
*/ */
class Translatable extends DataObjectDecorator { class Translatable extends DataObjectDecorator {
/**
* Indicates if the multilingual feature is enabled
*
* @var boolean
*/
protected static $enabled = false;
/** /**
* The 'default' language. * The 'default' language.
* @var string * @var string
*/ */
protected static $default_lang = 'en'; protected static $default_locale = 'en_US';
/** /**
* The language in which we are reading dataobjects. * The language in which we are reading dataobjects.
@ -47,35 +132,20 @@ class Translatable extends DataObjectDecorator {
* @see Director::get_site_mode() * @see Director::get_site_mode()
* @var string * @var string
*/ */
protected static $reading_lang = null; protected static $reading_locale = null;
/** /**
* Indicates if the start language has been determined using choose_site_lang * Indicates if the start language has been determined using choose_site_locale()
* @var boolean * @var boolean
*/ */
protected static $language_decided = false; protected static $language_decided = false;
/**
* Indicates whether the 'Lang' transformation when modifying queries should be bypassed
* If it's true
*
* @var boolean
*/
protected static $bypass = false;
/** /**
* A cached list of existing tables * A cached list of existing tables
* *
* @var mixed * @var mixed
*/ */
protected static $tableList = null; protected static $tableList = null;
/**
* Dataobject's original ID when we're creating a new language version of an object
*
* @var unknown_type
*/
protected static $creatingFromID;
/** /**
* An array of fields that can be translated. * An array of fields that can be translated.
@ -88,162 +158,118 @@ class Translatable extends DataObjectDecorator {
* @var array * @var array
*/ */
protected $original_values = null; protected $original_values = null;
function getLang() {
$record = $this->owner->toMap();
return (isset($record["Lang"])) ? $record["Lang"] : Translatable::default_lang();
}
/** /**
* Checks if a table given table exists in the db * @var boolean Temporarily override the "auto-filter" for {@link current_locale()}
* * in {@link augmentSQL()}. IMPORTANT: You must set this value back to TRUE
* @param mixed $table Table name * after the temporary usage.
* @return boolean Returns true if $table exists.
*/ */
static function table_exists($table) { protected static $enable_lang_filter = true;
if (!self::$tableList) self::$tableList = DB::tableList();
return isset(self::$tableList[strtolower($table)]);
}
/** /**
* Choose the language the site is currently on. * Choose the language the site is currently on.
* If $_GET['lang'] or $_COOKIE['lang'] is set, then it will use that language, and store it in the session. * If $_GET['locale'] or $_COOKIE['locale'] is set, then it will use that language, and store it in the session.
* Otherwise it checks the session for a possible stored language, either from namespace to the site_mode * Otherwise it checks the session for a possible stored language, either from namespace to the site_mode
* ('site' or 'cms'), or for a 'global' language setting. * ('site' or 'cms'), or for a 'global' language setting.
* The final option is the member preference. * The final option is the member preference.
* *
* @uses Director::get_site_mode() * @todo Re-implement cookie and member option
* *
* @uses Director::get_site_mode()
* @param $langsAvailable array A numerical array of languages which are valid choices (optional) * @param $langsAvailable array A numerical array of languages which are valid choices (optional)
* @return string Selected language (also saved in $reading_lang). * @return string Selected language (also saved in $reading_locale).
*/ */
static function choose_site_lang($langsAvailable = null) { static function choose_site_locale($langsAvailable = array()) {
$siteMode = Director::get_site_mode(); // either 'cms' or 'site' $siteMode = Director::get_site_mode(); // either 'cms' or 'site'
if(self::$reading_locale) {
self::$language_decided = true;
return self::$reading_locale;
}
if(isset($_GET['lang']) && (!isset($langsAvailable) || in_array($_GET['lang'], $langsAvailable))) { if((isset($_GET['locale']) && !$langsAvailable) || (isset($_GET['locale']) && in_array($_GET['locale'], $langsAvailable))) {
// get from GET parameter // get from GET parameter
self::set_reading_lang($_GET['lang']); self::set_reading_locale($_GET['locale']);
} elseif(isset($_COOKIE['lang.' . $siteMode]) && $siteMode && (!isset($langsAvailable) || in_array($_COOKIE['lang.' . $siteMode], $langsAvailable))) {
// get from namespaced cookie
self::set_reading_lang($_COOKIE[$siteMode . '.lang']);
} elseif(isset($_COOKIE['lang']) && (!isset($langsAvailable) || in_array($_COOKIE['lang'], $langsAvailable))) {
// get from generic cookie
self::set_reading_lang($_COOKIE['lang']);
} else if(Session::get('lang.' . $siteMode) && (!isset($langsAvailable) || in_array(Session::get('lang.' . $siteMode), $langsAvailable))) {
// get from namespaced session ('cms' or 'site')
self::set_reading_lang(Session::get('lang.' . $siteMode));
} else if(Session::get('lang.global') && (!isset($langsAvailable) || in_array(Session::get('lang.global'), $langsAvailable))) {
// get from global session
self::set_reading_lang(Session::get('lang.global'));
} else { } else {
// get default lang stored in class self::set_reading_locale(self::default_locale());
self::set_reading_lang(self::default_lang());
} }
return self::$reading_lang;
self::$language_decided = true;
return self::$reading_locale;
} }
/** /**
* Get the current reading language. * Get the current reading language.
* This value has to be set before the schema is built with translatable enabled,
* any changes after this can cause unintended side-effects.
*
* @return string * @return string
*/ */
static function default_lang() { static function default_locale() {
return self::$default_lang; return self::$default_locale;
} }
/** /**
* Set default language. * Set default language.
* *
* @param $lang String * @param $locale String
*/ */
static function set_default_lang($lang) { static function set_default_locale($locale) {
self::$default_lang = $lang; self::$default_locale = $locale;
} }
/** /**
* Check whether the default and current reading language are the same. * Check whether the default and current reading language are the same.
* @return boolean Return true if both default and reading language are the same. * @return boolean Return true if both default and reading language are the same.
*/ */
static function is_default_lang() { static function is_default_locale() {
return (!self::current_lang() || self::$default_lang == self::current_lang()); return (!self::current_locale() || self::$default_locale == self::current_locale());
} }
/** /**
* Get the current reading language. * Get the current reading language.
* @return string * @return string
*/ */
static function current_lang() { static function current_locale() {
if (!self::$language_decided) self::choose_site_lang(); if (!self::$language_decided) self::choose_site_locale();
return self::$reading_lang; return self::$reading_locale;
} }
/** /**
* Set the reading language, either namespaced to 'site' (website content) * Set the reading language, either namespaced to 'site' (website content)
* or 'cms' (management backend). * or 'cms' (management backend). This value is used in {@link augmentSQL()}
* to "auto-filter" all SELECT queries by this language.
* See {@link $enable_lang_filter} on how to override this behaviour temporarily.
* *
* @param string $lang New reading language. * @param string $lang New reading language.
*/ */
static function set_reading_lang($lang) { static function set_reading_locale($locale) {
$key = (Director::get_site_mode()) ? 'lang.' . Director::get_site_mode() : 'lang.global'; self::$reading_locale = $locale;
Session::set($key, $lang); self::$language_decided = true;
self::$reading_lang = $lang;
} }
/** /**
* Get a singleton instance of a class in the given language. * Get a singleton instance of a class in the given language.
* @param string $class The name of the class. * @param string $class The name of the class.
* @param string $lang The name of the language. * @param string $locale The name of the language.
* @param string $filter A filter to be inserted into the WHERE clause. * @param string $filter A filter to be inserted into the WHERE clause.
* @param boolean $cache Use caching (default: false) * @param boolean $cache Use caching (default: false)
* @param string $orderby A sort expression to be inserted into the ORDER BY clause. * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
* @return DataObject * @return DataObject
*/ */
static function get_one_by_lang($class, $lang, $filter = '', $cache = false, $orderby = "") { static function get_one_by_locale($class, $locale, $filter = '', $cache = false, $orderby = "") {
$oldLang = self::current_lang(); $orig = Translatable::current_locale();
self::set_reading_lang($lang); Translatable::set_reading_locale($locale);
$result = DataObject::get_one($class, $filter, $cache, $orderby); $do = DataObject::get_one($class, $filter, $cache, $orderby);
self::set_reading_lang($oldLang); Translatable::set_reading_locale($orig);
return $result; return $do;
}
/**
* Get a singleton instance of a class in the most convenient language (@see choose_site_lang())
*
* @param string $callerClass The name of the class
* @param string $filter A filter to be inserted into the WHERE clause.
* @param boolean $cache Use caching (default: false)
* @param string $orderby A sort expression to be inserted into the ORDER BY clause.
* @return DataObject
*/
static function get_one($callerClass, $filter = "", $cache = false, $orderby = "") {
self::$language_decided = true;
self::$reading_lang = self::default_lang();
$record = DataObject::get_one($callerClass, $filter);
if (!$record) {
self::$bypass = true;
$record = DataObject::get_one($callerClass, $filter, $cache, $orderby);
self::$bypass = false;
if ($record) self::set_reading_lang($record->Lang);
} else {
$langsAvailable = (array)self::get_langs_by_id($callerClass, $record->ID);
$langsAvailable[] = self::default_lang();
$lang = self::choose_site_lang($langsAvailable);
if (isset($lang)) {
$transrecord = self::get_one_by_lang($callerClass, $lang, "`$callerClass`.ID = $record->ID");
if ($transrecord) {
self::set_reading_lang($lang);
$record = $transrecord;
}
}
}
return $record;
} }
/** /**
* Get all the instances of the given class translated to the given language * Get all the instances of the given class translated to the given language
* *
* @param string $class The name of the class * @param string $class The name of the class
* @param string $lang The name of the language * @param string $locale The name of the language
* @param string $filter A filter to be inserted into the WHERE clause. * @param string $filter A filter to be inserted into the WHERE clause.
* @param string $sort A sort expression to be inserted into the ORDER BY clause. * @param string $sort A sort expression to be inserted into the ORDER BY clause.
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
@ -252,38 +278,57 @@ class Translatable extends DataObjectDecorator {
* @param string $having A filter to be inserted into the HAVING clause. * @param string $having A filter to be inserted into the HAVING clause.
* @return mixed The objects matching the conditions. * @return mixed The objects matching the conditions.
*/ */
static function get_by_lang($class, $lang, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") { static function get_by_locale($class, $locale, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") {
$oldLang = self::current_lang(); $oldLang = self::current_locale();
self::set_reading_lang($lang); self::set_reading_locale($locale);
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass, $having); $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass, $having);
self::set_reading_lang($oldLang); self::set_reading_locale($oldLang);
return $result; return $result;
} }
/** /**
* Get a record in his original language version. * Gets all translations for this specific page.
* @param string $class The name of the class. * Doesn't include the language of the current record.
* @param string $originalLangID The original record id. *
* @return DataObject * @return array Numeric array of all language codes, sorted alphabetically.
*/ */
static function get_original($class, $originalLangID) {
$baseClass = $class;
while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p;
return self::get_one_by_lang($class,self::default_lang(),"`$baseClass`.ID = $originalLangID");
}
function getTranslatedLangs() { function getTranslatedLangs() {
$class = ClassInfo::baseDataClass($this->owner->class); //Base Class $langs = array();
$baseDataClass = ClassInfo::baseDataClass($this->owner->class); //Base Class
$translationGroupClass = $baseDataClass . "_translationgroups";
if($this->owner->hasExtension("Versioned") && Versioned::current_stage() == "Live") { if($this->owner->hasExtension("Versioned") && Versioned::current_stage() == "Live") {
$class = $class."_Live"; $baseDataClass = $baseDataClass . "_Live";
} }
$id = $this->owner->ID; $translationGroupID = $this->getTranslationGroup();
if(is_numeric($id)) { if(is_numeric($translationGroupID)) {
$query = new SQLQuery('distinct Lang',"$class","(`$class`.OriginalID =$id)"); $query = new SQLQuery(
'DISTINCT Locale',
sprintf(
'`%s` LEFT JOIN `%s` ON `%s`.`OriginalID` = `%s`.`ID`',
$baseDataClass,
$translationGroupClass,
$translationGroupClass,
$baseDataClass
), // from
sprintf(
'`%s`.`TranslationGroupID` = %d AND `%s`.`Locale` != \'%s\'',
$translationGroupClass,
$translationGroupID,
$baseDataClass,
$this->owner->Locale
) // where
);
$langs = $query->execute()->column(); $langs = $query->execute()->column();
} }
return ($langs) ? array_values($langs) : array(); if($langs) {
$langCodes = array_values($langs);
sort($langCodes);
return $langCodes;
} else {
return array();
};
} }
/** /**
@ -294,177 +339,200 @@ class Translatable extends DataObjectDecorator {
* @return array List of languages * @return array List of languages
*/ */
static function get_langs_by_id($class, $id) { static function get_langs_by_id($class, $id) {
$query = new SQLQuery('Lang',"{$class}_lang","(`{$class}_lang`.OriginalLangID =$id)"); $do = DataObject::get_by_id($class, $id);
$langs = $query->execute()->column(); return ($do ? $do->getTranslatedLangs() : array());
return ($langs) ? array_values($langs) : false;
}
/**
* Writes an object in a certain language. Use this instead of $object->write() if you want to write
* an instance in a determinated language independently of the currently set working language
*
* @param DataObject $object Object to be written
* @param string $lang The name of the language
*/
static function write(DataObject $object, $lang) {
$oldLang = self::current_lang();
self::set_reading_lang($lang);
$result = $object->write();
self::set_reading_lang($oldLang);
} }
/** /**
* Enables the multilingual feature * Enables the multilingual feature
* *
* @deprecated 2.4 Use Object::add_extension('SiteTree', 'Translatable')
*/ */
static function enable() { static function enable() {
self::$enabled = true; Object::add_extension('SiteTree', 'Translatable');
} }
/** /**
* Disable the multilingual feature * Disable the multilingual feature
* *
* @deprecated 2.4 Use Object::remove_extension('SiteTree', 'Translatable')
*/ */
static function disable() { static function disable() {
self::$enabled = false; Object::remove_extension('SiteTree', 'Translatable');
} }
/** /**
* Check whether multilingual support has been enabled * Check whether multilingual support has been enabled
* *
* @deprecated 2.4 Use Object::has_extension('SiteTree', 'Translatable')
* @return boolean True if enabled * @return boolean True if enabled
*/ */
static function is_enabled() { static function is_enabled() {
return self::$enabled; return Object::has_extension('SiteTree', 'Translatable');
} }
/**
* When creating, set the original ID value
*
* @param int $id
*/
static function creating_from($id) {
self::$creatingFromID = $id;
}
//-----------------------------------------------------------------------------------------------//
/** /**
* Construct a new Translatable object. * Construct a new Translatable object.
* @var array $translatableFields The different fields of the object that can be translated. * @var array $translatableFields The different fields of the object that can be translated.
* This is currently not implemented, all fields are marked translatable (see {@link setOwner()}).
*/ */
function __construct($translatableFields) { function __construct($translatableFields = null) {
parent::__construct(); parent::__construct();
// @todo Disabled selection of translatable fields - we're setting all fields as translatable in setOwner()
/*
if(!is_array($translatableFields)) { if(!is_array($translatableFields)) {
$translatableFields = func_get_args(); $translatableFields = func_get_args();
} }
$this->translatableFields = $translatableFields; $this->translatableFields = $translatableFields;
*/
// workaround for extending a method on another decorator (Hierarchy):
// split the method into two calls, and overwrite the wrapper AllChildrenIncludingDeleted()
// Has to be executed even with Translatable disabled, as it overwrites the method with same name
// on Hierarchy class, and routes through to Hierarchy->doAllChildrenIncludingDeleted() instead.
// Caution: There's an additional method for augmentAllChildrenIncludingDeleted()
}
function setOwner(Object $owner) {
parent::setOwner($owner);
// setting translatable fields by inspecting owner - this should really be done in the constructor
$this->translatableFields = array_keys($this->owner->inheritedDatabaseFields());
}
function extraStatics() {
if(get_class($this->owner) == ClassInfo::baseDataClass(get_class($this->owner))) {
return array(
"db" => array(
"Locale" => "Varchar(12)",
"TranslationMasterID" => "Int" // optional relation to a "translation master"
),
"defaults" => array(
"Locale" => Translatable::default_locale() // as an overloaded getter as well: getLang()
)
);
} else {
return array();
}
} }
/**
* Changes any SELECT query thats not filtering on an ID
* to limit by the current language defined in {@link current_locale()}.
* It falls back to "Locale='' OR Lang IS NULL" and assumes that
* this implies querying for the default language.
*
* Use {@link $enable_lang_filter} to temporarily disable this "auto-filtering".
*/
function augmentSQL(SQLQuery &$query) { function augmentSQL(SQLQuery &$query) {
if (! $this->stat('enabled', true)) return false; $lang = Translatable::current_locale();
if((($lang = self::current_lang()) && !self::is_default_lang()) || self::$bypass) { $baseTable = ClassInfo::baseDataClass($this->owner->class);
foreach($query->from as $table => $dummy) { $where = $query->where;
if(!isset($baseTable)) { if(
$baseTable = $table; $lang
} // unless the filter has been temporarily disabled
&& self::$enable_lang_filter
if (self::table_exists("{$table}_lang")) { // DataObject::get_by_id() should work independently of language
$query->renameTable($table, $table . '_lang'); && !$query->filtersOnID()
if (stripos($query->sql(),'.ID')) { // the query contains this table
// Every reference to ID is now OriginalLangID // @todo Isn't this always the case?!
$query->replaceText(".ID",".OriginalLangID"); && array_search($baseTable, array_keys($query->from)) !== false
$query->where = str_replace("`ID`", "`OriginalLangID`",$query->where); // or we're already filtering by Lang (either from an earlier augmentSQL() call or through custom SQL filters)
$query->select[] = "`{$baseTable}_lang`.OriginalLangID AS ID"; && !preg_match('/("|\')Lang("|\')/', $query->getFilter())
} //&& !$query->filtersOnFK()
if ($query->where) foreach ($query->where as $i => $wherecl) { ) {
if (substr($wherecl,0,4) == 'ID =') $qry = "`Locale` = '$lang'";
// Another reference to ID to be changed if(Translatable::is_default_locale()) {
$query->where[$i] = str_replace('ID =','OriginalLangID =',$wherecl); $qry .= " OR `Locale` = '' ";
else { $qry .= " OR `Locale` IS NULL ";
$parts = explode(' AND ',$wherecl);
foreach ($parts as $j => $part) {
// Divide this clause between the left ($innerparts[1]) and right($innerparts[2]) part of the condition
ereg('(`?[[:alnum:]_-]*`?\.?`?[[:alnum:]_-]*`?)(.*)', $part, $innerparts);
if (strpos($innerparts[1],'.') === false)
//it may be ambiguous, so sometimes we will need to add the table
$parts[$j] = ($this->isInAugmentedTable($innerparts[1], $table) ? "`{$table}_lang`." : "")."$part";
else {
/* if the table has been specified we have to determine if the original (without _lang) name has to be used
* because we don't have the queried field in the augmented table (which usually means
* that is not a translatable field)
*/
$clauseparts = explode('.',$innerparts[1]);
$originalTable = str_replace('`','',str_replace('_lang','',$clauseparts[0]));
$parts[$j] = ($this->isInAugmentedTable($clauseparts[1], $originalTable) ? "`{$originalTable}_lang`" : "`$originalTable`")
. ".{$clauseparts[1]}{$innerparts[2]}";
}
}
$query->where[$i] = implode(' AND ',$parts);
}
}
if($table != $baseTable) {
$query->from["{$table}_lang"] = $query->from[$table];
} else {
// _lang is now the base table (the first one)
$query->from = array("{$table}_lang" => $query->from[$table]) + $query->from;
}
// unless we are bypassing this query, add the language filter
if (!self::$bypass) $query->where[] = "`{$table}_lang`.Lang = '$lang'";
// unless this is a deletion, the query is applied to the joined table
if (!$query->delete) {
$query->from[$table] = "INNER JOIN `$table`".
" ON `{$table}_lang`.OriginalLangID = `$table`.ID";
/* if we are selecting fields (not doing counts for example) we need to select everything from
* the original table (was renamed to _lang) since some fields that we require may be there
*/
if ($query->select[0][0] == '`') $query->select = array_merge(array("`$table`.*"),$query->select);
} else unset($query->from[$table]);
} else {
$query->from[$table] = str_replace("`{$table}`.OriginalLangID","`{$table}`.ID",$query->from[$table]);
}
} }
$query->where[] = $qry;
} }
} }
/**
* Create <table>_translation database table to enable
* tracking of "translation groups" in which each related
* translation of an object acts as a sibling, rather than
* a parent->child relation.
*/
function augmentDatabase() {
$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
if($this->owner->class != $baseDataClass) return;
/** $fields = array(
* Check whether a WHERE clause should be applied to the augmented table 'OriginalID' => 'Int',
* 'TranslationGroupID' => 'Int',
* @param string $clause Where clause that need to know if can be applied to the augmented (suffixed) table );
* @param string $table Name of the non-augmented table $indexes = array(
* @return boolean True if the clause can be applied to the augmented table 'OriginalID' => true,
*/ 'TranslationGroupID' => true
function isInAugmentedTable($clause, $table) { );
$clause = str_replace('`','',$clause);
$table = str_replace('_lang','',$table); DB::requireTable("{$baseDataClass}_translationgroups", $fields, $indexes);
if (strpos($table,'_') !== false) return false;
$field = ereg_replace('[[:blank:]]*([[:alnum:]]*).*','\\1',$clause);
$field = trim($field);
$allFields = $this->allFieldsInTable($table);
return (array_search($field,$allFields) !== false);
} }
/** /**
* Determine if the DataObject has any own translatable field (not inherited). * Add a record to a "translation group",
* @return boolean * so its relationship to other translations
* based off the same object can be determined later on.
* See class header for further comments.
*
* @param int $originalID Either the primary key of the record this new translation is based on,
* or the primary key of this record, to create a new translation group
*/ */
function hasOwnTranslatableFields() { public function addTranslationGroup($originalID) {
$ownFields = $this->owner->stat('db'); if(!$this->owner->exists()) return false;
if ($ownFields == singleton($this->owner->parentClass())->stat('db'))return false;
foreach ((array)$this->translatableFields as $translatableField) { $baseDataClass = ClassInfo::baseDataClass($this->owner->class);
if (isset($ownFields[$translatableField])) return true; $existingGroupID = $this->getTranslationGroup($originalID);
if(!$existingGroupID) {
DB::query(
sprintf('INSERT INTO `%s_translationgroups` (`TranslationGroupID`,`OriginalID`) VALUES (%d,%d)', $baseDataClass, $originalID, $this->owner->ID)
);
} }
return false;
} }
/**
* Gets the translation group for the current record.
* This ID might equal the record ID, but doesn't have to -
* it just points to one "original" record in the list.
*
* @return int Numeric ID of the translationgroup in the <classname>_translationgroup table
*/
public function getTranslationGroup() {
if(!$this->owner->exists()) return false;
$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
return DB::query(
sprintf('SELECT `TranslationGroupID` FROM `%s_translationgroups` WHERE `OriginalID` = %d', $baseDataClass, $this->owner->ID)
)->value();
}
/**
* Removes a record from the translation group lookup table.
* Makes no assumptions on other records in the group - meaning
* if this happens to be the last record assigned to the group,
* this group ceases to exist.
*/
public function removeTranslationGroup() {
$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
DB::query(
sprintf('DELETE FROM `%s_translationgroups` WHERE `OriginalID` = %d', $baseDataClass, $this->owner->ID)
);
}
/*
function augmentNumChildrenCountQuery(SQLQuery $query) {
if($this->isTranslation()) {
$query->where[0] = '"ParentID" = '.$this->getOriginalPage()->ID;
}
}
*/
/** /**
* Determine if a table needs Versioned support * Determine if a table needs Versioned support
* This is called at db/build time * This is called at db/build time
@ -473,138 +541,170 @@ class Translatable extends DataObjectDecorator {
* @return boolean * @return boolean
*/ */
function isVersionedTable($table) { function isVersionedTable($table) {
// Every _lang table wants Versioned support return false;
return ($this->owner->databaseFields() && $this->hasOwnTranslatableFields());
} }
function augmentDatabase() { function contentcontrollerInit($controller) {
if (! $this->stat('enabled', true)) return false; Translatable::choose_site_locale();
self::set_reading_lang(self::default_lang()); $controller->Locale = Translatable::current_locale();
$table = $this->owner->class; }
function modelascontrollerInit($controller) {
//$this->contentcontrollerInit($controller);
}
function initgetEditForm($controller) {
$this->contentcontrollerInit($controller);
}
if(($fields = $this->owner->databaseFields()) && $this->hasOwnTranslatableFields()) { /**
//Calculate the required fields * Recursively creates translations for parent pages in this language
foreach ($fields as $field => $type) { * if they aren't existing already. This is a necessity to make
if (array_search($field,$this->translatableFields) === false) unset($fields[$field]); * nested pages accessible in a translated CMS page tree.
} * It would be more userfriendly to grey out untranslated pages,
$metaFields = array_diff((array)$this->owner->databaseFields(), (array)$this->owner->customDatabaseFields()); * but this involves complicated special cases in AllChildrenIncludingDeleted().
$indexes = $this->owner->databaseIndexes(); */
function onBeforeWrite() {
$langFields = array_merge( // If language is not set explicitly, set it to current_locale.
array( // This might be a bit overzealous in assuming the language
"Lang" => "Varchar(12)", // of the content, as a "single language" website might be expanded
"OriginalLangID" => "Int" // later on.
), if(!$this->owner->ID && !$this->owner->Locale) {
$fields, $this->owner->Locale = Translatable::current_locale();
$metaFields }
);
foreach ($indexes as $index => $type) {
if (true === $type && array_search($index,$langFields) === false) unset($indexes[$index]);
}
$langIndexes = array_merge( // Specific logic for SiteTree subclasses.
array( // If page has untranslated parents, create (unpublished) translations
'OriginalLangID_Lang' => '(OriginalLangID, Lang)', // of those as well to avoid having inaccessible children in the sitetree.
'OriginalLangID' => true, // Caution: This logic is very sensitve to infinite loops when translation status isn't determined properly
'Lang' => true, if($this->owner->hasField('ParentID')) {
), if(
(array)$indexes !$this->owner->ID
); && $this->owner->ParentID
&& !$this->owner->Parent()->hasTranslation($this->owner->Locale)
// Create table for translated instances ) {
DB::requireTable("{$table}_lang", $langFields, $langIndexes); $parentTranslation = $this->owner->Parent()->createTranslation($this->owner->Locale);
$this->owner->ParentID = $parentTranslation->ID;
} else { }
DB::dontRequireTable("{$table}_lang"); }
// Specific logic for SiteTree subclasses.
// Append language to URLSegment to disambiguate URLs, meaning "myfrenchpage"
// will save as "myfrenchpage-fr" (only if we're not in the "default language").
// Its bad SEO to have multiple resources with different content (=language) under the same URL.
if($this->owner->hasField('URLSegment')) {
if(!$this->owner->ID && $this->owner->Locale != Translatable::default_locale()) {
$SQL_URLSegment = Convert::raw2sql($this->owner->URLSegment);
$existingOriginalPage = Translatable::get_one_by_lang('SiteTree', Translatable::default_locale(), "`URLSegment` = '{$SQL_URLSegment}'");
if($existingOriginalPage) $this->owner->URLSegment .= "-{$this->owner->Locale}";
}
}
// see onAfterWrite()
if(!$this->owner->ID) {
$this->owner->_TranslatableIsNewRecord = true;
} }
} }
function onAfterWrite() {
/** // hacky way to determine if the record was created in the database,
* Augment a write-record request. // or just updated
* @param SQLQuery $manipulation Query to augment. if($this->owner->_TranslatableIsNewRecord) {
*/ // this would kick in for all new records which are NOT
function augmentWrite(&$manipulation) { // created through createTranslation(), meaning they don't
if (! $this->stat('enabled', true)) return false; // have the translation group automatically set.
if(($lang = self::current_lang()) && !self::is_default_lang()) { $translationGroupID = $this->getTranslationGroup();
$tables = array_keys($manipulation); if(!$translationGroupID) $this->addTranslationGroup($this->owner->_TranslationGroupID ? $this->owner->_TranslationGroupID : $this->owner->ID);
foreach($tables as $table) { unset($this->owner->_TranslatableIsNewRecord);
if (self::table_exists("{$table}_lang")) { unset($this->owner->_TranslationGroupID);
$manipulation["{$table}_lang"] = $manipulation[$table];
if ($manipulation[$table]['command'] == 'insert') {
$fakeID = $this->owner->ID;
// In an insert we've to populate our fields and generate a new id (since the passed one it's relative to $table)
$SessionOrigID = Session::get($this->owner->ID.'_originalLangID');
$manipulation["{$table}_lang"]['fields']['OriginalLangID'] = $this->owner->ID =
( $SessionOrigID ? $SessionOrigID : self::$creatingFromID);
$manipulation["{$table}_lang"]['RecordID'] = $manipulation["{$table}_lang"]['fields']['OriginalLangID'];
// populate lang field
$manipulation["{$table}_lang"]['fields']['Lang'] = "'$lang'" ;
// get a valid id, pre-inserting
DB::query("INSERT INTO {$table}_lang SET Created = NOW(), Lang = '$lang'");
$manipulation["{$table}_lang"]['id'] = $manipulation["{$table}_lang"]['fields']['ID'] = DB::getGeneratedID("{$table}_lang");
$manipulation["{$table}_lang"]['command'] = 'update';
// we don't have to insert anything in $table if we are inserting in $table_lang
unset($manipulation[$table]);
// now dataobjects may create a record before the real write in the base table, so we have to delete it - 20/08/2007
if (is_numeric($fakeID)) DB::query("DELETE FROM $table WHERE ID=$fakeID");
}
else {
if (!isset($manipulation[$table]['fields']['OriginalLangID'])) {
// for those updates that may become inserts populate these fields
$manipulation["{$table}_lang"]['fields']['OriginalLangID'] = $this->owner->ID;
$manipulation["{$table}_lang"]['fields']['Lang'] = "'$lang'";
}
$id = $manipulation["{$table}_lang"]['id'];
if(!$id) user_error("Couldn't find ID in manipulation", E_USER_ERROR);
if (isset($manipulation["{$table}_lang"]['where'])) {
$manipulation["{$table}_lang"]['where'] .= "AND (Lang = '$lang') AND (OriginalLangID = $id)";
} else {
$manipulation["{$table}_lang"]['where'] = "(Lang = '$lang') AND (OriginalLangID = $id)";
}
$realID = DB::query("SELECT ID FROM {$table}_lang WHERE (OriginalLangID = $id) AND (Lang = '$lang') LIMIT 1")->value();
$manipulation["{$table}_lang"]['id'] = $realID;
$manipulation["{$table}_lang"]['RecordID'] = $manipulation["{$table}_lang"]['fields']['OriginalLangID'];
// we could be updating non-translatable fields at the same time, so these will remain
foreach ($manipulation[$table]['fields'] as $field => $dummy) {
if ($this->isInAugmentedTable($field, $table) ) unset($manipulation[$table]['fields'][$field]);
}
if (count($manipulation[$table]['fields']) == 0) unset($manipulation[$table]);
}
foreach ($manipulation["{$table}_lang"]['fields'] as $field => $dummy) {
if (! $this->isInAugmentedTable($field, $table) ) unset($manipulation["{$table}_lang"]['fields'][$field]);
}
}
}
} }
}
}
/**
* Remove the record from the translation group mapping.
*/
function onBeforeDelete() {
$this->removeTranslationGroup();
parent::onBeforeDelete();
}
/**
* Getter specifically for {@link SiteTree} subclasses
* which is hooked in to {@link SiteTree::get_by_url()}.
* Disables translatable to get the page independently
* of the current language setting.
*
* @param string $urlSegment
* @param string $extraFilter
* @param boolean $cache
* @param string|array $orderby
* @return DataObject
*/
function alternateGetByUrl($urlSegment, $extraFilter, $cache = null, $orderby = null) {
$SQL_URLSegment = Convert::raw2sql($urlSegment);
Translatable::disable();
$record = DataObject::get_one('SiteTree', "`URLSegment` = '{$SQL_URLSegment}'");
Translatable::enable();
return $record;
}
//-----------------------------------------------------------------------------------------------// //-----------------------------------------------------------------------------------------------//
/**
* If the record is not shown in the default language, this method
* will try to autoselect a master language which is shown alongside
* the normal formfields as a readonly representation.
* This gives translators a powerful tool for their translation workflow
* without leaving the translated page interface.
* Translatable also adds a new tab "Translation" which shows existing
* translations, as well as a formaction to create new translations based
* on a dropdown with available languages.
*
* @todo This is specific to SiteTree and CMSMain
* @todo Implement a special "translation mode" which triggers display of the
* readonly fields, so you can translation INTO the "default language" while
* seeing readonly fields as well.
*/
function updateCMSFields(FieldSet &$fields) { function updateCMSFields(FieldSet &$fields) {
if(!$this->stat('enabled', true)) return false; // Don't apply these modifications for normal DataObjects - they rely on CMSMain logic
if(!($this->owner instanceof SiteTree)) return;
// add hidden fields for the used language and original record
$fields->push(new HiddenField("Lang", "Lang", $this->getLang()) );
$fields->push(new HiddenField("OriginalID", "OriginalID", $this->owner->OriginalID) );
// used in CMSMain->init() to set language state when reading/writing record
$fields->push(new HiddenField("Locale", "Locale", $this->owner->Locale) );
// if a language other than default language is used, we're in "translation mode", // if a language other than default language is used, we're in "translation mode",
// hence have to modify the original fields // hence have to modify the original fields
$isTranslationMode = (Translatable::default_lang() != $this->getLang() && $this->getLang()); $creating = false;
if($isTranslationMode) { $baseClass = $this->owner->class;
$allFields = $fields->toArray();
while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p;
// try to get the record in "default language"
$originalRecord = $this->owner->getTranslation(Translatable::default_locale());
// if no translation in "default language", fall back to first translation
if(!$originalRecord) {
$translations = $this->owner->getTranslations();
$originalRecord = ($translations) ? $translations->First() : null;
}
$isTranslationMode = $this->owner->Locale != Translatable::default_locale();
if($originalRecord && $isTranslationMode) {
$originalLangID = Session::get($this->owner->ID . '_originalLangID'); $originalLangID = Session::get($this->owner->ID . '_originalLangID');
$translatableFieldNames = $this->getTranslatableFields(); $translatableFieldNames = $this->getTranslatableFields();
$allDataFields = $fields->dataFields(); $allDataFields = $fields->dataFields();
$transformation = new Translatable_Transformation(Translatable::get_original($this->owner->class, $this->owner->ID));
$transformation = new Translatable_Transformation($originalRecord);
// iterate through sequential list of all datafields in fieldset // iterate through sequential list of all datafields in fieldset
// (fields are object references, so we can replace them with the translatable CompositeField) // (fields are object references, so we can replace them with the translatable CompositeField)
foreach($allDataFields as $dataField) { foreach($allDataFields as $dataField) {
if($dataField instanceof HiddenField) continue;
if(in_array($dataField->Name(), $translatableFieldNames)) { if(in_array($dataField->Name(), $translatableFieldNames)) {
//var_dump($dataField->Name());
// if the field is translatable, perform transformation // if the field is translatable, perform transformation
$fields->replaceField($dataField->Name(), $transformation->transformFormField($dataField)); $fields->replaceField($dataField->Name(), $transformation->transformFormField($dataField));
} else { } else {
@ -612,36 +712,77 @@ class Translatable extends DataObjectDecorator {
$fields->replaceField($dataField->Name(), $dataField->performReadonlyTransformation()); $fields->replaceField($dataField->Name(), $dataField->performReadonlyTransformation());
} }
} }
} else {
// if we're not in "translation mode", show a dropdown to create a new translation.
// this action should just be possible when showing the default language,
// you can't create new translations from within a "translation mode" form.
$alreadyTranslatedLangs = array(); } elseif($this->owner->isNew()) {
foreach ($alreadyTranslatedLangs as $i => $langCode) {
$alreadyTranslatedLangs[$i] = i18n::get_language_name($langCode);
}
$fields->addFieldsToTab( $fields->addFieldsToTab(
'Root', 'Root',
new Tab(_t('Translatable.TRANSLATIONS', 'Translations'), new Tab(_t('Translatable.TRANSLATIONS', 'Translations'),
new HeaderField('CreateTransHeader', _t('Translatable.CREATE', 'Create new translation'), 2), new LiteralField('SaveBeforeCreatingTranslationNote',
$langDropdown = new LanguageDropdownField("NewTransLang", _t('Translatable.NEWLANGUAGE', 'New language'), $alreadyTranslatedLangs), sprintf('<p class="message">%s</p>',
$createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create')) _t('Translatable.NOTICENEWPAGE', 'Please save this page before creating a translation')
)
)
) )
); );
if (count($alreadyTranslatedLangs)) { }
$fields->addFieldsToTab(
'Root.Translations', // Show a dropdown to create a new translation.
new FieldSet( // This action is possible both when showing the "default language"
new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3), // and a translation.
new LiteralField('existingtrans',implode(', ',$alreadyTranslatedLangs)) $alreadyTranslatedLangs = $this->getTranslatedLangs();
)
); // We'd still want to show the default lang though,
} // as records in this language might have NULL values in their $Lang property
$langDropdown->addExtraClass('languageDropdown'); // and otherwise wouldn't show up here
$createButton->addExtraClass('createTranslationButton'); //$alreadyTranslatedLangs[Translatable::default_locale()] = i18n::get_locale_name(Translatable::default_locale());
$createButton->includeDefaultJS(false);
// Exclude the current language from being shown.
if(Translatable::current_locale() != Translatable::default_locale()) {
$currentLangKey = array_search(Translatable::current_locale(), $alreadyTranslatedLangs);
if($currentLangKey) unset($alreadyTranslatedLangs[$currentLangKey]);
} }
$fields->addFieldsToTab(
'Root',
new Tab(_t('Translatable.TRANSLATIONS', 'Translations'),
new HeaderField('CreateTransHeader', _t('Translatable.CREATE', 'Create new translation'), 2),
$langDropdown = new LanguageDropdownField(
"NewTransLang",
_t('Translatable.NEWLANGUAGE', 'New language'),
$alreadyTranslatedLangs,
'SiteTree',
'Locale-English'
),
$createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create'))
)
);
$createButton->includeDefaultJS(false);
if($alreadyTranslatedLangs) {
$fields->addFieldToTab(
'Root.Translations',
new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3)
);
$existingTransHTML = '<ul>';
foreach($alreadyTranslatedLangs as $i => $langCode) {
$existingTranslation = $this->owner->getTranslation($langCode);
if($existingTranslation) {
$existingTransHTML .= sprintf('<li><a href="%s">%s</a></li>',
sprintf('admin/show/%d/?locale=%s', $existingTranslation->ID, $langCode),
i18n::get_locale_name($langCode)
);
}
}
$existingTransHTML .= '</ul>';
$fields->addFieldToTab(
'Root.Translations',
new LiteralField('existingtrans',$existingTransHTML)
);
}
$langDropdown->addExtraClass('languageDropdown');
$createButton->addExtraClass('createTranslationButton');
} }
/** /**
@ -651,63 +792,7 @@ class Translatable extends DataObjectDecorator {
* @return array Map where the keys are db, indexes and the values are the table fields * @return array Map where the keys are db, indexes and the values are the table fields
*/ */
function fieldsInExtraTables($table){ function fieldsInExtraTables($table){
return array('db'=>null,'indexes'=>null);
if(($fields = $this->owner->databaseFields()) && $this->hasOwnTranslatableFields()) {
//Calculate the required fields
foreach ($fields as $field => $type) {
if (array_search($field,$this->translatableFields) === false) unset($fields[$field]);
}
$metaFields = array_diff((array)$this->owner->databaseFields(), (array)$this->owner->customDatabaseFields());
$indexes = $this->owner->databaseIndexes();
$langFields = array_merge(
array(
"Lang" => "Varchar(12)",
"OriginalLangID" => "Int"
),
$fields,
$metaFields
);
foreach ($indexes as $index => $type) {
if (true === $type && array_search($index,$langFields) === false) unset($indexes[$index]);
}
return array('db' => $langFields, 'indexes' => $indexes);
}
}
/**
* Get a list of fields in the {$table}_lang table
*
* @param string $table Table name
* @return array
*/
function allFieldsInTable($table){
$fields = singleton($table)->databaseFields();
//Calculate the required fields
foreach ($fields as $field => $type) {
if (array_search($field,$this->translatableFields) === false) unset($fields[$field]);
}
$metaFields = array_diff((array)singleton('DataObject')->databaseFields(), (array)$this->owner->customDatabaseFields());
$langFields = array_merge(
array(
"ID",
"LastEdited",
"Created",
"ClassName",
"Version",
"WasPublished",
"Lang",
"OriginalLangID"
),
$this->translatableFields,
array_keys($fields),
array_keys($metaFields)
);
return $langFields;
} }
/** /**
@ -731,39 +816,290 @@ class Translatable extends DataObjectDecorator {
return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage"; return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage";
} }
/**
* Extends $table with a suffix if required
*
* @param string $table Name of the table
* @return string Extended table name
*/
function extendWithSuffix($table) { function extendWithSuffix($table) {
if((($lang = self::current_lang()) && !self::is_default_lang())) {
if (self::table_exists("{$table}_lang")) return $table.'_lang';
}
return $table; return $table;
} }
/**
* Gets all related translations for the current object,
* excluding itself. See {@link getTranslation()} to retrieve
* a single translated object.
*
* @param string $locale
* @return DataObjectSet
*/
function getTranslations($locale = null) {
if($this->owner->exists()) {
// HACK need to disable language filtering in augmentSQL(),
// as we purposely want to get different language
self::$enable_lang_filter = false;
$translationGroupID = $this->getTranslationGroup();
$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
$filter = sprintf('`%s_translationgroups`.`TranslationGroupID` = %d', $baseDataClass, $translationGroupID);
if($locale) {
$filter .= sprintf(' AND `%s`.`Locale` = \'%s\'', $baseDataClass, Convert::raw2sql($locale));
} else {
// exclude the language of the current owner
$filter .= sprintf(' AND `%s`.`Locale` != \'%s\'', $baseDataClass, $this->owner->Locale);
}
$join = sprintf('LEFT JOIN `%s_translationgroups` ON `%s_translationgroups`.`OriginalID` = `%s`.`ID`',
$baseDataClass,
$baseDataClass,
$baseDataClass
);
if($this->owner->hasExtension("Versioned") && Versioned::current_stage()) {
$translations = Versioned::get_by_stage($this->owner->class, Versioned::current_stage(), $filter, null, $join);
} else {
$translations = DataObject::get($this->owner->class, $filter, null, $join);
}
self::$enable_lang_filter = true;
return $translations;
}
}
/**
* Gets an existing translation based on the language code.
* Use {@link hasTranslation()} as a quicker alternative to check
* for an existing translation without getting the actual object.
*
* @param String $locale
* @return DataObject Translated object
*/
function getTranslation($locale) {
$translations = $this->getTranslations($locale);
return ($translations) ? $translations->First() : null;
}
/**
* Creates a new translation for the owner object of this decorator.
* Checks {@link getTranslation()} to return an existing translation
* instead of creating a duplicate. Writes the record to the database before
* returning it. Use this method if you want the "translation group"
* mechanism to work, meaning that an object knows which group of translations
* it belongs to. For "original records" which are not created through this
* method, the "translation group" is set in {@link onAfterWrite()}.
*
* @param string $locale
* @return DataObject The translated object
*/
function createTranslation($locale) {
if(!$this->owner->exists()) {
user_error('Translatable::createTranslation(): Please save your record before creating a translation', E_USER_ERROR);
}
$existingTranslation = $this->getTranslation($locale);
if($existingTranslation) return $existingTranslation;
$class = $this->owner->class;
$newTranslation = new $class;
// copy all fields from owner (apart from ID)
$newTranslation->update($this->owner->toMap());
$newTranslation->ID = 0;
$newTranslation->Locale = $locale;
// hacky way to set an existing translation group in onAfterWrite()
$translationGroupID = $this->getTranslationGroup();
$newTranslation->_TranslationGroupID = $translationGroupID ? $translationGroupID : $this->owner->ID;
$newTranslation->write();
return $newTranslation;
}
/**
* Returns TRUE if the current record has a translation in this language.
* Use {@link getTranslation()} to get the actual translated record from
* the database.
*
* @param string $locale
* @return boolean
*/
function hasTranslation($locale) {
return (array_search($locale, $this->getTranslatedLangs()) !== false);
}
/*
function augmentStageChildren(DataObjectSet $children, $showall = false) {
if($this->isTranslation()) {
$children->merge($this->getOriginalPage()->stageChildren($showall));
}
}
*/
function AllChildrenIncludingDeleted($context = null) {
$children = $this->owner->doAllChildrenIncludingDeleted($context);
return $children;
}
/**
* If called with default language, doesn't affect the results.
* Otherwise (called in translation mode) the method tries to find translations
* for each page in its original language and replace the original.
* The result will contain a mixture of translated and untranslated pages.
*
* Caution: We also create a method AllChildrenIncludingDeleted() dynamically in the class constructor.
*
* @param DataObjectSet $untranslatedChildren
* @param Object $context
*/
/*
function augmentAllChildrenIncludingDeleted(DataObjectSet $children, $context) {
$find = array();
$replace = array();
if($context && $context->Locale && $context->Locale != Translatable::default_locale()) {
if($children) {
foreach($children as $child) {
if($child->hasTranslation($context->Locale)) {
$trans = $child->getTranslation($context->Locale);
if($trans) {
$find[] = $child;
$replace[] = $trans;
}
}
}
foreach($find as $i => $found) {
$children->replace($found, $replace[$i]);
}
}
}
}
*/
/** /**
* Get a list of languages with at least one element translated in (including the default language) * Get a list of languages with at least one element translated in (including the default language)
* *
* @param string $className Look for languages in elements of this class * @param string $className Look for languages in elements of this class
* @return array Map of languages in the form langCode => langName * @return array Map of languages in the form locale => langName
*/ */
static function get_existing_content_languages($className = 'SiteTree', $where = '') { static function get_existing_content_languages($className = 'SiteTree', $where = '') {
if(!Translatable::is_enabled()) return false;
$baseTable = ClassInfo::baseDataClass($className); $baseTable = ClassInfo::baseDataClass($className);
$query = new SQLQuery('Lang',$baseTable.'_lang',$where,"",'Lang'); $query = new SQLQuery('Distinct Locale',$baseTable,$where,"",'Locale');
$dbLangs = $query->execute()->column(); $dbLangs = $query->execute()->column();
$langlist = array_merge((array)Translatable::default_lang(), (array)$dbLangs); $langlist = array_merge((array)Translatable::default_locale(), (array)$dbLangs);
$returnMap = array(); $returnMap = array();
$allCodes = array_merge(i18n::$all_locales, i18n::$common_languages); $allCodes = array_merge(i18n::$all_locales, i18n::$common_locales);
foreach ($langlist as $langCode) { foreach ($langlist as $langCode) {
if($langCode) if($langCode)
$returnMap[$langCode] = (is_array($allCodes[$langCode]) ? $allCodes[$langCode][0] : $allCodes[$langCode]); $returnMap[$langCode] = (is_array($allCodes[$langCode]) ? $allCodes[$langCode][0] : $allCodes[$langCode]);
} }
return $returnMap; return $returnMap;
} }
/**
* Gets a URLSegment value for a homepage in another language.
* The value is inferred by finding the homepage in default language
* (as identified by RootURLController::$default_homepage_urlsegment).
* Returns NULL if no translated page can be found.
*
* @param string $locale
* @return string|boolean URLSegment (e.g. "home")
*/
static function get_homepage_urlsegment_by_language($locale) {
$origHomepageObj = Translatable::get_one_by_locale(
'SiteTree',
Translatable::default_locale(),
sprintf('`URLSegment` = \'%s\'', RootUrlController::get_default_homepage_urlsegment())
);
if($origHomepageObj) {
$translatedHomepageObj = $origHomepageObj->getTranslation(Translatable::current_locale());
if($translatedHomepageObj) {
return $translatedHomepageObj->URLSegment;
}
}
return null;
}
/**
* @deprecated 2.4 Use is_default_locale()
*/
static function is_default_lang() {
return self::is_default_locale();
}
/**
* @deprecated 2.4 Use set_default_locale()
*/
static function set_default_lang($lang) {
self::set_default_locale(i18n::get_locale_from_lang($lang));
}
/**
* @deprecated 2.4 Use get_default_locale()
*/
static function get_default_lang() {
return i18n::get_lang_from_locale(self::get_default_locale());
}
/**
* @deprecated 2.4 Use current_locale()
*/
static function current_lang() {
return i18n::get_lang_from_locale(self::current_locale());
}
/**
* @deprecated 2.4 Use set_reading_locale()
*/
static function set_reading_lang($lang) {
self::set_reading_locale(i18n::get_locale_from_lang($lang));
}
/**
* @deprecated 2.4 Use get_reading_locale()
*/
static function get_reading_lang() {
return i18n::get_lang_from_locale(self::get_reading_locale());
}
/**
* @deprecated 2.4 Use default_locale()
*/
static function default_lang() {
return i18n::get_lang_from_locale(self::default_locale());
}
/**
* @deprecated 2.4 Use get_by_locale()
*/
static function get_by_lang($class, $lang, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") {
return self::get_by_locale($class, i18n::get_locale_from_lang($lang), $filter, $sort, $join, $limit, $containerClass, $having);
}
/**
* @deprecated 2.4 Use get_one_by_locale()
*/
static function get_one_by_lang($class, $lang, $filter = '', $cache = false, $orderby = "") {
return self::get_one_by_locale($class, i18n::get_locale_from_lang($lang), $filter, $cache, $orderby);
}
/**
* Determines if the record has a locale,
* and if this locale is different from the "default locale"
* set in {@link Translatable::default_locale()}.
* Does not look at translation groups to see if the record
* is based on another record.
*
* @return boolean
* @deprecated 2.4
*/
function isTranslation() {
return ($this->owner->Locale && ($this->owner->Locale != Translatable::default_locale()));
}
/**
* @deprecated 2.4 Use choose_site_locale()
*/
static function choose_site_lang($langsAvail=null) {
return self::choose_site_locale($langAvail);
}
} }
@ -821,7 +1157,6 @@ class Translatable_Transformation extends FormTransformation {
$nonEditableField_holder = new CompositeField($nonEditableField); $nonEditableField_holder = new CompositeField($nonEditableField);
$nonEditableField_holder->setName($fieldname.'_holder'); $nonEditableField_holder->setName($fieldname.'_holder');
$nonEditableField_holder->addExtraClass('originallang_holder'); $nonEditableField_holder->addExtraClass('originallang_holder');
$nonEditableField->setValue($this->original->$fieldname); $nonEditableField->setValue($this->original->$fieldname);
$nonEditableField->setName($fieldname.'_original'); $nonEditableField->setName($fieldname.'_original');
$nonEditableField->addExtraClass('originallang'); $nonEditableField->addExtraClass('originallang');

View File

@ -325,22 +325,8 @@ class Versioned extends DataObjectDecorator {
* @return boolean * @return boolean
*/ */
function canBeVersioned($table) { function canBeVersioned($table) {
$dbFields = singleton($table)->databaseFields();
$tableParts = explode('_',$table); return !(!ClassInfo::exists($table) || !is_subclass_of($table, 'DataObject' ) || empty( $dbFields ));
$dbFields = singleton($tableParts[0])->databaseFields();
if (!ClassInfo::exists( $tableParts[0] ) || !is_subclass_of( $tableParts[0], 'DataObject' ) || empty( $dbFields )){
return false;
} else if (count($tableParts)>1) {
foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) {
if ($this->owner->hasExtension($versionableExtension)) {
foreach ((array)$suffixes as $suffix) {
if ($part = array_search($suffix,$tableParts)) unset($tableParts[$part]);
}
}
}
if (count($tableParts)>1) return false;
}
return true;
} }
/** /**

View File

@ -219,9 +219,7 @@ abstract class DBField extends ViewableData {
* Returns a FormField instance used as a default * Returns a FormField instance used as a default
* for form scaffolding. * for form scaffolding.
* *
* @usedby {@link SearchContext} * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}
* @usedby {@link ModelAdmin}
* @usedby {@link DataObject::scaffoldFormFields()}
* *
* @param string $title Optional. Localized title of the generated instance * @param string $title Optional. Localized title of the generated instance
* @return FormField * @return FormField
@ -236,9 +234,7 @@ abstract class DBField extends ViewableData {
* Returns a FormField instance used as a default * Returns a FormField instance used as a default
* for searchform scaffolding. * for searchform scaffolding.
* *
* @usedby {@link SearchContext} * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}.
* @usedby {@link ModelAdmin}
* @usedby {@link DataObject::scaffoldFormFields()}
* *
* @param string $title Optional. Localized title of the generated instance * @param string $title Optional. Localized title of the generated instance
* @return FormField * @return FormField

View File

@ -1,6 +1,8 @@
<?php <?php
/** /**
* *
* @package sapphire
* @subpackage model
*/ */
class Double extends DBField { class Double extends DBField {

View File

@ -21,7 +21,7 @@ class ForeignKey extends Int {
protected static $default_search_filter_class = 'ExactMatchMultiFilter'; protected static $default_search_filter_class = 'ExactMatchMultiFilter';
function __construct($name, $object) { function __construct($name, $object = null) {
$this->object = $object; $this->object = $object;
parent::__construct($name); parent::__construct($name);
} }
@ -52,4 +52,4 @@ class ForeignKey extends Int {
} }
} }
?> ?>

View File

@ -1,12 +1,11 @@
<?php <?php
/** /**
* A special type Int field used for primary keys. * A special type Int field used for primary keys.
* *
* @todo Allow for custom limiting/filtering of scaffoldFormField dropdown * @todo Allow for custom limiting/filtering of scaffoldFormField dropdown
* *
* @param string $name * @package sapphire
* @param DataOject $object The object that this is primary key for (should have a relation with $name) * @subpackage model
*/ */
class PrimaryKey extends Int { class PrimaryKey extends Int {
/** /**
@ -16,6 +15,10 @@ class PrimaryKey extends Int {
protected static $default_search_filter_class = 'ExactMatchMultiFilter'; protected static $default_search_filter_class = 'ExactMatchMultiFilter';
/**
* @param string $name
* @param DataOject $object The object that this is primary key for (should have a relation with $name)
*/
function __construct($name, $object) { function __construct($name, $object) {
$this->object = $object; $this->object = $object;
parent::__construct($name); parent::__construct($name);

View File

@ -190,15 +190,14 @@ abstract class BulkLoader extends ViewableData {
* Useful for generation of spec documents for technical end users. * Useful for generation of spec documents for technical end users.
* *
* Return Format: * Return Format:
* <example> * <code>
* array( * array(
* 'fields' => array('myFieldName'=>'myDescription'), * 'fields' => array('myFieldName'=>'myDescription'),
* 'relations' => array('myRelationName'=>'myDescription'), * 'relations' => array('myRelationName'=>'myDescription'),
* ) * )
* </example> * </code>
* *
* @todo Mix in custom column mappings * @todo Mix in custom column mappings
* @usedby {@link ModelAdmin}
* *
* @return array * @return array
**/ **/

View File

@ -1,5 +1,4 @@
<?php <?php
/** /**
* Class to handle parsing of CSV files, where the column headers are in the first row. * Class to handle parsing of CSV files, where the column headers are in the first row.
* The idea is that you pass it another object to handle the actual procesing of the data in the CSV file. * The idea is that you pass it another object to handle the actual procesing of the data in the CSV file.
@ -19,6 +18,9 @@
* $obj->write(); * $obj->write();
* } * }
* </code> * </code>
*
* @package sapphire
* @subpackage bulkloading
*/ */
class CSVParser extends Object implements Iterator { class CSVParser extends Object implements Iterator {
protected $filename; protected $filename;

View File

@ -1,7 +1,9 @@
<?php <?php
/** /**
* Test reporter optimised for CLI (ie, plain-text) output * Test reporter optimised for CLI (ie, plain-text) output
*
* @package sapphire
* @subpackage testing
*/ */
class CliTestReporter extends SapphireTestReporter { class CliTestReporter extends SapphireTestReporter {

View File

@ -1,7 +1,9 @@
<?php <?php
/** /**
* Allows human reading of a test in a format suitable for agile documentation * Allows human reading of a test in a format suitable for agile documentation
*
* @package sapphire
* @subpackage tools
*/ */
class CodeViewer extends Controller { class CodeViewer extends Controller {
/** /**

View File

@ -255,7 +255,8 @@ class Debug {
if(Director::is_ajax()) { if(Director::is_ajax()) {
echo $friendlyErrorMessage; echo $friendlyErrorMessage;
} else { } else {
if(file_exists(ASSETS_PATH . "/error-$statusCode.html")) { $errorFilePath = ErrorPage::get_filepath_for_errorcode($statusCode, Translatable::current_lang());
if(file_exists($errorfilePath)) {
echo file_get_contents(ASSETS_PATH . "/error-$statusCode.html"); echo file_get_contents(ASSETS_PATH . "/error-$statusCode.html");
} else { } else {
$renderer = new DebugView(); $renderer = new DebugView();

View File

@ -5,7 +5,7 @@
* and includes them as iFrames. * and includes them as iFrames.
* *
* To create your own tests, please use this template: * To create your own tests, please use this template:
* <example> * <code>
* <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> * <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
* <html> * <html>
* <head> * <head>
@ -28,7 +28,7 @@
* <div id="main"></div> * <div id="main"></div>
* </body> * </body>
* </html> * </html>
* </example> * </code>
* *
* @package sapphire * @package sapphire
* @subpackage testing * @subpackage testing

View File

@ -1,8 +1,10 @@
<?php <?php
/** /**
* Gives you a nice way of viewing your data model. * Gives you a nice way of viewing your data model.
* Access at dev/viewmodel * Access at dev/viewmodel
*
* @package sapphire
* @subpackage tools
*/ */
class ModelViewer extends Controller { class ModelViewer extends Controller {
static $url_handlers = array( static $url_handlers = array(

View File

@ -1,8 +1,10 @@
<?php <?php
/** /**
* Class to facilitate command-line output. * Class to facilitate command-line output.
* Support less-trivial output stuff such as colours (on xterm-color) * Support less-trivial output stuff such as colours (on xterm-color)
*
* @package sapphire
* @subpackage dev
*/ */
class SSCli extends Object { class SSCli extends Object {
static function supports_colour() { static function supports_colour() {

View File

@ -85,7 +85,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
/** /**
* Array of * Called once per test case ({@link SapphireTest} subclass).
* This is different to {@link setUp()}, which gets called once
* per method. Useful to initialize expensive operations which
* don't change state for any called method inside the test,
* e.g. dynamically adding an extension. See {@link tear_down_once()}
* for tearing down the state again.
*/
static function set_up_once() {
}
/**
* Array
*/ */
protected $fixtureDictionary; protected $fixtureDictionary;
@ -152,6 +163,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->originalIsRunningTest = null; $this->originalIsRunningTest = null;
} }
static function tear_down_once() {
}
/** /**
* Clear the log of emails sent * Clear the log of emails sent
*/ */

View File

@ -9,6 +9,9 @@
* Changelog: * Changelog:
* 0.6 First created [David Spurr] * 0.6 First created [David Spurr]
* 0.7 Added fix to getTestException provided [Glen Ogilvie] * 0.7 Added fix to getTestException provided [Glen Ogilvie]
*
* @package sapphire
* @subpackage testing
* *
* @version 0.7 2006-03-12 * @version 0.7 2006-03-12
* @author David Spurr * @author David Spurr

32
dev/SapphireTestSuite.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/**
* Light wrapper around {@link PHPUnit_Framework_TestSuite}
* which allows to have {@link setUp()} and {@link tearDown()}
* methods which are called just once per suite, not once per
* test method in each suite/case.
*
* @package sapphire
* @subpackage testing
*/
class SapphireTestSuite extends PHPUnit_Framework_TestSuite {
function setUp() {
foreach($this->groups as $group) {
// Assumption: All testcases in the group are the same, as defined in TestRunner->runTests()
$class = get_class($group[0]);
if(class_exists($class) && is_subclass_of($class, 'SapphireTest')) {
eval("$class::set_up_once();");
}
}
}
function tearDown() {
foreach($this->groups as $group) {
$class = get_class($group[0]);
// Assumption: All testcases in the group are the same, as defined in TestRunner->runTests()
if(class_exists($class) && is_subclass_of($class, 'SapphireTest')) {
eval("$class::tear_down_once();");
}
}
}
}
?>

View File

@ -145,7 +145,7 @@ class TestRunner extends Controller {
foreach($classList as $className) { foreach($classList as $className) {
// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this. // Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
class_exists($className); class_exists($className);
$suite->addTest(new PHPUnit_Framework_TestSuite($className)); $suite->addTest(new SapphireTestSuite($className));
} }
// Remove the error handler so that PHPUnit can add its own // Remove the error handler so that PHPUnit can add its own

View File

@ -469,9 +469,7 @@ class Email extends ViewableData {
* unless overwritten. Also shown to users on live environments * unless overwritten. Also shown to users on live environments
* as a contact address on system error pages. * as a contact address on system error pages.
* *
* @usedby Email->send() * Used by {@link Email->send()}, {@link Email->sendPlain()}, {@link Debug->friendlyError()}.
* @usedby Email->sendPlain()
* @usedby Debug->friendlyError()
* *
* @param string $newEmail * @param string $newEmail
*/ */

View File

@ -13,7 +13,7 @@ class LanguageDropdownField extends GroupedDropdownField {
* @param string $title * @param string $title
* @param array $dontInclude list of languages that won't be included * @param array $dontInclude list of languages that won't be included
* @param string $translatingClass Name of the class with translated instances where to look for used languages * @param string $translatingClass Name of the class with translated instances where to look for used languages
* @param string $list Indicates the source language list. Can be either Common-English, Common-Native Locale * @param string $list Indicates the source language list. Can be either Common-English, Common-Native, Locale-English, Locale-Native
*/ */
function __construct($name, $title, $dontInclude = array(), $translatingClass = 'SiteTree', $list = 'Common-English' ) { function __construct($name, $title, $dontInclude = array(), $translatingClass = 'SiteTree', $list = 'Common-English' ) {
$usedlangs = array_diff( $usedlangs = array_diff(
@ -26,22 +26,24 @@ class LanguageDropdownField extends GroupedDropdownField {
array_flip($dontInclude) array_flip($dontInclude)
); );
if (isset($usedlangs[Translatable::default_lang()])) unset($usedlangs[Translatable::default_lang()]); //if (isset($usedlangs[Translatable::default_locale()])) unset($usedlangs[Translatable::default_locale()]);
if ('Common-English' == $list) $languageList = i18n::get_common_languages(); if ('Common-English' == $list) $languageList = i18n::get_common_languages();
else if ('Common-Native' == $list) $languageList = i18n::get_common_languages(true); else if ('Common-Native' == $list) $languageList = i18n::get_common_languages(true);
else if ('Locale-English' == $list) $languageList = i18n::get_common_locales();
else if ('Locale-Native' == $list) $languageList = i18n::get_common_locales(true);
else $languageList = i18n::get_locale_list(); else $languageList = i18n::get_locale_list();
$alllangs = array_diff( $alllangs = array_diff(
$languageList, $languageList,
(array)$usedlangs, (array)$usedlangs,
$dontInclude $dontInclude
); );
$alllangs = array_flip(array_diff( $alllangs = array_flip(array_diff(
array_flip($alllangs), array_flip($alllangs),
$dontInclude $dontInclude
)); ));
if (isset($alllangs[Translatable::default_lang()])) unset($alllangs[Translatable::default_lang()]); if (isset($alllangs[Translatable::default_locale()])) unset($alllangs[Translatable::default_locale()]);
asort($alllangs); asort($alllangs);
if (count($usedlangs)) { if (count($usedlangs)) {

View File

@ -1,8 +1,10 @@
<?php <?php
/** /**
* This is a form decorator that lets you place a form inside another form. * This is a form decorator that lets you place a form inside another form.
* The actions will be appropriately rewritten so that the nested form gets called, rather than the parent form. * The actions will be appropriately rewritten so that the nested form gets called, rather than the parent form.
*
* @package sapphire
* @subpackage forms
*/ */
class NestedForm extends ViewableData { class NestedForm extends ViewableData {
protected $form; protected $form;

View File

@ -1,5 +1,4 @@
<?php <?php
/************************************************************************************ /************************************************************************************
************************************************************************************ ************************************************************************************
** ** ** **
@ -9,6 +8,10 @@
************************************************************************************ ************************************************************************************
************************************************************************************/ ************************************************************************************/
/**
* @package sapphire
* @subpackage core
*/
$majorVersion = strtok(phpversion(),'.'); $majorVersion = strtok(phpversion(),'.');
if($majorVersion < 5) { if($majorVersion < 5) {
header("HTTP/1.1 500 Server Error"); header("HTTP/1.1 500 Server Error");
@ -26,7 +29,7 @@ if($majorVersion < 5) {
* - Gets an up-to-date manifest from {@link ManifestBuilder} * - Gets an up-to-date manifest from {@link ManifestBuilder}
* - Sets up error handlers with {@link Debug::loadErrorHandlers()} * - Sets up error handlers with {@link Debug::loadErrorHandlers()}
* - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should * - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should
& be defined in an _config.php * be defined in an _config.php
* - Sets up the default director rules using {@link Director::addRules()} * - Sets up the default director rules using {@link Director::addRules()}
* *
* After that, it calls {@link Director::direct()}, which is responsible for doing most of the * After that, it calls {@link Director::direct()}, which is responsible for doing most of the

View File

@ -1,10 +1,9 @@
<?php <?php
/** /**
* @package sapphire * @package sapphire
* @subpackage core * @subpackage core
*
* Alternative main.php file for servers that need the php5 extension * Alternative main.php file for servers that need the php5 extension
*/ */
include("main.php"); include("main.php");
?> ?>

View File

@ -8,7 +8,6 @@
* @package sapphire * @package sapphire
* @subpackage parsers * @subpackage parsers
* @author Ingo Schommer, Silverstripe Ltd. (<firstname>@silverstripe.com) * @author Ingo Schommer, Silverstripe Ltd. (<firstname>@silverstripe.com)
* @usedby Database->databaseError()
*/ */
class SQLFormatter extends Object { class SQLFormatter extends Object {

View File

@ -11,15 +11,9 @@
* *
* In case you need multiple contexts, consider namespacing your request parameters * In case you need multiple contexts, consider namespacing your request parameters
* by using {@link FieldSet->namespace()} on the $fields constructor parameter. * by using {@link FieldSet->namespace()} on the $fields constructor parameter.
* *
* @usedby {@link ModelAdmin} * @package sapphire
* * @subpackage search
* @param string $modelClass The base {@link DataObject} class that search properties related to.
* Also used to generate a set of result objects based on this class.
* @param FieldSet $fields Optional. FormFields mapping to {@link DataObject::$db} properties
* which are to be searched. Derived from modelclass using
* {@link DataObject::scaffoldSearchFields()} if left blank.
* @param array $filters Optional. Derived from modelclass if left blank
*/ */
class SearchContext extends Object { class SearchContext extends Object {
@ -58,8 +52,14 @@ class SearchContext extends Object {
* Usually these values come from a submitted searchform * Usually these values come from a submitted searchform
* in the form of a $_REQUEST object. * in the form of a $_REQUEST object.
* CAUTION: All values should be treated as insecure client input. * CAUTION: All values should be treated as insecure client input.
*/ *
* @param string $modelClass The base {@link DataObject} class that search properties related to.
* Also used to generate a set of result objects based on this class.
* @param FieldSet $fields Optional. FormFields mapping to {@link DataObject::$db} properties
* which are to be searched. Derived from modelclass using
* {@link DataObject::scaffoldSearchFields()} if left blank.
* @param array $filters Optional. Derived from modelclass if left blank
*/
function __construct($modelClass, $fields = null, $filters = null) { function __construct($modelClass, $fields = null, $filters = null) {
$this->modelClass = $modelClass; $this->modelClass = $modelClass;
$this->fields = ($fields) ? $fields : new FieldSet(); $this->fields = ($fields) ? $fields : new FieldSet();

View File

@ -2,6 +2,11 @@
/** /**
* Standard basic search form which conducts a fulltext search on all {@link SiteTree} * Standard basic search form which conducts a fulltext search on all {@link SiteTree}
* objects. * objects.
*
* If multilingual content is enabled through the {@link Translatable} extension,
* only pages the currently set language on the holder for this searchform are found.
* The language is set through a hidden field in the form, which is prepoluated
* with {@link Translatable::current_locale()} when then form is constructed.
* *
* @see Use ModelController and SearchContext for a more generic search implementation based around DataObject * @see Use ModelController and SearchContext for a more generic search implementation based around DataObject
* @package sapphire * @package sapphire
@ -49,6 +54,10 @@ class SearchForm extends Form {
)); ));
} }
if(singleton('SiteTree')->hasExtension('Translatable')) {
$fields->push(new HiddenField('locale', 'locale', Translatable::current_locale()));
}
if(!$actions) { if(!$actions) {
$actions = new FieldSet( $actions = new FieldSet(
new FormAction("getResults", _t('SearchForm.GO', 'Go')) new FormAction("getResults", _t('SearchForm.GO', 'Go'))
@ -93,6 +102,11 @@ class SearchForm extends Form {
public function getResults($pageLength = 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 // legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials
if(!isset($data)) $data = $_REQUEST; if(!isset($data)) $data = $_REQUEST;
// set language (if present)
if(singleton('SiteTree')->hasExtension('Translatable') && isset($data['locale'])) {
Translatable::set_reading_locale($data['locale']);
}
$keywords = $data['Search']; $keywords = $data['Search'];

View File

@ -1,14 +1,9 @@
<?php <?php
/**
* @package search
* @subpackage filters
*/
/** /**
* Matches on rows where the field is not equal to the given value. * Matches on rows where the field is not equal to the given value.
* *
* @package search * @package sapphire
* @subpackage filters * @subpackage search
*/ */
class NegationFilter extends SearchFilter { class NegationFilter extends SearchFilter {

View File

@ -181,8 +181,6 @@ abstract class SearchFilter extends Object {
* Relies on the field being populated with * Relies on the field being populated with
* {@link setValue()} * {@link setValue()}
* *
* @usedby SearchContext
*
* @return boolean * @return boolean
*/ */
public function isEmpty() { public function isEmpty() {

View File

@ -16,7 +16,8 @@ class BasicAuth extends Object {
/** /**
* Require basic authentication. Will request a username and password if none is given. * Require basic authentication. Will request a username and password if none is given.
* *
* @usedby Controller::init() * Used by {@link Controller::init()}.
*
* @param string $realm * @param string $realm
* @param string|array $permissionCode * @param string|array $permissionCode
* @return Member $member * @return Member $member

View File

@ -0,0 +1,85 @@
<?php
/**
* @package sapphire
* @subpackage tasks
*/
class MigrateTranslatableTask extends BuildTask {
protected $title = "Migrate Translatable Task";
protected $description = "Migrates site translations from SilverStripe 2.1/2.2 to new database structure.";
function init() {
if(!Director::is_cli() && !Director::isDev() && !Permission::check("ADMIN")) Security::permissionFailure();
parent::init();
}
function run($request) {
$ids = array();
//$_REQUEST['showqueries'] = 1;
foreach(array('Stage', 'Live') as $stage) {
echo "<h2>Migrating stage $stage</h2>";
echo "<ul>";
$suffix = ($stage == 'Live') ? '_Live' : '';
// First get all entries in SiteTree_lang
// This should be all translated pages
$trans = DB::query('SELECT * FROM SiteTree_lang' . $suffix);
// Iterate over each translated pages
foreach($trans as $oldtrans) {
echo "<li>Migrating $oldtrans[Lang] translation of " . Convert::raw2xml($oldtrans['Title']) . '</li>';
// Get the untranslated page
$original = Versioned::get_one_by_stage($oldtrans['ClassName'], $stage, '`SiteTree`.ID = ' . $oldtrans['OriginalLangID']);
// Clone the original, and set it up as a translation
$newtrans = $original->duplicate(false);
$newtrans->OriginalID = $original->ID;
$newtrans->Lang = $oldtrans['Lang'];
if($stage == 'Live' && array_key_exists($original->ID, $ids)) {
$newtrans->ID = $ids[$original->ID];
}
// Look at each class in the ancestry, and see if there is a _lang table for it
foreach(ClassInfo::ancestry($oldtrans['ClassName']) as $classname) {
$oldtransitem = false;
// If the class is SiteTree, we already have the DB record, else check for the table and get the record
if($classname == 'SiteTree') {
$oldtransitem = $oldtrans;
} elseif(in_array(strtolower($classname) . '_lang', DB::tableList())) {
$oldtransitem = DB::query('SELECT * FROM ' . $classname . '_lang' . $suffix . ' WHERE OriginalLangID = ' . $original->ID . ' AND Lang = \'' . $oldtrans['Lang'] . '\'')->first();
}
// Copy each translated field into the new translation
if($oldtransitem) foreach($oldtransitem as $key => $value) {
if(!in_array($key, array('ID', 'OriginalLangID', 'ClassName', 'Lang'))) {
$newtrans->$key = $value;
}
}
}
// Write the new translation to the database
$sitelang = Translatable::current_lang();
Translatable::set_reading_lang($newtrans->Lang);
$newtrans->writeToStage($stage, true);
Translatable::set_reading_lang($sitelang);
if($stage == 'Stage') {
$ids[$original->ID] = $newtrans->ID;
}
}
echo '</ul>';
}
echo '<strong>Done!</strong>';
}
}
?>

View File

@ -6,9 +6,9 @@ class ArrayDataTest extends SapphireTest {
/* ViewableData objects will be preserved, but other objects will be converted */ /* ViewableData objects will be preserved, but other objects will be converted */
$arrayData = new ArrayData(array( $arrayData = new ArrayData(array(
"A" => new Varchar("A"), "A" => new Varchar("A"),
"B" => new Object(), "B" => new stdClass(),
)); ));
$this->assertEquals("Varchar", get_class($arrayData->A)); $this->assertEquals("Varchar", get_class($arrayData->A));
$this->assertEquals("ArrayData", get_class($arrayData->B)); $this->assertEquals("ArrayData", get_class($arrayData->B));
} }
} }

View File

@ -35,6 +35,10 @@ class ControllerTest extends SapphireTest {
$response = Director::test("ControllerTest_SecuredController/adminonly"); $response = Director::test("ControllerTest_SecuredController/adminonly");
$this->assertEquals(403, $response->getStatusCode()); $this->assertEquals(403, $response->getStatusCode());
// test that a controller without a specified allowed_actions allows actions through
$response = Director::test('ControllerTest_UnsecuredController/stringaction');
$this->assertEquals(200, $response->getStatusCode());
} }
/** /**
@ -105,4 +109,6 @@ class ControllerTest_SecuredController extends Controller {
function adminonly() { function adminonly() {
return "You must be an admin!"; return "You must be an admin!";
} }
} }
class ControllerTest_UnsecuredController extends ControllerTest_SecuredController {}

View File

@ -44,7 +44,7 @@ class ObjectTest extends SapphireTest {
foreach($trueMethods as $method) { foreach($trueMethods as $method) {
$methodU = strtoupper($method); $methodU = strtoupper($method);
$methodL = strtoupper($method); $methodL = strtoupper($method);
$this->assertTrue($obj->hasMethod($method), "Test that obj#$i has method $method"); $this->assertTrue($obj->hasMethod($method), "Test that obj#$i has method $method ($obj->class)");
$this->assertTrue($obj->hasMethod($methodU), "Test that obj#$i has method $methodU"); $this->assertTrue($obj->hasMethod($methodU), "Test that obj#$i has method $methodU");
$this->assertTrue($obj->hasMethod($methodL), "Test that obj#$i has method $methodL"); $this->assertTrue($obj->hasMethod($methodL), "Test that obj#$i has method $methodL");
@ -88,12 +88,6 @@ class ObjectTest extends SapphireTest {
$obj->stat('mystaticProperty'), $obj->stat('mystaticProperty'),
'Uninherited statics through stat() on a singleton behave the same as built-in PHP statics' 'Uninherited statics through stat() on a singleton behave the same as built-in PHP statics'
); );
/*
$this->assertNull(
$obj->stat('mystaticSubProperty'),
'Statics through stat() on a singleton dont inherit statics from child classes'
);
*/
} }
function testStaticInheritanceGetters() { function testStaticInheritanceGetters() {
@ -107,39 +101,6 @@ class ObjectTest extends SapphireTest {
'MyObject', 'MyObject',
'Statics defined on a parent class are available through stat() on a subclass' 'Statics defined on a parent class are available through stat() on a subclass'
); );
/*
$this->assertEquals(
$subObj->uninherited('mystaticProperty'),
'MySubObject',
'Statics re-defined on a subclass are available through uninherited()'
);
*/
}
function testStaticInheritanceSetters() {
static $_SINGLETONS;
$_SINGLETONS = null;
$obj = singleton('ObjectTest_MyObject');
$subObj = singleton('ObjectTest_MyObject');
$subObj->set_uninherited('mystaticProperty', 'MySubObject_changed');
$this->assertEquals(
$obj->stat('mystaticProperty'),
'MyObject',
'Statics set on a subclass with set_uninherited() dont influence parent class'
);
/*
$this->assertEquals(
$subObj->stat('mystaticProperty'),
'MySubObject_changed',
'Statics set on a subclass with set_uninherited() are changed on subclass when using stat()'
);
*/
$this->assertEquals(
$subObj->uninherited('mystaticProperty'),
'MySubObject_changed',
'Statics set on a subclass with set_uninherited() are changed on subclass when using uninherited()'
);
} }
function testStaticSettingOnSingletons() { function testStaticSettingOnSingletons() {
@ -168,16 +129,224 @@ class ObjectTest extends SapphireTest {
'changed', 'changed',
'Statics setting through set_stat() is populated throughout instances without explicitly clearing cache' 'Statics setting through set_stat() is populated throughout instances without explicitly clearing cache'
); );
/* }
/**
* Tests that {@link Object::create()} correctly passes all arguments to the new object
*/
public function testCreateWithArgs() {
$createdObj = Object::create('ObjectTest_CreateTest', 'arg1', 'arg2', array(), null, 'arg5');
$this->assertEquals($createdObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5'));
$strongObj = Object::strong_create('ObjectTest_CreateTest', 'arg1', 'arg2', array(), null, 'arg5');
$this->assertEquals($strongObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5'));
}
/**
* Tests that {@link Object::useCustomClass()} correnctly replaces normal and strong objects
*/
public function testUseCustomClass() {
$obj1 = Object::create('ObjectTest_CreateTest');
$this->assertTrue($obj1->is_a('ObjectTest_CreateTest'));
Object::useCustomClass('ObjectTest_CreateTest', 'ObjectTest_CreateTest2');
$obj2 = Object::create('ObjectTest_CreateTest');
$this->assertTrue($obj2->is_a('ObjectTest_CreateTest2'));
$obj2_2 = Object::strong_create('ObjectTest_CreateTest');
$this->assertTrue($obj2_2->is_a('ObjectTest_CreateTest'));
Object::useCustomClass('ObjectTest_CreateTest', 'ObjectTest_CreateTest3', true);
$obj3 = Object::create('ObjectTest_CreateTest');
$this->assertTrue($obj3->is_a('ObjectTest_CreateTest3'));
$obj3_2 = Object::strong_create('ObjectTest_CreateTest');
$this->assertTrue($obj3_2->is_a('ObjectTest_CreateTest3'));
}
public function testGetExtensions() {
$this->assertEquals( $this->assertEquals(
ObjectTest_MyObject::$mystaticProperty, Object::get_extensions('ObjectTest_ExtensionTest'),
'changed', array(
'Statics setting through set_stat() reflects on PHP built-in statics on the class' 'oBjEcTTEST_ExtendTest1',
"ObjectTest_ExtendTest2",
)
); );
$this->assertEquals(
Object::get_extensions('ObjectTest_ExtensionTest', true),
array(
'oBjEcTTEST_ExtendTest1',
"ObjectTest_ExtendTest2('FOO', 'BAR')",
)
);
$inst = new ObjectTest_ExtensionTest();
$extensions = $inst->getExtensionInstances();
$this->assertEquals(count($extensions), 2);
$this->assertType(
'ObjectTest_ExtendTest1',
$extensions['ObjectTest_ExtendTest1']
);
$this->assertType(
'ObjectTest_ExtendTest2',
$extensions['ObjectTest_ExtendTest2']
);
$this->assertType(
'ObjectTest_ExtendTest1',
$inst->getExtensionInstance('ObjectTest_ExtendTest1')
);
$this->assertType(
'ObjectTest_ExtendTest2',
$inst->getExtensionInstance('ObjectTest_ExtendTest2')
);
}
/**
* Tests {@link Object::has_extension()}, {@link Object::add_extension()}
*/
public function testHasAndAddExtension() {
// ObjectTest_ExtendTest1 is built in via $extensions
$this->assertTrue(
Object::has_extension('ObjectTest_ExtensionTest', 'OBJECTTEST_ExtendTest1'),
"Extensions are detected when set on Object::\$extensions on has_extension() without case-sensitivity"
);
$this->assertTrue(
Object::has_extension('ObjectTest_ExtensionTest', 'ObjectTest_ExtendTest1'),
"Extensions are detected when set on Object::\$extensions on has_extension() without case-sensitivity"
);
$this->assertTrue(
singleton('ObjectTest_ExtensionTest')->hasExtension('ObjectTest_ExtendTest1'),
"Extensions are detected when set on Object::\$extensions on instance hasExtension() without case-sensitivity"
);
// ObjectTest_ExtendTest2 is built in via $extensions (with parameters)
$this->assertTrue(
Object::has_extension('ObjectTest_ExtensionTest', 'ObjectTest_ExtendTest2'),
"Extensions are detected with static has_extension() when set on Object::\$extensions with additional parameters"
);
$this->assertTrue(
singleton('ObjectTest_ExtensionTest')->hasExtension('ObjectTest_ExtendTest2'),
"Extensions are detected with instance hasExtension() when set on Object::\$extensions with additional parameters"
);
$this->assertFalse(
Object::has_extension('ObjectTest_ExtensionTest', 'ObjectTest_ExtendTest3'),
"Other extensions available in the system are not present unless explicitly added to this object when checking through has_extension()"
);
$this->assertFalse(
singleton('ObjectTest_ExtensionTest')->hasExtension('ObjectTest_ExtendTest3'),
"Other extensions available in the system are not present unless explicitly added to this object when checking through instance hasExtension()"
);
// ObjectTest_ExtendTest3 is added manually
Object::add_extension('ObjectTest_ExtensionTest', 'ObjectTest_ExtendTest3("Param")');
$this->assertTrue(
Object::has_extension('ObjectTest_ExtensionTest', 'ObjectTest_ExtendTest3'),
"Extensions are detected with static has_extension() when added through add_extension()"
);
// a singleton() wouldn't work as its already initialized
$objectTest_ExtensionTest = new ObjectTest_ExtensionTest();
$this->assertTrue(
$objectTest_ExtensionTest->hasExtension('ObjectTest_ExtendTest3'),
"Extensions are detected with instance hasExtension() when added through add_extension()"
);
// @todo At the moment, this does NOT remove the extension due to parameterized naming,
// meaning the extension will remain added in further test cases
Object::remove_extension('ObjectTest_ExtensionTest', 'ObjectTest_ExtendTest3');
}
public function testRemoveExtension() {
// manually add ObjectTest_ExtendTest2
Object::add_extension('ObjectTest_ExtensionRemoveTest', 'ObjectTest_ExtendTest2');
$this->assertTrue(
Object::has_extension('ObjectTest_ExtensionRemoveTest', 'ObjectTest_ExtendTest2'),
"Extension added through \$add_extension() are added correctly"
);
Object::remove_extension('ObjectTest_ExtensionRemoveTest', 'ObjectTest_ExtendTest2');
$this->assertFalse(
Object::has_extension('ObjectTest_ExtensionRemoveTest', 'ObjectTest_ExtendTest2'),
"Extension added through \$add_extension() are detected as removed in has_extension()"
);
$this->assertFalse(
singleton('ObjectTest_ExtensionRemoveTest')->hasExtension('ObjectTest_ExtendTest2'),
"Extensions added through \$add_extension() are detected as removed in instances through hasExtension()"
);
// ObjectTest_ExtendTest1 is already present in $extensions
Object::remove_extension('ObjectTest_ExtensionRemoveTest', 'ObjectTest_ExtendTest1');
$this->assertFalse(
Object::has_extension('ObjectTest_ExtensionRemoveTest', 'ObjectTest_ExtendTest1'),
"Extension added through \$extensions are detected as removed in has_extension()"
);
$objectTest_ExtensionRemoveTest = new ObjectTest_ExtensionRemoveTest();
$this->assertFalse(
$objectTest_ExtensionRemoveTest->hasExtension('ObjectTest_ExtendTest1'),
"Extensions added through \$extensions are detected as removed in instances through hasExtension()"
);
}
public function testParentClass() {
$this->assertEquals(Object::create('ObjectTest_MyObject')->parentClass(), 'Object');
}
public function testIsA() {
$this->assertTrue(Object::create('ObjectTest_MyObject')->is_a('Object'));
$this->assertTrue(Object::create('ObjectTest_MyObject')->is_a('ObjectTest_MyObject'));
}
/**
* Tests {@link Object::hasExtension() and Object::extInstance()}
*/
public function testExtInstance() {
$obj = new ObjectTest_ExtensionTest2();
$this->assertTrue($obj->hasExtension('ObjectTest_Extension'));
$this->assertTrue($obj->extInstance('ObjectTest_Extension')->is_a('ObjectTest_Extension'));
}
public function testCacheToFile() {
/*
// This doesn't run properly on our build slave.
$obj = new ObjectTest_CacheTest();
$obj->clearCache('cacheMethod');
$obj->clearCache('cacheMethod', null, array(true));
$obj->clearCache('incNumber');
$this->assertEquals('noarg', $obj->cacheToFile('cacheMethod', -1));
$this->assertEquals('hasarg', $obj->cacheToFile('cacheMethod', -1, null, array(true)));
$this->assertEquals('hasarg', $obj->cacheToFile('cacheMethod', 3600, null, array(true)));
// -1 lifetime will ensure that the cache isn't read - number incremented
$this->assertEquals(1, $obj->cacheToFile('incNumber', -1));
// -1 lifetime will ensure that the cache isn't read - number incremented
$this->assertEquals(2, $obj->cacheToFile('incNumber', -1));
// Number shouldn't be incremented now because we're using the cached version
$this->assertEquals(2, $obj->cacheToFile('incNumber'));
*/ */
} }
public function testExtend() {
$object = new ObjectTest_ExtendTest();
$argument = 'test';
$this->assertEquals($object->extend('extendableMethod'), array('ExtendTest2()'));
$this->assertEquals($object->extend('extendableMethod', $argument), array('ExtendTest2(modified)'));
$this->assertEquals($argument, 'modified');
$this->assertEquals($object->invokeWithExtensions('extendableMethod'), array('ExtendTest()', 'ExtendTest2()'));
$this->assertEquals (
$object->invokeWithExtensions('extendableMethod', 'test'),
array('ExtendTest(test)', 'ExtendTest2(modified)')
);
}
} }
/**#@+
* @ignore
*/
class ObjectTest_T1A extends Object { class ObjectTest_T1A extends Object {
function testMethod() { function testMethod() {
return true; return true;
@ -243,3 +412,77 @@ class ObjectTest_MySubObject extends ObjectTest_MyObject {
static $mystaticSubProperty = "MySubObject"; static $mystaticSubProperty = "MySubObject";
static $mystaticArray = array('two'); static $mystaticArray = array('two');
} }
class ObjectTest_CreateTest extends Object {
public $constructArguments;
public function __construct() {
$this->constructArguments = func_get_args();
parent::__construct();
}
}
class ObjectTest_CreateTest2 extends Object {}
class ObjectTest_CreateTest3 extends Object {}
class ObjectTest_ExtensionTest extends Object {
public static $extensions = array (
'oBjEcTTEST_ExtendTest1',
"ObjectTest_ExtendTest2('FOO', 'BAR')",
);
}
class ObjectTest_ExtensionTest2 extends Object {
public static $extensions = array('ObjectTest_Extension');
}
class ObjectTest_ExtensionRemoveTest extends Object {
public static $extensions = array (
'ObjectTest_ExtendTest1',
);
}
class ObjectTest_Extension extends Extension {}
class ObjectTest_CacheTest extends Object {
public $count = 0;
public function cacheMethod($arg1 = null) {
return ($arg1) ? 'hasarg' : 'noarg';
}
public function incNumber() {
$this->count++;
return $this->count;
}
}
class ObjectTest_ExtendTest extends Object {
public static $extensions = array('ObjectTest_ExtendTest1', 'ObjectTest_ExtendTest2');
public function extendableMethod($argument = null) { return "ExtendTest($argument)"; }
}
class ObjectTest_ExtendTest1 extends Extension {
public function extendableMethod(&$argument = null) {
if($argument) $argument = 'modified';
return null;
}
}
class ObjectTest_ExtendTest2 extends Extension {
public function extendableMethod($argument = null) { return "ExtendTest2($argument)"; }
}
class ObjectTest_ExtendTest3 extends Extension {
public function extendableMethod($argument = null) { return "ExtendTest3($argument)"; }
}
/**#@-*/

View File

@ -1,5 +1,7 @@
<?php <?php
/** /**
* @todo Test Versioned getters
*
* @package sapphire * @package sapphire
* @subpackage tests * @subpackage tests
*/ */
@ -7,38 +9,180 @@ class TranslatableTest extends FunctionalTest {
static $fixture_file = 'sapphire/tests/model/TranslatableTest.yml'; static $fixture_file = 'sapphire/tests/model/TranslatableTest.yml';
protected $recreateTempDb = true;
/** /**
* @todo Necessary because of monolithic Translatable design * @todo Necessary because of monolithic Translatable design
*/ */
protected $origTranslatableSettings = array(); static protected $origTranslatableSettings = array();
function setUp() { static function set_up_once() {
$this->origTranslatableSettings['enabled'] = Translatable::is_enabled(); // needs to recreate the database schema with language properties
$this->origTranslatableSettings['default_lang'] = Translatable::default_lang();
Translatable::enable();
Translatable::set_default_lang("en");
// needs to recreate the database schema with *_lang tables
self::kill_temp_db(); self::kill_temp_db();
self::create_temp_db();
// store old defaults
parent::setUp(); self::$origTranslatableSettings['has_extension'] = singleton('SiteTree')->hasExtension('Translatable');
self::$origTranslatableSettings['default_locale'] = Translatable::default_locale();
// overwrite locale
Translatable::set_default_locale("en_US");
// refresh the decorated statics - different fields in $db with Translatable enabled
if(!self::$origTranslatableSettings['has_extension']) Object::add_extension('SiteTree', 'Translatable');
Object::add_extension('TranslatableTest_DataObject', 'Translatable');
// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
global $_SINGLETONS;
$_SINGLETONS = array();
// @todo Hack to refresh statics on the newly decorated classes
$newSiteTree = new SiteTree();
foreach($newSiteTree->getExtensionInstances() as $extInstance) {
$extInstance->loadExtraStatics();
}
// @todo Hack to refresh statics on the newly decorated classes
$TranslatableTest_DataObject = new TranslatableTest_DataObject();
foreach($TranslatableTest_DataObject->getExtensionInstances() as $extInstance) {
$extInstance->loadExtraStatics();
}
// recreate database with new settings
$dbname = self::create_temp_db();
DB::set_alternative_database_name($dbname);
parent::set_up_once();
} }
function tearDown() { static function tear_down_once() {
if(!$this->origTranslatableSettings['enabled']) Translatable::disable(); if(!self::$origTranslatableSettings['has_extension']) Object::remove_extension('SiteTree', 'Translatable');
Translatable::set_default_lang($this->origTranslatableSettings['default_lang']);
Translatable::set_default_locale(self::$origTranslatableSettings['default_locale']);
self::kill_temp_db(); self::kill_temp_db();
self::create_temp_db(); self::create_temp_db();
parent::tearDown(); parent::tear_down_once();
}
function testTranslationGroups() {
// first in french
$frPage = new SiteTree();
$frPage->Locale = 'fr_FR';
$frPage->write();
// second in english (from french "original")
$enPage = $frPage->createTranslation('en_US');
// third in spanish (from the english translation)
$esPage = $enPage->createTranslation('es_ES');
// test french
$this->assertEquals(
$frPage->getTranslations()->column('Locale'),
array('en_US','es_ES')
);
$this->assertNotNull($frPage->getTranslation('en_US'));
$this->assertEquals(
$frPage->getTranslation('en_US')->ID,
$enPage->ID
);
$this->assertNotNull($frPage->getTranslation('es_ES'));
$this->assertEquals(
$frPage->getTranslation('es_ES')->ID,
$esPage->ID
);
// test english
$this->assertEquals(
$enPage->getTranslations()->column('Locale'),
array('fr_FR','es_ES')
);
$this->assertNotNull($frPage->getTranslation('fr_FR'));
$this->assertEquals(
$enPage->getTranslation('fr_FR')->ID,
$frPage->ID
);
$this->assertNotNull($frPage->getTranslation('es_ES'));
$this->assertEquals(
$enPage->getTranslation('es_ES')->ID,
$esPage->ID
);
// test spanish
$this->assertEquals(
$esPage->getTranslations()->column('Locale'),
array('fr_FR','en_US')
);
$this->assertNotNull($esPage->getTranslation('fr_FR'));
$this->assertEquals(
$esPage->getTranslation('fr_FR')->ID,
$frPage->ID
);
$this->assertNotNull($esPage->getTranslation('en_US'));
$this->assertEquals(
$esPage->getTranslation('en_US')->ID,
$enPage->ID
);
}
function testGetTranslationOnSiteTree() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('fr_FR');
$getTranslationPage = $origPage->getTranslation('fr_FR');
$this->assertNotNull($getTranslationPage);
$this->assertEquals($getTranslationPage->ID, $translatedPage->ID);
}
function testGetTranslatedLanguages() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
// through createTranslation()
$translationAf = $origPage->createTranslation('af_ZA');
// create a new language on an unrelated page which shouldnt be returned from $origPage
$otherPage = new Page();
$otherPage->write();
$otherTranslationEs = $otherPage->createTranslation('es_ES');
$this->assertEquals(
$origPage->getTranslatedLangs(),
array(
'af_ZA',
//'en_US', // default language is not included
),
'Language codes are returned specifically for the queried page through getTranslatedLangs()'
);
$pageWithoutTranslations = new Page();
$pageWithoutTranslations->write();
$this->assertEquals(
$pageWithoutTranslations->getTranslatedLangs(),
array(),
'A page without translations returns empty array through getTranslatedLangs(), ' .
'even if translations for other pages exist in the database'
);
// manual creation of page without an original link
$translationDeWithoutOriginal = new Page();
$translationDeWithoutOriginal->Locale = 'de_DE';
$translationDeWithoutOriginal->write();
$this->assertEquals(
$translationDeWithoutOriginal->getTranslatedLangs(),
array(),
'A translated page without an original doesn\'t return anything through getTranslatedLang()'
);
}
function testTranslationCanHaveSameURLSegment() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
$translatedPage->URLSegment = 'testpage';
$this->assertEquals($origPage->URLSegment, $translatedPage->URLSegment);
} }
function testUpdateCMSFieldsOnSiteTree() { function testUpdateCMSFieldsOnSiteTree() {
$pageOrigLang = $this->objFromFixture('Page', 'home'); $pageOrigLang = $this->objFromFixture('Page', 'testpage_en');
// first test with default language // first test with default language
$fields = $pageOrigLang->getCMSFields(); $fields = $pageOrigLang->getCMSFields();
@ -53,7 +197,7 @@ class TranslatableTest extends FunctionalTest {
); );
// then in "translation mode" // then in "translation mode"
$pageTranslated = Translatable::get_one_by_lang('Page',"fr", "ID = $pageOrigLang->ID"); $pageTranslated = $pageOrigLang->createTranslation('fr_FR');
$fields = $pageTranslated->getCMSFields(); $fields = $pageTranslated->getCMSFields();
$this->assertType( $this->assertType(
'TextField', 'TextField',
@ -68,5 +212,473 @@ class TranslatableTest extends FunctionalTest {
); );
} }
function testDataObjectGetWithReadingLanguage() {
$origTestPage = $this->objFromFixture('Page', 'testpage_en');
$otherTestPage = $this->objFromFixture('Page', 'othertestpage_en');
$translatedPage = $origTestPage->createTranslation('de_DE');
// test in default language
$resultPagesDefaultLang = DataObject::get(
'Page',
sprintf("`SiteTree`.`MenuTitle` = '%s'", 'A Testpage')
);
$this->assertEquals($resultPagesDefaultLang->Count(), 2);
$this->assertContains($origTestPage->ID, $resultPagesDefaultLang->column('ID'));
$this->assertContains($otherTestPage->ID, $resultPagesDefaultLang->column('ID'));
$this->assertNotContains($translatedPage->ID, $resultPagesDefaultLang->column('ID'));
// test in custom language
Translatable::set_reading_locale('de_DE');
$resultPagesCustomLang = DataObject::get(
'Page',
sprintf("`SiteTree`.`MenuTitle` = '%s'", 'A Testpage')
);
$this->assertEquals($resultPagesCustomLang->Count(), 1);
$this->assertNotContains($origTestPage->ID, $resultPagesCustomLang->column('ID'));
$this->assertNotContains($otherTestPage->ID, $resultPagesCustomLang->column('ID'));
// casting as a workaround for types not properly set on duplicated dataobjects from createTranslation()
$this->assertContains((string)$translatedPage->ID, $resultPagesCustomLang->column('ID'));
Translatable::set_reading_locale('en_US');
}
function testDataObjectGetByIdWithReadingLanguage() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
$compareOrigPage = DataObject::get_by_id('Page', $origPage->ID);
$this->assertEquals(
$origPage->ID,
$compareOrigPage->ID,
'DataObject::get_by_id() should work independently of the reading language'
);
}
function testDataObjectGetOneWithReadingLanguage() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
// running the same query twice with different
Translatable::set_reading_locale('de_DE');
$compareTranslatedPage = DataObject::get_one(
'Page',
sprintf("`SiteTree`.`Title` = '%s'", $translatedPage->Title)
);
$this->assertNotNull($compareTranslatedPage);
$this->assertEquals(
$translatedPage->ID,
$compareTranslatedPage->ID,
"Translated page is found through get_one() when reading lang is not the default language"
);
// reset language to default
Translatable::set_reading_locale('de_DE');
}
function testModifyTranslationWithDefaultReadingLang() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
Translatable::set_reading_locale('en_US');
$translatedPage->Title = 'De Modified';
$translatedPage->write();
$savedTranslatedPage = $origPage->getTranslation('de_DE');
$this->assertEquals(
$savedTranslatedPage->Title,
'De Modified',
'Modifying a record in language which is not the reading language should still write the record correctly'
);
$this->assertEquals(
$origPage->Title,
'Home',
'Modifying a record in language which is not the reading language does not modify the original record'
);
}
function testSiteTreePublication() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
Translatable::set_reading_locale('en_US');
$origPage->Title = 'En Modified';
$origPage->write();
// modifying a record in language which is not the reading language should still write the record correctly
$translatedPage->Title = 'De Modified';
$translatedPage->write();
$origPage->publish('Stage', 'Live');
$liveOrigPage = Versioned::get_one_by_stage('Page', 'Live', "`SiteTree`.ID = {$origPage->ID}");
$this->assertEquals(
$liveOrigPage->Title,
'En Modified',
'Publishing a record in its original language publshes correct properties'
);
}
function testDeletingTranslationKeepsOriginal() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
$translatedPageID = $translatedPage->ID;
$translatedPage->delete();
$translatedPage->flushCache();
$origPage->flushCache();
$this->assertNull($origPage->getTranslation('de_DE'));
$this->assertNotNull(DataObject::get_by_id('Page', $origPage->ID));
}
function testHierarchyChildren() {
$parentPage = $this->objFromFixture('Page', 'parent');
$child1Page = $this->objFromFixture('Page', 'child1');
$child2Page = $this->objFromFixture('Page', 'child2');
$child3Page = $this->objFromFixture('Page', 'child3');
$grandchildPage = $this->objFromFixture('Page', 'grandchild');
$parentPageTranslated = $parentPage->createTranslation('de_DE');
$child4PageTranslated = new SiteTree();
$child4PageTranslated->Locale = 'de_DE';
$child4PageTranslated->ParentID = $parentPageTranslated->ID;
$child4PageTranslated->write();
Translatable::set_reading_locale('en_US');
$this->assertEquals(
$parentPage->Children()->column('ID'),
array(
$child1Page->ID,
$child2Page->ID,
$child3Page->ID
),
"Showing Children() in default language doesnt show children in other languages"
);
Translatable::set_reading_locale('de_DE');
$parentPage->flushCache();
$this->assertEquals(
$parentPageTranslated->Children()->column('ID'),
array($child4PageTranslated->ID),
"Showing Children() in translation mode doesnt show children in default languages"
);
// reset language
Translatable::set_reading_locale('en_US');
}
function testHierarchyLiveStageChildren() {
$parentPage = $this->objFromFixture('Page', 'parent');
$child1Page = $this->objFromFixture('Page', 'child1');
$child1Page->publish('Stage', 'Live');
$child2Page = $this->objFromFixture('Page', 'child2');
$child3Page = $this->objFromFixture('Page', 'child3');
$grandchildPage = $this->objFromFixture('Page', 'grandchild');
$parentPageTranslated = $parentPage->createTranslation('de_DE');
$child4PageTranslated = new SiteTree();
$child4PageTranslated->Locale = 'de_DE';
$child4PageTranslated->ParentID = $parentPageTranslated->ID;
$child4PageTranslated->write();
$child4PageTranslated->publish('Stage', 'Live');
$child5PageTranslated = new SiteTree();
$child5PageTranslated->Locale = 'de_DE';
$child5PageTranslated->ParentID = $parentPageTranslated->ID;
$child5PageTranslated->write();
Translatable::set_reading_locale('en_US');
$this->assertNotNull($parentPage->liveChildren());
$this->assertEquals(
$parentPage->liveChildren()->column('ID'),
array(
$child1Page->ID
),
"Showing liveChildren() in default language doesnt show children in other languages"
);
$this->assertNotNull($parentPage->stageChildren());
$this->assertEquals(
$parentPage->stageChildren()->column('ID'),
array(
$child1Page->ID,
$child2Page->ID,
$child3Page->ID
),
"Showing stageChildren() in default language doesnt show children in other languages"
);
Translatable::set_reading_locale('de_DE');
$parentPage->flushCache();
$this->assertNotNull($parentPageTranslated->liveChildren());
$this->assertEquals(
$parentPageTranslated->liveChildren()->column('ID'),
array($child4PageTranslated->ID),
"Showing liveChildren() in translation mode doesnt show children in default languages"
);
$this->assertNotNull($parentPageTranslated->stageChildren());
$this->assertEquals(
$parentPageTranslated->stageChildren()->column('ID'),
array(
$child4PageTranslated->ID,
$child5PageTranslated->ID,
),
"Showing stageChildren() in translation mode doesnt show children in default languages"
);
// reset language
Translatable::set_reading_locale('en_US');
}
function testTranslatablePropertiesOnSiteTree() {
$origObj = $this->objFromFixture('TranslatableTest_Page', 'testpage_en');
$translatedObj = $origObj->createTranslation('fr_FR');
$translatedObj->TranslatableProperty = 'fr_FR';
$translatedObj->write();
$this->assertEquals(
$origObj->TranslatableProperty,
'en_US',
'Creating a translation doesnt affect database field on original object'
);
$this->assertEquals(
$translatedObj->TranslatableProperty,
'fr_FR',
'Translated object saves database field independently of original object'
);
}
function testCreateTranslationOnSiteTree() {
$origPage = $this->objFromFixture('Page', 'testpage_en');
$translatedPage = $origPage->createTranslation('de_DE');
$this->assertEquals($translatedPage->Locale, 'de_DE');
$this->assertNotEquals($translatedPage->ID, $origPage->ID);
$subsequentTranslatedPage = $origPage->createTranslation('de_DE');
$this->assertEquals(
$translatedPage->ID,
$subsequentTranslatedPage->ID,
'Subsequent calls to createTranslation() dont cause new records in database'
);
}
function testTranslatablePropertiesOnDataObject() {
$origObj = $this->objFromFixture('TranslatableTest_DataObject', 'testobject_en');
$translatedObj = $origObj->createTranslation('fr_FR');
$translatedObj->TranslatableProperty = 'fr_FR';
$translatedObj->TranslatableDecoratedProperty = 'fr_FR';
$translatedObj->write();
$this->assertEquals(
$origObj->TranslatableProperty,
'en_US',
'Creating a translation doesnt affect database field on original object'
);
$this->assertEquals(
$origObj->TranslatableDecoratedProperty,
'en_US',
'Creating a translation doesnt affect decorated database field on original object'
);
$this->assertEquals(
$translatedObj->TranslatableProperty,
'fr_FR',
'Translated object saves database field independently of original object'
);
$this->assertEquals(
$translatedObj->TranslatableDecoratedProperty,
'fr_FR',
'Translated object saves decorated database field independently of original object'
);
}
function testCreateTranslationWithoutOriginal() {
$origParentPage = $this->objFromFixture('Page', 'testpage_en');
$translatedParentPage = $origParentPage->createTranslation('de_DE');
$translatedPageWithoutOriginal = new SiteTree();
$translatedPageWithoutOriginal->ParentID = $translatedParentPage->ID;
$translatedPageWithoutOriginal->Locale = 'de_DE';
$translatedPageWithoutOriginal->write();
Translatable::set_reading_locale('de_DE');
$this->assertEquals(
$translatedParentPage->stageChildren()->column('ID'),
array(
$translatedPageWithoutOriginal->ID
),
"Children() still works on a translated page even if no translation group is set"
);
Translatable::set_reading_locale('en_US');
}
function testCreateTranslationTranslatesUntranslatedParents() {
$parentPage = $this->objFromFixture('Page', 'parent');
$child1Page = $this->objFromFixture('Page', 'child1');
$child1PageOrigID = $child1Page->ID;
$grandchildPage = $this->objFromFixture('Page', 'grandchild');
$this->assertFalse($grandchildPage->hasTranslation('de_DE'));
$this->assertFalse($child1Page->hasTranslation('de_DE'));
$this->assertFalse($parentPage->hasTranslation('de_DE'));
$translatedGrandChildPage = $grandchildPage->createTranslation('de_DE');
$this->assertTrue($grandchildPage->hasTranslation('de_DE'));
$this->assertTrue($child1Page->hasTranslation('de_DE'));
$this->assertTrue($parentPage->hasTranslation('de_DE'));
}
function testHierarchyAllChildrenIncludingDeleted() {
// Original tree in 'en_US':
// parent
// child1 (Live only, deleted from stage)
// child2 (Stage only, never published)
// child3 (Stage only, never published, untranslated)
// Translated tree in 'de_DE':
// parent
// child1 (Live only, deleted from stage)
// child2 (Stage only)
// Create parent
$parentPage = $this->objFromFixture('Page', 'parent');
$parentPageID = $parentPage->ID;
// Create parent translation
$translatedParentPage = $parentPage->createTranslation('de_DE');
$translatedParentPageID = $translatedParentPage->ID;
// Create child1
$child1Page = $this->objFromFixture('Page', 'child1');
$child1PageID = $child1Page->ID;
$child1Page->publish('Stage', 'Live');
// Create child1 translation
$child1PageTranslated = $child1Page->createTranslation('de_DE');
$child1PageTranslatedID = $child1PageTranslated->ID;
$child1PageTranslated->publish('Stage', 'Live');
$child1PageTranslated->deleteFromStage('Stage'); // deleted from stage only, record still exists on live
$child1Page->deleteFromStage('Stage'); // deleted from stage only, record still exists on live
// Create child2
$child2Page = $this->objFromFixture('Page', 'child2');
$child2PageID = $child2Page->ID;
// Create child2 translation
$child2PageTranslated = $child2Page->createTranslation('de_DE');
$child2PageTranslatedID = $child2PageTranslated->ID;
// Create child3
$child3Page = $this->objFromFixture('Page', 'child3');
$child3PageID = $child3Page->ID;
// on original parent in default language
Translatable::set_reading_locale('en_US');
SiteTree::flush_and_destroy_cache();
$parentPage = $this->objFromFixture('Page', 'parent');
$children = $parentPage->AllChildrenIncludingDeleted();
$this->assertEquals(
$parentPage->AllChildrenIncludingDeleted()->column('ID'),
array(
$child2PageID,
$child3PageID,
$child1PageID // $child1Page was deleted from stage, so the original record doesn't have the ID set
),
"Showing AllChildrenIncludingDeleted() in default language doesnt show deleted children in other languages"
);
// on original parent in translation mode
Translatable::set_reading_locale('de_DE');
SiteTree::flush_and_destroy_cache();
$parentPage = $this->objFromFixture('Page', 'parent');
$this->assertEquals(
$parentPage->AllChildrenIncludingDeleted()->column('ID'),
array(
$child2PageTranslatedID,
$child1PageTranslatedID // $child1PageTranslated was deleted from stage, so the original record doesn't have the ID set
),
"Showing AllChildrenIncludingDeleted() in translation mode with parent page in default language shows children in default language"
);
// @todo getTranslation() doesn't switch languages for future calls, its handled statically through set_reading_locale()
// // on translated page in translation mode using getTranslation()
// SiteTree::flush_and_destroy_cache();
// $parentPage = $this->objFromFixture('Page', 'parent');
// $translatedParentPage = $parentPage->getTranslation('de_DE');
// $this->assertEquals(
// $translatedParentPage->AllChildrenIncludingDeleted()->column('ID'),
// array(
// $child2PageTranslatedID,
// $child1PageTranslatedID,
// ),
// "Showing AllChildrenIncludingDeleted() in translation mode with translated parent page shows only translated children"
// );
// reset language
Translatable::set_reading_locale('en_US');
}
function testRootUrlDefaultsToTranslatedUrlSegment() {
$_originalHost = $_SERVER['HTTP_HOST'];
$origPage = $this->objFromFixture('Page', 'homepage_en');
$origPage->publish('Stage', 'Live');
$translationDe = $origPage->createTranslation('de_DE');
$translationDe->URLSegment = 'heim';
$translationDe->write();
$translationDe->publish('Stage', 'Live');
// test with translatable enabled
$_SERVER['HTTP_HOST'] = '/?locale=de';
Translatable::set_reading_locale('de_DE');
$this->assertEquals(
RootURLController::get_homepage_urlsegment(),
'heim',
'Homepage with different URLSegment in non-default language is found'
);
// @todo Fix add/remove extension
// test with translatable disabled
// Object::remove_extension('Page', 'Translatable');
// $_SERVER['HTTP_HOST'] = '/';
// $this->assertEquals(
// RootURLController::get_homepage_urlsegment(),
// 'home',
// 'Homepage is showing in default language if ?lang GET variable is left out'
// );
// Object::add_extension('Page', 'Translatable');
// setting back to default
Translatable::set_reading_locale('en_US');
$_SERVER['HTTP_HOST'] = $_originalHost;
}
} }
class TranslatableTest_DataObject extends DataObject implements TestOnly {
// add_extension() used to add decorator at end of file
static $db = array(
'TranslatableProperty' => 'Text'
);
}
class TranslatableTest_Decorator extends DataObjectDecorator implements TestOnly {
function extraStatics() {
return array(
'db' => array(
'TranslatableDecoratedProperty' => 'Text'
)
);
}
}
class TranslatableTest_Page extends Page implements TestOnly {
// static $extensions is inherited from SiteTree,
// we don't need to explicitly specify the fields
static $db = array(
'TranslatableProperty' => 'Text'
);
}
DataObject::add_extension('TranslatableTest_DataObject', 'TranslatableTest_Decorator');
?> ?>

View File

@ -1,12 +1,43 @@
Page: Page:
home: homepage_en:
Title: Home Title: Home
URLSegment: home URLSegment: home
ShowInMenus: Locale: en_US
testpage_en:
SiteTree_lang: Title: Home
home: MenuTitle: A Testpage
OriginalLangID: =>Page.home URLSegment: testpage
Title: Home fr Locale: en_US
Lang: fr othertestpage_en:
ClassName: Page Title: Other Testpage
MenuTitle: A Testpage
URLSegment: othertestpage
Locale: en_US
parent:
Title: Parent
URLSegment: parent
child1:
Title: Child 1
URLSegment: child1
Parent: =>Page.parent
child2:
Title: Child 2
URLSegment: child2
Parent: =>Page.parent
child3:
Title: Child 3
URLSegment: child3
Parent: =>Page.parent
grandchild:
Title: Grandchild
URLSegment: grandchild
Parent: =>Page.child1
TranslatableTest_DataObject:
testobject_en:
TranslatableProperty: en_US
TranslatableDecoratedProperty: en_US
TranslatableTest_Page:
testpage_en:
Title: En
TranslatableProperty: en_US
URLSegment: testpage-en

View File

@ -219,6 +219,8 @@ class SecurityTest extends FunctionalTest {
*/ */
function doTestLoginForm($email, $password, $backURL = 'test/link') { function doTestLoginForm($email, $password, $backURL = 'test/link') {
$this->session()->inst_set('BackURL', $backURL); $this->session()->inst_set('BackURL', $backURL);
$this->get('Security/logout');
$this->get('Security/login'); $this->get('Security/login');
return $this->submitForm( return $this->submitForm(