1<?php 2 3namespace dokuwiki\plugin\pureldap\classes; 4 5use dokuwiki\Utf8\PhpString; 6use FreeDSx\Ldap\Entry\Attribute; 7use FreeDSx\Ldap\Exception\BindException; 8use FreeDSx\Ldap\Exception\ConnectionException; 9use FreeDSx\Ldap\Exception\OperationException; 10use FreeDSx\Ldap\LdapClient; 11 12require_once __DIR__ . '/../vendor/autoload.php'; 13 14abstract class Client 15{ 16 const FILTER_EQUAL = 'equal'; 17 const FILTER_CONTAINS = 'contains'; 18 const FILTER_STARTSWITH = 'startsWith'; 19 const FILTER_ENDSWITH = 'endsWith'; 20 21 /** @var array the configuration */ 22 protected $config; 23 24 /** @var LdapClient */ 25 protected $ldap; 26 27 /** @var bool is this client authenticated already? */ 28 protected $isAuthenticated = false; 29 30 /** @var array cached user info */ 31 protected $userCache = []; 32 33 /** @var array cached group list */ 34 protected $groupCache = []; 35 36 /** 37 * Client constructor. 38 * @param array $config 39 */ 40 public function __construct($config) 41 { 42 $this->config = $this->prepareConfig($config); 43 $this->ldap = new LdapClient($this->config); 44 } 45 46 /** 47 * Setup sane config defaults 48 * 49 * @param array $config 50 * @return array 51 */ 52 protected function prepareConfig($config) 53 { 54 $defaults = [ 55 'defaultgroup' => 'user', // we expect this to be passed from global conf 56 'suffix' => '', 57 'port' => '', 58 'encryption' => false, 59 'admin_username' => '', 60 'admin_password' => '', 61 'page_size' => 150, 62 'use_ssl' => false, 63 'validate' => 'strict', 64 'primarygroup' => 'domain users', 65 'attributes' => [], 66 ]; 67 68 $config = array_merge($defaults, $config); 69 70 // default port depends on SSL setting 71 if (!$config['port']) { 72 $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389; 73 } 74 75 // set ssl parameters 76 $config['use_ssl'] = ($config['encryption'] === 'ssl'); 77 if ($config['validate'] === 'none') { 78 $config['ssl_validate_cert'] = false; 79 } elseif ($config['validate'] === 'self') { 80 $config['ssl_allow_self_signed'] = true; 81 } 82 83 $config['suffix'] = PhpString::strtolower($config['suffix']); 84 $config['primarygroup'] = $this->cleanGroup($config['primarygroup']); 85 86 return $config; 87 } 88 89 /** 90 * Authenticate as admin 91 */ 92 public function autoAuth() 93 { 94 if ($this->isAuthenticated) return true; 95 96 $user = $this->prepareBindUser($this->config['admin_username']); 97 $ok = $this->authenticate($user, $this->config['admin_password']); 98 if (!$ok) { 99 $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__); 100 } 101 return $ok; 102 } 103 104 /** 105 * Authenticates a given user. This client will remain authenticated 106 * 107 * @param string $user 108 * @param string $pass 109 * @return bool was the authentication successful? 110 * @noinspection PhpRedundantCatchClauseInspection 111 */ 112 public function authenticate($user, $pass) 113 { 114 $user = $this->prepareBindUser($user); 115 116 if ($this->config['encryption'] === 'tls') { 117 try { 118 $this->ldap->startTls(); 119 } catch (OperationException $e) { 120 $this->fatal($e); 121 } 122 } 123 124 try { 125 $this->ldap->bind($user, $pass); 126 } catch (BindException $e) { 127 return false; 128 } catch (ConnectionException $e) { 129 $this->fatal($e); 130 return false; 131 } catch (OperationException $e) { 132 $this->fatal($e); 133 return false; 134 } 135 136 $this->isAuthenticated = true; 137 return true; 138 } 139 140 /** 141 * Get info for a single user, use cache if available 142 * 143 * @param string $username 144 * @param bool $fetchgroups Are groups needed? 145 * @return array|null 146 */ 147 public function getCachedUser($username, $fetchgroups = true) 148 { 149 global $conf; 150 151 // memory cache first 152 if (isset($this->userCache[$username])) { 153 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 154 return $this->userCache[$username]; 155 } 156 } 157 158 // disk cache second 159 $cachename = getCacheName($username, '.pureldap-user'); 160 $cachetime = @filemtime($cachename); 161 if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 162 $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 163 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 164 return $this->userCache[$username]; 165 } 166 } 167 168 // fetch fresh data 169 $info = $this->getUser($username, $fetchgroups); 170 171 // store in cache 172 if ($info !== null) { 173 $this->userCache[$username] = $info; 174 file_put_contents($cachename, json_encode($info)); 175 } 176 177 return $info; 178 } 179 180 /** 181 * Fetch a single user 182 * 183 * @param string $username 184 * @param bool $fetchgroups Shall groups be fetched, too? 185 * @return null|array 186 */ 187 abstract public function getUser($username, $fetchgroups = true); 188 189 /** 190 * Return a list of all available groups, use cache if available 191 * 192 * @return string[] 193 */ 194 public function getCachedGroups() 195 { 196 if (empty($this->groupCache)) { 197 $this->groupCache = $this->getGroups(); 198 } 199 200 return $this->groupCache; 201 } 202 203 /** 204 * Return a list of all available groups 205 * 206 * Optionally filter the list 207 * 208 * @param null|string $match Filter for this, null for all groups 209 * @param string $filtermethod How to match the groups 210 * @return string[] 211 */ 212 abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 213 214 /** 215 * Clean the user name for use in DokuWiki 216 * 217 * @param string $user 218 * @return string 219 */ 220 abstract public function cleanUser($user); 221 222 /** 223 * Clean the group name for use in DokuWiki 224 * 225 * @param string $group 226 * @return string 227 */ 228 abstract public function cleanGroup($group); 229 230 /** 231 * Inheriting classes may want to manipulate the user before binding 232 * 233 * @param string $user 234 * @return string 235 */ 236 protected function prepareBindUser($user) 237 { 238 return $user; 239 } 240 241 /** 242 * Helper method to get the first value of the given attribute 243 * 244 * The given attribute may be null, an empty string is returned then 245 * 246 * @param Attribute|null $attribute 247 * @return string 248 */ 249 protected function attr2str($attribute) 250 { 251 if ($attribute !== null) { 252 return $attribute->firstValue(); 253 } 254 return ''; 255 } 256 257 /** 258 * Get the attributes that should be fetched for a user 259 * 260 * Can be extended in sub classes 261 * 262 * @return Attribute[] 263 */ 264 protected function userAttributes() 265 { 266 // defaults 267 $attr = [ 268 new Attribute('dn'), 269 new Attribute('displayName'), 270 new Attribute('mail'), 271 ]; 272 // additionals 273 foreach ($this->config['attributes'] as $attribute) { 274 $attr[] = new Attribute($attribute); 275 } 276 return $attr; 277 } 278 279 /** 280 * Handle fatal exceptions 281 * 282 * @param \Exception $e 283 */ 284 protected function fatal(\Exception $e) 285 { 286 if (class_exists('\dokuwiki\ErrorHandler')) { 287 \dokuwiki\ErrorHandler::logException($e); 288 } 289 290 if (defined('DOKU_UNITTEST')) { 291 throw new \RuntimeException('', 0, $e); 292 } 293 294 msg('[pureldap] ' . hsc($e->getMessage()), -1); 295 } 296 297 /** 298 * Handle error output 299 * 300 * @param string $msg 301 * @param string $file 302 * @param int $line 303 */ 304 protected function error($msg, $file, $line) 305 { 306 if (class_exists('\dokuwiki\Logger')) { 307 \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 308 } 309 310 if (defined('DOKU_UNITTEST')) { 311 throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 312 } 313 314 msg('[pureldap] ' . hsc($msg), -1); 315 } 316 317 /** 318 * Handle debug output 319 * 320 * @param string $msg 321 * @param string $file 322 * @param int $line 323 */ 324 protected function debug($msg, $file, $line) 325 { 326 global $conf; 327 328 if (class_exists('\dokuwiki\Logger')) { 329 \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 330 } 331 332 if ($conf['allowdebug']) { 333 msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 334 } 335 } 336} 337