diff --git a/appinfo/application.php b/appinfo/application.php index ef3457e3..31685716 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -2,6 +2,7 @@ namespace OCA\OJSXC\AppInfo; +use OCA\OJSXC\Controller\ManagedServerController; use OCA\OJSXC\Controller\SettingsController; use OCA\OJSXC\Controller\ExternalApiController; use OCA\OJSXC\Middleware\ExternalApiMiddleware; @@ -19,6 +20,7 @@ use OCA\OJSXC\StanzaHandlers\Presence; use OCA\OJSXC\StanzaLogger; use OCA\OJSXC\RawRequest; +use OCA\OJSXC\DataRetriever; use OCP\AppFramework\App; use OCA\OJSXC\ILock; use OCA\OJSXC\DbLock; @@ -89,6 +91,19 @@ public function __construct(array $urlParams=array()){ ); }); + $container->registerService('ManagedServerController', function(IContainer $c) { + return new ManagedServerController( + $c->query('AppName'), + $c->query('Request'), + $c->query('URLGenerator'), + \OC::$server->getConfig(), + \OC::$server->getUserSession(), + $c->query('Logger'), + $c->query('DataRetriever'), + 'https://xmpp.jsxc.ch/registration' + ); + }); + /** * Middleware */ @@ -231,6 +246,13 @@ public function __construct(array $urlParams=array()){ return new RawRequest(); }); + /** + * Data retriever + */ + $container->registerService('DataRetriever', function($c) { + return new DataRetriever(); + }); + } /** diff --git a/appinfo/routes.php b/appinfo/routes.php index bbb441ac..dce78aa5 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -2,19 +2,26 @@ use \OCA\OJSXC\AppInfo\Application; +$this->create('ojsxc_ajax_registerManagedServer', 'ajax/registerManagedServer.php') + ->actionInclude('ojsxc/ajax/registerManagedServer.php'); + $application = new Application(); $application->registerRoutes($this, array( 'routes' => array( array('name' => 'http_bind#index', 'url' => '/http-bind', 'verb' => 'POST'), + array('name' => 'settings#index', 'url' => '/settings', 'verb' => 'POST'), array('name' => 'settings#setAdmin', 'url' => '/settings/admin', 'verb' => 'POST'), array('name' => 'settings#setUser', 'url' => '/settings/user', 'verb' => 'POST'), array('name' => 'settings#getIceServers', 'url' => '/settings/iceServers', 'verb' => 'GET'), array('name' => 'settings#getUsers', 'url' => '/settings/users', 'verb' => 'GET'), + array('name' => 'externalApi#index', 'url' => '/ajax/externalApi.php', 'verb' => 'POST'), array('name' => 'externalApi#check_password', 'url' => '/api/v2/checkPassword', 'verb' => 'POST'), array('name' => 'externalApi#is_user', 'url' => '/api/v2/isUser', 'verb' => 'POST'), array('name' => 'externalApi#shared_roster', 'url' => '/api/v2/sharedRoster', 'verb' => 'POST'), + + array('name' => 'managedServer#register', 'url' => '/managedServer/register', 'verb' => 'POST') ) )); ?> diff --git a/js/jsxc b/js/jsxc index da3b0926..fc8e0852 160000 --- a/js/jsxc +++ b/js/jsxc @@ -1 +1 @@ -Subproject commit da3b092628024142c1f735959261d4177eb2174b +Subproject commit fc8e08527858fbed502246a25eb1420ecb8ffce7 diff --git a/js/settings/admin.js b/js/settings/admin.js index 4187ff7c..cb3a3751 100644 --- a/js/settings/admin.js +++ b/js/settings/admin.js @@ -75,7 +75,7 @@ $(document).ready(function() { } } - if(xhr.status === 0) { + if (xhr.status === 0) { // cross-side fail('Cross domain request was not possible. Either your BOSH server does not send any ' + 'Access-Control-Allow-Origin header or the content-security-policy (CSP) blocks your request. ' + @@ -93,15 +93,15 @@ $(document).ready(function() { }); } - $('#ojsxc [name=serverType]').change(function(){ - $('#ojsxc .ojsxc-external, #ojsxc .ojsxc-internal').hide(); - $('#ojsxc .ojsxc-external, #ojsxc .ojsxc-internal').find('.required').removeAttr('required'); + $('#ojsxc [name=serverType]').change(function() { + $('#ojsxc .ojsxc-external, #ojsxc .ojsxc-internal, #ojsxc .ojsxc-managed').hide(); + $('#ojsxc .ojsxc-external, #ojsxc .ojsxc-internal, #ojsxc .ojsxc-managed').find('.required').removeAttr('required'); $('#ojsxc .ojsxc-' + $(this).val()).show(); $('#ojsxc .ojsxc-' + $(this).val()).find('.required').attr('required', 'true'); }); $('#ojsxc [name=serverType]:checked').change(); - $('#boshUrl, #xmppDomain').on('input', function(){ + $('#boshUrl, #xmppDomain').on('input', function() { var self = $(this); var timeout = self.data('timeout'); @@ -151,7 +151,7 @@ $(document).ready(function() { status.addClass('jsxc_fail').text('Error!'); } - setTimeout(function(){ + setTimeout(function() { status.hide('slow'); }, 3000); }); @@ -182,7 +182,7 @@ $(document).ready(function() { var options = jsxc.options.get('httpUpload') || {}; - var services = $('[name="externalServices[]"]').map(function(){ + var services = $('[name="externalServices[]"]').map(function() { var inputField = $(this); return inputField.val() || null; @@ -190,13 +190,13 @@ $(document).ready(function() { if (options.server && services.toArray().indexOf(options.server) < 0) { // insert service - var emptyInputFields = $('[name="externalServices[]"]').filter(function(){ + var emptyInputFields = $('[name="externalServices[]"]').filter(function() { return $(this).val() === ''; }); var targetInputField; - if(emptyInputFields.length === 0) { + if (emptyInputFields.length === 0) { $(this).parents('.form-group').find('.add-input').click(); targetInputField = $('[name="externalServices[]"]').last(); } else { @@ -207,12 +207,78 @@ $(document).ready(function() { } }); - var apiUrl = window.location.origin + OC.filePath('ojsxc', 'ajax', 'externalApi.php'); - $('#jsxc-api-url').val(apiUrl); - - $('#ojsxc input[readonly]').focus(function(){ - if(typeof this.select === 'function') { + $('#ojsxc input[readonly]').focus(function() { + if (typeof this.select === 'function') { this.select(); } }); + + $('#ojsxc-register').click(function() { + var el = $(this); + var msgEl = el.parents('.ojsxc-managed').find('.msg'); + var promotionCode = $('#ojsxc-managed-promotion-code').val(); + + if (promotionCode.length > 0 && !/^[0-9a-z]+$/i.test(promotionCode)) { + msgEl.addClass('jsxc_fail'); + msgEl.text('Your promotion code is invalid.'); + + $('#ojsxc-managed-promotion-code').one('input', function() { + msgEl.removeClass('jsxc_fail'); + msgEl.text(''); + }); + + return; + } + + el.prop('disabled', 'disabled'); + el.val(el.attr('data-toggle-value')); + el.addClass('jsxc-loading'); + + $.ajax({ + method: 'POST', + url: OC.generateUrl('apps/ojsxc/managedServer/register'), + data: { + promotionCode: promotionCode + } + }).always(function(responseJSON) { + el.removeClass('jsxc-loading'); + + if (responseJSON && responseJSON.result === 'success') { + $('.ojsxc-managed-registration').hide(); + + msgEl.addClass('jsxc_success'); + msgEl.text('Congratulations! You got your own XMPP server. Please log out and in again.'); + + var submitEl = $('#ojsxc input[type="submit"]'); + submitEl.prop('disabled', 'disabled'); + submitEl.val('Please reload this page to continue'); + + return; + } + + if (responseJSON.responseJSON) { + responseJSON = responseJSON.responseJSON; + } + + var errorMsg = (responseJSON && responseJSON.data) ? responseJSON.data.msg : 'unknown error'; + + msgEl.addClass('jsxc_fail'); + msgEl.append($('').text('Sorry we couldn\'t complete your registration.')); + msgEl.append($('
')); + msgEl.append($('').text(errorMsg)); + + el.val('Registration failed'); + }); + }); + + $('.ojsxc-refresh-registration').click(function(ev){ + ev.preventDefault(); + + var msgEl = $(this).parents('.msg'); + + msgEl.removeClass('jsxc_success'); + msgEl.empty(); + + $('.ojsxc-managed-registration').show(); + }); }); diff --git a/lib/Controller/ExternalApiController.php b/lib/Controller/ExternalApiController.php index 8b612140..f534fac4 100644 --- a/lib/Controller/ExternalApiController.php +++ b/lib/Controller/ExternalApiController.php @@ -38,24 +38,28 @@ public function __construct( $this->logger = $logger; } + /** + * @PublicPage + * @NoCSRFRequired + */ public function index($operation) { switch ($operation) { case 'auth': - return checkPassword( + return $this->checkPassword( $this->request->getParam('username'), $this->request->getParam('password'), $this->request->getParam('domain') ); break; case 'isuser': - return isUser( + return $this->isUser( $this->request->getParam('username'), $this->request->getParam('domain') ); break; case 'sharedroster': - return sharedRoster( + return $this->sharedRoster( $this->request->getParam('username'), $this->request->getParam('domain') ); @@ -65,6 +69,10 @@ public function index($operation) } } + /** + * @PublicPage + * @NoCSRFRequired + */ public function checkPassword($username = '', $password = '', $domain = '') { $currentUser = null; @@ -100,6 +108,10 @@ public function checkPassword($username = '', $password = '', $domain = '') ]; } + /** + * @PublicPage + * @NoCSRFRequired + */ public function isUser($username = '', $domain = '') { $this->logger->info('ExAPI: Check if "'.$username.'@'.$domain.'" exists'); @@ -123,6 +135,10 @@ public function isUser($username = '', $domain = '') ]; } + /** + * @PublicPage + * @NoCSRFRequired + */ public function sharedRoster($username = '', $domain = '') { if (!empty($username)) { diff --git a/lib/Controller/ManagedServerController.php b/lib/Controller/ManagedServerController.php new file mode 100644 index 00000000..cf4a7204 --- /dev/null +++ b/lib/Controller/ManagedServerController.php @@ -0,0 +1,124 @@ +urlGenerator = $urlGenerator; + $this->config = $config; + $this->userSession = $userSession; + $this->logger = $logger; + $this->dataRetriever = $dataRetriever; + $this->registrationUrl = $registrationUrl; + } + + public function register($promotionCode = null) + { + $promotionCode = (preg_match('/^[0-9a-z]+$/i', $promotionCode)) ? $promotionCode : null; + $registrationResult = false; + + try { + $registrationResult = $this->doRegistration($promotionCode); + } catch (\Exception $exception) { + $this->logger->warning('RMS: Abort with message: '.$exception->getMessage()); + + return new JSONResponse([ + 'result' => 'error', + 'data' => [ + 'msg' => $exception->getMessage() + ] + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + if ($registrationResult) { + return [ + 'result' => 'success', + 'data' => [] + ]; + } + } + + private function doRegistration($promotionCode) + { + $apiUrl = $this->urlGenerator->linkToRouteAbsolute('ojsxc.externalApi.index'); + $apiSecret = $this->config->getAppValue('ojsxc', 'apiSecret'); + $userId = $this->userSession->getUser()->getUID(); + + $data = [ + 'apiUrl' => $apiUrl, + 'apiSecret' => $apiSecret, + 'apiVersion' => 1, + 'userId' => $userId, + 'promotionCode' => $promotionCode + ]; + + $response = $this->dataRetriever->fetchUrl($this->registrationUrl, $data); + + if ($response['body'] === false) { + throw new Exception('Couldn\'t reach the registration server'); + } + + $responseJSON = json_decode($response['body']); + + if ($responseJSON === null) { + throw new Exception('Couldn\'t parse the response. Response code: '.$response['headers']['response_code']); + } + + if ($response['headers']['response_code'] !== 200) { + $this->logger->info('RMS: Response code: '.$response['headers']['response_code']); + + throw new Exception(htmlspecialchars($responseJSON->message)); + } + + if (!preg_match('#^https://#', $responseJSON->boshUrl) || + !preg_match('#/http-bind/?$#', $responseJSON->boshUrl) || + preg_match('/\?|#/', $responseJSON->boshUrl)) { + throw new Exception('Got a bad bosh URL'); + } + + if (!preg_match('/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i', $responseJSON->domain) || + !preg_match('/^.{1,253}$/', $responseJSON->domain) || + !preg_match('/^[^\.]{1,63}(\.[^\.]{1,63})*$/', $responseJSON->domain)) { + throw new Exception('Got a bad domain'); + } + + $this->config->setAppValue('ojsxc', 'serverType', 'managed'); + $this->config->setAppValue('ojsxc', 'boshUrl', $responseJSON->boshUrl); + $this->config->setAppValue('ojsxc', 'xmppDomain', $responseJSON->domain); + $this->config->setAppValue('ojsxc', 'timeLimitedToken', 'true'); + $this->config->setAppValue('ojsxc', 'managedServer', 'registered'); + $this->config->setAppValue('ojsxc', 'externalServices', implode('|', $responseJSON->externalServices)); + + return true; + } +} diff --git a/lib/DataRetriever.php b/lib/DataRetriever.php new file mode 100644 index 00000000..b9eca336 --- /dev/null +++ b/lib/DataRetriever.php @@ -0,0 +1,49 @@ + + [ + 'method' => 'POST', + 'ignore_errors' => '1', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => http_build_query($data) + ] + ]); + + $body = file_get_contents($url, false, $context); + $headers = []; + + if ($body !== false) { + $headers = $this->parseHeaders($http_response_header); + } + + return [ + 'body' => $body, + 'headers' => $headers + ]; + } + + private function parseHeaders($headers) + { + $head = []; + + foreach ($headers as $k => $v) { + $t = explode(':', $v, 2); + if (isset($t[1])) { + $head[ trim($t[0]) ] = trim($t[1]); + } else { + $head[] = $v; + if (preg_match('#HTTP/[0-9\.]+\s+([0-9]+)#', $v, $out)) { + $head['response_code'] = intval($out[1]); + } + } + } + + return $head; + } +} diff --git a/lib/IDataRetriever.php b/lib/IDataRetriever.php new file mode 100644 index 00000000..5b7175d7 --- /dev/null +++ b/lib/IDataRetriever.php @@ -0,0 +1,8 @@ +config->getAppValue('ojsxc', 'serverType'); + $apiUrl = \OC::$server->getURLGenerator()->linkToRouteAbsolute('ojsxc.externalApi.index'); + $parameters = [ 'serverType' => (!empty($serverType))? $serverType : 'internal', 'boshUrl' => $this->config->getAppValue('ojsxc', 'boshUrl'), @@ -43,7 +45,10 @@ public function getForm() 'chromeExtension' => $this->config->getAppValue('ojsxc', 'chromeExtension'), 'timeLimitedToken' => $this->config->getAppValue('ojsxc', 'timeLimitedToken'), 'externalServices' => $externalServices, - 'apiSecret' => $this->config->getAppValue('ojsxc', 'apiSecret') + 'apiUrl' => $apiUrl, + 'apiSecret' => $this->config->getAppValue('ojsxc', 'apiSecret'), + 'userId' => \OC::$server->getUserSession()->getUser()->getUID(), + 'managedServer' => $this->config->getAppValue('ojsxc', 'managedServer') ]; return new TemplateResponse('ojsxc', 'settings/admin', $parameters); diff --git a/scss/_oc.scss b/scss/_oc.scss index ea70317d..3c8672e0 100644 --- a/scss/_oc.scss +++ b/scss/_oc.scss @@ -1,3 +1,9 @@ +@keyframes jsxc-left-to-right { + 0% {background-position-x: 0%} + 50% {background-position-x: 100%} + 100% {background-position-x: 0%} +} + #jsxc_roster { top: 45px; z-index: 1500; @@ -98,16 +104,32 @@ &[type='checkbox'] { margin: 13px 0; width: auto; + min-height: auto; + height: auto; } &[type='radio'] { width: auto; + min-height: auto; + height: auto; + } + + &[readonly] { + background-color: #f1f1f1; } &:invalid { border: 1px solid red; position: relative; } + + &.jsxc-loading { + background-image: linear-gradient(to right, #fff, #fff); + background-position-x: 10%; + background-size: 30px 100%; + background-repeat: no-repeat; + animation: jsxc-left-to-right 5s linear infinite; + } } em, .boshUrl-msg { diff --git a/templates/settings/admin.php b/templates/settings/admin.php index 23d4491e..4b69eaa9 100644 --- a/templates/settings/admin.php +++ b/templates/settings/admin.php @@ -10,10 +10,11 @@

JavaScript Xmpp Client

+

Server type

Limited functionality only: No clients besides JSXC in ownCloud, no multi-user chat, no server-to-server federations.
@@ -24,6 +25,13 @@ Choose this option to use your own XMPP server.
+
+ + Get your own full featured XMPP server directly hosted by the core team of JSXC. For more information visit [todo]. +

Basic

@@ -71,6 +79,49 @@
+
+ +
+