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