];
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();
}
}
}
];
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')
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);
}
}
}, $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);
/**
* 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()
*/
*/
protected $checkPermissions = TRUE;
+ /**
+ * Add debugging info to the api result.
+ *
+ * @var bool
+ */
+ protected $debug = FALSE;
+
/**
* @var string
*/
*/
private $_id;
+ protected $_debugOutput = [];
+
/**
* Action constructor.
*
/** @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;
}
/**
* @var string
*/
public $action;
+ /**
+ * @var array
+ */
+ public $debug;
/**
* Api version
* @var int
$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);
*/
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
$results = [];
$sql = $this->query->toSQL();
+ if (is_array($this->debugOutput)) {
+ $this->debugOutput['main_sql'] = $sql;
+ }
$query = \CRM_Core_DAO::executeQuery($sql);
while ($query->fetch()) {
</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>
$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,
$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
*/
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;
}