class_user.inc 16.5 KB
Newer Older
1 2 3
<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
4
  Copyright (C) 2013-2018  FusionDirectory
5 6 7 8 9 10 11 12 13 14 15 16 17

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
18
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
19 20
*/

21 22 23 24
class PostalAddressAttribute extends TextAreaAttribute
{
  function inputValue ($ldapValue)
  {
25
    return str_replace(
26 27
      ['$',  '\24','\5C'],
      ["\n", '$',  '\\'],
28 29
      $ldapValue
    );
30 31 32 33
  }

  function computeLdapValue ()
  {
34
    return str_replace(
35
      ["\r\n", "\n", "\r"],
36 37
      '$',
      str_replace(
38 39
        ['\\', '$'],
        ['\5C','\24'],
40 41 42
        $this->getValue()
      )
    );
43 44 45
  }
}

46
class user extends simplePlugin
47
{
48 49
  private $was_locked;

50
  static function plInfo (): array
51
  {
52
    return [
53 54
      'plShortName'   => _('User'),
      'plDescription' => _('User account information'),
55 56
      'plIcon'        => 'geticon.php?context=applications&icon=user-info&size=48',
      'plSmallIcon'   => 'geticon.php?context=applications&icon=user-info&size=16',
57
      'plSelfModify'  => TRUE,
58
      'plObjectClass' => ['inetOrgPerson','organizationalPerson','person'],
59
      'plFilter'      => '(objectClass=inetOrgPerson)',
60
      'plObjectType'  => ['user' => [
61
        'name'        => _('User'),
62
        'description' => _('User account'),
63 64
        'mainAttr'    => 'uid',
        'nameAttr'    => 'cn',
65
        'icon'        => 'geticon.php?context=types&icon=user&size=16',
66
        'ou'          => get_ou('userRDN'),
67 68 69 70 71
      ]],
      'plForeignKeys'  => [
        'manager' => ['user','dn','manager=%oldvalue%','*']
      ],
      'plSearchAttrs' => ['uid','description'],
72

73 74
      'plProvidedAcls' => array_merge(
        parent::generatePlProvidedAcls(static::getAttributesInfo()),
75
        ['userLock' => _('User lock status')]
76
      )
77
    ];
78
  }
79

80
  static function getAttributesInfo (): array
81
  {
82
    global $config;
83 84
    $languages = Language::getList(TRUE);
    asort($languages);
85 86 87
    $languages = array_merge(['' => ''], $languages);
    $attributesInfo = [
      'perso' => [
88
        'name'  => _('Personal information'),
89
        'icon'  => 'geticon.php?context=types&icon=user&size=16',
90
        'attrs' => [
91
          new HiddenAttribute('cn'),
92
          new StringAttribute(
93
            _('Last name'), _('Last name of this user'),
94
            'sn', TRUE,
95
            '', '', '/^[^,+"?()=<>;\\\\]+$/'
96
          ),
97
          new StringAttribute(
98 99
            _('First name'), _('First name of this user'),
            'givenName', TRUE,
100
            '', '', '/^[^,+"?()=<>;\\\\]+$/'
101
          ),
102
          new TextAreaAttribute(
103 104 105
            _('Description'), _('Short description of the user'),
            'description', FALSE
          ),
106
          new ImageAttribute(
107 108 109 110
            _('Picture'), _('The avatar for this user'),
            'jpegPhoto', FALSE,
            150, 200, 'jpeg'
          ),
111 112 113
        ]
      ],
      'contact' => [
114
        'name'  => _('Organizational contact information'),
115
        'icon'  => 'geticon.php?context=types&icon=contact&size=16',
116
        'attrs' => [
117
          new StringAttribute(
118 119 120
            _('Location'), _('Location'),
            'l', FALSE
          ),
121
          new StringAttribute(
122 123 124
            _('State'), _('State'),
            'st', FALSE
          ),
125
          new PostalAddressAttribute(
126 127 128
            _('Address'), _('Business postal address'),
            'postalAddress', FALSE
          ),
129
          new StringAttribute(
130 131 132
            _('Room No.'), _('Room number'),
            'roomNumber', FALSE
          ),
133
          new PhoneNumberButtonAttribute(
134
            _('Phone'), _('Business phone number'),
135 136 137
            'telephoneNumber', FALSE,
            '',
            'phone'
138
          ),
139
          new PhoneNumberButtonAttribute(
140
            _('Mobile'), _('Business mobile number'),
141 142 143
            'mobile', FALSE,
            '',
            'mobile'
144
          ),
145
          new PhoneNumberAttribute(
146 147 148
            _('Pager'), _('Business pager number'),
            'pager', FALSE
          ),
149
          new PhoneNumberAttribute(
150 151 152
            _('Fax'), _('Business fax number'),
            'facsimileTelephoneNumber', FALSE
          ),
153
          new URLAttribute(
154 155 156
            _('Homepage'), _('Personal homepage'),
            'labeledURI', FALSE
          ),
157 158 159
        ]
      ],
      'account' => [
160 161
        'name'  => _('Account information'),
        'icon'  => 'geticon.php?context=applications&icon=ldap&size=16',
162
        'attrs' => [
163 164
          new BaseSelectorAttribute(get_ou("userRDN")),
          new UidAttribute(
165 166 167
            _('Login'), _('Login of this user'),
            'uid', TRUE
          ),
168
          new SelectAttribute(
169 170 171 172 173 174 175
            _('Preferred language'), _('Preferred language'),
            'preferredLanguage', FALSE,
            array_keys($languages), '', array_values($languages)
          ),
          new UserPasswordAttribute(
            _('Password'), _('Password of the user'),
            'userPassword', FALSE
176
          ),
177 178 179
        ]
      ],
      'homecontact' => [
180 181
        'name'  => _('Personal contact information'),
        'icon'  => 'geticon.php?context=types&icon=contact&size=16',
182
        'attrs' => [
183
          new StringAttribute(
184 185 186
            _('Display name'), _('Name this user should appear as. Used by Exchange.'),
            'displayName', FALSE
          ),
187
          new PostalAddressAttribute(
188 189 190
            _('Home address'), _('Home postal address'),
            'homePostalAddress', FALSE
          ),
191
          new PhoneNumberAttribute(
192 193 194
            _('Private phone'), _('Home phone number'),
            'homePhone', FALSE
          ),
195 196 197
        ]
      ],
      'organization' => [
198
        'name'  => _('Organizational information'),
199
        'icon'  => 'geticon.php?context=places&icon=folder&size=16',
200
        'attrs' => [
201 202
          new SetAttribute(
            new StringAttribute(
203 204 205 206
              _('Title'), _('Title of a person in their organizational context. Each title is one value of this multi-valued attribute'),
              'title', FALSE
            )
          ),
207
          new StringAttribute(
208 209 210
            _('Organization'), _('Organization'),
            'o', FALSE
          ),
211
          new StringAttribute(
212
            _('Unit'), _('Organizational unit this user belongs to'),
213 214
            'ou', FALSE
          ),
215
          new StringAttribute(
216 217 218
            _('Department No.'), _('Department number'),
            'departmentNumber', FALSE
          ),
219
          new StringAttribute(
220 221 222
            _('Employee No.'), _('Employee number'),
            'employeeNumber', FALSE
          ),
223
          new StringAttribute(
224 225 226
            _('Employee type'), _('Employee type'),
            'employeeType', FALSE
          ),
227
          new UserAttribute(
228 229 230
            _('Manager'), _('Manager'),
            'manager', FALSE
          ),
231 232 233
        ]
      ],
    ];
234 235
    if ($config->get_cfg_value('SplitPostalAddress') == 'TRUE') {
      $attributesInfo['contact']['attrs'][2]->setVisible(FALSE);
236
      array_splice($attributesInfo['contact']['attrs'], 3, 0, [
237
        new StringAttribute(
238 239 240
          _('Street'), _('Street part of the address'),
          'street', FALSE
        ),
241
        new StringAttribute(
242 243 244
          _('Post office box'), _('Post office box'),
          'postOfficeBox', FALSE
        ),
245
        new IntAttribute(
246 247 248 249
          _('Postal code'), _('Postal code'),
          'postalCode', FALSE,
          0, FALSE
        ),
250
      ]);
251 252
    }
    return $attributesInfo;
253
  }
254

255
  function __construct ($dn = NULL, $object = NULL, $parent = NULL, $mainTab = FALSE)
256
  {
257
    parent::__construct($dn, $object, $parent, $mainTab);
258

259
    $this->attributesAccess['uid']->setUnique('whole');
260
    $this->attributesAccess['uid']->setAutocomplete(FALSE);
261
    $this->attributesAccess['uid']->setDisabled($this->initially_was_account && !$this->is_template);
262

263
    $filename = './plugins/users/images/default.jpg';
264 265
    $fd       = fopen($filename, 'rb');
    $this->attributesAccess['jpegPhoto']->setPlaceholder(fread($fd, filesize($filename)));
266 267

    $this->was_locked = $this->attributesAccess['userPassword']->isLocked();
268
  }
269

270
  function resetCopyInfos ()
271
  {
272
    parent::resetCopyInfos();
273
    $this->attributesAccess['uid']->setDisabled($this->initially_was_account && !$this->is_template);
274 275
  }

276
  private function update_cn ()
277
  {
278 279
    global $config;
    $pattern  = $config->get_cfg_value('CnPattern', '%givenName% %sn%');
280 281 282 283 284
    $this->attributesAccess['cn']->setValue($this->applyPattern($pattern));
  }

  private function applyPattern ($pattern)
  {
285
    $fields   = templateHandling::listFields($pattern);
286
    $attrs    = [];
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
    foreach ($fields as $field) {
      if (in_array($field, $this->attributes)) {
        $attrs[$field] = $this->$field;
        continue;
      }

      if (isset($this->parent->by_object)) {
        foreach ($this->parent->by_object as $object) {
          if (in_array($field, $object->attributes)) {
            $attrs[$field] = $object->$field;
            continue 2;
          }
        }
      }
      trigger_error('Could not find field '.$field.' in any tab!');
    }
303 304

    return templateHandling::parseString($pattern, $attrs);
305
  }
306

307
  function compute_dn (): string
308
  {
309
    global $config;
310

311
    if ($this->is_template) {
312
      return 'cn='.ldap_escape_dn($this->_template_cn).',ou=templates,'.get_ou('userRDN').$this->base;
313 314
    }

315
    $this->update_cn();
316
    $attribute = $config->get_cfg_value('accountPrimaryAttribute', 'uid');
317

318
    return $this->create_unique_dn($attribute, get_ou('userRDN').$this->base);
319
  }
320

321
  function execute (): string
322 323 324 325 326 327
  {
    $smarty = get_smarty();
    $smarty->append('css_files', 'plugins/users/style/user_tab.css');
    return parent::execute();
  }

328
  protected function shouldSave (): bool
329 330 331 332 333 334 335 336
  {
    if ($this->attributesAccess['userPassword']->getClear() != '') {
      /* There may be hooks using this even if LDAP object is not modified */
      return TRUE;
    }
    return parent::shouldSave();
  }

337
  protected function prepare_save (): array
338 339 340 341 342 343 344 345 346 347 348 349
  {
    global $config;
    if ($config->get_cfg_value('SplitPostalAddress') == 'TRUE') {
      $pattern = $config->get_cfg_value('PostalAddressPattern', '');
      if (!empty($pattern)) {
        $this->postalAddress = $this->applyPattern($this->attributesAccess['postalAddress']->inputValue($pattern));
      }
    }

    return parent::prepare_save();
  }

350
  function ldap_save (): array
351
  {
352 353 354 355 356
    $errors = parent::ldap_save();

    if (!empty($errors)) {
      return $errors;
    }
357 358 359

    if (!$this->is_template && $this->was_locked && $this->attributesAccess['userPassword']->hasChanged()) {
      $methods  = passwordMethod::get_available_methods();
360 361
      $pmethod  = new $methods[$this->attributesAccess['userPassword']->getMethod()]($this->dn);
      $pmethod->lock_account($this->dn);
362
    }
363 364

    return $errors;
365 366
  }

367
  function post_save ()
368
  {
369
    global $ui;
370

371 372 373 374 375
    /* Update current locale settings, if we have edited ourselves */
    if (isset($this->attrs['preferredLanguage']) && ($this->dn == $ui->dn)) {
      $ui->language = $this->preferredLanguage;
      session::set('ui', $ui);
      session::set('Last_init_lang', 'update');
376
    }
377 378

    return parent::post_save();
379
  }
380

381
  function adapt_from_template (array $attrs, array $skip = [])
382 383 384 385
  {
    if ($this->uid != '') {
      $skip[] = 'uid';
    }
386
    parent::adapt_from_template($attrs, array_merge($skip, ['userPassword']));
387
    if (isset($this->attrs['userPassword']) && !in_array('userPassword', $skip)) {
388
      $this->userPassword = $this->attributesAccess['userPassword']->readUserPasswordValues($this->attrs['userPassword'][0], TRUE);
389
    }
390 391
  }

392
  function fillHookAttrs (array &$addAttrs)
393
  {
394
    parent::fillHookAttrs($addAttrs);
395
    $addAttrs['passwordMethod'] = $this->attributesAccess['userPassword']->getMethod();
396
    $addAttrs['userLocked']     = (int)($this->attributesAccess['userPassword']->isLocked());
397 398
    $addAttrs['passwordClear']  = $this->attributesAccess['userPassword']->getClear();
  }
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415

  static function reportPasswordProblems ($user, $new_password, $repeated_password, $current_password = NULL)
  {
    global $config, $ui;

    /* Should we check different characters in new password */
    $check_differ = ($config->get_cfg_value('passwordMinDiffer') != '');
    $differ       = $config->get_cfg_value('passwordMinDiffer', 0);
    if ($current_password === NULL) {
      $check_differ = FALSE;
    }

    /* Enable length check ? */
    $check_length = ($config->get_cfg_value('passwordMinLength') != '');
    $length       = $config->get_cfg_value('passwordMinLength', 0);

    $ldap = $config->get_ldap_link();
416
    $ldap->cat($user, ['pwdPolicySubentry', 'pwdHistory', 'pwdChangedTime', 'userPassword']);
417 418 419 420 421 422 423 424 425 426 427
    $attrs = $ldap->fetch();
    $ppolicydn = '';
    if (isset($attrs['pwdPolicySubentry'][0])) {
      $ppolicydn = $attrs['pwdPolicySubentry'][0];
    } else {
      $ppolicydn = $config->get_cfg_value('ppolicyDefaultCn', '');
      if (!empty($ppolicydn)) {
        $ppolicydn = 'cn='.$ppolicydn.','.get_ou('ppolicyRDN').$config->current['BASE'];
      }
    }
    if (!empty($ppolicydn)) {
428
      $ldap->cat($ppolicydn, ['pwdAllowUserChange', 'pwdMinLength', 'pwdMinAge', 'pwdSafeModify']);
429 430 431 432 433 434 435 436 437 438 439 440
      $policy = $ldap->fetch();
      if (!$policy) {
        return sprintf(_('Ppolicy "%s" could not be found in the LDAP!'), $ppolicydn);
      }
      if (isset($policy['pwdAllowUserChange'][0]) && ($policy['pwdAllowUserChange'][0] == 'FALSE') && ($ui->dn == $user)) {
        return _('You are not allowed to change your own password');
      }
      if (isset($policy['pwdMinLength'][0])) {
        $check_length = TRUE;
        $length       = $policy['pwdMinLength'][0];
      }
      if (isset($policy['pwdMinAge'][0]) && isset($attrs['pwdChangedTime'][0])) {
441 442
        $date = LdapGeneralizedTime::fromString($attrs['pwdChangedTime'][0]);
        $date->setTimezone(timezone::utc());
443 444 445 446 447
        $now  = new DateTime('now', timezone::utc());
        if ($now->getTimeStamp() < $date->getTimeStamp() + $policy['pwdMinAge'][0]) {
          return sprintf(_('You must wait %d seconds before changing your password again'), $policy['pwdMinAge'][0] - ($now->getTimeStamp() - $date->getTimeStamp()));
        }
      }
448 449
      if (isset($policy['pwdSafeModify'][0]) && ($policy['pwdSafeModify'][0] == 'FALSE') && empty($current_password)) {
        $current_password = NULL;
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
      }
      if (isset($attrs['pwdHistory'][0])) {
        unset($attrs['pwdHistory']['count']);
        foreach ($attrs['pwdHistory'] as $pwdHistory) {
          $pwdHistory = explode('#', $pwdHistory, 4);
          $method = passwordMethod::get_method($pwdHistory[3], $user);
          if (($method !== NULL) && $method->checkPassword($new_password, $pwdHistory[3])) {
            return _('Password is in history of old passwords');
          }
        }
      }
      if (($current_password !== NULL) && ($current_password == $new_password)) {
        return _('Password is not being changed from existing value');
      } elseif (isset($attrs['userPassword'][0])) {
        $method = passwordMethod::get_method($attrs['userPassword'][0], $user);
        if (($method !== NULL) && $method->checkPassword($new_password, $attrs['userPassword'][0])) {
          return _('Password is not being changed from existing value');
        }
468 469 470 471 472 473
      }
    }

    // Perform FusionDirectory password policy checks
    if (($current_password !== NULL) && empty($current_password)) {
      return _('You need to specify your current password in order to proceed.');
474 475 476 477
    } elseif ($new_password != $repeated_password) {
      return _('The passwords you\'ve entered as "New password" and "Repeated new password" do not match.');
    } elseif ($new_password == '') {
      return msgPool::required(_('New password'));
478
    } elseif ($check_differ && (mb_substr($current_password, 0, $differ) == mb_substr($new_password, 0, $differ))) {
479
      return _('The password used as new and current are too similar.');
480
    } elseif ($check_length && (mb_strlen($new_password) < $length)) {
Côme Chilliet's avatar
Côme Chilliet committed
481
      return _('The password used as new is too short.');
482 483 484 485 486 487
    } elseif (!passwordMethod::is_harmless($new_password)) {
      return _('The password contains possibly problematic Unicode characters!');
    }

    return FALSE;
  }
488
}