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