CRM-16173 - crmCxn - Add UI for connecting and disconnecting
authorTim Otten <totten@civicrm.org>
Fri, 27 Mar 2015 11:08:04 +0000 (04:08 -0700)
committerTim Otten <totten@civicrm.org>
Tue, 14 Jul 2015 04:00:07 +0000 (21:00 -0700)
 * Split available and actual connections
 * Display alert if sysadmin disables Cxn security
 * Display permissions when confirming

CRM/Utils/Check/Security.php
Civi/Angular/Manager.php
ang/crmCxn.css [new file with mode: 0644]
ang/crmCxn.js [new file with mode: 0644]
ang/crmCxn/ConfirmConnectCtrl.html [new file with mode: 0644]
ang/crmCxn/ConfirmConnectCtrl.js [new file with mode: 0644]
ang/crmCxn/ManageCtrl.html [new file with mode: 0644]
ang/crmCxn/ManageCtrl.js [new file with mode: 0644]
ang/crmCxn/PermTable.html [new file with mode: 0644]
ang/crmCxn/PermTable.js [new file with mode: 0644]
api/v3/Cxn.php

index 393fddbf1614ec0fc85d8cf7d43cf74a91a55d60..f0dd733adf0dac199f36ead287f0fddb74f0ebaf 100644 (file)
@@ -58,6 +58,7 @@ class CRM_Utils_Check_Security {
    */
   public function checkAll() {
     $messages = array_merge(
+      $this->checkCxnOverrides(),
       $this->checkLogFileIsNotAccessible(),
       $this->checkUploadsAreNotAccessible(),
       $this->checkDirectoriesAreNotBrowseable(),
@@ -242,6 +243,37 @@ class CRM_Utils_Check_Security {
     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
    *
index acf473bf3e838aa3331a896f6468ee2935a6e1f9..8977e1c14f5eb6451a7ba92e2b9155c8bbc60bfe 100644 (file)
@@ -81,6 +81,12 @@ class Manager {
         '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'),
diff --git a/ang/crmCxn.css b/ang/crmCxn.css
new file mode 100644 (file)
index 0000000..11aeb14
--- /dev/null
@@ -0,0 +1,3 @@
+.crmCxn-footer {
+  text-align: center;
+}
diff --git a/ang/crmCxn.js b/ang/crmCxn.js
new file mode 100644 (file)
index 0000000..38b4baf
--- /dev/null
@@ -0,0 +1,26 @@
+(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._);
diff --git a/ang/crmCxn/ConfirmConnectCtrl.html b/ang/crmCxn/ConfirmConnectCtrl.html
new file mode 100644 (file)
index 0000000..599be44
--- /dev/null
@@ -0,0 +1,12 @@
+<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>
diff --git a/ang/crmCxn/ConfirmConnectCtrl.js b/ang/crmCxn/ConfirmConnectCtrl.js
new file mode 100644 (file)
index 0000000..c303d5e
--- /dev/null
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+  angular.module('crmCxn').controller('CrmCxnConfirmConnectCtrl', function($scope) {
+    $scope.ts = CRM.ts(null);
+  });
+})(angular, CRM.$, CRM._);
diff --git a/ang/crmCxn/ManageCtrl.html b/ang/crmCxn/ManageCtrl.html
new file mode 100644 (file)
index 0000000..e6c0331
--- /dev/null
@@ -0,0 +1,73 @@
+<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>
diff --git a/ang/crmCxn/ManageCtrl.js b/ang/crmCxn/ManageCtrl.js
new file mode 100644 (file)
index 0000000..a0eb903
--- /dev/null
@@ -0,0 +1,66 @@
+(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._);
diff --git a/ang/crmCxn/PermTable.html b/ang/crmCxn/PermTable.html
new file mode 100644 (file)
index 0000000..08c35ff
--- /dev/null
@@ -0,0 +1,42 @@
+<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>
diff --git a/ang/crmCxn/PermTable.js b/ang/crmCxn/PermTable.js
new file mode 100644 (file)
index 0000000..eb7da35
--- /dev/null
@@ -0,0 +1,27 @@
+(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._);
index d5ccd3fcfc515715fab5cdaf1524f1ed71f7f149..4c0d29df90405c2b7b5dc94e14ebe573fab6a887 100644 (file)
@@ -173,5 +173,16 @@ function civicrm_api3_cxn_unregister($params) {
  *   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;
 }