Compare commits

..

2 Commits

Author SHA1 Message Date
Alan Cell
84548c4f63 Ability to add custom modules 2020-11-12 05:51:31 +01:00
Alan Cell
e5eccf32a7 Merge tag 'v28.1.1.OS' into develop
v28.1.1.OS
2020-11-07 12:52:02 +01:00
13 changed files with 337 additions and 7 deletions

View File

@@ -5,7 +5,7 @@ if(!file_exists('config.php')){
}
include ('config.php');
if(!isset($_REQUEST['g']) || !isset($_REQUEST['n'])){
header("Location:".CLIENT_BASE_URL."login.php");
header("Location:".CLIENT_BASE_URL."login.php");
exit();
}
$group = $_REQUEST['g'];
@@ -14,9 +14,16 @@ $name= $_REQUEST['n'];
$groups = array('admin','modules');
if($group == 'admin' || $group == 'modules'){
$name = str_replace("..","",$name);
$name = str_replace("..","",$name);
$name = str_replace("/","",$name);
include APP_BASE_PATH.'/'.$group.'/'.$name.'/index.php';
}else if ($group == 'extension'){
$name = str_replace("..","",$name);
$name = str_replace("/","",$name);
$moduleName = $name;
$moduleGroup = 'extensions';
$extensionIndex = APP_BASE_PATH.'/../extensions/'.$name.'/web/index.php';
include APP_BASE_PATH.'extensions/wrapper.php';
}else{
exit();
}
}

View File

@@ -36,3 +36,7 @@ define('ALL_CLIENT_BASE_PATH', '/var/www/icehrm.app/icehrmapp/');
define('LDAP_ENABLED', true);
define('RECRUITMENT_ENABLED', false);
define('APP_WEB_URL', 'https://icehrm.com');
if (!defined('EXTENSIONS_URL')) {
define('EXTENSIONS_URL', str_replace('/web/', '/extensions/', BASE_URL));
}

View File

@@ -0,0 +1,31 @@
<?php
use Classes\ExtensionManager;
use Utils\LogManager;
if (!isset($extensionIndex)) {
exit();
}
define('MODULE_PATH',APP_BASE_PATH.'extensions/'.$moduleName);
include APP_BASE_PATH.'header.php';
$extensionManager = new ExtensionManager();
$meta = $extensionManager->getExtensionMetaData($moduleName);
if (!$meta) {
LogManager::getInstance()->error("Extension metadata.json not found for $moduleName");
exit();
}
if ($meta->headless) {
LogManager::getInstance()->error("Extension running in headless mode for $moduleName");
exit();
}
?>
<script type="text/javascript" src="<?=BASE_URL.'dist/vendorReact.js'?>?v=<?=$jsVersion?>"></script>
<script type="text/javascript" src="<?=BASE_URL.'dist/vendorAntd.js'?>?v=<?=$jsVersion?>"></script>
<script type="text/javascript" src="<?=BASE_URL.'dist/vendorAntdIcons.js'?>?v=<?=$jsVersion?>"></script>
<script type="text/javascript" src="<?=BASE_URL.'dist/vendorAntv.js'?>?v=<?=$jsVersion?>"></script>
<script type="text/javascript" src="<?=BASE_URL.'dist/vendorOther.js'?>?v=<?=$jsVersion?>"></script>
<script type="text/javascript" src="<?=EXTENSIONS_URL.$moduleName.'/dist/'.$moduleName.'.js'?>?v=<?=$jsVersion?>"></script>
<?php
include $extensionIndex;
include APP_BASE_PATH.'footer.php';
?>

View File

@@ -215,6 +215,26 @@ if (defined('SYM_CLIENT')) {
<?php }?>
<?php foreach($extensions as $menu){?>
<?php if(count($menu['menu']) == 0){continue;}?>
<li class="treeview" ref="<?="extension_".str_replace(" ", "_", $menu['name'])?>">
<a href="#">
<i class="fa <?=!isset($mainIcons[$menu['name']])?"fa-th":$mainIcons[$menu['name']];?>"></i></i> <span><?=\Classes\LanguageManager::tran($menu['name'])?></span>
<i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu" id="<?="extension_".str_replace(" ", "_", $menu['name'])?>">
<?php foreach ($menu['menu'] as $item){?>
<li>
<a data-turbolinks="true" href="<?=CLIENT_BASE_URL?>?g=extension&n=<?=$item['name']?>&m=<?="extension_".str_replace(" ", "_", $menu['name'])?>">
<i class="fa <?=!isset($item['icon'])?"fa-angle-double-right":$item['icon']?>"></i> <?=\Classes\LanguageManager::tran($item['label'])?>
</a>
</li>
<?php }?>
</ul>
</li>
<?php }?>
<?php
if(file_exists(CLIENT_PATH.'/third_party_meta.json')){
$tpModules = json_decode(file_get_contents(CLIENT_PATH.'/third_party_meta.json'),true);

View File

@@ -291,6 +291,12 @@ foreach ($ams as $am) {
}
}
$extensionManager = new \Classes\ExtensionManager();
$extensionData = $extensionManager->setupExtensions();
$extensionIcons = $extensionData[0];
$extensionTemp = $extensionData[1];
$extensionMenus = array_keys($extensionIcons);
foreach ($adminModulesTemp as $k => $v) {
ksort($adminModulesTemp[$k]);
}
@@ -299,6 +305,10 @@ foreach ($userModulesTemp as $k => $v) {
ksort($userModulesTemp[$k]);
}
foreach ($extensionTemp as $k => $v) {
ksort($extensionTemp[$k]);
}
$adminIcons = json_decode(file_get_contents(CLIENT_PATH.'/admin/meta.json'), true);
$adminMenus = array_keys($adminIcons);
@@ -332,8 +342,6 @@ foreach ($userMenus as $menu) {
}
}
$mainIcons = array_merge($adminIcons, $userIcons);
foreach ($userModulesTemp as $k => $v) {
if (!in_array($k, $added)) {
$arr = array("name"=>$k,"menu"=>$userModulesTemp[$k]);
@@ -341,6 +349,25 @@ foreach ($userModulesTemp as $k => $v) {
}
}
$extensions = array();
foreach ($extensionMenus as $menu) {
if (isset($extensionTemp[$menu])) {
$arr = array("name"=>$menu,"menu"=>$extensionTemp[$menu]);
$extensions[] = $arr;
$added[] = $menu;
}
}
foreach ($extensionTemp as $k => $v) {
if (!in_array($k, $added)) {
$arr = array("name"=>$k,"menu"=>$extensionTemp[$k]);
$extensions[] = $arr;
}
}
// Merge icons
$mainIcons = array_merge($adminIcons, $userIcons, $extensionIcons);
//Remove modules having no permissions
if (!empty($user)) {
if (!empty($user->user_roles)) {
@@ -393,4 +420,24 @@ if (!empty($user)) {
}
}
}
foreach ($extensions as $fk => $menu) {
foreach ($menu['menu'] as $key => $item) {
// If the user's once of the user roles are blacklisted for the module
$commonRoles = array_intersect($item['user_roles_blacklist'], $userRoles);
if (!empty($commonRoles)) {
unset($extensions[$fk]['menu'][$key]);
}
if (!in_array($user->user_level, $item['user_levels'])) {
if (!empty($userRoles)) {
$commonRoles = array_intersect($item['user_roles'], $userRoles);
if (empty($commonRoles)) {
unset($extensions[$fk]['menu'][$key]);
}
} else {
unset($extensions[$fk]['menu'][$key]);
}
}
}
}
}

View File

@@ -278,4 +278,12 @@ abstract class AbstractModuleManager
{
BaseService::getInstance()->addCalculationHook($code, $name, $class, $method);
}
public function install() {
}
public function uninstall() {
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Classes;
use Utils\LogManager;
class ExtensionManager
{
const GROUP = 'extension';
protected function processExtensionInDB() {
$dbModule = new \Modules\Common\Model\Module();
$extensions = $dbModule->Find("mod_group = ?", array(self::GROUP));
$extensionsInDB = [];
foreach ($extensions as $dbm) {
$extensionsInDB[$dbm->name] = $dbm;
ModuleAccessService::getInstance()->setModule($dbm->name, self::GROUP, $dbm);
}
return $extensionsInDB;
}
public function getExtensionsPath() {
return APP_BASE_PATH.'../extensions/';
}
public function getExtensionMetaData($extensionName)
{
return json_decode(file_get_contents($this->getExtensionsPath().$extensionName.'/meta.json'));
}
public function setupExtensions() {
$menu = [];
$extensions = [];
$extensionDirs = scandir($this->getExtensionsPath());
$currentLocation = 0;
$extensionsInDB = $this->processExtensionInDB();
$needToInstall = false;
foreach ($extensionDirs as $extensionDir) {
if (is_dir($this->getExtensionsPath().$extensionDir) && $extensionDir != '.' && $extensionDir != '..') {
$meta = $this->getExtensionMetaData($extensionDir);
$arr = [];
$arr['name'] = $extensionDir;
$arr['label'] = $meta->label;
$arr['icon'] = $meta->icon;
$arr['menu'] = $meta->menu[0];
$arr['order'] = 0;
$arr['status'] = 'Enabled';
$arr['user_levels'] = $meta->user_levels;
$arr['user_roles'] = isset($meta->user_roles)?$meta->user_roles:"";
$arr['model_namespace'] = $meta->model_namespace;
$arr['manager'] = $meta->manager;
// Add menu
$menu[$meta->menu[0]] = $meta->menu[1];
//Check in admin dbmodules
if (isset($extensionsInDB[$arr['name']])) {
$dbModule = $extensionsInDB[$arr['name']];
$arr['name'] = $dbModule->name;
$arr['label'] = $dbModule->label;
$arr['icon'] = $dbModule->icon;
$arr['menu'] = $dbModule->menu;
$arr['status'] = $dbModule->status;
$arr['user_levels'] = json_decode($dbModule->user_levels);
$arr['user_roles'] = empty($dbModule->user_roles)
? [] : json_decode($dbModule->user_roles);
$arr['user_roles_blacklist'] = empty($dbModule->user_roles_blacklist)
? [] : json_decode($dbModule->user_roles_blacklist);
} else {
$dbModule = new \Modules\Common\Model\Module();
$dbModule->menu = $arr['menu'];
$dbModule->name = $arr['name'];
$dbModule->label = $arr['label'];
$dbModule->icon = $arr['icon'];
$dbModule->mod_group = self::GROUP;
$dbModule->mod_order = $arr['order'];
$dbModule->status = "Enabled";
$dbModule->version = isset($meta->version)?$meta->version:"";
$dbModule->update_path = self::GROUP.">".$extensionDir;
$dbModule->user_levels = isset($meta->user_levels)?json_encode($meta->user_levels):"";
$dbModule->user_roles = isset($meta->user_roles)?json_encode($meta->user_roles):"";
$ok = $dbModule->Save();
if (!$ok) {
LogManager::getInstance()->error('Error saving module: '.$dbModule->name);
}
$needToInstall = $ok;
}
/* @var \Classes\AbstractModuleManager */
$manager = $this->includeModuleManager($extensionDir, $arr);
if ($dbModule->status == 'Disabled') {
continue;
}
if ($needToInstall) {
$manager->install();
}
$menuName = $arr['menu'];
if (!isset($extensions[$menuName])) {
$extensions[$menuName] = array();
}
if (!$meta->headless) {
if ($arr['order'] == '0' || $arr['order'] == '') {
$extensions[$menuName]["Z".$currentLocation] = $arr;
$currentLocation++;
} else {
$extensions[$arr['menu']]["A".$arr['order']] = $arr;
}
}
$initializer = $manager->getInitializer();
if ($initializer !== null) {
$initializer->setBaseService(BaseService::getInstance());
$initializer->init();
}
}
}
return [$menu, $extensions];
}
public function includeModuleManager($name, $data)
{
include($this->getExtensionsPath().$name.'/'.$name.'.php');
$moduleManagerClass = $data['manager'];
/* @var \Classes\AbstractModuleManager $moduleManagerObj*/
$moduleManagerObj = new $moduleManagerClass();
$moduleManagerObj->setModuleObject($data);
$moduleManagerObj->setModuleType(self::GROUP);
$moduleManagerObj->setModulePath(CLIENT_PATH.'/'.self::GROUP.'/'.$name);
\Classes\BaseService::getInstance()->addModuleManager($moduleManagerObj);
return $moduleManagerObj;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Classes;
abstract class IceExtension extends AbstractModuleManager
{
public function initializeUserClasses()
{
// TODO: Implement initializeUserClasses() method.
}
public function initializeFieldMappings()
{
// TODO: Implement initializeFieldMappings() method.
}
public function initializeDatabaseErrorMappings()
{
// TODO: Implement initializeDatabaseErrorMappings() method.
}
}

View File

@@ -8,6 +8,8 @@
namespace Classes\Migration;
use Utils\LogManager;
abstract class AbstractMigration
{
protected $file;
@@ -16,7 +18,7 @@ abstract class AbstractMigration
protected $lastError;
public function __construct($file)
public function __construct($file = null)
{
$this->file = $file;
}
@@ -50,6 +52,7 @@ abstract class AbstractMigration
$ret = $this->db()->Execute($sql);
if (!$ret) {
$this->lastError = $this->db()->ErrorMsg();
LogManager::getInstance()->error('Error in migration: '.$this->lastError);
}
return $ret;
}

View File

@@ -12,7 +12,7 @@ class ModuleAccess
* @param $name
* @param $group
*/
public function __construct($name, $group)
public function __construct($name, $group = 'extension')
{
$this->name = $name;
$this->group = $group;

1
extensions/gitkeep Executable file
View File

@@ -0,0 +1 @@
git keep

View File

@@ -359,6 +359,48 @@ gulp.task('modules-js', (done) => {
.pipe(gulp.dest('./web/dist'));
});
gulp.task('extension-js', (done) => {
let extension = process.argv.filter((item) => item.substr(0, 3) === '--x');
if (extension.length === 1) {
extension = extension[0].substr(3);
}
// map them to our stream function
return browserify({
entries: [`extensions/${extension}/web/js/index.js`],
basedir: '.',
debug: true,
cache: {},
packageCache: {},
})
.external(vendorsFlat)
.transform('babelify', {
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
],
presets: ['@babel/preset-env', '@babel/preset-react'],
extensions: ['.js', '.jsx'],
})
.transform(require('browserify-css'))
.bundle()
.pipe(source(`${extension}.js`))
.pipe(buffer())
.pipe(ifElse(!isProduction, () => sourcemaps.init({ loadMaps: true })))
.pipe(ifElse(isProduction, () => uglifyes(
{
compress: true,
mangle: {
reserved: [],
},
},
)))
.pipe(ifElse(isProduction, () => javascriptObfuscator({
compact: true,
})))
.pipe(ifElse(!isProduction, () => sourcemaps.write('./')))
.pipe(gulp.dest(`./extensions/${extension}/dist`));
});
gulp.task('watch', () => {
gulp.watch('web/admin/src/*/*.js', gulp.series('admin-js'));

View File

@@ -943,3 +943,7 @@ table.dataTable{
.table-row-dark {
background-color: #fbfbfb;
}
.mod-tab {
margin-bottom:0px;margin-left:5px;border-bottom: none;
}