* @desc Describes a farm animal, providing ways to interract with its config / users / files * also provides static methods to create animals / get global infos about the farm. */ // Set the plugin include path depending if we are under DokuWiki or SOAPServer context if(!defined('DOKU_FARM_PLUGIN')) define('DOKU_FARM_PLUGIN', defined('DOKU_FARMPLUGINLOADED') ? DOKU_INC.'lib/plugins/farm/' : './'); require_once(DOKU_FARM_PLUGIN.'infos.animal.class.php'); class dokuwiki_farm_animal { private $name = ''; private $path = ''; var $manager = null; private $infos = null; private $wikiconf = array(); private static $farmconf = array(); /** * @param $name animal name * @param $manager object that must handle error(), success(), nicesize(), getLang() ... calls */ function __construct($name = '', $manager = null) { self::getConf(); if(self::exists($name)) { $this->name = $name; $this->manager = & $manager; $this->path = self::$farmconf['farmfsroot'].self::$farmconf['barn'].$this->name; } } /** * Return animal name * @return string */ public function getName() { return $this->name; } /** * Return animal path in filesystem, based on farmfsroot farmconfig parameter * @return string */ public function getPath() { return $this->path; } /** * Return url of animal ressource, with respect of animal config (useslash/userewrite) and farm config (userewrite/farmrewrite) * @param $id ressource id (eg. namespace ...) * @param $media boolean indicating whether the ressource is a media or a page * @param $xmlencode boolean indicating whether output xml entities must be encoded * @return url string */ public function getUrl($id = '', $media = false, $xmlencode = true) { self::getConf(); $wc = $this->getWikiConf(); if($id) { $id = str_replace('/', ':', $id); if(isset($wc['editable']['useslash'])) if($wc['editable']['useslash']) $id = str_replace(':', '/', $id); if(isset($wc['protected']['useslash'])) if($wc['protected']['useslash']) $id = str_replace(':', '/', $id); } $aur = false; if(isset($wc['editable']['userewrite'])) if($wc['editable']['userewrite']) $aur = true; if(isset($wc['protected']['userewrite'])) if($wc['protected']['userewrite']) $aur = true; $url = self::$farmconf['farmwebroot']; if(!self::$farmconf['userewrite']) { $url .= self::$farmconf['farmer']; if(!$aur) { if(!$media) { $url .= 'doku.php?animal='.$this->getName(); if($id) $url .= ($xmlencode ? '&' : '&').'id='.$id; }else $url .= 'lib/exe/fetch.php?w=&l=&cache=cache&media='.$id; }else{ if(!$media) { $url .= $id.'?animal='.$this->getName(); }else $url .= '_media/'.$id.'?w=&h=&cache=cache'; } }else{ if(!self::$farmconf['farmrewrite']) $url .= self::$farmconf['barn']; $url .= $this->getName(); if(!$aur) { if(!$media) { if($id) $url .= '/doku.php?id='.$id; }else $url .= 'lib/exe/fetch.php?w=&l=&cache=cache&media='.$id; }else{ if(!$media) { $url .= '/'.$id; }else $url .= '_media/'.$id.'?w=&h=&cache=cache'; } } return $url; } /** * Return the value of an animal's metadata * @param $name metadata identifier * @param $default value to return if "name" metadata not found * @return string */ public function getMetadata($name = '', $default = null) { global $conf; if(!self::exists($this->name)) return null; self::getConf(); $metadata = array(); foreach(explode("\n", @file_get_contents($this->path.'/animal.meta')) as $l) { $l = preg_split('`\s+:\s+`', $l); if(count($l) == 2) $metadata[$l[0]] = $l[1]; } if($name == '') return $metadata; return isset($metadata[$name]) ? $metadata[$name] : $default; } /** * Set animal's metadata * @param $name metadata identifier * @param $value metadata value * @return success as boolean */ public function setMetadata($name = '', $value = '') { global $conf; if($name == '') return false; if(!self::exists($this->name)) return null; self::getConf(); $metadata = array(); foreach(explode("\n", @file_get_contents($this->path.'/animal.meta')) as $l) { $l = preg_split('`\s+:\s+`', $l); if(count($l) == 2) $metadata[$l[0]] = $l[1]; } if(is_null($value)) { if(isset($metadata[$name])) unset($metadata[$name]); }else $metadata[$name] = $value; $out = array(); foreach($metadata as $k => $v) $out[] = $k.' : '.$v; if($fp = fopen($this->path.'/animal.meta', 'w')) { fwrite($fp, implode("\n", $out)); fclose($fp); return true; } return false; } /** * Returns animal's status * @return string */ public function getStatus() { return $this->getMetadata('status', 'open'); } /** * Returns animal's lock state * @return string */ public function getLockState() { return $this->getMetadata('lockstate'); } /** * Returns an animal_infos object loaded from the animal * @return dokuwiki_farm_animal_infos object */ public function getInfos() { if(!$this->infos) $this->infos = new dokuwiki_farm_animal_infos($this); return $this->infos; } /** * Gather and returns the animal wiki configuration as a multidimensionnal array * @return multidimensionnal associative array */ public function getWikiConf() { if(!self::exists($this->name)) return null; $getfromas = array( 'local.protected.php' => 'protected', 'local.php' => 'editable' ); foreach($getfromas as $from => $as) { if(@file_exists($this->path.'/conf/'.$from)) { $conf = array(); foreach(explode("\n", @file_get_contents($this->path.'/conf/'.$from)) as $line) { if(preg_match('`^\$conf\[([^]]+(\]\[[^]]+)?)\]\s*=\s*([^;]+);`', $line, $m)) { $path = array_map(create_function('$e', 'return preg_replace(\'`^(\\\'|")?([a-zA-Z0-9_-]+)(\\\'|")?$`i\', \'$2\', trim($e));'), explode('][', $m[1])); $value = $m[3]; eval('$conf['.implode('][', $path).'] = $value;'); } } $this->wikiconf[$as] = $conf; } } krsort($this->wikiconf); return $this->wikiconf; } /** * Set one of the animal wiki configuration parameters * @param $mode string pointing the target configuration * @param $path array containing the successive path elents used to access the target configuration parameter * @param $value value to be set * @return success as boolean */ public function setWikiConf($mode = 'editable', $path = array(), $value = null) { if(!in_array($mode, array('editable', 'protected'))) return false; if(!count($path)) return false; if(!count($this->wikiconf)) $this->getWikiConf(); $c = & $this->wikiconf[$mode]; foreach($path as $p) { if(!isset($c[$p])) $c[$p] = array(); $c = & $c[$p]; } $c = $value; return true; } /** * Turns a multidimensionnal animal wiki configuration array into an array of php code strings * @param $head path above element * @param $el array element * @return configuration php strings */ private function buildconfrecursive($head, $el) { if(is_array($el)) { $r = array(); foreach($el as $k => $v) $r = array_merge($r, $this->buildconfrecursive(array_merge($head, array($k)), $v)); return $r; } if(is_bool($el)) $el = $el ? 'true' : 'false'; elseif(is_numeric($el)) $el = "$el"; elseif( !(substr($el, 0, 1) == "'" && substr($el, -1) == "'") && !(substr($el, 0, 1) == '"' && substr($el, -1) == '"') && !preg_match('`^DOKU_[A-Z_]+`', $el) && !preg_match('`\.DOKU_[A-Z_]+$`', $el) ) $el = "'".$el."'"; return array('$conf[\''.implode("']['", $head).'\'] = '.$el.';'); } /** * Save the animal's wiki configuration * @return success as boolean */ public function saveWikiConf() { if(!self::exists($this->name)) return false; $savefromto = array( 'local.protected.php' => 'protected', 'local.php' => 'editable' ); $r = true; foreach($savefromto as $to => $from) { $conf = implode("\n", $this->buildconfrecursive(array(), $this->wikiconf[$from])); if($fp = @fopen($this->path.'/conf/'.$to, 'w')) { fwrite($fp, "wikiconf = array(); return $r; } /** * Returns the animal's raw wiki configuration as a single string * @param $mode string pointing the target configuration * @return configuration as string */ public function getRawWikiConf($mode = 'editable') { if(!self::exists($this->name)) return null; $rel = array( 'protected' => 'local.protected.php', 'editable' => 'local.php' ); if(!in_array($mode, array_keys($rel))) return null; if(!@file_exists($this->path.'/conf/'.$rel[$mode])) return null; $r = array(); foreach(explode("\n", @file_get_contents($this->path.'/conf/'.$rel[$mode])) as $line) { if(preg_match('`^(\$conf\[[^]]+(\]\[[^]]+)?\]\s*=\s*[^;]+);`', $line, $m)) $r[] = $m[1]; } return implode("\n", $r); } /** * Save animal wiki configuration from string * @param $mode string pointing the target configuration * @param $conf string * @return success as boolean */ public function saveRawWikiConf($mode = 'editable', $conf = '') { if(!self::exists($this->name)) return false; $rel = array( 'protected' => 'local.protected.php', 'editable' => 'local.php' ); if(!in_array($mode, array_keys($rel))) return false; if(!@file_exists($this->path.'/conf/'.$rel[$mode])) return false; $this->wikiconf = array(); $conf = implode("\n", array_map(create_function('$e', '$e = trim($e); if(substr($e, -1) != \';\') $e .= \';\'; return $e;'), explode("\n", $conf))); if($fp = @fopen($this->path.'/conf/'.$rel[$mode], 'w')) { fwrite($fp, "getWikiConf(); $conf = array_merge($wc['editable'], $wc['protected']); if(!isset($conf['authtype']) || empty($conf['authtype'])) $conf['authtype'] = 'plain'; if($conf['authtype'] == 'plain') { foreach(explode("\n", @file_get_contents($this->path.'/conf/users.auth.php')) as $line) { $line = preg_replace('/#.*$/','',$line); //ignore comments $line = trim($line); if(empty($line)) continue; $row = split(':', $line, 5); $groups = split(',', $row[4]); $users[$row[0]] = array( 'pass' => $row[1], 'name' => urldecode($row[2]), 'mail' => $row[3], 'grps' => $groups ); } }else{ $auth_class = 'auth_'.$conf['authtype']; if(!class_exists($auth_class)) if(@file_exists(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php')) include DOKU_INC.'auth/'.$conf['authtype'].'.class.php'; if(class_exists($auth_class)) { $localauth = new $auth_class(); $localauth->users = null; $users = $localauth->retrieveUsers(); } } $conf = $fconf; return $users; } /** * EXPERIMENTAL * Add a user to the animal * @param $user user login * @param $pass user clear password * @param $name user name * @param $mail user email * @param $grps array of groups the user is in * @return success as boolean, also false if backend not compatible */ public function addUser($user, $pass, $name, $mail, $grps=null) { if($name == '') $name = $user; $users = $this->getUsers(); if(isset($users[$user])) return false; global $conf; $fconf = $conf; $conf = array(); // Load animal conf $wc = $this->getWikiConf(); $conf = array_merge($wc['editable'], $wc['protected']); if(!isset($conf['authtype']) || empty($conf['authtype'])) $conf['authtype'] = 'plain'; if($conf['authtype'] == 'plain') { $authfile = @file_get_contents($this->path.'/conf/users.auth.php'); $pass = auth_cryptPassword($pass, isset($conf['passcrypt']) ? $conf['passcrypt'] : 'smd5'); if(!is_array($grps)) $grps = array($conf['defaultgroup']); $groups = join(',', $grps); $userline = join(':', array($user, $pass, $name, $mail, $groups))."\n"; if($authfile) $authfile .= $userline; if($authfile) { if($fp = fopen($this->path.'/conf/users.auth.php', 'w')) { fwrite($fp, $authfile); fclose($fp); $r = true; }else $r = false; }else $r = false; }else{ $auth_class = 'auth_'.$conf['authtype']; if(!class_exists($auth_class)) if(@file_exists(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php')) include DOKU_INC.'auth/'.$conf['authtype'].'.class.php'; if(class_exists($auth_class)) { $localauth = new $auth_class(); $localauth->users = null; $r = ($localauth->createUser($user, $pass, $name, $mail, $grps) != null); }else $r = false; } $conf = $fconf; return $r; } /** * EXPERIMENTAL * Delete a user from the animal * @param $user user login * @return success as boolean, also false if backend not compatible */ public function deleteUser($user) { if($user == '') return false; $users = $this->getUsers(); if(!isset($users[$user])) return false; global $conf; $fconf = $conf; $conf = array(); // Load animal conf $wc = $this->getWikiConf(); $conf = array_merge($wc['editable'], $wc['protected']); if(!isset($conf['authtype']) || empty($conf['authtype'])) $conf['authtype'] = 'plain'; if($conf['authtype'] == 'plain') { $out = array(); foreach(explode("\n", @file_get_contents($this->path.'/conf/users.auth.php')) as $line) { if(substr($line, 0, strlen($user) + 1) != $user.':') $out[] = $line; } if($fp = fopen($this->path.'/conf/users.auth.php', 'w')) { fwrite($fp, implode("\n", $out)); fclose($fp); $r = true; }else $r = false; }else{ $auth_class = 'auth_'.$conf['authtype']; if(!class_exists($auth_class)) if(@file_exists(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php')) include DOKU_INC.'auth/'.$conf['authtype'].'.class.php'; if(class_exists($auth_class)) { $localauth = new $auth_class(); $localauth->users = null; $r = ($localauth->deleteUsers(array($user)) > 0); }else $r = false; } $conf = $fconf; return $r; } /** * Scans the filesystem to gather informations about files / directories * @param $p path * @param $l level of depth * @param $bp start path to strip * @param $pref base64 encoded path reference * @return array of fs items descriptors */ private function scanfs($p='', $l=0, $bp='', $pref='') { if($p == '') return array(); if($bp == '') $bp = $p; if(substr($bp, -1) != '/') $bp .= '/'; $fs = array(); foreach(scandir($p) as $i) { if($i == '.' || $i == '..') continue; if(@is_file($p.'/'.$i)) { $fi = stat($p.'/'.$i); $fs[base64_encode(substr($p.'/'.$i, strlen($bp)))] = array( 'type' => 'file', 'name' => $i, 'size' => $fi['size'], 'mtime' => $fi['mtime'], 'lvl' => $l, 'pref' => $pref ); } if(@is_dir($p.'/'.$i)) { $fs[base64_encode(substr($p.'/'.$i, strlen($bp)))] = array( 'type' => 'dir', 'name' => $i, 'lvl' => $l, 'pref' => $pref ); $fs = array_merge($fs, $this->scanfs($p.'/'.$i, $l+1, $bp, base64_encode(substr($p.'/'.$i, strlen($bp))))); } } return $fs; } /** * Gather informations about the animal's filesystem * @return array of fs items descriptors */ public function getFs() { if(!self::exists($this->name)) return false; return $this->scanfs($this->getPath()); } /** * Returns the content of a file in the animal's filesystem * @return file content as a string */ public function getFileContent($file) { if(!self::exists($this->name)) return ''; if(!@file_exists($this->path.'/'.$file)) return ''; return @file_get_contents($this->path.'/'.$file); } /** * Set the content of a file in the animal's filesystem * @param $file file content as a string * @param $ctn file content to be put as a string * @param $delete boolean telling whether to delete the pointed file * @return success as boolean */ public function setFileContent($file, $ctn='', $delete=false) { if(!self::exists($this->name)) return false; if($delete && !@file_exists($this->path.'/'.$file)) return false; if($delete) return @unlink($this->path.'/'.$file); if($fp = fopen($this->path.'/'.$file, 'w')) { fwrite($fp, $ctn); fclose($fp); return true; }else return false; } /** * Deletes the animal, calling animal hooks * @return success as boolean */ public function delete() { self::getConf(); if(@file_exists(DOKU_FARM_PLUGIN.'hooks.animal.class.php')) { @include DOKU_FARM_PLUGIN.'hooks.animal.class.php'; } $hookhandler = class_exists('dokuwiki_farm_animal_hooks') ? new dokuwiki_farm_animal_hooks($handler) : null; if($hookhandler) { if(!$hookhandler->trigger('delete', 'before', array($this))) { if($handler) $handler->errors[] = array('code' => 'animal_delete_eventhandlercancel_failure'); return false; } } if($this->name == self::$farmconf['animaltemplate']) return false; $r = self::rmdir($this->path); if(!$r) return false; if($hookhandler) $hookhandler->trigger('delete', 'after', array($this->name)); return true; } /** * Creates a new animal, calling animal hooks * @param $name name of the new animal * @param $template template to use for clonning as string * @param $host host to bind if virtual mode in use * @param $handler handler object featuring a errors array property * @return success as boolean */ public static function createNew($name = '', $template = null, $host = null, $handler = null) { self::getConf(); if(@file_exists(DOKU_FARM_PLUGIN.'hooks.animal.class.php')) { @include DOKU_FARM_PLUGIN.'hooks.animal.class.php'; } $hookhandler = class_exists('dokuwiki_farm_animal_hooks') ? new dokuwiki_farm_animal_hooks($handler) : null; if($hookhandler) { if(!$hookhandler->trigger('create', 'before', array($name, $template))) { if($handler) $handler->errors[] = array('code' => 'animal_new_eventhandlercancel_failure'); return false; } } if($farmconf['farmmaxanimals'] > 0) { if(count(self::listAnimals()) >= $farmconf['farmmaxanimals']) { if($handler) $handler->errors[] = array('code' => 'animal_new_farmfull_failure'); return false; } } if($farmconf['farmmaxsize'] > 0) { if(count(self::farmSize()) >= $farmconf['farmmaxsize']) { if($handler) $handler->errors[] = array('code' => 'animal_new_farmtoobig_failure'); return false; } } if(!$template) $template = self::$farmconf['animaltemplate']; $name = strtr($name, "àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ", "aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY"); $name = preg_replace('`\s+`', '_', $name); if(!preg_match('`^[a-z0-9._-]+`i', $name)) { if($handler) $handler->errors[] = array('code' => 'animal_new_badname_failure'); return false; } if(self::exists($name) || ($name == trim(self::$farmconf['farmer'], '/')) || @is_dir(self::$farmconf['farmfsroot'].self::$farmconf['barn'].$name)) { if($handler) $handler->errors[] = array('code' => 'animal_new_namealreadyinuse_failure', 'data' => array($name)); return false; } if(!self::exists($template)) { if($handler) $handler->errors[] = array('code' => 'animal_new_templatenotfound_failure', 'data' => array($template)); return false; } $cph = create_function('$path,$isfile', ' if(!$isfile) return; $ctn = @file_get_contents($path); if(!preg_match(\'\\{ANIMAL\\}\', $ctn)) return; $ctn = str_replace(\'{ANIMAL}\', \''.$name.'\', $ctn); if($fp = fopen($path, \'w\')) { fwrite($fp, $ctn); fclose($fp); } '); if(!self::cp_r(self::$farmconf['farmfsroot'].self::$farmconf['barn'].$template, self::$farmconf['farmfsroot'].self::$farmconf['barn'].$name, false, array(), $cph)) { if($handler) $handler->errors[] = array('code' => 'animal_new_templatecopy_failure'); return false; } if($fp = fopen(self::$farmconf['farmfsroot'].self::$farmconf['barn'].$name.'/animal.meta', 'w')) { fwrite($fp, 'creation_date : '.time()); fclose($fp); }else{ if($handler) $handler->errors[] = array('code' => 'animal_new_metacreate_failure'); return false; } if(!self::exists($name)) { if($handler) $handler->errors[] = array('code' => 'animal_new_createdespite_failure'); return false; } $a = new dokuwiki_farm_animal($name, $handler->manager); if(!$a->setWikiConf('protected', array('basedir'), 'DOKU_ANIMAL_BASEDIR.\''.$name.'\'') || !$a->setWikiConf('editable', array('title'), $name)) { if($handler) $handler->errors[] = array('code' => 'animal_new_setconfig_failure'); return false; } if(!$a->saveWikiConf()) { if($handler) $handler->errors[] = array('code' => 'animal_new_saveconfig_failure'); return false; } if($host && self::$farmconf['virtual']) { $vh = @file_get_contents(DOKU_FARM_PLUGIN.'virtual_hosts.php'); $e = array_filter(array_merge(array(self::$farmconf['farmerhost']), array_map(create_function('$l', 'return preg_replace(\'`^([^\s]+)\s`\', \'$1\', $l);'), explode("\n", $vh))), create_function('$v', 'return ($v == \''.$host.'\');')); if(!count($e)) { $fp = fopen(DOKU_FARM_PLUGIN.'virtual_hosts.php', 'w'); if($fp) { fwrite($fp, $vh."\n".$host.' '.$name); fclose($fp); }else{ if($handler) $handler->errors[] = array('code' => 'animal_new_savevirtualhost_failure'); return false; } }else{ if($handler) $handler->errors[] = array('code' => 'animal_new_virtualhostexists_failure'); return false; } } if($hookhandler) $hookhandler->trigger('create', 'after', array($a)); return $name; } /** * Gather the list of animals * @return array of dokuwiki_farm_animal objects */ public static function listAnimals($manager = null) { self::getConf(); $a = array(); foreach(scandir(self::$farmconf['farmfsroot'].self::$farmconf['barn']) as $i) { if($i == '.' && $i == '..') continue; if(self::exists($i)) $a[] = new dokuwiki_farm_animal($i, $manager); } return $a; } /** * Get the farm disk usage * @return size in bytes */ public static function farmSize() { return array_sum(array_map(create_function('$a', 'return $a->getInfos()->getSize();'), self::listAnimals())); } /** * Copy / move a directory contents * @param $src source path * @param $dest destination path * @param $mv boolean telling whether copying or moving contents * @param $avoid path of elemnts that musn't be copied / moved * @param $copyhook function to handle cpoy operations * @return success as boolean */ private static function cp_r($src, $dest, $mv=false, $avoid=array(), $copyhook = null) { if(in_array($src, $avoid)) return true; if(@is_file($src)) { $r = copy($src, $dest); chmod($dest, fileperms($src)); if($mv) $r &= unlink($src); if($copyhook) $copyhook($dest, true); return $r; } if(!@is_dir($dest)) mkdir($dest, fileperms($src)); $r = true; foreach(scandir($src) as $i) { if($i == '.' || $i == '..') continue; $r &= self::cp_r($src.'/'.$i, $dest.'/'.$i, $mv, $avoid); } if($mv) $r &= rmdir($src); if($copyhook) $copyhook($dest, false); return $r; } /** * Removes a directory * @param $p path to the directory * @return success as boolean */ private static function rmdir($p) { $r = true; foreach(scandir($p) as $i) { if($i == '.' || $i == '..') continue; if(@is_dir($p.'/'.$i)) $r &= self::rmdir($p.'/'.$i); if(@is_file($p.'/'.$i)) $r &= unlink($p.'/'.$i); if(!$r) break; } if($r) rmdir($p); return $r; } /** * Gather and return the farm configuration * @return associative array of farm configuration parameters */ public static function getConf() { if(!self::$farmconf) { $farmconf = array(); include(DOKU_FARM_PLUGIN.'config.php'); self::$farmconf = $farmconf; } return self::$farmconf; } /** * Check if an animal exists * @param $name name to test * @return boolean */ public static function exists($name = '') { if($name == '') return false; $name = strtr($name, "àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ", "aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY"); $name = preg_replace('`\s+`', '_', $name); if(!preg_match('`^[a-z0-9._-]+`i', $name)) return false; self::getConf(); return @is_dir(self::$farmconf['farmfsroot'].self::$farmconf['barn'].$name) && @file_exists(self::$farmconf['farmfsroot'].self::$farmconf['barn'].$name.'/animal.meta'); } } ?>