From 2aaa95e6400a1211edf3b4d40a073f59c2c6a2bb Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 24 Jan 2011 09:00:29 -0800 Subject: [PATCH] Conduit server-side basics. --- src/__phutil_library_map__.php | 21 ++ ...AphrontDefaultApplicationConfiguration.php | 6 + .../api/PhabricatorConduitAPIController.php | 185 ++++++++++++++++++ .../conduit/controller/api/__init__.php | 21 ++ .../base/PhabricatorConduitController.php | 46 +++++ .../conduit/controller/base/__init__.php | 16 ++ .../PhabricatorConduitConsoleController.php | 181 +++++++++++++++++ .../conduit/controller/console/__init__.php | 22 +++ .../log/PhabricatorConduitLogController.php | 28 +++ .../conduit/controller/log/__init__.php | 12 ++ .../conduit/method/base/ConduitAPIMethod.php | 62 ++++++ .../conduit/method/base/__init__.php | 12 ++ .../upload/ConduitAPI_file_upload_Method.php | 54 +++++ .../conduit/method/file/upload/__init__.php | 13 ++ .../protocol/exception/ConduitException.php | 20 ++ .../conduit/protocol/exception/__init__.php | 10 + .../protocol/request/ConduitAPIRequest.php | 35 ++++ .../conduit/protocol/request/__init__.php | 10 + .../storage/base/PhabricatorConduitDAO.php | 25 +++ .../conduit/storage/base/__init__.php | 12 ++ .../PhabricatorConduitConnectionLog.php | 26 +++ .../storage/connectionlog/__init__.php | 12 ++ .../PhabricatorConduitMethodCallLog.php | 26 +++ .../storage/methodcalllog/__init__.php | 12 ++ src/view/layout/panel/AphrontPanelView.php | 1 + .../layout/sidenav/AphrontSideNavView.php | 45 +++++ src/view/layout/sidenav/__init__.php | 13 ++ webroot/rsrc/css/base.css | 61 ++++++ 28 files changed, 987 insertions(+) create mode 100644 src/applications/conduit/controller/api/PhabricatorConduitAPIController.php create mode 100644 src/applications/conduit/controller/api/__init__.php create mode 100644 src/applications/conduit/controller/base/PhabricatorConduitController.php create mode 100644 src/applications/conduit/controller/base/__init__.php create mode 100644 src/applications/conduit/controller/console/PhabricatorConduitConsoleController.php create mode 100644 src/applications/conduit/controller/console/__init__.php create mode 100644 src/applications/conduit/controller/log/PhabricatorConduitLogController.php create mode 100644 src/applications/conduit/controller/log/__init__.php create mode 100644 src/applications/conduit/method/base/ConduitAPIMethod.php create mode 100644 src/applications/conduit/method/base/__init__.php create mode 100644 src/applications/conduit/method/file/upload/ConduitAPI_file_upload_Method.php create mode 100644 src/applications/conduit/method/file/upload/__init__.php create mode 100644 src/applications/conduit/protocol/exception/ConduitException.php create mode 100644 src/applications/conduit/protocol/exception/__init__.php create mode 100644 src/applications/conduit/protocol/request/ConduitAPIRequest.php create mode 100644 src/applications/conduit/protocol/request/__init__.php create mode 100644 src/applications/conduit/storage/base/PhabricatorConduitDAO.php create mode 100644 src/applications/conduit/storage/base/__init__.php create mode 100644 src/applications/conduit/storage/connectionlog/PhabricatorConduitConnectionLog.php create mode 100644 src/applications/conduit/storage/connectionlog/__init__.php create mode 100644 src/applications/conduit/storage/methodcalllog/PhabricatorConduitMethodCallLog.php create mode 100644 src/applications/conduit/storage/methodcalllog/__init__.php create mode 100755 src/view/layout/sidenav/AphrontSideNavView.php create mode 100644 src/view/layout/sidenav/__init__.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2cc42fbfc3..044b3158f6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -40,16 +40,28 @@ phutil_register_library_map(array( 'AphrontRedirectResponse' => 'aphront/response/redirect', 'AphrontRequest' => 'aphront/request', 'AphrontResponse' => 'aphront/response/base', + 'AphrontSideNavView' => 'view/layout/sidenav', 'AphrontTableView' => 'view/control/table', 'AphrontURIMapper' => 'aphront/mapper', 'AphrontView' => 'view/base', 'AphrontWebpageResponse' => 'aphront/response/webpage', + 'ConduitAPIMethod' => 'applications/conduit/method/base', + 'ConduitAPIRequest' => 'applications/conduit/protocol/request', + 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload', + 'ConduitException' => 'applications/conduit/protocol/exception', 'DifferentialAction' => 'applications/differential/constants/action', 'DifferentialChangeType' => 'applications/differential/constants/changetype', 'DifferentialLintStatus' => 'applications/differential/constants/lintstatus', 'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus', 'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus', 'LiskDAO' => 'storage/lisk/dao', + 'PhabricatorConduitAPIController' => 'applications/conduit/controller/api', + 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog', + 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console', + 'PhabricatorConduitController' => 'applications/conduit/controller/base', + 'PhabricatorConduitDAO' => 'applications/conduit/storage/base', + 'PhabricatorConduitLogController' => 'applications/conduit/controller/log', + 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog', 'PhabricatorController' => 'applications/base/controller/base', 'PhabricatorDirectoryCategory' => 'applications/directory/storage/category', 'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete', @@ -126,8 +138,17 @@ phutil_register_library_map(array( 'AphrontQueryParameterException' => 'AphrontQueryException', 'AphrontQueryRecoverableException' => 'AphrontQueryException', 'AphrontRedirectResponse' => 'AphrontResponse', + 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontWebpageResponse' => 'AphrontResponse', + 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', + 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', + 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', + 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', + 'PhabricatorConduitController' => 'PhabricatorController', + 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', + 'PhabricatorConduitLogController' => 'PhabricatorConduitController', + 'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO', 'PhabricatorController' => 'AphrontController', 'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index da2ab26f63..bed40f15f5 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -69,6 +69,12 @@ class AphrontDefaultApplicationConfiguration 'edit/(?:(?\w+)/)?$' => 'PhabricatorPeopleEditController', ), '/p/(?\w+)/$' => 'PhabricatorPeopleProfileController', + '/conduit/' => array( + '$' => 'PhabricatorConduitConsoleController', + 'method/(?[^/]+)$' => 'PhabricatorConduitConsoleController', + 'log/$' => 'PhabricatorConduitLogController', + ), + '/api/(?[^/]+)$' => 'PhabricatorConduitAPIController', '.*' => 'AphrontDefaultApplicationController', ); } diff --git a/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php new file mode 100644 index 0000000000..c259cf4551 --- /dev/null +++ b/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php @@ -0,0 +1,185 @@ +method = $data['method']; + return $this; + } + + public function processRequest() { + $time_start = microtime(true); + $request = $this->getRequest(); + + $method = $this->method; + + $method_class = ConduitAPIMethod::getClassNameFromAPIMethodName($method); + $api_request = null; + + $log = new PhabricatorConduitMethodCallLog(); + $log->setMethod($method); + $metadata = array(); + + try { + + if (!class_exists($method_class)) { + throw new Exception( + "Unable to load the implementation class for method '{$method}'. ". + "You may have misspelled the method, need to define ". + "'{$method_class}', or need to run 'arc build'."); + } + + // Fake out checkModule, the class has already been autoloaded by the + // class_exists() call above. + $method_handler = newv($method_class, array()); + + if (isset($_REQUEST['params']) && is_array($_REQUEST['params'])) { + $params_post = $request->getArr('params'); + foreach ($params_post as $key => $value) { + $params_post[$key] = json_decode($value); + } + $params = $params_post; + } else { + $params_json = $request->getStr('params'); + if (!strlen($params_json)) { + $params = array(); + } else { + $params = json_decode($params_json); + if (!is_array($params)) { + throw new Exception( + "Invalid parameter information was passed to method ". + "'{$method}', could not decode JSON serialization."); + } + } + } + + $metadata = idx($params, '__conduit__', array()); + unset($params['__conduit__']); + + $api_request = new ConduitAPIRequest($params); + + try { + $result = $method_handler->executeMethod($api_request); + $error_code = null; + $error_info = null; + } catch (ConduitException $ex) { + $result = null; + $error_code = $ex->getMessage(); + $error_info = $method_handler->getErrorDescription($error_code); + } + } catch (Exception $ex) { + $result = null; + $error_code = 'ERR-CONDUIT-CORE'; + $error_info = $ex->getMessage(); + } + + $time_end = microtime(true); + + $connection_id = null; + if (idx($metadata, 'connectionID')) { + $connection_id = $metadata['connectionID']; + } else if (($method == 'conduit.connect') && $result) { + $connection_id = idx($result, 'connectionID'); + } + + $log->setConnectionID($connection_id); + $log->setError((string)$error_code); + $log->setDuration(1000000 * ($time_end - $time_start)); + $log->save(); + + $result = array( + 'result' => $result, + 'error_code' => $error_code, + 'error_info' => $error_info, + ); + + switch ($request->getStr('output')) { + case 'human': + return $this->buildHumanReadableResponse( + $method, + $api_request, + $result); + case 'json': + default: + return id(new AphrontFileResponse()) + ->setMimeType('application/json') + ->setContent(json_encode($result)); + } + } + + private function buildHumanReadableResponse( + $method, + ConduitAPIRequest $request = null, + $result = null) { + + $param_rows = array(); + $param_rows[] = array('Method', phutil_escape_html($method)); + if ($request) { + foreach ($request->getAllParameters() as $key => $value) { + $param_rows[] = array( + phutil_escape_html($key), + phutil_escape_html(json_encode($value)), + ); + } + } + + $param_table = new AphrontTableView($param_rows); + $param_table->setColumnClasses( + array( + 'header', + 'wide', + )); + + $result_rows = array(); + foreach ($result as $key => $value) { + $result_rows[] = array( + phutil_escape_html($key), + phutil_escape_html(json_encode($value)), + ); + } + + $result_table = new AphrontTableView($result_rows); + $result_table->setColumnClasses( + array( + 'header', + 'wide', + )); + + $param_panel = new AphrontPanelView(); + $param_panel->setHeader('Method Parameters'); + $param_panel->appendChild($param_table); + + $result_panel = new AphrontPanelView(); + $result_panel->setHeader('Method Result'); + $result_panel->appendChild($result_table); + + return $this->buildStandardPageResponse( + array( + $param_panel, + $result_panel, + ), + array( + 'title' => 'Method Call Result', + )); + } + +} diff --git a/src/applications/conduit/controller/api/__init__.php b/src/applications/conduit/controller/api/__init__.php new file mode 100644 index 0000000000..1e6db7a2be --- /dev/null +++ b/src/applications/conduit/controller/api/__init__.php @@ -0,0 +1,21 @@ +setApplicationName('Conduit'); + $page->setBaseURI('/conduit/'); + $page->setTitle(idx($data, 'title')); + $page->setTabs( + array( + 'console' => array( + 'href' => '/conduit/', + 'name' => 'Console', + ), + 'logs' => array( + 'href' => '/conduit/log/', + 'name' => 'Logs', + ), + ), + idx($data, 'tab')); + $page->setGlyph("\xE2\x87\xB5"); + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/conduit/controller/base/__init__.php b/src/applications/conduit/controller/base/__init__.php new file mode 100644 index 0000000000..6dd4aa3801 --- /dev/null +++ b/src/applications/conduit/controller/base/__init__.php @@ -0,0 +1,16 @@ +method = idx($data, 'method'); + } + + public function processRequest() { + $methods = $this->getAllMethods(); + if (empty($methods[$this->method])) { + $this->method = key($methods); + } + + $method_class = $methods[$this->method]; + PhutilSymbolLoader::loadClass($method_class); + $method_object = newv($method_class, array()); + + + $error_description = array(); + $error_types = $method_object->defineErrorTypes(); + if ($error_types) { + $error_description[] = '
    '; + foreach ($error_types as $error => $meaning) { + $error_description[] = + '
  • '. + ''.phutil_escape_html($error).': '. + phutil_escape_html($meaning). + '
  • '; + } + $error_description[] = '
'; + $error_description = implode("\n", $error_description); + } else { + $error_description = "This method does not raise any specific errors."; + } + + $form = new AphrontFormView(); + $form + ->setAction('/api/'.$this->method) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Description') + ->setValue( + phutil_escape_html($method_object->getMethodDescription()))) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Returns') + ->setValue( + phutil_escape_html($method_object->defineReturnType()))) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Errors') + ->setValue($error_description)) + ->appendChild( + '

Enter parameters using '. + 'JSON. For instance, to enter a list, type: '. + '["apple", "banana", "cherry"]'); + + $params = $method_object->defineParamTypes(); + foreach ($params as $param => $desc) { + $form->appendChild( + id(new AphrontFormTextControl()) + ->setLabel($param) + ->setName("params[{$param}]") + ->setCaption($desc)); + } + + $form + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Output Format') + ->setName('output') + ->setOptions( + array( + 'human' => 'Human Readable', + 'json' => 'JSON', + ))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Call Method')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Conduit API: '.phutil_escape_html($this->method)); + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); + + $view = new AphrontSideNavView(); + foreach ($this->buildNavItems() as $item) { + $view->addNavItem($item); + } + + $view->appendChild($panel); + + return $this->buildStandardPageResponse( + array($view), + array( + 'title' => 'Conduit Console', + 'tab' => 'console', + )); + } + + private function buildNavItems() { + $classes = $this->getAllMethodImplementationClasses(); + $method_names = array(); + foreach ($classes as $method_class) { + $method_name = ConduitAPIMethod::getAPIMethodNameFromClassName( + $method_class); + $method_names[] = array( + 'full_name' => $method_name, + 'group_name' => reset(explode('.', $method_name)), + ); + } + $method_names = igroup($method_names, 'group_name'); + ksort($method_names); + + $items = array(); + foreach ($method_names as $group => $methods) { + $items[] = phutil_render_tag( + 'a', + array( + ), + phutil_escape_html($group)); + foreach ($methods as $method) { + $method_name = $method['full_name']; + $selected = ($method_name == $this->method); + $items[] = phutil_render_tag( + 'a', + array( + 'class' => $selected ? 'aphront-side-nav-selected' : null, + 'href' => '/conduit/method/'.$method_name, + ), + ''. + phutil_escape_html($method_name). + ''); + } + $items[] = '


'; + } + // Pop off the last '
'. + array_pop($items); + + return $items; + } + + private function getAllMethods() { + $classes = $this->getAllMethodImplementationClasses(); + $methods = array(); + foreach ($classes as $class) { + $name = ConduitAPIMethod::getAPIMethodNameFromClassName($class); + $methods[$name] = $class; + } + return $methods; + } + + private function getAllMethodImplementationClasses() { + $classes = id(new PhutilSymbolLoader()) + ->setAncestorClass('ConduitAPIMethod') + ->setType('class') + ->selectSymbolsWithoutLoading(); + return array_values(ipull($classes, 'name')); + } + +} diff --git a/src/applications/conduit/controller/console/__init__.php b/src/applications/conduit/controller/console/__init__.php new file mode 100644 index 0000000000..43e8fc4f1f --- /dev/null +++ b/src/applications/conduit/controller/console/__init__.php @@ -0,0 +1,22 @@ +buildStandardPageResponse('stuff', array( + 'title' => 'Conduit Logs', + 'tab' => 'logs', + )); + } +} diff --git a/src/applications/conduit/controller/log/__init__.php b/src/applications/conduit/controller/log/__init__.php new file mode 100644 index 0000000000..cbe5f7c474 --- /dev/null +++ b/src/applications/conduit/controller/log/__init__.php @@ -0,0 +1,12 @@ +defineErrorTypes(), $error_code, 'Unknown Error'); + } + + public function executeMethod(ConduitAPIRequest $request) { + return $this->execute($request); + } + + public function getAPIMethodName() { + return self::getAPIMethodNameFromClassName(get_class($this)); + } + + public static function getClassNameFromAPIMethodName($method_name) { + $method_fragment = str_replace('.', '_', $method_name); + return 'ConduitAPI_'.$method_fragment.'_Method'; + } + + public static function getAPIMethodNameFromClassName($class_name) { + $match = null; + $is_valid = preg_match( + '/^ConduitAPI_(.*)_Method$/', + $class_name, + $match); + if (!$is_valid) { + throw new Exception( + "Parameter '{$class_name}' is not a valid Conduit API method class."); + } + $method_fragment = $match[1]; + return str_replace('_', '.', $method_fragment); + } + +} diff --git a/src/applications/conduit/method/base/__init__.php b/src/applications/conduit/method/base/__init__.php new file mode 100644 index 0000000000..f7268ea664 --- /dev/null +++ b/src/applications/conduit/method/base/__init__.php @@ -0,0 +1,12 @@ + 'required nonempty base64-bytes', + 'name' => 'optional string', + ); + } + + public function defineReturnType() { + return 'nonempty guid'; + } + + public function defineErrorTypes() { + return array( + ); + } + + protected function execute(ConduitAPIRequest $request) { + $data = $request->getValue('data_base64'); + $name = $request->getValue('name'); + $data = base64_decode($data, $strict = true); + + $file = PhabricatorFile::newFromFileData( + $data, + array( + 'name' => $name + )); + return $file->getPHID(); + } + +} diff --git a/src/applications/conduit/method/file/upload/__init__.php b/src/applications/conduit/method/file/upload/__init__.php new file mode 100644 index 0000000000..5b0c10c04c --- /dev/null +++ b/src/applications/conduit/method/file/upload/__init__.php @@ -0,0 +1,13 @@ +params = $params; + } + + public function getValue($key) { + return $this->params[$key]; + } + + public function getAllParameters() { + return $this->params; + } + +} diff --git a/src/applications/conduit/protocol/request/__init__.php b/src/applications/conduit/protocol/request/__init__.php new file mode 100644 index 0000000000..5aa52afcf9 --- /dev/null +++ b/src/applications/conduit/protocol/request/__init__.php @@ -0,0 +1,10 @@ +items[] = $item; + return $this; + } + + public function render() { + $view = new AphrontNullView(); + $view->appendChild($this->items); + + return + ''. + ''. + ''. + ''. + ''. + '
'. + $view->render(). + ''. + $this->renderChildren(). + '
'; + } + +} diff --git a/src/view/layout/sidenav/__init__.php b/src/view/layout/sidenav/__init__.php new file mode 100644 index 0000000000..b32c493b63 --- /dev/null +++ b/src/view/layout/sidenav/__init__.php @@ -0,0 +1,13 @@ +