*/
public function checkAll() {
$messages = array_merge(
+ $this->checkCxnOverrides(),
$this->checkLogFileIsNotAccessible(),
$this->checkUploadsAreNotAccessible(),
$this->checkDirectoriesAreNotBrowseable(),
return $messages;
}
+ /**
+ * Check that the sysadmin has not modified the Cxn
+ * security setup.
+ */
+ public function checkCxnOverrides() {
+ $list = array();
+ if (defined('CIVICRM_CXN_CA') && CIVICRM_CXN_CA !== 'CiviRootCA') {
+ $list[] = 'CIVICRM_CXN_CA';
+ }
+ if (defined('CIVICRM_CXN_APPS_VERIFY') && !CIVICRM_CXN_APPS_VERIFY) {
+ $list[] = 'CIVICRM_CXN_APPS_VERIFY';
+ }
+ if (defined('CIVICRM_CXN_APPS_URL') && CIVICRM_CXN_APPS_URL !== \Civi\Cxn\Rpc\Constants::OFFICIAL_APPMETAS_URL) {
+ $list[] = 'CIVICRM_CXN_APPS_URL';
+ }
+
+ $messages = array();
+
+ if (!empty($list)) {
+ $messages[] = new CRM_Utils_Check_Message(
+ 'checkCxnOverrides',
+ ts('The system administrator has disabled security settings (%1). Connections to remote applications are insecure.', array(
+ 1 => implode(', ', $list),
+ )),
+ ts('Security Warning')
+ );
+ }
+
+ return $messages;
+ }
+
/**
* Determine whether $url is a public, browsable listing for $dir
*
'ext' => 'civicrm',
'js' => array('ang/crmAutosave.js'),
);
+ $angularModules['crmCxn'] = array(
+ 'ext' => 'civicrm',
+ 'js' => array('ang/crmCxn.js', 'ang/crmCxn/*.js'),
+ 'css' => array('ang/crmCxn.css'),
+ 'partials' => array('ang/crmCxn'),
+ );
//$angularModules['crmExample'] = array(
// 'ext' => 'civicrm',
// 'js' => array('ang/crmExample.js'),
--- /dev/null
+.crmCxn-footer {
+ text-align: center;
+}
--- /dev/null
+(function (angular, $, _) {
+
+ angular.module('crmCxn', [
+ 'crmUtil', 'ngRoute', 'ngSanitize', 'ui.utils', 'crmUi', 'dialogService'
+ ]);
+
+ angular.module('crmCxn').config([
+ '$routeProvider',
+ function ($routeProvider) {
+ $routeProvider.when('/cxn', {
+ templateUrl: '~/crmCxn/ManageCtrl.html',
+ controller: 'CrmCxnManageCtrl',
+ resolve: {
+ apiCalls: function(crmApi){
+ var reqs = {};
+ reqs.cxns = ['Cxn', 'get', {sequential: 1}];
+ reqs.appMetas = ['CxnApp', 'get', {sequential: 1, return: ['id', 'title', 'desc', 'appId', 'appUrl', 'perm']}];
+ reqs.sysCheck = ['System', 'check', {}]; // FIXME: filter on checkCxnOverrides
+ return crmApi(reqs);
+ }
+ }
+ });
+ }
+ ]);
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div ng-controller="CrmCxnConfirmConnectCtrl">
+ <p>{{ts('The application, \"%1\", requests permission to access your system.', {1: appMeta.title})}}</p>
+ <div crm-ui-accordion="{title: ts('About'), collapsed: true}">
+ <div ng-bind-html="appMeta.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Summary'), collapsed: true}">
+ <div ng-bind-html="appMeta.perm.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Details'), collapsed: true}">
+ <div crm-cxn-perm-table="{perm: appMeta.perm}"></div>
+ </div>
+</div>
--- /dev/null
+(function(angular, $, _) {
+ angular.module('crmCxn').controller('CrmCxnConfirmConnectCtrl', function($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div crm-ui-debug="appMetas"></div>
+<div crm-ui-debug="cxns"></div>
+<div crm-ui-debug="alerts"></div>
+
+<!--
+ The merits of this layout:
+ * On a fresh install, the available connections show up first.
+ * Once you've made a connection, the extant connections bubble up.
+ * Extant connections can be portrayed as enabled or disabled.
+-->
+
+<div ng-show="cxns.length > 0">
+ <span crm-ui-order="{var: 'cxnOrder', defaults: ['-created_date']}"></span>
+ <h3>{{ts('Existing Connections')}}</h3>
+ <table class="display">
+ <thead>
+ <tr>
+ <th>{{ts('Title')}}</th> <!-- <a crm-ui-order-by="[cxnOrder, 'app_meta.appId']"> -->
+ <th>{{ts('Description')}}</th> <!-- <a crm-ui-order-by="[cxnOrder, 'desc']"> -->
+ <th>{{ts('Status')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="cxn in cxns | orderBy:cxnOrder.get()">
+ <td>{{cxn.app_meta.title}}</td>
+ <td><div ng-bind-html="cxn.app_meta.desc"></div></td>
+ <td>{{cxn.is_active ? ts('Enabled') : ts('Disabled')}}</td>
+ <td>
+ <!--
+ <a class="action-item crm-hover-button" ng-click="toggleCxn(cxn)">{{cxn.is_active ? ts('Disable') : ts('Enable') }}</a>
+ -->
+ <a class="action-item crm-hover-button"
+ crm-confirm='{width: "65%", resizable: true, title: ts("Disconnect"), message: ts("Are you sure you want to disconnect \"%1?\". Doing so may permanently destroy data linkage.", {1: cxn.app_meta.title})}'
+ on-yes="unregister(cxn.app_meta)"
+ >{{ts('Disconnect')}}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<div ng-show="hasAvailApps()">
+ <span crm-ui-order="{var: 'availOrder', defaults: ['title']}"></span>
+
+ <h3>{{ts('New Connections')}}</h3>
+ <table class="display">
+ <thead>
+ <tr>
+ <th><a crm-ui-order-by="[availOrder, 'title']">{{ts('Title')}}</a></th>
+ <th><a crm-ui-order-by="[availOrder, 'desc']">{{ts('Description')}}</a></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="appMeta in appMetas | orderBy:availOrder.get()" ng-show="!findCxnByAppId(appMeta.appId)">
+ <td>{{appMeta.title}}</td>
+ <td><div ng-bind-html="appMeta.desc"></div></td>
+ <td>
+ <a class="action-item crm-hover-button"
+ crm-confirm='{width: "65%", resizable: true, title:ts("Connect"), templateUrl: "~/crmCxn/ConfirmConnectCtrl.html", export: {appMeta: appMeta}}'
+ on-yes="register(appMeta)"
+ >{{ts('Connect')}}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<div ng-show="$.isEmptyObject(appMetas)" class="messages status no-popup">
+ <div class="icon inform-icon"></div>
+ {{ts('No available applications')}}
+</div>
--- /dev/null
+(function(angular, $, _) {
+
+ angular.module('crmCxn').controller('CrmCxnManageCtrl', function CrmCxnManageCtrl($scope, apiCalls, crmApi, crmUiAlert, crmBlocker, crmStatus, $timeout) {
+ var ts = $scope.ts = CRM.ts(null);
+ $scope.appMetas = apiCalls.appMetas.values;
+ $scope.cxns = apiCalls.cxns.values;
+ $scope.alerts = _.where(apiCalls.sysCheck.values, {name: 'checkCxnOverrides'});
+
+ $scope.filter = {};
+ var block = $scope.block = crmBlocker();
+
+ _.each($scope.alerts, function(alert){
+ crmUiAlert({text: alert.message, title: alert.title, type: 'error'});
+ });
+
+ $scope.findCxnByAppId = function(appId) {
+ var result = _.where($scope.cxns, {
+ app_guid: appId
+ });
+ switch (result.length) {
+ case 0:
+ return null;
+ case 1:
+ return result[0];
+ default:
+ throw "Error: Too many connections for appId: " + appId;
+ }
+ };
+
+ $scope.hasAvailApps = function() {
+ // This should usu return after the 1st or 2nd item, but in testing with small# apps, we may exhaust the list.
+ for (var i = 0; i< $scope.appMetas.length; i++) {
+ if (!$scope.findCxnByAppId($scope.appMetas[i].appId)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ $scope.refreshCxns = function() {
+ crmApi('Cxn', 'get', {sequential: 1}).then(function(result) {
+ $timeout(function(){
+ $scope.cxns = result.values;
+ });
+ });
+ };
+
+ $scope.register = function(appMeta) {
+ var reg = crmApi('Cxn', 'register', {app_guid: appMeta.appId}).then($scope.refreshCxns);
+ return block(crmStatus({start: ts('Connecting...'), success: ts('Connected')}, reg));
+ };
+
+ $scope.unregister = function(appMeta) {
+ var reg = crmApi('Cxn', 'unregister', {app_guid: appMeta.appId, debug: 1}).then($scope.refreshCxns);
+ return block(crmStatus({start: ts('Disconnecting...'), success: ts('Disconnected')}, reg));
+ };
+
+ $scope.toggleCxn = function toggleCxn(cxn) {
+ var reg = crmApi('Cxn', 'create', {id: cxn.id, is_active: !cxn.is_active, debug: 1}).then(function(){
+ cxn.is_active = !cxn.is_active;
+ });
+ return block(crmStatus({start: ts('Saving...'), success: ts('Saved')}, reg));
+ };
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<table>
+ <thead>
+ <tr>
+ <th>{{ts('Entity')}}</th>
+ <th>{{ts('Action(s)')}}</th>
+ <th>{{ts('Filter(s)')}}</th>
+ <th>{{ts('Field(s)')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="api in perm.api"
+ ng-class-even="'even-row even'"
+ ng-class-odd="'odd-row odd'">
+ <td>
+ <em ng-show="api.entity == '*'">{{ts('Any')}}</em>
+ <code ng-hide="api.entity == '*'">{{api.entity}}</code>
+ </td>
+ <td>
+ <div ng-switch="isString(api.actions)">
+ <span ng-switch-when="true">
+ <em ng-show="api.actions == '*'">{{ts('Any')}}</em>
+ <code ng-hide="api.actions == '*'">{{api.actions}}</code>
+ </span>
+ <span ng-switch-default>
+ <span ng-repeat="action in api.actions"><code>{{action}}</code><span ng-show="!$last">, </span></span>
+ </span>
+ </div>
+ </td>
+ <td>
+ <em ng-show="!hasRequiredFilters(api)">{{ts('Any')}}</em>
+ <div ng-repeat="(field,value) in api.required"><code>{{field}}</code> = "<code>{{value}}</code>"<span ng-show="!$last">, </span></div>
+ </td>
+ <td>
+ <em ng-show="api.fields == '*'">{{ts('Any')}}</em>
+ <span ng-hide="api.fields == '*'" ng-repeat="field in api.fields"><code>{{field}}</code><span ng-show="!$last">, </span></span>
+ </td>
+ </tr>
+ </tbody>
+</table>
+<div class="crmCxn-footer">
+ <em ng-bind-html="ts('For in-depth details about entities and actions, see the <a href=\'%1\' target=\'%2\'>API Explorer</a>.', {1: apiExplorerUrl, 2: '_blank'})"></em>
+</div>
--- /dev/null
+(function(angular, $, _) {
+
+ // This directive formats the data in appMeta.perm as a nice table.
+ // example: <div crm-cxn-perm-table="{perm: cxn.app_meta.perm}"></div>
+ angular.module('crmCxn').directive('crmCxnPermTable', function crmCxnPermTable() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmCxnPermTable: '='
+ },
+ templateUrl: '~/crmCxn/PermTable.html',
+ link: function(scope, element, attrs) {
+ scope.ts = CRM.ts(null);
+ scope.hasRequiredFilters = function(api) {
+ return !_.isEmpty(api.required);
+ };
+ scope.isString = function(v) {
+ return _.isString(v);
+ };
+ scope.apiExplorerUrl = CRM.url('civicrm/api');
+ scope.$watch('crmCxnPermTable', function(crmCxnPermTable){
+ scope.perm = crmCxnPermTable.perm;
+ });
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
* API result array.
*/
function civicrm_api3_cxn_get($params) {
- return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params);
+ $result = _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params);
+ if (is_array($result['values'])) {
+ foreach (array_keys($result['values']) as $i) {
+ if (!empty($result['values'][$i]['app_meta'])) {
+ $result['values'][$i]['app_meta'] = json_decode($result['values'][$i]['app_meta'], TRUE);
+ }
+ if (!empty($result['values'][$i]['perm'])) {
+ $result['values'][$i]['perm'] = json_decode($result['values'][$i]['perm'], TRUE);
+ }
+ }
+ }
+ return $result;
}