2007-07-19 12:40:28 +02:00
< ? php
2008-02-25 03:10:37 +01:00
/**
* A security group .
2008-10-16 02:49:51 +02:00
*
2008-02-25 03:10:37 +01:00
* @ package sapphire
* @ subpackage security
*/
2007-07-19 12:40:28 +02:00
class Group extends DataObject {
static $db = array (
" Title " => " Varchar " ,
" Description " => " Text " ,
" Code " => " Varchar " ,
" Locked " => " Boolean " ,
" Sort " => " Int " ,
2009-08-10 06:32:39 +02:00
" HtmlEditorConfig " => " Varchar "
2007-07-19 12:40:28 +02:00
);
2008-10-16 02:49:51 +02:00
2007-07-19 12:40:28 +02:00
static $has_one = array (
2008-11-21 02:06:21 +01:00
" Parent " => " Group " ,
2007-07-19 12:40:28 +02:00
);
2008-10-16 02:49:51 +02:00
2008-08-09 08:53:26 +02:00
static $has_many = array (
" Permissions " => " Permission " ,
2008-11-21 02:06:21 +01:00
" Groups " => " Group "
2008-08-09 08:53:26 +02:00
);
2008-10-16 02:49:51 +02:00
2007-07-19 12:40:28 +02:00
static $many_many = array (
" Members " => " Member " ,
2009-10-16 00:27:56 +02:00
" Roles " => " PermissionRole " ,
2007-07-19 12:40:28 +02:00
);
static $extensions = array (
" Hierarchy " ,
);
2011-03-15 22:16:01 +01:00
function populateDefaults () {
parent :: populateDefaults ();
if ( ! $this -> Title ) $this -> Title = _t ( 'SecurityAdmin.NEWGROUP' , " New Group " );
}
2010-04-12 03:46:20 +02:00
function getAllChildren () {
2011-05-05 12:40:24 +02:00
$doSet = new ArrayList ();
2010-04-12 03:46:20 +02:00
2010-05-25 06:23:03 +02:00
if ( $children = DataObject :: get ( 'Group' , '"ParentID" = ' . $this -> ID )) {
2010-04-12 03:46:20 +02:00
foreach ( $children as $child ) {
$doSet -> push ( $child );
$doSet -> merge ( $child -> getAllChildren ());
}
}
return $doSet ;
}
2008-04-09 13:34:10 +02:00
/**
* Caution : Only call on instances , not through a singleton .
2012-03-08 18:20:11 +01:00
* The " root group " fields will be created through { @ link SecurityAdmin -> EditForm ()} .
2008-04-09 13:34:10 +02:00
*
2011-10-28 03:37:27 +02:00
* @ return FieldList
2008-04-09 13:34:10 +02:00
*/
public function getCMSFields () {
2011-03-16 02:18:30 +01:00
Requirements :: javascript ( SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js' );
2011-05-11 09:51:54 +02:00
$fields = new FieldList (
2008-04-09 13:34:10 +02:00
new TabSet ( " Root " ,
2010-10-19 02:54:16 +02:00
new Tab ( 'Members' , _t ( 'SecurityAdmin.MEMBERS' , 'Members' ),
2008-11-02 21:04:10 +01:00
new TextField ( " Title " , $this -> fieldLabel ( 'Title' )),
2012-03-07 22:41:04 +01:00
$parentidfield = Object :: create ( 'DropdownField' ,
2012-03-05 18:31:52 +01:00
'ParentID' ,
$this -> fieldLabel ( 'Parent' ),
DataList :: create ( 'Group' ) -> exclude ( 'ID' , $this -> ID ) -> map ( 'ID' , 'Breadcrumbs' )
2012-03-07 22:41:04 +01:00
) -> setEmptyString ( ' ' )
2008-04-09 13:34:10 +02:00
),
2010-10-19 02:54:16 +02:00
$permissionsTab = new Tab ( 'Permissions' , _t ( 'SecurityAdmin.PERMISSIONS' , 'Permissions' ),
2012-03-06 01:22:10 +01:00
$permissionsField = new PermissionCheckboxSetField (
2009-10-29 00:03:35 +01:00
'Permissions' ,
2010-05-25 06:58:17 +02:00
false ,
2009-10-29 00:03:35 +01:00
'Permission' ,
2009-11-03 02:00:54 +01:00
'GroupID' ,
2010-02-22 05:37:34 +01:00
$this
)
2008-04-09 13:34:10 +02:00
)
)
);
2012-03-07 22:41:04 +01:00
$parentidfield -> setRightTitle ( '<span class="aligned_right_label">' . _t ( 'Group.GroupReminder' , 'If you choose a parent group, this group will take all it\'s roles' ) . '</span>' );
2012-03-05 18:31:52 +01:00
2012-03-06 01:22:10 +01:00
// Filter permissions
2012-03-09 00:54:02 +01:00
// TODO SecurityAdmin coupling, not easy to get to the form fields through GridFieldDetailForm
2012-03-06 01:22:10 +01:00
$permissionsField -> setHiddenPermissions ( SecurityAdmin :: $hidden_permissions );
2012-03-05 18:31:52 +01:00
if ( $this -> ID ) {
$config = new GridFieldConfig_RelationEditor ();
2012-03-09 00:54:02 +01:00
$config -> addComponents ( new GridFieldExportButton ());
$config -> getComponentByType ( 'GridFieldAddExistingAutocompleter' )
2012-03-05 18:31:52 +01:00
-> setResultsFormat ( '$Title ($Email)' ) -> setSearchFields ( array ( 'FirstName' , 'Surname' , 'Email' ));
2012-03-09 00:54:02 +01:00
$config -> getComponentByType ( 'GridFieldDetailForm' ) -> setValidator ( new Member_Validator ());
2012-03-05 18:31:52 +01:00
$memberList = Object :: create ( 'GridField' , 'Members' , false , $this -> Members (), $config ) -> addExtraClass ( 'members_grid' );
// @todo Implement permission checking on GridField
//$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
$fields -> addFieldToTab ( 'Root.Members' , $memberList );
}
2009-10-16 00:40:52 +02:00
2010-02-22 10:38:15 +01:00
// Only add a dropdown for HTML editor configurations if more than one is available.
// Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
$editorConfigMap = HtmlEditorConfig :: get_available_configs_map ();
if ( count ( $editorConfigMap ) > 1 ) {
$fields -> addFieldToTab ( 'Root.Permissions' ,
new DropdownField (
'HtmlEditorConfig' ,
'HTML Editor Configuration' ,
$editorConfigMap
),
'Permissions'
);
}
2008-04-09 13:34:10 +02:00
2008-04-26 08:27:15 +02:00
if ( ! Permission :: check ( 'EDIT_PERMISSIONS' )) {
$fields -> removeFieldFromTab ( 'Root' , 'Permissions' );
}
2009-10-16 00:13:19 +02:00
2010-05-25 06:58:17 +02:00
// Only show the "Roles" tab if permissions are granted to edit them,
// and at least one role exists
2009-10-16 00:23:39 +02:00
if ( Permission :: check ( 'APPLY_ROLES' ) && DataObject :: get ( 'PermissionRole' )) {
2010-10-13 03:02:00 +02:00
$fields -> findOrMakeTab ( 'Root.Roles' , _t ( 'SecurityAdmin.ROLES' , 'Roles' ));
$fields -> addFieldToTab ( 'Root.Roles' ,
2009-10-16 00:23:39 +02:00
new LiteralField (
" " ,
" <p> " .
2012-03-05 14:40:40 +01:00
_t (
'SecurityAdmin.ROLESDESCRIPTION' ,
" Roles are predefined sets of permissions, and can be assigned to groups.<br />They are inherited from parent groups if required. "
) . '<br />' .
sprintf (
'<a href="%s" class="add-role">%s</a>' ,
singleton ( 'SecurityAdmin' ) -> Link ( 'show/root#Root_Roles' ),
// TODO This should include #Root_Roles to switch directly to the tab,
// but tabstrip.js doesn't display tabs when directly adressed through a URL pragma
_t ( 'Group.RolesAddEditLink' , 'Manage roles' )
) .
2009-10-16 00:23:39 +02:00
" </p> "
)
2009-10-15 23:53:15 +02:00
);
2009-10-16 00:40:52 +02:00
2010-02-22 07:11:59 +01:00
// Add roles (and disable all checkboxes for inherited roles)
$allRoles = Permission :: check ( 'ADMIN' ) ? DataObject :: get ( 'PermissionRole' ) : DataObject :: get ( 'PermissionRole' , 'OnlyAdminCanApply = 0' );
2012-03-05 18:31:52 +01:00
if ( $this -> ID ) {
$groupRoles = $this -> Roles ();
$inheritedRoles = new ArrayList ();
$ancestors = $this -> getAncestors ();
foreach ( $ancestors as $ancestor ) {
$ancestorRoles = $ancestor -> Roles ();
if ( $ancestorRoles ) $inheritedRoles -> merge ( $ancestorRoles );
}
$groupRoleIDs = $groupRoles -> column ( 'ID' ) + $inheritedRoles -> column ( 'ID' );
$inheritedRoleIDs = $inheritedRoles -> column ( 'ID' );
} else {
$groupRoleIDs = array ();
$inheritedRoleIDs = array ();
2010-02-22 07:11:59 +01:00
}
2012-03-05 18:31:52 +01:00
2012-03-05 14:40:40 +01:00
$rolesField = Object :: create ( 'ListboxField' , 'Roles' , false , $allRoles -> map () -> toArray ())
-> setMultiple ( true )
-> setDefaultItems ( $groupRoleIDs )
-> setAttribute ( 'data-placeholder' , _t ( 'Group.AddRole' , 'Add a role for this group' ))
2012-03-05 18:31:52 +01:00
-> setDisabledItems ( $inheritedRoleIDs );
2012-03-05 14:40:40 +01:00
if ( ! $allRoles -> Count ()) $rolesField -> setAttribute ( 'data-placeholder' , _t ( 'Group.NoRoles' , 'No roles found' ));
$fields -> addFieldToTab ( 'Root.Roles' , $rolesField );
2009-10-16 00:23:39 +02:00
}
2009-10-15 23:53:15 +02:00
2008-04-09 13:34:10 +02:00
$fields -> push ( $idField = new HiddenField ( " ID " ));
2008-08-14 01:57:53 +02:00
$this -> extend ( 'updateCMSFields' , $fields );
2008-04-09 13:34:10 +02:00
return $fields ;
}
2009-04-29 02:07:39 +02:00
/**
*
* @ param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
*
*/
function fieldLabels ( $includerelations = true ) {
$labels = parent :: fieldLabels ( $includerelations );
2008-11-02 21:04:10 +01:00
$labels [ 'Title' ] = _t ( 'SecurityAdmin.GROUPNAME' , 'Group name' );
$labels [ 'Description' ] = _t ( 'Group.Description' , 'Description' );
$labels [ 'Code' ] = _t ( 'Group.Code' , 'Group Code' , PR_MEDIUM , 'Programmatical code identifying a group' );
$labels [ 'Locked' ] = _t ( 'Group.Locked' , 'Locked?' , PR_MEDIUM , 'Group is locked in the security administration area' );
$labels [ 'Sort' ] = _t ( 'Group.Sort' , 'Sort Order' );
2009-04-29 02:07:39 +02:00
if ( $includerelations ){
$labels [ 'Parent' ] = _t ( 'Group.Parent' , 'Parent Group' , PR_MEDIUM , 'One group has one parent group' );
$labels [ 'Permissions' ] = _t ( 'Group.has_many_Permissions' , 'Permissions' , PR_MEDIUM , 'One group has many permissions' );
$labels [ 'Members' ] = _t ( 'Group.many_many_Members' , 'Members' , PR_MEDIUM , 'One group has many members' );
}
2008-11-02 21:04:10 +01:00
return $labels ;
}
2007-07-19 12:40:28 +02:00
/**
2010-10-15 05:00:48 +02:00
* @ deprecated 2.5
2007-07-19 12:40:28 +02:00
*/
2010-10-15 05:00:48 +02:00
public static function addToGroupByName ( $member , $groupcode ) {
2011-10-29 01:02:11 +02:00
Deprecation :: notice ( '2.5' , 'Use $member->addToGroupByCode($groupcode) instead.' );
2010-10-15 05:00:48 +02:00
return $member -> addToGroupByCode ( $groupcode );
2007-07-19 12:40:28 +02:00
}
/**
2012-03-06 15:48:56 +01:00
* Get many - many relation to { @ link Member },
* including all members which are " inherited " from children groups of this record .
* See { @ link DirectMembers ()} for retrieving members without any inheritance .
2007-07-19 12:40:28 +02:00
*
2012-03-06 15:48:56 +01:00
* @ param String
* @ return ManyManyList
2007-07-19 12:40:28 +02:00
*/
2009-11-22 06:30:14 +01:00
public function Members ( $filter = " " , $sort = " " , $join = " " , $limit = " " ) {
2011-10-29 06:11:27 +02:00
if ( $sort || $join || $limit ) {
Deprecation :: notice ( '3.0' , " The sort, join, and limit arguments are deprcated, use sort(), join() and limit() on the resulting DataList instead. " );
}
2007-07-19 12:40:28 +02:00
2012-03-06 15:48:56 +01:00
// First get direct members as a base result
$result = $this -> DirectMembers ();
// Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
// Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific ones.
$result -> dataQuery () -> removeFilterOn ( 'Group_Members' );
// Now set all children groups as a new foreign key
$groups = DataList :: create ( " Group " ) -> byIDs ( $this -> collateFamilyIDs ());
$result = $result -> forForeignID ( $groups -> column ( 'ID' )) -> where ( $filter ) -> sort ( $sort ) -> limit ( $limit );
2011-10-29 06:11:27 +02:00
if ( $join ) $result = $result -> join ( $join );
return $result ;
2009-11-22 06:30:14 +01:00
}
/**
* Return only the members directly added to this group
*/
public function DirectMembers () {
return $this -> getManyManyComponents ( 'Members' );
2007-07-19 12:40:28 +02:00
}
2011-10-29 06:08:47 +02:00
public static function map ( $filter = " " , $sort = " " , $blank = " " ) {
Deprecation :: notice ( '3.0' , 'Use DataList::("Group")->map()' );
2007-07-19 12:40:28 +02:00
2011-10-29 06:08:47 +02:00
$list = DataList :: create ( " Group " ) -> where ( $filter ) -> sort ( $sort );
$map = $list -> map ();
if ( $blank ) $map -> unshift ( 0 , $blank );
return $map ;
2007-07-19 12:40:28 +02:00
}
/**
* Return a set of this record ' s " family " of IDs - the IDs of
2008-11-03 02:55:59 +01:00
* this record and all its descendants .
* @ return array
2007-07-19 12:40:28 +02:00
*/
public function collateFamilyIDs () {
2008-11-03 02:55:59 +01:00
$familyIDs = array ();
2007-07-19 12:40:28 +02:00
$chunkToAdd = array ( array ( " ID " => $this -> ID ));
while ( $chunkToAdd ) {
2009-11-21 02:42:27 +01:00
$idList = array ();
2007-07-19 12:40:28 +02:00
foreach ( $chunkToAdd as $item ) {
2008-11-03 02:55:59 +01:00
$idList [] = $item [ 'ID' ];
$familyIDs [] = $item [ 'ID' ];
2007-07-19 12:40:28 +02:00
}
2009-11-21 02:42:27 +01:00
$idList = implode ( ',' , $idList );
2007-07-19 12:40:28 +02:00
// Get the children of *all* the groups identified in the previous chunk.
// This minimises the number of SQL queries necessary
2011-10-29 06:12:37 +02:00
$chunkToAdd = DataList :: create ( 'Group' ) -> where ( " \" ParentID \" IN ( $idList ) " ) -> column ( 'ID' );
2007-07-19 12:40:28 +02:00
}
2008-11-03 02:55:59 +01:00
return $familyIDs ;
2007-07-19 12:40:28 +02:00
}
/**
* Returns an array of the IDs of this group and all its parents
*/
public function collateAncestorIDs () {
$parent = $this ;
2011-02-21 12:19:23 +01:00
while ( isset ( $parent ) && $parent instanceof Group ) {
2007-07-19 12:40:28 +02:00
$items [] = $parent -> ID ;
$parent = $parent -> Parent ;
}
return $items ;
}
2010-08-03 03:05:27 +02:00
/**
* This isn ' t a decendant of SiteTree , but needs this in case
* the group is " reorganised " ;
*/
function cmsCleanup_parentChanged () {
}
2007-07-19 12:40:28 +02:00
/**
* Override this so groups are ordered in the CMS
*/
public function stageChildren () {
2008-11-24 10:31:14 +01:00
return DataObject :: get ( 'Group' , " \" Group \" . \" ParentID \" = " . ( int ) $this -> ID . " AND \" Group \" . \" ID \" != " . ( int ) $this -> ID , '"Sort"' );
2007-07-19 12:40:28 +02:00
}
2010-12-16 06:16:06 +01:00
/**
* @ deprecated 3.0 Use getTreeTitle ()
*/
function TreeTitle () {
2011-10-29 01:02:11 +02:00
Deprecation :: notice ( '3.0' , 'Use getTreeTitle() instead.' );
2010-12-16 06:16:06 +01:00
return $this -> getTreeTitle ();
}
public function getTreeTitle () {
2010-04-12 05:21:00 +02:00
if ( $this -> hasMethod ( 'alternateTreeTitle' )) return $this -> alternateTreeTitle ();
else return htmlspecialchars ( $this -> Title , ENT_QUOTES );
2007-07-19 12:40:28 +02:00
}
/**
* Overloaded to ensure the code is always descent .
2010-12-20 04:18:51 +01:00
*
* @ param string
2007-07-19 12:40:28 +02:00
*/
public function setCode ( $val ){
2010-12-20 04:18:51 +01:00
$this -> setField ( " Code " , Convert :: raw2url ( $val ));
2007-07-19 12:40:28 +02:00
}
2007-10-30 04:17:18 +01:00
function onBeforeWrite () {
parent :: onBeforeWrite ();
2010-08-03 03:05:27 +02:00
2011-04-14 11:38:07 +02:00
// Only set code property when the group has a custom title, and no code exists.
// The "Code" attribute is usually treated as a more permanent identifier than database IDs
// in custom application logic, so can't be changed after its first set.
if ( ! $this -> Code && $this -> Title != _t ( 'SecurityAdmin.NEWGROUP' , " New Group " )) {
if ( ! $this -> Code ) $this -> setCode ( $this -> Title );
2007-10-30 04:17:18 +01:00
}
}
2008-02-25 03:10:37 +01:00
2009-12-16 06:43:47 +01:00
function onAfterDelete () {
parent :: onAfterDelete ();
// Delete associated permissions
$permissions = $this -> Permissions ();
foreach ( $permissions as $permission ) {
$permission -> delete ();
}
}
2009-02-03 23:44:11 +01:00
/**
* Checks for permission - code CMS_ACCESS_SecurityAdmin .
* If the group has ADMIN permissions , it requires the user to have ADMIN permissions as well .
*
* @ param $member Member
* @ return boolean
*/
public function canEdit ( $member = null ) {
if ( ! $member || ! ( is_a ( $member , 'Member' )) || is_numeric ( $member )) $member = Member :: currentUser ();
2011-04-15 11:35:30 +02:00
// extended access checks
2009-02-04 00:33:28 +01:00
$results = $this -> extend ( 'canEdit' , $member );
if ( $results && is_array ( $results )) if ( ! min ( $results )) return false ;
if (
// either we have an ADMIN
( bool ) Permission :: checkMember ( $member , " ADMIN " )
|| (
// or a privileged CMS user and a group without ADMIN permissions.
// without this check, a user would be able to add himself to an administrators group
// with just access to the "Security" admin interface
Permission :: checkMember ( $member , " CMS_ACCESS_SecurityAdmin " ) &&
! DataObject :: get ( " Permission " , " GroupID = $this->ID AND Code = 'ADMIN' " )
)
) {
return true ;
2008-04-26 08:35:39 +02:00
}
2009-02-04 00:33:28 +01:00
return false ;
2008-02-25 03:10:37 +01:00
}
2009-02-02 00:49:53 +01:00
2009-02-03 23:44:11 +01:00
/**
* Checks for permission - code CMS_ACCESS_SecurityAdmin .
*
* @ param $member Member
* @ return boolean
*/
public function canView ( $member = null ) {
if ( ! $member || ! ( is_a ( $member , 'Member' )) || is_numeric ( $member )) $member = Member :: currentUser ();
2011-04-15 11:35:30 +02:00
// extended access checks
2009-02-04 00:33:28 +01:00
$results = $this -> extend ( 'canView' , $member );
if ( $results && is_array ( $results )) if ( ! min ( $results )) return false ;
// user needs access to CMS_ACCESS_SecurityAdmin
if ( Permission :: checkMember ( $member , " CMS_ACCESS_SecurityAdmin " )) return true ;
return false ;
}
public function canDelete ( $member = null ) {
if ( ! $member || ! ( is_a ( $member , 'Member' )) || is_numeric ( $member )) $member = Member :: currentUser ();
2011-04-15 11:35:30 +02:00
// extended access checks
2009-02-04 00:33:28 +01:00
$results = $this -> extend ( 'canDelete' , $member );
if ( $results && is_array ( $results )) if ( ! min ( $results )) return false ;
return $this -> canEdit ( $member );
2009-02-02 00:49:53 +01:00
}
2008-02-25 03:10:37 +01:00
/**
* Returns all of the children for the CMS Tree .
* Filters to only those groups that the current user can edit
*/
function AllChildrenIncludingDeleted () {
2010-05-25 05:55:30 +02:00
$extInstance = $this -> getExtensionInstance ( 'Hierarchy' );
2009-06-04 08:48:44 +02:00
$extInstance -> setOwner ( $this );
$children = $extInstance -> AllChildrenIncludingDeleted ();
$extInstance -> clearOwner ();
2011-05-05 12:40:24 +02:00
$filteredChildren = new ArrayList ();
2008-02-25 03:10:37 +01:00
if ( $children ) foreach ( $children as $child ) {
2009-02-03 23:44:11 +01:00
if ( $child -> canView ()) $filteredChildren -> push ( $child );
2008-02-25 03:10:37 +01:00
}
return $filteredChildren ;
}
2008-04-26 08:35:03 +02:00
2010-08-03 03:05:27 +02:00
/**
* Add default records to database .
*
* This function is called whenever the database is built , after the
* database tables have all been created .
*/
public function requireDefaultRecords () {
parent :: requireDefaultRecords ();
// Add default author group if no other group exists
$allGroups = DataObject :: get ( 'Group' );
2011-03-30 07:06:15 +02:00
if ( ! $allGroups -> count ()) {
2010-08-03 03:05:27 +02:00
$authorGroup = new Group ();
$authorGroup -> Code = 'content-authors' ;
$authorGroup -> Title = _t ( 'Group.DefaultGroupTitleContentAuthors' , 'Content Authors' );
$authorGroup -> Sort = 1 ;
$authorGroup -> write ();
Permission :: grant ( $authorGroup -> ID , 'CMS_ACCESS_CMSMain' );
Permission :: grant ( $authorGroup -> ID , 'CMS_ACCESS_AssetAdmin' );
Permission :: grant ( $authorGroup -> ID , 'CMS_ACCESS_ReportAdmin' );
Permission :: grant ( $authorGroup -> ID , 'SITETREE_REORGANISE' );
}
// Add default admin group if none with permission code ADMIN exists
$adminGroups = Permission :: get_groups_by_permission ( 'ADMIN' );
2011-03-30 07:06:15 +02:00
if ( ! $adminGroups -> count ()) {
2010-08-03 03:05:27 +02:00
$adminGroup = new Group ();
$adminGroup -> Code = 'administrators' ;
$adminGroup -> Title = _t ( 'Group.DefaultGroupTitleAdministrators' , 'Administrators' );
$adminGroup -> Sort = 0 ;
$adminGroup -> write ();
Permission :: grant ( $adminGroup -> ID , 'ADMIN' );
}
// Members are populated through Member->requireDefaultRecords()
}
2009-11-21 03:33:42 +01:00
/**
* @ return String
*/
function CMSTreeClasses ( $controller ) {
$classes = sprintf ( 'class-%s' , $this -> class );
if ( ! $this -> canDelete ())
$classes .= " nodelete " ;
if ( $controller -> isCurrentPage ( $this ))
$classes .= " current " ;
if ( ! $this -> canEdit ())
$classes .= " disabled " ;
$classes .= $this -> markingClasses ();
return $classes ;
}
2007-07-19 12:40:28 +02:00
}
2009-06-22 04:42:42 +02:00
2012-02-12 21:22:11 +01:00