├── .gitignore ├── assets ├── members.publish.css ├── members.roles.js ├── members.roles.css └── members.events.js ├── LICENCE ├── lib ├── class.membersection.php ├── class.identity.php ├── class.membersevent.php ├── class.members.php └── class.role.php ├── content └── content.events.php ├── fields ├── field.membertimezone.php ├── field.memberusername.php ├── field.memberemail.php ├── field.memberrole.php └── field.memberactivation.php ├── extension.meta.xml ├── lang ├── lang.de.php ├── lang.ru.php ├── lang.el.php └── lang.it.php ├── events ├── event.members_regenerate_activation_code.php ├── event.members_generate_recovery_code.php ├── event.members_activate_account.php └── event.members_reset_password.php └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | .AppleDouble 2 | ._* 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /assets/members.publish.css: -------------------------------------------------------------------------------- 1 | .secondary label > i { 2 | height: 16px; 3 | white-space: nowrap; 4 | } 5 | .secondary label > i:hover { 6 | white-space:normal; 7 | height: auto; 8 | background: rgba(255, 255, 255, 0.8); 9 | } 10 | 11 | .field-memberpassword .column { 12 | margin-bottom: 0; 13 | } 14 | -------------------------------------------------------------------------------- /assets/members.roles.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function(){ 2 | 3 | var $role_permissions = jQuery('table.role-permissions'); 4 | 5 | // find columns containing form elements 6 | $role_permissions.find('thead th.new, thead th.edit') 7 | .each(function() { 8 | // append element for styling 9 | var text = jQuery(this).text(); 10 | 11 | jQuery(this).html('' + text + ''); 12 | }) 13 | .bind('click', function() { 14 | var index = jQuery(this).index(), 15 | // toggle whether this column is all-on or all-off 16 | check_all = jQuery(this).toggleClass('checked').hasClass('checked'); 17 | 18 | // remove toggle state from all other columns 19 | $role_permissions.find('th:not(:eq('+index+'))').removeClass('checked'); 20 | 21 | // toggle form elements in this column ("active" rows only) 22 | $role_permissions.find('tbody tr:not(.inactive) td:nth-child(' + (index + 1) + ')').each(function() { 23 | if(check_all) { 24 | jQuery(this).find('input').attr('checked', 'checked'); 25 | } 26 | else { 27 | jQuery(this).find('input').removeAttr('checked'); 28 | } 29 | }); 30 | }); 31 | 32 | // disable form elements of "inactive" rows 33 | $role_permissions.find('tr.inactive input').attr('disabled', 'disabled').removeAttr('checked'); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | All source code included in the "Members" archive is, unless otherwise 2 | specified, released under the MIT licence as follows: 3 | 4 | ----- begin license block ----- 5 | 6 | Copyright 2011-2018 Symphony Team 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | ----- end license block ----- -------------------------------------------------------------------------------- /assets/members.roles.css: -------------------------------------------------------------------------------- 1 | /* Role Permissions */ 2 | 3 | table.role-permissions { 4 | text-align: center; 5 | width: 100%; 6 | margin-bottom: 2em; 7 | padding: 0; 8 | } 9 | 10 | table.role-permissions th { 11 | text-align: center; 12 | } 13 | 14 | table.role-permissions th { 15 | padding: 3px 6px; 16 | white-space: nowrap; 17 | } 18 | 19 | table.role-permissions th.name { 20 | text-align: left; 21 | } 22 | 23 | table.role-permissions th.new, 24 | table.role-permissions th.edit { 25 | cursor: pointer; 26 | } 27 | 28 | table.role-permissions th span { 29 | padding: 0 0 1px 0; 30 | border-bottom: 1px dotted #666; 31 | } 32 | 33 | table.role-permissions th.new:hover, 34 | table.role-permissions th.edit:hover { 35 | color: #333; 36 | } 37 | 38 | table.role-permissions tr td { 39 | padding: 0 6px; 40 | width: 1px; 41 | } 42 | 43 | table.role-permissions tr td.name { 44 | width: auto; 45 | } 46 | 47 | table.role-permissions td:first-child { 48 | text-align: left; 49 | border-left: 1px solid #E6E6E5; 50 | } 51 | 52 | table.role-permissions td:last-child { 53 | border-right: 1px solid #E6E6E5; 54 | } 55 | 56 | table.role-permissions label { 57 | padding: 6px 25px; 58 | margin: 0; 59 | width: auto; 60 | display: inline-block; 61 | } 62 | 63 | table.role-permissions td.create label { 64 | padding: 6px 50px; 65 | } 66 | 67 | table.role-permissions input { 68 | float: none; 69 | margin: 0; 70 | padding: 0; 71 | width: auto; 72 | } 73 | 74 | table.role-permissions td label span { 75 | color: #666; 76 | margin-left: 0.25em; 77 | display: none; 78 | } 79 | 80 | table.role-permissions td.create span { 81 | display: none; 82 | } 83 | -------------------------------------------------------------------------------- /assets/members.events.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function() { 2 | var notifier = jQuery('#header').find('.notifier'); 3 | 4 | Symphony.Language.add({ 5 | 'Event updated at {$time}. Create another? View all Events': false, 6 | 'An error occurred while processing this form.': false 7 | }); 8 | 9 | Symphony.Members = { 10 | memberEventSave: function() { 11 | jQuery.ajax({ 12 | type: 'post', 13 | url: Symphony.Context.get('symphony') + '/extension/members/events/', 14 | async: false, 15 | data: jQuery('form').serialize(), 16 | beforeSend: function() { 17 | notifier.find('.members').trigger('detach.notify'); 18 | }, 19 | success: function(data, response) { 20 | if(response === "success") { 21 | notifier.trigger('attach.notify', [ 22 | Symphony.Language.get('Event updated at {$time}. Create another? View all Events', { 23 | time: jQuery(data).find('timestamp').text(), 24 | new_url: Symphony.Context.get('symphony') + '/blueprints/events/new/', 25 | url: Symphony.Context.get('symphony') + '/blueprints/events/' 26 | }), 27 | 'success members' 28 | ]); 29 | } 30 | else { 31 | notifier.trigger('attach.notify', [ 32 | Symphony.Language.get('An error occurred while processing this form.'), 33 | 'error members' 34 | ]); 35 | } 36 | }, 37 | error: function(data, response) { 38 | notifier.trigger('attach.notify', [ 39 | Symphony.Language.get('An error occurred while processing this form.'), 40 | 'error members' 41 | ]); 42 | } 43 | }); 44 | 45 | return false; 46 | } 47 | }; 48 | 49 | // Save our the Event's email preference 50 | jQuery('form').on('submit', Symphony.Members.memberEventSave); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/class.membersection.php: -------------------------------------------------------------------------------- 1 | section_id = $section_id; 12 | $this->data = $data; 13 | } 14 | 15 | /** 16 | * Returns the data of this section as an object 17 | * 18 | * @return object 19 | */ 20 | public function getData() { 21 | return (object)$this->data; 22 | } 23 | 24 | /** 25 | * Where `$name` is one of the following values, `role`, `timezone`, 26 | * `email`, `activation`, `authentication` and `identity`, this function 27 | * will return a Field instance. Typically this allows extensions to access 28 | * the Fields that are currently being used in the active Members section. 29 | * 30 | * @param string $type 31 | * @return Field|null 32 | * If `$type` is not given, or no Field was found, null will be returned. 33 | */ 34 | public function getField($type = null) { 35 | if(is_null($type)) return null; 36 | 37 | $type = extension_Members::getFieldType($type); 38 | 39 | // Check to see if this name has been stored in our 'static' cache 40 | // If it hasn't, lets go find it (for better or worse) 41 | if(!isset($this->fields[$type])) { 42 | $this->initialiseField($type, $this->section_id); 43 | } 44 | 45 | // No field, return null 46 | if(!isset($this->fields[$type])) return null; 47 | 48 | // If it has, return it. 49 | return $this->fields[$type]; 50 | } 51 | 52 | /** 53 | * Where `$name` is one of the following values, `role`, `timezone`, 54 | * `email`, `activation`, `authentication` and `identity`, this function 55 | * will return the Field's `element_name`. `element_name` is a handle 56 | * of the Field's label, used most commonly by events in `$_POST` data. 57 | * If no `$name` is given, an array of all Member field handles will 58 | * be returned. 59 | * 60 | * @param string $type 61 | * @return string 62 | */ 63 | public function getFieldHandle($type = null) { 64 | if(is_null($type)) return null; 65 | 66 | $type = extension_Members::getFieldType($type); 67 | 68 | // Check to see if this name has been stored in our 'static' cache 69 | // If it hasn't, lets go find it (for better or worse) 70 | if(!isset($this->handles[$type])) { 71 | $this->initialiseField($type, $this->section_id); 72 | } 73 | 74 | // No field, return null 75 | if(!isset($this->handles[$type])) return null; 76 | 77 | // Return the handle 78 | return $this->handles[$type]; 79 | } 80 | 81 | /** 82 | * Given a `$type` and potentially `$section_id`, fetch the Field 83 | * instance and populate the static `$fields` and `$handles` arrays 84 | * 85 | * @param string $type 86 | * @param integer $section_id 87 | */ 88 | public function initialiseField($type, $section_id = null) { 89 | $field = FieldManager::fetch(null, $section_id, 'ASC', 'sortorder', $type); 90 | 91 | if(!empty($field)) { 92 | $field = current($field); 93 | $this->fields[$type] = $field; 94 | $this->handles[$type] = $field->get('element_name'); 95 | } 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /content/content.events.php: -------------------------------------------------------------------------------- 1 | setHttpStatus(self::HTTP_STATUS_BAD_REQUEST); 11 | return; 12 | } 13 | // Check that the CONFIG is writable 14 | else if (!is_writable(CONFIG)) { 15 | $this->setHttpStatus(self::HTTP_STATUS_ERROR); 16 | $this->_Result->appendChild( 17 | new XMLElement('message', __('The Symphony configuration file, /manifest/config.php, is not writable. You will not be able to save changes to preferences.')) 18 | ); 19 | return; 20 | } 21 | 22 | $settings = $_POST['members']; 23 | 24 | // Generate Recovery Code 25 | if(isset($settings['generate-recovery-code-template'])) { 26 | Symphony::Configuration()->set('generate-recovery-code-template', implode(',', array_filter($settings['generate-recovery-code-template'])), 'members'); 27 | } 28 | // If no template was set, then the user selected nothing, 29 | // so remove the template preference 30 | else if($settings['event'] == 'generate-recovery-code') { 31 | Symphony::Configuration()->remove('generate-recovery-code-template', 'members'); 32 | } 33 | 34 | // Reset Password 35 | if(isset($settings['reset-password-template'])) { 36 | Symphony::Configuration()->set('reset-password-template', implode(',', array_filter($settings['reset-password-template'])), 'members'); 37 | } 38 | else if($settings['event'] == 'reset-password') { 39 | Symphony::Configuration()->remove('reset-password-template', 'members'); 40 | } 41 | 42 | if($settings['event'] == 'reset-password') { 43 | Symphony::Configuration()->set('reset-password-auto-login', $settings['auto-login'], 'members'); 44 | } 45 | 46 | // Regenerate Activation Code 47 | if(isset($settings['regenerate-activation-code-template'])) { 48 | Symphony::Configuration()->set('regenerate-activation-code-template', implode(',', array_filter($settings['regenerate-activation-code-template'])), 'members'); 49 | } 50 | else if($settings['event'] == 'regenerate-activation-code') { 51 | Symphony::Configuration()->remove('regenerate-activation-code-template', 'members'); 52 | } 53 | 54 | // Activate Account 55 | if(isset($settings['activate-account-template'])) { 56 | Symphony::Configuration()->set('activate-account-template', implode(',', array_filter($settings['activate-account-template'])), 'members'); 57 | } 58 | else if($settings['event'] == 'activate-account') { 59 | Symphony::Configuration()->remove('activate-account-template', 'members'); 60 | } 61 | 62 | if($settings['event'] == 'activate-account') { 63 | Symphony::Configuration()->set('activate-account-auto-login', $settings['auto-login'], 'members'); 64 | } 65 | 66 | // Return successful 67 | if(Symphony::Configuration()->write()) { 68 | $this->setHttpStatus(self::HTTP_STATUS_OK); 69 | $this->_Result->appendChild( 70 | new XMLElement('message', __('Preferences saved.')) 71 | ); 72 | $this->_Result->appendChild( 73 | new XMLElement('timestamp', 'generate() . ']]>') 74 | ); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /lib/class.identity.php: -------------------------------------------------------------------------------- 1 | _required = true; 28 | $this->set('required', 'yes'); 29 | } 30 | 31 | public function mustBeUnique() { 32 | return true; 33 | } 34 | 35 | public function canFilter(){ 36 | return true; 37 | } 38 | 39 | public function allowDatasourceParamOutput(){ 40 | return true; 41 | } 42 | 43 | public function canPrePopulate(){ 44 | return true; 45 | } 46 | 47 | /*------------------------------------------------------------------------- 48 | Utilities: 49 | -------------------------------------------------------------------------*/ 50 | 51 | /** 52 | * Given a Member ID, return Member 53 | * 54 | * @param integer $member_id 55 | * @return Entry 56 | */ 57 | public function fetchMemberFromID($member_id){ 58 | if(!(Identity::$driver instanceof Extension)){ 59 | Identity::$driver = Symphony::ExtensionManager()->create('members'); 60 | } 61 | 62 | return Identity::$driver->getMemberDriver()->initialiseMemberObject($member_id); 63 | } 64 | 65 | abstract public function fetchMemberIDBy($needle, $member_id = null); 66 | 67 | /*------------------------------------------------------------------------- 68 | Publish: 69 | -------------------------------------------------------------------------*/ 70 | 71 | public function displayPublishPanel(XMLElement &$wrapper, $data = null, $error = null, $prefix = null, $postfix = null, $entry_id = null) { 72 | $field_id = $this->get('id'); 73 | $handle = $this->get('element_name'); 74 | 75 | // Identity 76 | $label = Widget::Label($this->get('label')); 77 | if(!($this->get('required') == 'yes')) $label->appendChild(new XMLElement('i', __('Optional'))); 78 | 79 | $label->appendChild(Widget::Input( 80 | "fields{$prefix}[{$handle}]{$postfix}", $data['value'] 81 | )); 82 | 83 | // Error? 84 | if(!is_null($error)) { 85 | $wrapper->appendChild(Widget::Error($label, $error)); 86 | } 87 | else { 88 | $wrapper->appendChild($label); 89 | } 90 | } 91 | 92 | /*------------------------------------------------------------------------- 93 | Input: 94 | -------------------------------------------------------------------------*/ 95 | 96 | public function processRawFieldData($data, &$status, &$message=null, $simulate=false, $entry_id=null){ 97 | $status = self::__OK__; 98 | 99 | if(empty($data)) return array(); 100 | 101 | return array( 102 | 'value' => trim($data), 103 | 'handle' => Lang::createHandle(trim($data)) 104 | ); 105 | } 106 | 107 | /*------------------------------------------------------------------------- 108 | Output: 109 | -------------------------------------------------------------------------*/ 110 | 111 | public function prepareTableValue($data, XMLElement $link = null, $entry_id = null){ 112 | if(empty($data)) return __('None'); 113 | 114 | return parent::prepareTableValue(array('value' => General::sanitize($data['value'])), $link); 115 | } 116 | 117 | public function getParameterPoolValue(array $data, $entry_id = null) { 118 | return $data['value']; 119 | } 120 | 121 | /*------------------------------------------------------------------------- 122 | Filtering: 123 | -------------------------------------------------------------------------*/ 124 | 125 | public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation=false){ 126 | 127 | $field_id = $this->get('id'); 128 | 129 | // Filter is an regexp. 130 | if(self::isFilterRegex($data[0])) { 131 | $this->buildRegexSQL($data[0], array('value', 'handle', 'entry_id'), $joins, $where); 132 | } 133 | 134 | // Filter has + in it. 135 | else if($andOperation) { 136 | foreach($data as $key => $bit){ 137 | $bit = Symphony::Database()->cleanValue($bit); 138 | $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id$key` ON (`e`.`id` = `t$field_id$key`.entry_id) "; 139 | $where .= " AND ( 140 | `t$field_id$key`.value = '$bit' 141 | OR `t$field_id$key`.handle = '$bit' 142 | OR `t$field_id$key`.entry_id = '$bit' 143 | ) "; 144 | } 145 | } 146 | 147 | // Normal 148 | else { 149 | if(!is_array($data)) { 150 | $data = array($data); 151 | } 152 | $data = array_map(array(Symphony::Database(), 'cleanValue'), $data); 153 | $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id` ON (`e`.`id` = `t$field_id`.entry_id) "; 154 | $where .= " AND ( 155 | `t$field_id`.value IN ('".implode("', '", $data)."') 156 | OR `t$field_id`.handle IN ('".implode("', '", $data)."') 157 | OR `t$field_id`.entry_id IN ('".implode("', '", $data)."') 158 | ) "; 159 | } 160 | 161 | return true; 162 | } 163 | 164 | /*------------------------------------------------------------------------- 165 | Events: 166 | -------------------------------------------------------------------------*/ 167 | 168 | public function getExampleFormMarkup(){ 169 | 170 | $label = Widget::Label($this->get('label')); 171 | $label->appendChild(Widget::Input('fields['.$this->get('element_name').']')); 172 | 173 | return $label; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /lib/class.membersevent.php: -------------------------------------------------------------------------------- 1 | Error

You cannot directly access this file

'); 4 | 5 | Abstract Class MembersEvent extends Event { 6 | 7 | // For the delegates to populate 8 | public $filter_results = array(); 9 | public $filter_errors = array(); 10 | 11 | // Instance of the Members extension 12 | public $driver = null; 13 | 14 | // Don't allow a user to set permissions for any Members event 15 | // in the Roles interface. 16 | public function ignoreRolePermissions() { 17 | return true; 18 | } 19 | 20 | // The default filters for an event are the XSS Filter 21 | public $eParamFILTERS = array( 22 | 'xss-fail' 23 | ); 24 | 25 | /*------------------------------------------------------------------------- 26 | Utilities: 27 | -------------------------------------------------------------------------*/ 28 | 29 | /** 30 | * This function is directly copied from Symphony's default event 31 | * include. It takes a result from an Event filter and generates XML 32 | * to output with the custom events 33 | */ 34 | public static function buildFilterElement($name, $status, $message=NULL, array $attr=NULL){ 35 | $ret = new XMLElement('filter', (!$message || is_object($message) ? NULL : $message), array( 36 | 'name' => $name, 37 | 'status' => $status 38 | )); 39 | 40 | if(is_object($message)) $ret->appendChild($message); 41 | 42 | if(is_array($attr)) $ret->setAttributeArray($attr); 43 | 44 | return $ret; 45 | } 46 | 47 | protected function addEmailTemplates($template) { 48 | // Read the template from the Configuration if it exists 49 | // This is required for the Email Template Filter/Email Template Manager 50 | if(!is_null(extension_Members::getSetting($template))) { 51 | $this->eParamFILTERS = array_merge( 52 | $this->eParamFILTERS, 53 | explode(',',extension_Members::getSetting($template)) 54 | ); 55 | } 56 | } 57 | 58 | protected function setMembersSection(XMLElement $result, $member_section_id = null) { 59 | // Set the section ID 60 | if(isset($member_section_id) && $this->driver->setMembersSection($member_section_id) === false) { 61 | $result->setAttribute('result', 'error'); 62 | $result->appendChild( 63 | new XMLElement('message', __('Invalid Members section ID given.'), array( 64 | 'message-id' => MemberEventMessages::MEMBER_ERRORS 65 | )) 66 | ); 67 | } 68 | 69 | return $result; 70 | } 71 | 72 | /*------------------------------------------------------------------------- 73 | Delegates: 74 | -------------------------------------------------------------------------*/ 75 | 76 | protected function notifyEventPreSaveFilter(XMLElement &$result, array $fields, XMLElement $post_values) { 77 | /** 78 | * @delegate EventPreSaveFilter 79 | * @param string $context 80 | * '/frontend/' 81 | * @param array $fields 82 | * @param string $event 83 | * @param array $messages 84 | * @param XMLElement $post_values 85 | */ 86 | Symphony::ExtensionManager()->notifyMembers( 87 | 'EventPreSaveFilter', 88 | '/frontend/', 89 | array( 90 | 'fields' => &$fields, 91 | 'event' => &$this, 92 | 'messages' => &$this->filter_results, 93 | 'post_values' => &$post_values 94 | ) 95 | ); 96 | 97 | // Logic taken from `event.section.php` to fail should any `$this->filter_results` 98 | // be returned. This delegate can cause the event to exit early. 99 | if (is_array($this->filter_results) && !empty($this->filter_results)) { 100 | $can_proceed = true; 101 | 102 | foreach ($this->filter_results as $fr) { 103 | list($name, $status, $message, $attributes) = array_pad($fr, 4, null); 104 | 105 | $result->appendChild( 106 | MembersEvent::buildFilterElement($name, ($status ? 'passed' : 'failed'), $message, $attributes) 107 | ); 108 | 109 | if($status === false) $can_proceed = false; 110 | } 111 | 112 | if ($can_proceed !== true) { 113 | $result->setAttribute('result', 'error'); 114 | $result->appendChild($post_values); 115 | $result->appendChild(new XMLElement('message', __('Member event encountered errors when processing.'), array( 116 | 'message-id' => MemberEventMessages::FILTER_FAILED 117 | ))); 118 | return $result; 119 | } 120 | } 121 | } 122 | 123 | protected function notifyEventFinalSaveFilter(XMLElement &$result, array $fields, XMLElement $post_values, Entry $entry) { 124 | // We now need to simulate the EventFinalSaveFilter which the EmailTemplateFilter 125 | // and EmailTemplateManager use to send emails. 126 | /** 127 | * @delegate EventFinalSaveFilter 128 | * @param string $context 129 | * '/frontend/' 130 | * @param array $fields 131 | * @param string $event 132 | * @param array $messages 133 | * @param array $errors 134 | * @param Entry $entry 135 | */ 136 | Symphony::ExtensionManager()->notifyMembers( 137 | 'EventFinalSaveFilter', '/frontend/', array( 138 | 'fields' => $fields, 139 | 'event' => $this, 140 | 'messages' => $this->filter_results, 141 | 'errors' => &$this->filter_errors, 142 | 'entry' => $entry 143 | ) 144 | ); 145 | 146 | // Take the logic from `event.section.php` to append `$this->filter_errors` 147 | if(is_array($this->filter_errors) && !empty($this->filter_errors)){ 148 | foreach($this->filter_errors as $fr){ 149 | list($name, $status, $message, $attributes) = array_pad($fr, 4, null); 150 | 151 | $result->appendChild( 152 | MembersEvent::buildFilterElement($name, ($status ? 'passed' : 'failed'), $message, $attributes) 153 | ); 154 | } 155 | } 156 | } 157 | 158 | protected function notifyMembersPasswordResetFailure($username) { 159 | /** 160 | * A failed password reset attempt 161 | * 162 | * @delegate MembersPasswordResetFailure 163 | * @param string $context 164 | * '/frontend/' 165 | * @param string $username 166 | * Should be the value of the identity field for which the password reset has been attempted 167 | */ 168 | Symphony::ExtensionManager()->notifyMembers( 169 | 'MembersPasswordResetFailure', 170 | '/frontend/', 171 | array( 172 | 'username' => Symphony::Database()->cleanValue($username) 173 | ) 174 | ); 175 | } 176 | } 177 | 178 | /** 179 | * Basic lookup class for Event messages, allows for frontend developers 180 | * to localise and change event messages without relying on string 181 | * comparision. 182 | * 183 | * @since Symphony 2.4 184 | */ 185 | class MemberEventMessages extends EventMessages 186 | { 187 | const MEMBER_ERRORS = 104; 188 | const MEMBER_INVALID = 105; 189 | const UNAUTHORIZED = 106; 190 | 191 | const SECTION_INVALID = 201; 192 | 193 | const ACTIVATION_PRE_COMPLETED = 303; 194 | const ACTIVATION_CODE_INVALID = 304; 195 | const RECOVERY_CODE_INVALID = 305; 196 | const AUTHENTICATION_INVALID = 306; 197 | 198 | const ALREADY_LOGGED_IN = 501; 199 | } 200 | -------------------------------------------------------------------------------- /fields/field.membertimezone.php: -------------------------------------------------------------------------------- 1 | _name = __('Member: Timezone'); 14 | $this->_showassociation = false; 15 | } 16 | 17 | public function mustBeUnique() { 18 | return true; 19 | } 20 | 21 | /*------------------------------------------------------------------------- 22 | Setup: 23 | -------------------------------------------------------------------------*/ 24 | 25 | public static function createSettingsTable() { 26 | return Symphony::Database()->query(" 27 | CREATE TABLE IF NOT EXISTS `tbl_fields_membertimezone` ( 28 | `id` int(11) unsigned NOT NULL auto_increment, 29 | `field_id` int(11) unsigned NOT NULL, 30 | `available_zones` VARCHAR(255) DEFAULT NULL, 31 | PRIMARY KEY (`id`), 32 | UNIQUE KEY `field_id` (`field_id`) 33 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 34 | "); 35 | } 36 | 37 | /*------------------------------------------------------------------------- 38 | Utilities: 39 | -------------------------------------------------------------------------*/ 40 | 41 | /** 42 | * This function will return the offset value for a particular `$member_id` 43 | * The offset is the number of hours +/- from GMT 44 | * 45 | * @param integer $member_id 46 | * @return string 47 | * ie. Africa/Asmara 48 | */ 49 | public function getMemberTimezone($member_id) { 50 | return Symphony::Database()->fetchVar('value', 0, sprintf(" 51 | SELECT `value` 52 | FROM `tbl_entries_data_%d` 53 | WHERE `entry_id` = '%s' 54 | LIMIT 1 55 | ", $this->get('id'), $member_id 56 | )); 57 | } 58 | 59 | /** 60 | * Creates a list of Timezones for the With Selected dropdown in the backend. 61 | * This list has the limitation that the timezones cannot be grouped as the 62 | * With Selected menu already uses `` to separate the toggling of 63 | * different Field data. 64 | * 65 | * @return array 66 | */ 67 | public function getToggleStates() { 68 | $zones = explode(",", $this->get('available_zones')); 69 | 70 | $options = array(); 71 | foreach($zones as $zone) { 72 | $timezones = DateTimeObj::getTimezones($zone); 73 | 74 | foreach($timezones as $timezone) { 75 | $tz = new DateTime('now', new DateTimeZone($timezone)); 76 | 77 | $options[$timezone] = sprintf("%s %s", 78 | str_replace('_', ' ', substr(strrchr($timezone, '/'),1)), 79 | $tz->format('P') 80 | ); 81 | } 82 | } 83 | 84 | return $options; 85 | } 86 | 87 | /** 88 | * Builds a XMLElement containing a ` 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | Event information will be returned in the XML similar to the following example: 32 | 33 | 34 | 35 | 36 | 37 | The `$member-id` and `$member-role` parameters will be added to the Page Parameters for you to use in your datasources to get information about the logged in member. 38 | 39 | Note: All Member information (`$member-id` and `$member-role` parameters, `member-login-info` pseudo event XML) will be added to the page before any real events are executed. So if you change the Member's status via events resp. filters, the new status will not be reflected in these XML nodes. You may work around this rare issue by performing a redirect in your event, e.g. by specifying a redirect location in the corresponding frontend form — all subsequent pages will show the correct Member information, of course. 40 | 41 | 7. You can log a Member out using `Logout` 42 | 43 | 44 | ## Updating to 1.4 45 | 46 | When updating to 1.4, there are a couple of changes that you should be aware of. As discussed in [#242](https://github.com/symphonycms/members/issues/242), when using Role's the default for all new events will be `NO_PERMISSIONS`. This is different from previous versions where new events were considered to be 'ok' unless otherwise stated. 47 | 48 | It may be prudent to check your Member forms as the extension is now throwing additional errors, such as validating email addresses when using the Member Email field, on all Member Events. 49 | 50 | ## Updating to 1.3 51 | 52 | When updating from 1.2, you should not have to do anything except re-enable the extension from the Extensions page. This will increase the length of the password fields so that they can updated to the stronger algorithm as member's log in. When you start to use multiple sections, read the section in the README about [Multiple Section support](#multiple-section-support). 53 | 54 | ## Fields 55 | 56 | This extension provides six additional fields: 57 | 58 | - Member: Username 59 | - Member: Email 60 | - Member: Password 61 | - Member: Role 62 | - Member: Activation 63 | - Member: Timezone 64 | 65 | 66 | ### Member: Username 67 | 68 | The Member: Username field ensures all usernames are unique in the system. You can set a validator to ensure a username follows a particular pattern. Member's can login by providing their username and password (see Member: Password). 69 | 70 | ### Member: Email 71 | 72 | The Member: Email field accepts only an email address and ensures that all email's are unique in the system. This field outputs an MD5 hash of the email address in the XML so that it be used as Gravatar hash. Like the Member: Username field, members can login by providing their email address and password (see Member: Password). 73 | 74 | ### Member: Password 75 | 76 | The Member: Password field has a couple of additional settings to help improve the security of the member's password. Setting a password Salt can be done only once, and is used to provide some randomness when hashing the member's password. You can also set a minimum length required for a password and then there is three possible options for a minimum password strength, Weak, Good and Strong. 77 | 78 | - Weak: This password is all lowercase, all uppercase, all numbers or all punctuation 79 | - Good: The password must be a mixture of two of the following: lowercase, uppercase, numbers or punctuation 80 | - Strong: The password must be a mixture of three or more of the following: lowercase, uppercase, numbers or punctuation 81 | 82 | Passwords must be set with two fields, one to capture the password and one to confirm the password. The corresponding field names are: 83 | 84 | - `fields[{Member: Password element_name}][password]` 85 | - `fields[{Member: Password element_name}][confirm]` 86 | 87 | The `Members: Validate Password` filter requires a field with the following name: 88 | 89 | - `fields[{Member: Password element_name}][validate]` 90 | 91 | #### Events 92 | 93 | - [Members: Generate Recovery Code](https://github.com/symphonycms/members/wiki/Members%3A-Generate-Recovery-Code) 94 | - [Members: Reset Password](https://github.com/symphonycms/members/wiki/Members%3A-Reset-Password) 95 | 96 | #### Filters 97 | 98 | - Members: Validate Password 99 | - Members: Update Password 100 | - Members: Login 101 | 102 | ### Member: Role 103 | 104 | The Member: Role field allows you to assign members with different Roles to allow them to access pages or execute particular events. The Members extension installs with one default Role that cannot be deleted, Public. This Public Role is the default Role that all members of your website will fall under (ie. all unregistered members). This field allows you to set a default role, which the role that a member will take on when they register. 105 | 106 | #### Filters 107 | 108 | - Members: Lock Role 109 | 110 | 111 | ### Member: Activation 112 | 113 | The Member: Activation field enforces that all members who register to your site must first activate their account before they are treated as an authenticated member. This field allows you set a code expiry time, which is how long an activation code is valid for until it expires and a Member will have to request a new one (see Members: Regenerate Activation Code event) and an activation role. The activation role is given to a member when they register to your site, but haven't completed activation. Once they complete activation, they will be set to the default role as defined by the Member: Role field. 114 | 115 | #### Events 116 | 117 | - [Members: Activate Account](https://github.com/symphonycms/members/wiki/Members%3A-Activate-Account) 118 | - [Members: Regenerate Activation Code](https://github.com/symphonycms/members/wiki/Members%3A-Regenerate-Activation-Code) 119 | 120 | #### Filters 121 | 122 | - Members: Lock Activation 123 | 124 | 125 | ### Member: Timezone 126 | 127 | The Member: Timezone field allows members to have times displayed in their own timezone when on the site. It has one setting, Available Zones which allows you to set up what timezones, grouped by 'Zone', are available for members to pick from. 128 | 129 | ## Events 130 | 131 | This extension provides four additional events that can be added to your page: 132 | 133 | - [Members: Activate Account](https://github.com/symphonycms/members/wiki/Members%3A-Activate-Account) 134 | - [Members: Regenerate Activation Code](https://github.com/symphonycms/members/wiki/Members%3A-Regenerate-Activation-Code) 135 | - [Members: Generate Recovery Code](https://github.com/symphonycms/members/wiki/Members%3A-Generate-Recovery-Code) 136 | - [Members: Reset Password](https://github.com/symphonycms/members/wiki/Members%3A-Reset-Password) 137 | 138 | Go to Blueprints > Components and click on the event name to view documentation for that event. 139 | 140 | There are two global events that are available on any page your website: 141 | 142 | ### Members: Login 143 | 144 |
145 | 148 | 151 | 152 | 153 | 154 |
155 | 156 | ### Members: Logout 157 | 158 |
159 | 160 | 161 |
162 | 163 | or 164 | 165 | Logout 166 | 167 | ### Create a Member 168 | 169 | You can create new member records using standard Symphony events on your active members section. The [wiki](https://github.com/symphonycms/members/wiki/Members%3A-New) contains information about the response XML to expect from the fields provided by the Members extension. 170 | 171 | ## Multiple Section support 172 | 173 | Since Members 1.3, multiple section support is possible allowing you to create complex sites that house different types of Member data. One example of this could be having Customers and Administration sections, each with different field configurations. 174 | 175 | For the most part, upgrading your existing Members installation to 1.3 is seamless as the extension will fallback to existing behaviour if only one Members section is detected in your build. 176 | 177 | However, once you start to create multiple sections you will need to add a hidden field to your forms to tell Symphony exactly what section you'd like Members to verify this data against: 178 | 179 | 180 | 181 | This field will need to be added to your Login form at the very least as it tells Symphony to verify the credentials against the given Section schema (so you cannot have Customers logging into the Administration section etc.) 182 | 183 | ### What's changed 184 | 185 | - A `members-section-id` parameter is available for logged in users 186 | - The Member Roles page will now show the member breakdowns per section 187 | 188 | ## Filters 189 | 190 | This extension provides five event filters that you can add to your events to make them useful to Members: 191 | 192 | - Members: Lock Activation 193 | - Members: Lock Role 194 | - Members: Validate Password 195 | - Members: Update Password 196 | - Members: Login 197 | 198 | ### Members: Lock Activation 199 | 200 | The Members: Lock Activation filter is to be attached to your own Registration event to force a Member's activated state to be 'no' when a Member is registering for your site. If the Member already exists, using this filter on your event will ensure the Member's activation value cannot be changed. This prevents any DOM hacking to make members activate themselves. If you do not use the Member: Activation field, then you don't this filter on your Registration event. 201 | 202 | ### Members: Lock Role 203 | 204 | The Members: Lock Role filter should be used as an additional security measure to ensure that the member cannot DOM hack their own Role. This filter ensures a newly registered member will always be of the Default Role or if updating a Member record, the filter ensures the Role doesn't change from the Member's current role. If you do not use the Member: Role field, you don't need this filter on your Registration event. If you want to elevate a Member between Roles, this can be done in the backend, or don't use this filter. Care will need to be taken that a Member is not able to change their Role to whatever they please. 205 | 206 | ### Members: Validate Password 207 | 208 | The Members: Validate Password is a pre-save filter that will check if a posted password is correct for the logged-in Member. If the password is valid the filter will return true and the section event will be executed. Otherwise it will return false, which will terminate the section event before anything is saved. 209 | 210 | ### Members: Update Password 211 | 212 | The Members: Update Password filter is useful on Events where the member may update some of their profile information, and updating their password is optional. It essentially tells the extension that if the member hasn't provided their password, yet it's set to required, it's ok, just remember their current password details. Additionally, if a password has been posted, this filter will log the Member in with this new password. 213 | 214 | ### Members: Login 215 | 216 | The Members: Login filter is useful if a Member's password has been changed by an event. It will log the Member in with the new (posted) password. If you need the password to be optional, use the Members: Update Password filter instead. 217 | 218 | This filter can, for example, be used to directly log a Member in after registration. But since any "magically added" Member information in your page XML is actually added before events are executed, it will not reflect the login (i.e. still tell you that the Member is not logged in). In this case you should enforce a new page request by adding a redirect to your event. 219 | 220 | ## Roles and Permissions 221 | 222 | The Members extension comes with a single default Role, Public. This role cannot be deleted, but it can be renamed and modified to suit your requirements. This Role is assumed by any Frontend member who is not authenticated and by default, does not allow users to use any Events. Remember to modify the Public role to open up Events as necessary. 223 | 224 | Roles allow you to prevent access to certain pages and/or prevent what users can do with your Events. Read more about [Event Permissions](https://github.com/symphonycms/members/wiki/Event-Permissions) on the wiki. 225 | 226 | If a member is not allowed to see a particular page, the Members extension will display the page with the `403` type. 227 | If you want to have a special page for this case, the type `403-members` can also be used. 228 | 229 | ## Email Templates 230 | 231 | The [Email Template Manager](http://symphonyextensions.com/extensions/email_template_manager/) extension can be used to email information specific to the Members extension such as Member Registration, Password Reset and Activation Codes. This extension allows Email Templates to be added as Event Filters to your events. 232 | 233 | When using the email address as the member identifier, be sure to encode email addresses with [a URL encoding template](http://www.getsymphony.com/download/xslt-utilities/view/55460/) when emailing password reset confirmation links to allow for characters such as '+'. All bugs relating to this extension should be reported to it, not the Members extension. 234 | -------------------------------------------------------------------------------- /fields/field.memberrole.php: -------------------------------------------------------------------------------- 1 | _name = __('Member: Role'); 15 | $this->_showassociation = false; 16 | } 17 | 18 | public function canToggle(){ 19 | return true; 20 | } 21 | 22 | public function allowDatasourceOutputGrouping(){ 23 | return true; 24 | } 25 | 26 | public function mustBeUnique(){ 27 | return true; 28 | } 29 | 30 | public function canPrePopulate(){ 31 | return false; 32 | } 33 | 34 | /*------------------------------------------------------------------------- 35 | Setup: 36 | -------------------------------------------------------------------------*/ 37 | 38 | public static function createSettingsTable() { 39 | return Symphony::Database()->query(" 40 | CREATE TABLE IF NOT EXISTS `tbl_fields_memberrole` ( 41 | `id` int(11) unsigned NOT NULL auto_increment, 42 | `field_id` int(11) unsigned NOT NULL, 43 | `default_role` int(11) unsigned NOT NULL, 44 | PRIMARY KEY (`id`), 45 | UNIQUE KEY `field_id` (`field_id`) 46 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 47 | "); 48 | } 49 | 50 | public function createTable(){ 51 | return Symphony::Database()->query( 52 | "CREATE TABLE IF NOT EXISTS `tbl_entries_data_" . $this->get('id') . "` ( 53 | `id` int(11) unsigned NOT NULL auto_increment, 54 | `entry_id` int(11) unsigned NOT NULL, 55 | `role_id` int(11) unsigned NOT NULL, 56 | PRIMARY KEY (`id`), 57 | UNIQUE KEY `entry_id` (`entry_id`) 58 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 59 | "); 60 | } 61 | 62 | /*------------------------------------------------------------------------- 63 | Utilities: 64 | -------------------------------------------------------------------------*/ 65 | 66 | public function getToggleStates(){ 67 | $roles = RoleManager::fetch(); 68 | 69 | $states = array(); 70 | if(is_array($roles) && !empty($roles)) foreach($roles as $r){ 71 | $states[$r->get('id')] = $r->get('name'); 72 | } 73 | 74 | return $states; 75 | } 76 | 77 | public function toggleFieldData(array $data, $newState, $entry_id = null){ 78 | $data['role_id'] = $newState; 79 | return $data; 80 | } 81 | 82 | /*------------------------------------------------------------------------- 83 | Settings: 84 | -------------------------------------------------------------------------*/ 85 | 86 | public function setFromPOST(array $settings = array()) { 87 | $settings['required'] = 'yes'; 88 | 89 | parent::setFromPOST($settings); 90 | } 91 | 92 | public function displaySettingsPanel(XMLElement &$wrapper, $errors = NULL){ 93 | Field::displaySettingsPanel($wrapper, $errors); 94 | 95 | $group = new XMLElement('div'); 96 | $group->setAttribute('class', 'two columns'); 97 | 98 | // Get Role in system 99 | $roles = RoleManager::fetch(); 100 | $options = array(); 101 | if(is_array($roles) && !empty($roles)) { 102 | foreach($roles as $role) { 103 | $options[] = array($role->get('id'), ($this->get('default_role') == $role->get('id')), $role->get('name')); 104 | } 105 | } 106 | 107 | $label = new XMlElement('label', __('Default Member Role')); 108 | $label->setAttribute('class', 'column'); 109 | $label->appendChild(Widget::Select( 110 | "fields[{$this->get('sortorder')}][default_role]", $options 111 | )); 112 | 113 | $group->appendChild($label); 114 | $wrapper->appendChild($group); 115 | 116 | $div = new XMLElement('div', null, array('class' => 'two columns')); 117 | $this->appendShowColumnCheckbox($div); 118 | $wrapper->appendChild($div); 119 | } 120 | 121 | public function checkFields(array &$errors, $checkForDuplicates = true) { 122 | Field::checkFields($errors, $checkForDuplicates); 123 | } 124 | 125 | public function commit(){ 126 | if(!Field::commit()) return false; 127 | 128 | $id = $this->get('id'); 129 | 130 | if($id === false) return false; 131 | 132 | fieldMemberRole::createSettingsTable(); 133 | 134 | $fields = array( 135 | 'field_id' => $id, 136 | 'default_role' => $this->get('default_role') 137 | ); 138 | 139 | return FieldManager::saveSettings($id, $fields); 140 | } 141 | 142 | /*------------------------------------------------------------------------- 143 | Publish: 144 | -------------------------------------------------------------------------*/ 145 | 146 | /** 147 | * If the Members installation has a Activation field used, we need to make sure 148 | * that this field represents accurately what Role this Member actually has. 149 | * The Activation field allows developers to set a Activation Role, which is the role 150 | * assigned to Members who have registered, but not yet activated their account. 151 | * This Activation role masks the Role field's value, so the Member assumes the 152 | * Role of the Activation role. 153 | * 154 | * @param integer $entry_id 155 | * The Entry ID of the Member 156 | * @param integer $role_id 157 | * A given Role ID 158 | * @return integer 159 | * The resulting Role ID, whether that is the Activation ID or the 160 | * given `$role_id`. 161 | */ 162 | public function getActivationRole($entry_id = null, $role_id = null) { 163 | if(is_null($entry_id)) return null; 164 | 165 | $activation_role_id = null; 166 | $activation = extension_Members::getField('activation', $this->get('parent_section')); 167 | if(!is_null($activation) && !is_null($entry_id)) { 168 | $entry = EntryManager::fetch($entry_id); 169 | $entry = $entry[0]; 170 | 171 | if($entry instanceof Entry && $entry->getData($activation->get('id'), true)->activated != 'yes') { 172 | $activation_role_id = $activation->get('activation_role_id'); 173 | } 174 | } 175 | 176 | if(!is_null($role_id) && is_null($activation_role_id)) { 177 | return $role_id; 178 | } 179 | else { 180 | return $activation_role_id; 181 | } 182 | } 183 | 184 | public function displayPublishPanel(XMLElement &$wrapper, $data = null, $error = null, $prefix = null, $postfix = null, $entry_id = null) { 185 | $states = $this->getToggleStates(); 186 | $options = array(); 187 | 188 | if(is_null($entry_id)) { 189 | $data['role_id'] = $this->get('default_role'); 190 | } 191 | 192 | $activation_role_id = $this->getActivationRole($entry_id); 193 | 194 | // Loop over states to build the Select options array 195 | foreach($states as $role_id => $role_name){ 196 | $options[] = array( 197 | $role_id, 198 | !is_null($activation_role_id) ? ($role_id == $activation_role_id) : ($role_id == $data['role_id']), 199 | $role_name 200 | ); 201 | } 202 | 203 | $label = Widget::Label($this->get('label')); 204 | $label->appendChild(Widget::Select( 205 | 'fields'.$prefix.'['.$this->get('element_name').']'.$postfix, 206 | $options, 207 | !is_null($activation_role_id) ? array('disabled' => 'disabled') : array()) 208 | ); 209 | 210 | // Add message about user's Role when they activate and a hidden field that 211 | // contains the Default Role ID. The Default Role ID can be two things, 212 | // either the Default Role as set on the Settings page (most of the time) or 213 | // if a value has been explicitly set. 214 | if(!is_null($activation_role_id)) { 215 | $default_role_id = !is_null($data['role_id']) ? $data['role_id'] : $this->get('default_role'); 216 | $default_role = RoleManager::fetch($default_role_id); 217 | if($default_role instanceof Role) { 218 | $label->appendChild( 219 | new XMLElement('span', 220 | __('Member will assume the role %s when activated.', array($default_role->get('name'))), 221 | array('class' => 'help frame')) 222 | ); 223 | $label->appendChild( 224 | Widget::Input('fields'.$prefix.'['.$this->get('element_name').']'.$postfix, $default_role_id, 'hidden') 225 | ); 226 | } 227 | } 228 | 229 | if(!is_null($error)) { 230 | $wrapper->appendChild(Widget::Error($label, $error)); 231 | } 232 | else { 233 | $wrapper->appendChild($label); 234 | } 235 | } 236 | 237 | public function processRawFieldData($data, &$status, &$message=null, $simulate=false, $entry_id=NULL){ 238 | $status = self::__OK__; 239 | 240 | if(is_null($data)) { 241 | return array( 242 | 'role_id' => $this->get('default_role') 243 | ); 244 | } 245 | else return array('role_id' => $data); 246 | } 247 | 248 | /*------------------------------------------------------------------------- 249 | Output: 250 | -------------------------------------------------------------------------*/ 251 | 252 | public function fetchIncludableElements() { 253 | return array( 254 | $this->get('element_name'), 255 | $this->get('element_name') . ': permissions' 256 | ); 257 | } 258 | 259 | public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null) { 260 | if(!is_array($data) || empty($data)) return; 261 | 262 | $role_id = $this->getActivationRole($entry_id, $data['role_id']); 263 | $role = RoleManager::fetch($role_id); 264 | 265 | if(!$role instanceof Role) return; 266 | 267 | $element = new XMLElement($this->get('element_name'), null, array( 268 | 'id' => $role->get('id'), 269 | 'assumed-id' => $data["role_id"], 270 | 'mode' => ($mode == "permissions") ? $mode : 'normal' 271 | )); 272 | $element->appendChild( 273 | new XMLElement('name', General::sanitize($role->get('name')), array( 274 | 'handle' => $role->get('handle') 275 | )) 276 | ); 277 | 278 | if($mode == "permissions") { 279 | // The more information that's provided here, the easier it will be for 280 | // a developer to write XSLT that is dynamic and can work when the Roles 281 | // are updated in the backend. For instance you could check if a page was 282 | // denied access before creating a link to it. This check would then work 283 | // for all roles, instead of writing logic for Role A, Role B. Consider it 284 | // feature detection, rather than user agent detection. 285 | 286 | $forbidden_pages = $role->get('forbidden_pages'); 287 | if(is_array($forbidden_pages) & !empty($forbidden_pages)) { 288 | $page_data = PageManager::fetch(false, array('*'), array( 289 | sprintf('id IN (%s)', implode(',', $forbidden_pages)) 290 | )); 291 | 292 | if(is_array($page_data) && !empty($page_data)) { 293 | $pages = new XMLElement('forbidden-pages'); 294 | 295 | foreach($page_data as $key => $page) { 296 | $attributes = array( 297 | 'id' => $page['id'], 298 | 'handle' => $page['handle'] 299 | ); 300 | 301 | if(!is_null($page['path'])) { 302 | $attributes['parent-path'] = General::sanitize($page['path']); 303 | } 304 | 305 | $pages->appendChild( 306 | new XMLElement('page', $page['title'], $attributes) 307 | ); 308 | } 309 | 310 | $element->appendChild($pages); 311 | } 312 | } 313 | 314 | $event_permissions = $role->get('event_permissions'); 315 | if(is_array($event_permissions) & !empty($event_permissions)) { 316 | $events = new XMLElement('events'); 317 | 318 | foreach($event_permissions as $event => $event_data) { 319 | $item = new XMLElement('event', null, array('handle' => $event)); 320 | foreach($event_data as $action => $level) { 321 | $item->appendChild( 322 | new XMLElement('action', EventPermissions::$permissionMap[$level], array( 323 | 'type' => $action, 324 | 'handle' => Lang::createHandle(EventPermissions::$permissionMap[$level]) 325 | )) 326 | ); 327 | } 328 | 329 | $events->appendChild($item); 330 | } 331 | 332 | $element->appendChild($events); 333 | } 334 | } 335 | 336 | $wrapper->appendChild($element); 337 | } 338 | 339 | public function prepareTableValue($data, XMLElement $link = null, $entry_id = null) { 340 | $role_id = $this->getActivationRole($entry_id, $data['role_id']); 341 | 342 | $role = RoleManager::fetch($role_id); 343 | 344 | return parent::prepareTableValue(array( 345 | 'value' => $role instanceof Role ? General::sanitize($role->get('name')) : null 346 | ), $link, $entry_id); 347 | } 348 | 349 | public function getParameterPoolValue(array $data, $entry_id = null) { 350 | $role_id = $this->getActivationRole($entry_id, $data['role_id']); 351 | 352 | return $role_id; 353 | } 354 | 355 | /*------------------------------------------------------------------------- 356 | Filtering: 357 | -------------------------------------------------------------------------*/ 358 | 359 | public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation = false){ 360 | 361 | $field_id = $this->get('id'); 362 | 363 | if($andOperation) { 364 | foreach($data as $key => $bit){ 365 | $bit = Symphony::Database()->cleanValue($bit); 366 | $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id$key` ON (`e`.`id` = `t$field_id$key`.entry_id) "; 367 | $joins .= " LEFT JOIN `tbl_members_roles` AS `tg$field_id$key` ON (`t$field_id$key`.`role_id` = `tg$field_id$key`.id) "; 368 | $where .= " AND (`t$field_id$key`.role_id = '$bit' OR (`tg$field_id$key`.name = '$bit' OR `tg$field_id$key`.handle = '$bit')) "; 369 | } 370 | } 371 | else { 372 | $data = !is_array($data) ? array($data) : $data; 373 | $data = array_map(array(Symphony::Database(), 'cleanValue'), $data); 374 | $value = implode("', '", $data); 375 | 376 | $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id` ON (`e`.`id` = `t$field_id`.entry_id) "; 377 | $joins .= " LEFT JOIN `tbl_members_roles` AS `tg$field_id` ON (`t$field_id`.`role_id` = `tg$field_id`.id) "; 378 | $where .= " AND ( 379 | `t$field_id`.role_id IN ('$value') 380 | OR ( 381 | `tg$field_id`.name IN ('$value') 382 | OR 383 | `tg$field_id`.handle IN ('$value') 384 | ) 385 | ) "; 386 | } 387 | 388 | return true; 389 | } 390 | 391 | /*------------------------------------------------------------------------- 392 | Sorting: 393 | -------------------------------------------------------------------------*/ 394 | 395 | public function buildSortingSQL(&$joins, &$where, &$sort, $order='ASC') { 396 | if(in_array(strtolower($order), array('random', 'rand'))) { 397 | $sort = 'ORDER BY RAND()'; 398 | } 399 | else { 400 | $sort = sprintf( 401 | 'ORDER BY ( 402 | SELECT %s 403 | FROM tbl_entries_data_%d AS `ed` 404 | WHERE entry_id = e.id 405 | ) %s', 406 | '`ed`.role_id', 407 | $this->get('id'), 408 | $order 409 | ); 410 | } 411 | } 412 | 413 | /*------------------------------------------------------------------------- 414 | Grouping: 415 | -------------------------------------------------------------------------*/ 416 | 417 | public function groupRecords($records){ 418 | 419 | if(!is_array($records) || empty($records)) return; 420 | 421 | $groups = array($this->get('element_name') => array()); 422 | 423 | foreach($records as $r){ 424 | $data = $r->getData($this->get('id')); 425 | 426 | $role_id = $this->getActivationRole($entry_id, $data['role_id']); 427 | if(!$role = RoleManager::fetch($role_id)) continue; 428 | 429 | if(!isset($groups[$this->get('element_name')][$role_id])){ 430 | $groups[$this->get('element_name')][$role_id] = array( 431 | 'attr' => array( 432 | 'id' => $role_id, 433 | 'handle' => $role->get('handle'), 434 | 'name' => General::sanitize($role->get('name')) 435 | ), 436 | 'records' => array(), 'groups' => array() 437 | ); 438 | } 439 | 440 | $groups[$this->get('element_name')][$role_id]['records'][] = $r; 441 | 442 | } 443 | 444 | return $groups; 445 | } 446 | 447 | /*------------------------------------------------------------------------- 448 | Events: 449 | -------------------------------------------------------------------------*/ 450 | 451 | public function getExampleFormMarkup(){ 452 | $states = $this->getToggleStates(); 453 | foreach($states as $role_id => $role_name){ 454 | $options[] = array( 455 | $role_id, 456 | false, 457 | $role_name 458 | ); 459 | } 460 | 461 | $label = Widget::Label($this->get('label')); 462 | $label->appendChild(Widget::Select( 463 | 'fields'.$fieldnamePrefix.'['.$this->get('element_name').']'.$fieldnamePostfix, $options) 464 | ); 465 | 466 | return $label; 467 | } 468 | } 469 | 470 | -------------------------------------------------------------------------------- /fields/field.memberactivation.php: -------------------------------------------------------------------------------- 1 | _name = __('Member: Activation'); 21 | $this->_showassociation = false; 22 | } 23 | 24 | public function canToggle(){ 25 | return true; 26 | } 27 | 28 | public function canFilter(){ 29 | return true; 30 | } 31 | 32 | public function mustBeUnique(){ 33 | return true; 34 | } 35 | 36 | public function canPrePopulate(){ 37 | return false; 38 | } 39 | 40 | /*------------------------------------------------------------------------- 41 | Setup: 42 | -------------------------------------------------------------------------*/ 43 | 44 | public static function createSettingsTable() { 45 | return Symphony::Database()->query(" 46 | CREATE TABLE IF NOT EXISTS `tbl_fields_memberactivation` ( 47 | `id` int(11) unsigned NOT NULL auto_increment, 48 | `field_id` int(11) unsigned NOT NULL, 49 | `code_expiry` varchar(50) NOT NULL, 50 | `activation_role_id` int(11) unsigned NOT NULL, 51 | `deny_login` enum('yes','no') NOT NULL default 'yes', 52 | PRIMARY KEY (`id`), 53 | UNIQUE KEY `field_id` (`field_id`) 54 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 55 | "); 56 | } 57 | 58 | public function createTable(){ 59 | return Symphony::Database()->query( 60 | "CREATE TABLE IF NOT EXISTS `tbl_entries_data_" . $this->get('id') . "` ( 61 | `id` int(11) unsigned NOT NULL auto_increment, 62 | `entry_id` int(11) unsigned NOT NULL, 63 | `activated` enum('yes','no') NOT NULL default 'no', 64 | `timestamp` DATETIME default NULL, 65 | `code` varchar(40) default NULL, 66 | PRIMARY KEY (`id`), 67 | KEY `entry_id` (`entry_id`), 68 | UNIQUE KEY `code` (`code`) 69 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 70 | "); 71 | } 72 | 73 | /*------------------------------------------------------------------------- 74 | Utilities: 75 | -------------------------------------------------------------------------*/ 76 | 77 | /** 78 | * Given a `$entry_id`, check to see whether this Entry has a valid 79 | * code, if it doesn't, generate one and return an array for insertion 80 | * into the entry table. 81 | * 82 | * @param integer $entry_id 83 | * @return array 84 | */ 85 | public function generateCode($entry_id = null){ 86 | $code = false; 87 | 88 | if(!is_null($entry_id)) { 89 | $code = $this->isCodeActive($entry_id); 90 | if($code !== false) return $code; 91 | } 92 | 93 | // Generate a code 94 | do { 95 | $code = General::hash(uniqid(), 'sha1'); 96 | $row = Symphony::Database()->fetchRow(0, " 97 | SELECT 1 FROM `tbl_entries_data_{$this->get('id')}` WHERE `code` = '{$code}' 98 | "); 99 | } while(is_array($row) && !empty($row)); 100 | 101 | $data = array( 102 | 'code' => $code, 103 | 'timestamp' => DateTimeObj::get('Y-m-d H:i:s', time()) 104 | ); 105 | 106 | return $data; 107 | } 108 | 109 | /** 110 | * Given an `$entry_id`, this function will check to see if the 111 | * code generated is still valid by comparing it's generation timestamp 112 | * with the maximum code expiry time. 113 | * 114 | * @param integer $entry_id 115 | * @return array 116 | */ 117 | public function isCodeActive($entry_id) { 118 | 119 | // First check if a code already exists 120 | $code = Symphony::Database()->fetchRow(0, sprintf(" 121 | SELECT `code`, `timestamp` FROM `tbl_entries_data_%d` 122 | WHERE `entry_id` = %d 123 | AND DATE_FORMAT(timestamp, '%%Y-%%m-%%d %%H:%%i:%%s') < '%s' 124 | LIMIT 1", 125 | $this->get('id'), 126 | $entry_id, 127 | DateTimeObj::get('Y-m-d H:i:s', strtotime('now + ' . $this->get('code_expiry'))) 128 | )); 129 | 130 | if(is_array($code) && !empty($code) && !is_null($code['code'])) { 131 | return $code; 132 | } 133 | else { 134 | return false; 135 | } 136 | } 137 | 138 | /** 139 | * This function will remove all the codes from the database that are 140 | * invalid. An optional `$entry_id` parameter allows the code to just 141 | * be removed on a per member basis, or whether it should be a global 142 | * purge. 143 | * 144 | * @param integer $entry_id 145 | * @return boolean 146 | */ 147 | public function purgeCodes($entry_id = null){ 148 | $entry_id = Symphony::Database()->cleanValue($entry_id); 149 | 150 | return Symphony::Database()->update( 151 | array( 152 | 'code' => null 153 | ), 154 | "`tbl_entries_data_{$this->get('id')}`", 155 | sprintf("`activated` = 'no' AND DATE_FORMAT(timestamp, '%%Y-%%m-%%d %%H:%%i:%%s') < '%s' %s", 156 | DateTimeObj::get('Y-m-d H:i:s', strtotime('now - ' . $this->get('code_expiry'))), 157 | ($entry_id ? " OR `entry_id` = $entry_id" : '') 158 | ) 159 | ); 160 | } 161 | 162 | public static function findCodeExpiry() { 163 | return extension_Members::findCodeExpiry('tbl_fields_memberactivation'); 164 | } 165 | 166 | public function getToggleStates() { 167 | return array('yes' => __('Yes'), 'no' => __('No')); 168 | } 169 | 170 | public function toggleFieldData(array $data, $newState, $entry_id = NULL){ 171 | $data['activated'] = $newState; 172 | 173 | if($data['activated'] == 'no') { 174 | $data = array_merge($data, $this->generateCode($entry_id)); 175 | } 176 | else { 177 | $data['timestamp'] = DateTimeObj::get('Y-m-d H:i:s', time()); 178 | } 179 | 180 | return $data; 181 | } 182 | 183 | /*------------------------------------------------------------------------- 184 | Settings: 185 | -------------------------------------------------------------------------*/ 186 | 187 | public function setFromPOST(array $settings = array()) { 188 | $settings['deny_login'] = (isset($settings['deny_login']) && $settings['deny_login'] == 'yes' ? 'yes' : 'no'); 189 | 190 | parent::setFromPOST($settings); 191 | } 192 | 193 | public function displaySettingsPanel(XMLElement &$wrapper, $errors = NULL){ 194 | Field::displaySettingsPanel($wrapper, $errors); 195 | 196 | $group = new XMLElement('div'); 197 | $group->setAttribute('class', 'two columns'); 198 | 199 | // Add Activiation Code Expiry 200 | $div = new XMLElement('div'); 201 | $div->setAttribute('class', 'column'); 202 | 203 | $label = Widget::Label(__('Activation Code Expiry')); 204 | $label->appendChild( 205 | new XMLElement('i', __('How long a member\'s activation code will be valid for before it expires')) 206 | ); 207 | $label->appendChild(Widget::Input( 208 | "fields[{$this->get('sortorder')}][code_expiry]", $this->get('code_expiry') 209 | )); 210 | 211 | $ul = new XMLElement('ul', null, array('class' => 'tags singular', 'data-interactive' => 'data-interactive')); 212 | $tags = fieldMemberActivation::findCodeExpiry(); 213 | foreach($tags as $name => $time) { 214 | $ul->appendChild(new XMLElement('li', $name, array('class' => $time))); 215 | } 216 | 217 | $div->appendChild($label); 218 | $div->appendChild($ul); 219 | 220 | if (isset($errors['code_expiry'])) { 221 | $div = Widget::Error($div, $errors['code_expiry']); 222 | } 223 | 224 | // Get Roles in system 225 | $roles = RoleManager::fetch(); 226 | $options = array(); 227 | if(is_array($roles) && !empty($roles)) { 228 | foreach($roles as $role) { 229 | $options[] = array($role->get('id'), ($this->get('activation_role_id') == $role->get('id')), $role->get('name')); 230 | } 231 | } 232 | 233 | $label = new XMlElement('label', __('Role for Members who are awaiting activation')); 234 | $label->setAttribute('class', 'column'); 235 | $label->appendChild(Widget::Select( 236 | "fields[{$this->get('sortorder')}][activation_role_id]", $options 237 | )); 238 | 239 | $group->appendChild($label); 240 | 241 | // Add Group 242 | $group->appendChild($div); 243 | $wrapper->appendChild($group); 244 | 245 | $div = new XMLElement('div', null, array('class' => 'two columns')); 246 | 247 | // Add Deny Login 248 | $div->appendChild(Widget::Input("fields[{$this->get('sortorder')}][deny_login]", 'no', 'hidden')); 249 | 250 | $label = Widget::Label(); 251 | $label->setAttribute('class', 'column'); 252 | $input = Widget::Input("fields[{$this->get('sortorder')}][deny_login]", 'yes', 'checkbox'); 253 | 254 | if ($this->get('deny_login') == 'yes') $input->setAttribute('checked', 'checked'); 255 | 256 | $label->setValue(__('%s Prevent unactivated members from logging in', array($input->generate()))); 257 | 258 | $div->appendChild($label); 259 | 260 | // Add Show Column 261 | $this->appendShowColumnCheckbox($div); 262 | 263 | $wrapper->appendChild($div); 264 | } 265 | 266 | public function checkFields(array &$errors, $checkForDuplicates = true) { 267 | Field::checkFields($errors, $checkForDuplicates); 268 | 269 | if (trim($this->get('code_expiry')) == '') { 270 | $errors['code_expiry'] = __('This is a required field.'); 271 | } 272 | 273 | if(!DateTimeObj::validate($this->get('code_expiry'))) { 274 | $errors['code_expiry'] = __('Code expiry must be a unit of time, such as 1 day or 2 hours'); 275 | } 276 | } 277 | 278 | public function commit(){ 279 | if(!Field::commit()) return false; 280 | 281 | $id = $this->get('id'); 282 | 283 | if($id === false) return false; 284 | 285 | fieldMemberActivation::createSettingsTable(); 286 | 287 | $fields = array( 288 | 'field_id' => $id, 289 | 'code_expiry' => $this->get('code_expiry'), 290 | 'activation_role_id' => $this->get('activation_role_id'), 291 | 'deny_login' => $this->get('deny_login') == 'yes' ? 'yes' : 'no' 292 | ); 293 | 294 | return FieldManager::saveSettings($id, $fields); 295 | } 296 | 297 | /*------------------------------------------------------------------------- 298 | Publish: 299 | -------------------------------------------------------------------------*/ 300 | 301 | public function displayPublishPanel(XMLElement &$wrapper, $data = null, $error = null, $prefix = null, $postfix = null, $entry_id = null) { 302 | $isActivated = ($data['activated'] == 'yes'); 303 | 304 | // If $entry_id is null, just preset to Activated Account as it means an authorised user 305 | // has enough access to create the record in the backend anyway. 306 | $options = array( 307 | array('no', ($data['activated'] == 'no'), __('Not Activated')), 308 | array('yes', ($isActivated || is_null($entry_id) || is_null($data)), __('Activated')) 309 | ); 310 | 311 | $label = Widget::Label($this->get('label')); 312 | if(!$isActivated) { 313 | $label->appendChild(Widget::Select( 314 | 'fields'.$prefix.'['.$this->get('element_name').']'.$postfix, $options 315 | )); 316 | } 317 | else { 318 | $label->appendChild(Widget::Input( 319 | 'fields'.$prefix.'['.$this->get('element_name').']'.$postfix, 'yes', 'hidden' 320 | )); 321 | } 322 | 323 | // Member not activated 324 | if(!$isActivated && !is_null($data)) { 325 | // If code is still live, displays when the code was generated. 326 | if($this->isCodeActive($entry_id) !== false) { 327 | $label->appendChild( 328 | new XMLElement('span', __('Activation code %s', array('' . $data['code'] . '')), array('class' => 'frame')) 329 | ); 330 | } 331 | // If the code is expired, displays 'Expired' w/the expiration timestamp. 332 | else { 333 | $label->appendChild( 334 | new XMLElement('i', __('Activation code expired %s', array( 335 | DateTimeObj::get(__SYM_DATETIME_FORMAT__, strtotime($data['timestamp'])) 336 | ))) 337 | ); 338 | } 339 | } 340 | else { 341 | if(is_null($data)) { 342 | // If member is activated, help text shows datetime activated 343 | $label->appendChild( 344 | new XMLElement('i', __('Account will be activated when entry is saved')) 345 | ); 346 | } 347 | else { 348 | // If member is activated, help text shows datetime activated 349 | $label->appendChild( 350 | new XMLElement('i', __('Activated %s', array( 351 | DateTimeObj::get(__SYM_DATETIME_FORMAT__, strtotime($data['timestamp'])) 352 | ))) 353 | ); 354 | } 355 | } 356 | 357 | if(!is_null($error)) { 358 | $wrapper->appendChild(Widget::Error($label, $error)); 359 | } 360 | else { 361 | $wrapper->appendChild($label); 362 | } 363 | } 364 | 365 | public function processRawFieldData($data, &$status, &$message=null, $simulate=false, $entry_id=NULL){ 366 | $status = self::__OK__; 367 | 368 | return $this->prepareImportValue($data, ImportableField::ARRAY_VALUE, $entry_id); 369 | } 370 | 371 | /*------------------------------------------------------------------------- 372 | Import: 373 | -------------------------------------------------------------------------*/ 374 | 375 | public function getImportModes() { 376 | return array( 377 | 'getPostdata' => ImportableField::ARRAY_VALUE 378 | ); 379 | } 380 | 381 | public function prepareImportValue($data, $mode, $entry_id = null) { 382 | $modes = (object)$this->getImportModes(); 383 | 384 | if($mode === $modes->getPostdata) { 385 | if(is_null($data) && !is_null($entry_id)) { 386 | $entry = EntryManager::fetch($entry_id); 387 | 388 | $data = $entry[0]->getData($this->get('id')); 389 | if(empty($data)) { 390 | $data = $this->generateCode($entry_id); 391 | $data['activated'] = 'no'; 392 | } 393 | } 394 | else { 395 | if(!is_array($data)) { 396 | $data = array('activated' => $data); 397 | } 398 | 399 | if($data['activated'] == 'no') { 400 | $data = array_merge($data, $this->generateCode($entry_id)); 401 | } 402 | else { 403 | $data['timestamp'] = DateTimeObj::get('Y-m-d H:i:s', time()); 404 | } 405 | } 406 | } 407 | 408 | return $data; 409 | } 410 | 411 | /*------------------------------------------------------------------------- 412 | Output: 413 | -------------------------------------------------------------------------*/ 414 | 415 | public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null){ 416 | if (!is_array($data) or !isset($data['activated'])) return; 417 | 418 | $el = new XMLElement($this->get('element_name')); 419 | $el->setAttribute('activated', $data['activated']); 420 | 421 | if($data['activated'] == 'yes') { 422 | // Append the time the person was activated 423 | $el->appendChild( 424 | General::createXMLDateObject(strtotime($data['timestamp']), 'date') 425 | ); 426 | } 427 | else { 428 | // Append the code 429 | $el->appendChild( 430 | new XMLElement('code', $data['code']) 431 | ); 432 | 433 | // Add expiry timestamp, including how long the code is valid for 434 | $expiry = General::createXMLDateObject(strtotime($data['timestamp'] . ' + ' . $this->get('code_expiry')), 'expires'); 435 | $expiry->setAttribute('expiry', $this->get('code_expiry')); 436 | $el->appendChild($expiry); 437 | } 438 | 439 | $wrapper->appendChild($el); 440 | } 441 | 442 | public function prepareTableValue($data, XMLElement $link=NULL, $entry_id = null) { 443 | return parent::prepareTableValue(array( 444 | 'value' => ($data['activated'] == 'yes') ? __('Activated') : __('Not Activated') 445 | ), $link, $entry_id); 446 | } 447 | 448 | public function getParameterPoolValue(array $data, $entry_id = NULL) { 449 | return $data['activated']; 450 | } 451 | 452 | /*------------------------------------------------------------------------- 453 | Sorting: 454 | -------------------------------------------------------------------------*/ 455 | 456 | public function buildSortingSQL(&$joins, &$where, &$sort, $order='ASC') { 457 | if(in_array(strtolower($order), array('random', 'rand'))) { 458 | $sort = 'ORDER BY RAND()'; 459 | } 460 | else { 461 | $sort = sprintf( 462 | 'ORDER BY ( 463 | SELECT %s 464 | FROM tbl_entries_data_%d AS `ed` 465 | WHERE entry_id = e.id 466 | ) %s', 467 | '`ed`.activated', 468 | $this->get('id'), 469 | $order 470 | ); 471 | } 472 | } 473 | 474 | /*------------------------------------------------------------------------- 475 | Filtering: 476 | -------------------------------------------------------------------------*/ 477 | 478 | public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation=false){ 479 | 480 | $field_id = $this->get('id'); 481 | 482 | // Filter has + in it. 483 | if($andOperation) { 484 | foreach($data as $key => $bit){ 485 | $bit = Symphony::Database()->cleanValue($bit); 486 | $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id$key` ON (`e`.`id` = `t$field_id$key`.entry_id) "; 487 | $where .= " AND `t$field_id$key`.activated = '$bit' "; 488 | } 489 | } 490 | 491 | // Normal 492 | else { 493 | if(!is_array($data)) { 494 | $data = array($data); 495 | } 496 | $data = array_map(array(Symphony::Database(), 'cleanValue'), $data); 497 | $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id` ON (`e`.`id` = `t$field_id`.entry_id) "; 498 | $where .= " AND `t$field_id`.activated IN ('".implode("', '", $data)."') "; 499 | } 500 | 501 | return true; 502 | } 503 | 504 | 505 | /*------------------------------------------------------------------------- 506 | Events: 507 | -------------------------------------------------------------------------*/ 508 | 509 | public function getExampleFormMarkup(){ 510 | 511 | $label = Widget::Label($this->get('label')); 512 | $label->appendChild(Widget::Input('fields['.$this->get('element_name').']')); 513 | 514 | return $label; 515 | } 516 | 517 | } 518 | --------------------------------------------------------------------------------