First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,236 @@
<?php
namespace Civi\Angular;
/**
* The AngularLoader loads any JS/CSS/JSON resources
* required for setting up AngularJS.
*
* The AngularLoader stops short of bootstrapping AngularJS. You may
* need to `<div ng-app="..."></div>` or `angular.bootstrap(...)`.
*
* @code
* $loader = new AngularLoader();
* $loader->setPageName('civicrm/case/a');
* $loader->setModules(array('crmApp'));
* $loader->load();
* @endCode
*
* @link https://docs.angularjs.org/guide/bootstrap
*/
class AngularLoader {
/**
* The weight to assign to any Angular JS module files.
*/
const DEFAULT_MODULE_WEIGHT = 200;
/**
* The resource manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \CRM_Core_Resources
*/
protected $res;
/**
* The Angular module manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \Civi\Angular\Manager
*/
protected $angular;
/**
* The region of the page into which JavaScript will be loaded.
*
* @var string
*/
protected $region;
/**
* @var string
* Ex: 'civicrm/a'.
*/
protected $pageName;
/**
* @var array
* A list of modules to load.
*/
protected $modules;
/**
* AngularLoader constructor.
*/
public function __construct() {
$this->res = \CRM_Core_Resources::singleton();
$this->angular = \Civi::service('angular');
$this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header';
$this->pageName = isset($_GET['q']) ? $_GET['q'] : NULL;
$this->modules = array();
}
/**
* Register resources required by Angular.
*/
public function load() {
$angular = $this->getAngular();
$res = $this->getRes();
$moduleNames = $this->findActiveModules();
if (!$this->isAllModules($moduleNames)) {
$assetParams = array('modules' => implode(',', $moduleNames));
}
else {
// The module list will be "all modules that the user can see".
$assetParams = array('nonce' => md5(implode(',', $moduleNames)));
}
$res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) {
// TODO optimization; client-side caching
$result = array_merge($angular->getResources($moduleNames, 'settings', 'settings'), array(
'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
'angular' => array(
'modules' => $moduleNames,
'requires' => $angular->getResources($moduleNames, 'requires', 'requires'),
'cacheCode' => $res->getCacheCode(),
'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams),
),
));
return $result;
});
$res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->getRegion(), FALSE);
$res->addScriptFile('civicrm', 'js/crm.angular.js', 101, $this->getRegion(), FALSE);
$headOffset = 0;
$config = \CRM_Core_Config::singleton();
if ($config->debug) {
foreach ($moduleNames as $moduleName) {
foreach ($this->angular->getResources($moduleName, 'css', 'cacheUrl') as $url) {
$res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
}
foreach ($this->angular->getResources($moduleName, 'js', 'cacheUrl') as $url) {
$res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
// addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
}
}
}
else {
// Note: addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
// $aggScriptUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=js&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
$aggScriptUrl = \Civi::service('asset_builder')->getUrl('angular-modules.js', $assetParams);
$res->addScriptUrl($aggScriptUrl, 120, $this->getRegion());
// FIXME: The following CSS aggregator doesn't currently handle path-adjustments - which can break icons.
//$aggStyleUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=css&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
//$aggStyleUrl = \Civi::service('asset_builder')->getUrl('angular-modules.css', $assetParams);
//$res->addStyleUrl($aggStyleUrl, 120, $this->getRegion());
foreach ($this->angular->getResources($moduleNames, 'css', 'cacheUrl') as $url) {
$res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
}
}
}
/**
* Get a list of all Angular modules which should be activated on this
* page.
*
* @return array
* List of module names.
* Ex: array('angularFileUpload', 'crmUi', 'crmUtil').
*/
public function findActiveModules() {
return $this->angular->resolveDependencies(array_merge(
$this->getModules(),
$this->angular->resolveDefaultModules($this->getPageName())
));
}
/**
* @param $moduleNames
* @return int
*/
private function isAllModules($moduleNames) {
$allModuleNames = array_keys($this->angular->getModules());
return count(array_diff($allModuleNames, $moduleNames)) === 0;
}
/**
* @return \CRM_Core_Resources
*/
public function getRes() {
return $this->res;
}
/**
* @param \CRM_Core_Resources $res
*/
public function setRes($res) {
$this->res = $res;
}
/**
* @return \Civi\Angular\Manager
*/
public function getAngular() {
return $this->angular;
}
/**
* @param \Civi\Angular\Manager $angular
*/
public function setAngular($angular) {
$this->angular = $angular;
}
/**
* @return string
*/
public function getRegion() {
return $this->region;
}
/**
* @param string $region
*/
public function setRegion($region) {
$this->region = $region;
}
/**
* @return string
* Ex: 'civicrm/a'.
*/
public function getPageName() {
return $this->pageName;
}
/**
* @param string $pageName
* Ex: 'civicrm/a'.
*/
public function setPageName($pageName) {
$this->pageName = $pageName;
}
/**
* @return array
*/
public function getModules() {
return $this->modules;
}
/**
* @param array $modules
*/
public function setModules($modules) {
$this->modules = $modules;
}
}

View file

@ -0,0 +1,191 @@
<?php
namespace Civi\Angular;
class ChangeSet implements ChangeSetInterface {
/**
* Update a listing of resources.
*
* @param array $changeSets
* Array(ChangeSet).
* @param string $resourceType
* Ex: 'requires', 'settings'
* @param array $resources
* The list of resources.
* @return mixed
*/
public static function applyResourceFilters($changeSets, $resourceType, $resources) {
if ($resourceType === 'partials') {
return self::applyHtmlFilters($changeSets, $resources);
}
foreach ($changeSets as $changeSet) {
/** @var ChangeSet $changeSet */
foreach ($changeSet->resFilters as $filter) {
if ($filter['resourceType'] === $resourceType) {
$resources = call_user_func($filter['callback'], $resources);
}
}
}
return $resources;
}
/**
* Update a set of HTML snippets.
*
* @param array $changeSets
* Array(ChangeSet).
* @param array $strings
* Array(string $path => string $html).
* @return array
* Updated list of $strings.
* @throws \CRM_Core_Exception
*/
private static function applyHtmlFilters($changeSets, $strings) {
$coder = new Coder();
foreach ($strings as $path => $html) {
/** @var \phpQueryObject $doc */
$doc = NULL;
// Most docs don't need phpQueryObject. Initialize phpQuery on first match.
foreach ($changeSets as $changeSet) {
/** @var ChangeSet $changeSet */
foreach ($changeSet->htmlFilters as $filter) {
if (preg_match($filter['regex'], $path)) {
if ($doc === NULL) {
$doc = \phpQuery::newDocument($html, 'text/html');
if (\CRM_Core_Config::singleton()->debug && !$coder->checkConsistentHtml($html)) {
throw new \CRM_Core_Exception("Cannot process $path: inconsistent markup. Use check-angular.php to investigate.");
}
}
call_user_func($filter['callback'], $doc, $path);
}
}
}
if ($doc !== NULL) {
$strings[$path] = $coder->encode($doc);
}
}
return $strings;
}
/**
* @var string
*/
protected $name;
/**
* @var array
* Each item is an array with keys:
* - resourceType: string
* - callback: function
*/
protected $resFilters = array();
/**
* @var array
* Each item is an array with keys:
* - regex: string
* - callback: function
*/
protected $htmlFilters = array();
/**
* @param string $name
* Symbolic name for this changeset.
* @return \Civi\Angular\ChangeSetInterface
*/
public static function create($name) {
$changeSet = new ChangeSet();
$changeSet->name = $name;
return $changeSet;
}
/**
* Declare that $module requires additional dependencies.
*
* @param string $module
* @param string|array $dependencies
* @return ChangeSet
*/
public function requires($module, $dependencies) {
$dependencies = (array) $dependencies;
return $this->alterResource('requires',
function ($values) use ($module, $dependencies) {
if (!isset($values[$module])) {
$values[$module] = array();
}
$values[$module] = array_unique(array_merge($values[$module], $dependencies));
return $values;
});
}
/**
* Declare a change to a resource.
*
* @param string $resourceType
* @param callable $callback
* @return ChangeSet
*/
public function alterResource($resourceType, $callback) {
$this->resFilters[] = array(
'resourceType' => $resourceType,
'callback' => $callback,
);
return $this;
}
/**
* Declare a change to HTML.
*
* @param string $file
* A file name, wildcard, or regex.
* Ex: '~/crmHello/intro.html' (filename)
* Ex: '~/crmHello/*.html' (wildcard)
* Ex: ';(Edit|List)Ctrl\.html$;' (regex)
* @param callable $callback
* Function which accepts up to two parameters:
* - phpQueryObject $doc
* - string $path
* @return ChangeSet
*/
public function alterHtml($file, $callback) {
$this->htmlFilters[] = array(
'regex' => ($file{0} === ';') ? $file : $this->createRegex($file),
'callback' => $callback,
);
return $this;
}
/**
* Convert a string with a wildcard (*) to a regex.
*
* @param string $filterExpr
* Ex: "/foo/*.bar"
* @return string
* Ex: ";^/foo/[^/]*\.bar$;"
*/
protected function createRegex($filterExpr) {
$regex = preg_quote($filterExpr, ';');
$regex = str_replace('\\*', '[^/]*', $regex);
$regex = ";^$regex$;";
return $regex;
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @param string $name
*/
public function setName($name) {
$this->name = $name;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Civi\Angular;
interface ChangeSetInterface {
/**
* Get the symbolic name of the changeset.
*
* @return string
*/
public function getName();
/**
* Declare that $module requires additional dependencies.
*
* @param string $module
* @param string|array $dependencies
* @return ChangeSet
*/
public function requires($module, $dependencies);
/**
* Declare a change to HTML.
*
* @param string $file
* A file name, wildcard, or regex.
* Ex: '~/crmHello/intro.html' (filename)
* Ex: '~/crmHello/*.html' (wildcard)
* Ex: ';(Edit|List)Ctrl\.html$;' (regex)
* @param callable $callback
* Function which accepts up to two parameters:
* - phpQueryObject $doc
* - string $path
* @return ChangeSet
*/
public function alterHtml($file, $callback);
}

View file

@ -0,0 +1,82 @@
<?php
namespace Civi\Angular;
class Coder {
/**
*
* Determine whether an HTML snippet remains consistent (through an
* decode/encode loop).
*
* Note: Variations in whitespace are permitted.
*
* @param string $html
* @return bool
*/
public function checkConsistentHtml($html) {
try {
$recodedHtml = $this->recode($html);
}
catch (\Exception $e) {
return FALSE;
}
$htmlSig = preg_replace('/[ \t\r\n\/]+/', '', $this->cleanup($html));
$docSig = preg_replace('/[ \t\r\n\/]+/', '', $recodedHtml);
if ($htmlSig !== $docSig || empty($html) != empty($htmlSig)) {
return FALSE;
}
return TRUE;
}
/**
* Parse an HTML snippet and re-encode is as HTML.
*
* This is useful for detecting cases where the parser or encoder
* have quirks/bugs.
*
* @param string $html
* @return string
*/
public function recode($html) {
$doc = \phpQuery::newDocument("$html", 'text/html');
return $this->encode($doc);
}
/**
* Encode a phpQueryObject as HTML.
*
* @param \phpQueryObject $doc
* @return string
* HTML
*/
public function encode($doc) {
$doc->document->formatOutput = TRUE;
return $this->cleanup($doc->markupOuter());
}
protected function cleanup($html) {
$html = preg_replace_callback("/([\\-a-zA-Z0-9]+)=(')([^']*)(')/", array($this, 'cleanupAttribute'), $html);
$html = preg_replace_callback('/([\-a-zA-Z0-9]+)=(")([^"]*)(")/', array($this, 'cleanupAttribute'), $html);
return $html;
}
protected function cleanupAttribute($matches) {
list ($full, $attr, $lquote, $value, $rquote) = $matches;
switch ($attr) {
case 'href':
if (strpos($value, '%7B%7B') !== FALSE && strpos($value, '%7D%7D') !== FALSE) {
$value = urldecode($value);
}
break;
default:
$value = html_entity_decode($value);
break;
}
return "$attr=$lquote$value$rquote";
}
}

View file

@ -0,0 +1,413 @@
<?php
namespace Civi\Angular;
/**
* Manage Angular resources.
*
* @package Civi\Angular
*/
class Manager {
/**
* @var \CRM_Core_Resources
*/
protected $res = NULL;
/**
* @var array|NULL
* 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.
*/
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.
*/
public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) {
$this->res = $res;
$this->cache = $cache ? $cache : new \CRM_Utils_Cache_Arraycache(array());
}
/**
* 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 = array();
$angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.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['crmExample'] = include "$civicrm_root/ang/crmExample.ang.php";
$angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.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['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php";
$angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php";
$angularModules['ui.utils'] = include "$civicrm_root/ang/ui.utils.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['statuspage'] = include "$civicrm_root/ang/crmStatusPage.ang.php";
foreach (\CRM_Core_Component::getEnabledComponents() as $component) {
$angularModules = array_merge($angularModules, $component->getAngularModules());
}
\CRM_Utils_Hook::angularModules($angularModules);
foreach (array_keys($angularModules) as $module) {
if (!isset($angularModules[$module]['basePages'])) {
$angularModules[$module]['basePages'] = array('civicrm/a');
}
}
$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').
*/
public function resolveDependencies($names) {
$allModules = $this->getModules();
$visited = array();
$result = $names;
while (($missingModules = array_diff($result, array_keys($visited))) && !empty($missingModules)) {
foreach ($missingModules as $module) {
$visited[$module] = 1;
if (!isset($allModules[$module])) {
\Civi::log()->warning('Unrecognized Angular module {name}. Please ensure that all Angular modules are declared.', array(
'name' => $module,
'civi.tag' => 'deprecated',
));
}
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 = array();
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 = array();
foreach ($modules as $moduleKey => $module) {
foreach (array('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 = array();
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 = array();
$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, array(
'domain' => array($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 = array();
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 = array();
$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 = array();
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 = array();
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 'requires':
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 = array();
\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;
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Civi\Angular\Page;
/**
* This page is simply a container; any Angular modules defined by CiviCRM (or by CiviCRM extensions)
* will be activated on this page.
*
* @link https://issues.civicrm.org/jira/browse/CRM-14479
*/
class Main extends \CRM_Core_Page {
/**
* The weight to assign to any Angular JS module files.
*/
const DEFAULT_MODULE_WEIGHT = 200;
/**
* The resource manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \CRM_Core_Resources
* @deprecated
*/
public $res;
/**
* The Angular module manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \Civi\Angular\Manager
* @deprecated
*/
public $angular;
/**
* The region of the page into which JavaScript will be loaded.
*
* @var String
* @deprecated
*/
public $region;
/**
* @param string $title
* Title of the page.
* @param int $mode
* Mode of the page.
* @param \CRM_Core_Resources|null $res
* Resource manager.
*/
public function __construct($title = NULL, $mode = NULL, $res = NULL) {
parent::__construct($title, $mode);
$this->res = \CRM_Core_Resources::singleton();
$this->angular = \Civi::service('angular');
$this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header';
}
/**
* This function takes care of all the things common to all
* pages. This typically involves assigning the appropriate
* smarty variable :)
*
* @return string
* The content generated by running this page
*/
public function run() {
$this->registerResources();
return parent::run();
}
/**
* Register resources required by Angular.
*/
public function registerResources() {
$loader = new \Civi\Angular\AngularLoader();
$loader->setPageName('civicrm/a');
$loader->setModules(array('crmApp'));
$loader->load();
// If trying to load an Angular page via AJAX, the route must be passed as a
// URL parameter, since the server doesn't receive information about
// URL fragments (i.e, what comes after the #).
\CRM_Core_Resources::singleton()->addSetting(array(
'crmApp' => array(
'defaultRoute' => NULL,
),
'angularRoute' => \CRM_Utils_Request::retrieve('route', 'String'),
));
}
/**
* @inheritdoc
*/
public function getTemplateFileName() {
return 'Civi/Angular/Page/Main.tpl';
}
}

View file

@ -0,0 +1,178 @@
<?php
namespace Civi\Angular\Page;
/**
* This page aggregates data from Angular modules.
*
* Example: Aggregate metadata about all modules in JSON format.
* civicrm/ajax/angular-modules?format=json
*
* Example: Aggregate metadata for crmUi and crmUtil modules.
* civicrm/ajax/angular-modules?format=json&modules=crmUi,crmUtil
*
* Example: Aggregate *.js files for all modules.
* civicrm/ajax/angular-modules?format=js
*
* Example: Aggregate *.css files for all modules.
* civicrm/ajax/angular-modules?format=css
*/
class Modules extends \CRM_Core_Page {
/**
* Generate asset content (when accessed via older, custom
* "civicrm/ajax/anulgar-modules" route).
*
* @deprecated
*/
public function run() {
/**
* @var \Civi\Angular\Manager $angular
*/
$angular = \Civi::service('angular');
$moduleNames = $this->parseModuleNames(\CRM_Utils_Request::retrieve('modules', 'String'), $angular);
switch (\CRM_Utils_Request::retrieve('format', 'String')) {
case 'json':
case '':
$this->send(
'application/javascript',
json_encode($this->getMetadata($moduleNames, $angular))
);
break;
case 'js':
$this->send(
'application/javascript',
$this->digestJs($angular->getResources($moduleNames, 'js', 'path'))
);
break;
case 'css':
$this->send(
'text/css',
\CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n")
);
break;
default:
\CRM_Core_Error::fatal("Unrecognized format");
}
\CRM_Utils_System::civiExit();
}
/**
* Generate asset content (when accessed via AssetBuilder).
*
* @param \Civi\Core\Event\GenericHookEvent $event
* @see CRM_Utils_hook::buildAsset()
* @see \Civi\Core\AssetBuilder
*/
public static function buildAngularModules($event) {
$page = new Modules();
$angular = \Civi::service('angular');
switch ($event->asset) {
case 'angular-modules.json':
$moduleNames = $page->parseModuleNames(\CRM_Utils_Array::value('modules', $event->params), $angular);
$event->mimeType = 'application/json';
$event->content = json_encode($page->getMetadata($moduleNames, $angular));
break;
case 'angular-modules.js':
$moduleNames = $page->parseModuleNames(\CRM_Utils_Array::value('modules', $event->params), $angular);
$event->mimeType = 'application/javascript';
$event->content = $page->digestJs($angular->getResources($moduleNames, 'js', 'path'));
break;
case 'angular-modules.css':
$moduleNames = $page->parseModuleNames(\CRM_Utils_Array::value('modules', $event->params), $angular);
$event->mimeType = 'text/css';
$event->content = \CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n");
default:
// Not our problem.
}
}
/**
* @param array $files
* File paths.
* @return string
*/
public function digestJs($files) {
$scripts = array();
foreach ($files as $file) {
$scripts[] = file_get_contents($file);
}
$scripts = \CRM_Utils_JS::dedupeClosures(
$scripts,
array('angular', '$', '_'),
array('angular', 'CRM.$', 'CRM._')
);
// This impl of stripComments currently adds 10-20ms and cuts ~7%
return \CRM_Utils_JS::stripComments(implode("\n", $scripts));
}
/**
* @param string $modulesExpr
* Comma-separated list of module names.
* @param \Civi\Angular\Manager $angular
* @return array
* Any well-formed module names. All if moduleExpr is blank.
*/
public function parseModuleNames($modulesExpr, $angular) {
if ($modulesExpr) {
$moduleNames = preg_grep(
'/^[a-zA-Z0-9\-_\.]+$/',
explode(',', $modulesExpr)
);
return $moduleNames;
}
else {
$moduleNames = array_keys($angular->getModules());
return $moduleNames;
}
}
/**
* @param array $moduleNames
* List of module names.
* @param \Civi\Angular\Manager $angular
* @return array
*/
public function getMetadata($moduleNames, $angular) {
$modules = $angular->getModules();
$result = array();
foreach ($moduleNames as $moduleName) {
if (isset($modules[$moduleName])) {
$result[$moduleName] = array();
$result[$moduleName]['domain'] = $modules[$moduleName]['ext'];
$result[$moduleName]['js'] = $angular->getResources($moduleName, 'js', 'rawUrl');
$result[$moduleName]['css'] = $angular->getResources($moduleName, 'css', 'rawUrl');
$result[$moduleName]['partials'] = $angular->getPartials($moduleName);
$result[$moduleName]['strings'] = $angular->getTranslatedStrings($moduleName);
}
}
return $result;
}
/**
* Send a response.
*
* @param string $type
* Content type.
* @param string $data
* Content.
*/
public function send($type, $data) {
// Encourage browsers to cache for a long time - 1 year
$ttl = 60 * 60 * 24 * 364;
\CRM_Utils_System::setHttpHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl));
\CRM_Utils_System::setHttpHeader("Content-Type", $type);
\CRM_Utils_System::setHttpHeader("Cache-Control", "max-age=$ttl, public");
echo $data;
}
}