Webylon 3.1 API Docs
  • Package
  • Class
  • Tree
  • Deprecated
  • Download
Version: current
  • 3.2
  • 3.1

Packages

  • auth
  • Booking
  • cart
    • shipping
    • steppedcheckout
  • Catalog
  • cms
    • assets
    • batchaction
    • batchactions
    • bulkloading
    • comments
    • content
    • core
    • export
    • newsletter
    • publishers
    • reports
    • security
    • tasks
  • Dashboard
  • DataObjectManager
  • event
  • faq
  • forms
    • actions
    • core
    • fields-basic
    • fields-dataless
    • fields-datetime
    • fields-files
    • fields-formatted
    • fields-formattedinput
    • fields-relational
    • fields-structural
    • transformations
    • validators
  • googlesitemaps
  • guestbook
  • installer
  • newsletter
  • None
  • photo
    • gallery
  • PHP
  • polls
  • recaptcha
  • sapphire
    • api
    • bulkloading
    • control
    • core
    • cron
    • dev
    • email
    • fields-formattedinput
    • filesystem
    • formatters
    • forms
    • i18n
    • integration
    • misc
    • model
    • parsers
    • search
    • security
    • tasks
    • testing
    • tools
    • validation
    • view
    • widgets
  • seo
    • open
      • graph
  • sfDateTimePlugin
  • spamprotection
  • stealth
    • captha
  • subsites
  • userform
    • pagetypes
  • userforms
  • webylon
  • widgets

Classes

  • Authenticator
  • BasicAuth
  • ChangePasswordForm
  • Group
  • GroupCsvBulkLoader
  • LoginAttempt
  • LoginForm
  • Member
  • Member_ChangePasswordEmail
  • Member_ForgotPasswordEmail
  • Member_GroupSet
  • Member_ProfileForm
  • Member_SignupEmail
  • Member_Validator
  • MemberAuthenticator
  • MemberCsvBulkLoader
  • MemberLoginForm
  • MemberPassword
  • NZGovtPasswordValidator
  • PasswordEncryptor
  • PasswordEncryptor_LegacyPHPHash
  • PasswordEncryptor_MySQLOldPassword
  • PasswordEncryptor_MySQLPassword
  • PasswordEncryptor_None
  • PasswordEncryptor_PHPHash
  • PasswordValidator
  • Permission
  • Permission_Group
  • PermissionCheckboxSetField
  • PermissionCheckboxSetField_Readonly
  • PermissionRole
  • PermissionRoleCode
  • Security

Interfaces

  • PermissionProvider

Exceptions

  • PasswordEncryptor_NotFoundException
   1 <?php
   2 /**
   3  * The member class which represents the users of the system
   4  * @package sapphire
   5  * @subpackage security
   6  */
   7 class Member extends DataObject {
   8 
   9     static $db = array(
  10         'FirstName' => 'Varchar',
  11         'Surname' => 'Varchar',
  12         'Email' => 'Varchar',
  13         'Password' => 'Varchar(160)',
  14         'RememberLoginToken' => 'Varchar(50)',
  15         'NumVisit' => 'Int',
  16         'LastVisited' => 'SS_Datetime',
  17         'Bounced' => 'Boolean', // Note: This does not seem to be used anywhere.
  18         'AutoLoginHash' => 'Varchar(30)',
  19         'AutoLoginExpired' => 'SS_Datetime',
  20         // This is an arbitrary code pointing to a PasswordEncryptor instance,
  21         // not an actual encryption algorithm.
  22         // Warning: Never change this field after its the first password hashing without
  23         // providing a new cleartext password as well.
  24         'PasswordEncryption' => "Varchar(50)",
  25         'Salt' => 'Varchar(50)',
  26         'PasswordExpiry' => 'Date',
  27         'LockedOutUntil' => 'SS_Datetime',
  28         'Locale' => 'Varchar(6)',
  29         // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
  30         'FailedLoginCount' => 'Int', 
  31     );
  32 
  33     static $belongs_many_many = array(
  34         'Groups' => 'Group',
  35     );
  36 
  37     static $has_one = array();
  38     
  39     static $has_many = array();
  40     
  41     static $many_many = array();
  42     
  43     static $many_many_extraFields = array();
  44 
  45     static $default_sort = '"Surname", "FirstName"';
  46 
  47     static $indexes = array(
  48         'Email' => true,
  49         //'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true) //Removed due to duplicate null values causing MSSQL problems
  50     );
  51 
  52     static $notify_password_change = false;
  53     
  54     /**
  55      * All searchable database columns
  56      * in this object, currently queried
  57      * with a "column LIKE '%keywords%'
  58      * statement.
  59      *
  60      * @var array
  61      * @todo Generic implementation of $searchable_fields on DataObject,
  62      * with definition for different searching algorithms
  63      * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
  64      */
  65     static $searchable_fields = array(
  66         'FirstName',
  67         'Surname',
  68         'Email',
  69     );
  70     
  71     static $summary_fields = array(
  72         'FirstName',
  73         'Surname',
  74         'Email',
  75     );
  76     
  77     /**
  78      * @var Array See {@link set_title_columns()}
  79      */
  80     static $title_format = null;
  81     
  82     /**
  83      * The unique field used to identify this member.
  84      * By default, it's "Email", but another common
  85      * field could be Username.
  86      * 
  87      * @var string
  88      */
  89     protected static $unique_identifier_field = 'Email';
  90     
  91     /**
  92      * {@link PasswordValidator} object for validating user's password
  93      */
  94     protected static $password_validator = null;
  95     
  96     /**
  97      * The number of days that a password should be valid for.
  98      * By default, this is null, which means that passwords never expire
  99      */
 100     protected static $password_expiry_days = null;
 101 
 102     protected static $lock_out_after_incorrect_logins = null;
 103     
 104     /**
 105      * If this is set, then a session cookie with the given name will be set on log-in,
 106      * and cleared on logout.
 107      */
 108     protected static $login_marker_cookie = null;
 109 
 110 
 111     /**
 112      * Ensure the locale is set to something sensible by default.
 113      */
 114     public function populateDefaults() {
 115         parent::populateDefaults();
 116         $this->Locale = i18n::get_locale();
 117     }
 118     
 119     function requireDefaultRecords() {
 120         // Default groups should've been built by Group->requireDefaultRecords() already
 121         
 122         // Find or create ADMIN group
 123         $adminGroups = Permission::get_groups_by_permission('ADMIN');
 124         if(!$adminGroups) {
 125             singleton('Group')->requireDefaultRecords();
 126             $adminGroups = Permission::get_groups_by_permission('ADMIN');
 127         }
 128         $adminGroup = $adminGroups->First();
 129         
 130         // Add a default administrator to the first ADMIN group found (most likely the default
 131         // group created through Group->requireDefaultRecords()).
 132         $admins = Permission::get_members_by_permission('ADMIN');
 133         if(!$admins) {
 134             // Leave 'Email' and 'Password' are not set to avoid creating
 135             // persistent logins in the database. See Security::setDefaultAdmin().
 136             $admin = Object::create('Member');
 137             $admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
 138             $admin->write();
 139             $admin->Groups()->add($adminGroup);
 140         }       
 141     }
 142 
 143     /**
 144      * If this is called, then a session cookie will be set to "1" whenever a user
 145      * logs in.  This lets 3rd party tools, such as apache's mod_rewrite, detect
 146      * whether a user is logged in or not and alter behaviour accordingly.
 147      * 
 148      * One known use of this is to bypass static caching for logged in users.  This is
 149      * done by putting this into _config.php
 150      * <pre>
 151      * Member::set_login_marker_cookie("SS_LOGGED_IN");
 152      * </pre>
 153      * 
 154      * And then adding this condition to each of the rewrite rules that make use of
 155      * the static cache.
 156      * <pre>
 157      * RewriteCond %{HTTP_COOKIE} !SS_LOGGED_IN=1
 158      * </pre>
 159      * 
 160      * @param $cookieName string The name of the cookie to set.
 161      */
 162     static function set_login_marker_cookie($cookieName) {
 163         self::$login_marker_cookie = $cookieName;
 164     } 
 165 
 166     /**
 167      * Check if the passed password matches the stored one (if the member is not locked out).
 168      *
 169      * @param  string $password
 170      * @return ValidationResult
 171      */
 172     public function checkPassword($password) {
 173         $result = $this->canLogIn();
 174         // <inxo>
 175         if(!$result->valid()){
 176             return $result;
 177         }
 178         // </inxo>
 179         $spec = Security::encrypt_password(
 180             $password, 
 181             $this->Salt, 
 182             $this->PasswordEncryption,
 183             $this
 184         );
 185         $e = $spec['encryptor'];
 186 
 187         if(!$e->compare($this->Password, $spec['password'])) {
 188             $result->error(_t (
 189                 'Member.ERRORWRONGCRED',
 190                 'That doesn\'t seem to be the right e-mail address or password. Please try again.'
 191             ));
 192         }
 193 
 194         return $result;
 195     }
 196 
 197     /**
 198      * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
 199      * one with error messages to display if the member is locked out.
 200      *
 201      * You can hook into this with a "canLogIn" method on an attached extension.
 202      *
 203      * @return ValidationResult
 204      */
 205     public function canLogIn() {
 206         $result = new ValidationResult();
 207 
 208         if($this->isLockedOut()) {
 209             $result->error(_t (
 210                 'Member.ERRORLOCKEDOUT',
 211                 'Your account has been temporarily disabled because of too many failed attempts at ' .
 212                 'logging in. Please try again in 20 minutes.'
 213             ));
 214         }
 215 
 216         $this->extend('canLogIn', $result);
 217         return $result;
 218     }
 219 
 220     /**
 221      * Returns true if this user is locked out
 222      */
 223     public function isLockedOut() {
 224         return $this->LockedOutUntil && time() < strtotime($this->LockedOutUntil);
 225     }
 226 
 227     /**
 228      * Regenerate the session_id.
 229      * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.  
 230      * They have caused problems in certain
 231      * quirky problems (such as using the Windmill 0.3.6 proxy).
 232      */
 233     static function session_regenerate_id() {
 234         // This can be called via CLI during testing.
 235         if(Director::is_cli()) return;
 236         
 237         $file = '';
 238         $line = '';
 239         
 240         // @ is to supress win32 warnings/notices when session wasn't cleaned up properly
 241         // There's nothing we can do about this, because it's an operating system function!
 242         if(!headers_sent($file, $line)) @session_regenerate_id(true);
 243     }
 244     
 245     /**
 246      * Get the field used for uniquely identifying a member
 247      * in the database. {@see Member::$unique_identifier_field}
 248      * 
 249      * @return string
 250      */
 251     static function get_unique_identifier_field() {
 252         return self::$unique_identifier_field;
 253     }
 254     
 255     /**
 256      * Set the field used for uniquely identifying a member
 257      * in the database. {@see Member::$unique_identifier_field}
 258      * 
 259      * @param $field The field name to set as the unique field
 260      */
 261     static function set_unique_identifier_field($field) {
 262         self::$unique_identifier_field = $field;
 263     }
 264     
 265     /**
 266      * Set a {@link PasswordValidator} object to use to validate member's passwords.
 267      */
 268     static function set_password_validator($pv) {
 269         self::$password_validator = $pv;
 270     }
 271     
 272     /**
 273      * Returns the current {@link PasswordValidator}
 274      */
 275     static function password_validator() {
 276         return self::$password_validator;
 277     }
 278 
 279     /**
 280      * Set the number of days that a password should be valid for.
 281      * Set to null (the default) to have passwords never expire.
 282      */
 283     static function set_password_expiry($days) {
 284         self::$password_expiry_days = $days;
 285     }
 286     
 287     /**
 288      * Configure the security system to lock users out after this many incorrect logins
 289      */
 290     static function lock_out_after_incorrect_logins($numLogins) {
 291         self::$lock_out_after_incorrect_logins = $numLogins;
 292     }
 293     
 294     
 295     function isPasswordExpired() {
 296         if(!$this->PasswordExpiry) return false;
 297         return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
 298     }
 299 
 300     /**
 301      * Logs this member in
 302      *
 303      * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
 304      */
 305     function logIn($remember = false) {
 306         self::session_regenerate_id();
 307 
 308         Session::set("loggedInAs", $this->ID);
 309         // This lets apache rules detect whether the user has logged in
 310         if(self::$login_marker_cookie) Cookie::set(self::$login_marker_cookie, 1, 0);
 311 
 312         $this->NumVisit++;
 313 
 314         if($remember) {
 315             $token = substr(md5(uniqid(rand(), true)), 0, 49 - strlen($this->ID));
 316             $this->RememberLoginToken = $token;
 317             Cookie::set('alc_enc', $this->ID . ':' . $token, 90, null, null, null, true);
 318         } else {
 319             $this->RememberLoginToken = null;
 320             Cookie::set('alc_enc', null);
 321             Cookie::forceExpiry('alc_enc');
 322         }
 323         
 324         // Clear the incorrect log-in count
 325         if(self::$lock_out_after_incorrect_logins) {
 326             $this->FailedLoginCount = 0;
 327         }
 328         
 329         // Don't set column if its not built yet (the login might be precursor to a /dev/build...)
 330         if(array_key_exists('LockedOutUntil', DB::fieldList('Member'))) {
 331             $this->LockedOutUntil = null;
 332         }
 333 
 334         $this->write();
 335         
 336         // Audit logging hook
 337         $this->extend('memberLoggedIn');
 338     }
 339 
 340     /**
 341      * Check if the member ID logged in session actually
 342      * has a database record of the same ID. If there is
 343      * no logged in user, FALSE is returned anyway.
 344      * 
 345      * @return boolean TRUE record found FALSE no record found
 346      */
 347     static function logged_in_session_exists() {
 348         if($id = Member::currentUserID()) {
 349             if($member = DataObject::get_by_id('Member', $id)) {
 350                 if($member->exists()) return true;
 351             }
 352         }
 353         
 354         return false;
 355     }
 356     
 357     /**
 358      * Log the user in if the "remember login" cookie is set
 359      *
 360      * The <i>remember login token</i> will be changed on every successful
 361      * auto-login.
 362      */
 363     static function autoLogin() {
 364         // Don't bother trying this multiple times
 365         self::$_already_tried_to_auto_log_in = true;
 366         
 367         if(strpos(Cookie::get('alc_enc'), ':') && !Session::get("loggedInAs")) {
 368             list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
 369             $SQL_uid = Convert::raw2sql($uid);
 370 
 371             $member = DataObject::get_one("Member", "\"Member\".\"ID\" = '$SQL_uid'");
 372 
 373             // check if autologin token matches
 374             if($member && (!$member->RememberLoginToken || $member->RememberLoginToken != $token)) {
 375                 $member = null;
 376             }
 377 
 378             if($member) {
 379                 self::session_regenerate_id();
 380                 Session::set("loggedInAs", $member->ID);
 381                 // This lets apache rules detect whether the user has logged in
 382                 if(self::$login_marker_cookie) Cookie::set(self::$login_marker_cookie, 1, 0, null, null, false, true);
 383 
 384                 $token = substr(md5(uniqid(rand(), true)), 0, 49 - strlen($member->ID));
 385                 $member->RememberLoginToken = $token;
 386                 Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
 387 
 388                 $member->NumVisit++;
 389                 $member->write();
 390                 
 391                 // Audit logging hook
 392                 $member->extend('memberAutoLoggedIn');
 393             }
 394         }
 395     }
 396 
 397     /**
 398      * Logs this member out.
 399      */
 400     function logOut() {
 401         Session::clear("loggedInAs");
 402         if(self::$login_marker_cookie) Cookie::set(self::$login_marker_cookie, null, 0);
 403         self::session_regenerate_id();
 404 
 405         $this->extend('memberBeforeLoggedOut');
 406 
 407         $this->RememberLoginToken = null;
 408         Cookie::set('alc_enc', null);
 409         Cookie::forceExpiry('alc_enc');
 410 
 411         $this->write();
 412         
 413         // Audit logging hook
 414         $this->extend('memberLoggedOut');
 415     }
 416 
 417 
 418     /**
 419      * Generate an auto login hash
 420      *
 421      * This creates an auto login hash that can be used to reset the password.
 422      *
 423      * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
 424      *
 425      * @todo Make it possible to handle database errors such as a "duplicate key" error
 426      */
 427     function generateAutologinHash($lifetime = 2) {
 428 
 429         do {
 430             $hash = substr(base_convert(md5(uniqid(mt_rand(), true)), 16, 36),
 431                                          0, 30);
 432         } while(DataObject::get_one('Member', "\"AutoLoginHash\" = '$hash'"));
 433 
 434         $this->AutoLoginHash = $hash;
 435         $this->AutoLoginExpired = date('Y-m-d', time() + (86400 * $lifetime));
 436 
 437         $this->write();
 438     }
 439 
 440     /**
 441      * Return the member for the auto login hash
 442      *
 443      * @param bool $login Should the member be logged in?
 444      */
 445     static function member_from_autologinhash($RAW_hash, $login = false) {
 446         $SQL_hash = Convert::raw2sql($RAW_hash);
 447 
 448         $member = DataObject::get_one('Member',"\"AutoLoginHash\"='" . $SQL_hash . "' AND \"AutoLoginExpired\" > " . DB::getConn()->now());
 449 
 450         if($login && $member)
 451             $member->logIn();
 452 
 453         return $member;
 454     }
 455 
 456 
 457     /**
 458      * Send signup, change password or forgot password informations to an user
 459      *
 460      * @param string $type Information type to send ("signup", "changePassword" or "forgotPassword")
 461      * @param array $data Additional data to pass to the email (can be used in the template)
 462      */
 463     function sendInfo($type = 'signup', $data = null) {
 464         switch($type) {
 465             case "signup":
 466                 $e = Object::create('Member_SignupEmail');
 467                 break;
 468             case "changePassword":
 469                 $e = Object::create('Member_ChangePasswordEmail');
 470                 break;
 471             case "forgotPassword":
 472                 $e = Object::create('Member_ForgotPasswordEmail');
 473                 break;
 474         }
 475 
 476         if(is_array($data)) {
 477             foreach($data as $key => $value)
 478                 $e->$key = $value;
 479         }
 480 
 481         $e->populateTemplate($this);
 482         $e->setTo($this->Email);
 483         $e->send();
 484     }
 485 
 486     /**
 487      * Returns the fields for the member form - used in the registration/profile module.
 488      * It should return fields that are editable by the admin and the logged-in user. 
 489      *
 490      * @return FieldSet Returns a {@link FieldSet} containing the fields for
 491      *                  the member form.
 492      * @param bool $newUser - new user flag (for registration)
 493      */
 494     public function getMemberFormFields($newUser = false) {
 495         $fields = parent::getFrontendFields();
 496 
 497         $fields->replaceField('Password', $password = new ConfirmedPasswordField (
 498             'Password',
 499             $this->fieldLabel('Password'),
 500             null,
 501             null,
 502             (bool) $this->ID
 503         ));
 504         $password->setCanBeEmpty($this->ID);
 505 
 506         $fields->replaceField('Locale', new DropdownField (
 507             'Locale',
 508             $this->fieldLabel('Locale'),
 509             i18n::get_existing_translations()
 510         ));
 511 
 512         $fields->removeByName('RememberLoginToken');
 513         $fields->removeByName('NumVisit');
 514         $fields->removeByName('LastVisited');
 515         $fields->removeByName('Bounced');
 516         $fields->removeByName('AutoLoginHash');
 517         $fields->removeByName('AutoLoginExpired');
 518         $fields->removeByName('PasswordEncryption');
 519         $fields->removeByName('Salt');
 520         $fields->removeByName('PasswordExpiry');
 521         $fields->removeByName('FailedLoginCount');
 522         $fields->removeByName('LastViewed');
 523         $fields->removeByName('LockedOutUntil');
 524         
 525         if ($f = $fields->dataFieldByName('FirstName')) {
 526             $f->setAutocomplete('given-name');
 527         }
 528         if ($f = $fields->dataFieldByName('Surname')) {
 529             $f->setAutocomplete('family-name');
 530         }
 531         $fields->replaceField('Email', new EmailField('Email', $this->fieldLabel('Email')));
 532         $fields->replaceField('Phone', new PhoneField('Phone', $this->fieldLabel('Phone')));
 533 
 534         $this->extend('updateMemberFormFields', $fields);
 535         return $fields;
 536     }
 537 
 538     function getValidator() {
 539         $obj = new Member_Validator();
 540         if (!$this->IsInDB())
 541             $obj->addRequiredfield('Password');
 542         $this->extend('updateValidator', $obj);
 543         return $obj;
 544     }
 545 
 546 
 547     /**
 548      * Returns the current logged in user
 549      *
 550      * @return bool|Member Returns the member object of the current logged in
 551      *                     user or FALSE.
 552      */
 553     static function currentUser() {
 554         $id = Member::currentUserID();
 555         if($id) {
 556             return DataObject::get_one("Member", "\"Member\".\"ID\" = $id");
 557         }
 558     }
 559 
 560 
 561     /**
 562      * Get the ID of the current logged in user
 563      *
 564      * @return int Returns the ID of the current logged in user or 0.
 565      */
 566     static function currentUserID() {
 567         $id = Session::get("loggedInAs");
 568         if(!$id && !self::$_already_tried_to_auto_log_in) {
 569             self::autoLogin();
 570             $id = Session::get("loggedInAs");
 571         }
 572 
 573         return is_numeric($id) ? $id : 0;
 574     }
 575     private static $_already_tried_to_auto_log_in = false;
 576 
 577 
 578     /*
 579      * Generate a random password, with randomiser to kick in if there's no words file on the
 580      * filesystem.
 581      *
 582      * @return string Returns a random password.
 583      */
 584     static function create_new_password() {
 585         if(file_exists(Security::get_word_list())) {
 586             $words = file(Security::get_word_list());
 587 
 588             list($usec, $sec) = explode(' ', microtime());
 589             srand($sec + ((float) $usec * 100000));
 590 
 591             $word = trim($words[rand(0,sizeof($words)-1)]);
 592             $number = rand(10,999);
 593 
 594             return $word . $number;
 595         } else {
 596             $random = rand();
 597             $string = md5($random);
 598             $output = substr($string, 0, 6);
 599             return $output;
 600         }
 601     }
 602 
 603     /**
 604      * Event handler called before writing to the database.
 605      */
 606     function onBeforeWrite() {
 607         if($this->SetPassword) $this->Password = $this->SetPassword;
 608 
 609         // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
 610         // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), 
 611         // but rather a last line of defense against data inconsistencies.
 612         $identifierField = self::$unique_identifier_field;
 613         if($this->$identifierField) {
 614             // Note: Same logic as Member_Validator class
 615             $idClause = ($this->ID) ? sprintf(" AND \"Member\".\"ID\" <> %d", (int)$this->ID) : '';
 616             $existingRecord = DataObject::get_one(
 617                 'Member', 
 618                 sprintf(
 619                     "\"%s\" = '%s' %s",
 620                     $identifierField,
 621                     Convert::raw2sql($this->$identifierField),
 622                     $idClause
 623                 )
 624             );
 625             if($existingRecord) {
 626                 throw new ValidationException(new ValidationResult(false, sprintf(
 627                     _t(
 628                         'Member.ValidationIdentifierFailed', 
 629                         'Can\'t overwrite existing member #%d with identical identifier (%s = %s))', 
 630                         PR_MEDIUM,
 631                         'The values in brackets show a fieldname mapped to a value, usually denoting an existing email address'
 632                     ),
 633                     $existingRecord->ID,
 634                     $identifierField,
 635                     $this->$identifierField
 636                 )));
 637             }
 638         }
 639 
 640         // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
 641         // However, if TestMailer is in use this isn't a risk.
 642         if(
 643             (Director::isLive() || Email::mailer() instanceof TestMailer) 
 644             && $this->isChanged('Password')
 645             && $this->record['Password'] 
 646             && Member::$notify_password_change
 647         ) {
 648             $this->sendInfo('changePassword');
 649         }
 650 
 651         // The test on $this->ID is used for when records are initially created.
 652         // Note that this only works with cleartext passwords, as we can't rehash
 653         // existing passwords.
 654         if(!$this->ID || $this->isChanged('Password')) {
 655             // Password was changed: encrypt the password according the settings
 656             $encryption_details = Security::encrypt_password(
 657                 $this->Password, // this is assumed to be cleartext
 658                 $this->Salt,
 659                 $this->PasswordEncryption,
 660                 $this
 661             );
 662             // Overwrite the Password property with the hashed value
 663             $this->Password = $encryption_details['password'];
 664             $this->Salt = $encryption_details['salt'];
 665             $this->PasswordEncryption = $encryption_details['algorithm'];
 666 
 667             // If we haven't manually set a password expiry
 668             if(!$this->isChanged('PasswordExpiry')) {
 669                 // then set it for us
 670                 if(self::$password_expiry_days) {
 671                     $this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::$password_expiry_days);
 672                 } else {
 673                     $this->PasswordExpiry = null;
 674                 }
 675             }
 676         }
 677 
 678         // save locale
 679         if(!$this->Locale) {
 680             $this->Locale = i18n::get_locale();
 681         }
 682 
 683         parent::onBeforeWrite();
 684     }
 685     
 686     function onAfterWrite() {
 687         parent::onAfterWrite();
 688 
 689         if($this->isChanged('Password')) {
 690             MemberPassword::log($this);
 691         }
 692     }
 693 
 694 
 695     /**
 696      * Check if the member is in one of the given groups.
 697      *
 698      * @param array|DataObjectSet $groups Collection of {@link Group} DataObjects to check
 699      * @param boolean $strict Only determine direct group membership if set to true (Default: false)
 700      * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
 701      */
 702     public function inGroups($groups, $strict = false) {
 703         if($groups) foreach($groups as $group) {
 704             if($this->inGroup($group, $strict)) return true;
 705         }
 706         
 707         return false;
 708     }
 709 
 710 
 711     /**
 712      * Check if the member is in the given group or any parent groups.
 713      *
 714      * @param int|Group|string $group Group instance, Group Code or ID
 715      * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
 716      * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
 717      */
 718     public function inGroup($group, $strict = false) {
 719         if(is_numeric($group)) {
 720             $groupCheckObj = DataObject::get_by_id('Group', $group);
 721         } elseif(is_string($group)) {
 722             $SQL_group = Convert::raw2sql($group);
 723             $groupCheckObj = DataObject::get_one('Group', "\"Code\" = '{$SQL_group}'");
 724         } elseif($group instanceof Group) {
 725             $groupCheckObj = $group;
 726         } else {
 727             user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
 728         }
 729         
 730         if(!$groupCheckObj) return false;
 731         
 732         $groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
 733         if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
 734             if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
 735         }
 736 
 737         return false;
 738     }
 739     
 740     /**
 741      * Returns true if this user is an administrator.
 742      * Administrators have access to everything.
 743      * 
 744      * @deprecated Use Permission::check('ADMIN') instead
 745      * @return Returns TRUE if this user is an administrator.
 746      */
 747     function isAdmin() {
 748         return Permission::checkMember($this, 'ADMIN');
 749     }
 750 
 751     /**
 752         *  Edit Profile Link
 753         * @return String
 754         */
 755     function EditProfileLink(){
 756         if($this->extend('EditProfileLink')){
 757             $a = $this->extend('EditProfileLink');
 758             return $a[0];
 759         }
 760         return 'myprofile';
 761     }
 762     
 763     /**
 764      * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
 765      * @param String $sep Separator
 766      */
 767     static function set_title_columns($columns, $sep = ' ') {
 768         if (!is_array($columns)) $columns = array($columns);
 769         self::$title_format = array('columns' => $columns, 'sep' => $sep);
 770     }
 771 
 772     //------------------- HELPER METHODS -----------------------------------//
 773 
 774     /**
 775      * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
 776      * Falls back to showing either field on its own.
 777      * 
 778      * You can overload this getter with {@link set_title_format()}
 779      * and {@link set_title_sql()}.
 780      *
 781      * @return string Returns the first- and surname of the member. If the ID
 782      *  of the member is equal 0, only the surname is returned.
 783      */
 784     public function getTitle() {
 785         if (self::$title_format) {
 786             $values = array();
 787             foreach(self::$title_format['columns'] as $col) {
 788                 $values[] = $this->getField($col);
 789             }
 790             return join(self::$title_format['sep'], $values);
 791         }
 792         if($this->getField('ID') === 0)
 793             return $this->getField('Surname');
 794         else{
 795             if($this->getField('Surname') && $this->getField('FirstName')){
 796                 return $this->getField('Surname') . ', ' . $this->getField('FirstName');
 797             }elseif($this->getField('Surname')){
 798                 return $this->getField('Surname');
 799             }elseif($this->getField('FirstName')){
 800                 return $this->getField('FirstName');
 801             }else{
 802                 return null;
 803             }
 804         }
 805     }
 806     
 807     /**
 808      * Return a SQL CONCAT() fragment suitable for a SELECT statement.
 809      * Useful for custom queries which assume a certain member title format.
 810      * 
 811      * @param String $tableName
 812      * @return String SQL
 813      */
 814     static function get_title_sql($tableName = 'Member') {
 815     
 816         if (self::$title_format) {
 817             $columnsWithTablename = array();
 818             foreach(self::$title_format['columns'] as $column) {
 819                 $columnsWithTablename[] = "\"$tableName\".\"$column\"";
 820             }
 821         
 822             return "(\"".join("'".self::$title_format['sep']."'" || $columnsWithTablename)."\")";
 823         } else {
 824             return "(\"$tableName\".\"Surname\" || ' ' || \"$tableName\".\"FirstName\")";
 825         }
 826     }
 827 
 828 
 829     /**
 830      * Get the complete name of the member
 831      *
 832      * @return string Returns the first- and surname of the member.
 833      */
 834     public function getName() {
 835         return $this->FirstName . ' ' . $this->Surname;
 836     }
 837 
 838 
 839     /**
 840      * Set first- and surname
 841      *
 842      * This method assumes that the last part of the name is the surname, e.g.
 843      * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
 844      *
 845      * @param string $name The name
 846      */
 847     public function setName($name) {
 848         $nameParts = explode(' ', $name);
 849         $this->Surname = array_pop($nameParts);
 850         $this->FirstName = join(' ', $nameParts);
 851     }
 852 
 853 
 854     /**
 855      * Alias for {@link setName}
 856      *
 857      * @param string $name The name
 858      * @see setName()
 859      */
 860     public function splitName($name) {
 861         return $this->setName($name);
 862     }
 863 
 864     //---------------------------------------------------------------------//
 865 
 866 
 867     /**
 868      * Get a "many-to-many" map that holds for all members their group
 869      * memberships
 870      *
 871      * @return Member_GroupSet Returns a map holding for all members their
 872      *                         group memberships.
 873      */
 874     public function Groups() {
 875         $groups = $this->getManyManyComponents("Groups");
 876         $groupIDs = $groups->column();
 877         $collatedGroups = array();
 878 
 879         if($groups) {
 880             foreach($groups as $group) {
 881                 $collatedGroups = array_merge((array)$collatedGroups, $group->collateAncestorIDs());
 882             }
 883         }
 884 
 885         $table = "Group_Members";
 886 
 887         if(count($collatedGroups) > 0) {
 888             $collatedGroups = implode(", ", array_unique($collatedGroups));
 889                         // FIX for subsites module
 890             $unfilteredGroups = singleton('Group')->instance_get("`Group`.`ID` IN ($collatedGroups)", "`Group`.`ID`", "", "", "Member_GroupSet");
 891             $result = new ComponentSet();
 892             
 893             // Only include groups where allowedIPAddress() returns true
 894             $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
 895             foreach($unfilteredGroups as $group) {
 896                 if($group->allowedIPAddress($ip)) $result->push($group);
 897             }
 898         } else {
 899             $result = new Member_GroupSet();
 900         }
 901 
 902         $result->setComponentInfo("many-to-many", $this, "Member", $table, "Group");
 903 
 904         return $result;
 905     }
 906 
 907 
 908     /**
 909      * Get member SQLMap
 910      *
 911      * @param string $filter Filter for the SQL statement (WHERE clause)
 912      * @param string $sort Sorting function (ORDER clause)
 913      * @param string $blank Shift a blank member in the items
 914      * @return SQLMap Returns an SQLMap that returns all Member data.
 915      *
 916      * @todo Improve documentation of this function! (Markus)
 917      */
 918     public function map($filter = "", $sort = "", $blank="") {
 919         $ret = new SQLMap(singleton('Member')->extendedSQL($filter, $sort));
 920         if($blank) {
 921             $blankMember = Object::create('Member');
 922             $blankMember->Surname = $blank;
 923             $blankMember->ID = 0;
 924 
 925             $ret->getItems()->shift($blankMember);
 926         }
 927 
 928         return $ret;
 929     }
 930 
 931 
 932     /**
 933      * Get a member SQLMap of members in specific groups
 934      *
 935      * @param mixed $groups Optional groups to include in the map. If NULL is
 936      *                      passed, all groups are returned, i.e.
 937      *                      {@link map()} will be called.
 938      * @return SQLMap Returns an SQLMap that returns all Member data.
 939      * @see map()
 940      *
 941      * @todo Improve documentation of this function! (Markus)
 942      */
 943     public static function mapInGroups($groups = null) {
 944         if(!$groups)
 945             return Member::map();
 946 
 947         $groupIDList = array();
 948 
 949         if(is_a($groups, 'DataObjectSet')) {
 950             foreach( $groups as $group )
 951                 $groupIDList[] = $group->ID;
 952         } elseif(is_array($groups)) {
 953             $groupIDList = $groups;
 954         } else {
 955             $groupIDList[] = $groups;
 956         }
 957 
 958         if(empty($groupIDList))
 959             return Member::map();
 960 
 961         return new SQLMap(singleton('Member')->extendedSQL(
 962             "\"GroupID\" IN (" . implode( ',', $groupIDList ) .
 963             ")", "Surname, FirstName", "", "INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\""));
 964     }
 965 
 966 
 967     /**
 968      * Get a map of all members in the groups given that have CMS permissions
 969      *
 970      * If no groups are passed, all groups with CMS permissions will be used.
 971      *
 972      * @param array $groups Groups to consider or NULL to use all groups with
 973      *                      CMS permissions.
 974      * @return SQLMap Returns a map of all members in the groups given that
 975      *                have CMS permissions.
 976      */
 977     public static function mapInCMSGroups($groups = null) {
 978         if(!$groups || $groups->Count() == 0) {
 979             $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
 980             
 981             $cmsPerms = singleton('CMSMain')->providePermissions();
 982             
 983             if(!empty($cmsPerms)) {
 984                 $perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
 985             }
 986             
 987             $SQL_perms = "'" . implode("', '", Convert::raw2sql($perms)) . "'";
 988             
 989             $groups = DataObject::get('Group', "", "",
 990                 "INNER JOIN \"Permission\" ON \"Permission\".\"GroupID\" = \"Group\".\"ID\" AND \"Permission\".\"Code\" IN ($SQL_perms)");
 991         }
 992 
 993         $groupIDList = array();
 994 
 995         if(is_a($groups, 'DataObjectSet')) {
 996             foreach($groups as $group) {
 997                 $groupIDList[] = $group->ID;
 998             }
 999         } elseif(is_array($groups)) {
1000             $groupIDList = $groups;
1001         }
1002 
1003         $filterClause = ($groupIDList)
1004             ? "\"GroupID\" IN (" . implode( ',', $groupIDList ) . ")"
1005             : "";
1006 
1007         return new SQLMap(singleton('Member')->extendedSQL($filterClause,
1008             "Surname, FirstName", "",
1009             "INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\" INNER JOIN \"Group\" ON \"Group\".\"ID\"=\"GroupID\""));
1010     }
1011 
1012 
1013     /**
1014      * Get the groups in which the member is NOT in
1015      *
1016      * When passed an array of groups, and a component set of groups, this
1017      * function will return the array of groups the member is NOT in.
1018      *
1019      * @param array $groupList An array of group code names.
1020      * @param array $memberGroups A component set of groups (if set to NULL,
1021      *                                                      $this->groups() will be used)
1022      * @return array Groups in which the member is NOT in.
1023      */
1024     public function memberNotInGroups($groupList, $memberGroups = null){
1025         if(!$memberGroups) $memberGroups = $this->Groups();
1026 
1027         foreach($memberGroups as $group) {
1028             if(in_array($group->Code, $groupList)) {
1029                 $index = array_search($group->Code, $groupList);
1030                 unset($groupList[$index]);
1031             }
1032         }
1033         
1034         return $groupList;
1035     }
1036 
1037 
1038     /**
1039      * Return a {@link FieldSet} of fields that would appropriate for editing
1040      * this member.
1041      *
1042      * @return FieldSet Return a FieldSet of fields that would appropriate for
1043      *                  editing this member.
1044      */
1045     public function getCMSFields() {
1046         $fields = parent::getCMSFields();
1047 
1048         $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
1049 
1050         $password = new ConfirmedPasswordField(
1051             'Password', 
1052             null, 
1053             null, 
1054             null, 
1055             true // showOnClick
1056         );
1057         $password->setCanBeEmpty(true);
1058         if(!$this->ID) $password->showOnClick = false;
1059         $mainFields->replaceField('Password', $password);
1060         
1061         $mainFields->insertBefore(
1062             new HeaderField('MemberDetailsHeader',_t('Member.PERSONALDETAILS', "Personal Details", PR_MEDIUM, 'Headline for formfields')),
1063             'FirstName'
1064         );
1065         
1066         $mainFields->insertBefore(
1067             new HeaderField('MemberUserDetailsHeader',_t('Member.USERDETAILS', "User Details", PR_MEDIUM, 'Headline for formfields')),
1068             'Email'
1069         );
1070         
1071         $mainFields->replaceField('Locale', new DropdownField(
1072             "Locale", 
1073             _t('Member.INTERFACELANG', "Interface Language", PR_MEDIUM, 'Language of the CMS'), 
1074             i18n::get_existing_translations()
1075         ));
1076         
1077         $mainFields->removeByName('Bounced');
1078         $mainFields->removeByName('RememberLoginToken');
1079         $mainFields->removeByName('AutoLoginHash');
1080         $mainFields->removeByName('AutoLoginExpired');
1081         $mainFields->removeByName('PasswordEncryption');
1082         $mainFields->removeByName('PasswordExpiry');
1083         $mainFields->removeByName('LockedOutUntil');
1084         
1085         if(!self::$lock_out_after_incorrect_logins) {
1086             $mainFields->removeByName('FailedLoginCount');
1087         }
1088         
1089         $mainFields->removeByName('Salt');
1090         $mainFields->removeByName('NumVisit');
1091         $mainFields->removeByName('LastVisited');
1092     
1093         $fields->removeByName('Subscriptions');
1094 
1095         // Groups relation will get us into logical conflicts because
1096         // Members are displayed within  group edit form in SecurityAdmin
1097         $fields->removeByName('Groups');
1098         
1099         if(Permission::check('EDIT_PERMISSIONS')) {
1100             $groupsField = new TreeMultiselectField('Groups', false, 'Group');
1101             $fields->findOrMakeTab('Root.Groups', singleton('Group')->i18n_plural_name());
1102             $fields->addFieldToTab('Root.Groups', $groupsField);
1103             
1104             // Add permission field (readonly to avoid complicated group assignment logic).
1105             // This should only be available for existing records, as new records start
1106             // with no permissions until they have a group assignment anyway.
1107             if($this->ID) {
1108                 $permissionsField = new PermissionCheckboxSetField_Readonly(
1109                     'Permissions',
1110                     singleton('Permission')->i18n_plural_name(),
1111                     'Permission',
1112                     'GroupID',
1113                     // we don't want parent relationships, they're automatically resolved in the field
1114                     $this->getManyManyComponents('Groups')
1115                 );
1116                 $fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1117                 $fields->addFieldToTab('Root.Permissions', $permissionsField);
1118             }
1119         }
1120 
1121         $this->extend('updateCMSFields', $fields);
1122         
1123         return $fields;
1124     }
1125     
1126     /**
1127      *
1128      * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1129      * 
1130      */
1131     function fieldLabels($includerelations = true) {
1132         $labels = parent::fieldLabels($includerelations);
1133         
1134         $labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1135         $labels['Surname'] = _t('Member.SURNAME', 'Surname');
1136         $labels['Email'] = _t('Member.EMAIL', 'Email');
1137         $labels['Password'] = _t('Member.db_Password', 'Password');
1138         $labels['NumVisit'] = _t('Member.db_NumVisit', 'Number of Visits');
1139         $labels['LastVisited'] = _t('Member.db_LastVisited', 'Last Visited Date');
1140         $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', PR_MEDIUM, 'Password expiry date');
1141         $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', PR_MEDIUM, 'Security related date');
1142         $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1143         $labels['Title'] = _t('Member.TITLE', 'Full Name');
1144         $labels['Created'] = _t('Member.db_Created', 'Created');
1145         if($includerelations){
1146             $labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups', PR_MEDIUM, 'Security Groups this member belongs to');
1147         }
1148         return $labels;
1149     }
1150     
1151     /**
1152      * Users can view their own record.
1153      * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1154      * This is likely to be customized for social sites etc. with a looser permission model.
1155      */
1156     function canView($member = null) {
1157         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1158         
1159         // decorated access checks
1160         $results = $this->extend('canView', $member);
1161         if($results && is_array($results)) {
1162             if(!min($results)) return false;
1163             else return true;
1164         }
1165         
1166         // members can usually edit their own record
1167         if($member && $this->ID == $member->ID) return true;
1168         
1169         if(
1170             Permission::checkMember($member, 'ADMIN')
1171             || Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin')
1172         ) {
1173             return true;
1174         }
1175         
1176         return false;
1177     }
1178     
1179     /**
1180      * Users can edit their own record.
1181      * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1182      */
1183     function canEdit($member = null) {
1184         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1185         
1186         // decorated access checks
1187         $results = $this->extend('canEdit', $member);
1188         if($results && is_array($results)) {
1189             if(!min($results)) return false;
1190             else return true;
1191         }
1192         
1193         // No member found
1194         if(!($member && $member->exists())) return false;
1195         
1196         return $this->canView($member);
1197     }
1198     
1199     /**
1200      * Users can edit their own record.
1201      * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1202      */
1203     function canDelete($member = null) {
1204         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1205         
1206         // decorated access checks
1207         $results = $this->extend('canDelete', $member);
1208         if($results && is_array($results)) {
1209             if(!min($results)) return false;
1210             else return true;
1211         }
1212         
1213         // No member found
1214         if(!($member && $member->exists())) return false;
1215         
1216         return $this->canEdit($member);
1217     }
1218 
1219 
1220     /**
1221      * Validate this member object.
1222      */
1223     function validate() {
1224         $valid = parent::validate();
1225         
1226         if(!$this->ID || $this->isChanged('Password')) {
1227             if($this->Password && self::$password_validator) {
1228                 $valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1229             }
1230         }
1231 
1232         if((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
1233             if($this->SetPassword && self::$password_validator) {
1234                 $valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this));
1235             }
1236         }
1237         $this->extend('updateValidation', $valid);
1238         return $valid;
1239     }   
1240     
1241     /**
1242      * Change password. This will cause rehashing according to
1243      * the `PasswordEncryption` property.
1244      * 
1245      * @param String $password Cleartext password
1246      */
1247     function changePassword($password) {
1248         $this->Password = $password;
1249         $valid = $this->validate();
1250         
1251         if($valid->valid()) {
1252             $this->AutoLoginHash = null;
1253             $this->write();
1254         }
1255         
1256         return $valid;
1257     }
1258     
1259     /**
1260      * Tell this member that someone made a failed attempt at logging in as them.
1261      * This can be used to lock the user out temporarily if too many failed attempts are made.
1262      */
1263     function registerFailedLogin() {
1264         if(self::$lock_out_after_incorrect_logins) {
1265             // Keep a tally of the number of failed log-ins so that we can lock people out
1266             $this->FailedLoginCount = $this->FailedLoginCount + 1;
1267             $this->write();
1268     
1269             if($this->FailedLoginCount >= self::$lock_out_after_incorrect_logins) {
1270                 $this->LockedOutUntil = date('Y-m-d H:i:s', time() + 15*60);
1271                 $this->write();
1272             }
1273         }
1274     }
1275     
1276     /**
1277      * Get the HtmlEditorConfig for this user to be used in the CMS.
1278      * This is set by the group. If multiple configurations are set,
1279      * the one with the highest priority wins.
1280      * 
1281      * @return string
1282      */
1283     function getHtmlEditorConfigForCMS() {
1284         $currentName = '';
1285         $currentPriority = 0;
1286         
1287         foreach($this->Groups() as $group) {
1288             $configName = $group->HtmlEditorConfig;
1289             if($configName) {
1290                 $config = HtmlEditorConfig::get($group->HtmlEditorConfig);
1291                 if($config && $config->getOption('priority') > $currentPriority) {
1292                     $currentName = $configName;
1293                 }
1294             }
1295         }
1296         
1297         // If can't find a suitable editor, just default to cms
1298         return $currentName ? $currentName : 'cms';
1299     }
1300 
1301 }
1302 
1303 /**
1304  * Special kind of {@link ComponentSet} that has special methods for
1305  * manipulating a user's membership
1306  * @package sapphire
1307  * @subpackage security
1308  */
1309 class Member_GroupSet extends ComponentSet {
1310     /**
1311      * Control group membership with a number of checkboxes.
1312      *  - If the checkbox fields are present in $data, then the member will be
1313      *    added to the group with the same codename.
1314      *  - If the checkbox fields are *NOT* present in $data, then the member
1315      *    will be removed from the group with the same codename.
1316      *
1317      * @param array $checkboxes An array list of the checkbox fieldnames (only
1318      *                            values are used). E.g. array(0, 1, 2)
1319      * @param array $data The form data. Uually in the format array(0 => 2)
1320      *                    (just pass the checkbox data from your form)
1321      */
1322     function setByCheckboxes(array $checkboxes, array $data) {
1323         foreach($checkboxes as $checkbox) {
1324             if($data[$checkbox]) {
1325                 $add[] = $checkbox;
1326             } else {
1327                 $remove[] = $checkbox;
1328             }
1329         }
1330 
1331         if($add)
1332             $this->addManyByCodename($add);
1333 
1334         if($remove)
1335             $this->removeManyByCodename($remove);
1336     }
1337 
1338 
1339     /**
1340      * Allows you to set groups based on a CheckboxSetField
1341      *
1342      * Pass the form element from your post data directly to this method, and
1343      * it will update the groups and add and remove the member as appropriate.
1344      *
1345      * On the form setup:
1346      *
1347      * <code>
1348      * $fields->push(
1349      *   new CheckboxSetField(
1350      *     "NewsletterSubscriptions",
1351      *     "Receive email notification of events in ",
1352      *     $sourceitems = DataObject::get("NewsletterType")->toDropDownMap("GroupID","Title"),
1353      *     $selectedgroups = $member->Groups()->Map("ID","ID")
1354      *   )
1355      * );
1356      * </code>
1357      *
1358      * On the form handler:
1359      *
1360      * <code>
1361      * $groups = $member->Groups();
1362      * $checkboxfield = $form->Fields()->fieldByName("NewsletterSubscriptions");
1363      * $groups->setByCheckboxSetField($checkboxfield);
1364      * </code>
1365      *
1366      * @param CheckboxSetField $checkboxsetfield The CheckboxSetField (with
1367      *                                           data) from your form.
1368      */
1369     function setByCheckboxSetField(CheckboxSetField $checkboxsetfield) {
1370         // Get the values from the formfield.
1371         $values = $checkboxsetfield->Value();
1372         $sourceItems = $checkboxsetfield->getSource();
1373 
1374         if($sourceItems) {
1375             // If (some) values are present, add and remove as necessary.
1376             if($values) {
1377                 // update the groups based on the selections
1378                 foreach($sourceItems as $k => $item) {
1379                     if(in_array($k,$values)) {
1380                         $add[] = $k;
1381                     } else {
1382                         $remove[] = $k;
1383                     }
1384                 }
1385 
1386             // else we should be removing all from the necessary groups.
1387             } else {
1388                 $remove = array_keys($sourceItems);
1389             }
1390 
1391             if($add)
1392                 $this->addManyByGroupID($add);
1393 
1394             if($remove)
1395                 $this->RemoveManyByGroupID($remove);
1396 
1397         } else {
1398             USER_ERROR("Member::setByCheckboxSetField() - No source items could be found for checkboxsetfield " .
1399                                  $checkboxsetfield->Name(), E_USER_WARNING);
1400         }
1401     }
1402 
1403 
1404     /**
1405      * Adds this member to the groups based on the group IDs
1406      *
1407      * @param array $ids Group identifiers.
1408      */
1409     function addManyByGroupID($groupIds){
1410         $groups = $this->getGroupsFromIDs($groupIds);
1411         if($groups) {
1412             foreach($groups as $group) {
1413                 $this->add($group);
1414             }
1415         }
1416     }
1417 
1418 
1419     /**
1420      * Removes the member from many groups based on the group IDs
1421      *
1422      * @param array $ids Group identifiers.
1423      */
1424     function removeManyByGroupID($groupIds) {
1425         $groups = $this->getGroupsFromIDs($groupIds);
1426         if($groups) {
1427             foreach($groups as $group) {
1428                 $this->remove($group);
1429             }
1430         }
1431     }
1432 
1433 
1434     /**
1435      * Returns the groups from an array of group IDs
1436      *
1437      * @param array $ids Group identifiers.
1438      * @return mixed Returns the groups from the array of Group IDs.
1439      */
1440     function getGroupsFromIDs($ids){
1441         if($ids && count($ids) > 1) {
1442             return DataObject::get("Group", "\"ID\" IN (" . implode(",", $ids) . ")");
1443         } else {
1444             return DataObject::get_by_id("Group", $ids[0]);
1445         }
1446     }
1447 
1448 
1449     /**
1450      * Adds this member to the groups based on the group codenames
1451      *
1452      * @param array $codenames Group codenames
1453      */
1454     function addManyByCodename($codenames) {
1455         $groups = $this->codenamesToGroups($codenames);
1456         if($groups) {
1457             foreach($groups as $group){
1458                 $this->add($group);
1459             }
1460         }
1461     }
1462 
1463 
1464     /**
1465      * Removes this member from the groups based on the group codenames
1466      *
1467      * @param array $codenames Group codenames
1468      */
1469     function removeManyByCodename($codenames) {
1470         $groups = $this->codenamesToGroups($codenames);
1471         if($groups) {
1472             foreach($groups as $group) {
1473                 $this->remove($group);
1474             }
1475         }
1476     }
1477 
1478 
1479     /**
1480      * Helper function to return the appropriate groups via a codenames
1481      *
1482      * @param array $codenames Group codenames
1483      * @return array Returns the the appropriate groups.
1484      */
1485     protected function codenamesToGroups($codenames) {
1486         $list = "'" . implode("', '", $codenames) . "'";
1487         $output = DataObject::get("Group", "\"Code\" IN ($list)");
1488 
1489         // Some are missing - throw warnings
1490         if(!$output || ($output->Count() != sizeof($list))) {
1491             foreach($codenames as $codename)
1492                 $missing[$codename] = $codename;
1493 
1494             if($output) {
1495                 foreach($output as $record)
1496                     unset($missing[$record->Code]);
1497             }
1498 
1499             if($missing)
1500                 user_error("The following group-codes aren't matched to any groups: " .
1501                                      implode(", ", $missing) .
1502                                      ".  You probably need to link up the correct group codes in phpMyAdmin",
1503                                      E_USER_WARNING);
1504         }
1505 
1506         return $output;
1507     }
1508 }
1509 
1510 
1511 
1512 /**
1513  * Form for editing a member profile.
1514  * @package sapphire
1515  * @subpackage security
1516  */
1517 class Member_ProfileForm extends Form {
1518     
1519     function __construct($controller, $name, $member) {
1520         Requirements::clear();
1521         Requirements::css(CMS_DIR . '/css/typography.css');
1522         Requirements::css(CMS_DIR . '/css/cms_right.css');
1523         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/prototype/prototype.js");
1524         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/behaviour/behaviour.js");
1525         Requirements::javascript(SAPPHIRE_DIR . "/javascript/prototype_improvements.js");
1526         Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/scriptaculous.js");
1527         Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/controls.js");
1528         Requirements::javascript(SAPPHIRE_DIR . "/javascript/layout_helpers.js");
1529         Requirements::css(SAPPHIRE_DIR . "/css/Form.css");
1530         
1531         Requirements::css(SAPPHIRE_DIR . "/css/MemberProfileForm.css");
1532         
1533         
1534         $fields = singleton('Member')->getCMSFields();
1535         $fields->push(new HiddenField('ID','ID',$member->ID));
1536 
1537         $actions = new FieldSet(
1538             new FormAction('dosave', _t('CMSMain.SAVE', 'Save'))
1539         );
1540         
1541         $validator = new Member_Validator();
1542         
1543         parent::__construct($controller, $name, $fields, $actions, $validator);
1544         
1545         $this->loadDataFrom($member);
1546     }
1547     
1548     function dosave($data, $form) {
1549         // don't allow ommitting or changing the ID
1550         if(!isset($data['ID']) || $data['ID'] != Member::currentUserID()) {
1551             return Director::redirectBack();
1552         }
1553         
1554         $SQL_data = Convert::raw2sql($data);
1555         $member = DataObject::get_by_id("Member", $SQL_data['ID']);
1556         
1557         if($SQL_data['Locale'] != $member->Locale) {
1558             $form->addErrorMessage("Generic", _t('Member.REFRESHLANG'),"good");
1559         }
1560         
1561         $form->saveInto($member);
1562         $member->write();
1563         
1564         $closeLink = sprintf(
1565             '<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
1566             _t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
1567         );
1568         $message = _t('Member.PROFILESAVESUCCESS', 'Successfully saved.') . ' ' . $closeLink;
1569         $form->sessionMessage($message, 'good');
1570         
1571         Director::redirectBack();
1572     }
1573 }
1574 
1575 /**
1576  * Class used as template to send an email to new members
1577  * @package sapphire
1578  * @subpackage security
1579  */
1580 class Member_SignupEmail extends Email {
1581     protected $from = '';  // setting a blank from address uses the site's default administrator email
1582     protected $subject = '';
1583     protected $body = '';
1584 
1585     function __construct() {
1586         parent::__construct();
1587         $this->subject = _t('Member.EMAILSIGNUPSUBJECT', "Thanks for signing up");
1588         $this->body = '
1589             <h1>' . _t('Member.GREETING','Welcome') . ', $FirstName.</h1>
1590             <p>' . _t('Member.EMAILSIGNUPINTRO1','Thanks for signing up to become a new member, your details are listed below for future reference.') . '</p>
1591 
1592             <p>' . _t('Member.EMAILSIGNUPINTRO2','You can login to the website using the credentials listed below')  . ':
1593                 <ul>
1594                     <li><strong>' . _t('Member.EMAIL') . '</strong>$Email</li>
1595                     <li><strong>' . _t('Member.PASSWORD') . ':</strong>$Password</li>
1596                 </ul>
1597             </p>
1598 
1599             <h3>' . _t('Member.CONTACTINFO','Contact Information') . '</h3>
1600             <ul>
1601                 <li><strong>' . _t('Member.NAME','Name')  . ':</strong> $FirstName $Surname</li>
1602                 <% if Phone %>
1603                     <li><strong>' . _t('Member.PHONE','Phone') . ':</strong> $Phone</li>
1604                 <% end_if %>
1605 
1606                 <% if Mobile %>
1607                     <li><strong>' . _t('Member.MOBILE','Mobile') . ':</strong> $Mobile</li>
1608                 <% end_if %>
1609 
1610                 <li><strong>' . _t('Member.ADDRESS','Address') . ':</strong>
1611                 <br/>
1612                 $Number $Street $StreetType<br/>
1613                 $Suburb<br/>
1614                 $City $Postcode
1615                 </li>
1616 
1617             </ul>
1618         ';
1619     }
1620 }
1621 
1622 
1623 
1624 /**
1625  * Class used as template to send an email saying that the password has been
1626  * changed
1627  * @package sapphire
1628  * @subpackage security
1629  */
1630 class Member_ChangePasswordEmail extends Email {
1631     protected $from = '';   // setting a blank from address uses the site's default administrator email
1632     protected $subject = '';
1633     protected $ss_template = 'ChangePasswordEmail';
1634     
1635     function __construct() {
1636         parent::__construct();
1637         $this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", PR_MEDIUM, 'Email subject');
1638     }
1639 }
1640 
1641 
1642 
1643 /**
1644  * Class used as template to send the forgot password email
1645  * @package sapphire
1646  * @subpackage security
1647  */
1648 class Member_ForgotPasswordEmail extends Email {
1649     protected $from = '';  // setting a blank from address uses the site's default administrator email
1650     protected $subject = '';
1651     protected $ss_template = 'ForgotPasswordEmail';
1652     
1653     function __construct() {
1654         parent::__construct();
1655         $this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", PR_MEDIUM, 'Email subject');
1656     }
1657 }
1658 
1659 /**
1660  * Member Validator
1661  * @package sapphire
1662  * @subpackage security
1663  */
1664 class Member_Validator extends RequiredFields {
1665 
1666     protected $customRequired = array('FirstName', 'Email'); //, 'Password');
1667 
1668 
1669     /**
1670      * Constructor
1671      */
1672     public function __construct() {
1673         $required = func_get_args();
1674         if(isset($required[0]) && is_array($required[0])) {
1675             $required = $required[0];
1676         }
1677         $required = array_merge($required, $this->customRequired);
1678 
1679         parent::__construct($required);
1680     }
1681 
1682 
1683     /**
1684      * Check if the submitted member data is valid (server-side)
1685      *
1686      * Check if a member with that email doesn't already exist, or if it does
1687      * that it is this member.
1688      *
1689      * @param array $data Submitted data
1690      * @return bool Returns TRUE if the submitted data is valid, otherwise
1691      *              FALSE.
1692      */
1693     function php($data) {
1694         $valid = parent::php($data);
1695         
1696         $identifierField = Member::get_unique_identifier_field();
1697         
1698         $SQL_identifierField = Convert::raw2sql($data[$identifierField]);
1699         $member = DataObject::get_one('Member', "\"$identifierField\" = '{$SQL_identifierField}'");
1700 
1701         // if we are in a complex table field popup, use ctf[childID], else use ID
1702         if(isset($_REQUEST['ctf']['childID'])) {
1703             $id = $_REQUEST['ctf']['childID'];
1704         } elseif(isset($_REQUEST['ctf']['ID'])) {
1705             $id = $_REQUEST['ctf']['ID'];
1706         } elseif(isset($_REQUEST['ID'])) {
1707             $id = $_REQUEST['ID'];
1708         } else {
1709             $id = null;
1710         }
1711 
1712         if(is_object($member) && (($id && $member->ID != $id)||(!$id && $member->ID))) {
1713             $uniqueField = $this->form->dataFieldByName($identifierField);
1714             $this->validationError(
1715                 $uniqueField->id(),
1716                 sprintf(
1717                     _t(
1718                         'Member.VALIDATIONMEMBEREXISTS',
1719                         'A member already exists with the same %s'
1720                     ),
1721                     strtolower(singleton('Member')->fieldLabel($identifierField))
1722                 ),
1723                 'required'
1724             );
1725             $valid = false;
1726         }
1727 
1728         // Execute the validators on the extensions
1729         if($this->extension_instances) {
1730             foreach($this->extension_instances as $extension) {
1731                 if(method_exists($extension, 'hasMethod') && $extension->hasMethod('updatePHP')) {
1732                     $valid &= $extension->updatePHP($data, $this->form);
1733                 }
1734             }
1735         }
1736 
1737         return $valid;
1738     }
1739 
1740 
1741     /**
1742      * Check if the submitted member data is valid (client-side)
1743      *
1744      * @param array $data Submitted data
1745      * @return bool Returns TRUE if the submitted data is valid, otherwise
1746      *              FALSE.
1747      */
1748     function javascript() {
1749         $js = parent::javascript();
1750 
1751         // Execute the validators on the extensions
1752         if($this->extension_instances) {
1753             foreach($this->extension_instances as $extension) {
1754                 if(method_exists($extension, 'hasMethod') && $extension->hasMethod('updateJavascript')) {
1755                     $extension->updateJavascript($js, $this->form);
1756                 }
1757             }
1758         }
1759 
1760         return $js;
1761     }
1762 
1763 }
1764 ?>
[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. -
Webylon 3.1 API Docs API documentation generated by ApiGen 2.8.0