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