From bc4aa590eb545cb597fe25dadfaac8844ac0ae1a Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Thu, 5 Feb 2015 10:49:55 -0500 Subject: [PATCH] Expose code & docblocks to the api explorer --- CRM/Admin/Page/APIExplorer.php | 112 ++++++++++++++++++++++- CRM/Core/xml/Menu/Misc.xml | 5 + templates/CRM/Admin/Page/APIExplorer.js | 43 ++++++++- templates/CRM/Admin/Page/APIExplorer.tpl | 49 +++++++++- 4 files changed, 205 insertions(+), 4 deletions(-) diff --git a/CRM/Admin/Page/APIExplorer.php b/CRM/Admin/Page/APIExplorer.php index 79a7bacebe..b63f7dd2dc 100644 --- a/CRM/Admin/Page/APIExplorer.php +++ b/CRM/Admin/Page/APIExplorer.php @@ -42,7 +42,6 @@ class CRM_Admin_Page_APIExplorer extends CRM_Core_Page { * @return string */ public function run() { - CRM_Utils_System::setTitle('CiviCRM API'); CRM_Core_Resources::singleton() ->addScriptFile('civicrm', 'templates/CRM/Admin/Page/APIExplorer.js') ->addScriptUrl('//cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.js', 99) @@ -98,6 +97,117 @@ class CRM_Admin_Page_APIExplorer extends CRM_Core_Page { } CRM_Utils_System::civiExit(); } + CRM_Utils_System::permissionDenied(); + } + + /** + * Ajax callback to display code docs + */ + public static function getDoc() { + if (!empty($_GET['entity']) && strpos($_GET['entity'], '.') === FALSE) { + $entity = _civicrm_api_get_camel_name($_GET['entity']); + $action = CRM_Utils_Array::value('action', $_GET); + $doc = self::getDocblock($entity, $action); + $result = array( + 'doc' => $doc ? self::formatDocBlock($doc[0]) : 'Not found.', + 'code' => $doc ? $doc[1] : NULL, + ); + if (!$action) { + $actions = civicrm_api3($entity, 'getactions'); + $result['actions'] = CRM_Utils_Array::makeNonAssociative(array_combine($actions['values'], $actions['values'])); + } + CRM_Utils_JSON::output($result); + } + CRM_Utils_System::permissionDenied(); + } + + /** + * @param string $entity + * @param string|null $action + * @return array|bool + * [docblock, code] + */ + private static function getDocBlock($entity, $action) { + if (!$entity) { + return FALSE; + } + $contents = file_get_contents("api/v3/$entity.php", FILE_USE_INCLUDE_PATH); + if (!$contents) { + // Api does not exist + return FALSE; + } + $docblock = $code = array(); + // Fetch docblock for the api file + if (!$action) { + if (preg_match('#/\*\*\n.*?\n \*/\n#s', $contents, $docblock)) { + return array($docblock[0], NULL); + } + } + // Fetch block for a specific action + else { + $action = strtolower($action); + $fnName = 'civicrm_api3_' . _civicrm_api_get_entity_name_from_camel($entity) . '_' . $action; + // Support the alternate "1 file per action" structure + $actionFile = file_get_contents("api/v3/$entity/" . ucfirst($action) . '.php', FILE_USE_INCLUDE_PATH); + if ($actionFile) { + $contents = $actionFile; + } + // If action isn't in this file, try generic + if (strpos($contents, $fnName) === FALSE) { + $fnName = "civicrm_api3_generic_$action"; + $contents = file_get_contents("api/v3/Generic/" . ucfirst($action) . '.php', FILE_USE_INCLUDE_PATH); + if (!$contents) { + $contents = file_get_contents("api/v3/Generic.php", FILE_USE_INCLUDE_PATH); + } + } + if (preg_match('#(/\*\*(\n \*.*)*\n \*/\n)function[ ]+' . $fnName . '#i', $contents, $docblock)) { + // Fetch the code in a separate regex to preserve sanity + preg_match("#^function[ ]+$fnName.*?^}#ism", $contents, $code); + return array($docblock[1], $code[0]); + } + } + } + + /** + * Format a docblock to be a bit more readable + * Not a proper doc parser... patches welcome :) + * + * @param string $text + * @return string + */ + private static function formatDocBlock($text) { + // Get rid of comment stars + $text = str_replace(array("\n * ", "\n *\n", "\n */\n", "/**\n"), array("\n", "\n\n", '', ''), $text); + + // Format for html + $text = htmlspecialchars($text); + + // Extract code blocks - save for later to skip html conversion + $code = array(); + preg_match_all('#@code(.*?)@endcode#is', $text, $code); + $text = preg_replace('#@code.*?@endcode#is', '
', $text);
+
+    // Convert @annotations to titles
+    $text = preg_replace_callback(
+      '#^[ ]*@(\w+)([ ]*)#m',
+      function($matches) {
+        return "" . ucfirst($matches[1]) . "" . (empty($matches[2]) ? '' : ': ');
+      },
+      $text);
+
+    // Preserve indentation
+    $text = str_replace("\n ", "\n    ", $text);
+
+    // Convert newlines
+    $text = nl2br($text);
+
+    // Add unformatted code blocks back in
+    if ($code && !empty($code[1])) {
+      foreach ($code[1] as $block) {
+        $text = preg_replace('#
#', "
$block
", $text, 1); + } + } + return $text; } } diff --git a/CRM/Core/xml/Menu/Misc.xml b/CRM/Core/xml/Menu/Misc.xml index 2fab624cfa..8de633f080 100644 --- a/CRM/Core/xml/Menu/Misc.xml +++ b/CRM/Core/xml/Menu/Misc.xml @@ -96,6 +96,11 @@ CRM_Admin_Page_APIExplorer::getExampleFile access CiviCRM + + civicrm/ajax/apidoc + CRM_Admin_Page_APIExplorer::getDoc + access CiviCRM + civicrm/ajax/rest CRM_Utils_REST::ajax diff --git a/templates/CRM/Admin/Page/APIExplorer.js b/templates/CRM/Admin/Page/APIExplorer.js index e8c8119cf9..b071b54a26 100644 --- a/templates/CRM/Admin/Page/APIExplorer.js +++ b/templates/CRM/Admin/Page/APIExplorer.js @@ -14,6 +14,7 @@ optionsTpl = _.template($('#api-options-tpl').html()), returnTpl = _.template($('#api-return-tpl').html()), chainTpl = _.template($('#api-chain-tpl').html()), + docCodeTpl = _.template($('#doc-code-tpl').html()), // These types of entityRef don't require any input to open OPEN_IMMEDIATELY = ['RelationshipType', 'Event', 'Group', 'Tag'], @@ -555,6 +556,41 @@ } } + /** + * Fetch entity docs & actions + */ + function getDocEntity() { + CRM.utils.setOptions($('#doc-action').prop('disabled', true).addClass('loading'), []); + $.getJSON(CRM.url('civicrm/ajax/apidoc', {entity: $(this).val()})) + .done(function(result) { + CRM.utils.setOptions($('#doc-action').prop('disabled', false).removeClass('loading'), result.actions); + $('#doc-result').html(result.doc); + prettyPrint(); + }); + } + + /** + * Fetch entity+action docs & code + */ + function getDocAction() { + var + entity = $('#doc-entity').val(), + action = $('#doc-action').val(); + if (entity && action) { + $('#doc-result').block(); + $.get(CRM.url('civicrm/ajax/apidoc', {entity: entity, action: action})) + .done(function(result) { + $('#doc-result').unblock().html(result.doc); + if (result.code) { + $('#doc-result').append(docCodeTpl(result)); + } + prettyPrint(); + }); + } else { + $('#doc-result').html($('#doc-result').attr('title')); + } + } + $(document).ready(function() { // Set up tabs - bind active tab to document hash because... it's cool? document.location.hash = document.location.hash || 'explorer'; @@ -567,9 +603,12 @@ document.location.hash = ui.newPanel.attr('id').replace('-tab', ''); } }); + $(window).on('hashchange', function() { + $('#mainTabContainer').tabs('option', 'active', $(document.location.hash + '-tab').index() - 1); + }); // Initialize widgets - $('#api-entity, #example-entity').crmSelect2({ + $('#api-entity, #example-entity, #doc-entity').crmSelect2({ // Add strikethough class to selection to indicate deprecated apis formatSelection: function(option) { return $(option.element).hasClass('strikethrough') ? '' + option.text + '' : option.text; @@ -612,6 +651,8 @@ .on('change', 'select.api-chain-entity', getChainedAction); $('#example-entity').on('change', getExamples); $('#example-action').on('change', getExample); + $('#doc-entity').on('change', getDocEntity); + $('#doc-action').on('change', getDocAction); $('#api-params-add').on('click', function(e) { e.preventDefault(); addField(); diff --git a/templates/CRM/Admin/Page/APIExplorer.tpl b/templates/CRM/Admin/Page/APIExplorer.tpl index e85dc00a4a..4d4e43a62f 100644 --- a/templates/CRM/Admin/Page/APIExplorer.tpl +++ b/templates/CRM/Admin/Page/APIExplorer.tpl @@ -40,6 +40,7 @@ margin-bottom: .6em; } pre#api-result, + div#doc-result, pre#example-result { padding:1em; max-height: 50em; @@ -91,13 +92,28 @@ pre ol.linenums li:hover { color: #9c9c9c; } + .api-doc-code { + margin-top: 1em; + border-top: 1px solid #d3d3d3; + } + .api-doc-code .collapsible-title { + font-weight: bold; + margin-top: .5em; + } {/literal}
@@ -189,6 +205,28 @@
+ +
+
+ + +    + + +
+ {ts}Results are displayed here.{/ts} +
+
+
{strip} @@ -257,4 +295,11 @@ + + {/strip} -- 2.25.1