mirror of
https://git.code.sf.net/p/seeddms/code
synced 2024-11-26 15:32:13 +00:00
643 lines
18 KiB
PHP
643 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Implementation of an extension management.
|
|
*
|
|
* SeedDMS can be extended by extensions. Extension usually implement
|
|
* hook.
|
|
*
|
|
* @category DMS
|
|
* @package SeedDMS
|
|
* @license GPL 2
|
|
* @version @version@
|
|
* @author Uwe Steinmann <uwe@steinmann.cx>
|
|
* @copyright 2011 Uwe Steinmann
|
|
* @version Release: @package_version@
|
|
*/
|
|
|
|
/**
|
|
* Class to represent an extension manager
|
|
*
|
|
* This class provides some very basic methods to manage extensions.
|
|
*
|
|
* @category DMS
|
|
* @package SeedDMS
|
|
* @author Markus Westphal, Malcolm Cowe, Uwe Steinmann <uwe@steinmann.cx>
|
|
* @copyright 2011 Uwe Steinmann
|
|
* @version Release: @package_version@
|
|
*/
|
|
class SeedDMS_Extension_Mgr {
|
|
/**
|
|
* @var string $extdir directory where extensions are located
|
|
* @access protected
|
|
*/
|
|
protected $extdir;
|
|
|
|
/**
|
|
* @var string $reposurl url for fetching list of extensions in repository
|
|
* @access protected
|
|
*/
|
|
protected $reposurl;
|
|
|
|
/**
|
|
* @var array[] $extconf configuration of all extensions
|
|
* @access protected
|
|
*/
|
|
protected $extconf;
|
|
|
|
/**
|
|
* @var string $cachedir directory where cached extension configuration
|
|
* is stored
|
|
* @access protected
|
|
*/
|
|
protected $cachedir;
|
|
|
|
/**
|
|
* @var string[] $errmsg list of error message from last operation
|
|
* @access protected
|
|
*/
|
|
protected $errmsgs;
|
|
|
|
/*
|
|
* Name of json file containg available extension from repository
|
|
*/
|
|
const repos_list_file = 'repository.json';
|
|
|
|
/**
|
|
* Compare two version
|
|
*
|
|
* This functions compares two version in the format x.x.x with x being
|
|
* an integer
|
|
*
|
|
* @param string $ver1
|
|
* @param string $ver2
|
|
* @return int -1 if $ver1 < $ver2, 0 if $ver1 == $ver2, 1 if $ver1 > $ver2
|
|
*/
|
|
static public function cmpVersion($ver1, $ver2) { /* {{{ */
|
|
$tmp1 = explode('.', $ver1);
|
|
$tmp2 = explode('.', $ver2);
|
|
if(intval($tmp1[0]) < intval($tmp2[0])) {
|
|
return -1;
|
|
} elseif(intval($tmp1[0]) > intval($tmp2[0])) {
|
|
return 1;
|
|
} else {
|
|
if(intval($tmp1[1]) < intval($tmp2[1])) {
|
|
return -1;
|
|
} elseif(intval($tmp1[1]) > intval($tmp2[1])) {
|
|
return 1;
|
|
} else {
|
|
if(intval($tmp1[2]) < intval($tmp2[2])) {
|
|
return -1;
|
|
} elseif(intval($tmp1[2]) > intval($tmp2[2])) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Constructor of extension manager
|
|
*
|
|
* Reads the configuration of all extensions and creates the
|
|
* configuration file if it does not exist and the extension dir
|
|
* is given
|
|
*/
|
|
public function __construct($extdir = '', $cachedir = '', $reposurl = '', $proxyurl='', $proxyuser='', $proxypass='') { /* {{{ */
|
|
$this->cachedir = $cachedir;
|
|
$this->extdir = $extdir;
|
|
$this->reposurl = $reposurl;
|
|
$this->proxyurl = $proxyurl;
|
|
$this->proxyuser = $proxyuser;
|
|
$this->proxypass = $proxypass;
|
|
$this->extconf = array();
|
|
if($extdir) {
|
|
if(!file_exists($this->getExtensionsConfFile())) {
|
|
$this->createExtensionConf();
|
|
}
|
|
include($this->getExtensionsConfFile());
|
|
if(!empty($EXT_CONF)) {
|
|
$this->extconf = $EXT_CONF;
|
|
}
|
|
}
|
|
} /* }}} */
|
|
|
|
private function getStreamContext() { /* {{{ */
|
|
if(!$this->proxyurl)
|
|
return null;
|
|
$url = parse_url($this->proxyurl);
|
|
$opts = [
|
|
$url['scheme'] => [
|
|
'proxy' => 'tcp://'.$url['host'].($url['port'] ? ':'.$url['port'] : ''),
|
|
'request_fulluri' => true,
|
|
]
|
|
];
|
|
if($this->proxyuser && $this->proxypass) {
|
|
$auth = base64_encode($this->proxyurl.':'.$this->proxypass);
|
|
$opts[$url['scheme']] = [
|
|
'header' => [
|
|
'Proxy-Authorization: Basic '.$auth
|
|
]
|
|
];
|
|
}
|
|
|
|
$context = stream_context_create($opts);
|
|
return $context;
|
|
} /* }}} */
|
|
|
|
protected function getExtensionsConfFile() { /* {{{ */
|
|
return $this->cachedir."/extensions.php";
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Get the configuration of extensions
|
|
*
|
|
* @return array[]
|
|
*/
|
|
public function getExtensionConfiguration() { /* {{{ */
|
|
return $this->extconf;
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Check if extension directory is writable
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isWritableExtDir() { /* {{{ */
|
|
return is_writable($this->extdir);
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Create the cached file containing extension information
|
|
*
|
|
* This function will always create a file, even if no extensions
|
|
* are installed.
|
|
*
|
|
* @return boolean true on success, false on error
|
|
*/
|
|
public function createExtensionConf() { /* {{{ */
|
|
$extensions = self::getExtensions();
|
|
$fp = @fopen(self::getExtensionsConfFile(), "w");
|
|
if($fp) {
|
|
if($extensions) {
|
|
foreach($extensions as $_ext) {
|
|
if(file_exists($this->extdir . "/" . $_ext . "/conf.php")) {
|
|
$content = file_get_contents($this->extdir . "/" . $_ext . "/conf.php");
|
|
fwrite($fp, $content);
|
|
}
|
|
}
|
|
}
|
|
fclose($fp);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Get names of locally installed extensions by scanning the extension dir
|
|
*
|
|
* @return string[] list of extension names
|
|
*/
|
|
protected function getExtensions() { /* {{{ */
|
|
$extensions = array();
|
|
if(file_exists($this->extdir)) {
|
|
$handle = opendir($this->extdir);
|
|
while ($entry = readdir($handle) ) {
|
|
if ($entry == ".." || $entry == ".")
|
|
continue;
|
|
else if (is_dir($this->extdir ."/". $entry))
|
|
array_push($extensions, $entry);
|
|
}
|
|
closedir($handle);
|
|
|
|
asort($extensions);
|
|
}
|
|
return $extensions;
|
|
} /* }}} */
|
|
|
|
static protected function Zip($source, $destination, $include_dir = false) { /* {{{ */
|
|
|
|
if (!extension_loaded('zip') || !file_exists($source)) {
|
|
return false;
|
|
}
|
|
|
|
if (file_exists($destination)) {
|
|
unlink ($destination);
|
|
}
|
|
|
|
$zip = new ZipArchive();
|
|
if (!$zip->open($destination, ZIPARCHIVE::CREATE)) {
|
|
return false;
|
|
}
|
|
$source = str_replace('\\', '/', realpath($source));
|
|
|
|
if (is_dir($source) === true) {
|
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST);
|
|
|
|
if ($include_dir) {
|
|
$arr = explode("/",$source);
|
|
$maindir = $arr[count($arr)- 1];
|
|
|
|
$source = "";
|
|
for ($i=0; $i < count($arr) - 1; $i++) {
|
|
$source .= '/' . $arr[$i];
|
|
}
|
|
|
|
$source = substr($source, 1);
|
|
|
|
$zip->addEmptyDir($maindir);
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
$file = str_replace('\\', '/', $file);
|
|
|
|
// Ignore "." and ".." folders
|
|
if( in_array(substr($file, strrpos($file, '/')+1), array('.', '..')) )
|
|
continue;
|
|
// Ignore all files and directories starting with a '.'
|
|
// but keep an .htaccess file
|
|
if( !preg_match('#\\.htaccess$#', $file) && preg_match('#/\\.#', $file) )
|
|
continue;
|
|
|
|
$file = realpath($file);
|
|
|
|
if (is_dir($file) === true) {
|
|
$zip->addEmptyDir(str_replace($source . '/', '', $file . '/'));
|
|
} else if (is_file($file) === true) {
|
|
$zip->addFromString(str_replace($source . '/', '', $file), file_get_contents($file));
|
|
}
|
|
}
|
|
} else if (is_file($source) === true) {
|
|
$zip->addFromString(basename($source), file_get_contents($source));
|
|
}
|
|
|
|
return $zip->close();
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Create zip archive of an extension
|
|
*
|
|
* @param string $extname name of extension
|
|
* @param string $version version of extension (x.y.z)
|
|
* @return string name of temporary file with archive
|
|
*/
|
|
public function createArchive($extname, $version) { /* {{{ */
|
|
if(!is_dir($this->extdir ."/". $extname))
|
|
return false;
|
|
|
|
$tmpfile = $this->cachedir."/".$extname."-".$version.".zip";
|
|
|
|
if(!SeedDMS_Extension_Mgr::Zip($this->extdir."/".$extname, $tmpfile)) {
|
|
return false;
|
|
}
|
|
// $cmd = "cd ".$this->extdir."/".$extname."; zip -r ".$tmpfile." .";
|
|
// exec($cmd);
|
|
|
|
return $tmpfile;
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Check content of extension directory or configuration of extension
|
|
*
|
|
* @param string|array $dir full path to extension directory or extension name
|
|
* or an array containing the configuration.
|
|
* @param boolean $noconstraints set to true if constraints to local seeddms
|
|
* installation shall not be checked.
|
|
*/
|
|
public function checkExtension($dir, $options=array()) { /* {{{ */
|
|
$this->errmsgs = array();
|
|
|
|
if(is_string($dir)) {
|
|
if(!file_exists($dir)) {
|
|
if(!file_exists($this->extdir.'/'.$dir))
|
|
return false;
|
|
else
|
|
$dir = $this->extdir.'/'.$dir;
|
|
}
|
|
if(!file_exists($dir."/conf.php")) {
|
|
$this->errmsgs[] = "Missing extension configuration";
|
|
return false;
|
|
}
|
|
include($dir."/conf.php");
|
|
if(!isset($EXT_CONF)) {
|
|
$this->errmsgs[] = "Missing \$EXT_CONF in configuration";
|
|
return false;
|
|
}
|
|
$extname = key($EXT_CONF);
|
|
if(!$extname || !preg_match('/[a-zA-Z_]*/', $extname)) {
|
|
$this->errmsgs[] = "Extension has invalid or no name";
|
|
return false;
|
|
}
|
|
|
|
$extconf = $EXT_CONF[$extname];
|
|
} elseif(is_array($dir)) {
|
|
$extconf = $dir;
|
|
/* If just the configuration is passed, then there is no way to check
|
|
* for existence of files.
|
|
*/
|
|
$options['nofiles'] = true;
|
|
}
|
|
|
|
if(!isset($extconf['constraints']['depends']['seeddms'])) {
|
|
$this->errmsgs[] = "Missing dependency on SeedDMS";
|
|
}
|
|
if(!isset($extconf['constraints']['depends']['php'])) {
|
|
$this->errmsgs[] = "Missing dependency on PHP";
|
|
}
|
|
if(!isset($extconf['version'])) {
|
|
$this->errmsgs[] = "Missing version information";
|
|
}
|
|
if(!isset($extconf['title'])) {
|
|
$this->errmsgs[] = "Missing title";
|
|
}
|
|
if(!isset($extconf['author'])) {
|
|
$this->errmsgs[] = "Missing author";
|
|
}
|
|
if(!isset($options['nofiles']) || $options['nofiles'] == false) {
|
|
if(!empty($extconf['language']['file']) && !file_exists($dir."/".$extconf['language']['file'])) {
|
|
$this->errmsgs[] = "Missing language file";
|
|
}
|
|
if(!empty($extconf['class']['file']) && !file_exists($dir."/".$extconf['class']['file'])) {
|
|
$this->errmsgs[] = "Missing class file";
|
|
}
|
|
if(!empty($extconf['icon']) && !file_exists($dir."/".$extconf['icon'])) {
|
|
$this->errmsgs[] = "Missing icon file";
|
|
}
|
|
}
|
|
|
|
if(!isset($options['noconstraints']) || $options['noconstraints'] == false) {
|
|
if(isset($extconf['constraints']['depends'])) {
|
|
foreach($extconf['constraints']['depends'] as $dkey=>$dval) {
|
|
switch($dkey) {
|
|
case 'seeddms':
|
|
$version = new SeedDMS_Version;
|
|
if(is_array($dval)) {
|
|
$fullfill = false;
|
|
foreach($dval as $ddval) {
|
|
$tmp = explode('-', $ddval, 2);
|
|
if(self::cmpVersion($tmp[0], $version->version()) > 0 || ($tmp[1] && self::cmpVersion($tmp[1], $version->version()) < 0))
|
|
; // No within version range
|
|
else
|
|
$fullfill = true;
|
|
}
|
|
if(!$fullfill)
|
|
$this->errmsgs[] = sprintf("Incorrect SeedDMS version (needs version \"%s\")", implode('" or "', $dval));
|
|
} elseif(is_string($dval)) {
|
|
$tmp = explode('-', $dval, 2);
|
|
if(self::cmpVersion($tmp[0], $version->version()) > 0 || ($tmp[1] && self::cmpVersion($tmp[1], $version->version()) < 0))
|
|
$this->errmsgs[] = sprintf("Incorrect SeedDMS version (needs version %s)", $extconf['constraints']['depends']['seeddms']);
|
|
}
|
|
break;
|
|
case 'php':
|
|
$tmp = explode('-', $dval, 2);
|
|
if(self::cmpVersion($tmp[0], phpversion()) > 0 || ($tmp[1] && self::cmpVersion($tmp[1], phpversion()) < 0))
|
|
$this->errmsgs[] = sprintf("Incorrect PHP version (needs version %s)", $dval);
|
|
break;
|
|
case 'phpext':
|
|
if(is_array($dval) && $dval) {
|
|
$extlist = get_loaded_extensions();
|
|
foreach($dval as $d) {
|
|
if(!in_array($d, $extlist))
|
|
$this->errmsgs[] = sprintf("Missing php extension '%s'", $d);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
$tmp = explode('-', $dval, 2);
|
|
if(isset($this->extconf[$dkey]['version'])) {
|
|
if(self::cmpVersion($tmp[0], $this->extconf[$dkey]['version']) > 0 || ($tmp[1] && self::cmpVersion($tmp[1], $this->extconf[$dkey]['version']) < 0))
|
|
$this->errmsgs[] = sprintf("Incorrect version of extension '%s' (needs version '%s' but provides '%s')", $dkey, $dval, $this->extconf[$dkey]['version']);
|
|
} else {
|
|
$this->errmsgs[] = sprintf("Missing extension or version for '%s'", $dkey);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if($this->errmsgs)
|
|
return false;
|
|
|
|
return true;
|
|
} /* }}} */
|
|
|
|
static protected function rrmdir($dir) { /* {{{ */
|
|
if (is_dir($dir)) {
|
|
$objects = scandir($dir);
|
|
foreach ($objects as $object) {
|
|
if ($object != "." && $object != "..") {
|
|
if (filetype($dir."/".$object) == "dir") self::rrmdir($dir."/".$object); else unlink($dir."/".$object);
|
|
}
|
|
}
|
|
reset($objects);
|
|
rmdir($dir);
|
|
}
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Update an extension
|
|
*
|
|
* This function will replace an existing extension or add a new extension
|
|
* The passed file has to be zipped content of the extension directory not
|
|
* including the directory itself.
|
|
*
|
|
* @param string $file name of extension archive
|
|
* @return boolean true on success, othewise false
|
|
*/
|
|
public function updateExtension($file) { /* {{{ */
|
|
/* unzip the extension in a temporary directory */
|
|
$newdir = $this->cachedir ."/ext.new";
|
|
/* First remove a left over from a previous extension */
|
|
if(file_exists($newdir)) {
|
|
self::rrmdir($newdir);
|
|
}
|
|
if(!mkdir($newdir, 0755)) {
|
|
$this->errmsgs[] = "Cannot create temp. extension directory";
|
|
return false;
|
|
}
|
|
$zip = new ZipArchive;
|
|
$res = $zip->open($file);
|
|
if ($res === TRUE) {
|
|
$zip->extractTo($newdir);
|
|
$zip->close();
|
|
} else {
|
|
$this->errmsgs[] = "Cannot open extension file";
|
|
return false;
|
|
}
|
|
// $cmd = "cd ".$newdir."; unzip ".$file;
|
|
// exec($cmd);
|
|
|
|
/* Check if extension is complete and fullfills the constraints */
|
|
if(!self::checkExtension($newdir)) {
|
|
self::rrmdir($newdir);
|
|
return false;
|
|
}
|
|
|
|
include($newdir."/conf.php");
|
|
$extname = key($EXT_CONF);
|
|
|
|
/* Create the target directory */
|
|
if(!is_dir($this->extdir)) {
|
|
if(!mkdir($this->extdir, 0755)) {
|
|
$this->errmsgs[] = "Cannot create extension directory";
|
|
self::rrmdir($newdir);
|
|
return false;
|
|
}
|
|
} elseif(is_dir($this->extdir ."/". $extname)) {
|
|
$this->rrmdir($this->extdir ."/". $extname);
|
|
}
|
|
/* Move the temp. created ext directory to the final location */
|
|
if(!rename($newdir, $this->extdir ."/". $extname)) {
|
|
$this->rrmdir($this->extdir ."/". $extname);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Get list of extensions from cached repository index
|
|
*
|
|
* This function returns the whole repository index file separated in
|
|
* single lines. Each line is either a comment if it starts with an '#'
|
|
* or a json encoded array containing the extension configuration.
|
|
*
|
|
* Run SeedDMS_Extension_Mgr::updateExtensionList() to ensure the
|
|
* currently cached extension list file is up to date.
|
|
*
|
|
* @return string[] list of json strings or comments
|
|
*/
|
|
public function getRawExtensionList() { /* {{{ */
|
|
if(file_exists($this->cachedir."/".self::repos_list_file)) {
|
|
return file($this->cachedir."/".self::repos_list_file);
|
|
} else {
|
|
return array();
|
|
}
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Get list of extensions from cached repository index
|
|
*
|
|
* This function reads the cache respository index and returns
|
|
* a list of extension configurations. Only the most recent version
|
|
* of an extension will be included.
|
|
*
|
|
* Run SeedDMS_Extension_Mgr::updateExtensionList() to ensure the
|
|
* currently cached extension list file is up to date.
|
|
*
|
|
* @return array[] list of extension configurations
|
|
*/
|
|
public function getExtensionList() { /* {{{ */
|
|
$list = self::getRawExtensionList();
|
|
$result = array();
|
|
$vcache = array(); // keep highest version of extension
|
|
foreach($list as $e) {
|
|
if($e[0] != '#') {
|
|
$re = json_decode($e, true);
|
|
if(!isset($result[$re['name']])) {
|
|
$result[$re['name']] = $re;
|
|
$vcache[$re['name']] = $re['version'];
|
|
} elseif(self::cmpVersion($re['version'], $vcache[$re['name']]) > 0) {
|
|
$result[$re['name']] = $re;
|
|
$vcache[$re['name']] = $re['version'];
|
|
}
|
|
}
|
|
}
|
|
return $result;
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Get list of version of an extension from cached repository index
|
|
*
|
|
* This function reads the cache respository index and returns
|
|
* a list of extension configurations. Only those extensions will
|
|
* be included which maches the given name.
|
|
*
|
|
* Run SeedDMS_Extension_Mgr::updateExtensionList() to ensure the
|
|
* currently cached extension list file is up to date.
|
|
*
|
|
* @return array[] list of extension configurations
|
|
*/
|
|
public function getExtensionListByName($extname) { /* {{{ */
|
|
$list = self::getRawExtensionList();
|
|
$result = array();
|
|
foreach($list as $e) {
|
|
if($e[0] != '#') {
|
|
$re = json_decode($e, true);
|
|
if($re['name'] == $extname) {
|
|
$result[$re['version']] = $re;
|
|
}
|
|
}
|
|
}
|
|
uksort($result, function($a, $b){return SeedDMS_Extension_Mgr::cmpVersion($b, $a);});
|
|
return $result;
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Import list of extension from repository
|
|
*
|
|
* @param boolean $force force download even if file already exists
|
|
*/
|
|
public function updateExtensionList($version='', $force=false) { /* {{{ */
|
|
if($this->reposurl) {
|
|
if(!file_exists($this->cachedir."/".self::repos_list_file) || $force) {
|
|
if(false !== ($file = @file_get_contents($this->reposurl.($version ? '?seeddms_version='.$version : ''), false, $this->getStreamContext()))) {
|
|
if(is_array($http_response_header)) {
|
|
$parts=explode(' ',$http_response_header[0]);
|
|
if(count($parts)>1) //HTTP/1.0 <code> <text>
|
|
if(intval($parts[1]) != 200) {
|
|
$this->errmsgs[] = 'Getting extension list returned http code ('.$parts[1].')';
|
|
return false;
|
|
}
|
|
}
|
|
if(!$file) {
|
|
$this->errmsgs[] = 'Extension list is empty';
|
|
return false;
|
|
}
|
|
file_put_contents($this->cachedir."/".self::repos_list_file, $file);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Download an extension from the repository
|
|
*
|
|
* @param string $file filename of extension
|
|
*/
|
|
public function getExtensionFromRepository($file) { /* {{{ */
|
|
$content = file_get_contents($this->reposurl."/".$file, false, $this->getStreamContext());
|
|
$tmpfile = tempnam(sys_get_temp_dir(), '');
|
|
file_put_contents($tmpfile, $content);
|
|
return $tmpfile;
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Return last error message
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getErrorMsg() { /* {{{ */
|
|
if($this->errmsgs)
|
|
return $this->errmsgs[0];
|
|
else
|
|
return '';
|
|
} /* }}} */
|
|
|
|
/**
|
|
* Return all error messages
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function getErrorMsgs() { /* {{{ */
|
|
return $this->errmsgs;
|
|
} /* }}} */
|
|
}
|