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} +

+{/htxt} + +{htxt id="param-op-title"} + {ts}Operator{/ts} +{/htxt} +{htxt id="param-op"} +

+ {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} +

+{/htxt} + +{htxt id="param-value-title"} + {ts}Parameter Value{/ts} +{/htxt} +{htxt id="param-value"} +

+ {ts}The following formats are accepted:{/ts} +

+

+{/htxt} diff --git a/templates/CRM/Admin/Page/APIExplorer.js b/templates/CRM/Admin/Page/APIExplorer.js index cf04eb6f4b..5a066f5dbf 100644 --- a/templates/CRM/Admin/Page/APIExplorer.js +++ b/templates/CRM/Admin/Page/APIExplorer.js @@ -1,161 +1,294 @@ -CRM.$(function($) { - var restURL = CRM.url("civicrm/ajax/rest"); +(function($, _, undefined) { + var + entity, + action, + fields = [], + options = {}, + params = {}, + fieldTpl = _.template($('#api-param-tpl').html()); - function toggleField (name, label, type) { - var h = '
\ - : \ - X\ -
'; - 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 | + +--------------------------------------------------------------------+ +*}
- - + {crmAPI entity="Entity" action="get" var="entities" version=3} {foreach from=$entities.values item=entity} {/foreach} -  |  - - - + @@ -34,40 +86,59 @@ -  |  +    -