1<?php 2 3/** 4 * DokuWiki Plugin authgooglesheets (Helper Component) 5 * 6 * @author Anna Dabrowska <dokuwiki@cosmocode.de> 7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 8 */ 9 10use Google\Service\Sheets\BatchUpdateSpreadsheetRequest; 11use Google\Service\Sheets\BatchUpdateValuesRequest; 12 13require_once(__DIR__ . '/vendor/autoload.php'); 14 15/** 16 * Class helper_plugin_authgooglesheets 17 */ 18class helper_plugin_authgooglesheets extends DokuWiki_Plugin 19{ 20 /** @var Google_Service_Sheets */ 21 protected $service; 22 protected $spreadsheetId; 23 24 protected $userCacheId = 'userCache'; 25 protected $users = []; 26 protected $requiredCols = ['user', 'pass', 'name', 'mail', 'grps']; 27 protected $columnMap = []; 28 29 protected $alpha = 'ABCDEFGHIJKLMNOPQRSTVWXYZ'; 30 protected $pattern; 31 32 33 public function __construct() 34 { 35 try { 36 $this->spreadsheetId = $this->getConf('sheetId'); 37 if (empty($this->spreadsheetId)) { 38 throw new Exception('Google Spreadsheet ID not set!'); 39 } 40 41 $client = $this->getClient(); 42 $this->service = new Google_Service_Sheets($client); 43 } catch (Exception $e) { 44 msg('Authentication Error: ' . $e->getMessage()); 45 } 46 } 47 48 /** 49 * Returns user data or false if user does not exist 50 * 51 * @param string $user 52 * @return array|false 53 */ 54 public function getUserData($user) 55 { 56 $users = $this->getUsers(); 57 return $users[$user] ?? false; 58 } 59 60 /** 61 * Returns user data as nested arrays 62 * 63 * @return array 64 */ 65 public function getUsers($start = 0, $limit = 0, $filter = null) 66 { 67 global $conf; 68 global $INPUT; 69 70 $userCache = new dokuwiki\Cache\Cache($this->userCacheId, 'authgooglesheets'); 71 $decoded = json_decode($userCache->retrieveCache(), true); 72 73 $depends['age'] = $conf['cachetime']; 74 $depends['purge'] = $INPUT->bool('purge'); 75 76 if (empty($decoded) || !$userCache->useCache($depends)) { 77 $values = $this->getSheet(); 78 79 $header = array_shift($values); 80 $this->columnMap = array_flip($header); 81 82 foreach ($values as $key => $row) { 83 // bump row number because index starts from 1 and we already removed the header row 84 $rowNum = $key + 2; 85 86 // ignore invalid rows without required user properties 87 if (empty($row[$this->columnMap['user']]) || empty($row[$this->columnMap['pass']]) || empty($row[$this->columnMap['mail']])) { 88 continue; 89 } 90 91 $name = $row[$this->columnMap['name']] ?? ''; 92 $grps = $row[$this->columnMap['grps']] ?? ''; 93 94 $grps = array_map('trim', explode(',', $grps)); 95 $this->users[$row[$this->columnMap['user']]] = [ 96 'pass' => $row[$this->columnMap['pass']], 97 'name' => $name, 98 'mail' => $row[$this->columnMap['mail']], 99 'grps' => $grps, 100 'row' => $rowNum 101 ]; 102 } 103 104 $userCache->storeCache(json_encode(['columnMap' => $this->columnMap, 'users' => $this->users])); 105 } else { 106 $this->users = $decoded['users'] ?? null; 107 $this->columnMap = $decoded['columnMap'] ?? null; 108 } 109 110 ksort($this->users); 111 112 return $this->getFilteredUsers($start, $limit, $filter); 113 } 114 115 /** 116 * Appends new user to auth sheet and writes a user creation stat 117 * 118 * @param array $userData 119 * @return bool 120 */ 121 public function appendUser($userData) 122 { 123 $range = $this->getConf('sheetName') . '!A2'; 124 $params = [ 125 'valueInputOption' => 'RAW' 126 ]; 127 128 $data = []; 129 foreach ($this->columnMap as $col => $index) { 130 if ($col === 'pass') { 131 $userData[$col] = auth_cryptPassword($userData[$col]); 132 } 133 $data[] = $userData[$col] ?? ''; 134 } 135 136 $body = new \Google\Service\Sheets\ValueRange(['values' => [$data]]); 137 try { 138 $this->service->spreadsheets_values->append($this->spreadsheetId, $range, $body, $params); 139 } catch (Exception $e) { 140 msg('User cannot be added'); 141 return false; 142 } 143 // reset users 144 $this->resetUsers(); 145 return true; 146 } 147 148 /** 149 * @param string $user 150 * @param array $changes Array in which keys specify columns 151 * @return bool 152 */ 153 public function update($user, $changes) 154 { 155 // ensure variable is not empty, e.g. in user profile 156 $this->users = $this->getUsers(); 157 158 $rangeStart = $this->getConf('sheetName') . '!'; 159 160 $data = []; 161 foreach ($changes as $col => $value) { 162 if ($col === 'pass') { 163 $value = auth_cryptPassword($value); 164 } 165 if ($col === 'grps') { 166 $value = implode(',', $value); 167 } 168 $data[] = [ 169 'range' => $rangeStart . $this->alpha[$this->columnMap[$col]] . ($this->users[$user]['row']), 170 'values' => [ 171 [$value] 172 ], 173 ]; 174 } 175 176 $body = new BatchUpdateValuesRequest( 177 [ 178 'valueInputOption' => 'RAW', 179 'data' => $data 180 ] 181 ); 182 183 try { 184 $this->service->spreadsheets_values->batchUpdate($this->spreadsheetId, $body); 185 } catch (Exception $e) { 186 msg('Update failed'); 187 return false; 188 } 189 // reset users 190 $this->resetUsers(); 191 return true; 192 } 193 194 /** 195 * @param array $users 196 * @return bool 197 */ 198 public function delete($users) 199 { 200 if (empty($users)) return false; 201 202 // FIXME load users somewhere else 203 $this->users = $this->getUsers(); 204 205 $requests = []; 206 207 $users = array_reverse($users); 208 foreach ($users as $user) { 209 $rowNum = $this->users[$user]['row']; 210 211 $requests[] = [ 212 "deleteDimension" => [ 213 "range" => [ 214 "sheetId" => $this->getConf('sheetGid'), 215 "dimension" => "ROWS", 216 "startIndex" => $rowNum - 1, // 0 based index here! 217 "endIndex" => $rowNum 218 ] 219 ] 220 ]; 221 } 222 223 $body = new BatchUpdateSpreadsheetRequest( 224 [ 225 'requests' => $requests 226 ] 227 ); 228 229 try { 230 $this->service->spreadsheets->batchUpdate($this->spreadsheetId, $body); 231 } catch (Exception $e) { 232 msg('Deletion failed'); 233 return false; 234 } 235 236 // reset users 237 $this->resetUsers(); 238 return true; 239 } 240 241 /** 242 * Filter implementation from authplain 243 * @see \auth_plugin_authplain 244 * 245 * @param int $start 246 * @param int $limit 247 * @param array $filter 248 * @return array 249 */ 250 protected function getFilteredUsers($start, $limit, $filter) 251 { 252 $filter = $filter ?? []; 253 $this->pattern = array(); 254 foreach ($filter as $item => $pattern) { 255 $this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters 256 } 257 258 $i = 0; 259 $count = 0; 260 $out = array(); 261 262 foreach ($this->users as $user => $info) { 263 if ($this->filter($user, $info)) { 264 if ($i >= $start) { 265 $out[$user] = $info; 266 $count++; 267 if (($limit > 0) && ($count >= $limit)) break; 268 } 269 $i++; 270 } 271 } 272 273 return $out; 274 } 275 276 /** 277 * return true if $user + $info match $filter criteria, false otherwise 278 * 279 * @author Chris Smith <chris@jalakai.co.uk> 280 * 281 * @param string $user User login 282 * @param array $info User's userinfo array 283 * @return bool 284 */ 285 protected function filter($user, $info) 286 { 287 foreach ($this->pattern as $item => $pattern) { 288 if ($item == 'user') { 289 if (!preg_match($pattern, $user)) return false; 290 } elseif ($item == 'grps') { 291 if (!count(preg_grep($pattern, $info['grps']))) return false; 292 } else { 293 if (!preg_match($pattern, $info[$item])) return false; 294 } 295 } 296 return true; 297 } 298 299 /** 300 * Returns all user rows from the auth sheet 301 * 302 * @return array[] 303 */ 304 protected function getSheet() 305 { 306 $range = $this->getConf('sheetName') . '!A1:Z'; 307 $response = $this->service->spreadsheets_values->get($this->spreadsheetId, $range); 308 $values = $response->getValues(); 309 310 return $values; 311 } 312 313 /** 314 * Cached check if the sheet is valid, i.e. has all required columns 315 * 316 * @return bool 317 */ 318 public function validateSheet() 319 { 320 $cache = new dokuwiki\Cache\Cache('validated', 'authgooglesheets'); 321 322 if ($cache->retrieveCache()) { 323 return true; 324 } 325 326 $range = $this->getConf('sheetName') . '!1:1'; 327 $response = $this->service->spreadsheets_values->get($this->spreadsheetId, $range); 328 $header = $response->getValues(); 329 330 $isValid = array_intersect($this->requiredCols, $header[0]) === $this->requiredCols; 331 332 if ($isValid) $cache->storeCache(time()); 333 334 return $isValid; 335 } 336 337 /** 338 * Returns an authorized API client. 339 * 340 * @return Google_Client the authorized client object 341 * @throws \Google\Exception 342 */ 343 protected function getClient() 344 { 345 $client = new \Google_Client(); 346 $config = DOKU_CONF . 'authgooglesheets_credentials.json'; 347 if (!is_file($config)) { 348 throw new Exception('Authentication configuration missing!'); 349 } 350 $client->setAuthConfig($config); 351 $client->setScopes([ 352 \Google_Service_Sheets::SPREADSHEETS, 353 ]); 354 return $client; 355 } 356 357 /** 358 * Clear users stored in class variable and filesystem cache 359 * 360 * @return void 361 */ 362 protected function resetUsers() 363 { 364 $this->users = []; 365 $userCache = new dokuwiki\Cache\Cache($this->userCacheId, 'authgooglesheets'); 366 $userCache->removeCache(); 367 } 368} 369