1<?php 2 3/** 4 * DokuWiki Plugin attribute (Helper Component) 5 * 6 * @author Mike Wilmes <mwilmes@avc.edu> 7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 8 */ 9 10/** 11 * Class helper_plugin_attribute 12 */ 13class helper_plugin_attribute extends DokuWiki_Plugin 14{ 15 public $success = false; 16 protected $storepath = null; 17 protected $cache = null; 18 protected $secure = true; 19 20 /** 21 * Constructor 22 */ 23 public function __construct() 24 { 25 $this->loadConfig(); 26 // Create the path used for attribute data. 27 $path = substr($this->conf['store'], 0, 1) == '/' ? $this->conf['store'] : DOKU_INC . $this->conf['store']; 28 $this->storepath = ($this->conf['store'] === '' || !io_mkdir_p($path)) ? null : $path; 29 // A directory is needed. 30 if (is_null($this->storepath)) { 31 msg("Attribute: Configuration item 'store' is not set to a writeable directory.", -1); 32 return; 33 } 34 $this->success = true; 35 // Create a memory cache for this execution. 36 $this->cache = array(); 37 } 38 39 /** 40 * Allows overriding the secure setting 41 * 42 * When set to false, no user validation checks are made. 43 * 44 * @param bool $secure 45 * @return void 46 */ 47 public function setSecure($secure) 48 { 49 $this->secure = $secure; 50 } 51 52 /** 53 * Return info about supported methods in this Helper Plugin 54 * 55 * @return array Array of public methods 56 */ 57 public function getMethods() 58 { 59 $result = array(); 60 $result[] = array( 61 'name' => 'enumerateAttributes', 62 'desc' => "Generates a list of known attributes in the specified namespace for a user. If user is present, must be an admin, otherwise defaults to currently logged in user.", 63 'parameters' => array( 64 'namespace' => 'string', 65 'user' => 'string (optional)', 66 ), 67 'return' => array('attributes' => 'array'), // returns false on error. 68 ); 69 $result[] = array( 70 'name' => 'enumerateUsers', 71 'desc' => "Generates a list of users that have assigned attributes in the specified namespace.", 72 'parameters' => array( 73 'namespace' => 'string', 74 ), 75 'return' => array('users' => 'array'), // returns false on error. 76 ); 77 $result[] = array( 78 'name' => 'set', 79 'desc' => "Set the value of an attribute in a specified namespace. Returns boolean success (false if something went wrong). If user is present, must be an admin, otherwise defaults to currently logged in user.", 80 'parameters' => array( 81 'namespace' => 'string', 82 'attribute' => 'string', 83 'value' => 'mixed (serializable)', 84 'user' => 'string (optional)', 85 ), 86 'return' => array('success' => 'boolean'), 87 ); 88 $result[] = array( 89 'name' => 'exists', 90 'desc' => "Checks if an attribute exists for a user in a given namespace. If user is present, must be an admin, otherwise defaults to currently logged in user.", 91 'parameters' => array( 92 'namespace' => 'string', 93 'attribute' => 'string', 94 'user' => 'string (optional)', 95 ), 96 'return' => array('exists' => 'boolean'), 97 ); 98 $result[] = array( 99 'name' => 'del', 100 'desc' => "Deletes attribute data in a specified namespace by its name. If user is present, must be an admin, otherwise defaults to currently logged in user.", 101 'parameters' => array( 102 'namespace' => 'string', 103 'attribute' => 'string', 104 'user' => 'string (optional)', 105 ), 106 'return' => array('success' => 'boolean'), 107 ); 108 $result[] = array( 109 'name' => 'get', 110 'desc' => "Retrieves a value for an attribute in a specified namespace. Returns retrieved value or null. \$success out-parameter can be checked to check success (you may have false, null, 0, or '' as stored value). If user is present, must be an admin, otherwise defaults to currently logged in user.", 111 'parameters' => array( 112 'namespace' => 'string', 113 'attribute' => 'string', 114 'success' => 'boolean (out)', 115 'user' => 'string (optional)', 116 ), 117 'return' => array('value' => 'mixed'), // returns false on error. 118 ); 119 $result[] = array( 120 'name' => 'purge', 121 'desc' => "Deletes all attribute data for a specified namespace for a user. Only useable by an admin.", 122 'parameters' => array( 123 'namespace' => 'string', 124 'user' => 'string', 125 ), 126 'return' => array('success' => 'boolean'), 127 ); 128 return $result; 129 } 130 131 /** 132 * Validate that the user may access another user's attribute. If the user 133 * is an admin and another user name is supplied, that value is returned. 134 * Otherwise the name of the logged in user is supplied. If no user is 135 * logged in, null is returned. 136 * 137 * This check can be disabled with the setSecure() method. 138 * 139 * @param string $user 140 * 141 * @return null|string 142 */ 143 private function validateUser($user) 144 { 145 if(!$this->secure) return $user; 146 147 // We need a special circumstance. If a user is not logged in, but we 148 // are performing a login, enable access to the attributes of the user 149 // being logged in IF DIRECTLY SPECIFIED. 150 global $INFO, $ACT, $USERINFO, $INPUT; 151 if ($ACT == 'login' && !$USERINFO && $user == $INPUT->str('u')) { 152 return $user; 153 } 154 // This does not meet the special circumstance listed above. 155 // Perform rights validation. 156 // If no one is logged in, then return null. 157 if ($_SERVER['REMOTE_USER'] == '') { 158 return null; 159 } 160 // If the user is not an admin, no user is specified, or the 161 // named user is not the logged in user, then return the currently 162 // logged in user. 163 if (!$user || ($user !== $_SERVER['REMOTE_USER'] && !$INFO['isadmin'])) { 164 return $_SERVER['REMOTE_USER']; 165 } 166 // The user is an admin and a name was specified. 167 return $user; 168 } 169 170 /** 171 * Load all attribute data for a user in the specified namespace. 172 * This loads all user attribute data from file. A copy is stored in 173 * memory to alleviate repeated file accesses. 174 * 175 * @param $namespace 176 * @param $user 177 * 178 * @return array|mixed 179 */ 180 private function loadAttributes($namespace, $user) 181 { 182 $key = rawurlencode($namespace) . '.' . rawurlencode($user); 183 $filename = $this->storepath . "/" . $key; 184 185 // If the file does not exist, then return an empty attribute array. 186 if (!is_file($filename)) { 187 return array(); 188 } 189 190 if (array_key_exists($filename, $this->cache)) { 191 return $this->cache[$filename]; 192 } 193 194 $packet = io_readFile($filename, false); 195 196 // Unserialize returns false on bad data. 197 $preserial = @unserialize($packet); 198 if ($preserial !== false) { 199 list($compressed, $serial) = $preserial; 200 if ($compressed) { 201 $serial = gzuncompress($serial); 202 } 203 $unserial = @unserialize($serial); 204 if ($unserial !== false) { 205 list($filekey, $data) = $unserial; 206 if ($filekey != $key) { 207 $data = array(); 208 } 209 } 210 } 211 212 // Set a reasonable default if either unserialize failed. 213 if ($preserial == false || $unserial === false) { 214 $data = array(); 215 } 216 217 $this->cache[$filename] = $data; 218 219 return $data; 220 } 221 222 /** 223 * Saves attributes in $data to a file. The file is flagged with the 224 * namespace and use that the data was saved for. The data and key will 225 * normally be compressed, but this can be turned off for debugging. 226 * There is an uncompressed flag to denote whether the data was compressed 227 * or not, so both compressed and uncompressed data can be loaded 228 * regardless of the compression configuration. 229 * 230 * @param $namespace 231 * @param $user 232 * @param $data 233 * 234 * @return bool 235 */ 236 private function saveAttributes($namespace, $user, $data) 237 { 238 $key = rawurlencode($namespace) . '.' . rawurlencode($user); 239 $filename = $this->storepath . "/" . $key; 240 241 $this->cache[$filename] = $data; 242 243 $serial = serialize(array($key, $data)); 244 $compressed = $this->conf['no_compress'] === 0; 245 if ($compressed) { 246 $serial = gzcompress($serial); 247 } 248 $packet = serialize(array($compressed, $serial)); 249 250 return io_saveFile($filename, $packet); 251 } 252 253 /** 254 * Generates a list of users that have assigned attributes in the 255 * specified namespace. 256 * 257 * @param string $namespace 258 * 259 * @return array|bool 260 */ 261 public function enumerateUsers($namespace) 262 { 263 if (!$this->success) { 264 return false; 265 } 266 267 $listing = scandir($this->storepath, SCANDIR_SORT_DESCENDING); 268 269 // Restrict to namespace 270 $key = rawurlencode($namespace) . '.'; 271 $files = array_filter( 272 $listing, 273 function ($x) use ($key) { 274 return substr($x, 0, strlen($key)) == $key; 275 } 276 ); 277 // Get usernames from files 278 $users = array_map( 279 function ($x) use ($key) { 280 return rawurldecode(substr($x, strlen($key))); 281 }, 282 $files 283 ); 284 285 return $users; 286 } 287 288 /** 289 * set - Set the value of an attribute in a specified namespace. Returns 290 * boolean success (false if something went wrong). If user is present, 291 * must be an admin, otherwise defaults to currently logged in user. 292 * 293 * @param string $namespace 294 * @param string $attribute 295 * @param string $value 296 * @param null $user 297 * 298 * @return bool 299 */ 300 public function set($namespace, $attribute, $value, $user = null) 301 { 302 if (!$this->success) { 303 return false; 304 } 305 306 $user = $this->validateUser($user); 307 if ($user === null) { 308 return false; 309 } 310 $lock= $namespace . '.' . $user; 311 io_lock($lock); 312 313 $data = $this->loadAttributes($namespace, $user); 314 315 $result = false; 316 if ($data !== null) { 317 // Set the data in the array. 318 $data[$attribute] = $value; 319 // Store the changed data. 320 $result = $this->saveAttributes($namespace, $user, $data); 321 } 322 323 io_unlock($lock); 324 325 return $result; 326 } 327 328 /** 329 * Generates a list of users that have assigned attributes in the 330 * specified namespace. 331 * 332 * @param string $namespace 333 * @param string|null $user 334 * 335 * @return array|bool 336 */ 337 public function enumerateAttributes($namespace, $user = null) 338 { 339 if (!$this->success) { 340 return false; 341 } 342 343 $user = $this->validateUser($user); 344 if ($user === null) { 345 return false; 346 } 347 348 $lock = $namespace . '.' . $user; 349 io_lock($lock); 350 351 $data = $this->loadAttributes($namespace, $user); 352 353 io_unlock($lock); 354 355 if ($data === null) { 356 return false; 357 } 358 359 // Return just the keys. The values are cached. 360 return array_keys($data); 361 } 362 363 /** 364 * Checks if an attribute exists for a user in a given namespace. If user 365 * is present, must be an admin, otherwise defaults to currently logged in 366 * user. 367 * 368 * @param string $namespace 369 * @param string $attribute 370 * @param string|null $user 371 * 372 * @return bool 373 */ 374 public function exists($namespace, $attribute, $user = null) 375 { 376 if (!$this->success) { 377 return false; 378 } 379 380 $user = $this->validateUser($user); 381 if ($user === null) { 382 return false; 383 } 384 385 $lock = $namespace . '.' . $user; 386 io_lock($lock); 387 388 $data = $this->loadAttributes($namespace, $user); 389 390 io_unlock($lock); 391 392 if (!is_array($data)) { 393 return false; 394 } 395 396 return array_key_exists($attribute, $data); 397 } 398 399 /** 400 * Deletes attribute data in a specified namespace by its name. If user is 401 * present, must be an admin, otherwise defaults to currently logged in 402 * user. 403 * 404 * @param string $namespace 405 * @param string $attribute 406 * @param string|null $user 407 * 408 * @return bool 409 */ 410 public function del($namespace, $attribute, $user = null) 411 { 412 if (!$this->success) { 413 return false; 414 } 415 416 $user = $this->validateUser($user); 417 if ($user === null) { 418 return false; 419 } 420 421 $lock = $namespace . '.' . $user; 422 io_lock($lock); 423 424 $data = $this->loadAttributes($namespace, $user); 425 if ($data !== null) { 426 // Special case- if the attribute already does not exist, then 427 // return true. We are at the desired state. 428 if (array_key_exists($attribute, $data)) { 429 unset($data[$attribute]); 430 $result = $this->saveAttributes($namespace, $user, $data); 431 } else { 432 $result = true; 433 } 434 } else { 435 $result = false; 436 } 437 438 io_unlock($lock); 439 440 return $result; 441 } 442 443 /** 444 * Deletes all attribute data for a specified namespace for a user. Only 445 * usable the user themselves or an admin. 446 * 447 * @param string $namespace 448 * @param string $user 449 * 450 * @return bool 451 */ 452 public function purge($namespace, $user) 453 { 454 if (!$this->success) { 455 return false; 456 } 457 458 if ($this->validateUser($user) === null) { 459 return false; 460 } 461 462 $lock = $namespace . '.' . $user; 463 io_lock($lock); 464 465 $key = rawurlencode($namespace) . '.' . rawurlencode($user); 466 $filename = $this->storepath . "/" . $key; 467 468 if (file_exists($filename)) { 469 $result = unlink($filename); 470 } else { 471 // If the file does not exist, the desired end state has been 472 // reached. 473 $result = true; 474 } 475 476 io_unlock($lock); 477 478 return $result; 479 } 480 481 /** 482 * Retrieves a value for an attribute in a specified namespace. Returns 483 * retrieved value or null. $success out-parameter can be checked to check 484 * success (you may have false, null, 0, or '' as stored value). If user 485 * is present, must be an admin, otherwise defaults to currently logged in 486 * user. 487 * 488 * @param string $namespace 489 * @param string $attribute 490 * @param bool $success 491 * @param string|null $user 492 * 493 * @return bool 494 */ 495 public function get($namespace, $attribute, &$success = false, $user = null) 496 { 497 // Prepare the supplied success flag as false. It will be changed to 498 // true on success. 499 $success = false; 500 501 if (!$this->success) { 502 return false; 503 } 504 505 $user = $this->validateUser($user); 506 if ($user === null) { 507 return false; 508 } 509 510 $lock = $namespace . '.' . $user; 511 io_lock($lock); 512 513 $data = $this->loadAttributes($namespace, $user); 514 515 io_unlock($lock); 516 517 if ($data === null || !array_key_exists($attribute, $data)) { 518 return false; 519 } 520 521 $success = true; 522 return $data[$attribute]; 523 } 524} 525 526// vim:ts=4:sw=4:et: 527