Add debug info to api4 output
authorColeman Watts <coleman@civicrm.org>
Wed, 22 Jan 2020 11:46:30 +0000 (06:46 -0500)
committerColeman Watts <coleman@civicrm.org>
Wed, 22 Jan 2020 14:17:49 +0000 (09:17 -0500)
CRM/Api4/Page/AJAX.php
CRM/Api4/Page/Api4Explorer.php
Civi/API/Subscriber/XDebugSubscriber.php
Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php
Civi/Api4/Generic/AbstractAction.php
Civi/Api4/Generic/Result.php
Civi/Api4/Generic/Traits/DAOActionTrait.php
Civi/Api4/Query/Api4SelectQuery.php
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js
css/api4-explorer.css

index 39c8febe29428bdbe3db4eceb8513aa7e9b24c77..da37b04682985781db5a365fc20da5a20abb79d2 100644 (file)
@@ -89,8 +89,8 @@ class CRM_Api4_Page_AJAX extends CRM_Core_Page {
       ];
       if (CRM_Core_Permission::check('view debug output')) {
         $response['error_message'] = $e->getMessage();
-        if (\Civi::settings()->get('backtrace')) {
-          $response['backtrace'] = $e->getTrace();
+        if (!empty($params['debug']) && \Civi::settings()->get('backtrace')) {
+          $response['debug']['backtrace'] = $e->getTrace();
         }
       }
     }
index 65af14d1475c01ac744a96a86923b86d73065d32..f1fb6f734d67c5757c7c30f22501551f492ce718 100644 (file)
@@ -30,6 +30,7 @@ class CRM_Api4_Page_Api4Explorer extends CRM_Core_Page {
     ];
     Civi::resources()
       ->addVars('api4', $vars)
+      ->addPermissions(['access debug output'])
       ->addScriptFile('civicrm', 'js/load-bootstrap.js')
       ->addScriptFile('civicrm', 'bower_components/js-yaml/dist/js-yaml.min.js')
       ->addScriptFile('civicrm', 'bower_components/google-code-prettify/bin/prettify.min.js')
index 4b2f6d9a24d8fe7c88890795fff15596a0dc3f25..aec0c1259e935c838fe4b51ee7e651f1503daae0 100644 (file)
@@ -36,14 +36,26 @@ class XDebugSubscriber implements EventSubscriberInterface {
   public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
     $apiRequest = $event->getApiRequest();
     $result = $event->getResponse();
-    if (function_exists('xdebug_time_index')
-      && \CRM_Utils_Array::value('debug', $apiRequest['params'])
-      // result would not be an array for getvalue
-      && is_array($result)
+    if (
+      function_exists('xdebug_time_index')
+      && !empty($apiRequest['params']['debug'])
+      && (empty($apiRequest['params']['check_permissions']) || \CRM_Core_Permission::check('view debug output'))
     ) {
-      $result['xdebug']['peakMemory'] = xdebug_peak_memory_usage();
-      $result['xdebug']['memory'] = xdebug_memory_usage();
-      $result['xdebug']['timeIndex'] = xdebug_time_index();
+      if (is_a($result, '\Civi\Api4\Generic\Result')) {
+        $result->debug = $result->debug ?? [];
+        $debug =& $result->debug;
+      }
+      // result would not be an array for api3 getvalue
+      elseif (is_array($result)) {
+        $result['xdebug'] = $result['xdebug'] ?? [];
+        $debug =& $result['xdebug'];
+      }
+      else {
+        return;
+      }
+      $debug['peakMemory'] = xdebug_peak_memory_usage();
+      $debug['memory'] = xdebug_memory_usage();
+      $debug['timeIndex'] = xdebug_time_index();
       $event->setResponse($result);
     }
   }
index 00b2cc943997141c6175c052ea744e73e070ec99..7add31036bcd06c5eb68d72e89421b75d3a8ce4a 100644 (file)
@@ -240,9 +240,13 @@ class PostSelectQuerySubscriber implements EventSubscriberInterface {
     }, $selects, array_keys($selects));
 
     $newSelect = sprintf('SELECT DISTINCT %s', implode(", ", $aliasedSelects));
-    $sql = str_replace("\n", ' ', $query->getQuery()->toSQL());
-    $originalSelect = substr($sql, 0, strpos($sql, ' FROM'));
-    $sql = str_replace($originalSelect, $newSelect, $sql);
+    $sql = $query->getQuery()->toSQL();
+    // Replace the "SELECT" clause
+    $sql = $newSelect . substr($sql, strpos($sql, "\nFROM"));
+
+    if (is_array($query->debugOutput)) {
+      $query->debugOutput['join_sql'][] = $sql;
+    }
 
     $relatedResults = [];
     $resultDAO = \CRM_Core_DAO::executeQuery($sql);
index 965c5fc7242a4b57568a7d6763a35da548e45353..24570099483de909e43773226a47ab9c64ff3e56 100644 (file)
@@ -26,8 +26,10 @@ use Civi\Api4\Utils\ActionUtil;
 /**
  * Base class for all api actions.
  *
- * @method $this setCheckPermissions(bool $value)
+ * @method $this setCheckPermissions(bool $value) Enable/disable permission checks
  * @method bool getCheckPermissions()
+ * @method $this setDebug(bool $value) Enable/disable debug output
+ * @method bool getDebug()
  * @method $this setChain(array $chain)
  * @method array getChain()
  */
@@ -70,6 +72,13 @@ abstract class AbstractAction implements \ArrayAccess {
    */
   protected $checkPermissions = TRUE;
 
+  /**
+   * Add debugging info to the api result.
+   *
+   * @var bool
+   */
+  protected $debug = FALSE;
+
   /**
    * @var string
    */
@@ -107,6 +116,8 @@ abstract class AbstractAction implements \ArrayAccess {
    */
   private $_id;
 
+  protected $_debugOutput = [];
+
   /**
    * Action constructor.
    *
@@ -216,7 +227,14 @@ abstract class AbstractAction implements \ArrayAccess {
     /** @var \Civi\API\Kernel $kernel */
     $kernel = \Civi::service('civi_api_kernel');
 
-    return $kernel->runRequest($this);
+    $result = $kernel->runRequest($this);
+    if ($this->debug && (!$this->checkPermissions || \CRM_Core_Permission::check('view debug output'))) {
+      $result->debug = array_merge($result->debug, $this->_debugOutput);
+    }
+    else {
+      $result->debug = NULL;
+    }
+    return $result;
   }
 
   /**
index ca10ef6f5d99797b83c479c1a01d61aba513c084..c32bfa57496f04bee41d823bf39c4038c8384168 100644 (file)
@@ -23,6 +23,10 @@ class Result extends \ArrayObject {
    * @var string
    */
   public $action;
+  /**
+   * @var array
+   */
+  public $debug;
   /**
    * Api version
    * @var int
index 941056a050f5267aaa974083948bafde3d182144..451e31f7f264270b63001383050a1897514f0d91 100644 (file)
@@ -75,6 +75,9 @@ trait DAOActionTrait {
     $query->orderBy = $this->getOrderBy();
     $query->limit = $this->getLimit();
     $query->offset = $this->getOffset();
+    if ($this->getDebug()) {
+      $query->debugOutput =& $this->_debugOutput;
+    }
     $result = $query->run();
     if (is_array($result)) {
       \CRM_Utils_API_HTMLInputCoder::singleton()->decodeRows($result);
index d0baccee6be7a8837f9b8bf91e38d12b8d5f8532..3556aaca8e9458ba065f1d7868621537b7b5d1d2 100644 (file)
@@ -54,6 +54,13 @@ class Api4SelectQuery extends SelectQuery {
    */
   protected $joinedTables = [];
 
+  /**
+   * If set to an array, this will start collecting debug info.
+   *
+   * @var null|array
+   */
+  public $debugOutput = NULL;
+
   /**
    * @param string $entity
    * @param bool $checkPermissions
@@ -105,6 +112,9 @@ class Api4SelectQuery extends SelectQuery {
 
     $results = [];
     $sql = $this->query->toSQL();
+    if (is_array($this->debugOutput)) {
+      $this->debugOutput['main_sql'] = $sql;
+    }
     $query = \CRM_Core_DAO::executeQuery($sql);
 
     while ($query->fetch()) {
index bd7b8ba5e206a429869c5b38c65d31a2f8f9fc5e..462581267ba2fc3e771980c3390a357f999bbea3 100644 (file)
         </div>
       </div>
       <div class="panel explorer-result-panel panel-{{ status }}" >
-        <div class="panel-heading">
-          <h3 class="panel-title">
-            <i class="fa fa-circle-o" ng-if="status === 'default'"></i>
-            <i class="fa fa-check-circle" ng-if="status === 'success'"></i>
-            <i class="fa fa-minus-circle" ng-if="status === 'danger'"></i>
-            <i class="fa fa-spinner fa-pulse" ng-if="status === 'warning'"></i>
-            {{ ts('Result') }}
-          </h3>
-        </div>
+        <ul class="panel-heading nav nav-tabs">
+          <li role="presentation" ng-class="{active: resultTab.selected === 'result'}">
+            <a href ng-click="resultTab.selected = 'result'">
+              <i class="fa fa-fw fa-circle-o" ng-if="status === 'default'"></i>
+              <i class="fa fa-fw fa-check-circle" ng-if="status === 'success'"></i>
+              <i class="fa fa-fw fa-minus-circle" ng-if="status === 'danger'"></i>
+              <i class="fa fa-fw fa-spinner fa-pulse" ng-if="status === 'warning'"></i>
+              <span>{{ ts('Result') }}</span>
+            </a>
+          </li>
+          <li role="presentation" ng-if="perm.accessDebugOutput" ng-class="{active: resultTab.selected === 'debug'}">
+            <a href ng-click="resultTab.selected = 'debug'">
+              <i class="fa fa-fw fa-{{ debug ? 'bug' : 'circle-o' }}"></i>
+              <span>{{ ts('Debug') }}</span>
+            </a>
+          </li>
+        </ul>
         <div class="panel-body">
-          <pre class="prettyprint" ng-repeat="code in result" ng-bind-html="code"></pre>
+          <div ng-show="resultTab.selected === 'result'">
+            <pre class="prettyprint" ng-repeat="code in result" ng-bind-html="code"></pre>
+          </div>
+          <div ng-show="resultTab.selected === 'debug'">
+            <pre ng-if="debug" class="prettyprint" ng-bind-html="debug"></pre>
+            <p ng-if="!debug">
+              {{ ts('To view debugging output, enable the debug param before executing.') }}
+            </p>
+            <p ng-if="!debug">
+              {{ ts('Enable backtrace in system settings to see error backtraces.') }}
+            </p>
+          </div>
         </div>
       </div>
   </div>
index 33596ec3e52b3ea431054656e7f7136ea2a8c5ba..10a78d5ae6b34b371e28725cc0083d34ee431516 100644 (file)
     $scope.availableParams = {};
     $scope.params = {};
     $scope.index = '';
+    $scope.resultTab = {selected: 'result'};
+    $scope.perm = {
+      accessDebugOutput: CRM.checkPerm('access debug output')
+    };
     var getMetaParams = {},
       objectParams = {orderBy: 'ASC', values: '', chain: ['Entity', '', '{}']},
       docs = CRM.vars.api4.docs,
@@ -39,6 +43,7 @@
     $scope.helpContent = {};
     $scope.entity = $routeParams.api4entity;
     $scope.result = [];
+    $scope.debug = null;
     $scope.status = 'default';
     $scope.loading = false;
     $scope.controls = {};
       }).then(function(resp) {
           $scope.loading = false;
           $scope.status = 'success';
+          $scope.debug = debugFormat(resp.data);
           $scope.result = [formatMeta(resp.data), prettyPrintOne(_.escape(JSON.stringify(resp.data.values, null, 2)), 'js', 1)];
         }, function(resp) {
           $scope.loading = false;
           $scope.status = 'danger';
+          $scope.debug = debugFormat(resp.data);
           $scope.result = [formatMeta(resp), prettyPrintOne(_.escape(JSON.stringify(resp.data, null, 2)))];
         });
     };
 
+    function debugFormat(data) {
+      var debug = data.debug ? prettyPrintOne(_.escape(JSON.stringify(data.debug, null, 2)).replace(/\\n/g, "\n")) : null;
+      delete data.debug;
+      return debug;
+    }
+
     /**
      * Format value to look like php code
      */
index 9451bc26d60d864d4cc3568444aa0f06d994c3d5..8041edda26dd6cb750ab37db7721e06152388222 100644 (file)
   border-bottom-left-radius: 0;
   margin-bottom: 0;
 }
+#bootstrap-theme.api4-explorer-page .panel-heading.nav-tabs {
+  padding: 8px 0 0 20px;
+}
+#bootstrap-theme .panel-heading>li>a {
+  background-color: #f1f1f18c
+}
 #bootstrap-theme.api4-explorer-page .explorer-code-panel table td:first-child {
   width: 5em;
 }