'use strict'; //////////////////////////////////////////////////////////////////////////////// /// @brief Foxx Controller /// /// @file /// /// DISCLAIMER /// /// Copyright 2013 triagens GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is triAGENS GmbH, Cologne, Germany /// /// @author Lucas Dohmen /// @author Copyright 2013, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// const deprecated = require('@arangodb/deprecated'); const RequestContext = require('@arangodb/foxx/request_context'); const BaseMiddleware = require('@arangodb/foxx/base_middleware').BaseMiddleware; const _ = require('underscore'); const is = require('@arangodb/is'); const internal = require('@arangodb/foxx/internals'); const swagger = require('@arangodb/foxx/swagger'); // ----------------------------------------------------------------------------- // --SECTION-- Controller // ----------------------------------------------------------------------------- var authControllerProps = { //////////////////////////////////////////////////////////////////////////////// /// JSF_foxx_controller_getUsers /// @brief Get the users of this controller //////////////////////////////////////////////////////////////////////////////// getUsers() { const foxxAuthentication = require('@arangodb/foxx/authentication'); return new foxxAuthentication.Users(this.applicationContext); }, //////////////////////////////////////////////////////////////////////////////// /// JSF_foxx_controller_getAuth /// @brief Get the auth object of this controller //////////////////////////////////////////////////////////////////////////////// getAuth() { return this.auth; }, //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_login /// /// `FoxxController#login(path, opts)` /// /// Add a route for the login. You can provide further customizations via the /// the options: /// /// * *usernameField* and *passwordField* can be used to adjust the expected attributes /// in the *post* request. They default to *username* and *password*. /// * *onSuccess* is a function that you can define to do something if the login was /// successful. This includes sending a response to the user. This defaults to a /// function that returns a JSON with *user* set to the identifier of the user and /// * *key* set to the session key. /// * *onError* is a function that you can define to do something if the login did not /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "Username or Password was wrong". /// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// @EXAMPLES /// /// ```js /// app.login('/login', { /// onSuccess(req, res) { /// res.json({"success": true}); /// } /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// login(route, opts) { var authentication = require('@arangodb/foxx/authentication'); return this.post(route, authentication.createStandardLoginHandler(this.getAuth(), this.getUsers(), opts)); }, //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_logout /// /// `FoxxController#logout(path, opts)` /// /// This works pretty similar to the logout function and adds a path to your /// app for the logout functionality. You can customize it with a custom *onSuccess* /// and *onError* function: /// /// * *onSuccess* is a function that you can define to do something if the logout was /// successful. This includes sending a response to the user. This defaults to a /// function that returns a JSON with *message* set to "logged out". /// * *onError* is a function that you can define to do something if the logout did not /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "No session was found". /// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// /// @EXAMPLES /// /// ```js /// app.logout('/logout', { /// onSuccess(req, res) { /// res.json({"message": "Bye, Bye"}); /// } /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// logout(route, opts) { var authentication = require('@arangodb/foxx/authentication'); return this.post(route, authentication.createStandardLogoutHandler(this.getAuth(), opts)); }, //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_register /// /// `FoxxController#register(path, opts)` /// /// This works pretty similar to the logout function and adds a path to your /// app for the register functionality. You can customize it with a custom *onSuccess* /// and *onError* function: /// /// * *onSuccess* is a function that you can define to do something if the registration was /// successful. This includes sending a response to the user. This defaults to a /// function that returns a JSON with *user* set to the created user document. /// * *onError* is a function that you can define to do something if the registration did not /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "Registration failed". /// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// You can also set the fields containing the username and password via *usernameField* /// (defaults to *username*) and *passwordField* (defaults to *password*). /// If you want to accept additional attributes for the user document, use the option /// *acceptedAttributes* and set it to an array containing strings with the names of /// the additional attributes you want to accept. All other attributes in the request /// will be ignored. /// /// If you want default attributes for the accepted attributes or set additional fields /// (for example *admin*) use the option *defaultAttributes* which should be a hash /// mapping attribute names to default values. /// /// @EXAMPLES /// /// ```js /// app.register('/logout', { /// acceptedAttributes: ['name'], /// defaultAttributes: { /// admin: false /// } /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// register(route, opts) { var authentication = require('@arangodb/foxx/authentication'); return this.post( route, authentication.createStandardRegistrationHandler(this.getAuth(), this.getUsers(), opts) ).errorResponse(authentication.UserAlreadyExistsError, 400, 'User already exists'); }, //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_changePassword /// /// FoxxController#changePassword(route, opts)` /// /// Add a route for the logged in user to change the password. /// You can provide further customizations via the /// the options: /// /// * *passwordField* can be used to adjust the expected attribute /// in the *post* request. It defaults to *password*. /// * *onSuccess* is a function that you can define to do something if the change was /// successful. This includes sending a response to the user. This defaults to a /// function that returns a JSON with *notice* set to "Changed password!". /// * *onError* is a function that you can define to do something if the login did not /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "No session was found". /// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// @EXAMPLES /// /// ```js /// app.changePassword('/changePassword', { /// onSuccess(req, res) { /// res.json({"success": true}); /// } /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// changePassword(route, opts) { var authentication = require('@arangodb/foxx/authentication'); return this.post(route, authentication.createStandardChangePasswordHandler(this.getUsers(), opts)); } }; var sessionControllerProps = { //////////////////////////////////////////////////////////////////////////////// /// JSF_foxx_controller_getSessions /// @brief Get the sessions object of this controller //////////////////////////////////////////////////////////////////////////////// getSessions() { return this.sessions; }, //////////////////////////////////////////////////////////////////////////////// /// @brief defines a route to logout/destroy the session //////////////////////////////////////////////////////////////////////////////// destroySession(route, opts) { var method = opts.method; if (typeof method === 'string') { method = method.toLowerCase(); } if (!method || typeof this[method] !== 'function') { method = 'post'; } var sessions = require('@arangodb/foxx/sessions'); return this[method](route, sessions.createDestroySessionHandler(this.getSessions(), opts)); } }; class Controller { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_initializer /// /// `new Controller(applicationContext, options)` /// /// This creates a new Controller. The first argument is the controller /// context available in the variable *applicationContext*. The second one is an /// options array with the following attributes: /// /// * *urlPrefix*: All routes you define within will be prefixed with it. /// /// @EXAMPLES /// /// ```js /// app = new Controller(applicationContext, { /// urlPrefix: "/meadow" /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// constructor(context, options) { this.currentPriority = 0; var urlPrefix, baseMiddleware; context.clearComments(); if (is.notExisty(context)) { throw new Error('parameter is missing'); } context.clearComments(); this.routingInfo = { routes: [] }; // Models for the Documentation this.models = {}; options = options || {}; urlPrefix = options.urlPrefix || ''; if (urlPrefix === '') { urlPrefix = context.prefix; } else if (context.prefix !== '') { urlPrefix = context.prefix + '/' + urlPrefix; } this.injected = Object.create(null); this.injectors = Object.create(null); this.routingInfo.urlPrefix = urlPrefix; this.collectionPrefix = context.collectionPrefix; this.extensions = {}; baseMiddleware = new BaseMiddleware(); this.routingInfo.middleware = [ { url: { match: '/*' }, action: { callback: baseMiddleware.functionRepresentation, options: { name: context.name, version: context.version, mount: context.mount, isDevelopment: context.isDevelopment, isProduction: context.isProduction, prefix: context.prefix, options: context.options } } } ]; this.allRoutes = new RequestContext.Buffer(); context.foxxes.push(this); if (is.existy(context.manifest.rootElement)) { this.rootElement = context.manifest.rootElement; } else { this.rootElement = false; } this.applicationContext = context; } addInjector(name, factory) { deprecated('2.9', '"addInjector" is deprecated, use regular variables instead'); if (factory === undefined) { _.extend(this.injectors, name); } else { this.injectors[name] = factory; } } //////////////////////////////////////////////////////////////////////////////// /// /// The *handleRequest* method is the raw way to create a new route. You /// probably wont call it directly, but it is used in the other request methods: /// /// When defining a route you can also define a so called 'parameterized' route /// like */goose/:barn*. In this case you can later get the value the user /// provided for *barn* via the *params* function (see the Request object). //////////////////////////////////////////////////////////////////////////////// handleRequest(method, route, callback) { var constraints = {queryParams: {}, urlParams: {}}; var newRoute = internal.constructRoute(method, route, callback, this, constraints); var requestContext = new RequestContext( this.allRoutes, this.models, newRoute, route, this.rootElement, constraints, this.extensions ); var summary; var undocumentedBody; this.routingInfo.routes.push(newRoute); if (this.applicationContext.comments.length > 0) { do { summary = this.applicationContext.comments.shift(); } while (summary === ''); requestContext.summary(summary || ''); requestContext.notes(this.applicationContext.comments.join('\n')); } this.applicationContext.clearComments(); if (method === 'post' || method === 'put' || method === 'patch') { const Model = require('@arangodb/foxx').Model; undocumentedBody = class UndocumentedBody extends Model {}; requestContext.bodyParam('undocumented body', { description: 'Undocumented body param', type: undocumentedBody, allowInvalid: true }); } return requestContext; } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_head /// /// `Controller.head(path, callback)` /// /// Defines a new route on `path` that handles requests from the HTTP verb `head`. /// This route can also be 'parameterized' like `/goose/:barn`. /// In this case you can later get the value the user provided for `barn` /// via the `params` function in the `request`. /// The function defined in `callback` will be invoked whenever this type of /// request is recieved. /// `callback` get's two arguments `request` and `response`, see below for further /// information about these objects. /// /// @EXAMPLES /// /// ```js /// app.head('/goose/barn', function (req, res) { /// // Take this request and deal with it! /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// head(route, callback) { return this.handleRequest('head', route, callback); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_get /// /// `Controller.get(path, callback)` /// /// Defines a new route on `path` that handles requests from the HTTP verb `get`. /// This route can also be 'parameterized' like `/goose/:barn`. /// In this case you can later get the value the user provided for `barn` /// via the `params` function in the `request`. /// The function defined in `callback` will be invoked whenever this type of /// request is recieved. /// `callback` get's two arguments `request` and `response`, see below for further /// information about these objects. /// /// @EXAMPLES /// /// ```js /// app.get('/goose/barn', function (req, res) { /// // Take this request and deal with it! /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// get(route, callback) { return this.handleRequest('get', route, callback); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_post /// /// `Controller.post(path, callback)` /// /// Defines a new route on `path` that handles requests from the HTTP verb `post`. /// This route can also be 'parameterized' like `/goose/:barn`. /// In this case you can later get the value the user provided for `barn` /// via the `params` function in the `request`. /// The function defined in `callback` will be invoked whenever this type of /// request is recieved. /// `callback` get's two arguments `request` and `response`, see below for further /// information about these objects. /// /// @EXAMPLES /// /// ```js /// app.post('/goose/barn', function (req, res) { /// // Take this request and deal with it! /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// post(route, callback) { return this.handleRequest('post', route, callback); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_put /// /// `Controller.put(path, callback)` /// /// Defines a new route on `path` that handles requests from the HTTP verb `put`. /// This route can also be 'parameterized' like `/goose/:barn`. /// In this case you can later get the value the user provided for `barn` /// via the `params` function in the `request`. /// The function defined in `callback` will be invoked whenever this type of /// request is recieved. /// `callback` get's two arguments `request` and `response`, see below for further /// information about these objects. /// /// @EXAMPLES /// /// ```js /// app.put('/goose/barn', function (req, res) { /// // Take this request and deal with it! /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// put(route, callback) { return this.handleRequest('put', route, callback); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_patch /// /// `Controller.patch(path, callback)` /// /// Defines a new route on `path` that handles requests from the HTTP verb `patch`. /// This route can also be 'parameterized' like `/goose/:barn`. /// In this case you can later get the value the user provided for `barn` /// via the `params` function in the `request`. /// The function defined in `callback` will be invoked whenever this type of /// request is recieved. /// `callback` get's two arguments `request` and `response`, see below for further /// information about these objects. /// /// @EXAMPLES /// /// ```js /// app.patch('/goose/barn', function (req, res) { /// // Take this request and deal with it! /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// patch(route, callback) { return this.handleRequest('patch', route, callback); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_delete /// /// `Controller.delete(path, callback)` /// /// Defines a new route on `path` that handles requests from the HTTP verb `delete`. /// This route can also be 'parameterized' like `/goose/:barn`. /// In this case you can later get the value the user provided for `barn` /// via the `params` function in the `request`. /// The function defined in `callback` will be invoked whenever this type of /// request is recieved. /// `callback` get's two arguments `request` and `response`, see below for further /// information about these objects. /// /// @EXAMPLES /// /// ```js /// app.delete('/goose/barn', function (req, res) { /// // Take this request and deal with it! /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// delete(route, callback) { return this.handleRequest('delete', route, callback); } del(route, callback) { deprecated('2.9', '"del" is deprecated, use "delete" instead'); return this.delete(route, callback); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_before /// /// `Controller.before(path, callback)` /// /// Defines an additional function on the route `path` which will be executed /// before the callback defined for a specific HTTP verb is executed. /// The `callback` function has the same signature as the `callback` in the /// specific route. /// You can also omit the `path`, in this case `callback` will be executed /// before handleing any request in this Controller. /// /// If `callback` returns the Boolean value `false`, the route handling /// will not proceed. You can use this to intercept invalid or unauthorized /// requests and prevent them from being passed to the matching routes. /// /// @EXAMPLES /// /// ```js /// app.before('/high/way', function(req, res) { /// //Do some crazy request logging /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// before(path, func) { if (is.notExisty(func)) { func = path; path = '/*'; } this.routingInfo.middleware.push({ priority: this.currentPriority = this.currentPriority + 1, url: {match: path}, action: { callback(req, res, opts, next) { var result = func(req, res, opts); if (result !== false) { next(); } } } }); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_after /// /// `Controller.after(path, callback)` /// /// Similar to `Controller.before(path, callback)` but `callback` will be invoked /// after the request is handled in the specific route. /// /// @EXAMPLES /// /// ```js /// app.after('/high/way', function(req, res) { /// //Do some crazy response logging /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// after(path, func) { if (is.notExisty(func)) { func = path; path = '/*'; } this.routingInfo.middleware.push({ priority: this.currentPriority = this.currentPriority + 1, url: {match: path}, action: { callback(req, res, opts, next) { next(); func(req, res, opts); } } }); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_around /// /// `Controller.around(path, callback)` /// /// Similar to `Controller.before(path, callback)` `callback` will be invoked /// instead of the specific handler. /// `callback` takes two additional paramaters `opts` and `next` where /// `opts` contains options assigned to the route and `next` is a function. /// Whenever you call `next` in `callback` the specific handler is invoked, /// if you do not call `next` the specific handler will not be invoked at all. /// So using around you can execute code before and after a specific handler /// and even call the handler only under certain circumstances. /// If you omit `path` `callback` will be called on every request. /// /// @EXAMPLES /// /// ```js /// app.around('/high/way', function(req, res, opts, next) { /// //Do some crazy request logging /// next(); /// //Do some more crazy request logging /// }); /// ``` /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// around(path, func) { if (is.notExisty(func)) { func = path; path = '/*'; } this.routingInfo.middleware.push({ priority: this.currentPriority = this.currentPriority + 1, url: {match: path}, action: { callback(req, res, opts, next) { func(req, res, opts, next); } } }); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_activateAuthentication /// /// `Controller.activateAuthentication(opts)` /// /// To activate authentication for this controller, call this function before defining any routes. /// In the `opts` object you can set the following keys: /// /// * *type*: Currently we only support *cookie*, but this will change in the future /// * *cookieLifetime*: An integer. Lifetime of cookies in seconds /// * *cookieName*: A string used as the name of the cookie /// * *sessionLifetime*: An integer. Lifetime of sessions in seconds /// /// @EXAMPLES /// /// ```js /// app.activateAuthentication({ /// type: "cookie", /// cookieLifetime: 360000, /// cookieName: "my_cookie", /// sessionLifetime: 400, /// }); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// activateAuthentication(opts) { var authentication = require('@arangodb/foxx/authentication'); _.extend(this, authControllerProps); this.auth = authentication.createAuthObject(this.applicationContext, opts); this.before('/*', authentication.createAuthenticationMiddleware(this.auth, this.applicationContext)); this.after('/*', authentication.createSessionUpdateMiddleware()); } //////////////////////////////////////////////////////////////////////////////// /// @brief activate sessions with the giveon options for this controller //////////////////////////////////////////////////////////////////////////////// activateSessions(opts) { var sessions = require('@arangodb/foxx/sessions'); _.extend(this, sessionControllerProps); this.sessions = new sessions.Sessions(opts); sessions.decorateController(this.sessions, this); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_apiDocumentation /// /// `Controller.apiDocumentation(path, [opts])` /// /// Mounts the API documentation (Swagger) at the given `path`. /// /// Note that the `path` can use path parameters as usual but must not use any /// wildcard (`*`) or optional (`:name?`) parameters. /// /// The optional **opts** can be an object with any of the following properties: /// /// * **before**: a function that will be executed before a request to /// this endpoint is processed further. /// * **appPath**: the mount point of the app for which documentation will be /// shown. Default: the mount point of the active app. /// * **indexFile**: file path or file name of the Swagger HTML file. /// Default: `"index.html"`. /// * **swaggerJson**: file path or file name of the Swagger API description JSON /// file or a function `swaggerJson(req, res, opts)` that sends a Swagger API /// description in JSON. Default: the built-in Swagger description generator. /// * **swaggerRoot**: absolute path that will be used as the path path for any /// relative paths of the documentation assets, **swaggerJson** file and /// the **indexFile**. Default: the built-in Swagger distribution. /// /// If **opts** is a function, it will be used as the value of **opts.before**. /// /// If **opts.before** returns `false`, the request will not be processed /// further. /// /// If **opts.before** returns an object, any properties will override the /// equivalent properties of **opts** for the current request. /// /// Of course all **before**, **after** or **around** functions defined on the /// controller will also be executed as usual. /// /// **Examples** /// /// ```js /// controller.apiDocumentation('/my/dox'); /// /// ``` /// /// A request to `/my/dox` will be redirect to `/my/dox/index.html`, /// which will show the API documentation of the active app. /// /// ```js /// controller.apiDocumentation('/my/dox', function (req, res) { /// if (!req.session.get('uid')) { /// res.status(403); /// res.json({error: 'only logged in users may see the API'}); /// return false; /// } /// return {appPath: req.parameters.mount}; /// }); /// ``` /// /// A request to `/my/dox/index.html?mount=/_admin/aardvark` will show the /// API documentation of the admin frontend (mounted at `/_admin/aardvark`). /// If the user is not logged in, the error message will be shown instead. /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// apiDocumentation(route, opts) { if (route.charAt(route.length - 1) !== '/') { route += '/'; } var mountPath = this.applicationContext.mount; return this.get(route + '*', swagger.createSwaggerRouteHandler(mountPath, opts)); } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_controller_extend /// /// `Controller.extend(extensions)` /// /// Extends all functions to define routes in this controller. /// This allows to combine several route extensions with the invocation /// of a single function. /// This is especially useful if you use the same input parameter in several routes of /// your controller and want to apply the same validation, documentation and error handling /// for it. /// /// The `extensions` parameter is a JSON object with arbitrary keys. /// Each key is used as the name of the function you want to define (you cannot overwrite /// internal functions like `pathParam`) and the value is a function that will be invoked. /// This function can get arbitrary many arguments and the `this` of the function is bound /// to the route definition object (e.g. you can use `this.pathParam()`). /// Your newly defined function is chainable similar to the internal functions. /// /// **Examples** /// /// Define a validator for a queryParameter, including documentation and errorResponses /// in a single command: /// /// ```js /// controller.extend({ /// myParam: function (maxValue) { /// this.queryParam("value", {type: joi.number().required()}); /// this.onlyIf(function(req) { /// var v = req.param("value"); /// if (v > maxValue) { /// throw new NumberTooLargeError(); /// } /// }); /// this.errorResponse(NumberTooLargeError, 400, "The given value is too large"); /// } /// }); /// /// controller.get("/goose/barn", function(req, res) { /// // Will only be invoked if the request has parameter value and it is less or equal 5. /// }).myParam(5); /// ``` /// /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// extend(extensions) { var attr; var extensionWrapper = function(scope, functionName) { return function() { this.applyChain.push({ functionName: functionName, argumentList: arguments }); return this; }.bind(scope); }; for (attr in extensions) { if (extensions.hasOwnProperty(attr)) { this.extensions[attr] = extensions[attr]; this.allRoutes[attr] = extensionWrapper(this.allRoutes, attr); } } } } exports.Controller = Controller; // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- /// Local Variables: /// mode: outline-minor /// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint" /// End: