| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- <?php
- /**
- * The zen file of extension module of ZenTaoPMS.
- *
- * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
- * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
- * @author Yuting Wang<wangyuting@easycorp.ltd>
- * @package extension
- * @link https://www.zentao.net
- */
- class extensionZen extends extension
- {
- /**
- * 安全性校验。
- * Check safe.
- *
- * @access protected
- * @return void
- */
- protected function checkSafe()
- {
- /* 判断是否要跳转到安全校验页面。 */
- $statusFile = $this->loadModel('common')->checkSafeFile();
- if($statusFile) die($this->fetch('extension', 'safe', 'statusFile=' . helper::safe64Encode($statusFile)));
- }
- /**
- * 根据插件代号获取指定的钩子文件地址。
- * Get hook file for install or uninstall.
- *
- * @param string $extension
- * @param string $hook preinstall|postinstall|preuninstall|postuninstall
- * @access protected
- * @return string|false
- */
- protected function getHookFile($extension, $hook)
- {
- $hookFile = $this->extension->pkgRoot . "$extension/hook/$hook.php";
- if(file_exists($hookFile)) return $hookFile;
- return false;
- }
- /**
- * 根据数据库数据获取依赖当前插件的其他插件。
- * Get depends extension by database.
- *
- * @param string $extension
- * @access protected
- * @return array
- */
- protected function getDependsByDB($extension)
- {
- $extensionInfo = $this->extension->getInfoFromDB($extension);
- $dependsList = $this->extension->getDependsExtension($extension);
- $result = array();
- if($dependsList)
- {
- foreach($dependsList as $dependsExtension)
- {
- $depends = json_decode($dependsExtension->depends, true);
- if(empty($depends[$extension])) continue;
- if($this->compareForLimit($extensionInfo->version, $depends[$extension])) $result[] = $dependsExtension->name;
- }
- }
- return $result;
- }
- /**
- * 插件安装前的合规校验。
- * Check before installation.
- *
- * @param string $extension
- * @param string $ignoreCompatible
- * @param string $ignoreLink
- * @param string $overrideFile
- * @param string $overrideLink
- * @param string $installType
- * @access protected
- * @return bool
- */
- protected function checkExtension($extension, $ignoreCompatible, $ignoreLink, $overrideFile, $overrideLink, $installType)
- {
- /* 安全性校验。 */
- $statusFile = $this->loadModel('common')->checkSafeFile();
- if($statusFile)
- {
- $okFile = str_replace($this->app->getBasePath(), '', $statusFile);
- $this->view->error = sprintf($this->lang->noticeOkFile, $okFile, $statusFile);
- return false;
- }
- /* Check the package file exists or not. */
- $packageFile = $this->extension->getPackageFile($extension);
- if(!file_exists($packageFile))
- {
- $this->view->error = sprintf($this->lang->extension->errorPackageNotFound, $packageFile);
- return false;
- }
- /* Checking the extension paths. */
- $return = $this->checkExtensionPaths($extension);
- if($this->session->dirs2Created == false) $this->session->set('dirs2Created', $return->dirs2Created, 'admin'); // Save the dirs to be created.
- if($return->result != 'ok')
- {
- $this->view->error = $return->errors;
- return false;
- }
- /* Extract the package. */
- $return = $this->extractPackage($extension);
- if($return->result != 'ok')
- {
- $this->view->error = sprintf($this->lang->extension->errorExtracted, $packageFile, $return->error);
- return false;
- }
- /* Get condition. e.g. zentao|depends|conflicts. */
- $condition = $this->extension->getCondition($extension);
- $installedExts = $this->extension->getLocalExtensions('installed');
- if(!$this->checkCompatible($extension, $condition, $ignoreCompatible, $ignoreLink, $installType)) return false;
- if(!$this->checkConflicts($condition, $installedExts)) return false;
- if(!$this->checkDepends($condition, $installedExts)) return false;
- if(!$this->checkFile($extension, $overrideFile, $overrideLink)) return false;
- return true;
- }
- /**
- * 检查插件包的目录结构是否禅道目录结构冲突。
- * Check files in the package conflicts with exists files or not.
- *
- * @param string $extension
- * @access protected
- * @return object
- */
- protected function checkFileConflict($extension)
- {
- $return = new stdclass();
- $return->result = 'ok';
- $return->error = '';
- $appRoot = $this->app->getAppRoot();
- $extensionFiles = $this->extension->getFilesFromPackage($extension);
- foreach($extensionFiles as $extensionFile)
- {
- $compareFile = $appRoot . str_replace($this->extension->pkgRoot . $extension . DS, '', $extensionFile);
- if(!file_exists($compareFile)) continue;
- if(md5_file($extensionFile) != md5_file($compareFile)) $return->error .= $compareFile . '<br />';
- }
- if($return->error != '') $return->result = 'fail';
- return $return;
- }
- /**
- * 插件插件的hook文件到禅道目录。
- * Copy hookFiles to zentao.
- *
- * @param string $extension
- * @access public
- * @return void
- */
- public function copyHookFiles($extension)
- {
- $extHookPath = $this->extension->pkgRoot . $extension . DS . 'hook' . DS;
- $hookPath = $this->app->getBasePath() . DS . 'hook' . DS;
- if(!is_dir($extHookPath)) return;
- if(!is_dir($hookPath)) return;
- foreach(glob($hookPath . '*') as $hookFile) $this->extension->classFile->removeFile($hookFile);
- $this->extension->classFile->copyDir($extHookPath, $hookPath);
- }
- /**
- * 执行插件安装程序。
- * Install extension.
- *
- * @param string $extension
- * @param string $type
- * @param string $upgrade
- * @access protected
- * @return void
- */
- protected function installExtension($extension, $type, $upgrade)
- {
- /* The preInstall hook file. */
- $hook = $upgrade == 'yes' ? 'preupgrade' : 'preinstall';
- if($preHookFile = $this->getHookFile($extension, $hook)) include $preHookFile;
- /* Save to database. */
- $this->extension->saveExtension($extension, $type);
- /* Copy files to target directory. */
- $this->view->files = $this->copyPackageFiles($extension);
- /* Execute the install.sql. */
- $needExecuteDB = file_exists($this->extension->getDBFile($extension, 'install'));
- if($upgrade == 'no' && $needExecuteDB)
- {
- $return = $this->extension->executeDB($extension, 'install');
- if($return->result != 'ok')
- {
- $this->view->error = sprintf($this->lang->extension->errorInstallDB, $return->error);
- return false;
- }
- }
- /* Update status, dirs, files and installed time. */
- $data = array();
- $data['code'] = $extension;
- $data['status'] = 'installed';
- $data['dirs'] = $this->session->dirs2Created;
- $data['files'] = $this->view->files;
- $data['installedTime'] = helper::now();
- $this->extension->updateExtension($data);
- $this->session->set('dirs2Created', array(), 'admin'); // clean the session.
- $this->view->downloadedPackage = false;
- /* The postInstall hook file. */
- $hook = $upgrade == 'yes' ? 'postupgrade' : 'postinstall';
- if($postHookFile = $this->getHookFile($extension, $hook)) include $postHookFile;
- return true;
- }
- /**
- * 解压插件包到pkg目录。
- * Extract an extension.
- *
- * @param string $extension
- * @access protected
- * @return object
- */
- protected function extractPackage($extension)
- {
- $return = new stdclass();
- $return->result = 'ok';
- $return->error = '';
- /* 验证extension目录是否允许写入。 */
- $extensionRoot = $this->app->getExtensionRoot();
- if(is_dir($extensionRoot) && !is_writable($extensionRoot))
- {
- return (object)array('result' => 'fail', 'error' => strip_tags(sprintf($this->lang->extension->errorDownloadPathNotWritable, $extensionRoot, $extensionRoot)));
- }
- /* try remove pre extracted files. */
- $extensionPath = $this->extension->pkgRoot . $extension;
- if(is_dir($extensionPath)) $this->extension->classFile->removeDir($extensionPath);
- /* 获取插件包所在目录。 */
- $packageFile = $this->extension->getPackageFile($extension);
- /* 解压插件包到extensionPath目录。 */
- $this->app->loadClass('pclzip', true);
- $zip = new pclzip($packageFile);
- $files = $zip->listContent();
- $pathinfo = pathinfo($files[0]['filename']);
- $removePath = isset($pathinfo['dirname']) && $pathinfo['dirname'] != '.' ? $pathinfo['dirname'] : $pathinfo['basename'];
- /* 修复漏洞:过滤不安全的文件。 Fix vulnerability: filter unsafe files. */
- $unsafePatterns = '/\.\.[\/\\\]|^[^\w]|\w:[\/\\\]/'; // 非法路径包括: ../ | ..\ | /opt/ | c:/ | c:\
- $filteredFiles = array();
- $lastIndex = 0;
- foreach($files as $file)
- {
- $filename = $file['filename'];
- $lastIndex = $file['index'];
- if(preg_match($unsafePatterns, $filename)) $filteredFiles[] = $lastIndex;
- }
- $indexes = array();
- $startIndex = 0;
- foreach($filteredFiles as $index)
- {
- $preIndex = $index - 1;
- if($preIndex < 0) return $return;
- $indexes[] = $preIndex == $startIndex ? $startIndex : $startIndex . '-' . $preIndex;
- $startIndex = $index + 1;
- }
- if($startIndex <= $lastIndex) $indexes[] = $startIndex == $lastIndex ? $startIndex : $startIndex . '-' . $lastIndex;
- if($indexes && $zip->extract(PCLZIP_OPT_PATH, $extensionPath, PCLZIP_OPT_BY_INDEX, $indexes, PCLZIP_OPT_REMOVE_PATH, $removePath) == 0)
- {
- $return->result = 'fail';
- $return->error = $zip->errorInfo(true);
- }
- return $return;
- }
- /**
- * 复制插件包文件到禅道目录。
- * Copy package files.
- *
- * @param string $extension
- * @access protected
- * @return array
- */
- protected function copyPackageFiles($extension)
- {
- $appRoot = $this->app->getAppRoot();
- $extensionDir = $this->extension->pkgRoot . $extension . DS;
- $paths = scandir($extensionDir);
- $copiedFiles = array();
- foreach($paths as $path)
- {
- if($path == 'db' || $path == 'doc' || $path == 'hook' || $path == '..' || $path == '.') continue;
- $result = $this->extension->classFile->copyDir($extensionDir . $path, $appRoot . $path, true);
- $copiedFiles = zget($result, 'copiedFiles', array());
- }
- foreach($copiedFiles as $key => $copiedFile)
- {
- $copiedFiles[$copiedFile] = md5_file($copiedFile);
- unset($copiedFiles[$key]);
- }
- return $copiedFiles;
- }
- /**
- * 卸载插件前备份即将删除的表。
- * Backup db when uninstall extension.
- *
- * @param string $extension
- * @access protected
- * @return string|false
- */
- protected function backupDB($extension)
- {
- $sqls = file_get_contents($this->extension->getDBFile($extension, 'uninstall'));
- $sqls = explode(';', $sqls);
- /* Get tables for backup. */
- $backupTables = array();
- foreach($sqls as $sql)
- {
- $sql = str_replace('zt_', $this->config->db->prefix, $sql);
- $sql = preg_replace('/IF EXISTS /i', '', trim($sql));
- if(preg_match('/TABLE +`?([^` ]*)`?/i', $sql, $out))
- {
- if(!empty($out[1])) $backupTables[$out[1]] = $out[1];
- }
- }
- /* Back up database. */
- $zdb = $this->app->loadClass('zdb');
- if($backupTables)
- {
- $backupFile = $this->app->getTmpRoot() . $extension . '.' . date('Ymd') . '.sql';
- $result = $zdb->dump($backupFile, $backupTables);
- if($result->result) return $backupFile;
- }
- return false;
- }
- /**
- * 标记此插件是否被禁用。
- * Mark package active or disabled
- *
- * @param string $extension
- * @param string $action disabled|active
- * @access protected
- * @return bool
- */
- protected function togglePackageDisable($extension, $action = 'disabled')
- {
- if(!is_dir($this->extension->pkgRoot . $extension)) return true;
- $disabledFile = $this->extension->pkgRoot . $extension . DS . 'disabled';
- if($action == 'disabled') touch($disabledFile);
- if($action == 'active' && file_exists($disabledFile)) unlink($disabledFile);
- return true;
- }
- /**
- * 检查安装前的文件夹权限。
- * Check extension files.
- *
- * @param string $extension
- * @access private
- * @return object
- */
- private function checkExtensionPaths($extension)
- {
- $checkResult = new stdclass();
- $checkResult->result = 'ok';
- $checkResult->errors = '';
- $checkResult->mkdirCommands = '';
- $checkResult->chmodCommands = '';
- $checkResult->dirs2Created = array();
- /* 如果extension目录没有创建pkg文件夹并且创建pkg文件夹失败。 */
- if(!is_dir($this->extension->pkgRoot) && !mkdir($this->extension->pkgRoot))
- {
- $checkResult->errors .= sprintf($this->lang->extension->errorTargetPathNotExists, $this->extension->pkgRoot) . '<br />';
- $checkResult->mkdirCommands .= "sudo mkdir -p {$this->extension->pkgRoot}<br />";
- $checkResult->chmodCommands .= "sudo chmod -R 777 {$this->pkgRoot}<br />";
- }
- /* 如果extension目录有pkg文件夹但是pkg文件夹不可写。 */
- if(is_dir($this->extension->pkgRoot) && !is_writable($this->extension->pkgRoot))
- {
- $checkResult->errors .= sprintf($this->lang->extension->errorTargetPathNotWritable, $this->extension->pkgRoot) . '<br />';
- $checkResult->chmodCommands .= "sudo chmod -R 777 {$this->extension->pkgRoot}<br />";
- }
- /* 检查插件目录对应的禅道目录权限。 */
- $checkResult = $this->checkExtractPath($extension, $checkResult);
- if($checkResult->errors) $checkResult->result = 'fail';
- $checkResult->mkdirCommands = empty($checkResult->mkdirCommands) ? '' : '<code>' . str_replace('/', DIRECTORY_SEPARATOR, $checkResult->mkdirCommands) . '</code>';
- $checkResult->errors .= $this->lang->extension->executeCommands . $checkResult->mkdirCommands;
- if(PHP_OS == 'Linux') $checkResult->errors .= empty($checkResult->chmodCommands) ? '' : '<code>' . $checkResult->chmodCommands . '</code>';
- return $checkResult;
- }
- /**
- * 检查安装插件时对应的禅道目录权限。
- * Check extension path read-write permission.
- *
- * @param string $extension
- * @param object $checkResult
- * @access private
- * @return object
- */
- private function checkExtractPath($extension, $checkResult)
- {
- $appRoot = $this->app->getAppRoot();
- $paths = $this->extension->getPathsFromPackage($extension);
- foreach($paths as $path)
- {
- if($path == 'db' || $path == 'doc') continue;
- $path = rtrim($appRoot . $path, '/');
- if(is_dir($path))
- {
- /* 检查插件包里的代码文件夹对应禅道目录是否可写。 */
- if(!is_writable($path))
- {
- $checkResult->errors .= sprintf($this->lang->extension->errorTargetPathNotWritable, $path) . '<br />';
- $checkResult->chmodCommands .= "sudo chmod -R 777 $path<br />";
- }
- }
- else
- {
- /* 检查插件包里的代码文件的父目录对应禅道目录是否可写。 */
- $parentDir = dirname($path);
- while(!file_exists($parentDir)) $parentDir = dirname($parentDir);
- if(!is_writable($parentDir))
- {
- $checkResult->errors .= sprintf($this->lang->extension->errorTargetPathNotWritable, $path) . '<br />';
- $checkResult->chmodCommands .= "sudo chmod -R 777 $path<br />";
- $checkResult->errors .= sprintf($this->lang->extension->errorTargetPathNotExists, $path) . '<br />';
- $checkResult->mkdirCommands .= "sudo mkdir -p $path<br />";
- }
- elseif(!mkdir($path, 0777, true))
- {
- /* 如果目录不存在并且创建目录失败。 */
- $checkResult->errors .= sprintf($this->lang->extension->errorTargetPathNotExists, $path) . '<br />';
- $checkResult->mkdirCommands .= "sudo mkdir -p $path<br />";
- }
- if(file_exists($path) && realpath($path) != $this->extension->pkgRoot) $checkResult->dirs2Created[] = $path;
- }
- }
- return $checkResult;
- }
- /**
- * 插件兼容性检查。
- * Extension compatibility check.
- *
- * @param string $extension
- * @param object $condition
- * @param string $ignoreCompatible
- * @param string $ignoreLink
- * @param string $installType
- * @access private
- * @return bool
- */
- private function checkCompatible($extension, $condition, $ignoreCompatible, $ignoreLink, $installType)
- {
- /* 不兼容版本检查。 */
- /* Check version incompatible */
- $incompatible = $condition->zentao['incompatible'];
- if($this->extension->checkVersion($incompatible))
- {
- $this->view->error = $this->lang->extension->errorIncompatible;
- return false;
- }
- /* 兼容版本检查。 */
- /* Check version compatible. */
- $zentaoCompatible = $condition->zentao['compatible'];
- if(!$this->extension->checkVersion((string)$zentaoCompatible) && $ignoreCompatible == 'no')
- {
- $this->view->error = sprintf($this->lang->extension->errorCheckIncompatible, $installType, $ignoreLink, $installType, inlink('obtain'));
- return false;
- }
- return true;
- }
- /**
- * 插件与插件之间的冲突检查。
- * Check conflicts.
- *
- * @param object $condition
- * @param array $installedExts
- * @access private
- * @return bool
- */
- private function checkConflicts($condition, $installedExts)
- {
- $conflicts = $condition->conflicts;
- if($conflicts)
- {
- $conflictsExt = '';
- foreach($conflicts as $code => $limit)
- {
- if(isset($installedExts[$code]))
- {
- if($this->compareForLimit($installedExts[$code]->version, $limit)) $conflictsExt .= $installedExts[$code]->name . " ";
- }
- }
- if($conflictsExt)
- {
- $this->view->error = sprintf($this->lang->extension->errorConflicts, $conflictsExt);
- return false;
- }
- }
- return true;
- }
- /**
- * 相关依赖插件检查。
- * Check depends.
- *
- * @param object $condition
- * @param array $installedExts
- * @access private
- * @return bool
- */
- private function checkDepends($condition, $installedExts)
- {
- $depends = $condition->depends;
- if($depends)
- {
- $dependsExt = '';
- foreach($depends as $code => $limit)
- {
- $noDepends = false;
- if(isset($installedExts[$code]))
- {
- if($this->compareForLimit($installedExts[$code]->version, $limit, 'noBetween')) $noDepends = true;
- }
- else
- {
- $noDepends = true;
- }
- $extVersion = '';
- if($limit != 'all')
- {
- $extVersion .= '(';
- if(!empty($limit['min'])) $extVersion .= '>=v' . $limit['min'];
- if(!empty($limit['max'])) $extVersion .= ' <=v' . $limit['max'];
- $extVersion .=')';
- }
- if($noDepends) $dependsExt .= $code . $extVersion . ' ' . html::a(inlink('obtain', 'type=bycode¶m=' . helper::safe64Encode($code)), $this->lang->extension->installExt, '_blank') . '<br />';
- }
- if($noDepends)
- {
- $this->view->error = sprintf($this->lang->extension->errorDepends, $dependsExt);
- return false;
- }
- }
- return true;
- }
- /**
- * 插件文件和禅道已有文件的冲突检查。
- * Check files in the package conflicts with exists files or not.
- *
- * @param string $extension
- * @param string $overrideFile
- * @param string $overrideLink
- * @access private
- * @return bool
- */
- private function checkFile($extension, $overrideFile, $overrideLink)
- {
- if($overrideFile == 'no')
- {
- $return = $this->checkFileConflict($extension);
- if($return->result != 'ok')
- {
- $this->view->error = sprintf($this->lang->extension->errorFileConflicted, $return->error, $overrideLink, inlink('obtain'));
- return false;
- }
- }
- return true;
- }
- /**
- * 检查当前版本是否包含在指定版本内。
- * Compare for limit data.
- *
- * @param string $version
- * @param array|string $limit
- * @param string $type
- * @access private
- * @return bool
- */
- private function compareForLimit($version, $limit, $type = 'between')
- {
- $result = false;
- if(empty($limit)) return true;
- if($limit == 'all') return true;
- if(!empty($limit['min']) && $version >= $limit['min']) $result = true;
- if(!empty($limit['max']) && $version <= $limit['max']) $result = true;
- if(!empty($limit['max']) && $version > $limit['max'] && $result) $result = false;
- /* 如果取的不是被包含则返回取反的布尔值。 */
- if($type != 'between') return !$result;
- return $result;
- }
- }
|