Index: remoting/webapp/me2mom/chrome_ex_oauth.js |
diff --git a/remoting/webapp/me2mom/chrome_ex_oauth.js b/remoting/webapp/me2mom/chrome_ex_oauth.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7e137107b2fcf3a384fa3b9dbebd81361442ba89 |
--- /dev/null |
+++ b/remoting/webapp/me2mom/chrome_ex_oauth.js |
@@ -0,0 +1,593 @@ |
+/** |
+ * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this |
+ * source code is governed by a BSD-style license that can be found in the |
+ * LICENSE file. |
+ */ |
+ |
+/** |
+ * Constructor - no need to invoke directly, call initBackgroundPage instead. |
+ * @constructor |
+ * @param {String} url_request_token The OAuth request token URL. |
+ * @param {String} url_auth_token The OAuth authorize token URL. |
+ * @param {String} url_access_token The OAuth access token URL. |
+ * @param {String} consumer_key The OAuth consumer key. |
+ * @param {String} consumer_secret The OAuth consumer secret. |
+ * @param {String} oauth_scope The OAuth scope parameter. |
+ * @param {Object} opt_args Optional arguments. Recognized parameters: |
+ * "app_name" {String} Name of the current application |
+ * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name |
+ * this file was renamed to. |
+ */ |
+function ChromeExOAuth(url_request_token, url_auth_token, url_access_token, |
+ consumer_key, consumer_secret, oauth_scope, opt_args) { |
+ this.url_request_token = url_request_token; |
+ this.url_auth_token = url_auth_token; |
+ this.url_access_token = url_access_token; |
+ this.consumer_key = consumer_key; |
+ this.consumer_secret = consumer_secret; |
+ this.oauth_scope = oauth_scope; |
+ this.app_name = opt_args && opt_args['app_name'] || |
+ "ChromeExOAuth Library"; |
+ this.key_token = "oauth_token"; |
+ this.key_token_secret = "oauth_token_secret"; |
+ this.callback_page = opt_args && opt_args['callback_page'] || |
+ "chrome_ex_oauth.html"; |
+ this.auth_params = {}; |
+ if (opt_args && opt_args['auth_params']) { |
+ for (key in opt_args['auth_params']) { |
+ if (opt_args['auth_params'].hasOwnProperty(key)) { |
+ this.auth_params[key] = opt_args['auth_params'][key]; |
+ } |
+ } |
+ } |
+}; |
+ |
+/******************************************************************************* |
+ * PUBLIC API METHODS |
+ * Call these from your background page. |
+ ******************************************************************************/ |
+ |
+/** |
+ * Initializes the OAuth helper from the background page. You must call this |
+ * before attempting to make any OAuth calls. |
+ * @param {Object} oauth_config Configuration parameters in a JavaScript object. |
+ * The following parameters are recognized: |
+ * "request_url" {String} OAuth request token URL. |
+ * "authorize_url" {String} OAuth authorize token URL. |
+ * "access_url" {String} OAuth access token URL. |
+ * "consumer_key" {String} OAuth consumer key. |
+ * "consumer_secret" {String} OAuth consumer secret. |
+ * "scope" {String} OAuth access scope. |
+ * "app_name" {String} Application name. |
+ * "auth_params" {Object} Additional parameters to pass to the |
+ * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': |
+ * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth |
+ * @return {ChromeExOAuth} An initialized ChromeExOAuth object. |
+ */ |
+ChromeExOAuth.initBackgroundPage = function(oauth_config) { |
+ window.chromeExOAuthConfig = oauth_config; |
+ window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config); |
+ window.chromeExOAuthRedirectStarted = false; |
+ window.chromeExOAuthRequestingAccess = false; |
+ |
+ var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page); |
+ var tabs = {}; |
+ chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { |
+ if (changeInfo.url && |
+ changeInfo.url.substr(0, url_match.length) === url_match && |
+ changeInfo.url != tabs[tabId] && |
+ window.chromeExOAuthRequestingAccess == false) { |
+ chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) { |
+ tabs[tab.id] = tab.url; |
+ chrome.tabs.remove(tabId); |
+ }); |
+ } |
+ }); |
+ |
+ return window.chromeExOAuth; |
+}; |
+ |
+/** |
+ * Authorizes the current user with the configued API. You must call this |
+ * before calling sendSignedRequest. |
+ * @param {Function} callback A function to call once an access token has |
+ * been obtained. This callback will be passed the following arguments: |
+ * token {String} The OAuth access token. |
+ * secret {String} The OAuth access token secret. |
+ */ |
+ChromeExOAuth.prototype.authorize = function(callback) { |
+ if (this.hasToken()) { |
+ callback(this.getToken(), this.getTokenSecret()); |
+ } else { |
+ window.chromeExOAuthOnAuthorize = function(token, secret) { |
+ callback(token, secret); |
+ }; |
+ chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) }); |
+ } |
+}; |
+ |
+/** |
+ * Clears any OAuth tokens stored for this configuration. Effectively a |
+ * "logout" of the configured OAuth API. |
+ */ |
+ChromeExOAuth.prototype.clearTokens = function() { |
+ delete localStorage[this.key_token + encodeURI(this.oauth_scope)]; |
+ delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; |
+}; |
+ |
+/** |
+ * Returns whether a token is currently stored for this configuration. |
+ * Effectively a check to see whether the current user is "logged in" to |
+ * the configured OAuth API. |
+ * @return {Boolean} True if an access token exists. |
+ */ |
+ChromeExOAuth.prototype.hasToken = function() { |
+ return !!this.getToken(); |
+}; |
+ |
+/** |
+ * Makes an OAuth-signed HTTP request with the currently authorized tokens. |
+ * @param {String} url The URL to send the request to. Querystring parameters |
+ * should be omitted. |
+ * @param {Function} callback A function to be called once the request is |
+ * completed. This callback will be passed the following arguments: |
+ * responseText {String} The text response. |
+ * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to |
+ * send the request. Useful if you need to check response status |
+ * code, etc. |
+ * @param {Object} opt_params Additional parameters to configure the request. |
+ * The following parameters are accepted: |
+ * "method" {String} The HTTP method to use. Defaults to "GET". |
+ * "body" {String} A request body to send. Defaults to null. |
+ * "parameters" {Object} Query parameters to include in the request. |
+ * "headers" {Object} Additional headers to include in the request. |
+ */ |
+ChromeExOAuth.prototype.sendSignedRequest = function(url, callback, |
+ opt_params) { |
+ var method = opt_params && opt_params['method'] || 'GET'; |
+ var body = opt_params && opt_params['body'] || null; |
+ var params = opt_params && opt_params['parameters'] || {}; |
+ var headers = opt_params && opt_params['headers'] || {}; |
+ |
+ var signedUrl = this.signURL(url, method, params); |
+ |
+ ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) { |
+ if (xhr.readyState == 4) { |
+ callback(xhr.responseText, xhr); |
+ } |
+ }); |
+}; |
+ |
+/** |
+ * Adds the required OAuth parameters to the given url and returns the |
+ * result. Useful if you need a signed url but don't want to make an XHR |
+ * request. |
+ * @param {String} method The http method to use. |
+ * @param {String} url The base url of the resource you are querying. |
+ * @param {Object} opt_params Query parameters to include in the request. |
+ * @return {String} The base url plus any query params plus any OAuth params. |
+ */ |
+ChromeExOAuth.prototype.signURL = function(url, method, opt_params) { |
+ var token = this.getToken(); |
+ var secret = this.getTokenSecret(); |
+ if (!token || !secret) { |
+ throw new Error("No oauth token or token secret"); |
+ } |
+ |
+ var params = opt_params || {}; |
+ |
+ var result = OAuthSimple().sign({ |
+ action : method, |
+ path : url, |
+ parameters : params, |
+ signatures: { |
+ consumer_key : this.consumer_key, |
+ shared_secret : this.consumer_secret, |
+ oauth_secret : secret, |
+ oauth_token: token |
+ } |
+ }); |
+ |
+ return result.signed_url; |
+}; |
+ |
+/** |
+ * Generates the Authorization header based on the oauth parameters. |
+ * @param {String} url The base url of the resource you are querying. |
+ * @param {Object} opt_params Query parameters to include in the request. |
+ * @return {String} An Authorization header containing the oauth_* params. |
+ */ |
+ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method, |
+ opt_params) { |
+ var token = this.getToken(); |
+ var secret = this.getTokenSecret(); |
+ if (!token || !secret) { |
+ throw new Error("No oauth token or token secret"); |
+ } |
+ |
+ var params = opt_params || {}; |
+ |
+ return OAuthSimple().getHeaderString({ |
+ action: method, |
+ path : url, |
+ parameters : params, |
+ signatures: { |
+ consumer_key : this.consumer_key, |
+ shared_secret : this.consumer_secret, |
+ oauth_secret : secret, |
+ oauth_token: token |
+ } |
+ }); |
+}; |
+ |
+/******************************************************************************* |
+ * PRIVATE API METHODS |
+ * Used by the library. There should be no need to call these methods directly. |
+ ******************************************************************************/ |
+ |
+/** |
+ * Creates a new ChromeExOAuth object from the supplied configuration object. |
+ * @param {Object} oauth_config Configuration parameters in a JavaScript object. |
+ * The following parameters are recognized: |
+ * "request_url" {String} OAuth request token URL. |
+ * "authorize_url" {String} OAuth authorize token URL. |
+ * "access_url" {String} OAuth access token URL. |
+ * "consumer_key" {String} OAuth consumer key. |
+ * "consumer_secret" {String} OAuth consumer secret. |
+ * "scope" {String} OAuth access scope. |
+ * "app_name" {String} Application name. |
+ * "auth_params" {Object} Additional parameters to pass to the |
+ * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': |
+ * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth |
+ * @return {ChromeExOAuth} An initialized ChromeExOAuth object. |
+ */ |
+ChromeExOAuth.fromConfig = function(oauth_config) { |
+ return new ChromeExOAuth( |
+ oauth_config['request_url'], |
+ oauth_config['authorize_url'], |
+ oauth_config['access_url'], |
+ oauth_config['consumer_key'], |
+ oauth_config['consumer_secret'], |
+ oauth_config['scope'], |
+ { |
+ 'app_name' : oauth_config['app_name'], |
+ 'auth_params' : oauth_config['auth_params'] |
+ } |
+ ); |
+}; |
+ |
+/** |
+ * Initializes chrome_ex_oauth.html and redirects the page if needed to start |
+ * the OAuth flow. Once an access token is obtained, this function closes |
+ * chrome_ex_oauth.html. |
+ */ |
+ChromeExOAuth.initCallbackPage = function() { |
+ var background_page = chrome.extension.getBackgroundPage(); |
+ var oauth_config = background_page.chromeExOAuthConfig; |
+ var oauth = ChromeExOAuth.fromConfig(oauth_config); |
+ background_page.chromeExOAuthRedirectStarted = true; |
+ oauth.initOAuthFlow(function (token, secret) { |
+ background_page.chromeExOAuthOnAuthorize(token, secret); |
+ background_page.chromeExOAuthRedirectStarted = false; |
+ chrome.tabs.getSelected(null, function (tab) { |
+ chrome.tabs.remove(tab.id); |
+ }); |
+ }); |
+}; |
+ |
+/** |
+ * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls. |
+ * @param {String} method The HTTP method to use. |
+ * @param {String} url The URL to send the request to. |
+ * @param {Object} headers Optional request headers in key/value format. |
+ * @param {String} body Optional body content. |
+ * @param {Function} callback Function to call when the XMLHttpRequest's |
+ * ready state changes. See documentation for XMLHttpRequest's |
+ * onreadystatechange handler for more information. |
+ */ |
+ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) { |
+ var xhr = new XMLHttpRequest(); |
+ xhr.onreadystatechange = function(data) { |
+ callback(xhr, data); |
+ } |
+ xhr.open(method, url, true); |
+ if (headers) { |
+ for (var header in headers) { |
+ if (headers.hasOwnProperty(header)) { |
+ xhr.setRequestHeader(header, headers[header]); |
+ } |
+ } |
+ } |
+ xhr.send(body); |
+}; |
+ |
+/** |
+ * Decodes a URL-encoded string into key/value pairs. |
+ * @param {String} encoded An URL-encoded string. |
+ * @return {Object} An object representing the decoded key/value pairs found |
+ * in the encoded string. |
+ */ |
+ChromeExOAuth.formDecode = function(encoded) { |
+ var params = encoded.split("&"); |
+ var decoded = {}; |
+ for (var i = 0, param; param = params[i]; i++) { |
+ var keyval = param.split("="); |
+ if (keyval.length == 2) { |
+ var key = ChromeExOAuth.fromRfc3986(keyval[0]); |
+ var val = ChromeExOAuth.fromRfc3986(keyval[1]); |
+ decoded[key] = val; |
+ } |
+ } |
+ return decoded; |
+}; |
+ |
+/** |
+ * Returns the current window's querystring decoded into key/value pairs. |
+ * @return {Object} A object representing any key/value pairs found in the |
+ * current window's querystring. |
+ */ |
+ChromeExOAuth.getQueryStringParams = function() { |
+ var urlparts = window.location.href.split("?"); |
+ if (urlparts.length >= 2) { |
+ var querystring = urlparts.slice(1).join("?"); |
+ return ChromeExOAuth.formDecode(querystring); |
+ } |
+ return {}; |
+}; |
+ |
+/** |
+ * Binds a function call to a specific object. This function will also take |
+ * a variable number of additional arguments which will be prepended to the |
+ * arguments passed to the bound function when it is called. |
+ * @param {Function} func The function to bind. |
+ * @param {Object} obj The object to bind to the function's "this". |
+ * @return {Function} A closure that will call the bound function. |
+ */ |
+ChromeExOAuth.bind = function(func, obj) { |
+ var newargs = Array.prototype.slice.call(arguments).slice(2); |
+ return function() { |
+ var combinedargs = newargs.concat(Array.prototype.slice.call(arguments)); |
+ func.apply(obj, combinedargs); |
+ }; |
+}; |
+ |
+/** |
+ * Encodes a value according to the RFC3986 specification. |
+ * @param {String} val The string to encode. |
+ */ |
+ChromeExOAuth.toRfc3986 = function(val){ |
+ return encodeURIComponent(val) |
+ .replace(/\!/g, "%21") |
+ .replace(/\*/g, "%2A") |
+ .replace(/'/g, "%27") |
+ .replace(/\(/g, "%28") |
+ .replace(/\)/g, "%29"); |
+}; |
+ |
+/** |
+ * Decodes a string that has been encoded according to RFC3986. |
+ * @param {String} val The string to decode. |
+ */ |
+ChromeExOAuth.fromRfc3986 = function(val){ |
+ var tmp = val |
+ .replace(/%21/g, "!") |
+ .replace(/%2A/g, "*") |
+ .replace(/%27/g, "'") |
+ .replace(/%28/g, "(") |
+ .replace(/%29/g, ")"); |
+ return decodeURIComponent(tmp); |
+}; |
+ |
+/** |
+ * Adds a key/value parameter to the supplied URL. |
+ * @param {String} url An URL which may or may not contain querystring values. |
+ * @param {String} key A key |
+ * @param {String} value A value |
+ * @return {String} The URL with URL-encoded versions of the key and value |
+ * appended, prefixing them with "&" or "?" as needed. |
+ */ |
+ChromeExOAuth.addURLParam = function(url, key, value) { |
+ var sep = (url.indexOf('?') >= 0) ? "&" : "?"; |
+ return url + sep + |
+ ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value); |
+}; |
+ |
+/** |
+ * Stores an OAuth token for the configured scope. |
+ * @param {String} token The token to store. |
+ */ |
+ChromeExOAuth.prototype.setToken = function(token) { |
+ localStorage[this.key_token + encodeURI(this.oauth_scope)] = token; |
+}; |
+ |
+/** |
+ * Retrieves any stored token for the configured scope. |
+ * @return {String} The stored token. |
+ */ |
+ChromeExOAuth.prototype.getToken = function() { |
+ return localStorage[this.key_token + encodeURI(this.oauth_scope)]; |
+}; |
+ |
+/** |
+ * Stores an OAuth token secret for the configured scope. |
+ * @param {String} secret The secret to store. |
+ */ |
+ChromeExOAuth.prototype.setTokenSecret = function(secret) { |
+ localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret; |
+}; |
+ |
+/** |
+ * Retrieves any stored secret for the configured scope. |
+ * @return {String} The stored secret. |
+ */ |
+ChromeExOAuth.prototype.getTokenSecret = function() { |
+ return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; |
+}; |
+ |
+/** |
+ * Starts an OAuth authorization flow for the current page. If a token exists, |
+ * no redirect is needed and the supplied callback is called immediately. |
+ * If this method detects that a redirect has finished, it grabs the |
+ * appropriate OAuth parameters from the URL and attempts to retrieve an |
+ * access token. If no token exists and no redirect has happened, then |
+ * an access token is requested and the page is ultimately redirected. |
+ * @param {Function} callback The function to call once the flow has finished. |
+ * This callback will be passed the following arguments: |
+ * token {String} The OAuth access token. |
+ * secret {String} The OAuth access token secret. |
+ */ |
+ChromeExOAuth.prototype.initOAuthFlow = function(callback) { |
+ if (!this.hasToken()) { |
+ var params = ChromeExOAuth.getQueryStringParams(); |
+ if (params['chromeexoauthcallback'] == 'true') { |
+ var oauth_token = params['oauth_token']; |
+ var oauth_verifier = params['oauth_verifier'] |
+ this.getAccessToken(oauth_token, oauth_verifier, callback); |
+ } else { |
+ var request_params = { |
+ 'url_callback_param' : 'chromeexoauthcallback' |
+ } |
+ this.getRequestToken(function(url) { |
+ window.location.href = url; |
+ }, request_params); |
+ } |
+ } else { |
+ callback(this.getToken(), this.getTokenSecret()); |
+ } |
+}; |
+ |
+/** |
+ * Requests an OAuth request token. |
+ * @param {Function} callback Function to call once the authorize URL is |
+ * calculated. This callback will be passed the following arguments: |
+ * url {String} The URL the user must be redirected to in order to |
+ * approve the token. |
+ * @param {Object} opt_args Optional arguments. The following parameters |
+ * are accepted: |
+ * "url_callback" {String} The URL the OAuth provider will redirect to. |
+ * "url_callback_param" {String} A parameter to include in the callback |
+ * URL in order to indicate to this library that a redirect has |
+ * taken place. |
+ */ |
+ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) { |
+ if (typeof callback !== "function") { |
+ throw new Error("Specified callback must be a function."); |
+ } |
+ var url = opt_args && opt_args['url_callback'] || |
+ window && window.top && window.top.location && |
+ window.top.location.href; |
+ |
+ var url_param = opt_args && opt_args['url_callback_param'] || |
+ "chromeexoauthcallback"; |
+ var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true"); |
+ |
+ var result = OAuthSimple().sign({ |
+ path : this.url_request_token, |
+ parameters: { |
+ "xoauth_displayname" : this.app_name, |
+ "scope" : this.oauth_scope, |
+ "oauth_callback" : url_callback |
+ }, |
+ signatures: { |
+ consumer_key : this.consumer_key, |
+ shared_secret : this.consumer_secret |
+ } |
+ }); |
+ var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback); |
+ ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); |
+}; |
+ |
+/** |
+ * Called when a request token has been returned. Stores the request token |
+ * secret for later use and sends the authorization url to the supplied |
+ * callback (for redirecting the user). |
+ * @param {Function} callback Function to call once the authorize URL is |
+ * calculated. This callback will be passed the following arguments: |
+ * url {String} The URL the user must be redirected to in order to |
+ * approve the token. |
+ * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the |
+ * request token. |
+ */ |
+ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) { |
+ if (xhr.readyState == 4) { |
+ if (xhr.status == 200) { |
+ var params = ChromeExOAuth.formDecode(xhr.responseText); |
+ var token = params['oauth_token']; |
+ this.setTokenSecret(params['oauth_token_secret']); |
+ var url = ChromeExOAuth.addURLParam(this.url_auth_token, |
+ "oauth_token", token); |
+ for (var key in this.auth_params) { |
+ if (this.auth_params.hasOwnProperty(key)) { |
+ url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]); |
+ } |
+ } |
+ callback(url); |
+ } else { |
+ throw new Error("Fetching request token failed. Status " + xhr.status); |
+ } |
+ } |
+}; |
+ |
+/** |
+ * Requests an OAuth access token. |
+ * @param {String} oauth_token The OAuth request token. |
+ * @param {String} oauth_verifier The OAuth token verifier. |
+ * @param {Function} callback The function to call once the token is obtained. |
+ * This callback will be passed the following arguments: |
+ * token {String} The OAuth access token. |
+ * secret {String} The OAuth access token secret. |
+ */ |
+ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier, |
+ callback) { |
+ if (typeof callback !== "function") { |
+ throw new Error("Specified callback must be a function."); |
+ } |
+ var bg = chrome.extension.getBackgroundPage(); |
+ if (bg.chromeExOAuthRequestingAccess == false) { |
+ bg.chromeExOAuthRequestingAccess = true; |
+ |
+ var result = OAuthSimple().sign({ |
+ path : this.url_access_token, |
+ parameters: { |
+ "oauth_token" : oauth_token, |
+ "oauth_verifier" : oauth_verifier |
+ }, |
+ signatures: { |
+ consumer_key : this.consumer_key, |
+ shared_secret : this.consumer_secret, |
+ oauth_secret : this.getTokenSecret(this.oauth_scope) |
+ } |
+ }); |
+ |
+ var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback); |
+ ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); |
+ } |
+}; |
+ |
+/** |
+ * Called when an access token has been returned. Stores the access token and |
+ * access token secret for later use and sends them to the supplied callback. |
+ * @param {Function} callback The function to call once the token is obtained. |
+ * This callback will be passed the following arguments: |
+ * token {String} The OAuth access token. |
+ * secret {String} The OAuth access token secret. |
+ * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the |
+ * access token. |
+ */ |
+ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) { |
+ if (xhr.readyState == 4) { |
+ var bg = chrome.extension.getBackgroundPage(); |
+ if (xhr.status == 200) { |
+ var params = ChromeExOAuth.formDecode(xhr.responseText); |
+ var token = params["oauth_token"]; |
+ var secret = params["oauth_token_secret"]; |
+ this.setToken(token); |
+ this.setTokenSecret(secret); |
+ bg.chromeExOAuthRequestingAccess = false; |
+ callback(token, secret); |
+ } else { |
+ bg.chromeExOAuthRequestingAccess = false; |
+ throw new Error("Fetching access token failed with status " + xhr.status); |
+ } |
+ } |
+}; |