mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
ced1f25a4e
commit
3469e4d22a
@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage formatters
|
||||
*/
|
||||
|
||||
abstract class DataFormatter extends Object {
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage formatters
|
||||
*/
|
||||
class JSONDataFormatter extends DataFormatter {
|
||||
/**
|
||||
* @todo pass this from the API to the data formatter somehow
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Simple wrapper to allow access to the live site via REST
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage integration
|
||||
*/
|
||||
class VersionedRestfulServer extends Controller {
|
||||
function handleRequest($request) {
|
||||
|
@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage formatters
|
||||
*/
|
||||
class XMLDataFormatter extends DataFormatter {
|
||||
/**
|
||||
* @todo pass this from the API to the data formatter somehow
|
||||
|
@ -116,14 +116,14 @@ class ClassInfo {
|
||||
* through the $class parameter as the first array value.
|
||||
*
|
||||
* Example usage:
|
||||
* <example>
|
||||
* <code>
|
||||
* ClassInfo::subclassesFor('BaseClass');
|
||||
* array(
|
||||
* 0 => 'BaseClass',
|
||||
* 'ChildClass' => 'ChildClass',
|
||||
* 'GrandChildClass' => 'GrandChildClass'
|
||||
* )
|
||||
* </example>
|
||||
* </code>
|
||||
*
|
||||
* @param mixed $class string of the classname or instance of the class
|
||||
* @return array Names of all subclasses as an associative array.
|
||||
|
@ -17,7 +17,6 @@
|
||||
* Objects of type {@link ViewableData} can have an "escaping type",
|
||||
* which determines if they are automatically escaped before output by {@link SSViewer}.
|
||||
*
|
||||
* @usedby ViewableData::XML_val()
|
||||
* @package sapphire
|
||||
* @subpackage misc
|
||||
*/
|
||||
|
@ -93,8 +93,8 @@ if(!isset($_SERVER['HTTP_HOST'])) {
|
||||
/**
|
||||
* Define system paths
|
||||
*/
|
||||
define('BASE_PATH', dirname(dirname($_SERVER['SCRIPT_FILENAME'])));
|
||||
define('BASE_URL', dirname(dirname($_SERVER['SCRIPT_NAME'])));
|
||||
define('BASE_PATH', rtrim(dirname(dirname($_SERVER['SCRIPT_FILENAME'])), DIRECTORY_SEPARATOR));
|
||||
define('BASE_URL', rtrim(dirname(dirname($_SERVER['SCRIPT_NAME'])), DIRECTORY_SEPARATOR));
|
||||
define('MODULES_DIR', 'modules');
|
||||
define('MODULES_PATH', BASE_PATH . '/' . MODULES_DIR);
|
||||
define('THIRDPARTY_DIR', 'jsparty');
|
||||
@ -255,8 +255,20 @@ function getClassFile($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) {
|
||||
static $_SINGLETONS;
|
||||
global $_SINGLETONS;
|
||||
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(!isset($_SINGLETONS[$className])) {
|
||||
|
@ -1,7 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* Add extension that can be added to an object with Object::add_extension().
|
||||
* For DataObject extensions, use DataObjectDecorator
|
||||
* Add extension that can be added to an object with {@link Object::add_extension()}.
|
||||
* 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
|
||||
* @subpackage core
|
||||
@ -35,6 +40,19 @@ abstract class Extension extends Object {
|
||||
public function getOwner() {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -338,7 +338,7 @@ class HTTP {
|
||||
// Now that we've generated them, either output them or attach them to the HTTPResponse as appropriate
|
||||
foreach($responseHeaders as $k => $v) {
|
||||
if($body) $body->addHeader($k, $v);
|
||||
else header("$k: $v");
|
||||
else if(!headers_sent()) header("$k: $v");
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,6 +352,13 @@ class HTTP {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
1265
core/Object.php
1265
core/Object.php
@ -1,629 +1,886 @@
|
||||
<?php
|
||||
/**
|
||||
* Base object that all others should inherit from.
|
||||
* This object provides a number of helper methods that patch over PHP's deficiencies.
|
||||
* A base class for all sapphire objects to inherit from.
|
||||
*
|
||||
* 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
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* @var array $statics
|
||||
*/
|
||||
protected static $statics = array();
|
||||
|
||||
/**
|
||||
* @var array $static_cached
|
||||
*/
|
||||
protected static $static_cached = array();
|
||||
|
||||
/**
|
||||
* This DataObjects extensions, eg Versioned.
|
||||
* @var array
|
||||
* @var array all current extension instances.
|
||||
*/
|
||||
protected $extension_instances = array();
|
||||
|
||||
/**
|
||||
* Extensions to be used on this object. An array of extension names
|
||||
* and parameters eg:
|
||||
* An implementation of the factory method, allows you to create an instance of a class
|
||||
*
|
||||
* static $extensions = array(
|
||||
* "Hierarchy",
|
||||
* "Versioned('Stage', 'Live')",
|
||||
* );
|
||||
* 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()}
|
||||
*
|
||||
* @var array
|
||||
* @param string $class the class name
|
||||
* @param mixed $arguments,... arguments to pass to the constructor
|
||||
* @return Object
|
||||
*/
|
||||
public static $extensions = null;
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $extraStatics
|
||||
* 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
|
||||
*/
|
||||
protected static $extraStatics = array();
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $classConstructed
|
||||
* 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
|
||||
*/
|
||||
protected static $classConstructed = array();
|
||||
public static function useCustomClass($oldClass, $newClass, $strong = false) {
|
||||
if($strong) {
|
||||
self::$strong_classes[$oldClass] = $newClass;
|
||||
} else {
|
||||
self::$custom_classes[$oldClass] = $newClass;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $extraMethods
|
||||
* 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
|
||||
*/
|
||||
protected static $extraMethods = array();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $builtInMethods
|
||||
* 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
|
||||
*/
|
||||
protected static $builtInMethods = array();
|
||||
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));
|
||||
|
||||
/**
|
||||
* @var array $custom_classes Use the class in the value instead of the class in the key
|
||||
*/
|
||||
private static $custom_classes = array();
|
||||
// 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $strong_classes
|
||||
* Set a static variable
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $name the property name to set
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static $strong_classes = array();
|
||||
public static function set_static($class, $name, $value) {
|
||||
self::$statics[$class][$name] = $value;
|
||||
self::$cached_statics[$class][$name] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $uninherited_statics
|
||||
* Get an uninherited static variable - a variable that is explicity set in this class, and not in the parent class.
|
||||
*
|
||||
* @todo Recursively filter out parent statics, currently only inspects the parent class
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
private static $uninherited_statics = array();
|
||||
public static function uninherited_static($class, $name) {
|
||||
$inherited = self::get_static($class, $name);
|
||||
$parent = null;
|
||||
|
||||
function __construct() {
|
||||
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.
|
||||
*
|
||||
* 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 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);
|
||||
} else {
|
||||
self::$extra_statics[$class][$name] = $value;
|
||||
}
|
||||
|
||||
if ($replace) {
|
||||
self::set_static($class, $name, $value);
|
||||
self::$replaced_statics[$class][$name] = true;
|
||||
} else {
|
||||
self::$cached_statics[$class][$name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if a class has a specified extension
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $requiredExtension the class name of the extension to check for.
|
||||
*/
|
||||
public static function has_extension($class, $requiredExtension) {
|
||||
$requiredExtension = strtolower($requiredExtension);
|
||||
if($extensions = self::get_static($class, 'extensions')) foreach($extensions as $extension) {
|
||||
$left = strtolower(Extension::get_classname_without_arguments($extension));
|
||||
$right = strtolower(Extension::get_classname_without_arguments($requiredExtension));
|
||||
if($left == $right) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')"
|
||||
*/
|
||||
public static function add_extension($class, $extension) {
|
||||
if(!preg_match('/([^(]*)/', $extension, $matches)) {
|
||||
return false;
|
||||
}
|
||||
$extensionClass = $matches[1];
|
||||
if(!class_exists($extensionClass)) {
|
||||
user_error(sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass), E_USER_ERROR);
|
||||
}
|
||||
|
||||
if(!is_subclass_of($extensionClass, 'Extension')) {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function remove_extension($class, $extension) {
|
||||
if(self::has_extension($class, $extension)) {
|
||||
self::set_static(
|
||||
$class,
|
||||
'extensions',
|
||||
array_diff(self::get_static($class, 'extensions'), array($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);
|
||||
|
||||
// Set up the extensions
|
||||
if($extensions = $this->stat('extensions')) {
|
||||
foreach($extensions as $extension) {
|
||||
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->setOwner($this);
|
||||
$this->extension_instances[$instance->class] = $instance;
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset(Object::$classConstructed[$this->class])) {
|
||||
if(!isset(self::$classes_constructed[$this->class])) {
|
||||
$this->defineMethods();
|
||||
Object::$classConstructed[$this->class] = true;
|
||||
self::$classes_constructed[$this->class] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a method.
|
||||
* 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.
|
||||
* Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
|
||||
*
|
||||
* @param oldClass = the old classname you want to replace with.
|
||||
* @param customClass = the new Classname you wish to replace the old class with.
|
||||
* @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.
|
||||
*/
|
||||
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.
|
||||
* You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
|
||||
* {@link Object::addWrapperMethod()}
|
||||
*
|
||||
* @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
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
function stat($name, $uncached = false) {
|
||||
if(!$this->class) $this->class = get_class($this);
|
||||
public function __call($method, $arguments) {
|
||||
$method = strtolower($method);
|
||||
|
||||
if(!isset(Object::$static_cached[$this->class][$name]) || $uncached) {
|
||||
$classes = ClassInfo::ancestry($this->class);
|
||||
foreach($classes as $class) {
|
||||
if(isset(Object::$extraStatics[$class][$name])) {
|
||||
$extra = Object::$extraStatics[$class][$name];
|
||||
if(!is_array($extra)) return $extra;
|
||||
break;
|
||||
}
|
||||
if(isset(self::$extra_methods[$this->class][$method])) {
|
||||
$config = self::$extra_methods[$this->class][$method];
|
||||
|
||||
switch(true) {
|
||||
case isset($config['property']) :
|
||||
$obj = $config['index'] !== null ?
|
||||
$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;");
|
||||
Object::$statics[$this->class][$name] = isset($extra) ? array_merge($extra, (array)$stat) : $stat;
|
||||
Object::$static_cached[$this->class][$name] = true;
|
||||
} else {
|
||||
throw new Exception("Object->__call(): the method '$method' does not exist on '$this->class'");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function hasMethod($method) {
|
||||
return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the names of all the methods available on this object
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
return Object::$statics[$this->class][$name];
|
||||
}
|
||||
/**
|
||||
* Set a static variable
|
||||
*/
|
||||
function set_stat($name, $val) {
|
||||
Object::$statics[$this->class][$name] = $val;
|
||||
Object::$static_cached[$this->class][$name] = true;
|
||||
self::$built_in_methods['_set'][$this->class] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this object "exists", i.e., has a sensible value.
|
||||
* Overload this in subclasses.
|
||||
* For example, an empty DataObject record could return false.
|
||||
* 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()}.
|
||||
*
|
||||
* @return boolean
|
||||
* @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() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function parentClass() {
|
||||
/**
|
||||
* @return string this classes parent class
|
||||
*/
|
||||
public function parentClass() {
|
||||
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
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
function is_a($class) {
|
||||
return is_a($this, $class);
|
||||
public function is_a($class) {
|
||||
return $this instanceof $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string the class name
|
||||
*/
|
||||
public function __toString() {
|
||||
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 unknown_type $arg
|
||||
* @param string $method the method name to call
|
||||
* @param mixed $argument a single argument to pass
|
||||
* @return mixed
|
||||
* @todo integrate inheritance rules
|
||||
*/
|
||||
public function invokeWithExtensions($funcName, $arg=null) {
|
||||
$results = array();
|
||||
if (method_exists($this, $funcName)) {
|
||||
$results[] = $this->$funcName($arg);
|
||||
}
|
||||
$extras = $this->extend($funcName, $arg);
|
||||
if ($extras) {
|
||||
return array_merge($results, $extras);
|
||||
} else {
|
||||
return $results;
|
||||
}
|
||||
public function invokeWithExtensions($method, $argument = null) {
|
||||
$result = method_exists($this, $method) ? array($this->$method($argument)) : array();
|
||||
$extras = $this->extend($method, $argument);
|
||||
|
||||
return $extras ? array_merge($result, $extras) : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) {
|
||||
$values = array();
|
||||
|
||||
if($this->extension_instances) {
|
||||
$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;
|
||||
}
|
||||
if($this->extension_instances) foreach($this->extension_instances as $instance) {
|
||||
if($instance->hasMethod($method)) {
|
||||
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
|
||||
if($value !== null) $values[] = $value;
|
||||
}
|
||||
return $returnArr;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an extension on this DataObject
|
||||
* Get an extension instance attached to this object by name.
|
||||
*
|
||||
* @param string $name Classname of the Extension (e.g. 'Versioned')
|
||||
* @uses hasExtension()
|
||||
*
|
||||
* @return DataObjectDecorator The instance of the extension
|
||||
* @param string $extension
|
||||
* @return Extension
|
||||
*/
|
||||
public function extInstance($name) {
|
||||
if(isset($this->extension_instances[$name])) {
|
||||
return $this->extension_instances[$name];
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
$cacheName = $this->class . '_' . $method;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
),
|
||||
));
|
||||
}
|
||||
if(!is_array($arguments)) $arguments = array($arguments);
|
||||
|
||||
public static function remove_extension($className, $extensionName) {
|
||||
Object::$extraStatics[$className]['extensions'] = array_diff(Object::$extraStatics[$className]['extensions'], array($extensionName));
|
||||
}
|
||||
if($ID) $cacheName .= '_' . $ID;
|
||||
if(count($arguments)) $cacheName .= '_' . implode('_', $arguments);
|
||||
|
||||
/**
|
||||
* Loads a current cache from the filesystem, if it can.
|
||||
*
|
||||
* @param string $cachename The name of the cache to load
|
||||
* @param int $expire The lifetime of the cache in seconds
|
||||
* @return mixed The data from the cache, or false if the cache wasn't loaded
|
||||
*/
|
||||
protected function loadCache($cachename, $expire = 3600) {
|
||||
$cache_dir = TEMP_FOLDER;
|
||||
$cache_path = $cache_dir . "/" . $this->sanitiseCachename($cachename);
|
||||
if((!isset($_GET['flush']) || $_GET['flush']!=1) && (@file_exists($cache_path) && ((@filemtime($cache_path) + $expire) > (time())))) {
|
||||
return @unserialize(file_get_contents($cache_path));
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 mixed $data The data to cache
|
||||
* @param string $cache the cache name
|
||||
* @param mixed $data data to save (must be serializable)
|
||||
*/
|
||||
protected function saveCache($cachename, $data) {
|
||||
$cache_dir = TEMP_FOLDER;
|
||||
$cache_path = $cache_dir . "/" . $this->sanitiseCachename($cachename);
|
||||
$fp = @fopen($cache_path, "w+");
|
||||
if(!$fp) {
|
||||
return; // Throw an error?
|
||||
}
|
||||
@fwrite($fp, @serialize($data));
|
||||
@fclose($fp);
|
||||
protected function saveCache($cache, $data) {
|
||||
file_put_contents(TEMP_FOLDER . '/' . $this->sanitiseCachename($cache), serialize($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return string the sanitised cache name
|
||||
* @param string $name
|
||||
* @return string the name with all special cahracters replaced with underscores
|
||||
*/
|
||||
protected function sanitiseCachename($cachename) {
|
||||
// Replace illegal characters with underscores
|
||||
$cachename = str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $cachename);
|
||||
return $cachename;
|
||||
protected function sanitiseCachename($name) {
|
||||
return str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the return value of a method.
|
||||
*
|
||||
* @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
|
||||
* @deprecated 2.4 Use getExtensionInstance
|
||||
*/
|
||||
public function cacheToFile($callback, $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 = $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;
|
||||
public function extInstance($extension) {
|
||||
return $this->getExtensionInstance($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
?>
|
@ -346,8 +346,6 @@ class Requirements_Backend {
|
||||
* Remembers the filepaths of all cleared Requirements
|
||||
* through {@link clear()}.
|
||||
*
|
||||
* @usedby {@link restore()}
|
||||
*
|
||||
* @var array $disabled
|
||||
*/
|
||||
protected $disabled = array();
|
||||
@ -529,6 +527,7 @@ class Requirements_Backend {
|
||||
$this->disabled['css'] = $this->css;
|
||||
$this->disabled['customScript'] = $this->customScript;
|
||||
$this->disabled['customCSS'] = $this->customCSS;
|
||||
$this->disabled['customHeadTags'] = $this->customHeadTags;
|
||||
|
||||
$this->javascript = array();
|
||||
$this->css = array();
|
||||
@ -561,6 +560,7 @@ class Requirements_Backend {
|
||||
$this->css = $this->disabled['css'];
|
||||
$this->customScript = $this->disabled['customScript'];
|
||||
$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) {
|
||||
$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
|
||||
}
|
||||
|
||||
foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
|
||||
$requirements .= "$customHeadTag\n";
|
||||
}
|
||||
|
@ -3,6 +3,9 @@
|
||||
* 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
|
||||
* can be used as a successful test.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage validation
|
||||
*/
|
||||
class ValidationException extends Exception {
|
||||
|
||||
|
@ -278,7 +278,6 @@ class ViewableData extends Object implements IteratorAggregate {
|
||||
/**
|
||||
* Return the string-format type for the given field.
|
||||
*
|
||||
* @usedby ViewableData::XML_val()
|
||||
* @param string $fieldName
|
||||
* @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,
|
||||
* and allows for versatile css inheritance and overrides.
|
||||
*
|
||||
* <example>
|
||||
* <code>
|
||||
* <body class="$CSSClasses">
|
||||
* </example>
|
||||
* </code>
|
||||
*
|
||||
* @uses ClassInfo
|
||||
*
|
||||
|
@ -318,7 +318,7 @@ HTML;
|
||||
* Returns the xml:lang and lang attributes
|
||||
*/
|
||||
function LangAttributes() {
|
||||
$lang = Translatable::current_lang();
|
||||
$lang = Translatable::current_locale();
|
||||
return "xml:lang=\"$lang\" lang=\"$lang\"";
|
||||
}
|
||||
|
||||
|
@ -51,9 +51,6 @@ class ContentNegotiator {
|
||||
return self::$encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @usedby Controller->handleRequest()
|
||||
*/
|
||||
static function process(HTTPResponse $response) {
|
||||
if(!self::enabled_for($response)) return;
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
* that controller will be used instead. It should be a subclass of ContentController.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage control
|
||||
*/
|
||||
class ModelAsController extends Controller implements NestedController {
|
||||
|
||||
@ -48,9 +49,9 @@ class ModelAsController extends Controller implements NestedController {
|
||||
$url = Controller::join_links(
|
||||
Director::baseURL(),
|
||||
$child->URLSegment,
|
||||
isset($this->urlParams['Action']) ? $this->urlParams['Action'] : null,
|
||||
isset($this->urlParams['ID']) ? $this->urlParams['ID'] : null,
|
||||
isset($this->urlParams['OtherID']) ? $this->urlParams['OtherID'] : null
|
||||
(isset($this->urlParams['Action'])) ? $this->urlParams['Action'] : null,
|
||||
(isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null,
|
||||
(isset($this->urlParams['OtherID'])) ? $this->urlParams['OtherID'] : null
|
||||
);
|
||||
|
||||
$response = new HTTPResponse();
|
||||
@ -64,6 +65,9 @@ class ModelAsController extends Controller implements NestedController {
|
||||
if($child) {
|
||||
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";
|
||||
|
||||
if($this->urlParams['Action'] && ClassInfo::exists($controllerClass.'_'.$this->urlParams['Action'])) {
|
||||
|
@ -24,6 +24,9 @@
|
||||
* Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
|
||||
*
|
||||
* {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage control
|
||||
*/
|
||||
class RequestHandler extends ViewableData {
|
||||
protected $request = null;
|
||||
@ -81,8 +84,7 @@ class RequestHandler extends ViewableData {
|
||||
$handlerClass = ($this->class) ? $this->class : get_class($this);
|
||||
// We stop after RequestHandler; in other words, at ViewableData
|
||||
while($handlerClass && $handlerClass != 'ViewableData') {
|
||||
// Todo: ajshort's stat rewriting could be useful here.
|
||||
$urlHandlers = eval("return $handlerClass::\$url_handlers;");
|
||||
$urlHandlers = Object::get_static($handlerClass, 'url_handlers');
|
||||
|
||||
if($urlHandlers) foreach($urlHandlers as $rule => $action) {
|
||||
if(isset($_REQUEST['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
|
||||
@ -150,50 +152,42 @@ class RequestHandler extends ViewableData {
|
||||
* It will interrogate {@link self::$allowed_actions} to determine this.
|
||||
*/
|
||||
function checkAccessAction($action) {
|
||||
// Collate self::$allowed_actions from this class and all parent classes
|
||||
$access = null;
|
||||
$className = ($this->class) ? $this->class : get_class($this);
|
||||
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);
|
||||
$action = strtolower($action);
|
||||
$allowedActions = Object::combined_static($this->class, 'allowed_actions');
|
||||
$newAllowedActions = array();
|
||||
|
||||
// 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
|
||||
if($this->extension_instances) {
|
||||
foreach($this->extension_instances as $inst) {
|
||||
$accessPart = $inst->stat('allowed_actions');
|
||||
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
|
||||
// merge in any $allowed_actions from extensions
|
||||
if($this->extension_instances) foreach($this->extension_instances as $extension) {
|
||||
if($extAccess = $extension->stat('allowed_actions')) {
|
||||
$allowedActions = array_merge($allowedActions, $extAccess);
|
||||
}
|
||||
}
|
||||
|
||||
if($action == 'index') return true;
|
||||
|
||||
// Make checkAccessAction case-insensitive
|
||||
$action = strtolower($action);
|
||||
if($access) {
|
||||
foreach($access as $k => $v) $newAccess[strtolower($k)] = strtolower($v);
|
||||
$access = $newAccess;
|
||||
if($allowedActions) {
|
||||
foreach($allowedActions as $key => $value) {
|
||||
$newAllowedActions[strtolower($key)] = strtolower($value);
|
||||
}
|
||||
|
||||
if(isset($access[$action])) {
|
||||
$test = $access[$action];
|
||||
if($test === true) return true;
|
||||
if(substr($test,0,2) == '->') {
|
||||
$funcName = substr($test,2);
|
||||
return $this->$funcName();
|
||||
$allowedActions = $newAllowedActions;
|
||||
|
||||
if(isset($allowedActions[$action])) {
|
||||
$test = $allowedActions[$action];
|
||||
|
||||
if($test === true) {
|
||||
return true;
|
||||
} elseif(substr($test, 0, 2) == '->') {
|
||||
return $this->{substr($test, 2)}();
|
||||
} elseif(Permission::check($test)) {
|
||||
return true;
|
||||
}
|
||||
if(Permission::check($test)) return true;
|
||||
} else if((($key = array_search($action, $access)) !== false) && is_numeric($key)) {
|
||||
} elseif((($key = array_search($action, $allowedActions)) !== false) && is_numeric($key)) {
|
||||
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
|
||||
// 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.
|
||||
|
@ -6,8 +6,19 @@
|
||||
* @subpackage control
|
||||
*/
|
||||
class RootURLController extends Controller {
|
||||
|
||||
/**
|
||||
* @var boolean $is_at_root
|
||||
*/
|
||||
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() {
|
||||
Director::set_site_mode('site');
|
||||
parent::init();
|
||||
@ -27,7 +38,6 @@ class RootURLController extends Controller {
|
||||
}
|
||||
|
||||
$controller = new ModelAsController();
|
||||
|
||||
$request = new HTTPRequest("GET", self::get_homepage_urlsegment().'/', $request->getVars(), $request->postVars());
|
||||
$request->match('$URLSegment//$Action', true);
|
||||
|
||||
@ -39,18 +49,29 @@ class RootURLController extends Controller {
|
||||
|
||||
/**
|
||||
* Return the URL segment for the current HTTP_HOST value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
static function get_homepage_urlsegment() {
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$host = str_replace('www.','',$host);
|
||||
$SQL_host = str_replace('.','\\.',Convert::raw2sql($host));
|
||||
$homePageOBJ = DataObject::get_one("SiteTree", "HomepageForDomain REGEXP '(,|^) *$SQL_host *(,|\$)'");
|
||||
$urlSegment = '';
|
||||
|
||||
if($homePageOBJ) {
|
||||
return $homePageOBJ->URLSegment;
|
||||
// @todo Temporarily restricted to MySQL database while testing db abstraction
|
||||
if(DB::getConn() instanceof MySQLDatabase) {
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$host = str_replace('www.','',$host);
|
||||
$SQL_host = str_replace('.','\\.',Convert::raw2sql($host));
|
||||
$homePageOBJ = DataObject::get_one("SiteTree", "HomepageForDomain REGEXP '(,|^) *$SQL_host *(,|\$)'");
|
||||
} 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;
|
||||
else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
static function get_default_homepage_urlsegment() {
|
||||
return self::$default_homepage_urlsegment;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
657
core/i18n.php
657
core/i18n.php
@ -575,8 +575,8 @@ class i18n extends Object {
|
||||
);
|
||||
|
||||
/**
|
||||
* A list of commonly used languages, in the form
|
||||
* langcode => array( EnglishName, NativeName)
|
||||
* @var array $common_languages A list of commonly used languages, in the form
|
||||
* langcode => array( EnglishName, NativeName)
|
||||
*/
|
||||
public static $common_languages = array(
|
||||
'af' => array('Afrikaans', 'Afrikaans'),
|
||||
@ -665,6 +665,99 @@ class i18n extends Object {
|
||||
'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', 'العربية'),
|
||||
'eu_ES' => array('Basque', 'euskera'),
|
||||
'be_BY' => array('Belarusian', 'Беларуская мова'),
|
||||
'bn_BD' => array('Bengali', 'বাংলা'),
|
||||
'bg_BG' => array('Bulgarian', 'български'),
|
||||
'ca_ES' => array('Catalan', 'català'),
|
||||
'zh-yue_ZH-YUE' => array('Chinese (Cantonese)', '廣東話 [广东话]'),
|
||||
'zh-cmn_ZH-CMN' => array('Chinese (Mandarin)', '普通話 [普通话]'),
|
||||
'zh-min-nan_ZH-MIN-NAN' => array('Chinese (Min Nan)', '台語'),
|
||||
'hr_HR' => array('Croatian', 'Hrvatski'),
|
||||
'cs_CZ' => array('Czech', 'češ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øroyska'),
|
||||
'fi_FI' => array('Finnish', 'suomi'),
|
||||
'fr_FR' => array('French', 'français'),
|
||||
'gd_GB' => array('Gaelic', 'Gaeilge'),
|
||||
'gl_ES' => array('Galician', 'Galego'),
|
||||
'de_DE' => array('German', 'Deutsch'),
|
||||
'el_GR' => array('Greek', 'ελληνικά'),
|
||||
'gu_IN' => array('Gujarati', 'ગુજરાતી'),
|
||||
'ha_NG' => array('Hausa', 'حَوْسَ'),
|
||||
'he_IL' => array('Hebrew', 'עברית'),
|
||||
'hi_IN' => array('Hindi', 'हिन्दी'),
|
||||
'hu_HU' => array('Hungarian', 'magyar'),
|
||||
'is_IS' => array('Icelandic', 'Í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', '日本語'),
|
||||
'jv_ID' => array('Javanese', 'basa Jawa'),
|
||||
'ko_KR' => array('Korean', '한국어 [韓國語]'),
|
||||
'ku_IQ' => array('Kurdish', 'Kurdí'),
|
||||
'lv_LV' => array('Latvian', 'latviešu'),
|
||||
'lt_LT' => array('Lithuanian', 'lietuviškai'),
|
||||
'lmo_LMO' => array('Lombard', 'Lombardo'),
|
||||
'mk_MK' => array('Macedonian', 'македонски'),
|
||||
'mi_NZ' => array('Maori', 'Maori'),
|
||||
'ms_MY' => array('Malay', 'Bahasa melayu'),
|
||||
'mt_MT' => array('Maltese', 'Malti'),
|
||||
'mr_IN' => array('Marathi', 'मराठी'),
|
||||
'ne_NP' => array('Nepali', 'नेपाली'),
|
||||
'no_NO' => array('Norwegian', 'Norsk'),
|
||||
'om_ET' => array('Oromo', 'Afaan Oromo'),
|
||||
'fa_IR' => array('Persian', 'فارسى'),
|
||||
'pl_PL' => array('Polish', 'polski'),
|
||||
'pt-PT_PT-PT' => array('Portuguese (Portugal)', 'português (Portugal)'),
|
||||
'pt-BR_PT-BR' => array('Portuguese (Brazil)', 'português (Brazil)'),
|
||||
'pa_IN' => array('Punjabi', 'ਪੰਜਾਬੀ'),
|
||||
'qu_PE' => array('Quechua', 'Quechua'),
|
||||
'rm_CH' => array('Romansh', 'rumantsch'),
|
||||
'ro_RO' => array('Romanian', 'român'),
|
||||
'ru_RU' => array('Russian', 'Русский'),
|
||||
'sco_SCO' => array('Scots', 'Scoats leid, Lallans'),
|
||||
'sr_RS' => array('Serbian', 'српски'),
|
||||
'sk_SK' => array('Slovak', 'slovenčina'),
|
||||
'sl_SI' => array('Slovenian', 'slovenščina'),
|
||||
'es_ES' => array('Spanish', 'español'),
|
||||
'sv_SE' => array('Swedish', 'Svenska'),
|
||||
'tl_PH' => array('Tagalog', 'Tagalog'),
|
||||
'ta_IN' => array('Tamil', 'தமிழ்'),
|
||||
'te_IN' => array('Telugu', 'తెలుగు'),
|
||||
'to_TO' => array('Tonga', 'chiTonga'),
|
||||
'ts_ZA' => array('Tsonga', 'xiTshonga'),
|
||||
'tn_ZA' => array('Tswana', 'seTswana'),
|
||||
'tr_TR' => array('Turkish', 'Türkçe'),
|
||||
'tk_TM' => array('Turkmen', 'түркmенче'),
|
||||
'tw_GH' => array('Twi', 'twi'),
|
||||
'uk_UA' => array('Ukrainian', 'Українська'),
|
||||
'ur_PK' => array('Urdu', 'اردو'),
|
||||
'uz_UZ' => array('Uzbek', 'ўзбек'),
|
||||
've_ZA' => array('Venda', 'tshiVenḓa'),
|
||||
'vi_VN' => array('Vietnamese', 'tiếng việt'),
|
||||
'wa_WA' => array('Walloon', 'walon'),
|
||||
'wo_SN' => array('Wolof', 'Wollof'),
|
||||
'xh_ZA' => array('Xhosa', 'isiXhosa'),
|
||||
'yi_YI' => array('Yiddish', 'ײִדיש'),
|
||||
'zu_ZA' => array('Zulu', 'isiZulu'),
|
||||
);
|
||||
|
||||
static $tinymce_lang = array(
|
||||
'ca_AD' => '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.
|
||||
*
|
||||
@ -876,6 +1436,20 @@ class i18n extends Object {
|
||||
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)
|
||||
*
|
||||
@ -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 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
|
||||
* @return Name of the locale
|
||||
@ -992,6 +1570,59 @@ class i18n extends Object {
|
||||
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)
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
static function default_lang() {
|
||||
return Translatable::default_lang();
|
||||
return Translatable::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() {
|
||||
Translatable::enable();
|
||||
@ -1077,6 +1708,8 @@ class i18n extends Object {
|
||||
|
||||
/**
|
||||
* Disable the multilingual content feature (proxy for Translatable::disable())
|
||||
*
|
||||
* @deprecated 2.4 Use Object::add_extension('Page', 'Translatable');
|
||||
*/
|
||||
static function disable() {
|
||||
Translatable::disable();
|
||||
@ -1149,7 +1782,7 @@ class i18n extends Object {
|
||||
*/
|
||||
public function removelang() {
|
||||
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) {
|
||||
$object->delete();
|
||||
}
|
||||
|
@ -53,7 +53,36 @@
|
||||
* @package sapphire
|
||||
* @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
|
||||
* by fieldname.
|
||||
@ -81,31 +110,6 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
*/
|
||||
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?
|
||||
*/
|
||||
@ -128,6 +132,44 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new DataObject.
|
||||
@ -372,8 +414,6 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
* }
|
||||
* </code>
|
||||
*
|
||||
* @usedby {@link DataObjectSet->toDropDownMap()}
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() {
|
||||
@ -657,10 +697,9 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
*/
|
||||
public function populateDefaults() {
|
||||
$classes = array_reverse(ClassInfo::ancestry($this));
|
||||
foreach($classes as $class) {
|
||||
$singleton = ($class == $this->class) ? $this : singleton($class);
|
||||
|
||||
$defaults = $singleton->stat('defaults');
|
||||
foreach($classes as $class) {
|
||||
$defaults = Object::get_static($class, 'defaults');
|
||||
|
||||
if($defaults) foreach($defaults as $fieldName => $fieldValue) {
|
||||
// SRM 2007-03-06: Stricter check
|
||||
@ -782,6 +821,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
if(isset($ancestry) && is_array($ancestry)) {
|
||||
foreach($ancestry as $idx => $class) {
|
||||
$classSingleton = singleton($class);
|
||||
|
||||
foreach($this->record as $fieldName => $fieldValue) {
|
||||
if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) {
|
||||
$fieldObj = $this->dbObject($fieldName);
|
||||
@ -1272,12 +1312,13 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue;
|
||||
|
||||
if($component) {
|
||||
$candidate = eval("return isset({$class}::\$has_one[\$component]) ? {$class}::\$has_one[\$component] : null;");
|
||||
if($candidate) {
|
||||
return $candidate;
|
||||
$hasOne = Object::get_static($class, 'has_one');
|
||||
|
||||
if(isset($hasOne[$component])) {
|
||||
return $hasOne[$component];
|
||||
}
|
||||
} else {
|
||||
$newItems = eval("return (array){$class}::\$has_one;");
|
||||
$newItems = (array) Object::get_static($class, 'has_one');
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
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) {
|
||||
$candidate = eval("return isset({$class}::\$db[\$component]) ? {$class}::\$db[\$component] : null;");
|
||||
if($candidate) {
|
||||
return $candidate;
|
||||
$db = Object::get_static($class, 'db');
|
||||
|
||||
if(isset($db[$component])) {
|
||||
return $db[$component];
|
||||
}
|
||||
} else {
|
||||
$newItems = eval("return (array){$class}::\$db;");
|
||||
$newItems = (array) Object::get_static($class, 'db');
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
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($component) {
|
||||
$candidate = eval("return isset({$class}::\$has_many[\$component]) ? {$class}::\$has_many[\$component] : null;");
|
||||
$candidate = eval("if ( isset({$class}::\$has_many[\$component]) ) { return {$class}::\$has_many[\$component]; } else { return false; }");
|
||||
if($candidate) {
|
||||
return $candidate;
|
||||
$hasMany = Object::get_static($class, 'has_many');
|
||||
|
||||
if(isset($hasMany[$component])) {
|
||||
return $hasMany[$component];
|
||||
}
|
||||
} else {
|
||||
$newItems = eval("return (array){$class}::\$has_many;");
|
||||
$newItems = (array) Object::get_static($class, 'has_many');
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
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($component) {
|
||||
$manyMany = singleton($class)->stat('many_many');
|
||||
$manyMany = Object::get_static($class, 'many_many');
|
||||
// Try many_many
|
||||
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
|
||||
if($candidate) {
|
||||
@ -1389,13 +1431,13 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
}
|
||||
|
||||
// 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;
|
||||
if($candidate) {
|
||||
$childField = $candidate . "ID";
|
||||
|
||||
// We need to find the inverse component name
|
||||
$otherManyMany = singleton($candidate)->stat('many_many');
|
||||
$otherManyMany = Object::get_static($candidate, 'many_many');
|
||||
if(!$otherManyMany) {
|
||||
user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR);
|
||||
}
|
||||
@ -1414,7 +1456,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
user_error("Orphaned \$belongs_many_many value for $this->class.$component", E_USER_ERROR);
|
||||
}
|
||||
} else {
|
||||
$newItems = eval("return (array){$class}::\$many_many;");
|
||||
$newItems = (array) Object::get_static($class, 'many_many');
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$many_many has a bad entry: "
|
||||
@ -1422,7 +1464,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
}
|
||||
$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
|
||||
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: "
|
||||
@ -1440,7 +1482,6 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
* Generates a SearchContext to be used for building and processing
|
||||
* a generic search form for properties on this object.
|
||||
*
|
||||
* @usedby {@link ModelAdmin}
|
||||
* @return SearchContext
|
||||
*/
|
||||
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
|
||||
* how generic or specific the field type is.
|
||||
*
|
||||
* @usedby {@link SearchContext}
|
||||
* Used by {@link SearchContext}.
|
||||
*
|
||||
* @param array $_params
|
||||
* '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
|
||||
* or decorate onto it by using {@link DataObjectDecorator->updateCMSFields()}.
|
||||
*
|
||||
* <example>
|
||||
* <code>
|
||||
* klass MyCustomClass extends DataObject {
|
||||
* static $db = array('CustomProperty'=>'Boolean');
|
||||
*
|
||||
@ -1562,7 +1603,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
* return $fields;
|
||||
* }
|
||||
* }
|
||||
* </example>
|
||||
* </code>
|
||||
*
|
||||
* @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;
|
||||
|
||||
// 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(!$fieldMap) {
|
||||
@ -1844,7 +1885,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -1852,10 +1893,14 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given class has its own table.
|
||||
* Uses the rules for whether the table should exist rather than actually looking in the database.
|
||||
* Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
|
||||
* actually looking in the database.
|
||||
*
|
||||
* @param string $dataClass
|
||||
* @return bool
|
||||
*/
|
||||
public function has_own_table($dataClass) {
|
||||
|
||||
// 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
|
||||
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') {
|
||||
self::$cache_has_own_table[$dataClass] = true;
|
||||
} else {
|
||||
$sng = singleton($dataClass);
|
||||
self::$cache_has_own_table[$dataClass] = $sng->uninherited('db',true) || $sng->uninherited('has_one',true);
|
||||
self::$cache_has_own_table[$dataClass] = Object::uninherited_static($dataClass, 'db') || Object::uninherited_static($dataClass, 'has_one');
|
||||
}
|
||||
}
|
||||
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.
|
||||
*
|
||||
@ -2141,12 +2182,11 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
$query->select[] = "`$tableClass`.*";
|
||||
|
||||
// Add SQL for multi-value fields
|
||||
$SNG = singleton($tableClass);
|
||||
$databaseFields = $SNG->databaseFields();
|
||||
$databaseFields = self::database_fields($tableClass);
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
if(!in_array($k, array('ClassName', 'LastEdited', 'Created'))) {
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
*
|
||||
@ -2400,7 +2449,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
*/
|
||||
public static function get_by_id($callerClass, $id) {
|
||||
if(is_numeric($id)) {
|
||||
if(singleton($callerClass) instanceof DataObject) {
|
||||
if(is_subclass_of($callerClass, 'DataObject')) {
|
||||
$tableClasses = ClassInfo::dataClassesFor($callerClass);
|
||||
$baseClass = array_shift($tableClasses);
|
||||
return DataObject::get_one($callerClass,"`$baseClass`.`ID` = $id");
|
||||
@ -2529,48 +2578,17 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the complete set of database fields, including Created, LastEdited and ClassName.
|
||||
*
|
||||
* @return array A map of field name to class of all databases fields on this object
|
||||
*
|
||||
* @see DataObject::database_fields()
|
||||
*/
|
||||
public function databaseFields() {
|
||||
// For base tables, add a classname field
|
||||
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();
|
||||
}
|
||||
return self::database_fields($this->class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom database fields for this object, from self::$db and self::$has_one,
|
||||
* but not built-in fields like ID, ClassName, Created, LastEdited.
|
||||
*
|
||||
* @return array
|
||||
* @see DataObject::custom_database_fields()
|
||||
*/
|
||||
public function customDatabaseFields() {
|
||||
$db = $this->uninherited('db',true);
|
||||
$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;
|
||||
return self::custom_database_fields($this->class);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2584,13 +2602,15 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP
|
||||
* @todo review whether this is still needed after recent API changes
|
||||
*/
|
||||
public function inheritedDatabaseFields() {
|
||||
$fields = array();
|
||||
$currentObj = $this;
|
||||
while(get_class($currentObj) != 'DataObject') {
|
||||
$fields = array_merge($fields, (array)$currentObj->customDatabaseFields());
|
||||
$currentObj = singleton($currentObj->parentClass());
|
||||
$fields = array();
|
||||
$currentObj = $this->class;
|
||||
|
||||
while($currentObj != 'DataObject') {
|
||||
$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($ancestorClass == 'ViewableData') break;
|
||||
$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){
|
||||
$types['has_one'] = (array)singleton($ancestorClass)->uninherited('has_one', true);
|
||||
|
@ -27,26 +27,45 @@ abstract class DataObjectDecorator extends Extension {
|
||||
'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.
|
||||
*/
|
||||
function loadExtraStatics() {
|
||||
// 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) {
|
||||
foreach($fields as $relationType => $fields) {
|
||||
if(in_array($relationType, self::$decoratable_statics)) {
|
||||
eval("$className::\$$relationType = array_merge((array){$className}::\$$relationType, (array)\$fields);");
|
||||
$this->owner->set_stat($relationType, eval("return $className::\$$relationType;"));
|
||||
if($fields = $this->extraStatics()) {
|
||||
foreach($fields as $relation => $fields) {
|
||||
if(in_array($relation, self::$decoratable_statics)) {
|
||||
// Can't use add_static_var() here as it would merge the array rather than replacing
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,8 @@ class ErrorPage extends Page {
|
||||
"ShowInSearch" => 0
|
||||
);
|
||||
|
||||
protected static $static_filepath = ASSETS_PATH;
|
||||
|
||||
/**
|
||||
* Ensures that there is always a 404 page
|
||||
* by checking if there's an instance of
|
||||
@ -105,12 +107,10 @@ class ErrorPage extends Page {
|
||||
mkdir(ASSETS_PATH, 02775);
|
||||
}
|
||||
|
||||
// Path to the error file in the file store
|
||||
$errorFile = ASSETS_PATH . "/error-$this->ErrorCode.html";
|
||||
|
||||
// Attempt to open the file, writing it if it doesn't exist
|
||||
$fh = @fopen($errorFile, "w");
|
||||
if($fh) {
|
||||
// if the page is published in a language other than default language,
|
||||
// write a specific language version of the HTML page
|
||||
$filePath = self::get_filepath_for_errorcode($this->ErrorCode, $this->Lang);
|
||||
if($fh = fopen($filePath, "w")) {
|
||||
fwrite($fh, $errorContent);
|
||||
fclose($fh);
|
||||
} else {
|
||||
@ -143,6 +143,39 @@ class ErrorPage extends Page {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,6 +43,7 @@ class Hierarchy extends DataObjectDecorator {
|
||||
$output = "<ul$attributes>\n";
|
||||
|
||||
foreach($children as $child) {
|
||||
|
||||
if(!$limitToMarked || $child->isMarked()) {
|
||||
$foundAChild = true;
|
||||
$output .= eval("return $titleEval;") . "\n" .
|
||||
@ -356,18 +357,18 @@ class Hierarchy extends DataObjectDecorator {
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
public function Children() {
|
||||
if(!$this->children) {
|
||||
if(!(isset($this->_cache_children) && $this->_cache_children)) {
|
||||
$result = $this->owner->stageChildren(false);
|
||||
if(isset($result)) {
|
||||
$this->children = new DataObjectSet();
|
||||
$this->_cache_children = new DataObjectSet();
|
||||
foreach($result as $child) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function flushCache() {
|
||||
$this->_cache_children = null;
|
||||
$this->_cache_allChildrenIncludingDeleted = null;
|
||||
$this->_cache_allChildren = null;
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
@ -462,8 +462,8 @@ class Image_Cached extends Image {
|
||||
* Is connected to the URL routing "/image" through sapphire/_config.php,
|
||||
* and used by all iframe-based upload-fields in the CMS.
|
||||
*
|
||||
* @usedby FileIFrameField
|
||||
* @usedby ImageField
|
||||
* Used by {@link FileIFrameField}, {@link ImageField}.
|
||||
*
|
||||
* @todo Refactor to using FileIFrameField and ImageField as a controller for the upload,
|
||||
* rather than something totally disconnected from the original Form and FormField
|
||||
* context. Without the original context its impossible to control permissions etc.
|
||||
@ -494,8 +494,8 @@ class Image_Uploader extends Controller {
|
||||
}
|
||||
|
||||
// set reading lang
|
||||
if(Translatable::is_enabled() && !Director::is_ajax()) {
|
||||
Translatable::choose_site_lang(array_keys(Translatable::get_existing_content_languages('SiteTree')));
|
||||
if(singleton('SiteTree')->hasExtension('Translatable') && !Director::is_ajax()) {
|
||||
Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SiteTree')));
|
||||
}
|
||||
|
||||
parent::init();
|
||||
|
@ -143,6 +143,12 @@ class RedirectorPage extends Page {
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
function subPagesToCache() {
|
||||
$urls = parent::subPagesToCache();
|
||||
$urls[] = $this->URLSegment . '/';
|
||||
return $urls;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -436,7 +436,7 @@ class SQLQuery extends Object {
|
||||
function filtersOnID() {
|
||||
return (
|
||||
$this->where
|
||||
&& count($this->where) == 1
|
||||
//&& count($this->where) == 1
|
||||
&& preg_match('/^(.*\.)?("|`)?ID("|`)?\s?=/', $this->where[0])
|
||||
);
|
||||
}
|
||||
|
@ -169,7 +169,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
|
||||
|
||||
static $extensions = array(
|
||||
"Hierarchy",
|
||||
"Translatable('Title', 'MenuTitle', 'Content', 'URLSegment', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Status')",
|
||||
"Versioned('Stage', 'Live')"
|
||||
);
|
||||
|
||||
@ -883,7 +882,10 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
|
||||
if($this->ExtraMeta) {
|
||||
$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
|
||||
$this->extend('updateMetaTags', $tags);
|
||||
@ -1726,6 +1728,11 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
|
||||
if(!$this->ShowInMenus)
|
||||
$classes .= " notinmenu";
|
||||
|
||||
//TODO: Add integration
|
||||
/*
|
||||
if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
|
||||
$classes .= " untranslated ";
|
||||
*/
|
||||
$classes .= $this->markingClasses();
|
||||
|
||||
return $classes;
|
||||
|
@ -1,44 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* The {Translatable} decorator allows your DataObjects to have versions in different languages,
|
||||
* defining which fields are can be translated.
|
||||
* The Translatable decorator allows your DataObjects to have versions in different languages,
|
||||
* 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
|
||||
* database-entities. On the other hand, the file-based i18n-translations
|
||||
* always have a "locale" (e.g. 'en_US').
|
||||
* Locales (e.g. 'en_US') are used in Translatable for identifying a record by language.
|
||||
*
|
||||
* 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>
|
||||
* static $extensions = array(
|
||||
* "Translatable('MyTranslatableVarchar', 'OtherTranslatableText')"
|
||||
* );
|
||||
* Object::add_extension('MyClass', 'Translatable');
|
||||
* </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
|
||||
* is stored and represented in UTF-8 (Unicode). Please make sure your database and
|
||||
* HTML-templates adjust to this.
|
||||
*
|
||||
* Caution: Further decorations of DataObject might conflict with this implementation,
|
||||
* e.g. when overriding the get_one()-calls (which are already extended by {Translatable}).
|
||||
* <h2>"Default" languages</h2>
|
||||
*
|
||||
* 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>
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage misc
|
||||
*/
|
||||
class Translatable extends DataObjectDecorator {
|
||||
|
||||
/**
|
||||
* Indicates if the multilingual feature is enabled
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected static $enabled = false;
|
||||
|
||||
/**
|
||||
* The 'default' language.
|
||||
* @var string
|
||||
*/
|
||||
protected static $default_lang = 'en';
|
||||
protected static $default_locale = 'en_US';
|
||||
|
||||
/**
|
||||
* The language in which we are reading dataobjects.
|
||||
@ -47,22 +132,14 @@ class Translatable extends DataObjectDecorator {
|
||||
* @see Director::get_site_mode()
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
*
|
||||
@ -70,13 +147,6 @@ class Translatable extends DataObjectDecorator {
|
||||
*/
|
||||
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.
|
||||
* @var array
|
||||
@ -89,161 +159,117 @@ class Translatable extends DataObjectDecorator {
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param mixed $table Table name
|
||||
* @return boolean Returns true if $table exists.
|
||||
* @var boolean Temporarily override the "auto-filter" for {@link current_locale()}
|
||||
* in {@link augmentSQL()}. IMPORTANT: You must set this value back to TRUE
|
||||
* after the temporary usage.
|
||||
*/
|
||||
static function table_exists($table) {
|
||||
if (!self::$tableList) self::$tableList = DB::tableList();
|
||||
return isset(self::$tableList[strtolower($table)]);
|
||||
}
|
||||
protected static $enable_lang_filter = true;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* ('site' or 'cms'), or for a 'global' language setting.
|
||||
* 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)
|
||||
* @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'
|
||||
|
||||
if(isset($_GET['lang']) && (!isset($langsAvailable) || in_array($_GET['lang'], $langsAvailable))) {
|
||||
// get from GET parameter
|
||||
self::set_reading_lang($_GET['lang']);
|
||||
} 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 {
|
||||
// get default lang stored in class
|
||||
self::set_reading_lang(self::default_lang());
|
||||
if(self::$reading_locale) {
|
||||
self::$language_decided = true;
|
||||
return self::$reading_locale;
|
||||
}
|
||||
|
||||
return self::$reading_lang;
|
||||
if((isset($_GET['locale']) && !$langsAvailable) || (isset($_GET['locale']) && in_array($_GET['locale'], $langsAvailable))) {
|
||||
// get from GET parameter
|
||||
self::set_reading_locale($_GET['locale']);
|
||||
} else {
|
||||
self::set_reading_locale(self::default_locale());
|
||||
}
|
||||
|
||||
|
||||
self::$language_decided = true;
|
||||
return self::$reading_locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
static function default_lang() {
|
||||
return self::$default_lang;
|
||||
static function default_locale() {
|
||||
return self::$default_locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default language.
|
||||
*
|
||||
* @param $lang String
|
||||
* @param $locale String
|
||||
*/
|
||||
static function set_default_lang($lang) {
|
||||
self::$default_lang = $lang;
|
||||
static function set_default_locale($locale) {
|
||||
self::$default_locale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the default and current reading language are the same.
|
||||
* @return boolean Return true if both default and reading language are the same.
|
||||
*/
|
||||
static function is_default_lang() {
|
||||
return (!self::current_lang() || self::$default_lang == self::current_lang());
|
||||
static function is_default_locale() {
|
||||
return (!self::current_locale() || self::$default_locale == self::current_locale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current reading language.
|
||||
* @return string
|
||||
*/
|
||||
static function current_lang() {
|
||||
if (!self::$language_decided) self::choose_site_lang();
|
||||
return self::$reading_lang;
|
||||
static function current_locale() {
|
||||
if (!self::$language_decided) self::choose_site_locale();
|
||||
return self::$reading_locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static function set_reading_lang($lang) {
|
||||
$key = (Director::get_site_mode()) ? 'lang.' . Director::get_site_mode() : 'lang.global';
|
||||
Session::set($key, $lang);
|
||||
self::$reading_lang = $lang;
|
||||
static function set_reading_locale($locale) {
|
||||
self::$reading_locale = $locale;
|
||||
self::$language_decided = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a singleton instance of a class in the given language.
|
||||
* @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 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_by_lang($class, $lang, $filter = '', $cache = false, $orderby = "") {
|
||||
$oldLang = self::current_lang();
|
||||
self::set_reading_lang($lang);
|
||||
$result = DataObject::get_one($class, $filter, $cache, $orderby);
|
||||
self::set_reading_lang($oldLang);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
static function get_one_by_locale($class, $locale, $filter = '', $cache = false, $orderby = "") {
|
||||
$orig = Translatable::current_locale();
|
||||
Translatable::set_reading_locale($locale);
|
||||
$do = DataObject::get_one($class, $filter, $cache, $orderby);
|
||||
Translatable::set_reading_locale($orig);
|
||||
return $do;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the instances of the given class translated to the given language
|
||||
*
|
||||
* @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 $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.
|
||||
@ -252,38 +278,57 @@ class Translatable extends DataObjectDecorator {
|
||||
* @param string $having A filter to be inserted into the HAVING clause.
|
||||
* @return mixed The objects matching the conditions.
|
||||
*/
|
||||
static function get_by_lang($class, $lang, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") {
|
||||
$oldLang = self::current_lang();
|
||||
self::set_reading_lang($lang);
|
||||
static function get_by_locale($class, $locale, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") {
|
||||
$oldLang = self::current_locale();
|
||||
self::set_reading_locale($locale);
|
||||
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass, $having);
|
||||
self::set_reading_lang($oldLang);
|
||||
self::set_reading_locale($oldLang);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a record in his original language version.
|
||||
* @param string $class The name of the class.
|
||||
* @param string $originalLangID The original record id.
|
||||
* @return DataObject
|
||||
* Gets all translations for this specific page.
|
||||
* Doesn't include the language of the current record.
|
||||
*
|
||||
* @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() {
|
||||
$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") {
|
||||
$class = $class."_Live";
|
||||
$baseDataClass = $baseDataClass . "_Live";
|
||||
}
|
||||
|
||||
$id = $this->owner->ID;
|
||||
if(is_numeric($id)) {
|
||||
$query = new SQLQuery('distinct Lang',"$class","(`$class`.OriginalID =$id)");
|
||||
$translationGroupID = $this->getTranslationGroup();
|
||||
if(is_numeric($translationGroupID)) {
|
||||
$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();
|
||||
}
|
||||
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
|
||||
*/
|
||||
static function get_langs_by_id($class, $id) {
|
||||
$query = new SQLQuery('Lang',"{$class}_lang","(`{$class}_lang`.OriginalLangID =$id)");
|
||||
$langs = $query->execute()->column();
|
||||
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);
|
||||
$do = DataObject::get_by_id($class, $id);
|
||||
return ($do ? $do->getTranslatedLangs() : array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the multilingual feature
|
||||
*
|
||||
* @deprecated 2.4 Use Object::add_extension('SiteTree', 'Translatable')
|
||||
*/
|
||||
static function enable() {
|
||||
self::$enabled = true;
|
||||
Object::add_extension('SiteTree', 'Translatable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the multilingual feature
|
||||
*
|
||||
* @deprecated 2.4 Use Object::remove_extension('SiteTree', 'Translatable')
|
||||
*/
|
||||
static function disable() {
|
||||
self::$enabled = false;
|
||||
Object::remove_extension('SiteTree', 'Translatable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether multilingual support has been enabled
|
||||
*
|
||||
* @deprecated 2.4 Use Object::has_extension('SiteTree', 'Translatable')
|
||||
* @return boolean True if 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.
|
||||
* @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();
|
||||
|
||||
// @todo Disabled selection of translatable fields - we're setting all fields as translatable in setOwner()
|
||||
/*
|
||||
if(!is_array($translatableFields)) {
|
||||
$translatableFields = func_get_args();
|
||||
}
|
||||
|
||||
$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 augmentSQL(SQLQuery &$query) {
|
||||
if (! $this->stat('enabled', true)) return false;
|
||||
if((($lang = self::current_lang()) && !self::is_default_lang()) || self::$bypass) {
|
||||
foreach($query->from as $table => $dummy) {
|
||||
if(!isset($baseTable)) {
|
||||
$baseTable = $table;
|
||||
}
|
||||
function setOwner(Object $owner) {
|
||||
parent::setOwner($owner);
|
||||
|
||||
if (self::table_exists("{$table}_lang")) {
|
||||
$query->renameTable($table, $table . '_lang');
|
||||
if (stripos($query->sql(),'.ID')) {
|
||||
// Every reference to ID is now OriginalLangID
|
||||
$query->replaceText(".ID",".OriginalLangID");
|
||||
$query->where = str_replace("`ID`", "`OriginalLangID`",$query->where);
|
||||
$query->select[] = "`{$baseTable}_lang`.OriginalLangID AS ID";
|
||||
}
|
||||
if ($query->where) foreach ($query->where as $i => $wherecl) {
|
||||
if (substr($wherecl,0,4) == 'ID =')
|
||||
// Another reference to ID to be changed
|
||||
$query->where[$i] = str_replace('ID =','OriginalLangID =',$wherecl);
|
||||
else {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
// setting translatable fields by inspecting owner - this should really be done in the constructor
|
||||
$this->translatableFields = array_keys($this->owner->inheritedDatabaseFields());
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a WHERE clause should be applied to the augmented table
|
||||
* 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.
|
||||
*
|
||||
* @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
|
||||
* @return boolean True if the clause can be applied to the augmented table
|
||||
* Use {@link $enable_lang_filter} to temporarily disable this "auto-filtering".
|
||||
*/
|
||||
function isInAugmentedTable($clause, $table) {
|
||||
$clause = str_replace('`','',$clause);
|
||||
$table = str_replace('_lang','',$table);
|
||||
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);
|
||||
function augmentSQL(SQLQuery &$query) {
|
||||
$lang = Translatable::current_locale();
|
||||
$baseTable = ClassInfo::baseDataClass($this->owner->class);
|
||||
$where = $query->where;
|
||||
if(
|
||||
$lang
|
||||
// unless the filter has been temporarily disabled
|
||||
&& self::$enable_lang_filter
|
||||
// DataObject::get_by_id() should work independently of language
|
||||
&& !$query->filtersOnID()
|
||||
// the query contains this table
|
||||
// @todo Isn't this always the case?!
|
||||
&& array_search($baseTable, array_keys($query->from)) !== false
|
||||
// or we're already filtering by Lang (either from an earlier augmentSQL() call or through custom SQL filters)
|
||||
&& !preg_match('/("|\')Lang("|\')/', $query->getFilter())
|
||||
//&& !$query->filtersOnFK()
|
||||
) {
|
||||
$qry = "`Locale` = '$lang'";
|
||||
if(Translatable::is_default_locale()) {
|
||||
$qry .= " OR `Locale` = '' ";
|
||||
$qry .= " OR `Locale` IS NULL ";
|
||||
}
|
||||
$query->where[] = $qry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the DataObject has any own translatable field (not inherited).
|
||||
* @return boolean
|
||||
* 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 hasOwnTranslatableFields() {
|
||||
$ownFields = $this->owner->stat('db');
|
||||
if ($ownFields == singleton($this->owner->parentClass())->stat('db'))return false;
|
||||
foreach ((array)$this->translatableFields as $translatableField) {
|
||||
if (isset($ownFields[$translatableField])) return true;
|
||||
}
|
||||
return false;
|
||||
function augmentDatabase() {
|
||||
$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
|
||||
if($this->owner->class != $baseDataClass) return;
|
||||
|
||||
$fields = array(
|
||||
'OriginalID' => 'Int',
|
||||
'TranslationGroupID' => 'Int',
|
||||
);
|
||||
$indexes = array(
|
||||
'OriginalID' => true,
|
||||
'TranslationGroupID' => true
|
||||
);
|
||||
|
||||
DB::requireTable("{$baseDataClass}_translationgroups", $fields, $indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a record to a "translation group",
|
||||
* 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
|
||||
*/
|
||||
public function addTranslationGroup($originalID) {
|
||||
if(!$this->owner->exists()) return false;
|
||||
|
||||
$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
|
||||
$existingGroupID = $this->getTranslationGroup($originalID);
|
||||
if(!$existingGroupID) {
|
||||
DB::query(
|
||||
sprintf('INSERT INTO `%s_translationgroups` (`TranslationGroupID`,`OriginalID`) VALUES (%d,%d)', $baseDataClass, $originalID, $this->owner->ID)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* This is called at db/build time
|
||||
@ -473,138 +541,170 @@ class Translatable extends DataObjectDecorator {
|
||||
* @return boolean
|
||||
*/
|
||||
function isVersionedTable($table) {
|
||||
// Every _lang table wants Versioned support
|
||||
return ($this->owner->databaseFields() && $this->hasOwnTranslatableFields());
|
||||
return false;
|
||||
}
|
||||
|
||||
function augmentDatabase() {
|
||||
if (! $this->stat('enabled', true)) return false;
|
||||
self::set_reading_lang(self::default_lang());
|
||||
$table = $this->owner->class;
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
$langIndexes = array_merge(
|
||||
array(
|
||||
'OriginalLangID_Lang' => '(OriginalLangID, Lang)',
|
||||
'OriginalLangID' => true,
|
||||
'Lang' => true,
|
||||
),
|
||||
(array)$indexes
|
||||
);
|
||||
|
||||
// Create table for translated instances
|
||||
DB::requireTable("{$table}_lang", $langFields, $langIndexes);
|
||||
|
||||
} else {
|
||||
DB::dontRequireTable("{$table}_lang");
|
||||
}
|
||||
function contentcontrollerInit($controller) {
|
||||
Translatable::choose_site_locale();
|
||||
$controller->Locale = Translatable::current_locale();
|
||||
}
|
||||
|
||||
function modelascontrollerInit($controller) {
|
||||
//$this->contentcontrollerInit($controller);
|
||||
}
|
||||
|
||||
function initgetEditForm($controller) {
|
||||
$this->contentcontrollerInit($controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment a write-record request.
|
||||
* @param SQLQuery $manipulation Query to augment.
|
||||
* Recursively creates translations for parent pages in this language
|
||||
* if they aren't existing already. This is a necessity to make
|
||||
* nested pages accessible in a translated CMS page tree.
|
||||
* It would be more userfriendly to grey out untranslated pages,
|
||||
* but this involves complicated special cases in AllChildrenIncludingDeleted().
|
||||
*/
|
||||
function augmentWrite(&$manipulation) {
|
||||
if (! $this->stat('enabled', true)) return false;
|
||||
if(($lang = self::current_lang()) && !self::is_default_lang()) {
|
||||
$tables = array_keys($manipulation);
|
||||
foreach($tables as $table) {
|
||||
if (self::table_exists("{$table}_lang")) {
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
function onBeforeWrite() {
|
||||
// If language is not set explicitly, set it to current_locale.
|
||||
// This might be a bit overzealous in assuming the language
|
||||
// of the content, as a "single language" website might be expanded
|
||||
// later on.
|
||||
if(!$this->owner->ID && !$this->owner->Locale) {
|
||||
$this->owner->Locale = Translatable::current_locale();
|
||||
}
|
||||
|
||||
// Specific logic for SiteTree subclasses.
|
||||
// If page has untranslated parents, create (unpublished) translations
|
||||
// of those as well to avoid having inaccessible children in the sitetree.
|
||||
// Caution: This logic is very sensitve to infinite loops when translation status isn't determined properly
|
||||
if($this->owner->hasField('ParentID')) {
|
||||
if(
|
||||
!$this->owner->ID
|
||||
&& $this->owner->ParentID
|
||||
&& !$this->owner->Parent()->hasTranslation($this->owner->Locale)
|
||||
) {
|
||||
$parentTranslation = $this->owner->Parent()->createTranslation($this->owner->Locale);
|
||||
$this->owner->ParentID = $parentTranslation->ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
// or just updated
|
||||
if($this->owner->_TranslatableIsNewRecord) {
|
||||
// this would kick in for all new records which are NOT
|
||||
// created through createTranslation(), meaning they don't
|
||||
// have the translation group automatically set.
|
||||
$translationGroupID = $this->getTranslationGroup();
|
||||
if(!$translationGroupID) $this->addTranslationGroup($this->owner->_TranslationGroupID ? $this->owner->_TranslationGroupID : $this->owner->ID);
|
||||
unset($this->owner->_TranslatableIsNewRecord);
|
||||
unset($this->owner->_TranslationGroupID);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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",
|
||||
// hence have to modify the original fields
|
||||
$isTranslationMode = (Translatable::default_lang() != $this->getLang() && $this->getLang());
|
||||
if($isTranslationMode) {
|
||||
$creating = false;
|
||||
$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');
|
||||
|
||||
$translatableFieldNames = $this->getTranslatableFields();
|
||||
$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
|
||||
// (fields are object references, so we can replace them with the translatable CompositeField)
|
||||
foreach($allDataFields as $dataField) {
|
||||
|
||||
if($dataField instanceof HiddenField) continue;
|
||||
if(in_array($dataField->Name(), $translatableFieldNames)) {
|
||||
//var_dump($dataField->Name());
|
||||
// if the field is translatable, perform transformation
|
||||
$fields->replaceField($dataField->Name(), $transformation->transformFormField($dataField));
|
||||
} else {
|
||||
@ -612,36 +712,77 @@ class Translatable extends DataObjectDecorator {
|
||||
$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();
|
||||
foreach ($alreadyTranslatedLangs as $i => $langCode) {
|
||||
$alreadyTranslatedLangs[$i] = i18n::get_language_name($langCode);
|
||||
}
|
||||
} elseif($this->owner->isNew()) {
|
||||
$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),
|
||||
$createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create'))
|
||||
new LiteralField('SaveBeforeCreatingTranslationNote',
|
||||
sprintf('<p class="message">%s</p>',
|
||||
_t('Translatable.NOTICENEWPAGE', 'Please save this page before creating a translation')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
if (count($alreadyTranslatedLangs)) {
|
||||
$fields->addFieldsToTab(
|
||||
'Root.Translations',
|
||||
new FieldSet(
|
||||
new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3),
|
||||
new LiteralField('existingtrans',implode(', ',$alreadyTranslatedLangs))
|
||||
)
|
||||
);
|
||||
}
|
||||
$langDropdown->addExtraClass('languageDropdown');
|
||||
$createButton->addExtraClass('createTranslationButton');
|
||||
$createButton->includeDefaultJS(false);
|
||||
}
|
||||
|
||||
// Show a dropdown to create a new translation.
|
||||
// This action is possible both when showing the "default language"
|
||||
// and a translation.
|
||||
$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
|
||||
// and otherwise wouldn't show up here
|
||||
//$alreadyTranslatedLangs[Translatable::default_locale()] = i18n::get_locale_name(Translatable::default_locale());
|
||||
|
||||
// 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
|
||||
*/
|
||||
function fieldsInExtraTables($table){
|
||||
|
||||
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;
|
||||
return array('db'=>null,'indexes'=>null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -731,33 +816,175 @@ class Translatable extends DataObjectDecorator {
|
||||
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) {
|
||||
if((($lang = self::current_lang()) && !self::is_default_lang())) {
|
||||
if (self::table_exists("{$table}_lang")) return $table.'_lang';
|
||||
}
|
||||
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)
|
||||
*
|
||||
* @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 = '') {
|
||||
if(!Translatable::is_enabled()) return false;
|
||||
$baseTable = ClassInfo::baseDataClass($className);
|
||||
$query = new SQLQuery('Lang',$baseTable.'_lang',$where,"",'Lang');
|
||||
$query = new SQLQuery('Distinct Locale',$baseTable,$where,"",'Locale');
|
||||
$dbLangs = $query->execute()->column();
|
||||
$langlist = array_merge((array)Translatable::default_lang(), (array)$dbLangs);
|
||||
$langlist = array_merge((array)Translatable::default_locale(), (array)$dbLangs);
|
||||
$returnMap = array();
|
||||
$allCodes = array_merge(i18n::$all_locales, i18n::$common_languages);
|
||||
$allCodes = array_merge(i18n::$all_locales, i18n::$common_locales);
|
||||
foreach ($langlist as $langCode) {
|
||||
if($langCode)
|
||||
$returnMap[$langCode] = (is_array($allCodes[$langCode]) ? $allCodes[$langCode][0] : $allCodes[$langCode]);
|
||||
@ -765,6 +992,115 @@ class Translatable extends DataObjectDecorator {
|
||||
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->setName($fieldname.'_holder');
|
||||
$nonEditableField_holder->addExtraClass('originallang_holder');
|
||||
|
||||
$nonEditableField->setValue($this->original->$fieldname);
|
||||
$nonEditableField->setName($fieldname.'_original');
|
||||
$nonEditableField->addExtraClass('originallang');
|
||||
|
@ -325,22 +325,8 @@ class Versioned extends DataObjectDecorator {
|
||||
* @return boolean
|
||||
*/
|
||||
function canBeVersioned($table) {
|
||||
|
||||
$tableParts = explode('_',$table);
|
||||
$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;
|
||||
$dbFields = singleton($table)->databaseFields();
|
||||
return !(!ClassInfo::exists($table) || !is_subclass_of($table, 'DataObject' ) || empty( $dbFields ));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -219,9 +219,7 @@ abstract class DBField extends ViewableData {
|
||||
* Returns a FormField instance used as a default
|
||||
* for form scaffolding.
|
||||
*
|
||||
* @usedby {@link SearchContext}
|
||||
* @usedby {@link ModelAdmin}
|
||||
* @usedby {@link DataObject::scaffoldFormFields()}
|
||||
* Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}
|
||||
*
|
||||
* @param string $title Optional. Localized title of the generated instance
|
||||
* @return FormField
|
||||
@ -236,9 +234,7 @@ abstract class DBField extends ViewableData {
|
||||
* Returns a FormField instance used as a default
|
||||
* for searchform scaffolding.
|
||||
*
|
||||
* @usedby {@link SearchContext}
|
||||
* @usedby {@link ModelAdmin}
|
||||
* @usedby {@link DataObject::scaffoldFormFields()}
|
||||
* Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}.
|
||||
*
|
||||
* @param string $title Optional. Localized title of the generated instance
|
||||
* @return FormField
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage model
|
||||
*/
|
||||
class Double extends DBField {
|
||||
|
||||
|
@ -21,7 +21,7 @@ class ForeignKey extends Int {
|
||||
|
||||
protected static $default_search_filter_class = 'ExactMatchMultiFilter';
|
||||
|
||||
function __construct($name, $object) {
|
||||
function __construct($name, $object = null) {
|
||||
$this->object = $object;
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A special type Int field used for primary keys.
|
||||
*
|
||||
* @todo Allow for custom limiting/filtering of scaffoldFormField dropdown
|
||||
*
|
||||
* @param string $name
|
||||
* @param DataOject $object The object that this is primary key for (should have a relation with $name)
|
||||
* @package sapphire
|
||||
* @subpackage model
|
||||
*/
|
||||
class PrimaryKey extends Int {
|
||||
/**
|
||||
@ -16,6 +15,10 @@ class PrimaryKey extends Int {
|
||||
|
||||
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) {
|
||||
$this->object = $object;
|
||||
parent::__construct($name);
|
||||
|
@ -190,15 +190,14 @@ abstract class BulkLoader extends ViewableData {
|
||||
* Useful for generation of spec documents for technical end users.
|
||||
*
|
||||
* Return Format:
|
||||
* <example>
|
||||
* <code>
|
||||
* array(
|
||||
* 'fields' => array('myFieldName'=>'myDescription'),
|
||||
* 'relations' => array('myRelationName'=>'myDescription'),
|
||||
* )
|
||||
* </example>
|
||||
* </code>
|
||||
*
|
||||
* @todo Mix in custom column mappings
|
||||
* @usedby {@link ModelAdmin}
|
||||
*
|
||||
* @return array
|
||||
**/
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -19,6 +18,9 @@
|
||||
* $obj->write();
|
||||
* }
|
||||
* </code>
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage bulkloading
|
||||
*/
|
||||
class CSVParser extends Object implements Iterator {
|
||||
protected $filename;
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test reporter optimised for CLI (ie, plain-text) output
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage testing
|
||||
*/
|
||||
class CliTestReporter extends SapphireTestReporter {
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Allows human reading of a test in a format suitable for agile documentation
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage tools
|
||||
*/
|
||||
class CodeViewer extends Controller {
|
||||
/**
|
||||
|
@ -255,7 +255,8 @@ class Debug {
|
||||
if(Director::is_ajax()) {
|
||||
echo $friendlyErrorMessage;
|
||||
} 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");
|
||||
} else {
|
||||
$renderer = new DebugView();
|
||||
|
@ -5,7 +5,7 @@
|
||||
* and includes them as iFrames.
|
||||
*
|
||||
* 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">
|
||||
* <html>
|
||||
* <head>
|
||||
@ -28,7 +28,7 @@
|
||||
* <div id="main"></div>
|
||||
* </body>
|
||||
* </html>
|
||||
* </example>
|
||||
* </code>
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage testing
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Gives you a nice way of viewing your data model.
|
||||
* Access at dev/viewmodel
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage tools
|
||||
*/
|
||||
class ModelViewer extends Controller {
|
||||
static $url_handlers = array(
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class to facilitate command-line output.
|
||||
* Support less-trivial output stuff such as colours (on xterm-color)
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage dev
|
||||
*/
|
||||
class SSCli extends Object {
|
||||
static function supports_colour() {
|
||||
|
@ -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;
|
||||
|
||||
@ -152,6 +163,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
$this->originalIsRunningTest = null;
|
||||
}
|
||||
|
||||
static function tear_down_once() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the log of emails sent
|
||||
*/
|
||||
|
@ -10,6 +10,9 @@
|
||||
* 0.6 First created [David Spurr]
|
||||
* 0.7 Added fix to getTestException provided [Glen Ogilvie]
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage testing
|
||||
*
|
||||
* @version 0.7 2006-03-12
|
||||
* @author David Spurr
|
||||
*/
|
||||
|
32
dev/SapphireTestSuite.php
Normal file
32
dev/SapphireTestSuite.php
Normal 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();");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
@ -145,7 +145,7 @@ class TestRunner extends Controller {
|
||||
foreach($classList as $className) {
|
||||
// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
|
||||
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
|
||||
|
@ -469,9 +469,7 @@ class Email extends ViewableData {
|
||||
* unless overwritten. Also shown to users on live environments
|
||||
* as a contact address on system error pages.
|
||||
*
|
||||
* @usedby Email->send()
|
||||
* @usedby Email->sendPlain()
|
||||
* @usedby Debug->friendlyError()
|
||||
* Used by {@link Email->send()}, {@link Email->sendPlain()}, {@link Debug->friendlyError()}.
|
||||
*
|
||||
* @param string $newEmail
|
||||
*/
|
||||
|
@ -13,7 +13,7 @@ class LanguageDropdownField extends GroupedDropdownField {
|
||||
* @param string $title
|
||||
* @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 $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' ) {
|
||||
$usedlangs = array_diff(
|
||||
@ -26,22 +26,24 @@ class LanguageDropdownField extends GroupedDropdownField {
|
||||
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();
|
||||
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();
|
||||
|
||||
$alllangs = array_diff(
|
||||
$languageList,
|
||||
(array)$usedlangs,
|
||||
$dontInclude
|
||||
);
|
||||
$languageList,
|
||||
(array)$usedlangs,
|
||||
$dontInclude
|
||||
);
|
||||
$alllangs = array_flip(array_diff(
|
||||
array_flip($alllangs),
|
||||
$dontInclude
|
||||
));
|
||||
if (isset($alllangs[Translatable::default_lang()])) unset($alllangs[Translatable::default_lang()]);
|
||||
array_flip($alllangs),
|
||||
$dontInclude
|
||||
));
|
||||
if (isset($alllangs[Translatable::default_locale()])) unset($alllangs[Translatable::default_locale()]);
|
||||
|
||||
asort($alllangs);
|
||||
if (count($usedlangs)) {
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage forms
|
||||
*/
|
||||
class NestedForm extends ViewableData {
|
||||
protected $form;
|
||||
|
7
main.php
7
main.php
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/************************************************************************************
|
||||
************************************************************************************
|
||||
** **
|
||||
@ -9,6 +8,10 @@
|
||||
************************************************************************************
|
||||
************************************************************************************/
|
||||
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage core
|
||||
*/
|
||||
$majorVersion = strtok(phpversion(),'.');
|
||||
if($majorVersion < 5) {
|
||||
header("HTTP/1.1 500 Server Error");
|
||||
@ -26,7 +29,7 @@ if($majorVersion < 5) {
|
||||
* - Gets an up-to-date manifest from {@link ManifestBuilder}
|
||||
* - Sets up error handlers with {@link Debug::loadErrorHandlers()}
|
||||
* - 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()}
|
||||
*
|
||||
* After that, it calls {@link Director::direct()}, which is responsible for doing most of the
|
||||
|
@ -1,10 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage core
|
||||
*
|
||||
* Alternative main.php file for servers that need the php5 extension
|
||||
*/
|
||||
|
||||
include("main.php");
|
||||
?>
|
@ -8,7 +8,6 @@
|
||||
* @package sapphire
|
||||
* @subpackage parsers
|
||||
* @author Ingo Schommer, Silverstripe Ltd. (<firstname>@silverstripe.com)
|
||||
* @usedby Database->databaseError()
|
||||
*/
|
||||
class SQLFormatter extends Object {
|
||||
|
||||
|
@ -12,14 +12,8 @@
|
||||
* In case you need multiple contexts, consider namespacing your request parameters
|
||||
* by using {@link FieldSet->namespace()} on the $fields constructor parameter.
|
||||
*
|
||||
* @usedby {@link ModelAdmin}
|
||||
*
|
||||
* @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
|
||||
* @package sapphire
|
||||
* @subpackage search
|
||||
*/
|
||||
class SearchContext extends Object {
|
||||
|
||||
@ -58,8 +52,14 @@ class SearchContext extends Object {
|
||||
* Usually these values come from a submitted searchform
|
||||
* in the form of a $_REQUEST object.
|
||||
* 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) {
|
||||
$this->modelClass = $modelClass;
|
||||
$this->fields = ($fields) ? $fields : new FieldSet();
|
||||
|
@ -3,6 +3,11 @@
|
||||
* Standard basic search form which conducts a fulltext search on all {@link SiteTree}
|
||||
* 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
|
||||
* @package sapphire
|
||||
* @subpackage search
|
||||
@ -49,6 +54,10 @@ class SearchForm extends Form {
|
||||
));
|
||||
}
|
||||
|
||||
if(singleton('SiteTree')->hasExtension('Translatable')) {
|
||||
$fields->push(new HiddenField('locale', 'locale', Translatable::current_locale()));
|
||||
}
|
||||
|
||||
if(!$actions) {
|
||||
$actions = new FieldSet(
|
||||
new FormAction("getResults", _t('SearchForm.GO', 'Go'))
|
||||
@ -94,6 +103,11 @@ class SearchForm extends Form {
|
||||
// legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials
|
||||
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'];
|
||||
|
||||
$andProcessor = create_function('$matches','
|
||||
|
@ -1,14 +1,9 @@
|
||||
<?php
|
||||
/**
|
||||
* @package search
|
||||
* @subpackage filters
|
||||
*/
|
||||
|
||||
/**
|
||||
* Matches on rows where the field is not equal to the given value.
|
||||
*
|
||||
* @package search
|
||||
* @subpackage filters
|
||||
* @package sapphire
|
||||
* @subpackage search
|
||||
*/
|
||||
class NegationFilter extends SearchFilter {
|
||||
|
||||
|
@ -181,8 +181,6 @@ abstract class SearchFilter extends Object {
|
||||
* Relies on the field being populated with
|
||||
* {@link setValue()}
|
||||
*
|
||||
* @usedby SearchContext
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEmpty() {
|
||||
|
@ -16,7 +16,8 @@ class BasicAuth extends Object {
|
||||
/**
|
||||
* 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|array $permissionCode
|
||||
* @return Member $member
|
||||
|
85
tasks/MigrateTranslatableTask.php
Normal file
85
tasks/MigrateTranslatableTask.php
Normal 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>';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -6,7 +6,7 @@ class ArrayDataTest extends SapphireTest {
|
||||
/* ViewableData objects will be preserved, but other objects will be converted */
|
||||
$arrayData = new ArrayData(array(
|
||||
"A" => new Varchar("A"),
|
||||
"B" => new Object(),
|
||||
"B" => new stdClass(),
|
||||
));
|
||||
$this->assertEquals("Varchar", get_class($arrayData->A));
|
||||
$this->assertEquals("ArrayData", get_class($arrayData->B));
|
||||
|
@ -35,6 +35,10 @@ class ControllerTest extends SapphireTest {
|
||||
|
||||
$response = Director::test("ControllerTest_SecuredController/adminonly");
|
||||
$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());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,3 +110,5 @@ class ControllerTest_SecuredController extends Controller {
|
||||
return "You must be an admin!";
|
||||
}
|
||||
}
|
||||
|
||||
class ControllerTest_UnsecuredController extends ControllerTest_SecuredController {}
|
@ -44,7 +44,7 @@ class ObjectTest extends SapphireTest {
|
||||
foreach($trueMethods as $method) {
|
||||
$methodU = 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($methodL), "Test that obj#$i has method $methodL");
|
||||
|
||||
@ -88,12 +88,6 @@ class ObjectTest extends SapphireTest {
|
||||
$obj->stat('mystaticProperty'),
|
||||
'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() {
|
||||
@ -107,39 +101,6 @@ class ObjectTest extends SapphireTest {
|
||||
'MyObject',
|
||||
'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() {
|
||||
@ -168,16 +129,224 @@ class ObjectTest extends SapphireTest {
|
||||
'changed',
|
||||
'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(
|
||||
ObjectTest_MyObject::$mystaticProperty,
|
||||
'changed',
|
||||
'Statics setting through set_stat() reflects on PHP built-in statics on the class'
|
||||
Object::get_extensions('ObjectTest_ExtensionTest'),
|
||||
array(
|
||||
'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 {
|
||||
function testMethod() {
|
||||
return true;
|
||||
@ -243,3 +412,77 @@ class ObjectTest_MySubObject extends ObjectTest_MyObject {
|
||||
static $mystaticSubProperty = "MySubObject";
|
||||
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)"; }
|
||||
}
|
||||
|
||||
/**#@-*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* @todo Test Versioned getters
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage tests
|
||||
*/
|
||||
@ -7,38 +9,180 @@ class TranslatableTest extends FunctionalTest {
|
||||
|
||||
static $fixture_file = 'sapphire/tests/model/TranslatableTest.yml';
|
||||
|
||||
protected $recreateTempDb = true;
|
||||
|
||||
/**
|
||||
* @todo Necessary because of monolithic Translatable design
|
||||
*/
|
||||
protected $origTranslatableSettings = array();
|
||||
static protected $origTranslatableSettings = array();
|
||||
|
||||
function setUp() {
|
||||
$this->origTranslatableSettings['enabled'] = Translatable::is_enabled();
|
||||
$this->origTranslatableSettings['default_lang'] = Translatable::default_lang();
|
||||
Translatable::enable();
|
||||
Translatable::set_default_lang("en");
|
||||
|
||||
// needs to recreate the database schema with *_lang tables
|
||||
static function set_up_once() {
|
||||
// needs to recreate the database schema with language properties
|
||||
self::kill_temp_db();
|
||||
self::create_temp_db();
|
||||
|
||||
parent::setUp();
|
||||
// store old defaults
|
||||
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() {
|
||||
if(!$this->origTranslatableSettings['enabled']) Translatable::disable();
|
||||
Translatable::set_default_lang($this->origTranslatableSettings['default_lang']);
|
||||
static function tear_down_once() {
|
||||
if(!self::$origTranslatableSettings['has_extension']) Object::remove_extension('SiteTree', 'Translatable');
|
||||
|
||||
Translatable::set_default_locale(self::$origTranslatableSettings['default_locale']);
|
||||
|
||||
self::kill_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() {
|
||||
$pageOrigLang = $this->objFromFixture('Page', 'home');
|
||||
$pageOrigLang = $this->objFromFixture('Page', 'testpage_en');
|
||||
|
||||
// first test with default language
|
||||
$fields = $pageOrigLang->getCMSFields();
|
||||
@ -53,7 +197,7 @@ class TranslatableTest extends FunctionalTest {
|
||||
);
|
||||
|
||||
// then in "translation mode"
|
||||
$pageTranslated = Translatable::get_one_by_lang('Page',"fr", "ID = $pageOrigLang->ID");
|
||||
$pageTranslated = $pageOrigLang->createTranslation('fr_FR');
|
||||
$fields = $pageTranslated->getCMSFields();
|
||||
$this->assertType(
|
||||
'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');
|
||||
?>
|
@ -1,12 +1,43 @@
|
||||
Page:
|
||||
home:
|
||||
homepage_en:
|
||||
Title: Home
|
||||
URLSegment: home
|
||||
ShowInMenus:
|
||||
|
||||
SiteTree_lang:
|
||||
home:
|
||||
OriginalLangID: =>Page.home
|
||||
Title: Home fr
|
||||
Lang: fr
|
||||
ClassName: Page
|
||||
Locale: en_US
|
||||
testpage_en:
|
||||
Title: Home
|
||||
MenuTitle: A Testpage
|
||||
URLSegment: testpage
|
||||
Locale: en_US
|
||||
othertestpage_en:
|
||||
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
|
@ -219,6 +219,8 @@ class SecurityTest extends FunctionalTest {
|
||||
*/
|
||||
function doTestLoginForm($email, $password, $backURL = 'test/link') {
|
||||
$this->session()->inst_set('BackURL', $backURL);
|
||||
|
||||
$this->get('Security/logout');
|
||||
$this->get('Security/login');
|
||||
|
||||
return $this->submitForm(
|
||||
|
Loading…
Reference in New Issue
Block a user