From e417635897464a8ff258c37ed6a0a8c4e6805590 Mon Sep 17 00:00:00 2001
From: Coleman Watts
Date: Sun, 20 Apr 2014 23:19:00 -0700
Subject: [PATCH] Api Explorer - rewrite for 4.5
---
CRM/Admin/Page/APIExplorer.php | 1 +
CRM/Core/DAO.php | 12 +-
CRM/Utils/REST.php | 4 +-
api/v3/utils.php | 5 +-
templates/CRM/Admin/Page/APIExplorer.hlp | 60 ++++
templates/CRM/Admin/Page/APIExplorer.js | 399 +++++++++++++++--------
templates/CRM/Admin/Page/APIExplorer.tpl | 157 ++++++---
7 files changed, 454 insertions(+), 184 deletions(-)
create mode 100644 templates/CRM/Admin/Page/APIExplorer.hlp
diff --git a/CRM/Admin/Page/APIExplorer.php b/CRM/Admin/Page/APIExplorer.php
index 56f9bd540e..912425ca33 100644
--- a/CRM/Admin/Page/APIExplorer.php
+++ b/CRM/Admin/Page/APIExplorer.php
@@ -41,6 +41,7 @@ class CRM_Admin_Page_APIExplorer extends CRM_Core_Page {
function run() {
CRM_Utils_System::setTitle(ts('API explorer and generator'));
CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Admin/Page/APIExplorer.js');
+ $this->assign('operators', CRM_Core_DAO::acceptedSQLOperators());
return parent::run();
}
diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php
index c722c18a5c..4652b79e79 100644
--- a/CRM/Core/DAO.php
+++ b/CRM/Core/DAO.php
@@ -1898,9 +1898,8 @@ EOS;
public static function createSQLFilter($fieldName, $filter, $type, $alias = NULL, $returnSanitisedArray = FALSE) {
// http://issues.civicrm.org/jira/browse/CRM-9150 - stick with 'simple' operators for now
// support for other syntaxes is discussed in ticket but being put off for now
- $acceptedSQLOperators = array('=', '<=', '>=', '>', '<', 'LIKE', "<>", "!=", "NOT LIKE", 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN');
foreach ($filter as $operator => $criteria) {
- if (in_array($operator, $acceptedSQLOperators)) {
+ if (in_array($operator, self::acceptedSQLOperators())) {
switch ($operator) {
// unary operators
case 'IS NULL':
@@ -1957,6 +1956,15 @@ EOS;
}
}
+ /**
+ * @see http://issues.civicrm.org/jira/browse/CRM-9150
+ * support for other syntaxes is discussed in ticket but being put off for now
+ * @return array
+ */
+ public static function acceptedSQLOperators() {
+ return array('=', '<=', '>=', '>', '<', 'LIKE', "<>", "!=", "NOT LIKE", 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN');
+ }
+
/**
* SQL has a limit of 64 characters on various names:
* table name, trigger name, column name ...
diff --git a/CRM/Utils/REST.php b/CRM/Utils/REST.php
index 2111f63565..92acf3ce97 100644
--- a/CRM/Utils/REST.php
+++ b/CRM/Utils/REST.php
@@ -127,7 +127,7 @@ class CRM_Utils_REST {
if (CRM_Utils_Array::value('json', $requestParams)) {
header('Content-Type: text/javascript');
$json = json_encode(array_merge($result));
- if (CRM_Utils_Array::value('debug', $requestParams)) {
+ if (CRM_Utils_Array::value('prettyprint', $requestParams)) {
return self::jsonFormated($json);
}
return $json;
@@ -491,7 +491,7 @@ class CRM_Utils_REST {
if (!$config->debug && (!array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) ||
$_SERVER['HTTP_X_REQUESTED_WITH'] != "XMLHttpRequest"
)) {
- $error = civicrm_api3_create_error("SECURITY ALERT: Ajax requests can only be issued by javascript clients, eg. CRM.api().",
+ $error = civicrm_api3_create_error("SECURITY ALERT: Ajax requests can only be issued by javascript clients, eg. CRM.api3().",
array(
'IP' => $_SERVER['REMOTE_ADDR'],
'level' => 'security',
diff --git a/api/v3/utils.php b/api/v3/utils.php
index ebb0a0cad1..7bcd686277 100644
--- a/api/v3/utils.php
+++ b/api/v3/utils.php
@@ -197,7 +197,7 @@ function civicrm_api3_create_success($values = 1, $params = array(), $entity = N
$allFields = array_keys($apiFields['values']);
}
$paramFields = array_keys($params);
- $undefined = array_diff($paramFields, $allFields, array_keys($_COOKIE), array('action', 'entity', 'debug', 'version', 'check_permissions', 'IDS_request_uri', 'IDS_user_agent', 'return', 'sequential', 'rowCount', 'option_offset', 'option_limit', 'custom', 'option_sort', 'options'));
+ $undefined = array_diff($paramFields, $allFields, array_keys($_COOKIE), array('action', 'entity', 'debug', 'version', 'check_permissions', 'IDS_request_uri', 'IDS_user_agent', 'return', 'sequential', 'rowCount', 'option_offset', 'option_limit', 'custom', 'option_sort', 'options', 'prettyprint'));
if ($undefined) {
$result['undefined_fields'] = array_merge($undefined);
}
@@ -569,9 +569,6 @@ function _civicrm_api3_dao_set_filter(&$dao, $params, $unique = TRUE, $entity) {
}
}
}
- // http://issues.civicrm.org/jira/browse/CRM-9150 - stick with 'simple' operators for now
- // support for other syntaxes is discussed in ticket but being put off for now
- $acceptedSQLOperators = array('=', '<=', '>=', '>', '<', 'LIKE', "<>", "!=", "NOT LIKE", 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN');
if (!$fields) {
$fields = array();
}
diff --git a/templates/CRM/Admin/Page/APIExplorer.hlp b/templates/CRM/Admin/Page/APIExplorer.hlp
new file mode 100644
index 0000000000..9346899600
--- /dev/null
+++ b/templates/CRM/Admin/Page/APIExplorer.hlp
@@ -0,0 +1,60 @@
+{*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.5 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+*}
+{htxt id="param-name-title"}
+ {ts}Parameter Name{/ts}
+{/htxt}
+{htxt id="param-name"}
+
+ {ts}Choose a parameter from the list, or select "other" for manual entry.{/ts}
+
+ {ts}The default operator is equals (=), and is the only operator supported for "create" or "delete" actions.{/ts}
+
+
+ {ts}Some apis support a special syntax to allow other operators for "get" type actions. Choosing a different operator will automatically use this syntax, although it may not work with every api.{/ts}
+
';
- if ( $('#extra [name=' + name + ']').length > 0) {
- $('#extra [name=' + name + ']').parent().remove();
- }
- else {
- $('#extra').append (h);
- }
+ function addField(name) {
+ $('#api-params').append($(fieldTpl({name: name || ''})));
+ var $row = $('tr:last-child', '#api-params');
+ $('.api-param-name', $row).select2({data: fields}).change();
}
- function buildForm (params) {
- var h = '';
- if (params.action == 'delete') {
- $('#extra').html(h);
+ function getFields() {
+ var required = [];
+ fields = [];
+ options = {};
+ // Special case for getfields
+ if (action === 'getfields') {
+ fields.push({
+ id: 'api_action',
+ text: 'Action'
+ });
+ options.api_action = [];
+ $('option', '#api-action').each(function() {
+ if (this.value) {
+ options.api_action.push({key: this.value, value: $(this).text()});
+ }
+ });
+ showFields(['api_action']);
return;
}
-
- CRM.api(params.entity, 'getFields', {}, {
- success:function (data) {
- h = '' + ts('Available fields (click to add/remove):') + '';
- $.each(data.values, function(key, value) {
- var required = value.required ? " required" : "";
- h += "" + value.title + "";
- });
- $('#selector').html(h);
- }
+ CRM.api3(entity, 'getFields', {'api_action': action, sequential: 1, options: {get_options: 'all'}}).done(function(data) {
+ _.each(data.values, function(field) {
+ if (field.name) {
+ fields.push({
+ id: field.name,
+ text: field.title || field.name,
+ required: field['api.required'] || false
+ });
+ if (field['api.required']) {
+ required.push(field.name);
+ }
+ if (field.options) {
+ options[field.name] = field.options;
+ }
+ }
+ });
+ showFields(required);
});
}
- function generateQuery () {
- var params = {};
- $('#api-explorer input:checkbox:checked, #api-explorer select, #extra input').each(function() {
- var val = $(this).val();
- if (val) {
- params[$(this).data('id')] = val;
- }
+ function showFields(required) {
+ fields.push({
+ id: '-',
+ text: ts('Other') + '...'
});
- query = CRM.url("civicrm/ajax/rest", params);
- $('#query').val(query);
- if (params.action == 'delete' && $('#selector a').length == 0) {
- buildForm (params);
- return;
- }
- if (params.action == 'create' && $('#selector a').length == 0) {
- buildForm (params);
- return;
+ $('#api-params').empty();
+ $('#api-params-add').show();
+ if (required.length) {
+ _.each(required, addField);
+ } else {
+ addField();
}
}
- function runQuery() {
- var vars = [],
- hash,
- smarty = '',
- php = "$params = array( 'version' => 3,",
- json = "{",
- link = "",
- key,
- value,
- entity,
- action,
- query = $('#query').val(),
- hashes = query.slice(query.indexOf('?') + 1).split('&');
- for(var i = 0; i < hashes.length; i++) {
- hash = hashes[i].split('=');
- key = hash[0];
- value = hash[1];
+ function toggleOptions() {
+ var name = $(this).val(),
+ $valField = $(this).closest('tr').find('.api-param-value');
+ if (options[name]) {
+ $valField.val('').select2({
+ multiple: true,
+ data: _.transform(options[name], function(result, option) {
+ result.push({id: option.key, text: option.value});
+ })
+ });
+ }
+ else if ($valField.data('select2')) {
+ $valField.select2('destroy');
+ }
+ if (name === '-') {
+ $(this).select2('destroy');
+ $(this).val('').focus();
+ }
+ }
- switch (key) {
- case 'version':
- case 'debug':
- case 'json':
- break;
- case 'action':
- action = value.toLowerCase();
- $('#action').val(action);
- break;
- case 'entity':
- entity = value.charAt(0).toUpperCase() + value.substr(1);
- $('#entity').val(entity);
- break;
- default:
- if (typeof value == 'undefined') {
- break;
- }
- value = isNaN(value) ? "'" + value + "'" : value;
- smarty += ' ' + key + '=' + value;
- php += "  '" + key +"' => " + value + ",";
- json += "'" + key + "': " + value + ", ";
+ /**
+ * Attempt to parse a string into a value of the intended type
+ * @param val
+ */
+ function evaluate(val, makeArray) {
+ try {
+ if (!val.length) {
+ return val;
+ }
+ var first = val.charAt(0),
+ last = val.slice(-1);
+ // Simple types
+ if (val === 'true' || val === 'false' || val === 'null' || !isNaN(val)) {
+ return eval(val);
+ }
+ // Quoted strings
+ if ((first === '"' || first === "'") && last === first) {
+ return val.slice(1, -1);
}
+ // Parse json
+ if ((first === '[' && last === ']') || (first === '{' && last === '}')) {
+ return eval('(' + val + ')');
+ }
+ // Transform csv to array
+ if (makeArray && val.indexOf(',') > 0) {
+ return val.split(',');
+ }
+ // Ok ok it's really a string
+ return val;
+ } catch(e) {
+ // If eval crashed return undefined
+ return undefined;
}
+ }
- if (!entity) {
- $('#query').val(ts('Choose an entity.'));
- $('#entity').val('');
- window.location.hash = 'explorer';
- return;
+ /**
+ * Format value to look like php code
+ * @param val
+ */
+ function phpFormat(val) {
+ var ret = '';
+ if ($.isPlainObject(val)) {
+ $.each(val, function(k, v) {
+ ret += (ret ? ',' : '') + "'" + k + "' => " + phpFormat(v);
+ });
+ return 'array(' + ret + ')';
}
- if (!action) {
- $('#query').val(ts('Choose an action.'));
- $('#action').val('');
- window.location.hash = 'explorer';
- return;
+ if ($.isArray(val)) {
+ $.each(val, function(k, v) {
+ ret += (ret ? ', ' : '') + phpFormat(v);
+ });
+ return 'array(' + ret + ')';
+ }
+ return JSON.stringify(val);
+ }
+
+ /**
+ * Smarty doesn't support array literals so we provide a stub
+ * @param js string
+ */
+ function smartyFormat(js, key) {
+ if (js.indexOf('[') > -1 || js.indexOf('{') > -1) {
+ return '$' + key.replace(/[. -]/g, '_');
+ }
+ return js;
+ }
+
+ function buildParams(e) {
+ params = {};
+ $('.api-param-checkbox:checked').each(function() {
+ params[this.name] = 1;
+ });
+ $('input.api-param-value').each(function() {
+ var $row = $(this).closest('tr'),
+ val = evaluate($(this).val(), $(this).is('.select2-offscreen')),
+ name = $('input.api-param-name', $row).val(),
+ op = $('select.api-param-op', $row).val();
+ if (name && val !== undefined) {
+ params[name] = op === '=' ? val : {};
+ if (op !== '=') {
+ params[name][op] = val;
+ }
+ clearError(this);
+ }
+ else if (name && (!e || e.type !== 'keyup')) {
+ setError(this);
+ }
+ });
+ if (entity && action) {
+ formatQuery();
}
+ }
- window.location.hash = query;
- $('#result').block();
- $.post(query,function(data) {
- $('#result').unblock().text(data);
- },'text');
- link="ajax query ";
- var RESTquery = CRM.config.resourceBase + "extern/rest.php?"+ query.substring(restURL.length,query.length) + "&api_key={yoursitekey}&key={yourkey}";
- $("#link").html(link+"|REST query.");
+ function setError(el) {
+ if (!$(el).hasClass('crm-error')) {
+ $(el)
+ .addClass('crm-error')
+ .attr('title', ts('Syntax error'))
+ .before('');
+ }
+ }
+ function clearError(el) {
+ $(el)
+ .removeClass('crm-error')
+ .attr('title', '')
+ .siblings('.ui-icon-alert').remove();
+ }
- json = (json.length > 1 ? json.slice (0,-2) : '{') + '}';
- php += " ); ";
- $('#php').html(php + "$result = civicrm_api('" + entity + "', '" + action + "', $params);");
- $('#jQuery').html ("CRM.api('"+entity+"', '"+action+"', "+json+", {success: function(data) { cj.each(data, function(key, value) {// do something }); } } );");
+ function formatQuery() {
+ var i = 0, q = {
+ smarty: "{crmAPI var='result' entity='" + entity + "' action='" + action + "'",
+ php: "$result = civicrm_api3('" + entity + "', '" + action + "'",
+ json: "CRM.api3('" + entity + "', '" + action + "'",
+ rest: CRM.config.resourceBase + "extern/rest.php?entity=" + entity + "&action=" + action + "&json=" + JSON.stringify(params) + "&api_key=yoursitekey&key=yourkey"
+ };
+ $.each(params, function(key, value) {
+ var js = JSON.stringify(value);
+ if (!i++) {
+ q.php += ", array(\n";
+ q.json += ", {\n";
+ } else {
+ q.json += ",\n";
+ }
+ q.php += " '" + key + "' => " + phpFormat(value) + ",\n";
+ q.json += " \"" + key + '": ' + js;
+ // FIXME: How to deal with complex values in smarty?
+ q.smarty += ' ' + key + '=' + smartyFormat(js, key);
+ });
+ if (i) {
+ q.php += ")";
+ q.json += "\n}";
+ }
+ q.php += ");";
+ q.json += ").done(function(result) {\n // do something\n});";
+ q.smarty += "}\n{foreach from=$result.values item=" + entity.toLowerCase() + "}\n {$" + entity.toLowerCase() + ".some_field}\n{/foreach}";
+ if (action.indexOf('get') < 0) {
+ q.smarty = '{* Smarty API only works with get actions *}';
+ }
+ $.each(q, function(type, val) {
+ $('#api-' + type).text(val);
+ });
+ }
- if (action.substring(0, 3) == "get") {//using smarty only make sense for get actions
- $('#smarty').html("{crmAPI var='result' entity='" + entity + "' action='" + action + "' " + smarty + '} {foreach from=$result.values item=' + entity + '} <li>{$' + entity +'.some_field}</li> {/foreach}');
+ function submit(e) {
+ e.preventDefault();
+ if (!entity || !action) {
+ alert(ts('Select an entity & action.'));
+ return;
+ }
+ if (action.indexOf('get') < 0) {
+ var msg = action === 'delete' ? ts('This will delete data from CiviCRM. Are you sure?') : ts('This will write to the database. Continue?');
+ CRM.confirm({title: ts('Confirm %1', {1: action}), message: msg}).on('crmConfirm:yes', execute);
} else {
- $('#smarty').html("smarty uses only 'get' actions");
+ execute();
}
- $('#generated').show();
}
- var query = window.location.hash;
- if (query.substring(1, restURL.length + 1) === restURL) {
- $('#query').val (query.substring(1)).focus();
- runQuery();
- } else {
- window.location.hash="explorer"; //to be sure to display the result under the generated code in the viewport
+ function execute() {
+ $('#api-result').html('');
+ $.ajax({
+ url: CRM.url('civicrm/ajax/rest'),
+ data: {
+ entity: entity,
+ action: action,
+ prettyprint: 1,
+ json: JSON.stringify(params)
+ },
+ type: action.indexOf('get') < 0 ? 'POST' : 'GET',
+ dataType: 'text'
+ }).done(function(text) {
+ $('#api-result').text(text);
+ });
}
- $('#entity, #action').change (function() {
- $("#selector, #extra").empty();
- generateQuery();
- runQuery();
- });
- $('#api-explorer input:checkbox').change(function() {
- generateQuery(); runQuery();
- });
- $('#api-explorer').submit(function(e) {
- e.preventDefault();
- runQuery();
- });
- $('#extra').on('keyup', 'input', generateQuery);
- $('#extra').on('click', 'a.remove-extra', function() {
- $(this).parent().remove();
- generateQuery();
- });
- $('#selector').on('click', 'a', function() {
- toggleField($(this).data('id'), this.innerHTML, this.class);
+
+ $(document).ready(function() {
+ $('form#api-explorer')
+ .on('change', '#api-entity, #api-action', function() {
+ entity = $('#api-entity').val();
+ action = $('#api-action').val();
+ if (entity && action) {
+ $('#api-params').html('
');
+ $('#api-params-table thead').show();
+ getFields();
+ buildParams();
+ } else {
+ $('#api-params, #api-generated pre').empty();
+ $('#api-params-add, #api-params-table thead').hide();
+ }
+ })
+ .on('change keyup', 'input.api-param-checkbox, input.api-param-value, input.api-param-name, select.api-param-op', buildParams)
+ .on('submit', submit);
+ $('#api-params')
+ .on('change', '.api-param-name', toggleOptions)
+ .on('click', '.api-param-remove', function(e) {
+ e.preventDefault();
+ $(this).closest('tr').remove();
+ buildParams();
+ });
+ $('#api-params-add').on('click', function(e) {
+ addField();
+ e.preventDefault();
+ });
+ $('#api-entity').change();
});
-});
+}(CRM.$, CRM._));
diff --git a/templates/CRM/Admin/Page/APIExplorer.tpl b/templates/CRM/Admin/Page/APIExplorer.tpl
index 846a2a8aa3..2e50b551cb 100644
--- a/templates/CRM/Admin/Page/APIExplorer.tpl
+++ b/templates/CRM/Admin/Page/APIExplorer.tpl
@@ -1,29 +1,81 @@
-
+{*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.5 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+*}