./lib
60% [721/1210]
79% [214/272]
  1. var cache = {}, layoutsCache = {}, fs = require('fs'),
  2. path = require('path'),
  3. // import railway utils
  4. utils = require('./railway_utils'),
  5. safe_merge = utils.safe_merge,
  6. camelize = utils.camelize,
  7. classify = utils.classify,
  8. underscore = utils.underscore,
  9. singularize = utils.singularize,
  10. pluralize = utils.pluralize,
  11. $ = utils.stylize.$,
  12. log = utils.debug,
  13. runCode = utils.runCode;1
  14. var IS_NODE_04 = process.versions.node < '0.6';1
  15. var ctlParams = {};1
  16. var id = 0;1
  17. var CommonjsController = require('./commonjs_controller');1
  18. /**
  19. * Controller encapsulates http request handling layer. It allows to
  20. * render response, redirect, and tons of other related stuff.
  21. *
  22. * Instance of controller is actual response handler, it's not a
  23. * like in RoR, you can not inherit controllers, just load, mix.
  24. *
  25. * Inheritance in controllers is bad idea.
  26. *
  27. * @param {String} name - name of controller
  28. */
  29. function Controller(name, root) {
  30. var self = this;11
  31. this.id = ++id;11
  32. this._beforeFilters = [];11
  33. this._afterFilters = [];11
  34. this._actions = {};11
  35. this._layout = null;11
  36. this._buffer = {};11
  37. ctlParams[this.id] = {};11
  38. this.root = this.__dirname = root || app.root;11
  39. if (!layoutsCache[name]) {
  40. // TODO: what if view engine name differs from extension?
  41. layoutsCache[name] = railway.utils.existsSync(app.root + '/app/views/layouts/' + name + '_layout.' + app.settings['view engine']) ? name : 'application';6
  42. }
  43. ctlParams[this.id].baseLayout =
  44. ctlParams[this.id].layout = layoutsCache[name];11
  45. // allow to disable layout by default for all views
  46. // using app.settings['view options'].layout = false
  47. if ((app.set('view options') || {}).layout === false) {
  48. ctlParams[this.id].baseLayout = false;
  49. }
  50. this.controllerName = name;11
  51. this.controllerFile = Controller.index[this.root][name];11
  52. if (!this.controllerFile) {
  53. throw new Error('Controller ' + name + ' is not defined');
  54. }
  55. // import outer context
  56. var outerContext = Controller.context[this.root][name];11
  57. if (outerContext) {
  58. Object.keys(outerContext).forEach(function (key) {
  59. self[key] = outerContext[key].bind(self);11
  60. });11
  61. }
  62. // fix object inheritance broken when using as context
  63. Object.keys(Controller.prototype).forEach(function (method) {
  64. self[method] = Controller.prototype[method].bind(self);319
  65. });11
  66. this._filterParams = ['password'];11
  67. if (!IS_NODE_04) {
  68. this.__defineGetter__('response', function () { return this.ctx.res }.bind(this));11
  69. this.__defineGetter__('res', function () { return this.ctx.res }.bind(this));11
  70. this.__defineGetter__('request', function () { return this.ctx.req }.bind(this));11
  71. this.__defineGetter__('req', function () { return this.ctx.req }.bind(this));11
  72. this.__defineGetter__('session', function () { return this.ctx.req.session }.bind(this));11
  73. this.__defineGetter__('params', function () { return this.ctx.req.params }.bind(this));11
  74. this.__defineGetter__('body', function () { return this.ctx.req.body }.bind(this));11
  75. this.__defineGetter__('next', function () { return this.ctx.next }.bind(this));11
  76. this.__defineGetter__('actionName',function () { return this.ctx.action }.bind(this));11
  77. this.__defineGetter__('path_to', function () { return this.ctx.paths }.bind(this));11
  78. }
  79. this.t = T();11
  80. this.t.locale = app.settings.defaultLocale || 'en';11
  81. this.T = T;11
  82. if (global.__cov && !this.__cov) {
  83. this.__cov = global.__cov;11
  84. }
  85. }
  86. /**
  87. * Publish some object or function to use in another controller
  88. */
  89. Controller.prototype.publish = function (name, obj) {
  90. if (typeof name === 'function') {
  91. obj = name;5
  92. this._buffer[obj.name] = obj;5
  93. } else if (typeof name === 'string') {
  94. this._buffer[name] = obj;5
  95. }
  96. };1
  97. /**
  98. * Get some object or function published in another controller
  99. */
  100. Controller.prototype.use = function (name) {
  101. var what = this._buffer[name];2
  102. if (typeof what === 'undefined') throw new Error(name + ' is not defined');2
  103. return what;2
  104. };1
  105. /**
  106. * Configure which query params should be filtered from logging
  107. * @param {String} param 1 name
  108. * @param {String} param 2 name
  109. * @param ...
  110. * @param {String} param n name
  111. */
  112. Controller.prototype.filterParameterLogging = function (args) {
  113. this._filterParams = this._filterParams.concat(Array.prototype.slice.call(arguments));5
  114. };1
  115. /**
  116. * Initialize controller (called on first request)
  117. * @private
  118. */
  119. Controller.prototype._init = function initializeController() {
  120. // reset scope variables
  121. this._actions = {};11
  122. this._beforeFilters = [];11
  123. this._afterFilters = [];11
  124. this._buffer = {};11
  125. ctlParams[this.id].layout = ctlParams[this.id].baseLayout;11
  126. // publish models
  127. if (app.models) {
  128. Object.keys(app.models).forEach(function (className) {
  129. this[className] = app.models[className];
  130. }.bind(this));11
  131. }
  132. runCode(this.controllerFile, this);11
  133. };1
  134. /**
  135. * @param {String} name - name of filter to skip
  136. * @param {Array} or {String} only - choose actions to skip filter
  137. */
  138. Controller.prototype.skipBeforeFilter = function (name, only) {
  139. this._beforeFilters.forEach(function (filter, i) {
  140. if (filter[0] && filter[0].customName && name === filter[0].customName) {
  141. skipFilter(this._beforeFilters, i, only ? only.only : null);4
  142. }
  143. }.bind(this));4
  144. };1
  145. /**
  146. * @param {String} name - name of filter to skip
  147. * @param {Array} or {String} only - choose actions to skip filter
  148. */
  149. Controller.prototype.skipAfterFilter = function (name, only) {
  150. this._afterFilters.forEach(function (filter, i) {
  151. if (filter[0] && filter[0].customName && name === filter[0].customName) {
  152. skipFilter(this._afterFilters, i, only ? only.only : null);
  153. }
  154. }.bind(this));
  155. };1
  156. /**
  157. * @param {Array} filters collection
  158. * @param {Number} index of filter to skip
  159. * @param {Array} or {String} only - choose actions to skip filter
  160. * @private
  161. */
  162. function skipFilter(filters, index, only) {
  163. if (!only) {
  164. delete filters[index];2
  165. } else if (filters[index][0]) {
  166. if (!filters[index][1]) {
  167. filters[index][1] = {except: []};2
  168. }
  169. if (!filters[index][1].except) {
  170. filters[index][1].except = [];
  171. } else if (typeof filters[index][1].except === 'string') {
  172. filters[index][1].except = [filters[index][1].except];
  173. }
  174. if (typeof only === 'string') {
  175. filters[index][1].except.push(only);
  176. } else if (only && only.constructor.name === 'Array') {
  177. only.forEach(function (name) {
  178. filters[index][1].except.push(name);2
  179. });2
  180. }
  181. }
  182. }
  183. /**
  184. * Define controller action
  185. *
  186. * @param name String - optional (if missed, named function required as first param)
  187. * @param action Funcion - required, should be named function if first arg missed
  188. *
  189. * @example
  190. * ```
  191. * action(function index() {
  192. * Post.all(function (err, posts) {
  193. * render({posts: posts});
  194. * });
  195. * });
  196. * ```
  197. *
  198. */
  199. Controller.prototype.action = function (name, action) {
  200. if (typeof name === 'function') {
  201. action = name;14
  202. name = action.name;14
  203. if (!name) {
  204. throw new Error('Named function required when `name` param omitted');
  205. }
  206. }
  207. action.isAction = true;16
  208. action.customName = name;16
  209. this._actions[name] = action;16
  210. };1
  211. /**
  212. * Internal request handler. Serves request using railway env:
  213. *
  214. * - update context links: req, res, next and other req-sensitive stuff
  215. * - run before filters
  216. * - run action
  217. * - run after filters
  218. *
  219. * `perform` method called by `ControllerBridge`
  220. *
  221. * @param {String} actionName
  222. * @param {IncomingMessage} req - incoming http request
  223. * @param {ServerResponse} res - http server response
  224. * @private
  225. */
  226. Controller.prototype.perform = function (actionName, req, res, nextRoute) {
  227. res.info = {
  228. controller: this.controllerName,
  229. action: actionName,
  230. startTime: Date.now()
  231. };12
  232. res.actionHistory = [];12
  233. if (!this.initialized) {
  234. this.initialized = true;5
  235. if (IS_NODE_04) {
  236. this.actionName = actionName;
  237. this.request = this.req = req;
  238. this.request.sandbox = {};
  239. this.response = this.res = res;
  240. this.params = req.params;
  241. this.session = res.session;
  242. this.body = req.body;
  243. this.next = next;
  244. this.path_to = Controller.getPathTo(actionName);
  245. }
  246. this._init();5
  247. }
  248. var ctl = this, timeStart = false, prevMethod;12
  249. // need to track uniqueness of filters by name
  250. var queueIndex = {};12
  251. this.ctx = {
  252. req: req,
  253. res: res,
  254. next: next,
  255. action: actionName,
  256. paths: Controller.getPathTo(actionName)
  257. };12
  258. req.sandbox = {};12
  259. log('');12
  260. log($((new Date).toString()).yellow + ' ' + $(this.id).bold);12
  261. log($(req.method).bold, $(req.url).grey, 'controller:', $(this.controllerName).cyan, 'action:', $(this.actionName).blue);12
  262. if (req.query && Object.keys(req.query).length) {
  263. log($('Query: ').bold + JSON.stringify(req.query));
  264. }
  265. if (req.body && req.method !== 'GET') {
  266. var filteredBody = {};3
  267. Object.keys(req.body).forEach(function (param) {
  268. if (!ctl._filterParams.some(function (filter) {return param.search(filter) !== -1;})) {
  269. filteredBody[param] = req.body[param];1
  270. } else {
  271. filteredBody[param] = '[FILTERED]';1
  272. }
  273. });3
  274. log($('Body: ').bold + JSON.stringify(filteredBody));3
  275. }
  276. // build queue using before-, after- filters and action
  277. var queue = [];12
  278. enqueue(this._beforeFilters, queue);12
  279. queue.push(getCaller(this._actions[actionName]));12
  280. enqueue(this._afterFilters, queue);12
  281. if (app.disabled('model cache')) {
  282. // queue.push(getCaller(app.disconnectSchemas));
  283. }
  284. if (app.enabled('eval cache')) {
  285. queue.push(getCaller(function () {
  286. backToPool(ctl);
  287. }));
  288. }
  289. // start serving request
  290. next();12
  291. var logActions = app.enabled('log actions');12
  292. function next(err) {
  293. if (logActions && timeStart && prevMethod) {
  294. log('<<< ' + prevMethod.customName + ' [' + (Date.now() - timeStart) + ' ms]');17
  295. }
  296. if (err && err.constructor.name === 'Error') {
  297. return nextRoute(err);1
  298. }
  299. if (timeStart && prevMethod) {
  300. res.actionHistory.push({name: prevMethod.customName, time: Date.now() - timeStart});16
  301. }
  302. // run next method in queue (if any callable method)
  303. var method = queue.shift();29
  304. if (typeof method == 'function') {
  305. process.nextTick(function () {
  306. method.call(ctl.request.sandbox, next);27
  307. });27
  308. } else {
  309. res.info.appTime = Date.now() - res.info.startTime;2
  310. }
  311. }
  312. function getCaller(method) {
  313. if (!method) {
  314. throw new Error('Undefined action');
  315. }
  316. return function (next) {29
  317. req.inAction = method.isAction;27
  318. if (logActions && method.customName) {
  319. if (method.isAction) {
  320. log('>>> perform ' + $(method.customName).bold.cyan);10
  321. } else {
  322. log('>>> perform ' + $(method.customName).bold.grey);16
  323. }
  324. }
  325. timeStart = Date.now();27
  326. prevMethod = method;27
  327. method.call(this, next);27
  328. }
  329. }
  330. function enqueue(collection, queue) {
  331. collection.forEach(function (f) {
  332. var params = f[1];26
  333. if (!params) {
  334. enqueue();6
  335. } else if (params.only && params.only.indexOf(actionName) !== -1 && (!params.except || params.except.indexOf(actionName) === -1)) {
  336. enqueue();3
  337. } else if (params.except && params.except.indexOf(actionName) === -1) {
  338. enqueue();8
  339. }
  340. function enqueue() {
  341. if (f[2]) {
  342. if (queueIndex[f[2]]) return;16
  343. queueIndex[f[2]] = true;16
  344. }
  345. queue.push(getCaller(f[0]));17
  346. }
  347. });24
  348. }
  349. };1
  350. /**
  351. * Layout setter/getter
  352. *
  353. * - when called without arguments, used as getter,
  354. * - when called with string, used as setter
  355. *
  356. * When `layout` not called controller trying to get guess which layout to use.
  357. * First of all controller looking for layout with the same name as controller,
  358. * for example `users_controller` will choose `users_laout`, if there's no
  359. * layout with this name, controller using `application_layout`.
  360. *
  361. * If you do not want to use any layout by default, you can just set it up:
  362. *
  363. * app.set('view options', {layout: false});
  364. *
  365. * this will prevent you from repeating `layout(false)` in each controller where
  366. * you do not want to use layout, for example in api controllers.
  367. *
  368. * - choose
  369. *
  370. * @param {String} layout - [optional] layout name
  371. */
  372. Controller.prototype.layout = function layout(l) {
  373. if (typeof l !== 'undefined') {
  374. ctlParams[this.id].layout = l;3
  375. }
  376. return ctlParams[this.id].layout ? ctlParams[this.id].layout + '_layout' : null;5
  377. };1
  378. function filter(args) {
  379. if (typeof args[0] === 'string' && typeof args[1] === 'function') {
  380. // change order
  381. args[1].customName = args[0];
  382. return [args[1], args[2], args[0]];
  383. } else {
  384. // normal order
  385. args[0].customName = args[0].name;19
  386. return [args[0], args[1], args[0].name];19
  387. }
  388. }
  389. /**
  390. * Schedule before filter to the end of queue. This method can be called
  391. * with named function as single param, or with two params: name and
  392. * anonimous function
  393. *
  394. * Examples:
  395. * ```
  396. * before('some named filter', function () {});
  397. * before(function namedMethod() {});
  398. * ```
  399. *
  400. * This filters can be skipped using this names in future. Examples:
  401. * ```
  402. * skipBeforeFilter('some named filter');
  403. * skipBeforeFilter('namedMethod');
  404. * ```
  405. *
  406. * Please note, that every named filter only can be scheduled once:
  407. * ```
  408. * before(function myMethod() {
  409. * ~~~C15~~~
  410. * });
  411. * before(function myMethod() {
  412. * ~~~C16~~~
  413. * });
  414. * ```
  415. * This will only schedule first method!
  416. *
  417. * @alias beforeFilter
  418. * @param {Funcion} f
  419. * @param {Object} params
  420. */
  421. Controller.prototype.before = function before(f, params) {
  422. this._beforeFilters.push(filter(arguments));19
  423. };1
  424. Controller.prototype.beforeFilter = Controller.prototype.before;1
  425. /**
  426. * Schedule before filter to the start of queue. This method can be called
  427. * with named function as single param, or with two params: name and
  428. * anonimous function.
  429. *
  430. * @alias prependBeforeFilter
  431. * @param {Funcion} f
  432. * @param {Object} params
  433. */
  434. Controller.prototype.prependBefore = function prependBefore(f, params) {
  435. this._beforeFilters.unshift(filter(arguments));
  436. };1
  437. Controller.prototype.prependBeforeFilter = Controller.prototype.prependBefore;1
  438. /**
  439. * @override default controller string representation
  440. */
  441. Controller.prototype.toString = function toString() {
  442. return 'Controller ' + this.controllerName;
  443. };1
  444. /**
  445. * @param {String} name - name of action
  446. * @returns whether controller responds to action
  447. */
  448. Controller.prototype.respondTo = function respondTo(name) {
  449. return typeof this._actions[name] == 'function';
  450. };1
  451. /**
  452. * Append after filter to the end of queue. This method can be called
  453. * with named function as single param, or with two params: name and
  454. * anonimous function.
  455. *
  456. * @param {Function} f
  457. * @param {Object} params
  458. */
  459. Controller.prototype.after = function after(f, params) {
  460. this._afterFilters.push(filter(arguments));
  461. };1
  462. Controller.prototype.afterFilter = Controller.prototype.after;1
  463. /**
  464. * Prepend after filter to the start of queue. This method can be called
  465. * with named function as single param, or with two params: name and
  466. * anonimous function.
  467. *
  468. * @param {Function} f
  469. * @param {Object} params
  470. */
  471. Controller.prototype.prependAfter = function prependAfter(f, params) {
  472. this._afterFilters.unshift(filter(arguments));
  473. };1
  474. Controller.prototype.prependAfterFilter = Controller.prototype.prependAfter;1
  475. /**
  476. * Set current locale. This setting will affect all views locale-specific helpers.
  477. *
  478. * @param locale
  479. */
  480. Controller.prototype.setLocale = function (locale) {
  481. this.t.locale = T.localeSupported(locale) ? locale : app.settings.defaultLocale;
  482. };1
  483. /**
  484. * Get current locale
  485. *
  486. * @returns {String} locale name
  487. */
  488. Controller.prototype.getLocale = function () {
  489. return this.t.locale;
  490. };1
  491. /**
  492. * Load another controller code in this context
  493. *
  494. * @param {String} controller - name of controller (without _controller suffix)
  495. */
  496. Controller.prototype.load = function (controller, alias) {
  497. var root = Controller.aliases[alias] || this.root;4
  498. var ctl = Controller.index[root][controller];4
  499. if (!ctl) {
  500. throw new Error('Controller ' + controller + ' is not defined. Please note that namespaced controllers names should include namespace when loading');
  501. }
  502. runCode(ctl, this);4
  503. };1
  504. /**
  505. * Send response, as described in ExpressJS guide:
  506. *
  507. * This method is a high level response utility allowing you
  508. * to pass objects to respond with json, strings for html,
  509. * Buffer instances, or numbers representing the status code.
  510. * The following are all valid uses:
  511. * ```
  512. * send(); ~~~C17~~~
  513. * send(new Buffer('wahoo'));
  514. * send({ some: 'json' });
  515. * send('&lt;p>some html&lt;/p>');
  516. * send('Sorry, cant find that', 404);
  517. * send('text', { 'Content-Type': 'text/plain' }, 201);
  518. * send(404);
  519. * ```
  520. *
  521. */
  522. Controller.prototype.send = function (x) {
  523. log('Send to client: ' + x);1
  524. this.response.send.apply(this.response, Array.prototype.slice.call(arguments));1
  525. if (this.request.inAction) this.next();1
  526. };1
  527. /**
  528. * Set or get response header
  529. *
  530. * @param {String} key - name of header
  531. * @param {String} val - value of header (optional)
  532. *
  533. * When second argument is omitted method acts as getter.
  534. *
  535. * Example:
  536. * ```
  537. * header('Content-Length');
  538. * ~~~C18~~~
  539. *
  540. * header('Content-Length', 123);
  541. * ~~~C19~~~
  542. *
  543. * header('Content-Length');
  544. * ~~~C20~~~
  545. * ```
  546. */
  547. Controller.prototype.header = function (key, val) {
  548. return this.response.header.call(this.response, key, val);
  549. };1
  550. /**
  551. * Redirect to `path`
  552. */
  553. Controller.prototype.redirect = function (path) {
  554. log('Redirected to', $(path).grey);
  555. this.response.redirect(path.toString());
  556. if (this.request.inAction) this.next();
  557. };1
  558. /**
  559. * Render response.
  560. *
  561. * @param {String} view name [optional]
  562. * @param {Object} locals - data passed to view as local variables [optional]
  563. *
  564. * When first parameter is omitted action name used as view name:
  565. * ```
  566. * action(function index() {
  567. * render(); ~~~C21~~~
  568. * });
  569. * ```
  570. *
  571. * Second argument is optional too, you can set local variables using `this` inside
  572. * action:
  573. * ```
  574. * action('new', function () {
  575. * this.title = 'Create new post';
  576. * this.post = new Post;
  577. * render();
  578. * });
  579. * ```
  580. * will result the same as
  581. * ```
  582. * action('new', function () {
  583. * render({
  584. * title: 'Create new post',
  585. * post: new Post
  586. * });
  587. * });
  588. * ```
  589. */
  590. Controller.prototype.render = function (arg1, arg2, cb) {
  591. var view, params;2
  592. if (typeof arg1 == 'string') {
  593. view = arg1;
  594. params = arg2;
  595. } else {
  596. view = this.actionName;2
  597. params = arg1;2
  598. }
  599. params = params || {};2
  600. params.controllerName = params.controllerName || this.controllerName;2
  601. params.actionName = params.actionName || this.actionName;2
  602. params.path_to = this.path_to;2
  603. params.request = this.request;2
  604. params.params = this.request.params;2
  605. params.t = this.t;2
  606. var layout = this.layout(),
  607. file = this.controllerName + '/' + view;2
  608. if (this.response.renderCalled) {
  609. log('Rendering', $(file).grey, 'using layout', $(layout).grey, 'called twice.', $('render() can be called only once!').red);
  610. return;
  611. }
  612. var helper;2
  613. try {
  614. helper = require(this.root + '/app/helpers/' + this.controllerName + '_helper');
  615. } catch(e) {
  616. helper = {};2
  617. }
  618. var appHelper;2
  619. try {
  620. appHelper = require(this.root + '/app/helpers/application_helper');
  621. } catch(e) {
  622. appHelper = {};2
  623. }
  624. log('Rendering', $(file).grey, 'using layout', $(layout).grey);2
  625. var helpers = railway.helpers.personalize(this);2
  626. app.set('views', this.root + '/app/views');2
  627. this.response.renderCalled = true;2
  628. this.response.render(file, {
  629. locals: safe_merge(params, this.request.sandbox, this.path_to, helpers, helpers.__proto__, helper, appHelper),
  630. layout: layout ? 'layouts/' + layout : false,
  631. debug: false
  632. }, cb);2
  633. if (this.request.inAction) this.next();2
  634. };1
  635. /**
  636. * Add flash error to display in next request
  637. *
  638. * @param {String} type
  639. * @param {String} message
  640. */
  641. Controller.prototype.flash = function flash(type, message) {
  642. this.request.flash.apply(this.request, Array.prototype.slice.call(arguments));
  643. };1
  644. /**
  645. * Respond to .:format
  646. * @param {Function} block
  647. *
  648. * Example (respond to json and html):
  649. *
  650. * action(function index() {
  651. * var fruits = this.fruits = ['apple', 'banana', 'kiwi'];
  652. * respondTo(function (format) {
  653. * format.html(render);
  654. * format.json(function () {
  655. * send(fruits);
  656. * });
  657. * });
  658. * });
  659. */
  660. Controller.prototype.respondTo = function respondTo(block) {
  661. var f = this.request.params.format || 'html';
  662. block({
  663. html: function (c) {
  664. if (f === 'html') {
  665. c();
  666. }
  667. },
  668. json: function (c) {
  669. if (f === 'json') {
  670. c();
  671. }
  672. }
  673. });
  674. };1
  675. var pool = {};1
  676. function backToPool(ctl) {
  677. pool[ctl.root][ctl.controllerName].push(ctl);
  678. }
  679. exports.load = function (name, root) {
  680. if (app.disabled('eval cache')) {
  681. return new Controller(name, root);11
  682. } else {
  683. if (!pool[root]) pool[root] = {};
  684. if (!pool[root][name]) pool[root][name] = [];
  685. var ctl = pool[root][name].shift();
  686. if (!ctl) {
  687. ctl = new Controller(name, root);
  688. }
  689. return ctl;
  690. }
  691. };1
  692. exports.loadNew = function (name, root) {
  693. if (app.disabled('eval cache')) {
  694. return CommonjsController.load(name, root);
  695. } else {
  696. if (!pool[root]) pool[root] = {};
  697. if (!pool[root][name]) pool[root][name] = [];
  698. var ctl = pool[root][name].shift();
  699. if (!ctl) {
  700. ctl = CommonjsController.load(name, root);
  701. }
  702. return ctl;
  703. }
  704. };1
  705. exports.loadAll = function (root) {
  706. Object.getOwnPropertyNames(Controller.index[root || app.root]).forEach(function(c) {
  707. exports.load(c, root)._init();6
  708. });1
  709. };1
  710. /**
  711. * @private
  712. */
  713. Controller.getPathTo = function () {
  714. return railway.routeMapper.pathTo;12
  715. };1
  716. /**
  717. * Add custom base controller dir to railway pool. It allows you to build
  718. * extensions with your own controllers, and build app
  719. * breaken by modules
  720. *
  721. * @param {String} basePath
  722. * @param {String} prefix
  723. * @param {Object} context - controller context tweaks, all members of
  724. * this object will be accesible in controller
  725. *
  726. * @public railway.controller.addBasePath
  727. */
  728. function addBasePath(basePath, prefix, context, root) {
  729. prefix = prefix || '';2
  730. if (!railway.utils.existsSync(basePath)) return;1
  731. root = root || app.root;1
  732. Controller.index[root] = Controller.index[root] || {};1
  733. Controller.context[root] = Controller.context[root] || {};1
  734. fs.readdirSync(basePath).forEach(addContoller);1
  735. function addContoller(file) {
  736. var stat = fs.statSync(path.join(basePath, file));6
  737. if (stat.isFile()) {
  738. var m = file.match(/(.*?)_?[cC]ontroller\.(js|coffee)$/);6
  739. if (m) {
  740. var ctl = prefix + m[1];6
  741. Controller.index[root][ctl] = Controller.index[root][ctl] || path.join(basePath, file);6
  742. Controller.context[root][ctl] = Controller.context[root][ctl] || context;6
  743. }
  744. } else if (stat.isDirectory()) {
  745. exports.addBasePath(path.join(basePath, file), prefix + file + '/', context, root);
  746. }
  747. }
  748. };1
  749. exports.addBasePath = addBasePath;1
  750. exports.Controller = Controller;1
  751. exports.init = function (root) {
  752. cache = {};1
  753. CommonjsController.index = Controller.index = Controller.index || {};1
  754. Controller.aliases = Controller.aliases || {};1
  755. Controller.context = Controller.context || {};1
  756. exports.addBasePath(root + '/app/controllers', null, null, root);1
  757. };1
  758. /**
  759. * Enables CSRF Protection
  760. *
  761. * This filter will check `authenticity_token` param of POST request
  762. * and compare with token calculated by session token and app-wide secret
  763. *
  764. * @param {String} secret
  765. * @param {String} paramName
  766. *
  767. * @example `app/controllers/application_controller.js`
  768. * ```
  769. * before('protect from forgery', function () {
  770. * protectFromForgery('415858f8c3f63ba98546437f03b5a9a4ddea301f');
  771. * });
  772. * ```
  773. */
  774. Controller.prototype.protectFromForgery = function protectFromForgery(secret, paramName) {
  775. var req = this.request;5
  776. if (!req.session) {
  777. return this.next();1
  778. }
  779. if (!req.session.csrfToken) {
  780. req.session.csrfToken = Math.random();1
  781. req.csrfParam = paramName || 'authenticity_token';1
  782. req.csrfToken = sign(req.session.csrfToken);1
  783. return this.next();1
  784. }
  785. // publish secure credentials
  786. req.csrfParam = paramName || 'authenticity_token';3
  787. req.csrfToken = sign(req.session.csrfToken);3
  788. if (req.originalMethod == 'POST') {
  789. var token = req.param('authenticity_token');2
  790. if (!token || token !== sign(req.session.csrfToken)) {
  791. railway.logger.write('Incorrect authenticity token');1
  792. this.send(403);1
  793. } else {
  794. this.next();1
  795. }
  796. } else {
  797. this.next();1
  798. }
  799. function sign(n) {
  800. return require('crypto').createHash('sha1').update(n.toString()).update(secret.toString()).digest('hex');5
  801. }
  802. };1
  803. Controller.prototype.protectedFromForgery = function () {
  804. return this.request.csrfToken && this.request.csrfParam;1
  805. };1
78% [207/265]
  1. /**
  2. * Module dependencies
  3. */
  4. var path = require('path');1
  5. var fs = require('fs');1
  6. var sys = require('util');1
  7. /**
  8. * Shortcut required railway utils
  9. */
  10. var pluralize = railway.utils.pluralize;1
  11. var camelize = railway.utils.camelize;1
  12. var $ = railway.utils.stylize.$;1
  13. var exec = require('child_process').exec
  14. /**
  15. * Command line options
  16. * populated by parseOptions method
  17. *
  18. * @api private
  19. */
  20. var options = {};1
  21. /**
  22. * Generators collection hash
  23. *
  24. * @type Hash
  25. * @api private
  26. */
  27. var collection = {};1
  28. /**
  29. * Add generator to collection
  30. *
  31. * @param name
  32. * @param callback
  33. * @param meta Object {description: String, examples: [String]}
  34. * @api public
  35. */
  36. function addGenerator(name, callback, meta) {
  37. meta = meta || {};5
  38. callback.meta = meta;5
  39. collection[name] = callback;5
  40. if (meta.alias) {
  41. collection[meta.alias] = callback;1
  42. }
  43. }
  44. exports.addGenerator = addGenerator;1
  45. /**
  46. * Check whether generator exists
  47. * @param name - name of generator
  48. * @api public
  49. */
  50. function exists(name) {
  51. return !!collection[name];
  52. }
  53. exports.exists = exists;1
  54. /**
  55. * Call generator method
  56. */
  57. function perform(name, args) {
  58. collection[name](args);5
  59. }
  60. exports.perform = perform;1
  61. exports.list = function () {
  62. return Object.keys(collection).join(' ');
  63. };1
  64. /**
  65. * Add built-in generators
  66. */
  67. (function () {
  68. addGenerator('init', initGenerator);1
  69. addGenerator('model', modelGenerator);1
  70. addGenerator('controller', controllerGenerator);1
  71. addGenerator('features', featuresGenerator);1
  72. addGenerator('crud', crudGenerator, {alias: 'scaffold'});1
  73. /**
  74. * Application initialization generator. Can be called with leading
  75. * appname param, or without it (init in current dir).
  76. *
  77. * When destination
  78. * is not empty old files wouldn't be overwritten by this generator, it's safe
  79. * to call generator again and again to restore some non-existing files
  80. * initial state
  81. *
  82. * @api public
  83. */
  84. function initGenerator(args) {
  85. parseOptions('appname');1
  86. [ 'app/',
  87. 'app/models/',
  88. 'app/controllers/',
  89. 'app/observers/',
  90. 'app/helpers/',
  91. 'app/views/',
  92. 'app/views/layouts/',
  93. 'db/',
  94. 'db/seeds/',
  95. 'db/seeds/development/',
  96. 'log/',
  97. 'public/',
  98. 'public/images',
  99. 'public/stylesheets/',
  100. 'public/javascripts/',
  101. 'node_modules/',
  102. 'config/',
  103. 'config/locales/',
  104. 'config/initializers/',
  105. 'config/environments/'
  106. ].forEach(createDir);1
  107. if (options.stylus) {
  108. createDir('app/assets/');1
  109. createDir('app/assets/styles/');1
  110. createFileByTemplate('app/assets/styles/example.styl', 'example.styl');1
  111. }
  112. var db = options.db;1
  113. createFileByTemplate('config/initializers/db-tools', 'db-tools');1
  114. createFileByTemplate('config/routes', 'config/routes');1
  115. // createFileByTemplate('config/requirements.json', 'requirements.json');
  116. var srv = createFileByTemplate('server', 'server');1
  117. var secret = require('crypto').createHash('sha1').update(Math.random().toString()).digest('hex');1
  118. if (options.coffee) {
  119. createFile('app/controllers/application_controller.coffee', 'before \'protect from forgery\', ->\n protectFromForgery \'' + secret + '\'\n\n');
  120. } else {
  121. createFile('app/controllers/application_controller.js', 'before(\'protect from forgery\', function () {\n protectFromForgery(\'' + secret + '\');\n});\n');1
  122. }
  123. createFileByTemplate('config/environment', 'config/environment', [ replaceViewEngine, replacePrependMiddleware ]);1
  124. createFileByTemplate('config/environments/test', 'config/environments/test');1
  125. createFileByTemplate('config/environments/development', 'config/environments/development');1
  126. createFileByTemplate('config/environments/production', 'config/environments/production');1
  127. createFileByTemplate('config/database.~', 'config/database_' + db + '.~', replaceAppname);1
  128. createFileByTemplate('db/schema', 'schema');1
  129. createViewByTemplate('app/views/layouts/application_layout', 'application_layout');1
  130. createFileByTemplate('public/index.html', 'index.html');1
  131. createFile('.gitignore', fs.readFileSync(path.join(__dirname, '/../templates/gitignore')));1
  132. // bootstrap files
  133. createFileByTemplate('public/stylesheets/bootstrap-responsive.css', 'bootstrap-responsive.css');1
  134. createFileByTemplate('public/stylesheets/bootstrap.css', 'bootstrap.css');1
  135. createFileByTemplate('public/images/glyphicons-halflings-white.png', 'glyphicons-halflings-white.png');1
  136. createFileByTemplate('public/images/glyphicons-halflings.png', 'glyphicons-halflings.png');1
  137. createFileByTemplate('public/javascripts/bootstrap.js', 'bootstrap.js');1
  138. createFileByTemplate('public/stylesheets/style.css', 'style.css');1
  139. createFileByTemplate('public/javascripts/rails.js', 'rails.js');1
  140. createFile('public/javascripts/application.js', '// place your application-wide javascripts here\n');1
  141. createFileByTemplate('npmfile', 'npmfile', replaceViewEngine);1
  142. createFileByTemplate('public/favicon.ico', 'favicon.ico');1
  143. var fileExtension = options.coffee ? '.coffee' : '.js';1
  144. var engine = options.coffee ? 'coffee' : 'node';1
  145. // this file is only needed for heroku deployments
  146. // maybe not produce it by default
  147. createFile('Procfile', 'web: ' + engine + ' server' + fileExtension);1
  148. createFileByTemplate('package.json', 'package.json', [replaceAppname, replaceViewEngine]);1
  149. fs.chmodSync(srv, 0755);1
  150. process.exit();1
  151. }
  152. /**
  153. * Generates model. Accepts list of params: first one is model name, then
  154. * fieldnames as name:type, by default type is string.
  155. *
  156. * This generator accepts --coffee modifier to generate code in coffee
  157. *
  158. * Example:
  159. *
  160. * railway generate User email birthdate:Date isAdmin:Boolean
  161. *
  162. * Affected files:
  163. *
  164. * - db/schema.js
  165. * - app/models/ModelName.js
  166. *
  167. * @api public
  168. */
  169. function modelGenerator(args) {
  170. parseOptions('model');2
  171. var model = options.model, code = '';2
  172. if (!model) {
  173. sys.puts($('Model name required').red.bold);
  174. return;
  175. }
  176. if (model.match(/\//)) {
  177. model = model.match(/\/([^\/]+)$/)[1];
  178. }
  179. if (model.match(/\./)) {
  180. model = model.match(/\.([^\.]+)$/)[1];
  181. }
  182. var fileExtension = options.coffee ? '.coffee' : '.js';2
  183. var Model = model[0].toUpperCase() + model.slice(1);2
  184. var attrs = [], result = [];2
  185. options.forEach(function (arg) {
  186. var property = arg.split(':')[0],
  187. plainType = arg.split(':')[1],
  188. type = formatType(plainType);4
  189. if (options.coffee) {
  190. attrs.push(' property \'' + property + '\', ' + type);
  191. } else {
  192. attrs.push(' property(\'' + property + '\', ' + type + ');');4
  193. }
  194. result.push({name: property, type: type, plainType: plainType});4
  195. });2
  196. createDir('app/');2
  197. createDir('app/models/');2
  198. createFile('app/models/' + model + fileExtension, '');2
  199. if (options.coffee) {
  200. code = Model + ' = describe \'' + Model + '\', () ->\n' +
  201. attrs.join('\n') + '\n';
  202. } else {
  203. code = 'var ' + Model + ' = describe(\'' + Model + '\', function () {\n' +
  204. attrs.join('\n') + '\n});';2
  205. }
  206. railway.utils.appendToFile('db/schema' + fileExtension, code);2
  207. return result;2
  208. }
  209. /**
  210. * Generates single controller and related views. Accepts arguments:
  211. *
  212. * - controller name
  213. * - action names
  214. * - `--coffee` modifier can be passed to generate code in coffee
  215. *
  216. * Affected files:
  217. *
  218. * - app/controllers/controller_name_controller.js
  219. * - app/views/controller_name/action1.ejs
  220. * - app/views/controller_name/action2.ejs
  221. *
  222. * @api public
  223. */
  224. function controllerGenerator(args) {
  225. parseOptions('controller');1
  226. var controller = options.controller;1
  227. if (!controller) {
  228. console.log('Usage example: railway g controller controllername actionName anotherActionName');
  229. console.log(' railway g controller controllername actionName anotherActionName --coffee');
  230. return;
  231. }
  232. var fileExtension;1
  233. var actions;1
  234. if (options.coffee) {
  235. fileExtension = '.coffee';
  236. actions = ['load \'application\''];
  237. options.forEach(function (action) {
  238. actions.push('action \'' + action + '\', () -> \n render\n title: "' + controller + '#' + action + '"');
  239. });
  240. } else {
  241. fileExtension = '.js';1
  242. actions = ['load(\'application\');'];1
  243. options.forEach(function (action) {
  244. actions.push('action(\'' + action + '\', function () {\n render({\n title: "' + controller + '#' + action + '"\n });\n});');6
  245. });1
  246. }
  247. var ns = controller.split('/');1
  248. ns.pop();1
  249. createDir('app/');1
  250. createDir('app/controllers/');1
  251. createParents(ns, 'app/controllers/');1
  252. // controller
  253. var filename = 'app/controllers/' + controller + '_controller' + fileExtension;1
  254. createFile(filename, actions.join('\n\n'));1
  255. createDir('app/helpers/');1
  256. createParents(ns, 'app/helpers/');1
  257. // helper
  258. filename = 'app/helpers/' + controller + '_helper.js';1
  259. createFile(filename, 'module.exports = {\n};');1
  260. // views
  261. createDir('app/views/');1
  262. createParents(ns, 'app/views/');1
  263. createDir('app/views/' + controller + '/');1
  264. options.forEach(function (action) {
  265. createView('app/views/' + controller + '/' + action, 'default_action_view', [controller, action]);6
  266. });1
  267. }
  268. /**
  269. * Cucumis features generator
  270. * @deprecated
  271. * @api public
  272. */
  273. function featuresGenerator() {
  274. createDir('features/');1
  275. createDir('features/step_definitions/');1
  276. createFileByTemplate('features/step_definitions/web_steps.js', 'features/step_definitions/web_steps.js');1
  277. createFileByTemplate('features/step_definitions/email_steps.js', 'features/step_definitions/email_steps.js');1
  278. createFileByTemplate('features/step_definitions/jquery.js', 'features/step_definitions/jquery.js');1
  279. try {
  280. require('cucumis');
  281. } catch(e) {
  282. sys.puts($('Cucumis is not installed').red + ' please run ' + $('npm install cucumis').yellow);1
  283. }
  284. }
  285. /**
  286. * Resource scaffolding generator. Accepts same arguments as model generator:
  287. * name of model (singilar) and list of model properties in
  288. * `property:type` format
  289. *
  290. * It creates ready-to-use model, controller, controller tests (nodeunit),
  291. * routing rule, views and layout
  292. *
  293. * @api public
  294. */
  295. function crudGenerator(args) {
  296. var model = args[0].split('.').pop();1
  297. var parents = args[0].split('.').slice(0, -1);1
  298. var models = pluralize(model).toLowerCase();1
  299. if (!model) {
  300. console.log('Usage example: railway g crud post title:string content:string published:boolean');
  301. console.log(' railway g crud post title:string content:string published:boolean --coffee');
  302. return;
  303. }
  304. var ns = models.split('/');1
  305. ns.pop();1
  306. createDir('app/');1
  307. createDir('app/controllers/');1
  308. createParents(ns, 'app/controllers/');1
  309. var result = modelGenerator.apply(this, Array.prototype.slice.call(arguments));1
  310. var fileExtension = options.coffee ? '.coffee' : '.js';1
  311. createFile('app/controllers/' + models + '_controller' + fileExtension, controllerCode(args[0], result));1
  312. createDir('app/helpers/');1
  313. createParents(ns, 'app/helpers/');1
  314. function replaceModel(code) {
  315. return code1
  316. .replace(/new_model/g, addParents('new_', parents, model))
  317. .replace(/edit_model/g, addParents('edit_', parents, model, true))
  318. .replace(/path_to\.models/g, addParents('path_to.', parents, models))
  319. .replace(/path_to\.model/g, addParents('path_to.', parents, model, true))
  320. .replace(/(path_to\..*?\()model/g, addParents('$1', parents.map(function (p) { return 'params.' + p + '_id';}), model, true, ', '))
  321. .replace(/models/g, models)
  322. .replace(/model/g, model.toLowerCase())
  323. .replace(/Model/g, camelize(model, true))
  324. .replace(/VALID_ATTRIBUTES/, result.map(function (attr) { return attr.name + ": ''" }).join(',\n '));
  325. }
  326. // helper
  327. createFile('app/helpers/' + models + '_helper.js', 'module.exports = {\n};');1
  328. // tests
  329. createDir('test/');1
  330. createFileByTemplate('test/test_helper.js', 'test_helper.js');1
  331. createDir('test/controllers');1
  332. createParents(ns, 'test/controllers/');1
  333. createFileByTemplate('test/controllers/' + models + '_controller_test.js', 'crud_controller_test.js', replaceModel);1
  334. // views
  335. // _form partial
  336. createDir('app/views/');1
  337. createParents(ns, 'app/views/');1
  338. createParents(ns, 'app/views/layouts/');1
  339. // layout
  340. createViewByTemplate('app/views/layouts/' + models + '_layout', 'scaffold_layout', replaceModel);1
  341. createDir('app/views/' + models + '/');1
  342. createView('app/views/' + models + '/_form', 'scaffold_form', result, replaceModel);1
  343. createView('app/views/' + models + '/show', 'scaffold_show', result, replaceModel);1
  344. ['new', 'edit', 'index'].forEach(function (template) {
  345. createViewByTemplate('app/views/' + models + '/' + template, 'scaffold_' + template, replaceModel);3
  346. });1
  347. // route
  348. var routesConfig = process.cwd() + '/config/routes.' + (options.coffee ? 'coffee' : 'js'),
  349. routes = fs.readFileSync(routesConfig, 'utf8')
  350. .toString()
  351. .replace(/\s+$/g, '')
  352. .split('\n'),
  353. firstLine = routes.shift(),
  354. firstLineRegexp = options.coffee ? /^exports\.routes = \(map\)\->/ : /^exports\.routes = function \(map\) \{/,
  355. mapRouteResources = options.coffee ? ' map.resources \'' + models + '\'' : ' map.resources(\'' + models + '\');'
  356. if (firstLine.match() && !routes.some(containRoute)) {
  357. routes.unshift(mapRouteResources);1
  358. routes.unshift(firstLine);1
  359. fs.writeFileSync(routesConfig, routes.join('\n'));1
  360. sys.puts($('patch').bold.blue + ' ' + routesConfig);1
  361. }
  362. function containRoute(line) {
  363. var m = line.match(/^\s*map\.resources(\(|\s)'([^']+)'/);6
  364. return m && m[1] == models;6
  365. }
  366. }
  367. /**
  368. * Get controller code
  369. * @private
  370. */
  371. function controllerCode(model, result) {
  372. var fileExtension = options.coffee ? '.coffee' : '.js';1
  373. var code;1
  374. var parents = model.split('.').slice(0, -1);1
  375. model = model.split('.').pop();1
  376. var models = pluralize(model).toLowerCase();1
  377. code = fs.readFileSync(__dirname + '/../templates/crud_controller' + fileExtension);1
  378. code = code
  379. .toString('utf8')
  380. .replace(/new_model/g, addParents('new_', parents, model))
  381. .replace(/edit_model/g, addParents('edit_', parents, model, true))
  382. .replace(/path_to\.models/g, addParents('path_to.', parents, models))
  383. .replace(/path_to\.model/g, addParents('path_to.', parents, model, true))
  384. .replace(/(path_to\..*?\()this\.model/g, addParents('$1', parents.map(function (p) { return 'params.' + p + '_id';}), 'this.' + model, true, ', '))
  385. .replace(/models/g, models)
  386. .replace(/model/g, model.toLowerCase())
  387. .replace(/Model/g, camelize(model, true))
  388. .replace(/FILTER_PROPERTIES/g, '[' + result.map(function (p) {
  389. return "'" + p.name + "'";2
  390. }).join(', ') + ']');1
  391. return code;1
  392. }
  393. function addParents(prefix, parents, base, skipParams, join) {
  394. var name;10
  395. if (!join) join = '_';10
  396. if (parents.length) {
  397. name = prefix + parents.join(join) + join + base;
  398. } else {
  399. name = prefix + base;10
  400. }
  401. if (skipParams) return name;4
  402. return name + '(' + parents.map(function (p) {4
  403. return 'params.' + p + '_id';
  404. }).join(', ') + ')';
  405. }
  406. /**
  407. * Parse command line options
  408. * @private
  409. */
  410. function parseOptions(defaultKeyName) {
  411. options = [];4
  412. options.tpl = app.settings['view engine'] || 'ejs';4
  413. options.coffee = app.enabled('coffee');4
  414. options.db = 'memory';4
  415. options.stylus = false;4
  416. var key = defaultKeyName || false;4
  417. args.forEach(function (arg) {
  418. if (arg.slice(0, 2) == '--') {
  419. key = arg.slice(2);1
  420. options[key] = true;1
  421. } else if (arg[0] == '-') {
  422. key = arg.slice(1);
  423. options[key] = true;
  424. } else if (key) {
  425. options[key] = arg;3
  426. key = false;3
  427. } else {
  428. options.push(arg);10
  429. }
  430. });4
  431. if (options.nocoffee) {
  432. options.coffee = false;
  433. }
  434. }
  435. /**
  436. * Create directory
  437. * @private
  438. */
  439. function createDir(dir) {
  440. var root = process.cwd();40
  441. if (options.appname && !createDir.rootCreated) {
  442. createDir.rootCreated = true;
  443. createDir('');
  444. }
  445. if (options.appname) {
  446. dir = path.join(options.appname, dir);
  447. }
  448. if (railway.utils.existsSync(path.join(root, dir))) {
  449. sys.puts($('exists').bold.grey + ' ' + dir);2
  450. } else {
  451. fs.mkdirSync(path.join(root, dir), 0755);38
  452. sys.puts($('create').bold.green + ' ' + dir);38
  453. }
  454. }
  455. /**
  456. * Append `contents` to a file
  457. *
  458. * @param {String} filename
  459. * @param {String} contents
  460. * @private
  461. */
  462. function appendToFile(filename, contents) {
  463. var root = process.cwd(),
  464. fd = fs.openSync(path.join(root, filename), 'a');
  465. fs.writeSync(fd, contents);
  466. fs.closeSync(fd);
  467. }
  468. railway.utils.appendToFile = appendToFile;1
  469. /**
  470. * Create file with given `contents`
  471. *
  472. * @param {String} filename
  473. * @param {String} contents
  474. * @private
  475. */
  476. function createFile(filename, contents) {
  477. var root = process.cwd();36
  478. if (options.appname) {
  479. filename = path.join(options.appname, filename);
  480. }
  481. var fullPath = root + '/' + filename;36
  482. if (railway.utils.existsSync(fullPath)) {
  483. sys.puts($('exists').bold.grey + ' ' + filename);2
  484. } else {
  485. fs.writeFileSync(fullPath, contents);34
  486. sys.puts($('create').bold.green + ' ' + filename);34
  487. }
  488. return fullPath;36
  489. }
  490. /**
  491. * Create file with name `filename` using contents of `template` file.
  492. * Run `prepare` function before returning result
  493. *
  494. * @param {String} filename
  495. * @param {String} template
  496. * @param {Function} prepare - called with (text: {String}),
  497. * should return {String}
  498. *
  499. * @private
  500. */
  501. function createFileByTemplate(filename, template, prepare) {
  502. var configFilenameRegexp = /\.~$/;26
  503. if (template.match(configFilenameRegexp)) {
  504. var fileExtension = options.coffee ? '.yml' : '.json';1
  505. template = template.replace(configFilenameRegexp, fileExtension);1
  506. filename = filename.replace(configFilenameRegexp, fileExtension);1
  507. }
  508. else if (!template.match(/\..+$/)) {
  509. var fileExtension = options.coffee ? '.coffee' : '.js';9
  510. template += fileExtension;9
  511. filename += fileExtension;9
  512. }
  513. var text = fs.readFileSync(path.join(__dirname, '/../templates/', template));26
  514. if (prepare) {
  515. text = text.toString('utf8');5
  516. if (typeof prepare === 'function') {
  517. prepare = [prepare];3
  518. }
  519. prepare.forEach(function (p) {
  520. text = p(text);7
  521. });5
  522. }
  523. return createFile(filename, text);26
  524. }
  525. /**
  526. * Create view file with name `filename` using contents of `template` file.
  527. * Run `prepare` function before returning result. It look for -tpl option
  528. * and result file with proper extension and contents
  529. *
  530. * @param {String} filename
  531. * @param {String} template
  532. * @param {Function} prepare - called with (text: {String}),
  533. * should return {String}
  534. *
  535. * @private
  536. */
  537. function createViewByTemplate(filename, template, prepare) {
  538. options.tpl = options.tpl || 'ejs';5
  539. var package = options.tpl + '-ext', tpl;5
  540. try {
  541. tpl = require(package);
  542. } catch(e) {
  543. sys.puts($('Templating engine ' + options.tpl + ' is not supported').red);5
  544. return;5
  545. }
  546. var text = fs.readFileSync(tpl.template(template));
  547. if (prepare) {
  548. text = prepare(text.toString('utf8'));
  549. }
  550. return createFile(filename + tpl.extension, text);
  551. }
  552. function createView(filename, template, data, fn) {
  553. options.tpl = options.tpl || 'ejs';8
  554. var package = options.tpl + '-ext';8
  555. try {
  556. var tpl = require(package);
  557. } catch(e) {
  558. sys.puts($('Templating engine ' + options.tpl + ' is not supported').red);8
  559. return;8
  560. }
  561. var text = tpl.templateText(template, data);
  562. if (typeof fn === 'function') {
  563. text = fn(text.toString());
  564. }
  565. return createFile(filename + tpl.extension, text);
  566. }
  567. function createParents(ns, d) {
  568. ns.forEach(function (dir) {
  569. d = path.join(d, dir);
  570. createDir(d);
  571. });8
  572. }
  573. function formatType(name) {
  574. name = (name || 'string').toLowerCase();4
  575. switch (name) {
  576. case 'text': return 'Text';
  577. case 'string': return 'String';
  578. case 'date': return 'Date';
  579. case 'bool':
  580. case 'boolean': return 'Boolean';
  581. case 'int':
  582. case 'real':
  583. case 'float':
  584. case 'decimal':
  585. case 'number': return 'Number';
  586. }
  587. return '"' + camelize(name, true) + '"';
  588. }
  589. function replaceAppname(template) {
  590. return template.replace(/APPNAME/g, options.appname || 'default');2
  591. }
  592. function replaceViewEngine(template) {
  593. return template.replace(/VIEWENGINE/g, options.tpl || 'ejs');3
  594. }
  595. function replacePrependMiddleware(template) {
  596. var mw = [];1
  597. if (options.stylus) {
  598. if (options.coffee) {
  599. mw.push("app.use require('stylus').middleware\n force: true, src: app.root + '/app/assets', dest: app.root + '/public', compress: true".replace(/, /g, ',\n '));
  600. } else {
  601. mw.push("app.use(require('stylus').middleware({\n force: true, src: app.root + '/app/assets', dest: app.root + '/public', compress: true\n }));".replace(/, /g, ',\n '));1
  602. }
  603. }
  604. return template.replace(/PREPEND_MIDDLEWARE/g, mw.join('\n '));1
  605. };1
  606. function loadConfig(filename) {
  607. var filenameExt = /\.([^\.]+)$/.exec(filename)[1];
  608. switch (filenameExt) {
  609. case '~':
  610. var config = null;
  611. ['.json','.yml'].forEach(function(testExt){
  612. try {
  613. var testFilename = filename.replace(/\.~$/, testExt)
  614. var stats = fs.lstatSync(testFilename);
  615. if (stats.isFile())
  616. config = loadConfig(testFilename);
  617. }
  618. catch(e) {}
  619. });
  620. if (config != null) return config;
  621. throw new Error( sys.format('Can not find configuration file by path template "%s"', filename) );
  622. break;
  623. case 'json':
  624. return JSON.parse(fs.readFileSync(filename, 'utf8'));
  625. case 'yml':
  626. return require(filename).shift();
  627. default:
  628. throw new Error( sys.format('Unknown configuration file name extension "%s"', filenameExt) );
  629. }
  630. };1
  631. })();1
46% [77/166]
  1. /**
  2. * Module dependencies
  3. */
  4. var path = require('path'),
  5. fs = require('fs'),
  6. crypto = require('crypto'),
  7. exists = fs.exists || path.exists;1
  8. /**
  9. * Import utilities
  10. */
  11. var htmlTagParams = require('./railway_utils').html_tag_params,
  12. safe_merge = require('./railway_utils').safe_merge,
  13. humanize = require('./railway_utils').humanize,
  14. undef;1
  15. /**
  16. * Config
  17. */
  18. var regexps = {
  19. 'cached': /^cache\//,
  20. 'isHttp': /^https?:\/\/|\/\//
  21. },
  22. exts = {
  23. 'css': '.css',
  24. 'js' : '.js'
  25. },
  26. paths = {
  27. 'css': '/stylesheets/',
  28. 'js' : '/javascripts/'
  29. },
  30. merged = {
  31. stylesheets: {ext: exts.css},
  32. javascripts: {ext: exts.js}
  33. };1
  34. /**
  35. * Publish HelperSet
  36. */
  37. module.exports = new HelperSet(null);1
  38. module.exports.HelperSet = HelperSet;1
  39. /**
  40. * Set of helper methods
  41. *
  42. * @namespace
  43. * @param {Object} ctl Controller object
  44. */
  45. function HelperSet(ctl) {
  46. var controller = ctl;3
  47. this.controller = ctl;3
  48. /**
  49. * CSRF Meta Tag generation
  50. *
  51. * @returns {String} Meta tags against CSRF-attacks
  52. */
  53. this.csrf_meta_tag = function () {
  54. return controller && controller.protectedFromForgery() ? [
  55. '<meta name="csrf-param" content="' + controller.request.csrfParam + '"/>',
  56. '<meta name="csrf-token" content="' + controller.request.csrfToken + '"/>'
  57. ].join('\n') : '';
  58. }
  59. }
  60. /**
  61. * Make helpers local to query
  62. *
  63. * @param {Object} controller Controller Object
  64. * @returns {Object} containing all helpers
  65. */
  66. module.exports.personalize = function (controller) {
  67. return new module.exports.HelperSet(controller);2
  68. };1
  69. /**
  70. * Return bunch of stylesheets link tags
  71. *
  72. * Example in ejs:
  73. *
  74. * <%- stylesheet_link_tag('bootstrap', 'style') %>
  75. *
  76. * This returns:
  77. *
  78. * <link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/bootstrap.css" />
  79. * <link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/style.css" />
  80. *
  81. * @param {String} stylesheet filename
  82. * @returns {String} HTML code to the stylesheets in the parameters
  83. */
  84. HelperSet.prototype.stylesheetLinkTag = function stylesheetLinkTag() {
  85. if (!paths.css || !paths.stylesheets) {
  86. paths.css = app.settings.cssDirectory || '/stylesheets/';1
  87. paths.stylesheets = paths.css;1
  88. }
  89. var args = Array.prototype.slice.call(arguments);4
  90. var options = {media: 'screen', rel: 'stylesheet', type: 'text/css'};4
  91. var links = [];4
  92. if (typeof args[args.length - 1] == 'object') {
  93. options = safe_merge(options, args.pop());
  94. }
  95. mergeFiles('stylesheets', args).forEach(function (file) {
  96. delete options.href;5
  97. // there should be an option to change the /stylesheets/ folder
  98. var href = checkFile('css', file);5
  99. links.push(genericTagSelfclosing('link', options, { href: href }));5
  100. });4
  101. return links.join('\n ');4
  102. };1
  103. HelperSet.prototype.stylesheet_link_tag = HelperSet.prototype.stylesheetLinkTag;1
  104. /**
  105. * Generates set of javascript includes composed from arguments
  106. *
  107. * Example in ejs:
  108. *
  109. * <%- javascript_include_tag('rails', 'application') %>
  110. *
  111. * This returns:
  112. *
  113. * <script type="text/javascript" src="/javascripts/rails.js"></script>
  114. * <script type="text/javascript" src="/javascripts/application.js"></script>
  115. *
  116. * @param {String} script filename
  117. * @returns {String} the generated &lt;script&gt; tags
  118. */
  119. HelperSet.prototype.javascriptIncludeTag = function javascriptIncludeTag() {
  120. if (!paths.js || !paths.javascripts) {
  121. paths.js = app.settings.jsDirectory || '/javascripts/'
  122. paths.javascripts = paths.js;1
  123. }
  124. var args = Array.prototype.slice.call(arguments);4
  125. var options = {type: 'text/javascript'};4
  126. if (typeof args[args.length - 1] == 'object') {
  127. options = safe_merge(options, args.pop());
  128. }
  129. var scripts = [];4
  130. mergeFiles('javascripts', args).forEach(function (file) {
  131. // there should be an option to change the /javascripts/ folder
  132. var href = checkFile('js', file);5
  133. delete options.src;5
  134. scripts.push(genericTag('script', '', options, {src: href}));5
  135. });4
  136. return scripts.join('\n ');4
  137. };1
  138. HelperSet.prototype.javascript_include_tag = HelperSet.prototype.javascriptIncludeTag;1
  139. /**
  140. * Merge files when caching enabled
  141. *
  142. * You can enable merging manually with the following configuration:
  143. *
  144. * app.set('merge javascripts')
  145. * app.set('merge stylesheets')
  146. *
  147. * @param {String} scope Scope which is merged, e.g. javascripts or stylesheets
  148. * @param {Array} files Array of files which should be merged
  149. * @see https:~~~C4~~~
  150. * @returns {String} Pathname to the merged file
  151. */
  152. function mergeFiles(scope, files) {
  153. // ensure that feature is enabled
  154. if (app.disabled('merge ' + scope)) {
  155. return files;8
  156. }
  157. var ext = merged[scope].ext,
  158. result = [],
  159. shasum = crypto.createHash('sha1'),
  160. minify = [],
  161. directory = merged[scope].directory = paths[scope].replace(/(\/+)/g, '');
  162. // only merge local files
  163. files.forEach(function (file) {
  164. if (!regexps.isHttp.test(file)) {
  165. shasum.update(file);
  166. minify.push(file);
  167. } else {
  168. result.push(file);
  169. }
  170. });
  171. // calculate name of new script based on names of merged files
  172. var digest = shasum.digest('hex');
  173. // check cache state (undefined = not cached, false = cache in progress, String = cached)
  174. var cached = merged[scope][digest];
  175. if (cached) {
  176. // push resulted filename to result
  177. result.push(cached);
  178. } else {
  179. // we have no cache at the moment, just return files
  180. result = result.concat(minify);
  181. // if caching process is not started yet
  182. if (cached !== false) {
  183. // mark caching process as started
  184. merged[scope][digest] = false;
  185. // write resulted script as merged `minify` files
  186. var stream = fs.createWriteStream(path.join(app.root, 'public', directory, 'cache_' + digest + ext));
  187. var counter = 0;
  188. var fileContents = {};
  189. minify.forEach(function (file) {
  190. var filename = path.join(app.root, 'public', directory, file + ext);
  191. exists(filename, function (exists) {
  192. if (exists) {
  193. counter += 1;
  194. fs.readFile(filename, 'utf8', function (err, data) {
  195. fileContents[file] = data;
  196. done();
  197. });
  198. }
  199. })
  200. });
  201. function done() {
  202. if (--counter === 0) {
  203. minify.forEach(function (file) {
  204. data = fileContents[file];
  205. stream.write('~~~C52~~~ \n');
  206. stream.write(data + '\n');
  207. });
  208. stream.end();
  209. }
  210. }
  211. // save name of resulted file to the merge scope registry
  212. stream.on('close', function () {
  213. merged[scope][digest] = ['cache', digest].join('_');
  214. })
  215. }
  216. }
  217. return result;
  218. }
  219. /**
  220. * Link helper
  221. *
  222. * Example in ejs:
  223. *
  224. * <%- link_to("Home", '/') %>
  225. *
  226. * This returns:
  227. *
  228. * <a href="/">Home</a>
  229. *
  230. * @param {String} text Text of the link
  231. * @param {String} url Url where the link points to
  232. * @param {Object} params Set of html params (class, style, etc..)
  233. * @returns {String} Generated html for link
  234. */
  235. HelperSet.prototype.linkTo = function linkTo(text, url, params) {
  236. ['remote', 'method', 'jsonp', 'confirm'].forEach(dataParam.bind(params));
  237. return genericTag('a', text, {href: url}, params);
  238. };1
  239. HelperSet.prototype.link_to = HelperSet.prototype.linkTo;1
  240. /**
  241. * Form tag helper
  242. *
  243. * @methodOf HelperSet.prototype
  244. * @param {Object} params
  245. * @param {Function} block
  246. */
  247. HelperSet.prototype.formTag = function (params, block) {
  248. var self = this;4
  249. var buf = arguments.callee.caller.buf;4
  250. // helper may be called with block only
  251. if (typeof params === 'function') {
  252. block = params;1
  253. params = {};1
  254. }
  255. if (typeof params === 'undefined') {
  256. params = {}
  257. }
  258. // default method is POST
  259. if (!params.method) {
  260. params.method = 'POST';2
  261. }
  262. // hook up alternative methods (PUT, DELETE)
  263. var _method;4
  264. var method = _method = params.method.toUpperCase();4
  265. if (method != 'GET' && method != 'POST') {
  266. _method = method;2
  267. params.method = 'POST';2
  268. }
  269. // hook up data-params
  270. ['remote', 'jsonp', 'confirm'].forEach(dataParam.bind(params));4
  271. // push output
  272. buf.push('<form' + htmlTagParams(params) + '>');4
  273. buf.push( this.csrf_tag() );4
  274. // alternative method?
  275. if (_method !== params.method) {
  276. buf.push(HelperSet.prototype.input_tag({type: "hidden", name: "_method", value: _method }));2
  277. }
  278. // function?
  279. if (typeof block === 'function') {
  280. block();2
  281. }
  282. buf.push('</form>');4
  283. };1
  284. HelperSet.prototype.form_tag = HelperSet.prototype.formTag;1
  285. /**
  286. * Prints error messages for the model instance
  287. *
  288. * @methodOf HelperSet.prototype
  289. * @param {ModelInstance} resource
  290. * @returns {String} Error messages from the model instance
  291. */
  292. HelperSet.prototype.errorMessagesFor = function errorMessagesFor(resource) {
  293. var out = '';
  294. var h = this;
  295. if (resource.errors) {
  296. out += genericTag('div', printErrors(), {class: 'alert alert-error'});
  297. }
  298. return out;
  299. function printErrors() {
  300. var out = '<p>';
  301. out += genericTag('strong', 'Validation failed. Fix following errors before you continue:');
  302. out += '</p>';
  303. for (var prop in resource.errors) {
  304. if (resource.errors.hasOwnProperty(prop)) {
  305. out += '<ul>';
  306. resource.errors[prop].forEach(function (msg) {
  307. out += genericTag('li', prop + ' ' + msg, {class: 'error-message'});
  308. });
  309. out += '</ul>';
  310. }
  311. }
  312. return out;
  313. }
  314. };1
  315. /**
  316. * Form fields for resource helper
  317. *
  318. * @methodOf HelperSet.prototype
  319. * @param {ModelInstance} resource
  320. * @param {Function} block
  321. * @namespace
  322. */
  323. HelperSet.prototype.fields_for = function (resource, block) {
  324. arguments.callee.buf = arguments.callee.caller.buf;
  325. resource = resource || {};
  326. var self = this;
  327. var resourceName = resource && resource.constructor && resource.constructor.modelName || false;
  328. var complexNames = (app.set('view options') || {}).complexNames;
  329. /**
  330. * Generates a name
  331. *
  332. * @requires resourceName
  333. * @param {String} name Name of the element
  334. * @returns {String} returns the generated name
  335. */
  336. function makeName(name) {
  337. return complexNames && resourceName ? (resourceName + '[' + name + ']') : name;
  338. }
  339. /**
  340. * Generates an id field
  341. *
  342. * @requires resourceName
  343. * @param {String} name Name of the element
  344. * @returns {String} returns the generated id name
  345. */
  346. function makeId(name) {
  347. return complexNames && resourceName ? (resourceName + '_' + name) : name;
  348. }
  349. block({
  350. /**
  351. * Input tag helper
  352. *
  353. * Example in ejs:
  354. *
  355. * <%- form.input("test") %>
  356. *
  357. * This returns:
  358. *
  359. * <input name="test"/>
  360. *
  361. * @param {String} name Name of the element
  362. * @param {Object} params Additional parameters
  363. */
  364. input: function (name, params) {
  365. params = params || {};
  366. if (params.value === undef) {
  367. params.value = resource.hasOwnProperty(name) ? resource[name] : '';
  368. }
  369. return HelperSet.prototype.input_tag({
  370. name: makeName(name),
  371. id: makeId(name)
  372. }, params);
  373. },
  374. checkbox: function (name, params) {
  375. params = params || {};
  376. if (params.value === undef) {
  377. params.value = resource[name] || 1;
  378. }
  379. if (params.checked === undef) {
  380. if(resource[name]) {
  381. params.checked = 'checked';
  382. }
  383. } else if (params.checked === false) {
  384. delete params.checked;
  385. }
  386. return HelperSet.prototype.input_tag({
  387. name: makeName(name),
  388. id: makeId(name),
  389. type: 'checkbox'
  390. }, params);
  391. },
  392. file: function (name, params) {
  393. return HelperSet.prototype.input_tag({
  394. name: makeName(name),
  395. id: makeId(name),
  396. type: 'file'
  397. }, params);
  398. },
  399. /*
  400. * Label helper
  401. *
  402. * Example in ejs:
  403. *
  404. * <%- form.label("test", false, {class: "control-label"}) %>
  405. *
  406. * This returns:
  407. *
  408. * <label for="test" class="control-label">Test</label>
  409. *
  410. * @param {String} name Name of the element
  411. * @param {String} caption Optional different caption of the elemt
  412. * @param {Object} params Additional parameters
  413. */
  414. label: function (name, caption, params) {
  415. return HelperSet.prototype.label_tag(
  416. caption || self.controller.t('models.' + resource.constructor.modelName + '.fields.' + name, humanize(name)),
  417. {for: makeId(name) },
  418. params);
  419. },
  420. submit: function (name, params) {
  421. return genericTag('button', name || 'Commit', {type: 'submit'}, params);
  422. },
  423. textarea: function (name, params) {
  424. return genericTag('textarea', sanitizeHTML(resource[name] || ''), {name: makeName(name), id: makeId(name)}, params);
  425. },
  426. /*
  427. * Provides a select tag
  428. *
  429. * In ejs:
  430. *
  431. * <%- form.select("state", states, {fieldname: 'name', fieldvalue: '_id'}) %>
  432. *
  433. * Possible params:
  434. * * blank: {STRING} Blank value to be added at the beginning of the list
  435. * * fieldname: {STRING} Sets the name of the field in "options" field where the displayed values can be found. Default: "value"
  436. * * fieldvalue: {STRING} Sets the name of the field in "options" field where the submitted values can be found. Default = fieldname
  437. * * multiple: Can be set to false if size >1 to only select one value.
  438. * * select: Select a value. If fieldname and fieldvalue are different, the value is compared with fieldvalue otherwise with fieldname.
  439. * * size: Sets the displayed size of the select field
  440. *
  441. * @author [Uli Wolf](https:~~~C22~~~
  442. * @param {String} name Name of the select tag
  443. * @param {Object} options Array of possible options
  444. * @param {Object} params Additional parameters
  445. */
  446. select: function (name, options, params) {
  447. var options = options || '';
  448. var params = params || {};
  449. // optional: Holds the displayed fieldname where the data can be found in 'options'
  450. var optionFieldname = params.fieldname || 'value';
  451. delete params.fieldname;
  452. // optional: Holds the submittable fieldvalue where the data can be found in 'options'
  453. var optionFieldvalue = params.fieldvalue || optionFieldname;
  454. delete params.fieldvalue;
  455. // optional: Holds the number of entries that can be seen at once
  456. // If size > 1, multiple values can be selected (can be switched off via multiple:false)
  457. // If size = 1, only one value is selectable (Drop-Down)
  458. if (params.size === undef) {
  459. params.size = 1;
  460. } else {
  461. if (params.size > 1) {
  462. if (params.multiple === undef || params.multiple === true) {
  463. params.multiple = 'multiple';
  464. } else {
  465. delete params.multiple;
  466. }
  467. }
  468. }
  469. // optional: Preselect an entry
  470. if (params.select === undef) {
  471. params.select = resource[name] || '';
  472. }
  473. var optionSelected = params.select;
  474. delete params.select;
  475. // Render the options
  476. var innerOptions = '';
  477. // optional: Add a blank field at the beginning
  478. if (params.blank !== undef) {
  479. innerOptions += HelperSet.prototype.option_tag(sanitizeHTML(params.blank), {value: ''})
  480. }
  481. for (var optionsNr in options) {
  482. var option = options[optionsNr];
  483. var optionParameters = new Array();
  484. // Is the value in a seperate field?
  485. if (option[optionFieldvalue] != option[optionFieldname]) {
  486. optionParameters.value = option[optionFieldvalue];
  487. }
  488. if(activeValue(option[optionFieldvalue], optionSelected)) {
  489. optionParameters.selected = 'selected';
  490. }
  491. // Generate the option Tags
  492. innerOptions += HelperSet.prototype.option_tag(sanitizeHTML(option[optionFieldname]), optionParameters)
  493. }
  494. // Render the select
  495. return HelperSet.prototype.select_tag(innerOptions, {name: makeName(name), id: makeId(name)}, params);
  496. }
  497. });
  498. };1
  499. /**
  500. * Form for resource helper
  501. *
  502. * @methodOf HelperSet.prototype
  503. * @param {ModelInstance} resource
  504. * @param {Object} params
  505. * @param {Function} block
  506. */
  507. HelperSet.prototype.formFor = function formFor(resource, params, block) {
  508. var self = this;1
  509. var buf = arguments.callee.buf = arguments.callee.caller.buf;1
  510. if (resource && resource.modelName) {
  511. if (typeof params !== 'object') {
  512. params = {};1
  513. }
  514. if (!params.method) {
  515. params.method = 'PUT';1
  516. }
  517. if (!params.action) {
  518. params.action = railway.routeMapper.pathTo[railway.utils.underscore(resource.modelName)](resource);1
  519. }
  520. }
  521. this.form_tag(params, function () {
  522. arguments.callee.buf = buf;1
  523. if (block) self.fields_for(resource, block);1
  524. });1
  525. };1
  526. HelperSet.prototype.form_for = HelperSet.prototype.formFor;1
  527. /**
  528. * Input tag helper
  529. *
  530. * @methodOf HelperSet.prototype
  531. * @param {String} text - inner html
  532. * @param {Object} params - set of tag attributes
  533. * @param {Object} override - set params to override params in previous arg
  534. * @returns {String} Finalized input tag
  535. */
  536. HelperSet.prototype.input_tag = function (params, override) {
  537. return '<input' + htmlTagParams(params, override) + ' />';2
  538. };1
  539. /**
  540. * Label tag helper
  541. *
  542. * Result:
  543. *
  544. * <label>text</label>
  545. *
  546. * @methodOf HelperSet.prototype
  547. * @param {String} text - inner html
  548. * @param {Object} params - set of tag attributes
  549. * @param {Object} override - set params to override params in previous arg
  550. * @returns {String} Finalized label tag
  551. */
  552. HelperSet.prototype.label_tag = function (text, params, override) {
  553. return genericTag('label', text, params, override);
  554. };1
  555. /**
  556. * Cross-site request forgery hidden inputs
  557. *
  558. * @methodOf HelperSet.prototype
  559. * @returns {String} CSRF-Tag with parameters
  560. */
  561. HelperSet.prototype.csrf_tag = function() {
  562. return '<input type="hidden" name="' + this.controller.request.csrfParam + '" value="' + this.controller.request.csrfToken + '" />';4
  563. }
  564. /**
  565. * Select tag helper
  566. *
  567. * Result:
  568. *
  569. * <select>innerOptions</select>
  570. *
  571. * @methodOf HelperSet.prototype
  572. * @author [Uli Wolf](https:~~~C34~~~
  573. * @param {String} innerOptions Inner html of the select tag
  574. * @param {Object} params Set of tag attributes
  575. * @param {Object} override Set params to override params in previous arg
  576. * @returns {String} Finalized select tag
  577. */
  578. HelperSet.prototype.select_tag = function (innerOptions, params, override) {
  579. return genericTag('select', innerOptions, params, override);
  580. };1
  581. /**
  582. * Option tag helper
  583. *
  584. * Result:
  585. *
  586. * <option>text</option>
  587. *
  588. * @methodOf HelperSet.prototype
  589. * @author [Uli Wolf](https:~~~C35~~~
  590. * @param {String} text Inner html
  591. * @param {Object} params Set of tag attributes
  592. * @param {Object} override Set params to override params in previous arg
  593. * @returns {String} Finalized option tag
  594. */
  595. HelperSet.prototype.option_tag = function (text, params, override) {
  596. return genericTag('option', text, params, override);
  597. };1
  598. /**
  599. * Private util methods
  600. */
  601. /**
  602. * Returns html code of one tag with contents
  603. *
  604. * @param {String} name name of tag
  605. * @param {String} inner inner html
  606. * @param {Object} params set of tag attributes
  607. * @param {Object} override set params to override params in previous arg
  608. * @returns {String} Finalized generic tag
  609. */
  610. function genericTag(name, inner, params, override) {
  611. return '<' + name + htmlTagParams(params, override) + '>' + inner + '</' + name + '>';5
  612. }
  613. /**
  614. * Returns html code of a selfclosing tag
  615. *
  616. * @param {String} name name of tag
  617. * @param {Object} params set of tag attributes
  618. * @param {Object} override set params to override params in previous arg
  619. * @returns {String} Finalized generic selfclosing tag
  620. */
  621. function genericTagSelfclosing(name, params, override) {
  622. return '<' + name + htmlTagParams(params, override) + ' />';5
  623. }
  624. /**
  625. * Prefixes key with 'data-'
  626. *
  627. * @param {String} key name of key
  628. */
  629. function dataParam(key) {
  630. if (this[key]) {
  631. this['data-' + key] = this[key];
  632. delete this[key];
  633. }
  634. }
  635. /**
  636. * Compares a string against a string or an array
  637. *
  638. * @author [Uli Wolf](https:~~~C36~~~
  639. * @param {String} value Content of the String to be compared
  640. * @param {String|Array} selectvalue String or Array of possiblities to be equal to the first string
  641. * @returns {Boolean} True if the string matches the other string or array. False when not.
  642. */
  643. function activeValue(value, selectvalue) {
  644. var returnBool = false;
  645. // If this is an Array (e.g. when multiple values should be selected), iterate
  646. if (Object.prototype.toString.call(selectvalue) === '[object Array]') {
  647. // This is an Array (e.g. when multiple values should be selected), iterate
  648. for (var selectvalueNr in selectvalue) {
  649. // Cast to String as these might be objects.
  650. if (String(value) == String(selectvalue[selectvalueNr])) {
  651. returnBool = true
  652. continue;
  653. }
  654. }
  655. } else {
  656. // This is just one entry
  657. // Cast to String as these might be objects.
  658. if (String(value) == String(selectvalue)) {
  659. returnBool = true
  660. }
  661. }
  662. return returnBool;
  663. }
  664. /**
  665. * Escape &, < and > symbols
  666. *
  667. * @param {String} html String with possible HTML-Elements
  668. * @returns {String} resulting string with escaped characters
  669. */
  670. function sanitizeHTML(html) {
  671. return html.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;');
  672. }
  673. HelperSet.prototype.sanitize = sanitizeHTML;1
  674. /**
  675. * Checks if the environment is production
  676. *
  677. * @returns {Boolean} True if production, otherwise false
  678. */
  679. function checkProd() {
  680. return app.settings.env === 'production';10
  681. }
  682. /**
  683. * Provides the link to a file. Checks if a file needs to be suffixed with a timestamp
  684. *
  685. * @param {String} type Type of the file, e.g. css or js
  686. * @param {String} file name (local file) or link (external) to the file
  687. * @returns {String} Final Link to the file
  688. */
  689. function checkFile(type, file) {
  690. var isExternalFile = regexps.isHttp.test(file),
  691. isCached = file.match(regexps.cached),
  692. href = !isExternalFile ? paths[type] + file + exts[type] : file,
  693. isProd = checkProd();10
  694. if (!isCached && !isProd && !isExternalFile && !app.disabled('assets timestamps')) {
  695. href += '?' + Date.now()
  696. }
  697. return href;10
  698. }
  1. module.exports = Controller;
  2. Controller.beforeFilters = [];1
  3. Controller.afterFilters = [];1
  4. Controller.actions = {};1
  5. Controller.layout = null;1
  6. Controller.buffer = {};1
  7. Controller.filterParams = [ 'password' ];1
  8. function Controller(req, res) {
  9. this.controllerName = this.constructor.controllerName;
  10. var root = this.constructor.root;
  11. this.root = root;
  12. this._layout = this.constructor.layout || 'application';
  13. try {
  14. this._helper = require(root + '/app/helpers/' + this.controllerName + '_helper');
  15. } catch(e) {
  16. this._helper = {};
  17. }
  18. try {
  19. this._appHelper = require(root + '/app/helpers/application_helper');
  20. } catch(e) {
  21. this._appHelper = {};
  22. }
  23. }
  24. /**
  25. * Configure which query params should be filtered from logging
  26. * @param {String} param 1 name
  27. * @param {String} param 2 name
  28. * @param ...
  29. * @param {String} param n name
  30. */
  31. Controller.filterParameterLogging = function (args) {
  32. this.filterParams = this.filterParams.concat(Array.prototype.slice.call(arguments));
  33. };1
  34. Controller.load = function (name, root) {
  35. root = root || app.root;
  36. var file = Controller.index[root][name];
  37. var Ctl = require(file);
  38. Ctl.root = root;
  39. Ctl.controllerName = name;
  40. Ctl.controllerFile = file;
  41. return Ctl;
  42. };1
  43. /**
  44. * Define controller action
  45. *
  46. * @param name String - optional (if missed, named function required as first param)
  47. * @param action Function - required, should be named function if first arg missed
  48. *
  49. * @example
  50. * ```
  51. * BlogController.action(function index() {
  52. * Post.all(function (err, posts) {
  53. * this.render({posts: posts});
  54. * }.bind(this));
  55. * });
  56. * ```
  57. *
  58. */
  59. Controller.action = function (name, action) {
  60. if (typeof name === 'function') {
  61. action = name;
  62. name = action.name;
  63. if (!name) {
  64. throw new Error('Named function required when `name` param omitted');
  65. }
  66. }
  67. action.isAction = true;
  68. action.customName = name;
  69. this.actions[name] = action;
  70. };1
  71. /**
  72. * Layout setter/getter
  73. *
  74. * - when called without arguments, used as getter,
  75. * - when called with string, used as setter
  76. *
  77. * When `layout` not called controller trying to get guess which layout to use.
  78. * First of all controller looking for layout with the same name as controller,
  79. * for example `users_controller` will choose `users_laout`, if there's no
  80. * layout with this name, controller using `application_layout`.
  81. *
  82. * If you do not want to use any layout by default, you can just set it up:
  83. *
  84. * app.set('view options', {layout: false});
  85. *
  86. * this will prevent you from repeating `layout(false)` in each controller where
  87. * you do not want to use layout, for example in api controllers.
  88. *
  89. * - choose
  90. *
  91. * @param {String} layout - [optional] layout name
  92. */
  93. Controller.prototype.layout = function layout(l) {
  94. if (typeof l !== 'undefined') {
  95. this._layout = l;
  96. }
  97. return this._layout ? this._layout + '_layout' : null;
  98. };1
  99. /**
  100. * Render response.
  101. *
  102. * @param {String} view name [optional]
  103. * @param {Object} locals - data passed to view as local variables [optional]
  104. *
  105. * When first parameter is omitted action name used as view name:
  106. * ```
  107. * BlogController.action(function index() {
  108. * this.render(); ~~~C0~~~
  109. * });
  110. * ```
  111. *
  112. * Second argument is optional too, you can set local variables using `this` inside
  113. * action:
  114. * ```
  115. * BlogController.action('new', function () {
  116. * this.title = 'Create new post';
  117. * this.post = new Post;
  118. * this.render();
  119. * });
  120. * ```
  121. * will result the same as
  122. * ```
  123. * BlogController.action('new', function () {
  124. * this.render({
  125. * title: 'Create new post',
  126. * post: new Post
  127. * });
  128. * });
  129. * ```
  130. */
  131. Controller.prototype.render = function (arg1, arg2) {
  132. var view, params;
  133. var ctlName = this.controllerName;
  134. var root = this.root;
  135. if (typeof arg1 == 'string') {
  136. view = arg1;
  137. params = arg2;
  138. } else {
  139. view = this.actionName;
  140. params = arg1;
  141. }
  142. params = params || {};
  143. var layout = this.layout(),
  144. file = ctlName + '/' + view;
  145. if (this.res.renderCalled) {
  146. log('Rendering', $(file).grey, 'using layout', $(layout).grey, 'called more than once.', $('render() can be called only once!').red);
  147. return;
  148. }
  149. log('Rendering', $(file).grey, 'using layout', $(layout).grey);
  150. var helpers = railway.helpers.personalize(this);
  151. app.set('views', this.root + '/app/views');
  152. this.res.renderCalled = true;
  153. this.res.render(file, {
  154. locals: safe_merge(params, this, helpers, helpers.__proto__, this._helper, this._appHelper),
  155. layout: layout ? 'layouts/' + layout : false,
  156. debug: false
  157. });
  158. if (this.req.inAction) this.next();
  159. };1
  160. /**
  161. * Internal request handler. Serves request using railway env:
  162. *
  163. * - update context links: req, res, next and other req-sensitive stuff
  164. * - run before filters
  165. * - run action
  166. * - run after filters
  167. *
  168. * `perform` method called by `ControllerBridge`
  169. *
  170. * @param {String} actionName
  171. * @param {IncomingMessage} req - incoming http request
  172. * @param {ServerResponse} res - http server response
  173. * @private
  174. */
  175. Controller.prototype.perform = function perform(actionName, req, res, nextRoute) {
  176. var self = this;
  177. res.info = {
  178. controller: this.controllerName,
  179. action: actionName,
  180. startTime: Date.now()
  181. };
  182. this.request = this.req = req;
  183. this.response = this.res = res;
  184. this.nextRoute = this.next = nextRoute;
  185. res.actionHistory = [];
  186. if (!this.initialized) {
  187. this.initialized = true;
  188. this._init();
  189. }
  190. this.actionName = actionName;
  191. var ctl = this, timeStart = false, prevMethod;
  192. // need to track uniqueness of filters by name
  193. var queueIndex = {};
  194. log('');
  195. log($((new Date).toString()).yellow + ' ' + $(this.id).bold);
  196. log($(req.method).bold, $(req.url).grey, 'controller:', $(this.controllerName).cyan, 'action:', $(this.actionName).blue);
  197. if (req.query && Object.keys(req.query).length) {
  198. log($('Query: ').bold + JSON.stringify(req.query));
  199. }
  200. if (req.body && req.method !== 'GET') {
  201. var filteredBody = {};
  202. Object.keys(req.body).forEach(function (param) {
  203. if (!ctl.constructor.filterParams.some(function (filter) {return param.search(filter) !== -1;})) {
  204. filteredBody[param] = req.body[param];
  205. } else {
  206. filteredBody[param] = '[FILTERED]';
  207. }
  208. });
  209. log($('Body: ').bold + JSON.stringify(filteredBody));
  210. }
  211. // build queue using before-, after- filters and action
  212. var queue = [];
  213. enqueue(this.constructor.beforeFilters, queue);
  214. queue.push(getCaller(this.constructor.actions[actionName]));
  215. enqueue(this.constructor.afterFilters, queue);
  216. if (app.enabled('eval cache')) {
  217. queue.push(getCaller(function () {
  218. backToPool(ctl);
  219. }));
  220. }
  221. // start serving request
  222. next();
  223. var logActions = app.enabled('log actions');
  224. function next(err) {
  225. if (logActions && timeStart && prevMethod) {
  226. log('<<< ' + prevMethod.customName + ' [' + (Date.now() - timeStart) + ' ms]');
  227. }
  228. if (err && err.constructor.name === 'Error') {
  229. return self.nextRoute(err);
  230. }
  231. if (timeStart && prevMethod) {
  232. res.actionHistory.push({name: prevMethod.customName, time: Date.now() - timeStart});
  233. }
  234. // run next method in queue (if any callable method)
  235. var method = queue.shift();
  236. if (typeof method == 'function') {
  237. process.nextTick(function () {
  238. method.call(ctl, next);
  239. });
  240. } else {
  241. res.info.appTime = Date.now() - res.info.startTime;
  242. }
  243. }
  244. function getCaller(method) {
  245. if (!method) {
  246. throw new Error('Undefined action');
  247. }
  248. return function (next) {
  249. req.inAction = method.isAction;
  250. if (logActions && method.customName) {
  251. if (method.isAction) {
  252. log('>>> perform ' + $(method.customName).bold.cyan);
  253. } else {
  254. log('>>> perform ' + $(method.customName).bold.grey);
  255. }
  256. }
  257. timeStart = Date.now();
  258. prevMethod = method;
  259. method.call(this, next);
  260. }
  261. }
  262. function enqueue(collection, queue) {
  263. collection.forEach(function (f) {
  264. var params = f[1];
  265. if (!params) {
  266. enqueue();
  267. } else if (params.only && params.only.indexOf(actionName) !== -1 && (!params.except || params.except.indexOf(actionName) === -1)) {
  268. enqueue();
  269. } else if (params.except && params.except.indexOf(actionName) === -1) {
  270. enqueue();
  271. }
  272. function enqueue() {
  273. if (f[2]) {
  274. if (queueIndex[f[2]]) return;
  275. queueIndex[f[2]] = true;
  276. }
  277. queue.push(getCaller(f[0]));
  278. }
  279. });
  280. }
  281. };1
64% [61/96]
  1. var fs = require('fs');
  2. var path = require('path');1
  3. var singularize = require("../vendor/inflection").singularize;1
  4. var utils = require('./railway_utils');1
  5. var safe_merge = utils.safe_merge;1
  6. var Map = require('railway-routes').Map;1
  7. var existsSync = fs.existsSync || path.existsSync;1
  8. var exists = fs.exists || path.exists;1
  9. require('coffee-script');1
  10. /**
  11. * Global railway API singleton.
  12. * Available everywhere in project.
  13. *
  14. * @member railway.locales - localization module
  15. * @member railway.utils - railway utilities (stylize, runCode, etc..)
  16. * @see railway_utils.html#
  17. * @member railway.controller
  18. * @see controller.html#
  19. * @member railway.extensions
  20. * @see extensions.html#
  21. * @member railway.generators
  22. * @see generators.html#
  23. * @member railway.tools
  24. * @see tools.html#
  25. * @member railway.logger
  26. * @see logger.html#
  27. * @member railway.helpers
  28. * @see helpers.html#
  29. * @member railway.models
  30. * @see model.html#
  31. */
  32. function Railway() {
  33. if (global.hasOwnProperty('railway')) return railway;1
  34. global.railway = this;1
  35. this.locales = require('./locales');1
  36. this.utils = require('./railway_utils');1
  37. this.ControllerBridge = require('./controller_bridge');1
  38. this.controllerBridge = new this.ControllerBridge;1
  39. this.controller = require('./controller');1
  40. this.extensions = require('./extensions');1
  41. this.generators = require('./generators');1
  42. this.tools = require('./tools');1
  43. this.logger = require('./logger');1
  44. this.routeMapper = new Map(app, this.controllerBridge.uniCaller.bind(this.controllerBridge));1
  45. this.helpers = require('./helpers');1
  46. this.models = require('./models');1
  47. }
  48. try {
  49. if (process.versions.node < '0.6') {
  50. Railway.prototype.version = JSON.parse(fs.readFileSync(__dirname + '/../package.json')).version;
  51. } else {
  52. Railway.prototype.version = require('../package').version;1
  53. }
  54. } catch(e) {
  55. }
  56. /**
  57. * Initialize railway application:
  58. *
  59. * - load modules
  60. * - run configurators (config/environment, config/environments/{env})
  61. * - init controllers
  62. * - init extensions (including ORM and db/schema)
  63. * - init models
  64. * - run initializers `config/initializers/*`
  65. * - add routes
  66. * - start http server
  67. * - locales
  68. * - loggers
  69. * - observers
  70. * - assets
  71. *
  72. */
  73. exports.init = function initRailway(app) {
  74. var isMainModule = !global.hasOwnProperty('app');1
  75. // globalize app object
  76. if (isMainModule) {
  77. global.app = app;1
  78. app.root = process.cwd();1
  79. app.models = {};1
  80. }
  81. var root;1
  82. if (typeof app === 'string') {
  83. root = app;
  84. if (!isMainModule) {
  85. var cb = new railway.ControllerBridge(root);
  86. }
  87. } else {
  88. root = app.root;1
  89. }
  90. // create API publishing object
  91. new Railway();1
  92. // run environment.{js|coffee} and environments/{test|development|production}.{js|coffee}
  93. configureApp(root, isMainModule);1
  94. // controllers should be loaded before extensions
  95. railway.controller.init(root);1
  96. // extensions should be loaded before server startup
  97. railway.extensions.init(root);1
  98. // init models in app/models/*
  99. railway.models.init(root);1
  100. // run config/initializers/*
  101. runInitializers(root);1
  102. if (existsSync(root + '/config') && (existsSync(root + '/config/routes.js') || existsSync(root + '/config/routes.coffee'))) {
  103. railway.routeMapper.addRoutes(root + '/config/routes', isMainModule ? null : cb.uniCaller.bind(cb));
  104. }
  105. // everything else can be done after starting server
  106. process.nextTick(function () {
  107. railway.locales.init(root);1
  108. railway.logger.init(app.root);1
  109. app.reloadModels = railway.models.loadModels;1
  110. loadObservers();1
  111. if (global.app.enabled('merge javascripts')) {
  112. ensureDirClean(app.root + '/public' + app.set('jsDirectory'), 'cache');
  113. }
  114. if (global.app.enabled('merge stylesheets')) {
  115. ensureDirClean(app.root + '/public' + app.set('cssDirectory'), 'cache');
  116. }
  117. });1
  118. };1
  119. /**
  120. * Create http server object. Automatically hook up SSL keys stored in
  121. * app.root/config/tsl.{cert|key}
  122. *
  123. * @param {Object} options: {key: 'path/to/tsl.key', cert: 'path/to/tsl.cert'}
  124. */
  125. exports.createServer = function (options) {
  126. options = options || {};1
  127. var express = require('express');1
  128. var server;1
  129. if (express.createServer) {
  130. server = express.createServer;1
  131. } else {
  132. server = express;
  133. }
  134. var keys, app,
  135. key = options.key || process.cwd() + '/config/tsl.key',
  136. cert = options.cert || process.cwd() + '/config/tsl.cert';1
  137. if (existsSync(key) && existsSync(cert)) {
  138. keys = {
  139. key: fs.readFileSync(key).toString('utf8'),
  140. cert: fs.readFileSync(cert).toString('utf8')
  141. };
  142. }
  143. if (keys) {
  144. app = server(keys);
  145. } else {
  146. app = server();1
  147. }
  148. exports.init(app);1
  149. return app;1
  150. };1
  151. /**
  152. * Run app configutators in `config/environment` and `config/environments/{env}`.
  153. * Also try to monkey patch ejs and jade. **weird**
  154. */
  155. function configureApp(root, isMainModule) {
  156. var mainEnv = root + '/config/environment';1
  157. if (isMainModule) {
  158. app.set('views', root + '/app/views');1
  159. requireIfExists(mainEnv + '.js') || requireIfExists(mainEnv + '.coffee');1
  160. }
  161. var supportEnv = app.root + '/config/environments/' + app.settings.env;1
  162. requireIfExists(supportEnv + '.js') || requireIfExists(supportEnv + '.coffee');1
  163. if (!isMainModule) {
  164. return;
  165. }
  166. // TODO: remove monkey-patching from here
  167. if (app.settings['view engine'] == 'ejs' && (!app.extensions || !app.extensions['ejs-ext'])) {
  168. // monkey patch ejs
  169. try {
  170. var ejs = require('ejs'), old_parse = ejs.parse;
  171. ejs.parse = function () {
  172. var str = old_parse.apply(this, Array.prototype.slice.call(arguments));
  173. return str.replace('var buf = [];', 'var buf = []; arguments.callee.buf = buf;');
  174. };
  175. } catch(e) {}
  176. }
  177. if (app.settings['view engine'] == 'jade' && (!app.extensions || !app.extensions['jade-ext'])) {
  178. // monkey patch jade
  179. try {
  180. var jade = require('jade'), old_parse = jade.Compiler.prototype.compile;
  181. jade.Compiler.prototype.compile = function () {
  182. var str = old_parse.apply(this, Array.prototype.slice.call(arguments));
  183. // console.log(str);
  184. return 'arguments.callee.buf = buf;' + str;
  185. };
  186. } catch(e) {}
  187. }
  188. }
  189. /**
  190. * Require `module` if it exists
  191. *
  192. * @param {String} module - path to file
  193. */
  194. function requireIfExists(module) {
  195. if (railway.utils.existsSync(module)) {
  196. require(module);
  197. return true;
  198. } else {
  199. return false;4
  200. }
  201. }
  202. /**
  203. * Run initializers in sandbox mode
  204. */
  205. function runInitializers(root) {
  206. var context = {global: {}};1
  207. for (var i in app.models) {
  208. context[i] = app.models[i];
  209. }
  210. var initializersPath = root + '/config/initializers/';1
  211. if (existsSync(initializersPath)) {
  212. fs.readdirSync(initializersPath).forEach(function (file) {
  213. if (file.match(/^\./)) return;
  214. var script_name = initializersPath + file;
  215. utils.runCode(script_name, context);
  216. });
  217. for (var i in context.global) {
  218. global[i] = context.global[i];
  219. }
  220. }
  221. }
  222. /**
  223. * Observer is a kind of controller, that listen for some event in
  224. * the system, for example: paypal, twitter or facebook observers
  225. * listens for callback from foreign service. Email observer may
  226. * listen some events related to emails.
  227. *
  228. * If you need app.on('someEvent') you should place this code in
  229. * APPROOT/app/observers/NAME_observer.js
  230. */
  231. function loadObservers() {
  232. var dir = app.root + '/app/observers';1
  233. exists(dir, function (exists) {
  234. if (exists) {
  235. fs.readdir(dir, function (err, files) {
  236. if (!err && files) {
  237. files.forEach(function (file) {
  238. if (file.match(/^[^\.]/)) {
  239. require(path.join(dir, file));
  240. }
  241. });
  242. }
  243. });
  244. }
  245. });1
  246. }
  247. /**
  248. * Cleanup or create dir
  249. */
  250. function ensureDirClean(dir, prefix) {
  251. exists(dir, function (exists) {
  252. if (exists) {
  253. fs.readdir(dir, function (err, files) {
  254. files.filter(function (file) {
  255. return file.indexOf(prefix + '_') === 0;
  256. }).map(function (file) {
  257. return path.join(dir, file);
  258. }).forEach(fs.unlink);
  259. });
  260. } else {
  261. fs.mkdir(dir, 0755);
  262. }
  263. });
  264. }
  1. var undef, sys = require('util'),
  2. path = require('path'),
  3. fs = require('fs'),
  4. Module = require('module'),
  5. vm = require('vm'),
  6. yaml = require('yaml-js');1
  7. exports.html_tag_params = function (params, override) {
  8. var maybe_params = '';16
  9. safe_merge(params, override);16
  10. for (var key in params) {
  11. if (params[key] != undef) {
  12. maybe_params += ' ' + key + '="' + params[key].toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '"';41
  13. }
  14. }
  15. return maybe_params;16
  16. };1
  17. var safe_merge = exports.safe_merge = function (merge_what) {
  18. merge_what = merge_what || {};18
  19. Array.prototype.slice.call(arguments).forEach(function (merge_with, i) {
  20. if (i == 0) return;28
  21. for (var key in merge_with) {
  22. if (!merge_with.hasOwnProperty(key) || key in merge_what) continue;50
  23. merge_what[key] = merge_with[key];50
  24. }
  25. });18
  26. return merge_what;18
  27. };1
  28. exports.humanize = function (underscored) {
  29. var res = underscored.replace(/_/g, ' ');1
  30. return res[0].toUpperCase() + res.substr(1);1
  31. };1
  32. exports.camelize = function (underscored, upcaseFirstLetter) {
  33. var res = '';5
  34. underscored.split('_').forEach(function (part) {
  35. res += part[0].toUpperCase() + part.substr(1);8
  36. });5
  37. return upcaseFirstLetter ? res : res[0].toLowerCase() + res.substr(1);5
  38. };1
  39. exports.classify = function (str) {
  40. return exports.camelize(exports.singularize(str));1
  41. };1
  42. exports.underscore = function (camelCaseStr) {
  43. var initialUnderscore = camelCaseStr.match(/^_/) ? '_' : '';4
  44. var str = camelCaseStr
  45. .replace(/^_([A-Z])/g, '$1')
  46. .replace(/([A-Z])/g, '_$1')
  47. .replace(/^_/, initialUnderscore);4
  48. return str.toLowerCase(); 4
  49. };1
  50. exports.singularize = require('../vendor/inflection.js').singularize;1
  51. exports.pluralize = require('../vendor/inflection.js').pluralize;1
  52. // Stylize a string
  53. function stylize(str, style) {
  54. var styles = {
  55. 'bold' : [1, 22],
  56. 'italic' : [3, 23],
  57. 'underline' : [4, 24],
  58. 'cyan' : [96, 39],
  59. 'blue' : [34, 39],
  60. 'yellow' : [33, 39],
  61. 'green' : [32, 39],
  62. 'red' : [31, 39],
  63. 'grey' : [90, 39],
  64. 'green-hi' : [92, 32]
  65. };300
  66. var s = styles[style];300
  67. return '\033[' + s[0] + 'm' + str + '\033[' + s[1] + 'm';300
  68. };1
  69. var $ = function (str) {
  70. str = new(String)(str);497
  71. ['bold', 'grey', 'yellow', 'red', 'green', 'cyan', 'blue', 'italic', 'underline'].forEach(function (style) {
  72. Object.defineProperty(str, style, {
  73. get: function () {
  74. return $(stylize(this, style));300
  75. }
  76. });4473
  77. });497
  78. return str;497
  79. };1
  80. stylize.$ = $;1
  81. exports.stylize = stylize;1
  82. exports.debug = function () {
  83. railway.logger.write(Array.prototype.join.call(arguments, ' '));85
  84. };1
  85. var addCoverage = exports.addCoverage = function (code, filename) {
  86. if (!global.__cov) return code;15
  87. return require('semicov').addCoverage(code, filename);15
  88. };1
  89. // cache for source code
  90. var cache = {};1
  91. // cache for compiled scripts
  92. var scriptCache = {};1
  93. function runCode(filename, context) {
  94. var isCoffee = filename.match(/coffee$/);15
  95. context = context || {};15
  96. var dirname = path.dirname(filename);15
  97. // extend context
  98. context.require = context.require || function (apath) {
  99. var isRelative = apath.match(/^\.\.?\//);
  100. return require(isRelative ? path.resolve(dirname, apath) : apath);
  101. };15
  102. context.app = app;15
  103. context.railway = railway;15
  104. context.console = console;15
  105. context.setTimeout = setTimeout;15
  106. context.setInterval = setInterval;15
  107. context.clearTimeout = clearTimeout;15
  108. context.clearInterval = clearInterval;15
  109. context.__filename = filename;15
  110. context.__dirname = dirname;15
  111. context.process = process;15
  112. context.t = context.t || t;15
  113. context.Buffer = Buffer;15
  114. // omit file reading and caching part if we have compiled script
  115. if (!scriptCache[filename]) {
  116. cache[filename] = cache[filename] || filename && exports.existsSync(filename) && require('fs').readFileSync(filename);15
  117. if (!cache[filename]) {
  118. return;
  119. }
  120. var code = cache[filename].toString();15
  121. if (isCoffee) {
  122. try {
  123. var cs = require('coffee-script');
  124. } catch(e) {
  125. throw new Error('Please install coffee-script npm package: `npm install coffee-script`');
  126. }
  127. try {
  128. code = require('coffee-script').compile(code);
  129. } catch(e) {
  130. console.log('Error in coffee code compilation in file ' + filename);
  131. throw e;
  132. }
  133. } else {
  134. code = addCoverage(code, filename);15
  135. }
  136. }
  137. try {
  138. var m;15
  139. if (scriptCache[filename]) {
  140. m = scriptCache[filename];
  141. } else {
  142. m = vm.createScript(code.toString('utf8'), filename);15
  143. scriptCache[filename] = m;15
  144. }
  145. m.runInNewContext(context);15
  146. } catch(e) {
  147. console.log('Error while executing ' + filename);
  148. throw e;
  149. }
  150. // disable caching in development mode
  151. if (app.disabled('eval cache')) {
  152. cache[filename] = null;15
  153. scriptCache[filename] = null;15
  154. }
  155. }
  156. exports.runCode = runCode;1
  157. function addSpaces(str, len, to_start) {
  158. var str_len = str.length;2
  159. for (var i = str_len; i < len; i += 1) {
  160. if (!to_start) {
  161. str += ' ';3
  162. } else {
  163. str = ' ' + str;3
  164. }
  165. }
  166. return str;2
  167. }
  168. exports.addSpaces = addSpaces;1
  169. function readYaml(file) {
  170. try {
  171. return require(file).shift();
  172. } catch(e) {
  173. console.log('Error in reading', file);
  174. console.log(e.message);
  175. console.log(e.stack);
  176. }
  177. }
  178. exports.readYaml = readYaml;1
  179. exports.existsSync = fs.existsSync || path.existsSync;1
16% [14/86]
  1. var path = require('path');
  2. var fs = require('fs');1
  3. var readline = require('readline');1
  4. var childProcess = require('child_process');1
  5. var sys = require('util');1
  6. var util = require('util');1
  7. /**
  8. * Print routing map. Optionally accepts `filter` param, allowing to filter
  9. * output by method or helper name
  10. *
  11. * @param filter
  12. *
  13. * ```
  14. * $ railway routes
  15. * projects GET /projects projects#index
  16. * update ALL /:user/:repo/update doc#update
  17. * GET /:user/:repo doc#make
  18. * * GET /:user/:repo/* doc#make
  19. * ```
  20. * same but filtered to show GET routes only
  21. * ```
  22. * $ railway routes get
  23. * projects GET /projects projects#index
  24. * GET /:user/:repo doc#make
  25. * * GET /:user/:repo/* doc#make
  26. * ```
  27. * same but filtered to show routes with helper name contains `pro`
  28. * ```
  29. * $ railway routes get
  30. * projects GET /projects projects#index
  31. w ```
  32. */
  33. function routes() {
  34. var mapper = railway.routeMapper;
  35. var addSpaces = railway.utils.addSpaces;
  36. var dump = mapper.dump;
  37. var max_len = 0, helper_max_len = 0;
  38. var filter = (args.shift() || '').toUpperCase();
  39. var filtered = [];
  40. dump.forEach(function (data) {
  41. var method = data.method.toUpperCase();
  42. if (!filter || filter === method || data.helper.toUpperCase().search(filter) !== -1) {
  43. if (data.path.length > max_len) {
  44. max_len = data.path.length;
  45. }
  46. if (data.helper.length > helper_max_len) {
  47. helper_max_len = data.helper.length;
  48. }
  49. filtered.push(data);
  50. }
  51. });
  52. filtered.forEach(function (data) {
  53. var method = data.method.toUpperCase();
  54. console.log(
  55. addSpaces(data.helper, helper_max_len + 1, true) + ' ' +
  56. addSpaces(method, 7) +
  57. addSpaces(data.path, max_len + 1) +
  58. data.file + "#" + data.action
  59. );
  60. });
  61. return true;
  62. }
  63. exports.routes = routes;1
  64. routes.help = {
  65. shortcut: 'r',
  66. usage: 'routes [filter]',
  67. description: 'Display application routes'
  68. };1
  69. /**
  70. * Debug console.
  71. * node REPL console with railway bindings
  72. *
  73. * Predefined helpers:
  74. *
  75. * - `c` - callback, assigning it's arguments to _0 _1 .. _N variables
  76. * - `reload` - reload models
  77. * - `exit` - quit repl
  78. *
  79. * Usage Example:
  80. * ```
  81. * $ railway console
  82. * railway> User.all(c)
  83. * undefined
  84. * railway> [ [ 'hgetall', 'User:68' ],
  85. * [ 'hgetall', 'User:69' ],
  86. * [ 'hgetall', 'User:70' ],
  87. * [ 'hgetall', 'User:71' ],
  88. * [ 'hgetall', 'User:72' ],
  89. * [ 'hgetall', 'User:73' ],
  90. * [ 'hgetall', 'User:74' ],
  91. * [ 'hgetall', 'User:75' ] ] '[2ms]'
  92. * Callback called with 2 arguments:
  93. * _0 = null
  94. * _1 = [object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
  95. * railway> _1[0].toObject()
  96. * { id: '68',
  97. * githubId: null,
  98. * displayName: null,
  99. * username: null,
  100. * avatar: null }
  101. * ```
  102. *
  103. */
  104. function railwayConsole() {
  105. var ctx = require('repl').start('railway> ').context;
  106. ctx.reload = function () {
  107. global.models = {};
  108. ctx.app = app;
  109. app.reloadModels();
  110. for (var m in models) {
  111. ctx[m] = models[m];
  112. }
  113. };
  114. ctx.c = function () {
  115. var l = arguments.length,
  116. message = 'Callback called with ' + l +
  117. ' argument' + (l === 1 ? '' : 's') + (l > 0 ? ':\n' : '');
  118. for (var i = 0; i < 10; i++) {
  119. if (i < arguments.length) {
  120. ctx['_' + i] = arguments[i];
  121. message += '_' + i + ' = ' + arguments[i] + '\n';
  122. } else {
  123. if (ctx.hasOwnProperty('_' + i)) {
  124. delete ctx['_' + i];
  125. }
  126. }
  127. }
  128. console.log(message);
  129. };
  130. ctx.exit = function () {
  131. process.exit(0);
  132. };
  133. process.nextTick(ctx.reload);
  134. return false;
  135. }
  136. exports.console = railwayConsole;1
  137. railwayConsole.help = {
  138. shortcut: 'c',
  139. usage: 'console',
  140. description: 'Debug console'
  141. };1
  142. exports.dbconsole = function dbconsole() {
  143. var db = childProcess.spawn('mongo');
  144. var rli = readline.createInterface(process.stdin, process.stdout, autocomplete);
  145. var data = '';
  146. db.stdout.on('data', function (chunk) {
  147. data += chunk;
  148. write();
  149. });
  150. function write() {
  151. if (write.to) {
  152. clearTimeout(write.to);
  153. }
  154. setTimeout(function () {
  155. process.stdout.write(data);
  156. rli.prompt();
  157. data = '';
  158. }, 50);
  159. }
  160. rli.on('SIGINT', rli.close.bind(rli));
  161. rli.addListener('close', process.exit);
  162. rli.setPrompt('mongo > ');
  163. rli.addListener('line', function (line) {
  164. db.stdin.write(line + '\n');
  165. });
  166. // console.log(db);
  167. function autocomplete(input) {
  168. return [['help', 'test'], input];
  169. }
  170. }
  171. /**
  172. * Railway server. Command optionally accept PORT argument
  173. *
  174. * ```
  175. * railway server 8000 # run server on 8000 port
  176. * railway s 3000 # run server on 3000 port using shorter alias
  177. * PORT=80 railway s # run server on 80 port usin env var PORT
  178. * ```
  179. */
  180. function server() {
  181. var port = process.env.PORT || args.shift() || 3000;
  182. app.listen(port);
  183. console.log("Railway server listening on port %d within %s environment", port, app.settings.env);
  184. return false;
  185. }
  186. exports.server = server;1
  187. server.help = {
  188. shortcut: 's',
  189. usage: 'server [port]',
  190. description: 'Run railway server'
  191. };1
  192. /**
  193. * Install railway extension
  194. */
  195. function install() {
  196. var what = args.shift(), where = args.shift();
  197. if (!what || !what.match(/\.git$/)) {
  198. console.log('What do you want to install?');
  199. return true;
  200. }
  201. if (!where) {
  202. var m = what.match(/\/([^\/]*?)\.git/);
  203. where = m[1];
  204. }
  205. if (!railway.utils.existsSync(app.root + '/node_modules')) {
  206. fs.mkdirSync(app.root + '/node_modules');
  207. }
  208. var command = 'clone';
  209. if (railway.utils.existsSync(app.root + '/.git')) {
  210. command = 'submodule add';
  211. }
  212. console.log('Installing ' + where + ' extension from ' + what + ' repo to node_modules/' + where);
  213. var cp = require('child_process');
  214. cp.exec('git ' + command + ' ' + what + ' node_modules/' + where, function () {
  215. if (railway.utils.existsSync(app.root + '/npmfile.coffee')) {
  216. cp.exec('echo "require \'' + where + '\'" >> npmfile.coffee');
  217. console.log('Patched npmfile.coffee');
  218. } else {
  219. cp.exec('echo "require(\'' + where + '\');" >> npmfile.js');
  220. console.log('Patched npmfile.js');
  221. }
  222. var installScript = app.root + '/node_modules/' + where + '/install.js';
  223. if (railway.utils.existsSync(installScript)) {
  224. console.log('Running installation script');
  225. require(installScript);
  226. } else {
  227. process.exit(0);
  228. }
  229. });
  230. return false;
  231. }
  232. exports.install = install;1
  233. install.help = {
  234. shortcut: 'x',
  235. usage: 'install gitUrl [extName]',
  236. description: 'Install railway eXtension'
  237. };1
25% [12/48]
  1. var fs = require('fs'),
  2. yaml = require('yaml-js'),
  3. coffee = require('coffee-script'),
  4. path = require('path'),
  5. localeData = {};1
  6. /**
  7. * Initialize localization module
  8. */
  9. exports.init = function () {
  10. var dir = app.root + '/config/locales';1
  11. if (!railway.utils.existsSync(dir)) {
  12. return false;1
  13. }
  14. app.locales = app.locales || [];
  15. exports.load(dir);
  16. };1
  17. /**
  18. * Load localization files from `dir`. Locales can be in yaml, json or coffee
  19. * format
  20. *
  21. * Example locale.yml file:
  22. *
  23. * en:
  24. * key: 'Value'
  25. *
  26. * @param {String} dir - absolute path to locales directory
  27. */
  28. exports.load = function (dir) {
  29. fs.readdirSync(dir).forEach(function (file) {
  30. if (file.match(/^\./)) return;
  31. var filename = dir + '/' + file;
  32. var code = fs.readFileSync(filename, 'utf8').toString();
  33. var obj;
  34. try {
  35. if (file.match(/\.ya?ml$/)) {
  36. obj = require(filename).shift();
  37. } else if (file.match(/\.json/)) {
  38. obj = JSON.parse(code);
  39. } else if (file.match(/\.coffee/)) {
  40. obj = coffee.eval(code);
  41. } else {
  42. console.log('Unsupported extension of locale file ' + filename);
  43. }
  44. } catch(e) {
  45. console.log('Parsing file ' + filename);
  46. console.log(e);
  47. console.log(e.stack);
  48. }
  49. if (obj) {
  50. addTranslation(obj);
  51. }
  52. });
  53. };1
  54. /**
  55. * Add translation to `lang` to application locales collection
  56. */
  57. function addTranslation(lang) {
  58. Object.keys(lang).forEach(function (localeName) {
  59. var translations = lang[localeName];
  60. if (app.locales.indexOf(localeName) === -1) {
  61. app.locales.push([localeName, translations.lang && translations.lang.name || localeName]);
  62. }
  63. localeData[localeName] = localeData[localeName] || {};
  64. Object.keys(translations).forEach(function (namespace) {
  65. localeData[localeName][namespace] = translations[namespace];
  66. });
  67. });
  68. }
  69. /**
  70. * Global translation helper
  71. *
  72. * @param {Boolean} global
  73. * @public
  74. */
  75. function T(global) {
  76. if (global) {
  77. // helper for global scope (models, initializers, etc)
  78. // requires two params (locale expected)
  79. return function t(path, locale, defaultValue) {1
  80. if (!locale) {
  81. throw new Error('Locale expected');
  82. }
  83. return translate(path, locale);
  84. };
  85. } else {
  86. // helper for local scope (controllers, views, helpers)
  87. // requires one param
  88. return function t(path, defaultValue) {11
  89. return translate(path, t.locale, defaultValue);
  90. };
  91. }
  92. function translate(path, locale, defaultValue) {
  93. var translation = localeData[locale], substitute;
  94. function nextPathItem(token) {
  95. return (translation = translation[token]);
  96. }
  97. if (typeof path === 'string') {
  98. substitute = false;
  99. } else {
  100. substitute = path;
  101. path = substitute.shift();
  102. }
  103. if (!translation || !path.split('.').every(nextPathItem)) {
  104. translation = defaultValue || translationMissing(locale, path);
  105. }
  106. if (translation && substitute && substitute.length) {
  107. substitute.forEach(function (substitution) {
  108. translation = translation.replace(/%/, substitution.toString().replace(/%/g, ''));
  109. });
  110. }
  111. return translation;
  112. }
  113. function translationMissing(locale, path) {
  114. switch (app.settings.translationMissing) {
  115. case 'display':
  116. return 'translation missing for ' + locale + '.' + path;
  117. case 'default':
  118. case undefined:
  119. var defLocale = app.settings.defaultLocale;
  120. return !defLocale || locale === defLocale ? '' : translate(path, defLocale);
  121. }
  122. }
  123. };1
  124. T.localeSupported = function (localeName) {
  125. return !!localeData[localeName];
  126. };1
  127. global.t = T(true);1
  128. global.T = T;1
57% [16/28]
  1. var utils = require('./railway_utils'),
  2. path = require('path');1
  3. app.extensions = {};1
  4. /**
  5. * Initialize extensions (using npmfile)
  6. */
  7. exports.init = function (root) {
  8. app.extensions = {};1
  9. var ctx = getNPMFileContext(root);1
  10. var js = 'npmfile.js', coffee = 'npmfile.coffee',
  11. filename;1
  12. if (railway.utils.existsSync(path.join(root, js))) {
  13. filename = js;
  14. } else if (railway.utils.existsSync(path.join(root, coffee))) {
  15. filename = coffee;
  16. }
  17. if (filename) {
  18. utils.runCode(path.join(root, filename), ctx);
  19. }
  20. initBundledExtensions();1
  21. };1
  22. /**
  23. * Prepare context for executing npm file. Context has two additional features:
  24. * - group
  25. * - improved require (run init method of module)
  26. */
  27. function getNPMFileContext(root) {
  28. var ctx = {};1
  29. ctx.require = function (package) {
  30. var ext = app.extensions[package] = require(package);
  31. if (ext && ext.init) {
  32. ext.init(root);
  33. }
  34. };1
  35. ctx.group = function (env, callback) {
  36. if (env == app.settings.env) {
  37. callback();
  38. }
  39. };1
  40. return ctx;1
  41. };1
  42. function initBundledExtensions() {
  43. envInfo();1
  44. }
  45. /**
  46. * Setup route /railway/environment.json to return information about environment
  47. */
  48. function envInfo() {
  49. var jugglingdbVersion, npmVersion;1
  50. app.all('/railway/environment.json', function (req, res) {
  51. try {
  52. var jugglingdbVersion = require('jugglingdb').version;
  53. } catch(e) {}
  54. try {
  55. var npmVersion = require('npm').version;
  56. } catch(e) {}
  57. try {
  58. var viewEngineVersion = require(app.root + '/node_modules/' + app.set('view engine')).version;
  59. } catch(e) {
  60. viewEngineVersion = 'not installed';
  61. }
  62. if (app.disabled('env info')) return res.send({forbidden: true});
  63. res.send({
  64. settings: app.settings,
  65. versions: {
  66. core: process.versions,
  67. npm: npmVersion,
  68. railway: railway.version,
  69. jugglingdb: jugglingdbVersion,
  70. templating: {
  71. name: app.set('view engine'),
  72. version: viewEngineVersion
  73. }
  74. },
  75. application: {
  76. root: app.root,
  77. database: require(app.root + '/config/database')[app.set('env')].driver,
  78. middleware: app.stack.map(function (m) {
  79. return m.handle.name;
  80. })
  81. },
  82. env: process.env,
  83. });
  84. });1
  85. }
38% [10/26]
  1. // Deps
  2. var fs = require('fs'),
  3. path = require('path'),
  4. Module = require('module'),
  5. utils = require('./railway_utils');1
  6. /**
  7. * Initialize models
  8. */
  9. exports.init = function (root) {
  10. // code coverage support
  11. if (process.cov) context.__cov = __cov;1
  12. // quietly fallback to default schema
  13. try {
  14. if (!railway.orm) require('jugglingdb').init(root);1
  15. } catch(e) {}
  16. // then run models
  17. exports.loadModels(root + '/app/models/');1
  18. };1
  19. global.publish = function (name, model) {
  20. console.log('WARNING: `publish` call inside model files deprecated now, use module.exports = MyModel in case of declaring new model in app/models/*.js file, and not in db/schema.js');
  21. if (typeof name === 'function') {
  22. model = name;
  23. name = model.name;
  24. }
  25. app.models[name] = model;
  26. global[name] = model;
  27. };1
  28. exports.loadModels = function (modelsDir) {
  29. var ctx = {};1
  30. Object.keys(app.models).forEach(function (model) {
  31. ctx[model] = app.models[model];
  32. if (ctx[model]._validations) delete ctx[model]._validations;
  33. });1
  34. if (railway.utils.existsSync(modelsDir)) {
  35. fs.readdirSync(modelsDir).forEach(function (file) {
  36. if (file.match(/^[^\.].*?\.(js|coffee)$/)) {
  37. var filename = path.join(modelsDir, file);
  38. delete Module._cache[filename];
  39. var m = require(filename);
  40. if (m && (m.name || m.modelName)) {
  41. var name = m.modelName || m.name;
  42. app.models[name] = m;
  43. global[name] = m;
  44. }
  45. }
  46. });
  47. }
  48. };1
  49. app.disconnectSchemas = function disconnectSchemas() {
  50. if (_schemas.length) {
  51. _schemas.forEach(function (schema) {
  52. schema.disconnect();
  53. });
  54. _schemas = [];
  55. }
  56. }
  1. function ControllerBrigde(root) {
  2. this.root = root || app.root;1
  3. };1
  4. ControllerBrigde.config = {
  5. subdomain: {
  6. tld: 2
  7. }
  8. };1
  9. ControllerBrigde.prototype.uniCaller = function (ns, controller, action, params) {
  10. return function (req, res, next) {
  11. var subdomain = req.headers.host
  12. .split('.')
  13. .slice(0, -1 * ControllerBrigde.config.subdomain.tld)
  14. req.subdomain = subdomain.join('.');
  15. if (params && params.subdomain) {
  16. if (params.subdomain !== req.subdomain) {
  17. if (params.subdomain.match(/\*/)) {
  18. var matched = true;
  19. params.subdomain.split('.').forEach(function (part, i) {
  20. if (part === '*') return;
  21. if (part !== subdomain[i]) matched = false;
  22. });
  23. if (!matched) return next(); // next route
  24. } else return next();
  25. }
  26. }
  27. var ctl = this.loadController(ns + (controller || req.params.controller));
  28. if (app.disabled('model cache')) {
  29. // TODO: reloadModels should work without any params
  30. // it just should remember all paths
  31. // called previously with
  32. app.reloadModels(this.root + '/app/models/');
  33. }
  34. ctl.perform(action || req.params.action, req, res, next);
  35. }.bind(this);
  36. };1
  37. ControllerBrigde.prototype.loadController = function (controllerFullName) {
  38. if (app.enabled('commonjs ctl')) {
  39. return railway.controller.loadNew(controllerFullName, this.root);
  40. } else {
  41. return railway.controller.load(controllerFullName, this.root);5
  42. }
  43. };1
  44. module.exports = ControllerBrigde;1
100% [7/7]
  1. var fs = require('fs');
  2. var path = require('path');1
  3. exports.stream = null;1
  4. exports.init = function () {
  5. exports.stream = app.settings.quiet ?
  6. fs.createWriteStream(path.join(app.root, 'log', app.settings.env + '.log')) :
  7. process.stdout;1
  8. };1
  9. exports.write = function (str) {
  10. (exports.stream || process.stdout).write(str + '\n');86
  11. };1