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

Packages

  • 1c
    • exchange
      • catalog
  • auth
  • Booking
  • building
    • company
  • cart
    • shipping
    • steppedcheckout
  • Catalog
    • monument
  • 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' => 'First Name',
  73         'Surname' => 'Last Name',
  74         'Email' => '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         $this->extend('updateMemberFormFields', $fields);
 526         return $fields;
 527     }
 528 
 529     function getValidator() {
 530         $obj = new Member_Validator();
 531         if (!$this->IsInDB())
 532             $obj->addRequiredfield('Password');
 533         $this->extend('updateValidator', $obj);
 534         return $obj;
 535     }
 536 
 537 
 538     /**
 539      * Returns the current logged in user
 540      *
 541      * @return bool|Member Returns the member object of the current logged in
 542      *                     user or FALSE.
 543      */
 544     static function currentUser() {
 545         $id = Member::currentUserID();
 546         if($id) {
 547             return DataObject::get_one("Member", "\"Member\".\"ID\" = $id");
 548         }
 549     }
 550 
 551 
 552     /**
 553      * Get the ID of the current logged in user
 554      *
 555      * @return int Returns the ID of the current logged in user or 0.
 556      */
 557     static function currentUserID() {
 558         $id = Session::get("loggedInAs");
 559         if(!$id && !self::$_already_tried_to_auto_log_in) {
 560             self::autoLogin();
 561             $id = Session::get("loggedInAs");
 562         }
 563 
 564         return is_numeric($id) ? $id : 0;
 565     }
 566     private static $_already_tried_to_auto_log_in = false;
 567 
 568 
 569     /*
 570      * Generate a random password, with randomiser to kick in if there's no words file on the
 571      * filesystem.
 572      *
 573      * @return string Returns a random password.
 574      */
 575     static function create_new_password() {
 576         if(file_exists(Security::get_word_list())) {
 577             $words = file(Security::get_word_list());
 578 
 579             list($usec, $sec) = explode(' ', microtime());
 580             srand($sec + ((float) $usec * 100000));
 581 
 582             $word = trim($words[rand(0,sizeof($words)-1)]);
 583             $number = rand(10,999);
 584 
 585             return $word . $number;
 586         } else {
 587             $random = rand();
 588             $string = md5($random);
 589             $output = substr($string, 0, 6);
 590             return $output;
 591         }
 592     }
 593 
 594     /**
 595      * Event handler called before writing to the database.
 596      */
 597     function onBeforeWrite() {
 598         if($this->SetPassword) $this->Password = $this->SetPassword;
 599 
 600         // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
 601         // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), 
 602         // but rather a last line of defense against data inconsistencies.
 603         $identifierField = self::$unique_identifier_field;
 604         if($this->$identifierField) {
 605             // Note: Same logic as Member_Validator class
 606             $idClause = ($this->ID) ? sprintf(" AND \"Member\".\"ID\" <> %d", (int)$this->ID) : '';
 607             $existingRecord = DataObject::get_one(
 608                 'Member', 
 609                 sprintf(
 610                     "\"%s\" = '%s' %s",
 611                     $identifierField,
 612                     Convert::raw2sql($this->$identifierField),
 613                     $idClause
 614                 )
 615             );
 616             if($existingRecord) {
 617                 throw new ValidationException(new ValidationResult(false, sprintf(
 618                     _t(
 619                         'Member.ValidationIdentifierFailed', 
 620                         'Can\'t overwrite existing member #%d with identical identifier (%s = %s))', 
 621                         PR_MEDIUM,
 622                         'The values in brackets show a fieldname mapped to a value, usually denoting an existing email address'
 623                     ),
 624                     $existingRecord->ID,
 625                     $identifierField,
 626                     $this->$identifierField
 627                 )));
 628             }
 629         }
 630 
 631         // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
 632         // However, if TestMailer is in use this isn't a risk.
 633         if(
 634             (Director::isLive() || Email::mailer() instanceof TestMailer) 
 635             && $this->isChanged('Password')
 636             && $this->record['Password'] 
 637             && Member::$notify_password_change
 638         ) {
 639             $this->sendInfo('changePassword');
 640         }
 641 
 642         // The test on $this->ID is used for when records are initially created.
 643         // Note that this only works with cleartext passwords, as we can't rehash
 644         // existing passwords.
 645         if(!$this->ID || $this->isChanged('Password')) {
 646             // Password was changed: encrypt the password according the settings
 647             $encryption_details = Security::encrypt_password(
 648                 $this->Password, // this is assumed to be cleartext
 649                 $this->Salt,
 650                 $this->PasswordEncryption,
 651                 $this
 652             );
 653             // Overwrite the Password property with the hashed value
 654             $this->Password = $encryption_details['password'];
 655             $this->Salt = $encryption_details['salt'];
 656             $this->PasswordEncryption = $encryption_details['algorithm'];
 657 
 658             // If we haven't manually set a password expiry
 659             if(!$this->isChanged('PasswordExpiry')) {
 660                 // then set it for us
 661                 if(self::$password_expiry_days) {
 662                     $this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::$password_expiry_days);
 663                 } else {
 664                     $this->PasswordExpiry = null;
 665                 }
 666             }
 667         }
 668 
 669         // save locale
 670         if(!$this->Locale) {
 671             $this->Locale = i18n::get_locale();
 672         }
 673 
 674         parent::onBeforeWrite();
 675     }
 676     
 677     function onAfterWrite() {
 678         parent::onAfterWrite();
 679 
 680         if($this->isChanged('Password')) {
 681             MemberPassword::log($this);
 682         }
 683     }
 684 
 685 
 686     /**
 687      * Check if the member is in one of the given groups.
 688      *
 689      * @param array|DataObjectSet $groups Collection of {@link Group} DataObjects to check
 690      * @param boolean $strict Only determine direct group membership if set to true (Default: false)
 691      * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
 692      */
 693     public function inGroups($groups, $strict = false) {
 694         if($groups) foreach($groups as $group) {
 695             if($this->inGroup($group, $strict)) return true;
 696         }
 697         
 698         return false;
 699     }
 700 
 701 
 702     /**
 703      * Check if the member is in the given group or any parent groups.
 704      *
 705      * @param int|Group|string $group Group instance, Group Code or ID
 706      * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
 707      * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
 708      */
 709     public function inGroup($group, $strict = false) {
 710         if(is_numeric($group)) {
 711             $groupCheckObj = DataObject::get_by_id('Group', $group);
 712         } elseif(is_string($group)) {
 713             $SQL_group = Convert::raw2sql($group);
 714             $groupCheckObj = DataObject::get_one('Group', "\"Code\" = '{$SQL_group}'");
 715         } elseif($group instanceof Group) {
 716             $groupCheckObj = $group;
 717         } else {
 718             user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
 719         }
 720         
 721         if(!$groupCheckObj) return false;
 722         
 723         $groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
 724         if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
 725             if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
 726         }
 727 
 728         return false;
 729     }
 730     
 731     /**
 732      * Returns true if this user is an administrator.
 733      * Administrators have access to everything.
 734      * 
 735      * @deprecated Use Permission::check('ADMIN') instead
 736      * @return Returns TRUE if this user is an administrator.
 737      */
 738     function isAdmin() {
 739         return Permission::checkMember($this, 'ADMIN');
 740     }
 741 
 742     /**
 743         *  Edit Profile Link
 744         * @return String
 745         */
 746     function EditProfileLink(){
 747         if($this->extend('EditProfileLink')){
 748             $a = $this->extend('EditProfileLink');
 749             return $a[0];
 750         }
 751         return 'myprofile';
 752     }
 753     
 754     /**
 755      * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
 756      * @param String $sep Separator
 757      */
 758     static function set_title_columns($columns, $sep = ' ') {
 759         if (!is_array($columns)) $columns = array($columns);
 760         self::$title_format = array('columns' => $columns, 'sep' => $sep);
 761     }
 762 
 763     //------------------- HELPER METHODS -----------------------------------//
 764 
 765     /**
 766      * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
 767      * Falls back to showing either field on its own.
 768      * 
 769      * You can overload this getter with {@link set_title_format()}
 770      * and {@link set_title_sql()}.
 771      *
 772      * @return string Returns the first- and surname of the member. If the ID
 773      *  of the member is equal 0, only the surname is returned.
 774      */
 775     public function getTitle() {
 776         if (self::$title_format) {
 777             $values = array();
 778             foreach(self::$title_format['columns'] as $col) {
 779                 $values[] = $this->getField($col);
 780             }
 781             return join(self::$title_format['sep'], $values);
 782         }
 783         if($this->getField('ID') === 0)
 784             return $this->getField('Surname');
 785         else{
 786             if($this->getField('Surname') && $this->getField('FirstName')){
 787                 return $this->getField('Surname') . ', ' . $this->getField('FirstName');
 788             }elseif($this->getField('Surname')){
 789                 return $this->getField('Surname');
 790             }elseif($this->getField('FirstName')){
 791                 return $this->getField('FirstName');
 792             }else{
 793                 return null;
 794             }
 795         }
 796     }
 797     
 798     /**
 799      * Return a SQL CONCAT() fragment suitable for a SELECT statement.
 800      * Useful for custom queries which assume a certain member title format.
 801      * 
 802      * @param String $tableName
 803      * @return String SQL
 804      */
 805     static function get_title_sql($tableName = 'Member') {
 806     
 807         if (self::$title_format) {
 808             $columnsWithTablename = array();
 809             foreach(self::$title_format['columns'] as $column) {
 810                 $columnsWithTablename[] = "\"$tableName\".\"$column\"";
 811             }
 812         
 813             return "(\"".join("'".self::$title_format['sep']."'" || $columnsWithTablename)."\")";
 814         } else {
 815             return "(\"$tableName\".\"Surname\" || ' ' || \"$tableName\".\"FirstName\")";
 816         }
 817     }
 818 
 819 
 820     /**
 821      * Get the complete name of the member
 822      *
 823      * @return string Returns the first- and surname of the member.
 824      */
 825     public function getName() {
 826         return $this->FirstName . ' ' . $this->Surname;
 827     }
 828 
 829 
 830     /**
 831      * Set first- and surname
 832      *
 833      * This method assumes that the last part of the name is the surname, e.g.
 834      * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
 835      *
 836      * @param string $name The name
 837      */
 838     public function setName($name) {
 839         $nameParts = explode(' ', $name);
 840         $this->Surname = array_pop($nameParts);
 841         $this->FirstName = join(' ', $nameParts);
 842     }
 843 
 844 
 845     /**
 846      * Alias for {@link setName}
 847      *
 848      * @param string $name The name
 849      * @see setName()
 850      */
 851     public function splitName($name) {
 852         return $this->setName($name);
 853     }
 854 
 855     //---------------------------------------------------------------------//
 856 
 857 
 858     /**
 859      * Get a "many-to-many" map that holds for all members their group
 860      * memberships
 861      *
 862      * @return Member_GroupSet Returns a map holding for all members their
 863      *                         group memberships.
 864      */
 865     public function Groups() {
 866         $groups = $this->getManyManyComponents("Groups");
 867         $groupIDs = $groups->column();
 868         $collatedGroups = array();
 869 
 870         if($groups) {
 871             foreach($groups as $group) {
 872                 $collatedGroups = array_merge((array)$collatedGroups, $group->collateAncestorIDs());
 873             }
 874         }
 875 
 876         $table = "Group_Members";
 877 
 878         if(count($collatedGroups) > 0) {
 879             $collatedGroups = implode(", ", array_unique($collatedGroups));
 880                         // FIX for subsites module
 881             $unfilteredGroups = singleton('Group')->instance_get("`Group`.`ID` IN ($collatedGroups)", "`Group`.`ID`", "", "", "Member_GroupSet");
 882             $result = new ComponentSet();
 883             
 884             // Only include groups where allowedIPAddress() returns true
 885             $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
 886             foreach($unfilteredGroups as $group) {
 887                 if($group->allowedIPAddress($ip)) $result->push($group);
 888             }
 889         } else {
 890             $result = new Member_GroupSet();
 891         }
 892 
 893         $result->setComponentInfo("many-to-many", $this, "Member", $table, "Group");
 894 
 895         return $result;
 896     }
 897 
 898 
 899     /**
 900      * Get member SQLMap
 901      *
 902      * @param string $filter Filter for the SQL statement (WHERE clause)
 903      * @param string $sort Sorting function (ORDER clause)
 904      * @param string $blank Shift a blank member in the items
 905      * @return SQLMap Returns an SQLMap that returns all Member data.
 906      *
 907      * @todo Improve documentation of this function! (Markus)
 908      */
 909     public function map($filter = "", $sort = "", $blank="") {
 910         $ret = new SQLMap(singleton('Member')->extendedSQL($filter, $sort));
 911         if($blank) {
 912             $blankMember = Object::create('Member');
 913             $blankMember->Surname = $blank;
 914             $blankMember->ID = 0;
 915 
 916             $ret->getItems()->shift($blankMember);
 917         }
 918 
 919         return $ret;
 920     }
 921 
 922 
 923     /**
 924      * Get a member SQLMap of members in specific groups
 925      *
 926      * @param mixed $groups Optional groups to include in the map. If NULL is
 927      *                      passed, all groups are returned, i.e.
 928      *                      {@link map()} will be called.
 929      * @return SQLMap Returns an SQLMap that returns all Member data.
 930      * @see map()
 931      *
 932      * @todo Improve documentation of this function! (Markus)
 933      */
 934     public static function mapInGroups($groups = null) {
 935         if(!$groups)
 936             return Member::map();
 937 
 938         $groupIDList = array();
 939 
 940         if(is_a($groups, 'DataObjectSet')) {
 941             foreach( $groups as $group )
 942                 $groupIDList[] = $group->ID;
 943         } elseif(is_array($groups)) {
 944             $groupIDList = $groups;
 945         } else {
 946             $groupIDList[] = $groups;
 947         }
 948 
 949         if(empty($groupIDList))
 950             return Member::map();
 951 
 952         return new SQLMap(singleton('Member')->extendedSQL(
 953             "\"GroupID\" IN (" . implode( ',', $groupIDList ) .
 954             ")", "Surname, FirstName", "", "INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\""));
 955     }
 956 
 957 
 958     /**
 959      * Get a map of all members in the groups given that have CMS permissions
 960      *
 961      * If no groups are passed, all groups with CMS permissions will be used.
 962      *
 963      * @param array $groups Groups to consider or NULL to use all groups with
 964      *                      CMS permissions.
 965      * @return SQLMap Returns a map of all members in the groups given that
 966      *                have CMS permissions.
 967      */
 968     public static function mapInCMSGroups($groups = null) {
 969         if(!$groups || $groups->Count() == 0) {
 970             $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
 971             
 972             $cmsPerms = singleton('CMSMain')->providePermissions();
 973             
 974             if(!empty($cmsPerms)) {
 975                 $perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
 976             }
 977             
 978             $SQL_perms = "'" . implode("', '", Convert::raw2sql($perms)) . "'";
 979             
 980             $groups = DataObject::get('Group', "", "",
 981                 "INNER JOIN \"Permission\" ON \"Permission\".\"GroupID\" = \"Group\".\"ID\" AND \"Permission\".\"Code\" IN ($SQL_perms)");
 982         }
 983 
 984         $groupIDList = array();
 985 
 986         if(is_a($groups, 'DataObjectSet')) {
 987             foreach($groups as $group) {
 988                 $groupIDList[] = $group->ID;
 989             }
 990         } elseif(is_array($groups)) {
 991             $groupIDList = $groups;
 992         }
 993 
 994         $filterClause = ($groupIDList)
 995             ? "\"GroupID\" IN (" . implode( ',', $groupIDList ) . ")"
 996             : "";
 997 
 998         return new SQLMap(singleton('Member')->extendedSQL($filterClause,
 999             "Surname, FirstName", "",
1000             "INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\" INNER JOIN \"Group\" ON \"Group\".\"ID\"=\"GroupID\""));
1001     }
1002 
1003 
1004     /**
1005      * Get the groups in which the member is NOT in
1006      *
1007      * When passed an array of groups, and a component set of groups, this
1008      * function will return the array of groups the member is NOT in.
1009      *
1010      * @param array $groupList An array of group code names.
1011      * @param array $memberGroups A component set of groups (if set to NULL,
1012      *                                                      $this->groups() will be used)
1013      * @return array Groups in which the member is NOT in.
1014      */
1015     public function memberNotInGroups($groupList, $memberGroups = null){
1016         if(!$memberGroups) $memberGroups = $this->Groups();
1017 
1018         foreach($memberGroups as $group) {
1019             if(in_array($group->Code, $groupList)) {
1020                 $index = array_search($group->Code, $groupList);
1021                 unset($groupList[$index]);
1022             }
1023         }
1024         
1025         return $groupList;
1026     }
1027 
1028 
1029     /**
1030      * Return a {@link FieldSet} of fields that would appropriate for editing
1031      * this member.
1032      *
1033      * @return FieldSet Return a FieldSet of fields that would appropriate for
1034      *                  editing this member.
1035      */
1036     public function getCMSFields() {
1037         $fields = parent::getCMSFields();
1038 
1039         $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
1040 
1041         $password = new ConfirmedPasswordField(
1042             'Password', 
1043             null, 
1044             null, 
1045             null, 
1046             true // showOnClick
1047         );
1048         $password->setCanBeEmpty(true);
1049         if(!$this->ID) $password->showOnClick = false;
1050         $mainFields->replaceField('Password', $password);
1051         
1052         $mainFields->insertBefore(
1053             new HeaderField('MemberDetailsHeader',_t('Member.PERSONALDETAILS', "Personal Details", PR_MEDIUM, 'Headline for formfields')),
1054             'FirstName'
1055         );
1056         
1057         $mainFields->insertBefore(
1058             new HeaderField('MemberUserDetailsHeader',_t('Member.USERDETAILS', "User Details", PR_MEDIUM, 'Headline for formfields')),
1059             'Email'
1060         );
1061         
1062         $mainFields->replaceField('Locale', new DropdownField(
1063             "Locale", 
1064             _t('Member.INTERFACELANG', "Interface Language", PR_MEDIUM, 'Language of the CMS'), 
1065             i18n::get_existing_translations()
1066         ));
1067         
1068         $mainFields->removeByName('Bounced');
1069         $mainFields->removeByName('RememberLoginToken');
1070         $mainFields->removeByName('AutoLoginHash');
1071         $mainFields->removeByName('AutoLoginExpired');
1072         $mainFields->removeByName('PasswordEncryption');
1073         $mainFields->removeByName('PasswordExpiry');
1074         $mainFields->removeByName('LockedOutUntil');
1075         
1076         if(!self::$lock_out_after_incorrect_logins) {
1077             $mainFields->removeByName('FailedLoginCount');
1078         }
1079         
1080         $mainFields->removeByName('Salt');
1081         $mainFields->removeByName('NumVisit');
1082         $mainFields->removeByName('LastVisited');
1083     
1084         $fields->removeByName('Subscriptions');
1085 
1086         // Groups relation will get us into logical conflicts because
1087         // Members are displayed within  group edit form in SecurityAdmin
1088         $fields->removeByName('Groups');
1089         
1090         if(Permission::check('EDIT_PERMISSIONS')) {
1091             $groupsField = new TreeMultiselectField('Groups', false, 'Group');
1092             $fields->findOrMakeTab('Root.Groups', singleton('Group')->i18n_plural_name());
1093             $fields->addFieldToTab('Root.Groups', $groupsField);
1094             
1095             // Add permission field (readonly to avoid complicated group assignment logic).
1096             // This should only be available for existing records, as new records start
1097             // with no permissions until they have a group assignment anyway.
1098             if($this->ID) {
1099                 $permissionsField = new PermissionCheckboxSetField_Readonly(
1100                     'Permissions',
1101                     singleton('Permission')->i18n_plural_name(),
1102                     'Permission',
1103                     'GroupID',
1104                     // we don't want parent relationships, they're automatically resolved in the field
1105                     $this->getManyManyComponents('Groups')
1106                 );
1107                 $fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1108                 $fields->addFieldToTab('Root.Permissions', $permissionsField);
1109             }
1110         }
1111 
1112         $this->extend('updateCMSFields', $fields);
1113         
1114         return $fields;
1115     }
1116     
1117     /**
1118      *
1119      * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1120      * 
1121      */
1122     function fieldLabels($includerelations = true) {
1123         $labels = parent::fieldLabels($includerelations);
1124         
1125         $labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1126         $labels['Surname'] = _t('Member.SURNAME', 'Surname');
1127         $labels['Email'] = _t('Member.EMAIL', 'Email');
1128         $labels['Password'] = _t('Member.db_Password', 'Password');
1129         $labels['NumVisit'] = _t('Member.db_NumVisit', 'Number of Visits');
1130         $labels['LastVisited'] = _t('Member.db_LastVisited', 'Last Visited Date');
1131         $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', PR_MEDIUM, 'Password expiry date');
1132         $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', PR_MEDIUM, 'Security related date');
1133         $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1134         $labels['Title'] = _t('Member.TITLE', 'Full Name');
1135         $labels['Created'] = _t('Member.db_Created', 'Created');
1136         if($includerelations){
1137             $labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups', PR_MEDIUM, 'Security Groups this member belongs to');
1138         }
1139         return $labels;
1140     }
1141     
1142     /**
1143      * Users can view their own record.
1144      * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1145      * This is likely to be customized for social sites etc. with a looser permission model.
1146      */
1147     function canView($member = null) {
1148         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1149         
1150         // decorated access checks
1151         $results = $this->extend('canView', $member);
1152         if($results && is_array($results)) {
1153             if(!min($results)) return false;
1154             else return true;
1155         }
1156         
1157         // members can usually edit their own record
1158         if($member && $this->ID == $member->ID) return true;
1159         
1160         if(
1161             Permission::checkMember($member, 'ADMIN')
1162             || Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin')
1163         ) {
1164             return true;
1165         }
1166         
1167         return false;
1168     }
1169     
1170     /**
1171      * Users can edit their own record.
1172      * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1173      */
1174     function canEdit($member = null) {
1175         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1176         
1177         // decorated access checks
1178         $results = $this->extend('canEdit', $member);
1179         if($results && is_array($results)) {
1180             if(!min($results)) return false;
1181             else return true;
1182         }
1183         
1184         // No member found
1185         if(!($member && $member->exists())) return false;
1186         
1187         return $this->canView($member);
1188     }
1189     
1190     /**
1191      * Users can edit their own record.
1192      * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1193      */
1194     function canDelete($member = null) {
1195         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1196         
1197         // decorated access checks
1198         $results = $this->extend('canDelete', $member);
1199         if($results && is_array($results)) {
1200             if(!min($results)) return false;
1201             else return true;
1202         }
1203         
1204         // No member found
1205         if(!($member && $member->exists())) return false;
1206         
1207         return $this->canEdit($member);
1208     }
1209 
1210 
1211     /**
1212      * Validate this member object.
1213      */
1214     function validate() {
1215         $valid = parent::validate();
1216         
1217         if(!$this->ID || $this->isChanged('Password')) {
1218             if($this->Password && self::$password_validator) {
1219                 $valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1220             }
1221         }
1222 
1223         if((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
1224             if($this->SetPassword && self::$password_validator) {
1225                 $valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this));
1226             }
1227         }
1228         $this->extend('updateValidation', $valid);
1229         return $valid;
1230     }   
1231     
1232     /**
1233      * Change password. This will cause rehashing according to
1234      * the `PasswordEncryption` property.
1235      * 
1236      * @param String $password Cleartext password
1237      */
1238     function changePassword($password) {
1239         $this->Password = $password;
1240         $valid = $this->validate();
1241         
1242         if($valid->valid()) {
1243             $this->AutoLoginHash = null;
1244             $this->write();
1245         }
1246         
1247         return $valid;
1248     }
1249     
1250     /**
1251      * Tell this member that someone made a failed attempt at logging in as them.
1252      * This can be used to lock the user out temporarily if too many failed attempts are made.
1253      */
1254     function registerFailedLogin() {
1255         if(self::$lock_out_after_incorrect_logins) {
1256             // Keep a tally of the number of failed log-ins so that we can lock people out
1257             $this->FailedLoginCount = $this->FailedLoginCount + 1;
1258             $this->write();
1259     
1260             if($this->FailedLoginCount >= self::$lock_out_after_incorrect_logins) {
1261                 $this->LockedOutUntil = date('Y-m-d H:i:s', time() + 15*60);
1262                 $this->write();
1263             }
1264         }
1265     }
1266     
1267     /**
1268      * Get the HtmlEditorConfig for this user to be used in the CMS.
1269      * This is set by the group. If multiple configurations are set,
1270      * the one with the highest priority wins.
1271      * 
1272      * @return string
1273      */
1274     function getHtmlEditorConfigForCMS() {
1275         $currentName = '';
1276         $currentPriority = 0;
1277         
1278         foreach($this->Groups() as $group) {
1279             $configName = $group->HtmlEditorConfig;
1280             if($configName) {
1281                 $config = HtmlEditorConfig::get($group->HtmlEditorConfig);
1282                 if($config && $config->getOption('priority') > $currentPriority) {
1283                     $currentName = $configName;
1284                 }
1285             }
1286         }
1287         
1288         // If can't find a suitable editor, just default to cms
1289         return $currentName ? $currentName : 'cms';
1290     }
1291 
1292 }
1293 
1294 /**
1295  * Special kind of {@link ComponentSet} that has special methods for
1296  * manipulating a user's membership
1297  * @package sapphire
1298  * @subpackage security
1299  */
1300 class Member_GroupSet extends ComponentSet {
1301     /**
1302      * Control group membership with a number of checkboxes.
1303      *  - If the checkbox fields are present in $data, then the member will be
1304      *    added to the group with the same codename.
1305      *  - If the checkbox fields are *NOT* present in $data, then the member
1306      *    will be removed from the group with the same codename.
1307      *
1308      * @param array $checkboxes An array list of the checkbox fieldnames (only
1309      *                            values are used). E.g. array(0, 1, 2)
1310      * @param array $data The form data. Uually in the format array(0 => 2)
1311      *                    (just pass the checkbox data from your form)
1312      */
1313     function setByCheckboxes(array $checkboxes, array $data) {
1314         foreach($checkboxes as $checkbox) {
1315             if($data[$checkbox]) {
1316                 $add[] = $checkbox;
1317             } else {
1318                 $remove[] = $checkbox;
1319             }
1320         }
1321 
1322         if($add)
1323             $this->addManyByCodename($add);
1324 
1325         if($remove)
1326             $this->removeManyByCodename($remove);
1327     }
1328 
1329 
1330     /**
1331      * Allows you to set groups based on a CheckboxSetField
1332      *
1333      * Pass the form element from your post data directly to this method, and
1334      * it will update the groups and add and remove the member as appropriate.
1335      *
1336      * On the form setup:
1337      *
1338      * <code>
1339      * $fields->push(
1340      *   new CheckboxSetField(
1341      *     "NewsletterSubscriptions",
1342      *     "Receive email notification of events in ",
1343      *     $sourceitems = DataObject::get("NewsletterType")->toDropDownMap("GroupID","Title"),
1344      *     $selectedgroups = $member->Groups()->Map("ID","ID")
1345      *   )
1346      * );
1347      * </code>
1348      *
1349      * On the form handler:
1350      *
1351      * <code>
1352      * $groups = $member->Groups();
1353      * $checkboxfield = $form->Fields()->fieldByName("NewsletterSubscriptions");
1354      * $groups->setByCheckboxSetField($checkboxfield);
1355      * </code>
1356      *
1357      * @param CheckboxSetField $checkboxsetfield The CheckboxSetField (with
1358      *                                           data) from your form.
1359      */
1360     function setByCheckboxSetField(CheckboxSetField $checkboxsetfield) {
1361         // Get the values from the formfield.
1362         $values = $checkboxsetfield->Value();
1363         $sourceItems = $checkboxsetfield->getSource();
1364 
1365         if($sourceItems) {
1366             // If (some) values are present, add and remove as necessary.
1367             if($values) {
1368                 // update the groups based on the selections
1369                 foreach($sourceItems as $k => $item) {
1370                     if(in_array($k,$values)) {
1371                         $add[] = $k;
1372                     } else {
1373                         $remove[] = $k;
1374                     }
1375                 }
1376 
1377             // else we should be removing all from the necessary groups.
1378             } else {
1379                 $remove = array_keys($sourceItems);
1380             }
1381 
1382             if($add)
1383                 $this->addManyByGroupID($add);
1384 
1385             if($remove)
1386                 $this->RemoveManyByGroupID($remove);
1387 
1388         } else {
1389             USER_ERROR("Member::setByCheckboxSetField() - No source items could be found for checkboxsetfield " .
1390                                  $checkboxsetfield->Name(), E_USER_WARNING);
1391         }
1392     }
1393 
1394 
1395     /**
1396      * Adds this member to the groups based on the group IDs
1397      *
1398      * @param array $ids Group identifiers.
1399      */
1400     function addManyByGroupID($groupIds){
1401         $groups = $this->getGroupsFromIDs($groupIds);
1402         if($groups) {
1403             foreach($groups as $group) {
1404                 $this->add($group);
1405             }
1406         }
1407     }
1408 
1409 
1410     /**
1411      * Removes the member from many groups based on the group IDs
1412      *
1413      * @param array $ids Group identifiers.
1414      */
1415     function removeManyByGroupID($groupIds) {
1416         $groups = $this->getGroupsFromIDs($groupIds);
1417         if($groups) {
1418             foreach($groups as $group) {
1419                 $this->remove($group);
1420             }
1421         }
1422     }
1423 
1424 
1425     /**
1426      * Returns the groups from an array of group IDs
1427      *
1428      * @param array $ids Group identifiers.
1429      * @return mixed Returns the groups from the array of Group IDs.
1430      */
1431     function getGroupsFromIDs($ids){
1432         if($ids && count($ids) > 1) {
1433             return DataObject::get("Group", "\"ID\" IN (" . implode(",", $ids) . ")");
1434         } else {
1435             return DataObject::get_by_id("Group", $ids[0]);
1436         }
1437     }
1438 
1439 
1440     /**
1441      * Adds this member to the groups based on the group codenames
1442      *
1443      * @param array $codenames Group codenames
1444      */
1445     function addManyByCodename($codenames) {
1446         $groups = $this->codenamesToGroups($codenames);
1447         if($groups) {
1448             foreach($groups as $group){
1449                 $this->add($group);
1450             }
1451         }
1452     }
1453 
1454 
1455     /**
1456      * Removes this member from the groups based on the group codenames
1457      *
1458      * @param array $codenames Group codenames
1459      */
1460     function removeManyByCodename($codenames) {
1461         $groups = $this->codenamesToGroups($codenames);
1462         if($groups) {
1463             foreach($groups as $group) {
1464                 $this->remove($group);
1465             }
1466         }
1467     }
1468 
1469 
1470     /**
1471      * Helper function to return the appropriate groups via a codenames
1472      *
1473      * @param array $codenames Group codenames
1474      * @return array Returns the the appropriate groups.
1475      */
1476     protected function codenamesToGroups($codenames) {
1477         $list = "'" . implode("', '", $codenames) . "'";
1478         $output = DataObject::get("Group", "\"Code\" IN ($list)");
1479 
1480         // Some are missing - throw warnings
1481         if(!$output || ($output->Count() != sizeof($list))) {
1482             foreach($codenames as $codename)
1483                 $missing[$codename] = $codename;
1484 
1485             if($output) {
1486                 foreach($output as $record)
1487                     unset($missing[$record->Code]);
1488             }
1489 
1490             if($missing)
1491                 user_error("The following group-codes aren't matched to any groups: " .
1492                                      implode(", ", $missing) .
1493                                      ".  You probably need to link up the correct group codes in phpMyAdmin",
1494                                      E_USER_WARNING);
1495         }
1496 
1497         return $output;
1498     }
1499 }
1500 
1501 
1502 
1503 /**
1504  * Form for editing a member profile.
1505  * @package sapphire
1506  * @subpackage security
1507  */
1508 class Member_ProfileForm extends Form {
1509     
1510     function __construct($controller, $name, $member) {
1511         Requirements::clear();
1512         Requirements::css(CMS_DIR . '/css/typography.css');
1513         Requirements::css(CMS_DIR . '/css/cms_right.css');
1514         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/prototype/prototype.js");
1515         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/behaviour/behaviour.js");
1516         Requirements::javascript(SAPPHIRE_DIR . "/javascript/prototype_improvements.js");
1517         Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/scriptaculous.js");
1518         Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/controls.js");
1519         Requirements::javascript(SAPPHIRE_DIR . "/javascript/layout_helpers.js");
1520         Requirements::css(SAPPHIRE_DIR . "/css/Form.css");
1521         
1522         Requirements::css(SAPPHIRE_DIR . "/css/MemberProfileForm.css");
1523         
1524         
1525         $fields = singleton('Member')->getCMSFields();
1526         $fields->push(new HiddenField('ID','ID',$member->ID));
1527 
1528         $actions = new FieldSet(
1529             new FormAction('dosave', _t('CMSMain.SAVE', 'Save'))
1530         );
1531         
1532         $validator = new Member_Validator();
1533         
1534         parent::__construct($controller, $name, $fields, $actions, $validator);
1535         
1536         $this->loadDataFrom($member);
1537     }
1538     
1539     function dosave($data, $form) {
1540         // don't allow ommitting or changing the ID
1541         if(!isset($data['ID']) || $data['ID'] != Member::currentUserID()) {
1542             return Director::redirectBack();
1543         }
1544         
1545         $SQL_data = Convert::raw2sql($data);
1546         $member = DataObject::get_by_id("Member", $SQL_data['ID']);
1547         
1548         if($SQL_data['Locale'] != $member->Locale) {
1549             $form->addErrorMessage("Generic", _t('Member.REFRESHLANG'),"good");
1550         }
1551         
1552         $form->saveInto($member);
1553         $member->write();
1554         
1555         $closeLink = sprintf(
1556             '<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
1557             _t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
1558         );
1559         $message = _t('Member.PROFILESAVESUCCESS', 'Successfully saved.') . ' ' . $closeLink;
1560         $form->sessionMessage($message, 'good');
1561         
1562         Director::redirectBack();
1563     }
1564 }
1565 
1566 /**
1567  * Class used as template to send an email to new members
1568  * @package sapphire
1569  * @subpackage security
1570  */
1571 class Member_SignupEmail extends Email {
1572     protected $from = '';  // setting a blank from address uses the site's default administrator email
1573     protected $subject = '';
1574     protected $body = '';
1575 
1576     function __construct() {
1577         parent::__construct();
1578         $this->subject = _t('Member.EMAILSIGNUPSUBJECT', "Thanks for signing up");
1579         $this->body = '
1580             <h1>' . _t('Member.GREETING','Welcome') . ', $FirstName.</h1>
1581             <p>' . _t('Member.EMAILSIGNUPINTRO1','Thanks for signing up to become a new member, your details are listed below for future reference.') . '</p>
1582 
1583             <p>' . _t('Member.EMAILSIGNUPINTRO2','You can login to the website using the credentials listed below')  . ':
1584                 <ul>
1585                     <li><strong>' . _t('Member.EMAIL') . '</strong>$Email</li>
1586                     <li><strong>' . _t('Member.PASSWORD') . ':</strong>$Password</li>
1587                 </ul>
1588             </p>
1589 
1590             <h3>' . _t('Member.CONTACTINFO','Contact Information') . '</h3>
1591             <ul>
1592                 <li><strong>' . _t('Member.NAME','Name')  . ':</strong> $FirstName $Surname</li>
1593                 <% if Phone %>
1594                     <li><strong>' . _t('Member.PHONE','Phone') . ':</strong> $Phone</li>
1595                 <% end_if %>
1596 
1597                 <% if Mobile %>
1598                     <li><strong>' . _t('Member.MOBILE','Mobile') . ':</strong> $Mobile</li>
1599                 <% end_if %>
1600 
1601                 <li><strong>' . _t('Member.ADDRESS','Address') . ':</strong>
1602                 <br/>
1603                 $Number $Street $StreetType<br/>
1604                 $Suburb<br/>
1605                 $City $Postcode
1606                 </li>
1607 
1608             </ul>
1609         ';
1610     }
1611 }
1612 
1613 
1614 
1615 /**
1616  * Class used as template to send an email saying that the password has been
1617  * changed
1618  * @package sapphire
1619  * @subpackage security
1620  */
1621 class Member_ChangePasswordEmail extends Email {
1622     protected $from = '';   // setting a blank from address uses the site's default administrator email
1623     protected $subject = '';
1624     protected $ss_template = 'ChangePasswordEmail';
1625     
1626     function __construct() {
1627         parent::__construct();
1628         $this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", PR_MEDIUM, 'Email subject');
1629     }
1630 }
1631 
1632 
1633 
1634 /**
1635  * Class used as template to send the forgot password email
1636  * @package sapphire
1637  * @subpackage security
1638  */
1639 class Member_ForgotPasswordEmail extends Email {
1640     protected $from = '';  // setting a blank from address uses the site's default administrator email
1641     protected $subject = '';
1642     protected $ss_template = 'ForgotPasswordEmail';
1643     
1644     function __construct() {
1645         parent::__construct();
1646         $this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", PR_MEDIUM, 'Email subject');
1647     }
1648 }
1649 
1650 /**
1651  * Member Validator
1652  * @package sapphire
1653  * @subpackage security
1654  */
1655 class Member_Validator extends RequiredFields {
1656 
1657     protected $customRequired = array('FirstName', 'Email'); //, 'Password');
1658 
1659 
1660     /**
1661      * Constructor
1662      */
1663     public function __construct() {
1664         $required = func_get_args();
1665         if(isset($required[0]) && is_array($required[0])) {
1666             $required = $required[0];
1667         }
1668         $required = array_merge($required, $this->customRequired);
1669 
1670         parent::__construct($required);
1671     }
1672 
1673 
1674     /**
1675      * Check if the submitted member data is valid (server-side)
1676      *
1677      * Check if a member with that email doesn't already exist, or if it does
1678      * that it is this member.
1679      *
1680      * @param array $data Submitted data
1681      * @return bool Returns TRUE if the submitted data is valid, otherwise
1682      *              FALSE.
1683      */
1684     function php($data) {
1685         $valid = parent::php($data);
1686         
1687         $identifierField = Member::get_unique_identifier_field();
1688         
1689         $SQL_identifierField = Convert::raw2sql($data[$identifierField]);
1690         $member = DataObject::get_one('Member', "\"$identifierField\" = '{$SQL_identifierField}'");
1691 
1692         // if we are in a complex table field popup, use ctf[childID], else use ID
1693         if(isset($_REQUEST['ctf']['childID'])) {
1694             $id = $_REQUEST['ctf']['childID'];
1695         } elseif(isset($_REQUEST['ctf']['ID'])) {
1696             $id = $_REQUEST['ctf']['ID'];
1697         } elseif(isset($_REQUEST['ID'])) {
1698             $id = $_REQUEST['ID'];
1699         } else {
1700             $id = null;
1701         }
1702 
1703         if(is_object($member) && (($id && $member->ID != $id)||(!$id && $member->ID))) {
1704             $uniqueField = $this->form->dataFieldByName($identifierField);
1705             $this->validationError(
1706                 $uniqueField->id(),
1707                 sprintf(
1708                     _t(
1709                         'Member.VALIDATIONMEMBEREXISTS',
1710                         'A member already exists with the same %s'
1711                     ),
1712                     strtolower(singleton('Member')->fieldLabel($identifierField))
1713                 ),
1714                 'required'
1715             );
1716             $valid = false;
1717         }
1718 
1719         // Execute the validators on the extensions
1720         if($this->extension_instances) {
1721             foreach($this->extension_instances as $extension) {
1722                 if(method_exists($extension, 'hasMethod') && $extension->hasMethod('updatePHP')) {
1723                     $valid &= $extension->updatePHP($data, $this->form);
1724                 }
1725             }
1726         }
1727 
1728         return $valid;
1729     }
1730 
1731 
1732     /**
1733      * Check if the submitted member data is valid (client-side)
1734      *
1735      * @param array $data Submitted data
1736      * @return bool Returns TRUE if the submitted data is valid, otherwise
1737      *              FALSE.
1738      */
1739     function javascript() {
1740         $js = parent::javascript();
1741 
1742         // Execute the validators on the extensions
1743         if($this->extension_instances) {
1744             foreach($this->extension_instances as $extension) {
1745                 if(method_exists($extension, 'hasMethod') && $extension->hasMethod('updateJavascript')) {
1746                     $extension->updateJavascript($js, $this->form);
1747                 }
1748             }
1749         }
1750 
1751         return $js;
1752     }
1753 
1754 }
1755 ?>
[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.2 API Docs API documentation generated by ApiGen 2.8.0