From: Christian Weiske Date: Thu, 14 Aug 2014 16:41:34 +0000 (+0200) Subject: wip X-Git-Tag: v0.1.0~21 X-Git-Url: https://git.cweiske.de/grauphel.git/commitdiff_plain/3780cf15a59c48b3d71e8ec27e3bdacd8a119460?ds=inline wip --- 3780cf15a59c48b3d71e8ec27e3bdacd8a119460 diff --git a/appinfo/app.php b/appinfo/app.php new file mode 100755 index 0000000..3e319df --- /dev/null +++ b/appinfo/app.php @@ -0,0 +1,13 @@ + 'grauphel', + 'order' => 2342, + 'href' => OCP\Util::linkTo( 'grauphel', 'index.php' ), + 'icon' => OCP\Util::imagePath( 'grauphel', 'notes.png' ), + 'name' => 'Tomboy notes' +)); diff --git a/appinfo/application.php b/appinfo/application.php new file mode 100644 index 0000000..ecee129 --- /dev/null +++ b/appinfo/application.php @@ -0,0 +1,45 @@ +getContainer(); + + /** + * Controllers + */ + $container->registerService( + 'ApiController', + function($c) { + return new \OCA\Grauphel\Controller\ApiController( + $c->query('AppName'), + $c->query('Request') + ); + } + ); + $container->registerService( + 'AccessController', + function($c) { + return new \OCA\Grauphel\Controller\AccessController( + $c->query('AppName'), + $c->query('Request') + ); + } + ); + $container->registerService( + 'OAuthController', + function($c) { + return new \OCA\Grauphel\Controller\OAuthController( + $c->query('AppName'), + $c->query('Request') + ); + } + ); + } +} +?> diff --git a/appinfo/database.xml b/appinfo/database.xml new file mode 100755 index 0000000..ae089ca --- /dev/null +++ b/appinfo/database.xml @@ -0,0 +1,173 @@ + + + *dbname* + true + false + utf8 + + *dbprefix*grauphel_oauth_tokens + + + token_id + integer + 0 + true + 1 + 11 + + + token_user + text + false + 64 + + + token_type + text + true + 16 + + + token_key + text + true + 128 + + + token_secret + text + true + 128 + + + token_verifier + text + true + 128 + + + token_callback + text + true + 2048 + + +
+ + + *dbprefix*grauphel_notes + + + note_id + integer + 0 + true + 1 + 11 + + + note_user + text + false + 64 + + + + note_guid + text + true + 128 + + + + note_create_date + text + true + 32 + + + note_last_change_date + text + true + 32 + + + note_last_metadata_change_date + text + true + 32 + + + + note_title + text + true + 1024 + + + note_content + clob + true + + + note_content_version + text + true + 16 + + + note_open_on_startup + integer + 0 + true + 1 + + + note_pinned + integer + 0 + true + 1 + + + note_tags + text + true + 1024 + + +
+ + + *dbprefix*grauphel_syncdata + + + syncdata_id + integer + 0 + true + 1 + 11 + + + syncdata_user + text + false + 64 + + + syncdata_current_sync_guid + text + true + 64 + + + syncdata_latest_sync_revision + integer + 0 + true + 11 + + +
+
diff --git a/appinfo/info.xml b/appinfo/info.xml new file mode 100755 index 0000000..ac0f9ec --- /dev/null +++ b/appinfo/info.xml @@ -0,0 +1,10 @@ + + + grauphel + Grauphel: Tomboy note server + Tomboy REST API server to sync notes between devices + 0.1 + AGPL3 or later + Christian Weiske + 7 + diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 0000000..da62dc9 --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,75 @@ +registerRoutes( + $this, + array( + 'routes' => array( + array( + 'url' => '/test', + 'name' => 'access#test', + ), + + array( + 'url' => '/authorize', + 'name' => 'access#authorize', + 'verb' => 'GET', + ),/* + array( + 'url' => '/authorize', + 'name' => 'access#authorize', + 'verb' => 'POST', + ),*/ + array( + 'url' => '/login', + 'name' => 'access#login', + 'verb' => 'GET', + ), + + array( + 'url' => '/oauth/access_token', + 'name' => 'oauth#accessToken', + 'verb' => 'POST', + ), + array( + 'url' => '/oauth/authorize', + 'name' => 'oauth#authorize', + 'verb' => 'GET', + ), + array( + 'url' => '/oauth/request_token', + 'name' => 'oauth#requestToken', + 'verb' => 'POST', + ), + + array( + 'url' => '/api/1.0', + 'name' => 'api#index', + 'verb' => 'GET', + ), + array( + 'url' => '/api/1.0/{user}/note/{guid}', + 'name' => 'api#note', + 'verb' => 'GET', + ), + array( + 'url' => '/api/1.0/{user}/notes', + 'name' => 'api#notes', + 'verb' => 'GET', + ), + array( + 'url' => '/api/1.0/{user}/notes', + 'name' => 'api#notes', + 'verb' => 'POST', + ), + array( + 'url' => '/api/1.0/{user}', + 'name' => 'api#user', + 'verb' => 'GET', + ), + ) + ) +); + +?> diff --git a/appinfo/version b/appinfo/version new file mode 100755 index 0000000..49d5957 --- /dev/null +++ b/appinfo/version @@ -0,0 +1 @@ +0.1 diff --git a/controller/accesscontroller.php b/controller/accesscontroller.php new file mode 100644 index 0000000..68789b7 --- /dev/null +++ b/controller/accesscontroller.php @@ -0,0 +1,145 @@ + + * @copyright 2014 Christian Weiske + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 + * @link http://cweiske.de/grauphel.htm + */ +namespace OCA\Grauphel\Controller; +use \OCP\AppFramework\Controller; + +/** + * Login and authorization handling + * + * @category Tools + * @package Grauphel + * @author Christian Weiske + * @copyright 2014 Christian Weiske + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 + * @version Release: @package_version@ + * @link http://cweiske.de/grauphel.htm + */ +class AccessController extends Controller +{ + public function login($returnUrl = null) + { + $returnUrl = $this->loadReturnUrl($returnUrl); + + if (isset($_POST['user']) && trim($_POST['user']) != '') { + $this->deps->frontend->setUser(trim($_POST['user'])); + header('Location: ' . $returnUrl); + exit(0); + } + + $hFormUrl = htmlspecialchars( + $this->deps->urlGen->addParams( + $this->deps->urlGen->accessLogin(), + array('returnurl' => $returnUrl) + ) + ); + //FIXME: do some real login + header('HTTP/1.0 200 OK'); + + echo << + + grauphel login + + +
+

+ Log into grauphel: +

+ + +
+ + + +HTM; + exit(0); + } + + public function authorize($returnUrl = null) + { + var_dump('asd');die(); + $returnUrl = $this->loadReturnUrl($returnUrl); + + if (isset($_POST['auth'])) { + if ($_POST['auth'] == 'ok') { + $this->deps->frontend->setAuth(true); + } else if ($_POST['auth'] == 'cancel') { + $this->deps->frontend->setAuth(false); + } + header('Location: ' . $returnUrl); + exit(0); + } + + header('HTTP/1.0 200 OK'); + $hFormUrl = htmlspecialchars( + $this->deps->urlGen->addParams( + $this->deps->urlGen->accessAuthorize(), + array('returnurl' => $returnUrl) + ) + ); + + echo << + + grauphel authorization + + +
+

+ Shall application FIXME get full access to the notes? +

+ + + + +HTM; + exit(0); + } + + protected function loadReturnUrl($returnUrl = null) + { + if ($returnUrl === null) { + if (isset($_GET['returnurl'])) { + $returnUrl = $_GET['returnurl']; + } else { + $returnUrl = $this->deps->urlGen->index(); + } + } + return $returnUrl; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function test() + { + var_dump('asd');die(); + $this->registerResponder('xml', function($value) { + return new XMLResponse($value); + }); + return array('foo' => 'bar'); + } +} +?> diff --git a/controller/apicontroller.php b/controller/apicontroller.php new file mode 100644 index 0000000..5e743e0 --- /dev/null +++ b/controller/apicontroller.php @@ -0,0 +1,213 @@ + + * @copyright 2014 Christian Weiske + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 + * @link http://cweiske.de/grauphel.htm + */ +namespace OCA\Grauphel\Controller; +use \OCP\AppFramework\Controller; + +/** + * Tomboy's REST API + * + * @category Tools + * @package Grauphel + * @author Christian Weiske + * @copyright 2014 Christian Weiske + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 + * @version Release: @package_version@ + * @link http://cweiske.de/grauphel.htm + */ +class ApiController extends Controller +{ + /** + * /api/1.0 + */ + public function index() + { + var_dump('asd');die(); + $authenticated = false; + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + $urlGen = $this->deps->urlGen; + + try { + $provider = new \OAuthProvider(); + $oauth->registerHandler($provider) + ->registerAccessTokenHandler($provider); + $provider->checkOAuthRequest($urlGen->fullPath()); + $authenticated = true; + $token = $this->deps->tokens->load('access', $provider->token); + $username = $token->user; + + } catch (OAuth_Exception $e) { + $this->deps->renderer->errorOut($e->getMessage()); + } catch (\OAuthException $e) { + if ($e->getCode() != OAUTH_PARAMETER_ABSENT) { + $oauth->error($e); + } + } + + $data = array( + 'oauth_request_token_url' => $urlGen->oauthRequestToken(), + 'oauth_authorize_url' => $urlGen->oauthAuthorize(), + 'oauth_access_token_url' => $urlGen->oauthAccessToken(), + 'api-version' => '1.0', + ); + + if ($authenticated) { + $data['user-ref'] = array( + 'api-ref' => $urlGen->user($username), + 'href' => $urlGen->userHtml($username), + ); + } + + $this->deps->renderer->sendJson($data); + } + + /** + * GET /api/1.0/$user/notes/$noteguid + */ + public function note() + { + $username = $this->deps->urlGen->loadUsername(); + $guid = $this->deps->urlGen->loadGuid(); + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + $oauth->verifyOAuthUser($username, $this->deps->urlGen->note($username, $guid)); + + $note = $this->deps->notes->load($username, $guid, false); + if ($note === null) { + header('HTTP/1.0 404 Not Found'); + header('Content-type: text/plain'); + echo "Note does not exist\n"; + exit(1); + } + + $data = array('note' => array($note)); + $this->deps->renderer->sendJson($data); + } + + /** + * GET|PUT /api/1.0/$user/notes + */ + public function notes() + { + $username = $this->deps->urlGen->loadUsername(); + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + $oauth->verifyOAuthUser($username, $this->deps->urlGen->notes($username)); + + $syncdata = $this->deps->notes->loadSyncData($username); + + $this->handleNoteSave($username, $syncdata); + + $since = null; + if (isset($_GET['since'])) { + $since = (int) $_GET['since']; + } + + if (isset($_GET['include_notes']) && $_GET['include_notes']) { + $notes = $this->deps->notes->loadNotesFull($username, $since); + } else { + $notes = $this->deps->notes->loadNotesOverview($username, $since); + } + + //work around bug https://bugzilla.gnome.org/show_bug.cgi?id=734313 + foreach ($notes as $note) { + if (isset($note->{'note-content-version'})) { + $note->{'note-content-version'} = 0.3; + } + } + + $data = array( + 'latest-sync-revision' => $syncdata->latestSyncRevision, + 'notes' => $notes, + ); + $this->deps->renderer->sendJson($data); + } + + protected function handleNoteSave($username, $syncdata) + { + if ($_SERVER['REQUEST_METHOD'] != 'PUT') { + return; + } + + $data = file_get_contents('php://input'); + $putObj = json_decode($data); + if ($putObj === NULL) { + errorOut('Invalid JSON data in PUT request'); + } + + //structural validation + if (!isset($putObj->{'latest-sync-revision'})) { + errorOut('Missing "latest-sync-revision"'); + } + if (!isset($putObj->{'note-changes'})) { + errorOut('Missing "note-changes"'); + } + foreach ($putObj->{'note-changes'} as $note) { + if (!isset($note->guid) || $note->guid == '') { + errorOut('Missing "guid" on note'); + } + } + + //content validation + if ($putObj->{'latest-sync-revision'} != $syncdata->latestSyncRevision +1 + && $syncdata->latestSyncRevision != -1 + ) { + errorOut('Wrong "latest-sync-revision". You are not up to date.'); + } + + //update + ++$syncdata->latestSyncRevision; + foreach ($putObj->{'note-changes'} as $noteUpdate) { + $note = $this->deps->notes->load($username, $noteUpdate->guid); + if (isset($noteUpdate->command) && $noteUpdate->command == 'delete') { + $this->deps->notes->delete($username, $noteUpdate->guid); + } else { + $this->deps->notes->update( + $note, $noteUpdate, $syncdata->latestSyncRevision + ); + $this->deps->notes->save($username, $note); + } + } + + $this->deps->notes->saveSyncData($username, $syncdata); + } + + /** + * GET /api/1.0/$user + */ + public function user() + { + $username = $this->deps->urlGen->loadUsername(); + + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + $oauth->verifyOAuthUser($username, $this->deps->urlGen->user($username)); + + $syncdata = $this->deps->notes->loadSyncData($username); + + $data = array( + 'user-name' => $username, + 'first-name' => null, + 'last-name' => null, + 'notes-ref' => array( + 'api-ref' => $this->deps->urlGen->notes($username), + 'href' => null, + ), + 'latest-sync-revision' => $syncdata->latestSyncRevision, + 'current-sync-guid' => $syncdata->currentSyncGuid, + ); + $this->deps->renderer->sendJson($data); + } +} +?> diff --git a/controller/oauthcontroller.php b/controller/oauthcontroller.php new file mode 100644 index 0000000..cb20832 --- /dev/null +++ b/controller/oauthcontroller.php @@ -0,0 +1,178 @@ + + * @copyright 2014 Christian Weiske + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 + * @link http://cweiske.de/grauphel.htm + */ +namespace OCA\Grauphel\Controller; +use \OCP\AppFramework\Controller; + +/** + * OAuth handling + * + * @category Tools + * @package Grauphel + * @author Christian Weiske + * @copyright 2014 Christian Weiske + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 + * @version Release: @package_version@ + * @link http://cweiske.de/grauphel.htm + */ +class OAuthController extends Controller +{ + /** + * Handle out an access token after verifying the verification token + * OAuth step 3 of 3 + */ + public function accessTokenAction() + { + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + + try { + $provider = new \OAuthProvider(); + $oauth->registerHandler($provider) + ->registerVerificationTokenHandler($provider); + $provider->checkOAuthRequest($this->deps->urlGen->oauthAccessToken()); + + $token = $this->deps->tokens->loadAndDelete('verify', $provider->token); + + $newToken = new OAuth_Token('access'); + $newToken->tokenKey = 'a' . bin2hex($provider->generateToken(8)); + $newToken->secret = 's' . bin2hex($provider->generateToken(8)); + $newToken->user = $token->user; + $this->deps->tokens->store($newToken); + + $this->deps->renderer->sendFormData( + array( + 'oauth_token' => $newToken->tokenKey, + 'oauth_token_secret' => $newToken->secret, + ) + ); + exit(0); + } catch (\OAuthException $e) { + $oauth->error($e); + } + } + + /** + * Log the user in and let him authorize that the app may access notes + * OAuth step 2 of 3 + */ + public function authorizeAction() + { + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + + if (!isset($_REQUEST['oauth_token'])) { + $this->deps->renderer->errorOut('oauth_token missing'); + } + if (!$oauth->validateToken($_REQUEST['oauth_token'])) { + $this->deps->renderer->errorOut('Invalid token string'); + } + + $reqToken = $_REQUEST['oauth_token']; + + try { + $token = $this->deps->tokens->load('temp', $reqToken); + } catch (OAuth_Exception $e) { + $this->deps->renderer->errorOut($e->getMessage()); + } + + $authState = $this->deps->frontend->doAuthorize( + $this->deps->urlGen->current() + ); + if ($authState === null) { + //this should not happen; doAuthorize() must block + exit(1); + } + + try { + $token = $this->deps->tokens->loadAndDelete('temp', $reqToken); + } catch (OAuth_Exception $e) { + $this->deps->renderer->errorOut($e->getMessage()); + } + + if ($authState === false) { + //user declined + + //http://wiki.oauth.net/w/page/12238543/ProblemReporting + $callback = $this->deps->urlGen->addParams( + $token->callback, + array( + 'oauth_token' => $token->tokenKey, + 'oauth_problem' => 'permission_denied', + ) + ); + header('Location: ' . $callback, true, 302); + exit(0); + } + + //the user is logged in and authorized + $provider = new \OAuthProvider(); + + $newToken = new OAuth_Token('verify'); + $newToken->tokenKey = $token->tokenKey; + $newToken->secret = $token->secret; + $newToken->verifier = 'v' . bin2hex($provider->generateToken(8)); + $newToken->user = $this->deps->frontend->getUser(); + + $this->deps->tokens->store($newToken); + + //redirect + //FIXME: if no callback is given, show the token to the user + $callback = $this->deps->urlGen->addParams( + $token->callback, + array( + 'oauth_token' => $newToken->tokenKey, + 'oauth_verifier' => $newToken->verifier + ) + ); + + header('Location: ' . $callback, true, 302); + exit(); + } + + /** + * Create and return a request token. + * OAuth step 1 of 3 + */ + public function requestTokenAction() + { + $oauth = new OAuth(); + $oauth->setDeps($this->deps); + + try { + $provider = new \OAuthProvider(); + $oauth->registerHandler($provider); + $provider->isRequestTokenEndpoint(true); + $provider->checkOAuthRequest($this->deps->urlGen->oauthRequestToken()); + + //store token + callback URI for later + $token = new OAuth_Token('temp'); + $token->tokenKey = 'r' . bin2hex($provider->generateToken(8)); + $token->secret = 's' . bin2hex($provider->generateToken(8)); + $token->callback = $provider->callback; + + $this->deps->tokens->store($token); + + $this->deps->renderer->sendFormData( + array( + 'oauth_token' => $token->tokenKey, + 'oauth_token_secret' => $token->secret, + 'oauth_callback_confirmed' => 'TRUE' + ) + ); + } catch (\OAuthException $e) { + $oauth->error($e); + } + } +} +?> diff --git a/img/notes.png b/img/notes.png new file mode 100644 index 0000000..a03a639 Binary files /dev/null and b/img/notes.png differ diff --git a/index.php b/index.php new file mode 100755 index 0000000..1129766 --- /dev/null +++ b/index.php @@ -0,0 +1,10 @@ +assign( 'somesetting', $somesetting ); +$tmpl->printPage(); diff --git a/src b/src new file mode 120000 index 0000000..acc4488 --- /dev/null +++ b/src @@ -0,0 +1 @@ +../../grauphel/src \ No newline at end of file diff --git a/templates/main.php b/templates/main.php new file mode 100755 index 0000000..527de40 --- /dev/null +++ b/templates/main.php @@ -0,0 +1,3 @@ +

tomboy notes

+ +t('Some Setting'));?>: "" diff --git a/templates/settings.php b/templates/settings.php new file mode 100755 index 0000000..476a202 --- /dev/null +++ b/templates/settings.php @@ -0,0 +1,8 @@ + +
+

t('App Template'));?>

+ +
+ +
+