mixed $value) * List of settings to preload. * - settingsFactory: callable * Callback function to fetch settings. * - permissions: array * List of permissions to make available client-side * - requires: array * List of other modules required */ protected $modules = NULL; /** * @var \CRM_Utils_Cache_Interface */ protected $cache; /** * @var array * Array(string $name => ChangeSet $change). */ protected $changeSets = NULL; /** * @param \CRM_Core_Resources $res * The resource manager. * @param $cache */ public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) { $this->res = $res; $this->cache = $cache ? $cache : new \CRM_Utils_Cache_ArrayCache([]); } /** * Clear out any runtime-cached metadata. * * This is useful if, eg, you have recently added or destroyed Angular modules. * * @return static */ public function clear() { $this->cache->clear(); $this->modules = NULL; $this->changeSets = NULL; // Force-refresh assetBuilder files \Civi::container()->get('asset_builder')->clear(FALSE); return $this; } /** * Get a list of AngularJS modules which should be autoloaded. * * @return array * Each item has some combination of these keys: * - ext: string * The Civi extension which defines the Angular module. * - js: array(string $relativeFilePath) * List of JS files (relative to the extension). * - css: array(string $relativeFilePath) * List of CSS files (relative to the extension). * - partials: array(string $relativeFilePath) * A list of partial-HTML folders (relative to the extension). * This will be mapped to "~/moduleName" by crmResource. * - settings: array(string $key => mixed $value) * List of settings to preload. */ public function getModules() { if ($this->modules === NULL) { $config = \CRM_Core_Config::singleton(); global $civicrm_root; // Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time // of writing CiviMail and CiviCase have special conditionals. $angularModules = []; $angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php"; $angularModules['checklist-model'] = include "$civicrm_root/ang/checklist-model.ang.php"; $angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php"; $angularModules['crmAttachment'] = include "$civicrm_root/ang/crmAttachment.ang.php"; $angularModules['crmAutosave'] = include "$civicrm_root/ang/crmAutosave.ang.php"; $angularModules['crmCxn'] = include "$civicrm_root/ang/crmCxn.ang.php"; $angularModules['crmDialog'] = include "$civicrm_root/ang/crmDialog.ang.php"; $angularModules['crmMonaco'] = include "$civicrm_root/ang/crmMonaco.ang.php"; $angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.ang.php"; $angularModules['crmRouteBinder'] = include "$civicrm_root/ang/crmRouteBinder.ang.php"; $angularModules['crmUi'] = include "$civicrm_root/ang/crmUi.ang.php"; $angularModules['crmUtil'] = include "$civicrm_root/ang/crmUtil.ang.php"; $angularModules['dialogService'] = include "$civicrm_root/ang/dialogService.ang.php"; $angularModules['jsonFormatter'] = include "$civicrm_root/ang/jsonFormatter.ang.php"; $angularModules['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php"; $angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php"; $angularModules['ui.bootstrap'] = include "$civicrm_root/ang/ui.bootstrap.ang.php"; $angularModules['ui.sortable'] = include "$civicrm_root/ang/ui.sortable.ang.php"; $angularModules['unsavedChanges'] = include "$civicrm_root/ang/unsavedChanges.ang.php"; $angularModules['crmQueueMonitor'] = include "$civicrm_root/ang/crmQueueMonitor.ang.php"; $angularModules['crmStatusPage'] = include "$civicrm_root/ang/crmStatusPage.ang.php"; $angularModules['exportui'] = include "$civicrm_root/ang/exportui.ang.php"; $angularModules['api4Explorer'] = include "$civicrm_root/ang/api4Explorer.ang.php"; $angularModules['api4'] = include "$civicrm_root/ang/api4.ang.php"; $angularModules['crmDashboard'] = include "$civicrm_root/ang/crmDashboard.ang.php"; $angularModules['crmD3'] = include "$civicrm_root/ang/crmD3.ang.php"; foreach (\CRM_Core_Component::getEnabledComponents() as $component) { $angularModules = array_merge($angularModules, $component->getAngularModules()); } \CRM_Utils_Hook::angularModules($angularModules); foreach ($angularModules as $module => $info) { // Merge in defaults $angularModules[$module] += ['basePages' => ['civicrm/a']]; // Validate settingsFactory callables if (isset($info['settingsFactory'])) { // To keep the cache small, we want `settingsFactory` to contain the string names of class & function, not an object if (!is_array($info['settingsFactory']) && !is_string($info['settingsFactory'])) { throw new \CRM_Core_Exception($module . ' settingsFactory must be a callable array or string'); } // To keep the cache small, convert full object to just the class name if (is_array($info['settingsFactory']) && is_object($info['settingsFactory'][0])) { $angularModules[$module]['settingsFactory'][0] = get_class($info['settingsFactory'][0]); } } } $this->modules = $this->resolvePatterns($angularModules); } return $this->modules; } /** * Get the descriptor for an Angular module. * * @param string $name * Module name. * @return array * Details about the module: * - ext: string, the name of the Civi extension which defines the module * - js: array(string $relativeFilePath). * - css: array(string $relativeFilePath). * - partials: array(string $relativeFilePath). * @throws \Exception */ public function getModule($name) { $modules = $this->getModules(); if (!isset($modules[$name])) { throw new \Exception("Unrecognized Angular module"); } return $modules[$name]; } /** * Resolve a full list of Angular dependencies. * * @param array $names * List of Angular modules. * Ex: array('crmMailing'). * @return array * List of Angular modules, include all dependencies. * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute'). * @throws \CRM_Core_Exception */ public function resolveDependencies($names) { $allModules = $this->getModules(); $visited = []; $result = $names; while (($missingModules = array_diff($result, array_keys($visited))) && !empty($missingModules)) { foreach ($missingModules as $module) { $visited[$module] = 1; if (!isset($allModules[$module])) { throw new \CRM_Core_Exception("Unrecognized Angular module {$module}. Please ensure that all Angular modules are declared."); } elseif (isset($allModules[$module]['requires'])) { $result = array_unique(array_merge($result, $allModules[$module]['requires'])); } } } sort($result); return $result; } /** * Get a list of Angular modules that should be loaded on the given * base-page. * * @param string $basePage * The name of the base-page for which we want a list of moudles. * @return array * List of Angular modules. * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute'). */ public function resolveDefaultModules($basePage) { $modules = $this->getModules(); $result = []; foreach ($modules as $moduleName => $module) { if (in_array($basePage, $module['basePages']) || in_array('*', $module['basePages'])) { $result[] = $moduleName; } } return $result; } /** * Convert any globs in an Angular module to file names. * * @param array $modules * List of Angular modules. * @return array * Updated list of Angular modules */ protected function resolvePatterns($modules) { $newModules = []; foreach ($modules as $moduleKey => $module) { foreach (['js', 'css', 'partials'] as $fileset) { if (!isset($module[$fileset])) { continue; } $module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]); } $newModules[$moduleKey] = $module; } return $newModules; } /** * Get the partial HTML documents for a module (unfiltered). * * @param string $name * Angular module name. * @return array * Array(string $extFilePath => string $html) * @throws \Exception * Invalid partials configuration. */ public function getRawPartials($name) { $module = $this->getModule($name); $result = !empty($module['partialsCallback']) ? \Civi\Core\Resolver::singleton()->call($module['partialsCallback'], [$name, $module]) : []; if (isset($module['partials'])) { foreach ($module['partials'] as $partialDir) { $partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir; $files = \CRM_Utils_File::findFiles($partialDir, '*.html', TRUE); foreach ($files as $file) { $filename = '~/' . $name . '/' . $file; $result[$filename] = file_get_contents($partialDir . '/' . $file); } } return $result; } return $result; } /** * Get the partial HTML documents for a module. * * @param string $name * Angular module name. * @return array * Array(string $extFilePath => string $html) * @throws \Exception * Invalid partials configuration. */ public function getPartials($name) { $cacheKey = "angular-partials_$name"; $cacheValue = $this->cache->get($cacheKey); if ($cacheValue === NULL) { $cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name)); $this->cache->set($cacheKey, $cacheValue); } return $cacheValue; } /** * Get list of translated strings for a module. * * @param string $name * Angular module name. * @return array * Translated strings: array(string $orig => string $translated). */ public function getTranslatedStrings($name) { $module = $this->getModule($name); $result = []; $strings = $this->getStrings($name); foreach ($strings as $string) { // TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]? // It doesn't look like client side really supports the domain right now... $translated = ts($string, [ 'domain' => [$module['ext'], NULL], ]); if ($translated != $string) { $result[$string] = $translated; } } return $result; } /** * Get list of translatable strings for a module. * * @param string $name * Angular module name. * @return array * Translatable strings. */ public function getStrings($name) { $module = $this->getModule($name); $result = []; if (isset($module['js'])) { foreach ($module['js'] as $file) { $strings = $this->res->getStrings()->get( $module['ext'], $this->res->getPath($module['ext'], $file), 'text/javascript' ); $result = array_unique(array_merge($result, $strings)); } } $partials = $this->getPartials($name); foreach ($partials as $partial) { $result = array_unique(array_merge($result, \CRM_Utils_JS::parseStrings($partial))); } return $result; } /** * Get resources for one or more modules. * * @param string|array $moduleNames * List of module names. * @param string $resType * Type of resource ('js', 'css', 'settings'). * @param string $refType * Type of reference to the resource ('cacheUrl', 'rawUrl', 'path', 'settings'). * @return array * List of URLs or paths. * @throws \CRM_Core_Exception */ public function getResources($moduleNames, $resType, $refType) { $result = []; $moduleNames = (array) $moduleNames; foreach ($moduleNames as $moduleName) { $module = $this->getModule($moduleName); if (isset($module[$resType])) { foreach ($module[$resType] as $file) { $refTypeSuffix = ''; if (is_string($file) && preg_match(';^(assetBuilder|ext)://;', $file)) { $refTypeSuffix = '-' . parse_url($file, PHP_URL_SCHEME); } switch ($refType . $refTypeSuffix) { case 'path': $result[] = $this->res->getPath($module['ext'], $file); break; case 'rawUrl': $result[] = $this->res->getUrl($module['ext'], $file); break; case 'cacheUrl': $result[] = $this->res->getUrl($module['ext'], $file, TRUE); break; case 'path-assetBuilder': $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH); $assetParams = []; parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams); $result[] = \Civi::service('asset_builder')->getPath($assetName, $assetParams); break; case 'rawUrl-assetBuilder': case 'cacheUrl-assetBuilder': $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH); $assetParams = []; parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams); $result[] = \Civi::service('asset_builder')->getUrl($assetName, $assetParams); break; case 'path-ext': $result[] = $this->res->getPath(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/')); break; case 'rawUrl-ext': $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/')); break; case 'cacheUrl-ext': $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'), TRUE); break; case 'settings': case 'settingsFactory': case 'requires': case 'permissions': case 'bundles': if (!empty($module[$resType])) { $result[$moduleName] = $module[$resType]; } break; default: throw new \CRM_Core_Exception("Unrecognized resource format"); } } } } return ChangeSet::applyResourceFilters($this->getChangeSets(), $resType, $result); } /** * @return array * Array(string $name => ChangeSet $changeSet). */ public function getChangeSets() { if ($this->changeSets === NULL) { $this->changeSets = []; \CRM_Utils_Hook::alterAngular($this); } return $this->changeSets; } /** * @param ChangeSet $changeSet * @return \Civi\Angular\Manager */ public function add($changeSet) { $this->changeSets[$changeSet->getName()] = $changeSet; return $this; } }