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: $oCoreModule = $this->loadModule('Core');
101:
102: if ($oCoreModule instanceof AbstractModule) {
103: $oUser = \Aurora\System\Api::authorise();
104: $oTenant = null;
105: if ($oUser instanceof User && $oUser->Role !== \Aurora\System\Enums\UserRole::SuperAdmin) {
106: $oTenant = \Aurora\Modules\Core\Module::Decorator()->GetTenantUnchecked($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: $aModuleDefaultSettings = $oSettings->GetDefaultConfigValues();
223: //overriding modules default configuration with pre-configuration data
224: if (isset($aModulesPreconfig[$sModuleName])) {
225: $aModulePreconfig = $aModulesPreconfig[$sModuleName];
226: foreach ($aModuleDefaultSettings as $key => $oSetting) {
227: if (array_key_exists($key, $aModulePreconfig)) {
228: $oSetting->Value = $aModulePreconfig[$key];
229: }
230: }
231: }
232: $aModuleSettings = [];
233: if (@\file_exists($oSettings->GetPath())) {
234: $aModuleSettings = $oSettings->GetValues();
235: }
236: //compiling default configuration and pre-configuration data into modules configuration
237: $aValues = array_merge(
238: $aModuleDefaultSettings,
239: $aModuleSettings
240: );
241: $oSettings->SetValues($aValues);
242: $oSettings->Save();
243: }
244: }
245: }
246: }
247:
248: /**
249: *
250: * @param string $sModuleName
251: * @return \Aurora\System\Module\AbstractModule
252: */
253: protected function loadModule($sModuleName, $sModulePath = null)
254: {
255: $mResult = false;
256: if (!isset($sModulePath)) {
257: $sModulePath = $this->GetModulePath($sModuleName);
258: }
259:
260: if ($sModulePath) {
261: if (!$this->isModuleLoaded($sModuleName)) {
262: $aArgs = array($sModuleName, $sModulePath);
263:
264: $this->broadcastEvent(
265: $sModuleName,
266: 'loadModule' . AbstractModule::$Delimiter . 'before',
267: $aArgs
268: );
269:
270: if (@\file_exists($sModulePath.$sModuleName.'/Module.php')) {
271: $sModuleClassName = '\\Aurora\\Modules\\' . $sModuleName . '\\Module';
272: $oModule = new $sModuleClassName($sModulePath);
273: if ($oModule instanceof AbstractModule) {
274: foreach ($oModule->GetRequireModules() as $sModule) {
275: if (!$this->loadModule($sModule, $sModulePath)) {
276: break;
277: }
278: }
279:
280: if ($oModule->initialize() && $oModule->isValid()) {
281: $this->_aModules[\strtolower($sModuleName)] = $oModule;
282: $mResult = $oModule;
283: }
284: }
285: }
286:
287: $this->broadcastEvent(
288: $sModuleName,
289: 'loadModule' . AbstractModule::$Delimiter . 'after',
290: $aArgs,
291: $mResult
292: );
293: } else {
294: $mResult = $this->GetModule($sModuleName);
295: }
296: }
297: return $mResult;
298: }
299:
300: /**
301: * @param string $sParsedTemplateID
302: * @param string $sParsedPlace
303: * @param string $sTemplateFileName
304: * @param string $sModuleName
305: */
306: public function includeTemplate($sParsedTemplateID, $sParsedPlace, $sTemplateFileName, $sModuleName = '')
307: {
308: if (!isset($this->_aTemplates[$sParsedTemplateID])) {
309: $this->_aTemplates[$sParsedTemplateID] = array();
310: }
311:
312: $this->_aTemplates[$sParsedTemplateID][] = array(
313: $sParsedPlace,
314: $sTemplateFileName,
315: $sModuleName
316: );
317: }
318:
319: /**
320: *
321: * @param string $sTemplateID
322: * @param string $sTemplateSource
323: * @return string
324: */
325: public function ParseTemplate($sTemplateID, $sTemplateSource)
326: {
327: if (isset($this->_aTemplates[$sTemplateID]) && \is_array($this->_aTemplates[$sTemplateID])) {
328: foreach ($this->_aTemplates[$sTemplateID] as $aItem) {
329: if (!empty($aItem[0]) && !empty($aItem[1]) && \file_exists($aItem[1])) {
330: $sTemplateHtml = \file_get_contents($aItem[1]);
331: if (!empty($aItem[2])) {
332: $sTemplateHtml = \str_replace('%ModuleName%', $aItem[2], $sTemplateHtml);
333: $sTemplateHtml = \str_replace('%MODULENAME%', \strtoupper($aItem[2]), $sTemplateHtml);
334: }
335: $sTemplateSource = \str_replace(
336: '{%INCLUDE-START/'.$aItem[0].'/INCLUDE-END%}',
337: $sTemplateHtml.'{%INCLUDE-START/'.$aItem[0].'/INCLUDE-END%}',
338: $sTemplateSource
339: );
340: }
341: }
342: }
343:
344: return $sTemplateSource;
345: }
346:
347: /**
348: *
349: * @param string $sModule
350: * @param string $sType
351: * @param array $aMap
352: */
353: public function extendObject($sModule, $sType, $aMap)
354: {
355: $this->oObjectExtender->extend($sModule, $sType, $aMap);
356: }
357:
358: /**
359: *
360: * @param string $sType
361: * @return array
362: */
363: public function getExtendedObject($sType)
364: {
365: return $this->oObjectExtender->getObject($sType);
366: }
367:
368: /**
369: *
370: * @param string $sType
371: * @return boolean
372: */
373: public function issetObject($sType)
374: {
375: return $this->oObjectExtender->issetObject($sType);
376: }
377:
378: /**
379: * @todo return correct path according to curent tenant
380: *
381: * @return string
382: */
383: public function GetModulesRootPath()
384: {
385: return AU_APP_ROOT_PATH.'modules/';
386: }
387:
388: /**
389: * @todo return correct path according to curent tenant
390: *
391: * @return array
392: */
393: public function GetModulesPaths()
394: {
395: if (!isset($this->_aModulesPaths)) {
396: $sModulesPath = $this->GetModulesRootPath();
397: $aModulePath = [
398: $sModulesPath
399: ];
400: $oCoreModule = $this->loadModule('Core', $sModulesPath);
401: if ($oCoreModule instanceof \Aurora\Modules\Core\Module) {
402: $sTenant = \trim($oCoreModule->GetTenantName());
403: if (!empty($sTenant)) {
404: $sTenantModulesPath = $this->GetTenantModulesPath($sTenant);
405: \array_unshift($aModulePath, $sTenantModulesPath);
406: }
407: }
408: $this->_aModulesPaths = [];
409: foreach ($aModulePath as $sModulesPath) {
410: if (@\is_dir($sModulesPath)) {
411: if (false !== ($rDirHandle = @\opendir($sModulesPath))) {
412: while (false !== ($sFileItem = @\readdir($rDirHandle))) {
413: if (0 < \strlen($sFileItem) && '.' !== $sFileItem[0] && \preg_match('/^[a-zA-Z0-9\-]+$/', $sFileItem)) {
414: $this->_aModulesPaths[$sFileItem] = $sModulesPath;
415: }
416: }
417:
418: @\closedir($rDirHandle);
419: }
420: }
421: }
422: }
423:
424: return $this->_aModulesPaths;
425: }
426:
427: /**
428: * @todo return correct path according to curent tenant
429: *
430: * @return string
431: */
432: public function GetModulePath($sModuleName)
433: {
434: $aModulesPaths = $this->GetModulesPaths();
435: return isset($aModulesPaths[$sModuleName]) ? $aModulesPaths[$sModuleName] : false;
436: }
437:
438: /**
439: * @todo return correct path according to curent tenant
440: *
441: * @return string
442: */
443: public function GetModulesSettingsPath()
444: {
445: return \Aurora\System\Api::DataPath() . '/settings/modules/';
446: }
447:
448: /**
449: * @return string
450: */
451: public function GetTenantModulesPath($sTenant)
452: {
453: return AU_APP_ROOT_PATH.'tenants/' . $sTenant . '/modules/';
454: }
455:
456: /**
457: * @return array
458: */
459: public function GetAllowedModulesName()
460: {
461: $aArgs = [];
462: $mResult = $this->_aAllowedModulesName;
463:
464: $this->broadcastEvent('System', 'GetAllowedModulesName', $aArgs, $mResult, true);
465:
466: return $mResult;
467: }
468:
469: /**
470: * @param string $sModuleName
471: * @return array
472: */
473: public function IsAllowedModule($sModuleName)
474: {
475: $aArgs = [
476: 'ModuleName' => $sModuleName
477: ];
478: $mResult = array_key_exists(\strtolower($sModuleName), $this->_aAllowedModulesName);
479:
480: $this->broadcastEvent('System', 'IsAllowedModule', $aArgs, $mResult, true);
481:
482: return $mResult;
483: }
484:
485: /**
486: * @return array
487: */
488: public function GetModules()
489: {
490: return $this->_aModules;
491: }
492:
493: /**
494: * @param string $sModuleName
495: * @return \Aurora\System\Module\Settings
496: */
497: public function &GetModuleSettings($sModuleName)
498: {
499: if (!isset($this->aModulesSettings[strtolower($sModuleName)])) {
500: $this->aModulesSettings[strtolower($sModuleName)] = new Settings($sModuleName);
501: }
502:
503: return $this->aModulesSettings[strtolower($sModuleName)];
504: }
505:
506: /**
507: * @param string $sModuleName
508: * @return void
509: */
510: public function FlushModuleSettings($sModuleName)
511: {
512: if (isset($this->aModulesSettings[strtolower($sModuleName)])) {
513: unset($this->aModulesSettings[strtolower($sModuleName)]);
514: }
515: }
516:
517: /**
518: * @param string $sModuleName
519: * @return \Aurora\System\Module\AbstractModule
520: */
521: public function GetModule($sModuleName)
522: {
523: $mResult = false;
524:
525: $sModuleNameLower = strtolower($sModuleName);
526: if ($this->isModuleLoaded($sModuleName)) {
527: $mResult = $this->_aModules[$sModuleNameLower];
528: }
529:
530: return $mResult;
531: }
532:
533:
534: /**
535: * @return \Aurora\System\Module\AbstractModule
536: */
537: public function GetModuleFromRequest()
538: {
539: $sModule = '';
540: $oHttp = \MailSo\Base\Http::SingletonInstance();
541: if ($oHttp->IsPost()) {
542: $sModule = $oHttp->GetPost('Module', null);
543: }
544: return $this->GetModule($sModule);
545: }
546:
547: /**
548: *
549: * @param string $sEntryName
550: * @return array
551: */
552: public function GetModulesByEntry($sEntryName)
553: {
554: $aModules = array();
555: $oResult = $this->GetModuleFromRequest();
556:
557: if ($oResult && !$oResult->HasEntry($sEntryName)) {
558: $oResult = false;
559: }
560: if ($oResult === false) {
561: foreach ($this->_aModules as $oModule) {
562: if ($oModule instanceof AbstractModule && $oModule->HasEntry($sEntryName)) {
563: $aModules[] = $oModule;
564: }
565: }
566: } else {
567: $aModules = array(
568: $oResult
569: );
570: }
571:
572: return $aModules;
573: }
574:
575: /**
576: * @param string $sModuleName
577: * @return bool
578: */
579: public function ModuleExists($sModuleName)
580: {
581: return ($this->GetModule($sModuleName)) ? true : false;
582: }
583:
584: /**
585: *
586: * @param string $sEntryName
587: * @return mixed
588: */
589: public function RunEntry($sEntryName)
590: {
591: $aArguments = [
592: 'EntryName' => $sEntryName
593: ];
594: $mResult = false;
595: try {
596: $bEventResult = $this->broadcastEvent('System', 'RunEntry' . AbstractModule::$Delimiter . 'before', $aArguments, $mResult);
597:
598: if ($bEventResult !== true) {
599: if (!\Aurora\System\Router::getInstance()->hasRoute($sEntryName)) {
600: $sEntryName = 'default';
601: }
602:
603: $mResult = \Aurora\System\Router::getInstance()->route(
604: $sEntryName
605: );
606: }
607:
608: $this->broadcastEvent('System', 'RunEntry' . AbstractModule::$Delimiter . 'after', $aArguments, $mResult);
609: } catch(\Exception $oException) {
610: $mResult = \Aurora\System\Managers\Response::GetJsonFromObject(
611: "Json",
612: \Aurora\System\Managers\Response::ExceptionResponse("System", $oException)
613: );
614: \Aurora\System\Api::LogException($oException);
615: }
616:
617: return $mResult;
618: }
619:
620: /**
621: * @return string
622: */
623: public function GetModulesHash()
624: {
625: $sResult = md5(\Aurora\System\Api::Version());
626: $aModuleNames = $this->GetAllowedModulesName();
627: foreach ($aModuleNames as $sModuleName) {
628: $sResult = md5($sResult.$this->GetModuleHashByName($sModuleName));
629: }
630:
631: return $sResult;
632: }
633:
634: /**
635: * @toto need to add module version to information string
636: * @param string $sModuleName
637: *
638: * @return string
639: */
640: public function GetModuleHashByName($sModuleName)
641: {
642: $sResult = '';
643: $sTenantName = \Aurora\System\Api::getTenantName();
644:
645: $sResult .= $sTenantName !== 'Default' ? $this->GetModulesRootPath() : $this->GetTenantModulesPath($sTenantName);
646: $sResult .= $sModuleName;
647:
648: return md5($sResult);
649: }
650:
651: /**
652: * @param string $oExcetpion
653: */
654: public function SetLastException($oExcetpion)
655: {
656: $this->oLastException = $oExcetpion;
657: }
658:
659: /**
660: *
661: */
662: public function GetLastException()
663: {
664: return $this->oLastException;
665: }
666:
667: /**
668: *
669: * @param string $sModule
670: * @param string $sMethod
671: * @param mixed $mResult
672: */
673: public function AddResult($sModule, $sMethod, $aParameters, $mResult, $iErrorCode = 0)
674: {
675: if (is_string($mResult)) {
676: $mResult = \str_replace(\Aurora\System\Api::$aSecretWords, '*******', $mResult);
677: }
678:
679: $aMapParameters = array();
680: if (is_array($aParameters)) {
681: foreach ($aParameters as $sKey => $mParameter) {
682: if (!is_resource($mParameter) && gettype($mParameter) !== 'unknown type') {
683: $aMapParameters[$sKey] = $mParameter;
684: }
685: }
686: }
687:
688: $aResult = array(
689: 'Module' => $sModule,
690: 'Method' => $sMethod,
691: 'Parameters' => $aMapParameters,
692: 'Result' => $mResult
693: );
694:
695: if ($iErrorCode > 0) {
696: $aResult['ErrorCode'] = $iErrorCode;
697: }
698:
699: $this->_aResults[] = $aResult;
700: }
701:
702: /**
703: * @return array
704: */
705: public function GetResults()
706: {
707: return $this->_aResults;
708: }
709:
710: /**
711: * @param string $sModule
712: * @param string $sMethod
713: * @return array
714: */
715: public function GetResult($sModule, $sMethod)
716: {
717: foreach ($this->_aResults as $aResult) {
718: if ($aResult['Module'] === $sModule && $aResult['Method'] === $sMethod) {
719: return [$aResult];
720: }
721: }
722:
723: return [];
724: }
725:
726: /**
727: * Broadcasts an event
728: *
729: * This method will call all subscribers. If one of the subscribers returns false, the process stops.
730: *
731: * The arguments parameter will be sent to all subscribers
732: *
733: * @param string $sEvent
734: * @param array $aArguments
735: * @param mixed $mResult
736: * @return boolean
737: */
738: public function broadcastEvent($sModule, $sEvent, &$aArguments = [], &$mResult = null, $bSkipIsAllowedModuleCheck = false)
739: {
740: return $this->oEventEmitter->emit(
741: $sModule,
742: $sEvent,
743: $aArguments,
744: $mResult,
745: function ($sModule, $aArguments, $mResult) use ($sEvent) {
746: $this->AddResult($sModule, $sEvent, $aArguments, $mResult);
747: },
748: $bSkipIsAllowedModuleCheck
749: );
750: }
751:
752: /**
753: * Subscribe to an event.
754: *
755: * When the event is triggered, we'll call all the specified callbacks.
756: * It is possible to control the order of the callbacks through the
757: * priority argument.
758: *
759: * This is for example used to make sure that the authentication plugin
760: * is triggered before anything else. If it's not needed to change this
761: * number, it is recommended to ommit.
762: *
763: * @param string $sEvent
764: * @param callback $fCallback
765: * @param int $iPriority
766: * @return void
767: */
768: public function subscribeEvent($sEvent, $fCallback, $iPriority = 100)
769: {
770: return $this->oEventEmitter->on($sEvent, $fCallback, $iPriority);
771: }
772:
773: public function getEvents()
774: {
775: return $this->oEventEmitter->getListeners();
776: }
777:
778: public function GetSubscriptionsResult()
779: {
780: return $this->oEventEmitter->getListenersResult();
781: }
782: }
783: