From 66a1b435659bb9795d1b1802f0776460dc6ca8af Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sun, 15 Oct 2017 10:22:19 +0200 Subject: [PATCH 1/8] expiration status --- lam/lib/modules/shadowAccount.inc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lam/lib/modules/shadowAccount.inc b/lam/lib/modules/shadowAccount.inc index 4e539546..49107aaf 100644 --- a/lam/lib/modules/shadowAccount.inc +++ b/lam/lib/modules/shadowAccount.inc @@ -771,6 +771,22 @@ class shadowAccount extends baseModule implements passwordService { ); } + /** + * Returns if the given account is expired. + * + * @param array $attrs LDAP attributes + * @return bool expired + */ + public static function isAccountExpired($attrs) { + $attrs = array_change_key_case($attrs, CASE_LOWER); + if (empty($attrs['shadowexpire'][0])) { + return false; + } + $time = new DateTime('@' . $attrs['shadowexpire'][0] * 24 * 3600, new DateTimeZone('UTC')); + $now = new DateTime(null, getTimeZone()); + return ($time > $now); + } + } if (interface_exists('\LAM\JOB\Job', false)) { From e7898c43260d17c8441eb67ccff431f55c894ba5 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 16 Oct 2017 19:50:44 +0200 Subject: [PATCH 2/8] removed obsolete mt_srand --- lam/lib/config.inc | 2 -- 1 file changed, 2 deletions(-) diff --git a/lam/lib/config.inc b/lam/lib/config.inc index 7448f7aa..06863229 100644 --- a/lam/lib/config.inc +++ b/lam/lib/config.inc @@ -1090,7 +1090,6 @@ class LAMConfig { */ public function set_Passwd($value) { if (is_string($value)) { - mt_srand((microtime() * 1000000)); $rand = getRandomNumber(); $salt0 = substr(pack("h*", md5($rand)), 0, 8); $salt = substr(pack("H*", sha1($salt0 . $value)), 0, 4); @@ -2410,7 +2409,6 @@ class LAMCfgMain { * @param String $password new password */ public function setPassword($password) { - mt_srand((microtime() * 1000000)); $rand = getRandomNumber(); $salt0 = substr(pack("h*", md5($rand)), 0, 8); $salt = substr(pack("H*", sha1($salt0 . $password)), 0, 4); From e60aaf1a77312661d99d8d0184d77d2e6c9fd10e Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 16 Oct 2017 19:51:27 +0200 Subject: [PATCH 3/8] show expiration status --- lam/graphics/expired.png | Bin 0 -> 3648 bytes lam/lib/modules/shadowAccount.inc | 4 +- lam/lib/types/user.inc | 44 ++++++++++++++++++-- lam/tests/lib/modules/shadowAccountTest.php | 43 +++++++++++++++++-- 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 lam/graphics/expired.png diff --git a/lam/graphics/expired.png b/lam/graphics/expired.png new file mode 100644 index 0000000000000000000000000000000000000000..f6cbf8914bbbc48d5c01accfbbf94f41d81313d8 GIT binary patch literal 3648 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 $now); + return ($time < $now); } } diff --git a/lam/lib/types/user.inc b/lam/lib/types/user.inc index 9ca5c3c3..ede0a5d8 100644 --- a/lam/lib/types/user.inc +++ b/lam/lib/types/user.inc @@ -346,7 +346,25 @@ class user extends baseType { if ($isEditable) { $onClick = 'onclick="showConfirmationDialog(\'' . _('Change account status') . '\', \'' . _('Ok') . '\', \'' . _('Cancel') . '\', \'lam_accountStatusDialog\', \'inputForm\', \'lam_accountStatusResult\');"'; } - return $dialogDiv . 'status   '; + $dialogDiv .= 'status   '; + // expiration status + $expiredLabels = array(); + $shadowModule = $container->getAccountModule('shadowAccount'); + if ($shadowModule != null) { + $shadowAttrs = $shadowModule->getAttributes(); + if (shadowAccount::isAccountExpired($shadowAttrs)) { + $expiredLabels[] = _('Shadow'); + } + } + if (!empty($expiredLabels)) { + $expiredTip = ''; + foreach ($expiredLabels as $label) { + $expiredTip .= ''; + } + $expiredTip .= '
' . $label . '
'; + $dialogDiv .= 'expired   '; + } + return $dialogDiv; } /** @@ -899,6 +917,7 @@ class lamUserList extends lamList { $attrs[] = 'lockoutTime'; $attrs[] = 'nsAccountLock'; $attrs[] = 'accountUnlockTime'; + $attrs[] = 'shadowExpire'; $attrs[] = 'objectClass'; } return $attrs; @@ -978,16 +997,25 @@ class lamUserList extends lamList { && (!$sambaAvailable || $sambaLocked) && (!$ppolicyAvailable || $ppolicyLocked) && (!$windowsAvailable || $windowsLocked); + $shadowExpired = shadowAccount::isAccountExpired($attrs); + $expired = $shadowExpired; $icon = 'unlocked.png'; - if ($fullyLocked) { + if ($expired) { + $icon = 'expired.png'; + } + elseif ($fullyLocked) { $icon = 'lock.png'; } elseif ($partiallyLocked) { $icon = 'partiallyLocked.png'; } // print icon and detail tooltips - if ($unixAvailable || $sambaAvailable || $ppolicyAvailable || $windowsAvailable || $is389dsDeactivated) { + if ($unixAvailable || $sambaAvailable || $ppolicyAvailable || $windowsAvailable || $is389dsDeactivated || $expired) { $tipContent = ''; + // Shadow expired + if ($shadowExpired) { + $tipContent .= ''; + } // Unix if ($unixAvailable) { $unixIcon = 'unlocked.png'; @@ -1049,6 +1077,16 @@ class lamUserList extends lamList { return (isset($attrs['objectclass']) && in_array_ignore_case('posixAccount', $attrs['objectclass']) && isset($attrs['userpassword'][0])); } + /** + * Returns if the Shadow part exists. + * + * @param array $attrs LDAP attributes + * @return boolean Shadow part exists + */ + public static function isShadowAvailable(&$attrs) { + return (isset($attrs['objectclass']) && in_array_ignore_case('shadowAccount', $attrs['objectclass'])); + } + /** * Returns if the Unix part is locked. * diff --git a/lam/tests/lib/modules/shadowAccountTest.php b/lam/tests/lib/modules/shadowAccountTest.php index bfcacd30..f46a0f3c 100644 --- a/lam/tests/lib/modules/shadowAccountTest.php +++ b/lam/tests/lib/modules/shadowAccountTest.php @@ -21,13 +21,50 @@ */ -if (is_readable('lam/lib/passwordExpirationJob.inc')) { - include_once 'lam/lib/baseModule.inc'; include_once 'lam/lib/modules.inc'; - include_once 'lam/lib/passwordExpirationJob.inc'; + if (is_readable('lam/lib/passwordExpirationJob.inc')) { + include_once 'lam/lib/passwordExpirationJob.inc'; + } include_once 'lam/lib/modules/shadowAccount.inc'; + /** + * Checks the shadowAccount class. + * + * @author Roland Gruber + */ + class ShadowAccountTest extends PHPUnit_Framework_TestCase { + + public function test_isAccountExpired_noAttr() { + $attrs = array('objectClass' => array('shadowAccount')); + + $this->assertFalse(shadowAccount::isAccountExpired($attrs)); + } + + public function test_isAccountExpired_notExpired() { + $expire = intval(time() / (24*3600)) + 10000; + $attrs = array( + 'objectClass' => array('shadowAccount'), + 'sHadoweXpirE' => array(0 => $expire) + ); + + $this->assertFalse(shadowAccount::isAccountExpired($attrs)); + } + + public function test_isAccountExpired_expired() { + $expire = intval(time() / (24*3600)) - 10000; + $attrs = array( + 'objectClass' => array('shadowAccount'), + 'sHadoweXpirE' => array(0 => $expire) + ); + + $this->assertTrue(shadowAccount::isAccountExpired($attrs)); + } + + } + +if (is_readable('lam/lib/passwordExpirationJob.inc')) { + /** * Checks the shadow expire job. * From 7e450ebdfad73d68d30176bca880dfba7db0a7ae Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 16 Oct 2017 19:54:44 +0200 Subject: [PATCH 4/8] changed label --- lam/lib/types/user.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lam/lib/types/user.inc b/lam/lib/types/user.inc index ede0a5d8..792eed2a 100644 --- a/lam/lib/types/user.inc +++ b/lam/lib/types/user.inc @@ -353,7 +353,7 @@ class user extends baseType { if ($shadowModule != null) { $shadowAttrs = $shadowModule->getAttributes(); if (shadowAccount::isAccountExpired($shadowAttrs)) { - $expiredLabels[] = _('Shadow'); + $expiredLabels[] = _('Shadow') . ': ' . _('Account expiration'); } } if (!empty($expiredLabels)) { @@ -1014,7 +1014,7 @@ class lamUserList extends lamList { $tipContent = '
' . _('Shadow') . '  
'; // Shadow expired if ($shadowExpired) { - $tipContent .= ''; + $tipContent .= ''; } // Unix if ($unixAvailable) { From f1fc0c1fba82f82d805e84397864248819329d9c Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 17 Oct 2017 17:22:22 +0200 Subject: [PATCH 5/8] added expired to status selction --- lam/lib/types/user.inc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lam/lib/types/user.inc b/lam/lib/types/user.inc index 792eed2a..02b04cba 100644 --- a/lam/lib/types/user.inc +++ b/lam/lib/types/user.inc @@ -624,6 +624,8 @@ class lamUserList extends lamList { /** virtual attribute name for account status column */ const ATTR_ACCOUNT_STATUS = 'lam_virtual_account_status'; + /** filter value for expired accounts */ + const FILTER_EXPIRED = 1; /** filter value for locked accounts */ const FILTER_LOCKED = 2; /** filter value for partially locked accounts */ @@ -872,7 +874,8 @@ class lamUserList extends lamList { '' => '', _('Unlocked') => self::FILTER_UNLOCKED, _('Partially locked') => self::FILTER_SEMILOCKED, - _('Locked') => self::FILTER_LOCKED + _('Locked') => self::FILTER_LOCKED, + _('Expired') => self::FILTER_EXPIRED, ); $filterInput = new htmlSelect('filter' . strtolower($attrName), $filterOptions, array($value)); $filterInput->setCSSClasses(array($this->type->getScope() . '-dark')); @@ -950,8 +953,13 @@ class lamUserList extends lamList { || ($sambaAvailable && !$sambaLocked) || ($ppolicyAvailable && !$ppolicyLocked) || ($windowsAvailable && !$windowsLocked); + $shadowExpired = shadowAccount::isAccountExpired($this->entries[$i]); + $expired = $shadowExpired; $status = self::FILTER_UNLOCKED; - if ($hasLocked && $hasUnlocked) { + if ($expired) { + $status = self::FILTER_EXPIRED; + } + elseif ($hasLocked && $hasUnlocked) { $status = self::FILTER_SEMILOCKED; } elseif (!$hasUnlocked && $hasLocked) { From fb08739441761db295b33cd8698556844b4716f7 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 17 Oct 2017 17:46:04 +0200 Subject: [PATCH 6/8] check shadow password expiration --- lam/HISTORY | 1 + lam/lib/modules/shadowAccount.inc | 23 +++++++++ lam/lib/types/user.inc | 15 +++++- lam/tests/lib/modules/shadowAccountTest.php | 52 +++++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/lam/HISTORY b/lam/HISTORY index 1bb72b64..5b172506 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -1,5 +1,6 @@ December 2017 - PHP 5.6 and Internet Explorer 11 or later required + - Account status also shows expired accounts 19.09.2017 6.1 diff --git a/lam/lib/modules/shadowAccount.inc b/lam/lib/modules/shadowAccount.inc index 7a2b9c53..a9e7b1ce 100644 --- a/lam/lib/modules/shadowAccount.inc +++ b/lam/lib/modules/shadowAccount.inc @@ -787,6 +787,29 @@ class shadowAccount extends baseModule implements passwordService { return ($time < $now); } + /** + * Returns if the given password is expired. + * + * @param array $attrs LDAP attributes + * @return bool expired + */ + public static function isPasswordExpired($attrs) { + $attrs = array_change_key_case($attrs, CASE_LOWER); + if (empty($attrs['shadowlastchange'][0]) || empty($attrs['shadowmax'][0])) { + return false; + } + if (($attrs['shadowlastchange'][0] < 1) || ($attrs['shadowmax'][0] < 1)) { + return; + } + $time = new DateTime('@' . $attrs['shadowlastchange'][0] * 24 * 3600, new DateTimeZone('UTC')); + $time = $time->add(new DateInterval('P' . $attrs['shadowmax'][0] . 'D')); + if (!empty($attrs['shadowinactive'][0]) && ($attrs['shadowinactive'][0] > 0)) { + $time = $time->add(new DateInterval('P' . $attrs['shadowinactive'][0] . 'D')); + } + $now = new DateTime(null, getTimeZone()); + return ($time < $now); + } + } if (interface_exists('\LAM\JOB\Job', false)) { diff --git a/lam/lib/types/user.inc b/lam/lib/types/user.inc index 02b04cba..53d6a36b 100644 --- a/lam/lib/types/user.inc +++ b/lam/lib/types/user.inc @@ -355,6 +355,9 @@ class user extends baseType { if (shadowAccount::isAccountExpired($shadowAttrs)) { $expiredLabels[] = _('Shadow') . ': ' . _('Account expiration'); } + elseif (shadowAccount::isPasswordExpired($shadowAttrs)) { + $expiredLabels[] = _('Shadow') . ': ' . _('Password expiration'); + } } if (!empty($expiredLabels)) { $expiredTip = '
' . _('Shadow') . '  
' . _('Shadow') . ': ' . _('Account expiration') . '  
'; @@ -921,6 +924,9 @@ class lamUserList extends lamList { $attrs[] = 'nsAccountLock'; $attrs[] = 'accountUnlockTime'; $attrs[] = 'shadowExpire'; + $attrs[] = 'shadowLastChange'; + $attrs[] = 'shadowMax'; + $attrs[] = 'shadowInactive'; $attrs[] = 'objectClass'; } return $attrs; @@ -954,7 +960,8 @@ class lamUserList extends lamList { || ($ppolicyAvailable && !$ppolicyLocked) || ($windowsAvailable && !$windowsLocked); $shadowExpired = shadowAccount::isAccountExpired($this->entries[$i]); - $expired = $shadowExpired; + $shadowPasswordExpired = shadowAccount::isPasswordExpired($this->entries[$i]); + $expired = $shadowExpired || $shadowPasswordExpired; $status = self::FILTER_UNLOCKED; if ($expired) { $status = self::FILTER_EXPIRED; @@ -1006,7 +1013,8 @@ class lamUserList extends lamList { && (!$ppolicyAvailable || $ppolicyLocked) && (!$windowsAvailable || $windowsLocked); $shadowExpired = shadowAccount::isAccountExpired($attrs); - $expired = $shadowExpired; + $shadowPasswordExpired = shadowAccount::isPasswordExpired($attrs); + $expired = $shadowExpired || $shadowPasswordExpired; $icon = 'unlocked.png'; if ($expired) { $icon = 'expired.png'; @@ -1024,6 +1032,9 @@ class lamUserList extends lamList { if ($shadowExpired) { $tipContent .= ''; } + elseif ($shadowPasswordExpired) { + $tipContent .= ''; + } // Unix if ($unixAvailable) { $unixIcon = 'unlocked.png'; diff --git a/lam/tests/lib/modules/shadowAccountTest.php b/lam/tests/lib/modules/shadowAccountTest.php index f46a0f3c..43b9c458 100644 --- a/lam/tests/lib/modules/shadowAccountTest.php +++ b/lam/tests/lib/modules/shadowAccountTest.php @@ -61,6 +61,58 @@ $this->assertTrue(shadowAccount::isAccountExpired($attrs)); } + public function test_isPasswordExpired_noAttr() { + $attrs = array('objectClass' => array('shadowAccount')); + + $this->assertFalse(shadowAccount::isPasswordExpired($attrs)); + } + + public function test_isPasswordExpired_notExpired() { + $change = intval(time() / (24*3600)) - 10; + $attrs = array( + 'objectClass' => array('shadowAccount'), + 'shadoWlastCHange' => array(0 => $change), + 'shadowmax' => array(0 => '14'), + ); + + $this->assertFalse(shadowAccount::isPasswordExpired($attrs)); + } + + public function test_isPasswordExpired_expired() { + $change = intval(time() / (24*3600)) - 10; + $attrs = array( + 'objectClass' => array('shadowAccount'), + 'shadoWlastCHange' => array(0 => $change), + 'shadowmax' => array(0 => '7'), + ); + + $this->assertTrue(shadowAccount::isPasswordExpired($attrs)); + } + + public function test_isPasswordExpired_notExpiredInactiveSet() { + $change = intval(time() / (24*3600)) - 10; + $attrs = array( + 'objectClass' => array('shadowAccount'), + 'shadoWlastCHange' => array(0 => $change), + 'shadowmax' => array(0 => '7'), + 'shaDowinactIVe' => array(0 => '14'), + ); + + $this->assertFalse(shadowAccount::isPasswordExpired($attrs)); + } + + public function test_isPasswordExpired_expiredInactiveSet() { + $change = intval(time() / (24*3600)) - 10; + $attrs = array( + 'objectClass' => array('shadowAccount'), + 'shadoWlastCHange' => array(0 => $change), + 'shadowmax' => array(0 => '7'), + 'shaDowinactIVe' => array(0 => '2'), + ); + + $this->assertTrue(shadowAccount::isPasswordExpired($attrs)); + } + } if (is_readable('lam/lib/passwordExpirationJob.inc')) { From a52f4f1e5d9c978bb59bfa4d144ea14e2b77619e Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 19 Oct 2017 19:32:22 +0200 Subject: [PATCH 7/8] Windows expiration --- lam/lib/modules/windowsUser.inc | 22 ++++++ lam/lib/types/user.inc | 17 ++++- lam/tests/lib/modules/shadowAccountTest.php | 4 +- lam/tests/lib/modules/windowsUserTest.php | 84 +++++++++++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 lam/tests/lib/modules/windowsUserTest.php diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index 53c20ad8..f9fb5947 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -3373,6 +3373,28 @@ class windowsUser extends baseModule implements passwordService { return $replacements; } + /** + * Returns if the given account is expired. + * + * @param array $attrs LDAP attributes + * @return bool expired + */ + public static function isAccountExpired($attrs) { + $attrs = array_change_key_case($attrs, CASE_LOWER); + if (empty($attrs['accountexpires'][0])) { + return false; + } + $value = $attrs['accountexpires'][0]; + if ($value < 1) { + return false; + } + $seconds = substr($value, 0, -7); + $time = new DateTime('1601-01-01', new DateTimeZone('UTC')); + $time->add(new DateInterval('PT' . $seconds . 'S')); + $now = new DateTime(null, getTimeZone()); + return ($time < $now); + } + } if (interface_exists('\LAM\JOB\Job', false)) { diff --git a/lam/lib/types/user.inc b/lam/lib/types/user.inc index 53d6a36b..721c7741 100644 --- a/lam/lib/types/user.inc +++ b/lam/lib/types/user.inc @@ -359,6 +359,13 @@ class user extends baseType { $expiredLabels[] = _('Shadow') . ': ' . _('Password expiration'); } } + $windowsModule = $container->getAccountModule('windowsUser'); + if ($windowsModule != null) { + $windowsAttrs = $windowsModule->getAttributes(); + if (windowsUser::isAccountExpired($windowsAttrs)) { + $expiredLabels[] = _('Windows') . ': ' . _('Account expiration'); + } + } if (!empty($expiredLabels)) { $expiredTip = '
' . _('Shadow') . ': ' . _('Account expiration') . '  
' . _('Shadow') . ': ' . _('Password expiration') . '  
'; foreach ($expiredLabels as $label) { @@ -927,6 +934,7 @@ class lamUserList extends lamList { $attrs[] = 'shadowLastChange'; $attrs[] = 'shadowMax'; $attrs[] = 'shadowInactive'; + $attrs[] = 'accountExpires'; $attrs[] = 'objectClass'; } return $attrs; @@ -961,7 +969,8 @@ class lamUserList extends lamList { || ($windowsAvailable && !$windowsLocked); $shadowExpired = shadowAccount::isAccountExpired($this->entries[$i]); $shadowPasswordExpired = shadowAccount::isPasswordExpired($this->entries[$i]); - $expired = $shadowExpired || $shadowPasswordExpired; + $windowsExpired = windowsUser::isAccountExpired($this->entries[$i]); + $expired = $shadowExpired || $shadowPasswordExpired || $windowsExpired; $status = self::FILTER_UNLOCKED; if ($expired) { $status = self::FILTER_EXPIRED; @@ -1014,7 +1023,8 @@ class lamUserList extends lamList { && (!$windowsAvailable || $windowsLocked); $shadowExpired = shadowAccount::isAccountExpired($attrs); $shadowPasswordExpired = shadowAccount::isPasswordExpired($attrs); - $expired = $shadowExpired || $shadowPasswordExpired; + $windowsExpired = windowsUser::isAccountExpired($attrs); + $expired = $shadowExpired || $shadowPasswordExpired || $windowsExpired; $icon = 'unlocked.png'; if ($expired) { $icon = 'expired.png'; @@ -1066,6 +1076,9 @@ class lamUserList extends lamList { $windowsIcon = 'lock.png'; } $tipContent .= ''; + if ($windowsExpired) { + $tipContent .= ''; + } } if ($windowsAvailable && $windowsPasswordLocked) { $tipContent .= ''; diff --git a/lam/tests/lib/modules/shadowAccountTest.php b/lam/tests/lib/modules/shadowAccountTest.php index 43b9c458..bb3f9f80 100644 --- a/lam/tests/lib/modules/shadowAccountTest.php +++ b/lam/tests/lib/modules/shadowAccountTest.php @@ -3,7 +3,7 @@ $Id$ This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) - Copyright (C) 2016 Roland Gruber + Copyright (C) 2016 - 2017 Roland Gruber 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 @@ -254,4 +254,4 @@ if (is_readable('lam/lib/passwordExpirationJob.inc')) { } -?> \ No newline at end of file +?> diff --git a/lam/tests/lib/modules/windowsUserTest.php b/lam/tests/lib/modules/windowsUserTest.php new file mode 100644 index 00000000..9c443adb --- /dev/null +++ b/lam/tests/lib/modules/windowsUserTest.php @@ -0,0 +1,84 @@ + array('user')); + + $this->assertFalse(windowsUser::isAccountExpired($attrs)); + } + + public function test_isAccountExpired_notExpired() { + $expire = $this->getTimeStamp(14); + $attrs = array( + 'objectClass' => array('user'), + 'accounTExpIRes' => array(0 => $expire) + ); + + $this->assertFalse(windowsUser::isAccountExpired($attrs)); + } + + public function test_isAccountExpired_expired() { + $expire = $this->getTimeStamp(-14); + $attrs = array( + 'objectClass' => array('user'), + 'accounTExpIRes' => array(0 => $expire) + ); + + $this->assertTrue(windowsUser::isAccountExpired($attrs)); + } + + /** + * Returns the timestamp from now with given time difference. + * + * @param int $diff time difference in days + */ + private function getTimeStamp($diff) { + $timeBase = new DateTime('1601-01-01', getTimeZone()); + $time = new DateTime(null, getTimeZone()); + if ($diff > 0) { + $time->add(new DateInterval('P' . $diff . 'D')); + } + else { + $time->sub(new DateInterval('P' . abs($diff) . 'D')); + } + $timeDiff = $time->diff($timeBase); + $days = $timeDiff->format('%a'); + $seconds = $days * 24 * 3600 - ($time->getOffset()); + echo $seconds . ' '; + return $seconds . '0000000'; + } + + } + +?> From c7bc9ee2589c53ce017902d008c1f26d33a86001 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 19 Oct 2017 19:44:13 +0200 Subject: [PATCH 8/8] doc update --- lam/docs/manual-sources/chapter-modules.xml | 581 ++++++++++---------- 1 file changed, 290 insertions(+), 291 deletions(-) diff --git a/lam/docs/manual-sources/chapter-modules.xml b/lam/docs/manual-sources/chapter-modules.xml index af5f63f3..3cbb890d 100644 --- a/lam/docs/manual-sources/chapter-modules.xml +++ b/lam/docs/manual-sources/chapter-modules.xml @@ -29,7 +29,7 @@ - + @@ -43,7 +43,7 @@ - + @@ -60,7 +60,7 @@ - + @@ -249,7 +249,7 @@ - + @@ -264,21 +264,21 @@ - + Show account status: If you activate this option then there will be an additional column displayed that shows if the - account is locked. You can see more details when moving the mouse cursor - over the lock icon. This function supports Unix, Samba, PPolicy, Windows - and 389ds locking+deactivation. + account is locked or expired. You can see more details when moving the + mouse cursor over the lock icon. This function supports Unix, Samba, + PPolicy, Windows and 389ds locking+deactivation. - + @@ -300,7 +300,7 @@ - + @@ -320,7 +320,7 @@ - + @@ -332,7 +332,7 @@ - + @@ -340,7 +340,7 @@ - + @@ -368,7 +368,7 @@ - + @@ -384,7 +384,7 @@ - + @@ -397,7 +397,7 @@ - + @@ -408,7 +408,7 @@ - + @@ -697,17 +697,17 @@ - + - + - + @@ -795,7 +795,7 @@ - + @@ -806,7 +806,7 @@ - + @@ -822,7 +822,7 @@ - + @@ -838,7 +838,7 @@ - + @@ -883,17 +883,17 @@ - + - + - + @@ -917,7 +917,7 @@ - + @@ -929,7 +929,7 @@ - + @@ -946,7 +946,7 @@ - + @@ -959,7 +959,7 @@ - + @@ -975,7 +975,7 @@ - + @@ -992,7 +992,7 @@ - + @@ -1005,7 +1005,7 @@ - + @@ -1038,7 +1038,7 @@ - + @@ -1055,7 +1055,7 @@ - + @@ -1086,7 +1086,7 @@ - + @@ -1111,7 +1111,7 @@ - + @@ -1129,7 +1129,7 @@ - + @@ -1149,7 +1149,7 @@ - + @@ -1160,7 +1160,7 @@ - + @@ -1170,7 +1170,7 @@ - + @@ -1180,7 +1180,7 @@ - + @@ -1195,7 +1195,7 @@ - + @@ -1208,7 +1208,7 @@ - + @@ -1222,7 +1222,7 @@ - + @@ -1249,7 +1249,7 @@ - + @@ -1257,7 +1257,7 @@ - + @@ -1330,17 +1330,17 @@ - + - + - + @@ -1364,7 +1364,7 @@ - + @@ -1388,7 +1388,7 @@ - + @@ -1406,7 +1406,7 @@ - + @@ -1420,7 +1420,7 @@ - + @@ -1441,7 +1441,7 @@ - + @@ -1466,7 +1466,7 @@ - + @@ -1484,7 +1484,7 @@ - + @@ -1495,7 +1495,7 @@ - + @@ -1510,7 +1510,7 @@ - + @@ -1520,7 +1520,7 @@ - + @@ -1528,7 +1528,7 @@ - + @@ -1550,7 +1550,7 @@ - + @@ -1561,7 +1561,7 @@ - + @@ -1594,7 +1594,7 @@ - + @@ -1611,7 +1611,7 @@ - + @@ -1627,7 +1627,7 @@ - + @@ -1646,7 +1646,7 @@ - + @@ -1658,7 +1658,7 @@ - + @@ -1670,7 +1670,7 @@ - + @@ -1703,7 +1703,7 @@ - + @@ -1717,7 +1717,7 @@ - + @@ -1769,7 +1769,7 @@ - + @@ -1783,7 +1783,7 @@ - + @@ -1805,7 +1805,7 @@ - + @@ -1816,7 +1816,7 @@ - + @@ -1830,7 +1830,7 @@ - + @@ -1841,7 +1841,7 @@ - + @@ -1861,7 +1861,7 @@ - + @@ -1878,7 +1878,7 @@ - + @@ -1897,7 +1897,7 @@ - + @@ -1909,7 +1909,7 @@ - + @@ -1928,7 +1928,7 @@ - + @@ -1944,7 +1944,7 @@ - + @@ -1961,7 +1961,7 @@ - + @@ -1974,13 +1974,13 @@ service (e.g. sshd) by reading the LDAP attribute "authorizedService". This way you can manage all allowed services via LAM. - + To activate this PAM feature please setup your /etc/libnss-ldap.conf and set "pam_check_service_attr" to "yes". - + Inside LAM you can now set the allowed services. You may also setup default services in your account profiles. @@ -1988,7 +1988,7 @@ - + @@ -1999,7 +1999,7 @@ - + @@ -2012,7 +2012,7 @@ - + @@ -2032,7 +2032,7 @@ - + @@ -2076,7 +2076,7 @@ - + @@ -2087,7 +2087,7 @@ - + @@ -2104,7 +2104,7 @@ - + @@ -2114,7 +2114,7 @@ - + @@ -2135,7 +2135,7 @@ - + @@ -2145,7 +2145,7 @@
Groups - +
Unix @@ -2164,7 +2164,7 @@ - + @@ -2205,7 +2205,7 @@ - + @@ -2215,7 +2215,7 @@ - + @@ -2225,7 +2225,7 @@ - + @@ -2253,7 +2253,7 @@ - + @@ -2298,7 +2298,7 @@ - + @@ -2309,7 +2309,7 @@ - + @@ -2320,7 +2320,7 @@ - + @@ -2338,7 +2338,7 @@ - + @@ -2348,7 +2348,7 @@ - + @@ -2364,7 +2364,7 @@ - + @@ -2377,7 +2377,7 @@ - + @@ -2388,7 +2388,7 @@ - + @@ -2436,7 +2436,7 @@ - + @@ -2451,7 +2451,7 @@ - + @@ -2462,7 +2462,7 @@ - + @@ -2481,7 +2481,7 @@ - + @@ -2497,7 +2497,7 @@ - + @@ -2513,7 +2513,7 @@ - + @@ -2531,7 +2531,7 @@ - + @@ -2541,7 +2541,7 @@ - + @@ -2569,7 +2569,7 @@ - + @@ -2584,7 +2584,7 @@ - + @@ -2592,7 +2592,7 @@ - + @@ -2608,7 +2608,7 @@ - + @@ -2621,7 +2621,7 @@ - + @@ -2632,7 +2632,7 @@ - + @@ -2664,7 +2664,7 @@ - + @@ -2680,7 +2680,7 @@ - + @@ -2703,7 +2703,7 @@ - + @@ -2718,7 +2718,7 @@ - + @@ -2732,7 +2732,7 @@ - + @@ -2752,7 +2752,7 @@ - + @@ -2765,7 +2765,7 @@ - + @@ -2786,7 +2786,7 @@ - + @@ -2802,7 +2802,7 @@ - + @@ -2824,7 +2824,7 @@ - + @@ -2832,7 +2832,7 @@ - + @@ -2844,17 +2844,17 @@ - + - + - + @@ -2866,7 +2866,7 @@ - + @@ -2904,7 +2904,7 @@ - + @@ -2926,7 +2926,7 @@ - + @@ -2934,7 +2934,7 @@ - + @@ -2944,7 +2944,7 @@ - + @@ -2955,7 +2955,7 @@ - + @@ -2973,7 +2973,7 @@ - + @@ -2993,7 +2993,7 @@ - + @@ -3005,7 +3005,7 @@ - + @@ -3015,7 +3015,7 @@ - + @@ -3032,7 +3032,7 @@ - + @@ -3057,7 +3057,7 @@ - + @@ -3067,7 +3067,7 @@ - + @@ -3078,7 +3078,7 @@ - + @@ -3128,7 +3128,7 @@ - + @@ -3144,7 +3144,7 @@ - + @@ -3161,27 +3161,27 @@ - + - + - + - + - + @@ -3195,7 +3195,7 @@ - + @@ -3212,7 +3212,7 @@ - + @@ -3222,7 +3222,7 @@ - + @@ -3233,7 +3233,7 @@ - + @@ -3244,7 +3244,7 @@ - + @@ -3257,7 +3257,7 @@ - + @@ -3275,7 +3275,7 @@ - + @@ -3285,7 +3285,7 @@ - + @@ -3295,7 +3295,7 @@ - + @@ -3307,17 +3307,17 @@ - + - + - + @@ -3335,7 +3335,7 @@ - + @@ -3345,7 +3345,7 @@ - + @@ -3355,7 +3355,7 @@ - + @@ -3367,17 +3367,17 @@ - + - + - + @@ -3395,7 +3395,7 @@ - + @@ -3405,7 +3405,7 @@ - + @@ -3415,7 +3415,7 @@ - + @@ -3426,7 +3426,7 @@ - + @@ -3439,7 +3439,7 @@ - + @@ -3457,7 +3457,7 @@ - + @@ -3471,7 +3471,7 @@ - + @@ -3485,7 +3485,7 @@ - + @@ -3540,7 +3540,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3561,7 +3561,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3581,7 +3581,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3602,7 +3602,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3625,7 +3625,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3645,7 +3645,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3668,7 +3668,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3685,7 +3685,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3696,7 +3696,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3706,7 +3706,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3716,7 +3716,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3747,7 +3747,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3757,7 +3757,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + @@ -3767,13 +3767,12 @@ Attention: If the Active Directory schema is used then LAM will always use dn an - + - Example server - entry: + Example server entry: dn: cn=server,ou=dhcp,dc=ldap-account-manager,dc=org @@ -3842,7 +3841,7 @@ Attention: If the Active Directory schema is used then LAM will always use dn an ldap-debug-file "/var/log/dhcp-ldap-startup.log"; - + @@ -3864,7 +3863,7 @@ Run slapindex to rebuild the index. - + @@ -3874,7 +3873,7 @@ Run slapindex to rebuild the index. - + @@ -3884,7 +3883,7 @@ Run slapindex to rebuild the index. - + @@ -3898,7 +3897,7 @@ Run slapindex to rebuild the index. - + @@ -3909,7 +3908,7 @@ Run slapindex to rebuild the index. - + @@ -3932,7 +3931,7 @@ Run slapindex to rebuild the index. - + @@ -3943,7 +3942,7 @@ Run slapindex to rebuild the index. - + @@ -3954,7 +3953,7 @@ Run slapindex to rebuild the index. - + @@ -3972,7 +3971,7 @@ Run slapindex to rebuild the index. - + @@ -3990,7 +3989,7 @@ Run slapindex to rebuild the index. - + @@ -4046,7 +4045,7 @@ Run slapindex to rebuild the index. - + @@ -4062,7 +4061,7 @@ Run slapindex to rebuild the index. - + @@ -4079,7 +4078,7 @@ Run slapindex to rebuild the index. - + @@ -4095,7 +4094,7 @@ Run slapindex to rebuild the index. - + @@ -4112,7 +4111,7 @@ Run slapindex to rebuild the index. - + @@ -4128,7 +4127,7 @@ Run slapindex to rebuild the index. - + @@ -4160,7 +4159,7 @@ Run slapindex to rebuild the index. - + @@ -4190,7 +4189,7 @@ Run slapindex to rebuild the index. - + IN @@ -4200,7 +4199,7 @@ Run slapindex to rebuild the index. - + IN @@ -4210,7 +4209,7 @@ Run slapindex to rebuild the index. - + IN @@ -4220,7 +4219,7 @@ Run slapindex to rebuild the index. - + IN @@ -4260,7 +4259,7 @@ Run slapindex to rebuild the index. - + IN @@ -4306,7 +4305,7 @@ objectclass: top - + @@ -4314,7 +4313,7 @@ objectclass: top - + @@ -4332,7 +4331,7 @@ objectclass: top - + @@ -4351,7 +4350,7 @@ objectclass: top - + @@ -4366,7 +4365,7 @@ objectclass: top - + @@ -4381,7 +4380,7 @@ objectclass: top - + @@ -4391,7 +4390,7 @@ objectclass: top - + @@ -4413,7 +4412,7 @@ objectclass: top - + @@ -4431,7 +4430,7 @@ objectclass: top - + @@ -4446,7 +4445,7 @@ objectclass: top - + @@ -4458,7 +4457,7 @@ objectclass: top - + @@ -4470,7 +4469,7 @@ objectclass: top - + @@ -4528,7 +4527,7 @@ cn: OracleContext - + @@ -4540,7 +4539,7 @@ cn: OracleContext - + @@ -4550,7 +4549,7 @@ cn: OracleContext - + @@ -4567,7 +4566,7 @@ cn: OracleContext - + @@ -4619,7 +4618,7 @@ OK (10 msec) - + @@ -4640,7 +4639,7 @@ OK (10 msec) - + @@ -4648,7 +4647,7 @@ OK (10 msec) - + @@ -4658,7 +4657,7 @@ OK (10 msec) - + @@ -4670,7 +4669,7 @@ OK (10 msec) - + @@ -4681,7 +4680,7 @@ OK (10 msec) - + @@ -4697,7 +4696,7 @@ OK (10 msec) - + @@ -4705,7 +4704,7 @@ OK (10 msec) - + @@ -4716,7 +4715,7 @@ OK (10 msec) - + @@ -4728,7 +4727,7 @@ OK (10 msec) - + @@ -4802,7 +4801,7 @@ OK (10 msec) - + @@ -4825,7 +4824,7 @@ OK (10 msec) - + @@ -4850,7 +4849,7 @@ OK (10 msec) - + @@ -4867,7 +4866,7 @@ OK (10 msec) - + @@ -4877,7 +4876,7 @@ OK (10 msec) - + @@ -4885,7 +4884,7 @@ OK (10 msec) - + @@ -4901,7 +4900,7 @@ OK (10 msec) - + @@ -4945,7 +4944,7 @@ OK (10 msec) - + @@ -4955,7 +4954,7 @@ OK (10 msec) - + @@ -4969,7 +4968,7 @@ OK (10 msec) - + @@ -4979,7 +4978,7 @@ OK (10 msec) - + @@ -4998,7 +4997,7 @@ OK (10 msec) - + @@ -5008,7 +5007,7 @@ OK (10 msec) - + @@ -5023,7 +5022,7 @@ OK (10 msec) - + @@ -5033,7 +5032,7 @@ OK (10 msec) - + @@ -5050,7 +5049,7 @@ OK (10 msec) - + @@ -5060,7 +5059,7 @@ OK (10 msec) - + @@ -5076,7 +5075,7 @@ OK (10 msec) - + @@ -5086,7 +5085,7 @@ OK (10 msec) - + @@ -5094,7 +5093,7 @@ OK (10 msec) - + @@ -5108,7 +5107,7 @@ OK (10 msec) - + @@ -5127,7 +5126,7 @@ OK (10 msec) - + @@ -5140,7 +5139,7 @@ OK (10 msec) - + @@ -5187,7 +5186,7 @@ OK (10 msec) value):
' . _('Windows') . '  
' . _('Windows') . ': ' . _('Account expiration') . '  
' . _('Locked till') . '  ' . $windowsPasswordLockedTime->format('Y-m-d H:i:s') . '
- + @@ -5220,7 +5219,7 @@ OK (10 msec)
Constant value
- + Presentation: @@ -5229,7 +5228,7 @@ OK (10 msec) - + @@ -5245,7 +5244,7 @@ OK (10 msec) - + @@ -5257,7 +5256,7 @@ OK (10 msec) - + @@ -5302,7 +5301,7 @@ OK (10 msec) - + @@ -5319,7 +5318,7 @@ OK (10 msec) - + @@ -5497,7 +5496,7 @@ OK (10 msec) activating this option will only show the command output but not the command itself. - + You can see a preview of the commands which will be automatically executed on the "Custom scripts" tab. Here you can also run the manual @@ -5506,7 +5505,7 @@ OK (10 msec) - + @@ -5526,7 +5525,7 @@ OK (10 msec) - + @@ -5534,7 +5533,7 @@ OK (10 msec) - + @@ -5544,7 +5543,7 @@ OK (10 msec) - + @@ -5569,7 +5568,7 @@ OK (10 msec) - + @@ -5577,7 +5576,7 @@ OK (10 msec) - + @@ -5588,7 +5587,7 @@ OK (10 msec) - + @@ -5596,7 +5595,7 @@ OK (10 msec) - + @@ -5615,7 +5614,7 @@ OK (10 msec) - + @@ -5633,7 +5632,7 @@ OK (10 msec) - +