1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\System\Module;
9:
10: use Aurora\Modules\Core\Models\User;
11: use Aurora\System\Exceptions\ApiException;
12: use Aurora\System\Managers\Response;
13:
14: /**
15: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
16: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
17: * @copyright Copyright (c) 2019, Afterlogic Corp.
18: *
19: * @package Api
20: */
21: class Manager
22: {
23: /**
24: * This array contains a list of modules
25: *
26: * @var array
27: */
28: protected $_aModules = array();
29:
30: /**
31: * This array contains a list of modules paths
32: *
33: * @var array
34: */
35: protected $_aModulesPaths = null;
36:
37: /**
38: * This array contains a list of modules
39: *
40: * @var array
41: */
42: protected $_aAllowedModulesName = array(
43: 'core' => 'Core'
44: );
45:
46: /**
47: * @var array
48: */
49: private $_aTemplates;
50:
51: /**
52: * @var array
53: */
54: private $_aResults;
55:
56: /**
57: * @var \Aurora\System\EventEmitter
58: */
59: private $oEventEmitter;
60:
61: /**
62: * @var \Aurora\System\ObjectExtender
63: */
64: private $oObjectExtender;
65:
66: /**
67: * @var \Aurora\System\Exceptions\Exception
68: */
69: private $oLastException;
70:
71: /**
72: * @var array
73: */
74: private $aModulesSettings;
75:
76: /**
77: *
78: */
79: public function __construct()
80: {
81: $this->oEventEmitter = \Aurora\System\EventEmitter::getInstance();
82: $this->oObjectExtender = \Aurora\System\ObjectExtender::getInstance();
83: }
84:
85: /**
86: *
87: * @return self
88: */
89: public static function createInstance()
90: {
91: return new self();
92: }
93:
94: /**
95: *
96: * @return void
97: */
98: public function loadModules()
99: {
100: $oUser = \Aurora\System\Api::authorise();
101: $oCoreModule = $this->loadModule('Core');
102:
103: if ($oCoreModule instanceof AbstractModule) {
104: $oTenant = null;
105: if ($oUser instanceof User && $oUser->Role !== \Aurora\System\Enums\UserRole::SuperAdmin) {
106: $oTenant = \Aurora\Modules\Core\Module::Decorator()->GetTenantWithoutRoleCheck($oUser->IdTenant);
107: }
108: foreach ($this->GetModulesPaths() as $sModuleName => $sModulePath) {
109: $bIsModuleDisabledForTenant = \Aurora\Modules\Core\Module::Decorator()->IsModuleDisabledForObject($oTenant, $sModuleName);
110: $bIsModuleDisabledForUser = \Aurora\Modules\Core\Module::Decorator()->IsModuleDisabledForObject($oUser, $sModuleName);
111: $bModuleIsDisabled = $this->getModuleConfigValue($sModuleName, 'Disabled', false);
112: if (!($bIsModuleDisabledForUser || $bIsModuleDisabledForTenant) && !$bModuleIsDisabled) {
113: $oLoadedModule = $this->loadModule($sModuleName, $sModulePath);
114: $bClientModule = $this->isClientModule($sModuleName);
115: if ($oLoadedModule instanceof AbstractModule || $bClientModule) {
116: $this->_aAllowedModulesName[\strtolower($sModuleName)] = $sModuleName;
117: } else {
118: // \Aurora\System\Api::Log('Module ' . $sModuleName . ' is not allowed. $bModuleLoaded = ' . $oLoadedModule . '. $bClientModule = ' . $bClientModule . '.');
119: }
120: } else {
121: $this->FlushModuleSettings($sModuleName);
122: // \Aurora\System\Api::Log('Module ' . $sModuleName . ' is not allowed. $bIsModuleDisabledForUser = ' . $bIsModuleDisabledForUser . '. $bModuleIsDisabled = ' . $bModuleIsDisabled . '.');
123: }
124: }
125: } else {
126: echo "Can't load 'Core' Module";
127: exit;
128: }
129: }
130:
131: /**
132: *
133: * @param string $sModuleName
134: * @return boolean
135: */
136: protected function isClientModule($sModuleName)
137: {
138: $sModulePath = $this->GetModulePath($sModuleName);
139: return \file_exists($sModulePath . $sModuleName . '/js/manager.js') || \file_exists($sModulePath . $sModuleName . '/vue/manager.js');
140: }
141:
142: /**
143: *
144: * @param string $sModuleName
145: * @return boolean
146: */
147: public function isModuleLoaded($sModuleName)
148: {
149: return \array_key_exists(\strtolower($sModuleName), $this->_aModules);
150: }
151:
152: /**
153: *
154: * @param string $sModuleName
155: * @param string $sConfigName
156: * @param string $sDefaultValue
157: * @return mixed
158: */
159: public function getModuleConfigValue($sModuleName, $sConfigName, $sDefaultValue = null)
160: {
161: $mResult = $sDefaultValue;
162: $oModuleConfig = $this->getModuleSettings($sModuleName);
163:
164: if ($oModuleConfig) {
165: $mResult = $oModuleConfig->GetValue($sConfigName, $sDefaultValue);
166: }
167:
168: return $mResult;
169: }
170:
171: /**
172: *
173: * @param string $sModuleName
174: * @param string $sConfigName
175: * @param string $sValue
176: * @return mixed
177: */
178: public function setModuleConfigValue($sModuleName, $sConfigName, $sValue)
179: {
180: $oModuleConfig = $this->getModuleSettings($sModuleName);
181: if ($oModuleConfig) {
182: $oModuleConfig->SetValue($sConfigName, $sValue);
183: }
184: }
185:
186: /**
187: *
188: * @param string $sModuleName
189: * @return mixed
190: */
191: public function saveModuleConfigValue($sModuleName)
192: {
193: $oModuleConfig = $this->getModuleSettings($sModuleName);
194: if ($oModuleConfig) {
195: $oModuleConfig->Save();
196: }
197: }
198:
199: /**
200: *
201: */
202: public function SyncModulesConfigs()
203: {
204: $sConfigFilename = 'pre-config.json';
205: $sConfigPath = AU_APP_ROOT_PATH . $sConfigFilename;
206: $aModulesPreconfig = [];
207: //getting modules pre-configuration data
208: if (file_exists($sConfigPath)) {
209: $sPreConfig = file_get_contents($sConfigPath);
210:
211: $aPreConfig = json_decode($sPreConfig, true);
212:
213: if (is_array($aPreConfig) && isset($aPreConfig['modules'])) {
214: $aModulesPreconfig = $aPreConfig['modules'];
215: }
216: }
217:
218: foreach ($this->GetModulesPaths() as $sModuleName => $sModulePath) {
219: if (!empty($sModuleName)) {
220: $oSettings = $this->getModuleSettings($sModuleName);
221: if ($oSettings instanceof Settings) {
222: $oSettings->Load();
223: //overriding modules default configuration with pre-configuration data
224: if (isset($aModulesPreconfig[$sModuleName])) {
225: $aModulePreconfig = $aModulesPreconfig[$sModuleName];
226: foreach ($aModulePreconfig as $key => $val) {
227: $oProp = $oSettings->GetSettingsProperty($key);
228: if ($oProp && $oProp->IsDefault) {
229: if (!empty($oProp->SpecType)) {
230: $val = \Aurora\System\Enums\EnumConvert::FromXml($val, $oProp->SpecType);
231: }
232: $oSettings->SetValue($key, $val);
233: }
234: }
235: }
236: $oSettings->Save();
237: }
238: }
239: }
240: }
241:
242: /**
243: *
244: * @param string $sModuleName
245: * @return \Aurora\System\Module\AbstractModule
246: */
247: protected function loadModule($sModuleName, $sModulePath = null)
248: {
249: $mResult = false;
250: if (!isset($sModulePath)) {
251: $sModulePath = $this->GetModulePath($sModuleName);
252: }
253:
254: if ($sModulePath) {
255: if (!$this->isModuleLoaded($sModuleName)) {
256: $aArgs = array($sModuleName, $sModulePath);
257:
258: $this->broadcastEvent(
259: $sModuleName,
260: 'loadModule' . AbstractModule::$Delimiter . 'before',
261: $aArgs
262: );
263:
264: if (@\file_exists($sModulePath . $sModuleName . '/Module.php')) {
265: $sModuleClassName = '\\Aurora\\Modules\\' . $sModuleName . '\\Module';
266: $oModule = new $sModuleClassName($sModulePath);
267: if ($oModule instanceof AbstractModule) {
268: foreach ($oModule->GetRequireModules() as $sModule) {
269: if (!$this->loadModule($sModule, $sModulePath)) {
270: break;
271: }
272: }
273:
274: if ($oModule->initialize() && $oModule->isValid()) {
275: $this->_aModules[\strtolower($sModuleName)] = $oModule;
276: $mResult = $oModule;
277: }
278: }
279: }
280:
281: $this->broadcastEvent(
282: $sModuleName,
283: 'loadModule' . AbstractModule::$Delimiter . 'after',
284: $aArgs,
285: $mResult
286: );
287: } else {
288: $mResult = $this->GetModule($sModuleName);
289: }
290: }
291: return $mResult;
292: }
293:
294: /**
295: * @param string $sParsedTemplateID
296: * @param string $sParsedPlace
297: * @param string $sTemplateFileName
298: * @param string $sModuleName
299: */
300: public function includeTemplate($sParsedTemplateID, $sParsedPlace, $sTemplateFileName, $sModuleName = '')
301: {
302: if (!isset($this->_aTemplates[$sParsedTemplateID])) {
303: $this->_aTemplates[$sParsedTemplateID] = array();
304: }
305:
306: $this->_aTemplates[$sParsedTemplateID][] = array(
307: $sParsedPlace,
308: $sTemplateFileName,
309: $sModuleName
310: );
311: }
312:
313: /**
314: *
315: * @param string $sTemplateID
316: * @param string $sTemplateSource
317: * @return string
318: */
319: public function ParseTemplate($sTemplateID, $sTemplateSource)
320: {
321: if (isset($this->_aTemplates[$sTemplateID]) && \is_array($this->_aTemplates[$sTemplateID])) {
322: foreach ($this->_aTemplates[$sTemplateID] as $aItem) {
323: if (!empty($aItem[0]) && !empty($aItem[1]) && \file_exists($aItem[1])) {
324: $sTemplateHtml = \file_get_contents($aItem[1]);
325: if (!empty($aItem[2])) {
326: $sTemplateHtml = \str_replace('%ModuleName%', $aItem[2], $sTemplateHtml);
327: $sTemplateHtml = \str_replace('%MODULENAME%', \strtoupper($aItem[2]), $sTemplateHtml);
328: }
329: $sTemplateSource = \str_replace(
330: '{%INCLUDE-START/' . $aItem[0] . '/INCLUDE-END%}',
331: $sTemplateHtml . '{%INCLUDE-START/' . $aItem[0] . '/INCLUDE-END%}',
332: $sTemplateSource
333: );
334: }
335: }
336: }
337:
338: return $sTemplateSource;
339: }
340:
341: /**
342: *
343: * @param string $sModule
344: * @param string $sType
345: * @param array $aMap
346: */
347: public function extendObject($sModule, $sType, $aMap)
348: {
349: $this->oObjectExtender->extend($sModule, $sType, $aMap);
350: }
351:
352: /**
353: *
354: * @param string $sType
355: * @return array
356: */
357: public function getExtendedObject($sType)
358: {
359: return $this->oObjectExtender->getObject($sType);
360: }
361:
362: /**
363: *
364: * @param string $sType
365: * @return boolean
366: */
367: public function issetObject($sType)
368: {
369: return $this->oObjectExtender->issetObject($sType);
370: }
371:
372: /**
373: * @todo return correct path according to curent tenant
374: *
375: * @return string
376: */
377: public function GetModulesRootPath()
378: {
379: return AU_APP_ROOT_PATH . 'modules/';
380: }
381:
382: /**
383: * @todo return correct path according to curent tenant
384: *
385: * @return array
386: */
387: public function GetModulesPaths()
388: {
389: if (!isset($this->_aModulesPaths)) {
390: $sModulesPath = $this->GetModulesRootPath();
391: $aModulePath = [
392: $sModulesPath
393: ];
394: $oCoreModule = $this->loadModule('Core', $sModulesPath);
395: if ($oCoreModule instanceof \Aurora\Modules\Core\Module) {
396: $sTenant = \trim($oCoreModule->GetTenantName());
397: if (!empty($sTenant)) {
398: $sTenantModulesPath = $this->GetTenantModulesPath($sTenant);
399: \array_unshift($aModulePath, $sTenantModulesPath);
400: }
401: }
402: $this->_aModulesPaths = [];
403: foreach ($aModulePath as $sModulesPath) {
404: if (@\is_dir($sModulesPath)) {
405: if (false !== ($rDirHandle = @\opendir($sModulesPath))) {
406: while (false !== ($sFileItem = @\readdir($rDirHandle))) {
407: if (0 < \strlen($sFileItem) && '.' !== $sFileItem[0] && \preg_match('/^[a-zA-Z0-9\-]+$/', $sFileItem)) {
408: $this->_aModulesPaths[$sFileItem] = $sModulesPath;
409: }
410: }
411:
412: @\closedir($rDirHandle);
413: }
414: }
415: }
416: }
417:
418: return $this->_aModulesPaths;
419: }
420:
421: /**
422: * @todo return correct path according to curent tenant
423: *
424: * @return string
425: */
426: public function GetModulePath($sModuleName)
427: {
428: $aModulesPaths = $this->GetModulesPaths();
429: return isset($aModulesPaths[$sModuleName]) ? $aModulesPaths[$sModuleName] : false;
430: }
431:
432: /**
433: * @todo return correct path according to curent tenant
434: *
435: * @return string
436: */
437: public function GetModulesSettingsPath()
438: {
439: return \Aurora\System\Api::DataPath() . '/settings/modules/';
440: }
441:
442: /**
443: * @return string
444: */
445: public function GetTenantModulesPath($sTenant)
446: {
447: return AU_APP_ROOT_PATH . 'tenants/' . $sTenant . '/modules/';
448: }
449:
450: /**
451: * @return array
452: */
453: public function GetAllowedModulesName()
454: {
455: $aArgs = [];
456: $mResult = $this->_aAllowedModulesName;
457:
458: $this->broadcastEvent('System', 'GetAllowedModulesName', $aArgs, $mResult, true);
459:
460: return $mResult;
461: }
462:
463: /**
464: * @param string $sModuleName
465: * @return array
466: */
467: public function IsAllowedModule($sModuleName)
468: {
469: $aArgs = [
470: 'ModuleName' => $sModuleName
471: ];
472: $mResult = array_key_exists(\strtolower($sModuleName), $this->_aAllowedModulesName);
473:
474: $this->broadcastEvent('System', 'IsAllowedModule', $aArgs, $mResult, true);
475:
476: return $mResult;
477: }
478:
479: /**
480: * @return array
481: */
482: public function GetModules()
483: {
484: return $this->_aModules;
485: }
486:
487: /**
488: * @param string $sModuleName
489: * @return \Aurora\System\Module\Settings
490: */
491: public function &getModuleSettings($sModuleName)
492: {
493: if (!isset($this->aModulesSettings[strtolower($sModuleName)])) {
494: $sSettingsClassName = '\\Aurora\\Modules\\' . $sModuleName . '\\Settings';
495: if (class_exists($sSettingsClassName)) {
496: $this->aModulesSettings[strtolower($sModuleName)] = new $sSettingsClassName($sModuleName);
497: } else {
498: $this->aModulesSettings[strtolower($sModuleName)] = new Settings($sModuleName);
499: }
500: }
501:
502: return $this->aModulesSettings[strtolower($sModuleName)];
503: }
504:
505: /**
506: * @param string $sModuleName
507: * @return void
508: */
509: public function FlushModuleSettings($sModuleName)
510: {
511: if (isset($this->aModulesSettings[strtolower($sModuleName)])) {
512: unset($this->aModulesSettings[strtolower($sModuleName)]);
513: }
514: }
515:
516: /**
517: * @param string $sModuleName
518: * @return \Aurora\System\Module\AbstractModule
519: */
520: public function GetModule($sModuleName)
521: {
522: $mResult = false;
523:
524: $sModuleNameLower = strtolower($sModuleName);
525: if ($this->isModuleLoaded($sModuleName)) {
526: $mResult = $this->_aModules[$sModuleNameLower];
527: }
528:
529: return $mResult;
530: }
531:
532:
533: /**
534: * @return \Aurora\System\Module\AbstractModule
535: */
536: public function GetModuleFromRequest()
537: {
538: $sModule = '';
539: $oHttp = \MailSo\Base\Http::SingletonInstance();
540: if ($oHttp->IsPost()) {
541: $sModule = $oHttp->GetPost('Module', null);
542: }
543: return $this->GetModule($sModule);
544: }
545:
546: /**
547: *
548: * @param string $sEntryName
549: * @return array
550: */
551: public function GetModulesByEntry($sEntryName)
552: {
553: $aModules = array();
554: $oResult = $this->GetModuleFromRequest();
555:
556: if ($oResult && !$oResult->HasEntry($sEntryName)) {
557: $oResult = false;
558: }
559: if ($oResult === false) {
560: foreach ($this->_aModules as $oModule) {
561: if ($oModule instanceof AbstractModule && $oModule->HasEntry($sEntryName)) {
562: $aModules[] = $oModule;
563: }
564: }
565: } else {
566: $aModules = array(
567: $oResult
568: );
569: }
570:
571: return $aModules;
572: }
573:
574: /**
575: * @param string $sModuleName
576: * @return bool
577: */
578: public function ModuleExists($sModuleName)
579: {
580: return ($this->GetModule($sModuleName)) ? true : false;
581: }
582:
583: /**
584: *
585: * @param string $sEntryName
586: * @return mixed
587: */
588: public function RunEntry($sEntryName)
589: {
590: $oHttp = \MailSo\Base\Http::SingletonInstance();
591:
592: $aArguments = [
593: 'EntryName' => $sEntryName,
594: 'Module' => $oHttp->GetPost('Module', null),
595: 'Method' => $oHttp->GetPost('Method', null),
596: 'Parameters' => \json_decode($oHttp->GetPost('Parameters', ''), true)
597: ];
598: $mResult = false;
599:
600: try {
601: $bEventResult = $this->broadcastEvent('System', 'RunEntry' . AbstractModule::$Delimiter . 'before', $aArguments, $mResult);
602:
603: if ($bEventResult !== true) {
604: if (!\Aurora\System\Router::getInstance()->hasRoute($sEntryName)) {
605: $sEntryName = 'default';
606: }
607:
608: $mResult = \Aurora\System\Router::getInstance()->route(
609: $sEntryName
610: );
611: }
612: } catch(\Exception $oException) {
613: $mResult = \Aurora\System\Managers\Response::GetJsonFromObject(
614: 'Json',
615: \Aurora\System\Managers\Response::ExceptionResponse("System", $oException)
616: );
617: \Aurora\System\Api::LogException($oException);
618: } finally {
619: $this->broadcastEvent('System', 'RunEntry' . AbstractModule::$Delimiter . 'after', $aArguments, $mResult);
620: }
621:
622: return $mResult;
623: }
624:
625: /**
626: * @return string
627: */
628: public function GetModulesHash()
629: {
630: $sResult = md5(\Aurora\System\Api::Version());
631: $aModuleNames = $this->GetAllowedModulesName();
632: foreach ($aModuleNames as $sModuleName) {
633: $sResult = md5($sResult . $this->GetModuleHashByName($sModuleName));
634: }
635:
636: return $sResult;
637: }
638:
639: /**
640: * @toto need to add module version to information string
641: * @param string $sModuleName
642: *
643: * @return string
644: */
645: public function GetModuleHashByName($sModuleName)
646: {
647: $sResult = '';
648: $sTenantName = \Aurora\System\Api::getTenantName();
649:
650: $sResult .= $sTenantName !== 'Default' ? $this->GetModulesRootPath() : $this->GetTenantModulesPath($sTenantName);
651: $sResult .= $sModuleName;
652:
653: return md5($sResult);
654: }
655:
656: /**
657: * @param string $oExcetpion
658: */
659: public function SetLastException($oExcetpion)
660: {
661: $this->oLastException = $oExcetpion;
662: }
663:
664: /**
665: *
666: */
667: public function GetLastException()
668: {
669: return $this->oLastException;
670: }
671:
672: /**
673: *
674: * @param string $sModule
675: * @param string $sMethod
676: * @param mixed $mResult
677: */
678: public function AddResult($sModule, $sMethod, $aParameters, $mResult, $iErrorCode = 0)
679: {
680: if (is_string($mResult)) {
681: $mResult = \str_replace(\Aurora\System\Api::$aSecretWords, '*******', $mResult);
682: }
683:
684: $aMapParameters = array();
685: if (is_array($aParameters)) {
686: foreach ($aParameters as $sKey => $mParameter) {
687: if (!is_resource($mParameter) && gettype($mParameter) !== 'unknown type') {
688: $aMapParameters[$sKey] = $mParameter;
689: }
690: }
691: }
692:
693: $aResult = array(
694: 'Module' => $sModule,
695: 'Method' => $sMethod,
696: 'Parameters' => $aMapParameters,
697: 'Result' => $mResult
698: );
699:
700: if ($iErrorCode > 0) {
701: $aResult['ErrorCode'] = $iErrorCode;
702: }
703:
704: $this->_aResults[] = $aResult;
705: }
706:
707: /**
708: * @return array
709: */
710: public function GetResults()
711: {
712: return $this->_aResults;
713: }
714:
715: /**
716: * @param string $sModule
717: * @param string $sMethod
718: * @return array
719: */
720: public function GetResult($sModule, $sMethod)
721: {
722: foreach ($this->_aResults as $aResult) {
723: if ($aResult['Module'] === $sModule && $aResult['Method'] === $sMethod) {
724: return [$aResult];
725: }
726: }
727:
728: return [];
729: }
730:
731: /**
732: * Broadcasts an event
733: *
734: * This method will call all subscribers. If one of the subscribers returns false, the process stops.
735: *
736: * The arguments parameter will be sent to all subscribers
737: *
738: * @param string $sEvent
739: * @param array $aArguments
740: * @param mixed $mResult
741: * @return boolean
742: */
743: public function broadcastEvent($sModule, $sEvent, &$aArguments = [], &$mResult = null, $bSkipIsAllowedModuleCheck = false)
744: {
745: return $this->oEventEmitter->emit(
746: $sModule,
747: $sEvent,
748: $aArguments,
749: $mResult,
750: function ($sModule, $aArguments, $mResult) use ($sEvent) {
751: $this->AddResult($sModule, $sEvent, $aArguments, $mResult);
752: },
753: $bSkipIsAllowedModuleCheck
754: );
755: }
756:
757: /**
758: * Subscribe to an event.
759: *
760: * When the event is triggered, we'll call all the specified callbacks.
761: * It is possible to control the order of the callbacks through the
762: * priority argument.
763: *
764: * This is for example used to make sure that the authentication plugin
765: * is triggered before anything else. If it's not needed to change this
766: * number, it is recommended to ommit.
767: *
768: * @param string $sEvent
769: * @param callable $fCallback
770: * @param int $iPriority
771: * @return void
772: */
773: public function subscribeEvent($sEvent, $fCallback, $iPriority = 100)
774: {
775: $this->oEventEmitter->on($sEvent, $fCallback, $iPriority);
776: }
777:
778: public function getEvents()
779: {
780: return $this->oEventEmitter->getListeners();
781: }
782:
783: public function GetSubscriptionsResult()
784: {
785: return $this->oEventEmitter->getListenersResult();
786: }
787: }
788: