Version: current

      1 <?php
      2 /**
      3  * Enhances {ComplexTableField} with the ability to list groups and given members.
      4  * It is based around groups, so it deletes Members from a Group rather than from the entire system.
      5  *
      6  * In contrast to the original implementation, the URL-parameters "ParentClass" and "ParentID" are used
      7  * to specify "Group" (hardcoded) and the GroupID-relation.
      8  *
      9  *  @todo write a better description about what this field does.
     10  *
     11  * Returns either:
     12  * - provided members
     13  * - members of a provided group
     14  * - all members
     15  * - members based on a search-query
     16  *
     17  * @package cms
     18  * @subpackage security
     19  */
     20 class MemberTableField extends ComplexTableField {
     21
     22     protected $members;
     23
     24     protected $hidePassword;
     25
     26     protected $detailFormValidator;
     27
     28     protected $group;
     29
     30     protected $template = 'MemberTableField';
     31
     32     public $popupClass = 'MemberTableField_Popup';
     33
     34     public $itemClass = 'MemberTableField_Item';
     35
     36     static $data_class = 'Member';
     37
     38     /**
     39      * Set the page size for this table.
     40      * @var int
     41      */
     42     public static $page_size = 20;
     43
     44     protected $permissions = array(
     45         "add",
     46         "edit",
     47         "show",
     48         "delete",
     49 //      'inlineadd',
     50 //      "export",
     51     );
     52
     53     /**
     54      * Constructor method for MemberTableField.
     55      *
     56      * @param Controller $controller Controller class which created this field
     57      * @param string $name Name of the field (e.g. "Members")
     58      * @param mixed $group Can be the ID of a Group instance, or a Group instance itself
     59      * @param DataObjectSet $members Optional set of Members to set as the source items for this field
     60      * @param boolean $hidePassword Hide the password field or not in the summary?
     61      */
     62     function __construct($controller, $name, $group = null, $members = null, $hidePassword = true) {
     63         $sourceClass = self::$data_class;
     64         $SNG_member = singleton($sourceClass);
     65         $fieldList = $SNG_member->summaryFields();
     66         $memberDbFields = $SNG_member->db();
     67         $csvFieldList = array();
     68
     69         foreach($memberDbFields as $field => $dbFieldType) {
     70             $csvFieldList[$field] = $field;
     71         }
     72
     73         if($group) {
     74             if(is_object($group)) {
     75                 $this->group = $group;
     76             } elseif(is_numeric($group)) {
     77                 $this->group = DataObject::get_by_id('Group', $group);
     78             }
     79         } else if(isset($_REQUEST['ctf'][$this->Name()]["ID"]) && is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) {
     80             $this->group = DataObject::get_by_id('Group', $_REQUEST['ctf'][$this->Name()]["ID"]);
     81         }
     82
     83         if(!$hidePassword) {
     84             $fieldList["SetPassword"] = "Password";
     85         }
     86
     87         $this->hidePassword = $hidePassword;
     88
     89         // @todo shouldn't this use $this->group? It's unclear exactly
     90         // what group it should be customising the custom Member set with.
     91         if($members && $group) {
     92             $this->setCustomSourceItems($this->memberListWithGroupID($members, $group));
     93         }
     94
     95         parent::__construct($controller, $name, $sourceClass, $fieldList);
     96
     97         $sourceFilter = array();
     98         $SQL_search = isset($_REQUEST['MemberSearch']) ? Convert::raw2sql($_REQUEST['MemberSearch']) : null;
     99         if(!empty($_REQUEST['MemberSearch'])) {
    100             $searchFilters = array();
    101             foreach($SNG_member->searchableFields() as $fieldName => $fieldSpec) {
    102                 if(strpos($fieldName, '.') === false) $searchFilters[] = "\"$fieldName\" LIKE '%{$SQL_search}%'";
    103             }
    104             $sourceFilter[] = '(' . implode(' OR ', $searchFilters) . ')';
    105         }
    106
    107         if($this->group) {
    108             $groupIDs = array($this->group->ID);
    109             if($this->group->AllChildren()) $groupIDs = array_merge($groupIDs, $this->group->AllChildren()->column('ID'));
    110             $sourceFilter[] = sprintf(
    111                 '"Group_Members"."GroupID" IN (%s)',
    112                 implode(',', $groupIDs)
    113             );
    114         }
    115         $this->sourceFilter = $sourceFilter;
    116
    117         $this->sourceJoin = " INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\"";
    118         $this->setFieldListCsv($csvFieldList);
    119         $this->setPageSize($this->stat('page_size'));
    120     }
    121
    122     function FieldHolder() {
    123         $ret = parent::FieldHolder();
    124
    125         Requirements::javascript(CMS_DIR . '/javascript/MemberTableField.js');
    126         Requirements::javascript(CMS_DIR . "/javascript/MemberTableField_popup.js");
    127
    128         return $ret;
    129     }
    130
    131     function sourceID() {
    132         return ($this->group) ? $this->group->ID : 0;
    133     }
    134
    135     function AddLink() {
    136         return $this->Link() . '/add';
    137     }
    138
    139     function SearchForm() {
    140         $groupID = (isset($this->group)) ? $this->group->ID : 0;
    141         $query = isset($_GET['MemberSearch']) ? $_GET['MemberSearch'] : null;
    142
    143         $searchFields = new FieldGroup(
    144             new TextField('MemberSearch', _t('MemberTableField.SEARCH', 'Search'), $query),
    145             new HiddenField("ctf[ID]", '', $groupID),
    146             new HiddenField('MemberFieldName', '', $this->name),
    147             new HiddenField('MemberDontShowPassword', '', $this->hidePassword)
    148         );
    149
    150         $actionFields = new LiteralField('MemberFilterButton','<input type="submit" class="action" name="MemberFilterButton" value="'._t('MemberTableField.FILTER', 'Filter').'" id="MemberFilterButton"/>');
    151
    152         $fieldContainer = new FieldGroup(
    153             $searchFields,
    154             $actionFields
    155         );
    156
    157         return $fieldContainer->FieldHolder();
    158     }
    159
    160     /**
    161      * Add existing member to group rather than creating a new member
    162      */
    163     function addtogroup() {
    164         $data = $_REQUEST;
    165         $groupID = (isset($data['ctf']['ID'])) ? $data['ctf']['ID'] : null;
    166
    167         if(!is_numeric($groupID)) {
    168             FormResponse::status_messsage(_t('MemberTableField.ADDINGFIELD', 'Adding failed'), 'bad');
    169             return;
    170         }
    171
    172         // Get existing record either by ID or unique identifier.
    173         $identifierField = Member::get_unique_identifier_field();
    174         $className = self::$data_class;
    175         $record = null;
    176         if(isset($data[$identifierField])) {
    177             $record = DataObject::get_one(
    178                 $className,
    179                 sprintf('"%s" = \'%s\'', $identifierField, $data[$identifierField])
    180             );
    181         }
    182
    183         // Fall back to creating a new record
    184         if(!$record) $record = new $className();
    185
    186         // Update an existing record, or populate a new one.
    187         // If values on an existing (autocompleted) record have been changed,
    188         // they will overwrite current data. We need to unset 'ID'
    189         // record as it points to the group rather than the member record, and would
    190         // cause the member to be written to a potentially existing record.
    191         unset($data['ID']);
    192         $record->update($data);
    193
    194         // Validate record, mainly password restrictions.
    195         // Note: Doesn't use Member_Validator
    196         $valid = $record->validate();
    197         if($valid->valid()) {
    198             $record->write();
    199             $record->Groups()->add($groupID);
    200
    201             $this->sourceItems();
    202
    203             // TODO add javascript to highlight added row (problem: might not show up due to sorting/filtering)
    204             FormResponse::update_dom_id($this->id(), $this->renderWith($this->template), true);
    205             FormResponse::status_message(
    206                 _t(
    207                     'MemberTableField.ADDEDTOGROUP','Added member to group'
    208                 ),
    209                 'good'
    210             );
    211
    212         } else {
    213             $message = sprintf(
    214                 _t(
    215                     'MemberTableField.ERRORADDINGUSER',
    216                     'There was an error adding the user to the group: %s'
    217                 ),
    218                 Convert::raw2xml($valid->starredList())
    219             );
    220
    221             FormResponse::status_message($message, 'bad');
    222         }
    223
    224         return FormResponse::respond();
    225     }
    226
    227     /**
    228      * Custom delete implementation:
    229      * Remove member from group rather than from the database
    230      */
    231     function delete() {
    232         $groupID = Convert::raw2sql($_REQUEST['ctf']['ID']);
    233         $memberID = Convert::raw2sql($_REQUEST['ctf']['childID']);
    234         if(is_numeric($groupID) && is_numeric($memberID)) {
    235             $member = DataObject::get_by_id('Member', $memberID);
    236             $member->Groups()->remove($groupID);
    237         } else {
    238             user_error("MemberTableField::delete: Bad parameters: Group=$groupID, Member=$memberID", E_USER_ERROR);
    239         }
    240
    241         return FormResponse::respond();
    242
    243     }
    244
    245     /**
    246      * #################################
    247      *           Utility Functions
    248      * #################################
    249      */
    250     function getParentClass() {
    251         return 'Group';
    252     }
    253
    254     function getParentIdName($childClass, $parentClass) {
    255         return 'GroupID';
    256     }
    257
    258     /**
    259      * #################################
    260      *           Custom Functions
    261      * #################################
    262      */
    263
    264     /**
    265      * Customise an existing DataObjectSet of Member
    266      * objects with a GroupID.
    267      *
    268      * @param DataObjectSet $members Set of Member objects to customise
    269      * @param Group $group Group object to customise with
    270      * @return DataObjectSet Customised set of Member objects
    271      */
    272     function memberListWithGroupID($members, $group) {
    273         $newMembers = new DataObjectSet();
    274         foreach($members as $member) {
    275             $newMembers->push($member->customise(array('GroupID' => $group->ID)));
    276         }
    277         return $newMembers;
    278     }
    279
    280     function setGroup($group) {
    281         $this->group = $group;
    282     }
    283
    284     /**
    285      * @return Group
    286      */
    287     function getGroup() {
    288         return $this->group;
    289     }
    290
    291     function setController($controller) {
    292         $this->controller = $controller;
    293     }
    294
    295     function GetControllerName() {
    296         return $this->controller->class;
    297     }
    298
    299     /**
    300      * Add existing member to group by name (with JS-autocompletion)
    301      */
    302     function AddRecordForm() {
    303         return '';
    304         $fields = new FieldSet();
    305         foreach($this->FieldList() as $fieldName => $fieldTitle) {
    306             // If we're adding the set password field, we want to hide the text from any peeping eyes
    307             if($fieldName == 'SetPassword') {
    308                 $fields->push($f = new PasswordField($fieldName));
    309             } else {
    310                 $fields->push($f = new TextField($fieldName));
    311             }
    312             $f->addExtraAttribute('autocomplete', 'off');
    313         }
    314         if($this->group) {
    315             $fields->push(new HiddenField('ctf[ID]', null, $this->group->ID));
    316         }
    317         $actions = new FieldSet(
    318             new FormAction('addtogroup', _t('MemberTableField.ADD','Add'))
    319         );
    320
    321         return new TabularStyle(
    322             new NestedForm(
    323                 new Form(
    324                     $this,
    325                     'AddRecordForm',
    326                     $fields,
    327                     $actions
    328                 )
    329             )
    330         );
    331     }
    332
    333     function AddForm($childID = NULL) {
    334         $form = parent::AddForm($childID);
    335
    336         // Set default groups - also implemented in MemberTableField_Popup::__construct()
    337         if($this->group) {
    338             $groupsField = $form->Fields()->dataFieldByName('Groups');
    339             if($groupsField) $groupsField->setValue($this->group->ID);
    340         }
    341
    342         return $form;
    343     }
    344
    345     /**
    346      * Same behaviour as parent class, but adds the
    347      * member to the passed GroupID.
    348      *
    349      * @return string
    350      */
    351     function saveComplexTableField($data, $form, $params) {
    352         $className = $this->sourceClass();
    353         $childData = new $className();
    354
    355         // Needs to write before saveInto() to ensure the 'Groups' TreeMultiselectField saves
    356         $childData->write();
    357
    358         try {
    359             $form->saveInto($childData);
    360             $childData->write();
    361         } catch(ValidationException $e) {
    362             var_dump($e->getResult());
    363             $form->sessionMessage($e->getResult()->message(), 'bad');
    364             return Director::redirectBack();
    365         }
    366
    367         $closeLink = sprintf(
    368             '<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
    369             _t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
    370         );
    371         $message = sprintf(
    372             _t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'),
    373             $childData->i18n_singular_name(),
    374             '<a href="' . $this->Link() . '">' . htmlspecialchars($childData->Title, ENT_QUOTES) . '</a>',
    375             $closeLink
    376         );
    377         $form->sessionMessage($message, 'good');
    378
    379         Director::redirectBack();
    380     }
    381
    382     /**
    383      * Cached version for getting the appropraite members for this particular group.
    384      *
    385      * This includes getting inherited groups, such as groups under groups.
    386      */
    387     function sourceItems() {
    388         // Caching.
    389         if($this->sourceItems) {
    390             return $this->sourceItems;
    391         }
    392
    393         // Setup limits
    394         $limitClause = '';
    395         if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
    396             $limitClause = ($_REQUEST['ctf'][$this->Name()]['start']) . ", {$this->pageSize}";
    397         } else {
    398             $limitClause = "0, {$this->pageSize}";
    399         }
    400
    401         // We use the group to get the members, as they already have the bulk of the look up functions
    402         $start = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
    403
    404         $this->sourceItems = false;
    405
    406         if($this->group) {
    407             $this->sourceItems = $this->group->Members(
    408                 $this->pageSize, // limit
    409                 $start, // offset
    410                 $this->sourceFilter,
    411                 $this->sourceSort
    412             );
    413         } else {
    414             $this->sourceItems = DataObject::get(self::$data_class,
    415                 $this->sourceFilter,
    416                 $this->sourceSort,
    417                 null,
    418                 array('limit' => $this->pageSize, 'start' => $start)
    419             );
    420         }
    421         // Because we are not used $this->upagedSourceItems any more, and the DataObjectSet is usually the source
    422         // that a large member set runs out of memory. we disable it here.
    423         //$this->unpagedSourceItems = $this->group->Members('', '', $this->sourceFilter, $this->sourceSort);
    424         $this->totalCount = ($this->sourceItems) ? $this->sourceItems->TotalItems() : 0;
    425
    426         return $this->sourceItems;
    427     }
    428
    429     function TotalCount() {
    430         $this->sourceItems(); // Called for its side-effect of setting total count
    431         return $this->totalCount;
    432     }
    433
    434     /**
    435      * Handles item requests
    436      * MemberTableField needs its own item request class so that it can overload the delete method
    437      */
    438     function handleItem($request) {
    439         return new MemberTableField_ItemRequest($this, $request->param('ID'));
    440     }
    441 }
    442
    443 /**
    444  * Popup window for {@link MemberTableField}.
    445  * @package cms
    446  * @subpackage security
    447  */
    448 class MemberTableField_Popup extends ComplexTableField_Popup {
    449
    450     function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) {
    451         $group = ($controller instanceof MemberTableField) ? $controller->getGroup() : $controller->getParent()->getGroup();
    452         // Set default groups - also implemented in AddForm()
    453         if($group) {
    454             $groupsField = $fields->dataFieldByName('Groups');
    455             if($groupsField) $groupsField->setValue($group->ID);
    456         }
    457
    458         parent::__construct($controller, $name, $fields, $validator, $readonly, $dataObject);
    459     }
    460
    461     function forTemplate() {
    462         $ret = parent::forTemplate();
    463
    464         Requirements::css(CMS_DIR . '/css/SecurityAdmin.css');
    465         Requirements::javascript(CMS_DIR . '/javascript/MemberTableField.js');
    466         Requirements::javascript(CMS_DIR . '/javascript/MemberTableField_popup.js');
    467
    468         return $ret;
    469     }
    470
    471 }
    472
    473 /**
    474 * @package cms
    475 * @subpackage security
    476 */
    477 class MemberTableField_Item extends ComplexTableField_Item {
    478
    479     function Actions() {
    480         $actions = parent::Actions();
    481
    482         foreach($actions as $action) {
    483             if($action->Name == 'delete') {
    484                 if($this->parent->getGroup()) {
    485                     $action->TitleText = _t('MemberTableField.DeleteTitleText',
    486                         'Delete from this group',
    487                         PR_MEDIUM,
    488                         'Delete button hover text'
    489                     );
    490                 } else {
    491                     $action->TitleText = _t('MemberTableField.DeleteTitleTextDatabase',
    492                         'Delete from database and all groups',
    493                         PR_MEDIUM,
    494                         'Delete button hover text'
    495                     );
    496                 }
    497             }
    498         }
    499
    500         return $actions;
    501     }
    502 }
    503
    504 /**
    505 * @package cms
    506 * @subpackage security
    507 */
    508
    509 class MemberTableField_ItemRequest extends ComplexTableField_ItemRequest {
    510
    511     /**
    512      * Deleting an item from a member table field should just remove that member from the group
    513      */
    514     function delete() {
    515         if($this->ctf->Can('delete') !== true) {
    516             return false;
    517         }
    518
    519         // if a group limitation is set on the table, remove relation.
    520         // otherwise remove the record from the database
    521         if($this->ctf->getGroup()) {
    522             $groupID = $this->ctf->sourceID();
    523             $group = DataObject::get_by_id('Group', $groupID);
    524
    525             // Remove from group and all child groups
    526             foreach($group->getAllChildren() as $subGroup) {
    527                 $this->dataObj()->Groups()->remove($subGroup);
    528             }
    529             $this->dataObj()->Groups()->remove($groupID);
    530         } else {
    531             $this->dataObj()->delete();
    532         }
    533     }
    534
    535     function getParent() {
    536         return $this->ctf;
    537     }
    538 }
    539
    540 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -